From 6af49ec67a88b212e4c3b8cb151334a11baae99e Mon Sep 17 00:00:00 2001 From: Pranavkumar Patel Date: Fri, 2 Oct 2026 19:18:03 -0400 Subject: [PATCH] Draw the feature graphic as the format itself Play asks for a feature graphic that extends the icon rather than repeats it, on a ground that does not vanish into the store's white, and that says what the app is for. The old one was the icon's four bars on the page's near-white ground. The new one is how a code works: every character names one of 25 parts of the cell before it, five by five. Three cells are drawn cut into their parts, each lighting the part the next is drawn from, down to one doorway in brass. The parts lit are the ones St. Lawrence Market's north door, #G3RJM-8X3L1, is drawn from, as the reference implementation places them. It keeps the icon's four tints, level one as the ground and levels four, seven and ten as the cells, and has no words. Play's play button, laid over the centre when the listing's video does not play by itself, falls on the middle cell. It is still drawn by design/build-icons.mjs from the tokens, with no image library, and --check still compares it byte for byte. --- android/PLAY.md | 2 +- android/play/feature-graphic.png | Bin 3830 -> 11919 bytes design/build-icons.mjs | 128 +++++++++++++++++++++++-------- 3 files changed, 98 insertions(+), 32 deletions(-) diff --git a/android/PLAY.md b/android/PLAY.md index 1f431cf..36820c8 100644 --- a/android/PLAY.md +++ b/android/PLAY.md @@ -126,7 +126,7 @@ address the site is run from. | Asset | Play requires | Here | | --- | --- | --- | | App icon | 512 by 512, 32-bit PNG, at most 1,024 KB | [`web/public/icon-512.png`](../web/public/icon-512.png): a full, opaque square from `design/build-icons.mjs` | -| Feature graphic | 1024 by 500, JPEG or 24-bit PNG with no alpha | [`play/feature-graphic.png`](play/feature-graphic.png): the same four bars on the same ground, from the same script, with no words | +| Feature graphic | 1024 by 500, JPEG or 24-bit PNG with no alpha | [`play/feature-graphic.png`](play/feature-graphic.png), from the same script: the format itself, three cells each cut into the 25 parts a character chooses between, lighting the parts St. Lawrence Market's north door is drawn from, down to the door in brass. In the icon's four tints, with no words, and with the centre, where Play lays its play button when the video does not play by itself, on the middle cell rather than the door | | Phone screenshots | 2 to 8; the long side at most twice the short; four at 9:16 and 1080 or more to be eligible for promotion | six in [`play/screenshots/phone`](play/screenshots/phone), 1080 by 1920: a place on the map, search by name, the short form anchored to a landmark, saved places, the emergency card and the QR code | | 7-inch tablet screenshots | 4 or more to be shown for tablets, 1080 to 7680 pixels, 16:9 or 9:16 | four in [`play/screenshots/tablet-7`](play/screenshots/tablet-7), 1920 by 1080 drawn at 1067 by 600 dp, as a 7-inch tablet on its side draws them: the same four screens as the 10-inch set | | 10-inch tablet screenshots | the same | four in [`play/screenshots/tablet-10`](play/screenshots/tablet-10), 1920 by 1080 drawn at 1280 by 720 dp, the wide layout: a place, search by name beside the map, the short form anchored, and the dark theme over its night basemap | diff --git a/android/play/feature-graphic.png b/android/play/feature-graphic.png index bf3c10e32d7c44d5e304fc45552d90b5ccf72d25..d46a7d538c4b299e0b08f95f5b32163b30bd1a24 100644 GIT binary patch literal 11919 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 |UyxX56U6W>;)CQNWxEq|V& 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 literal 3830 zcmeHKziU)M5T0FDITJ3V&?b%BXd$AA|G+9w%z*@nRu*ztS*e9WgbQM#ooPpGyBba^Udsgcl-3r$>!M97!fsF z)5R>2;!E+XA^GcI@phhQ;zg@Cc6RQ|#`=+_HIv2rql(pGj4)zvHT)F9ofuoteqzW?~gvxRfBA6{NQzqnj4vfh*CdsX`4u~pFA=ek2LlmnEZqtD&bI@@j z*cuol72md64x3;s{GbN2Lcu|w>6)If*n*n{&k5!Da5ItoXWbg7Xw0cSif0g;RO#95Uocx3|t=24O-gt6d}Ba zz>1U!6Nt>9Vq(d^-OCRD71HcicLOl { - const [r, g, b] = channels(tint); - const from = Math.round(left + index * (bar + gap)); - const to = Math.round(from + bar); - for (let y = top; y < top + tall; y += 1) { - for (let x = from; x < to; x += 1) { - const at = y * row + 1 + x * 3; - raw[at] = r; - raw[at + 1] = g; - raw[at + 2] = b; + const put = (x, y, [r, g, b], cover = 1) => { + if (x < 0 || y < 0 || x >= width || y >= height || cover <= 0) return; + const at = y * row + 1 + x * 3; + raw[at] = Math.round(raw[at] + (r - raw[at]) * cover); + raw[at + 1] = Math.round(raw[at + 1] + (g - raw[at + 1]) * cover); + raw[at + 2] = Math.round(raw[at + 2] + (b - raw[at + 2]) * cover); + }; + const fill = (x0, y0, x1, y1, colour) => { + for (let y = Math.round(y0); y < Math.round(y1); y += 1) { + for (let x = Math.round(x0); x < Math.round(x1); x += 1) put(x, y, colour); + } + }; + const frame = (x0, y0, x1, y1, thick, colour) => { + fill(x0, y0, x1, y0 + thick, colour); + fill(x0, y1 - thick, x1, y1, colour); + fill(x0, y0, x0 + thick, y1, colour); + fill(x1 - thick, y0, x1, y1, colour); + }; + // A straight line, its edge softened by how far each pixel's centre lies from it. + const line = (ax, ay, bx, by, thick, colour, strength) => { + const dx = bx - ax; + const dy = by - ay; + const length = Math.hypot(dx, dy); + for (let y = Math.floor(Math.min(ay, by) - thick); y <= Math.ceil(Math.max(ay, by) + thick); y += 1) { + for (let x = Math.floor(Math.min(ax, bx) - thick); x <= Math.ceil(Math.max(ax, bx) + thick); x += 1) { + const px = x + 0.5 - ax; + const py = y + 0.5 - ay; + const along = Math.max(0, Math.min(length, (px * dx + py * dy) / length)); + const away = Math.hypot(px - (along * dx) / length, py - (along * dy) / length); + put(x, y, colour, strength * Math.max(0, Math.min(1, thick / 2 + 0.5 - away))); } } + }; + + fill(0, 0, width, height, deep); + + const side = 236; + const gap = 76; + const cell = side / 5; + const rule = 3; + const top = (height - side) / 2; + const left = (width - (3 * side + 2 * gap)) / 2; + const shades = [3, 6, 9].map((i) => channels(levels[i])); + + DOOR.forEach(([col, part], k) => { + const x = left + k * (side + gap); + const px = x + col * cell; + const py = top + part * cell; + const last = k === DOOR.length - 1; + // The lit part is filled to its edges before the rules are laid over it, + // so one on the cell's outer edge meets that edge rather than stopping short. + fill(x, top, x + side, top + side, shades[k]); + fill(px, py, px + cell, py + cell, last ? brass : shades[k + 1]); + for (let i = 1; i < 5; i += 1) { + fill(x + i * cell - rule / 2, top, x + i * cell + rule / 2, top + side, deep); + fill(x, top + i * cell - rule / 2, x + side, top + i * cell + rule / 2, deep); + } + if (!last) frame(px + rule / 2, py + rule / 2, px + cell - rule / 2, py + cell - rule / 2, 3, brass); + }); + + // The lines that carry each lit part out to the cell drawn from it. + DOOR.slice(0, 2).forEach(([col, part], k) => { + const x = left + k * (side + gap); + const next = x + side + gap; + const px = x + (col + 1) * cell; + const py = top + part * cell; + line(px, py, next, top, 2.5, brass, 0.9); + line(px, py + cell, next, top + side, 2.5, brass, 0.9); }); const header = Buffer.alloc(13);