Skip to content

Commit ce11388

Browse files
committed
feat(ui): add Gold and Stone themes, and shrink the theme tiles
Only one hue was genuinely free. Measuring the distance between every pair of theme primaries, the existing six hold a minimum of 52.7, and the two greens that were reported as looking alike measured 40.6, so that is roughly where "different theme" stops being true. Gold takes the warm gap at 49.4. It is nearer yellow than the metal usually is because a redder gold falls to 39.8 against Dirt once Material darkens it for light mode. Stone is distinct by having no hue at all, which is also what it is for: something quiet to work against. Lapis was the obvious third and is not here. Blue sits between Diamond's cyan and Obsidian's purple, and nothing in the seed or variant space gets it past 42 from both, which is nearer the two greens than the rest of the set. The distinctness test allowed 24, low enough that it would have passed the greens it was written after. It now allows 45, just under what the set holds, and a second test checks each theme's art is on disk and bundled, which otherwise fails only when someone picks that theme in a real build. The tiles are smaller and the grid denser, since eight of them at the old size wrapped awkwardly. The two new logos are recoloured from dirt.png by a script rather than drawn, because those blocks are the same speckle in another colour.
1 parent 9aa1050 commit ce11388

101 files changed

Lines changed: 33630 additions & 9 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/logo/LOGO_DESIGN.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,24 @@ The `variants` folder contains the Admincraft logo and its alternative versions.
3737
3838
- App icons for all platforms will be updated and applied the next time the app is built.
3939
40+
### Recoloured Variants
41+
42+
Several Minecraft blocks are the same speckled stone in a different colour, so
43+
a variant for one of those does not need drawing from scratch.
44+
[recolor_variant.py](recolor_variant.py) builds one by mapping the brightness of
45+
`dirt.png`'s texture onto a new pair of colours, leaving the prompt glyph alone.
46+
`gold.png` and `stone.png` are made this way; add a ramp to `RAMPS` and rerun it
47+
to add another:
48+
49+
```
50+
cd docs\logo
51+
pip install -r requirements.txt
52+
python recolor_variant.py
53+
```
54+
55+
Blocks whose pattern differs from dirt, and anything with a face on it, still
56+
need to be drawn by hand as above.
57+
4058
### Technical Details
4159
4260
- The Python scripts upscale the logo to ensure suitability for various sizes and formats across platforms, while maintaining the crispness of pixel art.

