mirror of
https://github.com/kuhyx/testsAndMisc.git
synced 2026-11-11 00:27:01 +01:00
Every Flutter app still shipped the stock Flutter "beach" launcher icon, and none had an adaptive-icon descriptor at all. Rather than hand-drawing five one-off icons, define the family once and let each app contribute only an accent colour and a glyph. The family: a charcoal #1B1D21 field carrying one glyph in that app's own theme seed colour, at a single stroke weight. Glyphs are optically centred automatically - render.centre_offset rasterises a glyph alone, measures its ink box, and translates (never scales) the fragment, so a row of icons lines up while every icon keeps the exact family stroke weight. safe_box_overflow warns when a glyph exceeds the 560px adaptive-icon safe box; it caught three real overflows that a launcher mask would have clipped. The registry points dufs_client at ~/dufs-cloud/app: ~/dufs_client was a stale, remote-less checkout with an identical package name, since deleted. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
159 lines
4.5 KiB
Python
159 lines
4.5 KiB
Python
"""Build a contact sheet of the whole icon family for eyeballing.
|
|
|
|
Three rows are produced: full size, circle-masked (what a round launcher mask
|
|
does to the art), and a 48dp downscale (legibility at real home-screen size).
|
|
Reviewing all three together is the cheapest way to catch a glyph that only
|
|
works at poster size.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
import subprocess
|
|
import tempfile
|
|
|
|
from python_pkg.app_icons import apps, glyphs, render, style
|
|
|
|
# Size each icon is drawn at in the contact sheet.
|
|
_TILE = 512
|
|
# Roughly a 48dp launcher icon on a high-density screen, then blown back up
|
|
# with nearest-neighbour so the pixel grid stays visible.
|
|
_SMALL = 96
|
|
_SMALL_ZOOM = 200
|
|
|
|
|
|
def _row(tiles: list[Path], destination: Path) -> None:
|
|
"""Montage a list of PNGs into a single horizontal strip.
|
|
|
|
Parameters:
|
|
tiles (list[Path]): Images to place, left to right.
|
|
destination (Path): PNG path to write.
|
|
"""
|
|
subprocess.run(
|
|
[
|
|
render.resolve_tool("magick"),
|
|
"montage",
|
|
*[str(tile) for tile in tiles],
|
|
"-tile",
|
|
f"{len(tiles)}x1",
|
|
"-geometry",
|
|
"+14+14",
|
|
"-background",
|
|
"#3a3a3a",
|
|
str(destination),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def _circle_mask(source: Path, destination: Path) -> None:
|
|
"""Apply a round launcher mask to an icon.
|
|
|
|
Parameters:
|
|
source (Path): Full-size icon PNG.
|
|
destination (Path): PNG path to write.
|
|
"""
|
|
radius = _TILE // 2
|
|
subprocess.run(
|
|
[
|
|
render.resolve_tool("magick"),
|
|
str(source),
|
|
"(",
|
|
"-size",
|
|
f"{_TILE}x{_TILE}",
|
|
"xc:none",
|
|
"-fill",
|
|
"white",
|
|
"-draw",
|
|
f"circle {radius},{radius} {radius},0",
|
|
")",
|
|
"-alpha",
|
|
"set",
|
|
"-compose",
|
|
"DstIn",
|
|
"-composite",
|
|
str(destination),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def _downscale(source: Path, destination: Path) -> None:
|
|
"""Shrink an icon to launcher size and blow it back up for inspection.
|
|
|
|
Parameters:
|
|
source (Path): Full-size icon PNG.
|
|
destination (Path): PNG path to write.
|
|
"""
|
|
subprocess.run(
|
|
[
|
|
render.resolve_tool("magick"),
|
|
str(source),
|
|
"-resize",
|
|
f"{_SMALL}x{_SMALL}",
|
|
"-filter",
|
|
"point",
|
|
"-resize",
|
|
f"{_SMALL_ZOOM}x{_SMALL_ZOOM}",
|
|
str(destination),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
|
|
|
|
def build_contact_sheet(keys: list[str], destination: Path) -> Path:
|
|
"""Render a three-row contact sheet for the given apps.
|
|
|
|
Parameters:
|
|
keys (list[str]): App keys to include, in display order.
|
|
destination (Path): PNG path to write the sheet to.
|
|
|
|
Returns:
|
|
Path: The written sheet, for convenience.
|
|
"""
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
work = Path(tmp)
|
|
full: list[Path] = []
|
|
masked: list[Path] = []
|
|
small: list[Path] = []
|
|
for key in keys:
|
|
app = apps.get_app(key)
|
|
glyph = glyphs.get_glyph(app.glyph)
|
|
offset = render.centre_offset(glyph, app.accent)
|
|
svg_text = render.build_svg(
|
|
glyph, app.accent, with_background=True, offset=offset
|
|
)
|
|
tile = work / f"{key}.png"
|
|
render.rasterise(svg_text, tile, _TILE)
|
|
full.append(tile)
|
|
masked_tile = work / f"{key}_masked.png"
|
|
_circle_mask(tile, masked_tile)
|
|
masked.append(masked_tile)
|
|
small_tile = work / f"{key}_small.png"
|
|
_downscale(tile, small_tile)
|
|
small.append(small_tile)
|
|
|
|
rows = [work / "row_full.png", work / "row_masked.png", work / "row_small.png"]
|
|
for tiles, row_path in zip((full, masked, small), rows, strict=True):
|
|
_row(tiles, row_path)
|
|
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
subprocess.run(
|
|
[
|
|
render.resolve_tool("magick"),
|
|
*[str(row) for row in rows],
|
|
"-background",
|
|
style.BACKGROUND,
|
|
"-gravity",
|
|
"center",
|
|
"-append",
|
|
str(destination),
|
|
],
|
|
check=True,
|
|
capture_output=True,
|
|
)
|
|
return destination
|