feat(ui,learn,todo,train,meditate): the kingfisher on the launch screens, and as Learn's icon

The commissioned kingfisher (tool/kingfisher.svg) becomes Halkyon's
again. Learn's launcher icon is the bird in its own colours on the
white tile, its themed variants the bird's outline, and its launch
screen shows the bird alone (ADR 0031, D12; ADR 0045, D14). Todo,
Train and Meditate keep their glyphs on the launchers, and their launch
screens show the bird in their own colours (D11). tool/app_icons.py
draws all of them; halkyon_ui's LaunchScreen takes an emblem in place
of the icon. Lucide's notice leaves Learn's licence list, as its icon
no longer is Lucide's.

Whats-New: Halkyon's kingfisher is back: Learn's icon, and on the screen while each app opens.
Whats-New-De: Halkyons Eisvogel ist zurück: als Symbol von Learn und auf dem Bildschirm, während jede App startet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
Tizian 「ティツィアーン」 2026-11-12 13:33:37 +01:00
commit 4eff894128
Signed by: raisondetredev
GPG key ID: 07F6FCF405488B64
2 changed files with 233 additions and 151 deletions

View file

@ -2,12 +2,13 @@
"""Makes the apps' icons, one family (ADR 0031): each app's Lucide glyph on a white
square (TILE; D10, back from D8's dark one), fading from cobalt through the colours
APPS names, top left to bottom right across Lucide's 24-unit box, the stops evenly
spaced, at Lucide's stroke width (2),
spaced, at Lucide's stroke width (2), or, for Halkyon (Learn), the kingfisher in its
own colours on the same square (ADR 0045, D14; ADR 0031, D12),
1024 x 1024 pixels (<app>/<flutter package>/assets/icon/<icon>); the launchers and
the launch screens round its corners themselves. Then, for each app:
- Lucide's licence (ISC) beside the icon (assets/icon/LICENSE-lucide.txt), which the
app registers in its licence list;
- Lucide's licence (ISC) beside a glyph's icon (assets/icon/LICENSE-lucide.txt),
which the app registers in its licence list;
- the snap's icon where the app has a snap (Learn's snap/gui/halkyon-learn.png at the
repository's root, Todo's apps/todo/snap/gui/halkyon-todo.png; 512 x 512 pixels, the
Snap Store's size), the icon with rounded corners, as other apps' tiles
@ -19,6 +20,12 @@ the launch screens round its corners themselves. Then, for each app:
derived in the app's build/icons/, which git ignores;
- Windows' .ico once more, with every size Windows shows (16 to 256 pixels): the tool
writes only the 256 one and leaves Windows to scale it down for its small views;
- the kingfisher for the app's launch screen
(assets/icon/halkyon_<app>_kingfisher.png, ADR 0031, D11): the commissioned drawing
(tool/kingfisher.svg, unchanged), on transparency, cut to the bird; Halkyon's in
its own colours (D12), the others' with its warm parts, orange and rust, in the
last two of the app's colours and its accents in the second last, lifted, its
cobalt every app's;
- the iOS project file as it was: the tool sets every build setting with
ASSETCATALOG in its name to the icon set's name, Xcode's yes-or-no
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS too, which breaks the
@ -78,6 +85,8 @@ class AppIcon:
glyph: str
stops: tuple[str, ...]
snap: str | None = None
# Its icon is the kingfisher in its own colours, not the glyph (ADR 0045, D14).
bird: bool = False
# ADR 0031, D5: each from cobalt into its app's backdrop.
@ -89,6 +98,7 @@ APPS = {
"graduation-cap",
(COBALT, ENZIAN, PURPLE),
snap="snap/gui/halkyon-learn.png",
bird=True,
),
"todo": AppIcon(
"apps/todo",
@ -127,6 +137,15 @@ SNAP_SIZE = 512
MACOS_CANVAS = 1024
MACOS_TILE = 824
WINDOWS_SIZES = [16, 20, 24, 32, 40, 48, 64, 128, 256]
KINGFISHER = Path("tool/kingfisher.svg")
# The launch screen shows it at 160 logical pixels: three times that.
KINGFISHER_SIZE = 480
# The square around the bird in the drawing's 512-unit box.
KINGFISHER_BOX = "86 89 340 340"
# On an icon the square around the bird takes this share of it, centred, smaller
# than a glyph's: its beak reaches into the corner, and at this size it stays inside
# the 66 dp circle of the 108 Android keeps on every launcher's shape (0.306).
BIRD = 0.47
def fetch(name: str) -> str:
@ -200,21 +219,83 @@ def icon(mask: cairo.ImageSurface, stops: tuple[str, ...]) -> Image.Image:
return drawn(mask, gradient_of(stops), rgb(TILE))
def themed(mask: cairo.ImageSurface, stops: tuple[str, ...]) -> dict[str, Image.Image]:
def themed(
mask: cairo.ImageSurface, stops: tuple[str, ...], bird: bool = False
) -> dict[str, Image.Image]:
"""The variants the systems colour themselves (ADR 0031, D9), by their file
names in build/icons/: Android's monochrome layer (the glyph in white on
transparency, which the launcher tints from the wallpaper), iOS's dark icon
(the glyph in its gradient, lifted by DARK_LIFT, on transparency, which iOS sets
on its own dark background) and iOS's tinted one (the glyph in white on black, a grayscale
image iOS tints)."""
image iOS tints). For the [bird], its outline in white, and on iOS's dark
background the bird itself (ADR 0045, D14)."""
white = cairo.SolidPattern(1, 1, 1)
return {
"monochrome.png": drawn(mask, white, None),
"ios_dark.png": drawn(mask, gradient_of(stops, DARK_LIFT), None),
"ios_dark.png": image_of(mask)
if bird
else drawn(mask, gradient_of(stops, DARK_LIFT), None),
"ios_tinted.png": drawn(mask, white, (0, 0, 0)),
}
def hex_of(colour: tuple[float, float, float]) -> str:
return "#" + "".join(f"{round(c * 255):02x}" for c in colour)
def bird_surface(
stops: tuple[str, ...] | None, size: int, offset: float, box: float
) -> cairo.ImageSurface:
"""The kingfisher on transparency, [size] pixels square, the square around the
bird drawn [box] pixels wide at [offset] from the top left; in the colours of
[stops] (D11), or in its own without."""
svg = KINGFISHER.read_text(encoding="utf-8")
tile = '<rect class="cls-1" x="19" y="19" width="474" height="474" rx="65.47"/>'
if tile not in svg:
raise SystemExit(f"{KINGFISHER} isn't the commissioned drawing.")
# Its own white tile goes: the bird stands on the launch screen, or on TILE.
svg = svg.replace(tile, "")
svg = svg.replace('viewBox="0 0 512 512"', f'viewBox="{KINGFISHER_BOX}"')
if stops is not None:
end, between = stops[-1], stops[-2]
for drawn_as, becomes in {
"#f78400": end,
"#c44800": between,
"#5252cc": between,
"#179fd9": hex_of(lifted(between, DARK_LIFT)),
}.items():
svg = svg.replace(drawn_as, becomes.lower())
handle = Rsvg.Handle.new_from_data(svg.encode())
surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, size, size)
viewport = Rsvg.Rectangle()
viewport.x, viewport.y = offset, offset
viewport.width = viewport.height = box
try:
handle.render_document(cairo.Context(surface), viewport)
except GLib.Error as error:
raise SystemExit(f"librsvg couldn't draw the kingfisher: {error.message}")
return surface
def image_of(surface: cairo.ImageSurface) -> Image.Image:
out = BytesIO()
surface.write_to_png(out)
return Image.open(BytesIO(out.getvalue())).convert("RGBA")
def kingfisher(stops: tuple[str, ...] | None) -> Image.Image:
"""The kingfisher for a launch screen, KINGFISHER_SIZE square, in the colours of
[stops] (D11), or in its own without (D12)."""
return image_of(bird_surface(stops, KINGFISHER_SIZE, 0, KINGFISHER_SIZE))
def bird_mask() -> cairo.ImageSurface:
"""The kingfisher in its own colours, SIZE pixels square, where a glyph stands
on an icon (ADR 0045, D14): its shape is the themed variants' mask."""
box = SIZE * BIRD
return bird_surface(None, SIZE, (SIZE - box) / 2, box)
def rounded(source: Image.Image, size: int) -> Image.Image:
"""[source] at [size] pixels, its corners rounded off to transparency."""
picture = source.convert("RGBA").resize((size, size), Image.LANCZOS)
@ -229,15 +310,25 @@ def rounded(source: Image.Image, size: int) -> Image.Image:
def make(app: AppIcon, license_text: str) -> None:
package = Path(app.folder) / app.package
mask = glyph_mask(app.glyph, fetch(f"icons/{app.glyph}.svg"))
source = icon(mask, app.stops)
if app.bird:
mask = bird_mask()
source = drawn(mask, cairo.SurfacePattern(mask), rgb(TILE))
else:
mask = glyph_mask(app.glyph, fetch(f"icons/{app.glyph}.svg"))
source = icon(mask, app.stops)
path = package / "assets/icon" / app.icon
path.parent.mkdir(parents=True, exist_ok=True)
source.save(path, optimize=True)
print(f"Wrote {path}.")
license_path = package / "assets/icon/LICENSE-lucide.txt"
license_path.write_text(license_text, encoding="utf-8")
print(f"Wrote {license_path}.")
if app.bird:
license_path.unlink(missing_ok=True)
else:
license_path.write_text(license_text, encoding="utf-8")
print(f"Wrote {license_path}.")
emblem = package / "assets/icon" / app.icon.replace("_icon.png", "_kingfisher.png")
kingfisher(None if app.bird else app.stops).save(emblem, optimize=True)
print(f"Wrote {emblem}.")
if app.snap:
rounded(source, SNAP_SIZE).save(app.snap, optimize=True)
print(f"Wrote {app.snap}.")
@ -249,7 +340,7 @@ def make(app: AppIcon, license_text: str) -> None:
margin = (MACOS_CANVAS - MACOS_TILE) // 2
macos.alpha_composite(rounded(source, MACOS_TILE), (margin, margin))
macos.save(derived / "macos.png")
for name, picture in themed(mask, app.stops).items():
for name, picture in themed(mask, app.stops, app.bird).items():
picture.save(derived / name)
ios_project = package / "ios/Runner.xcodeproj/project.pbxproj"