docs/logo/recolor_variant.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Builds a logo variant by recolouring the block texture of an existing one.
2+
3+
Several Minecraft blocks are the same speckled stone in a different colour, so
4+
a variant for one of those is a recolour rather than a new drawing. The prompt
5+
glyph is left alone: it is near-grey, and the texture is not, which is enough
6+
to tell them apart without hand-marking pixels.
7+
8+
cd docs/logo
9+
python recolor_variant.py
10+
11+
Writes into variants/. Existing files are overwritten, so it is safe to rerun
12+
after changing a ramp.
13+
"""
14+
15+
import os
16+
17+
from PIL import Image
18+
19+
SOURCE = os.path.join("variants", "dirt.png")
20+
21+
# Darkest and lightest tone of each block. The shades in between are taken from
22+
# the source's own brightness, which is what keeps the speckle pattern.
23+
RAMPS = {
24+
# Gold is a flatter block in game, so its ramp is deliberately short: the
25+
# same speckle at full strength reads as sand.
26+
"gold": ((0xC2, 0x9E, 0x1A), (0xFA, 0xF0, 0x62)),
27+
# Stone is speckled exactly like this, so it takes the full contrast.
28+
"stone": ((0x5E, 0x5E, 0x5E), (0xA8, 0xA8, 0xA8)),
29+
}
30+
31+
32+
def luminance(pixel):
33+
r, g, b = pixel[:3]
34+
return 0.299 * r + 0.587 * g + 0.114 * b
35+
36+
37+
def is_texture(pixel):
38+
"""True for block pixels, false for the glyph drawn on top of them.
39+
40+
The glyph is white or near-grey; every texture tone is saturated, so the
41+
spread between the channels separates the two cleanly.
42+
"""
43+
r, g, b = pixel[:3]
44+
return max(r, g, b) - min(r, g, b) > 25
45+
46+
47+
def recolor(image, dark, light):
48+
# Read through tobytes rather than getdata, which Pillow has deprecated in
49+
# favour of a method too new to rely on here.
50+
raw = image.tobytes()
51+
pixels = [tuple(raw[i:i + 4]) for i in range(0, len(raw), 4)]
52+
texture = [p for p in pixels if is_texture(p)]
53+
low = min(luminance(p) for p in texture)
54+
high = max(luminance(p) for p in texture)
55+
span = high - low or 1
56+
57+
out = []
58+
for pixel in pixels:
59+
if not is_texture(pixel):
60+
out.append(pixel)
61+
continue
62+
t = (luminance(pixel) - low) / span
63+
out.append(
64+
(
65+
round(dark[0] + (light[0] - dark[0]) * t),
66+
round(dark[1] + (light[1] - dark[1]) * t),
67+
round(dark[2] + (light[2] - dark[2]) * t),
68+
pixel[3],
69+
)
70+
)
71+
72+
return Image.frombytes(
73+
"RGBA", image.size, bytes(channel for pixel in out for channel in pixel)
74+
)
75+
76+
77+
def main():
78+
source = Image.open(SOURCE).convert("RGBA")
79+
for name, (dark, light) in RAMPS.items():
80+
path = os.path.join("variants", f"{name}.png")
81+
recolor(source, dark, light).save(path)
82+
print(f"wrote {path}")
83+
source.close()
84+
85+
86+
if __name__ == "__main__":
87+
main()

docs/logo/variants/gold.png

420 Bytes
Loading

docs/logo/variants/stone.png

403 Bytes
Loading

lib/models/app_theme.dart

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,28 @@ enum AppTheme {
4343
description: 'Soft and rosy',
4444
seedColor: Color(0xFFD2536B),
4545
logoAsset: 'docs/logo/variants/pig.png',
46+
),
47+
// The last warm hue with room left. It is nearer yellow than the metal
48+
// usually is, because a redder gold collapses into Dirt once Material
49+
// darkens it for light mode, and it takes the vibrant variant for the same
50+
// reason Creeper does.
51+
gold(
52+
label: 'Gold',
53+
description: 'Bright and precious',
54+
seedColor: Color(0xFFF9EC4E),
55+
logoAsset: 'docs/logo/variants/gold.png',
56+
variant: DynamicSchemeVariant.vibrant,
57+
),
58+
// Distinct by having no hue at all rather than by finding a free one, which
59+
// is also the point of it: something quiet to work against. Blue was the
60+
// obvious alternative and does not work, because Diamond and Obsidian sit
61+
// either side of it and leave no room.
62+
stone(
63+
label: 'Stone',
64+
description: 'Quiet and neutral',
65+
seedColor: Color(0xFF7A7A7A),
66+
logoAsset: 'docs/logo/variants/stone.png',
67+
variant: DynamicSchemeVariant.neutral,
4668
);
4769

4870
final String label;

lib/views/preferences_view.dart

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -86,11 +86,17 @@ class _PreferencesViewState extends State<PreferencesView> {
8686
const SizedBox(height: 8),
8787
LayoutBuilder(
8888
builder: (context, constraints) {
89-
final columns = constraints.maxWidth >= 720
90-
? 5
91-
: constraints.maxWidth >= 440
92-
? 3
93-
: 2;
89+
// Every breakpoint keeps a tile at roughly 100px or
90+
// more, which is what the longest label needs. The
91+
// icon stays 32px: it is 16x16 pixel art, so only
92+
// multiples of 16 scale without blurring it.
93+
final columns = constraints.maxWidth >= 880
94+
? 8
95+
: constraints.maxWidth >= 720
96+
? 6
97+
: constraints.maxWidth >= 440
98+
? 4
99+
: 3;
94100
const spacing = 8.0;
95101
final width =
96102
(constraints.maxWidth - spacing * (columns - 1)) /
@@ -289,23 +295,24 @@ class _ThemeChoice extends StatelessWidget {
289295
key: ValueKey('app-theme-${theme.name}'),
290296
onTap: onTap,
291297
child: Padding(
292-
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 10),
298+
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 8),
293299
child: Column(
294300
children: [
295301
Image.asset(
296302
theme.logoAsset,
303+
// A multiple of 16 keeps the pixel grid exact.
297304
width: 32,
298305
height: 32,
299306
fit: BoxFit.fill,
300307
filterQuality: FilterQuality.none,
301308
isAntiAlias: false,
302309
),
303-
const SizedBox(height: 6),
310+
const SizedBox(height: 4),
304311
Text(
305312
theme.label,
306313
maxLines: 1,
307314
overflow: TextOverflow.ellipsis,
308-
style: Theme.of(context).textTheme.labelLarge,
315+
style: Theme.of(context).textTheme.labelMedium,
309316
),
310317
],
311318
),

pubspec.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ flutter:
7575
- docs/logo/variants/diamond.png
7676
- docs/logo/variants/obsidian_glow.png
7777
- docs/logo/variants/pig.png
78+
# Recoloured from dirt.png by docs/logo/recolor_variant.py.
79+
- docs/logo/variants/gold.png
80+
- docs/logo/variants/stone.png
7881
# Item icons from mcicons (MIT), named after Minecraft identifiers.
7982
# See the credits in README.md.
8083
- assets/mcicons/

0 commit comments

Comments
 (0)