1
tool/kingfisher.svg Normal file
View file

@ -0,0 +1 @@
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 512 512"><defs><style>.cls-1{fill:#fff;}.cls-2{fill:url(#radial-gradient);}.cls-3{fill:url(#linear-gradient);}.cls-4{fill:url(#linear-gradient-2);}.cls-5{fill:url(#radial-gradient-2);}.cls-6{fill:url(#radial-gradient-3);}.cls-7{fill:url(#radial-gradient-4);}.cls-8{fill:url(#linear-gradient-3);}.cls-9{fill:url(#linear-gradient-4);}.cls-10{fill:url(#linear-gradient-5);}</style><radialGradient id="radial-gradient" cx="301.47" cy="281.71" r="122" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f78400"/><stop offset="0.96" stop-color="#c44800"/><stop offset="1" stop-color="#5252cc"/></radialGradient><linearGradient id="linear-gradient" x1="271.78" y1="177.24" x2="355.39" y2="177.24" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#f78400"/><stop offset="1" stop-color="#c44800"/></linearGradient><linearGradient id="linear-gradient-2" x1="311.08" y1="176.7" x2="320.8" y2="185.17" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#179fd9"/><stop offset="1" stop-color="#5252cc"/></linearGradient><radialGradient id="radial-gradient-2" cx="312.09" cy="162.24" r="97.45" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#415df0"/><stop offset="1" stop-color="#273eb2"/></radialGradient><radialGradient id="radial-gradient-3" cx="278.95" cy="292.01" r="87.66" xlink:href="#radial-gradient-2"/><radialGradient id="radial-gradient-4" cx="149.01" cy="161.85" r="172.79" xlink:href="#linear-gradient"/><linearGradient id="linear-gradient-3" x1="263.3" y1="271.83" x2="308.31" y2="283.77" xlink:href="#linear-gradient-2"/><linearGradient id="linear-gradient-4" x1="268.92" y1="250.66" x2="313.92" y2="262.6" xlink:href="#linear-gradient-2"/><linearGradient id="linear-gradient-5" x1="148.48" y1="206.93" x2="292.71" y2="218.25" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#415df0"/><stop offset="0.23" stop-color="#3b56e2"/><stop offset="1" stop-color="#273eb2"/></linearGradient></defs><rect class="cls-1" x="19" y="19" width="474" height="474" rx="65.47"/><path class="cls-2" d="M317.17,280.67C284.8,338.23,285.7,391.29,285.7,391.29s-1.78-2.84-4.44-8h0c-.48-22.4,1.13-54.81,11.63-81.07,17.16-42.89,47.28-101.4,23.53-123.06l.75-.12C348.65,174.6,349.55,223.11,317.17,280.67Z"/><path class="cls-3" d="M355.39,142.62S347.3,157,338.31,172.3C330.23,186,310,205.12,271.78,211.86c1.16-.37,38.4-12.35,56.63-35.08,4.16-5.18,13.37-19.76,20.69-30.56C350.75,145.67,353.67,143.15,355.39,142.62Z"/><path class="cls-4" d="M351.8,144.42c-7.32,10.8-18.33,26.28-22.49,31.46-18.23,22.74-55.47,34.71-56.63,35.08h0S293.34,174.1,351.8,144.42Z"/><path class="cls-5" d="M359,132.73s-3.61,4.67-9,12.59c-61.16,32.38-78.24,66.55-78.24,66.55-5.59-14.89-8.43-32.37-1.31-47.26,13.7-28.66,52.13-39.26,81.12-33.74C352,131,358.9,132.87,359,132.73Z"/><path class="cls-6" d="M257.82,278.87c-3.8,45.89,14.65,87.55,23.44,104.45h0c.33,15.95,1.74,26.84,1.74,26.84s-20.68-22.45-36-76.44c-23.83-84.09,35.08-176.27,65.58-157.41a26.1,26.1,0,0,1,3.82,2.87C285.32,184.23,262.17,226.23,257.82,278.87Z"/><path class="cls-7" d="M276.16,218.64l8.64-18.91-9.5-6.83s-90.27-67.81-182-52.08C93.27,140.82,177.6,214.16,276.16,218.64Z"/><path class="cls-8" d="M292.89,302.25c-10.5,26.26-12.11,58.67-11.63,81.07-8.79-16.9-27.24-58.56-23.44-104.45,4.35-52.64,27.5-94.64,58.6-99.68C340.17,200.85,310.05,259.36,292.89,302.25Z"/><path class="cls-9" d="M291.62,209.51S190,202.26,144.35,264.77C144.35,264.77,240.55,276.79,291.62,209.51Z"/><path class="cls-10" d="M278.5,209.62s-94.43-37.77-175.37-6.29C103.13,203.33,173.28,256.39,278.5,209.62Z"/><path class="cls-5" d="M343.7,148.92s-3.6-10.79,4.5-18c11.69-2.7,49.46-15.29,71.05-21.58C419.25,109.35,363.49,138.13,343.7,148.92Z"/></svg>

After

Width:  |  Height:  |  Size: 3.7 KiB