From 9ec84f36a096e30b2dbfc3714d3dec204a2aa18f Mon Sep 17 00:00:00 2001 From: sganggs Date: Sat, 10 Oct 2026 02:36:12 +0800 Subject: [PATCH] 0.2.3: the install icon keeps the 0.2.x mint rook (the owner's choice), refined: a narrower tower, lighter base and more padding so it sits well among launcher icons; manifest sizes, maskable and favicons re-exported (the ICO keeps its transparent mask) --- CHANGELOG.md | 2 +- docs/ASSETS.md | 25 +++++++++++------- docs/design/client.md | 7 +++-- docs/history/0.2.3.md | 16 +++++++++--- public/icons/app-192.png | Bin 1254 -> 884 bytes public/icons/app-512.png | Bin 3302 -> 2553 bytes public/icons/app-maskable-192.png | Bin 1366 -> 1043 bytes public/icons/app-maskable-512.png | Bin 3150 -> 2489 bytes public/icons/app.svg | 10 +++---- public/icons/apple-touch-icon.png | Bin 1356 -> 1021 bytes public/icons/favicon-16.png | Bin 308 -> 275 bytes public/icons/favicon-32.png | Bin 404 -> 356 bytes public/icons/favicon-48.png | Bin 569 -> 422 bytes public/icons/favicon.ico | Bin 11790 -> 11502 bytes test/ui/pwa-install-023.test.js | 42 +++++++++++++++++++++++++----- tools/export-app-icons.py | 24 ++++++++++++++++- 16 files changed, 95 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 140f8ace..20e0b8f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,7 +16,7 @@ - 设置新增四档文字大小,支持长译文换行;干员调配可逐干员选择中文、日语或跟随全局语音(#435、#436,PR #446,感谢 @7eLaryn)。 - 补齐司霆惊蛰、赤刃明霄陈等干员技能期间的攻击与命中音,保留各技能音效自己的音量与播放概率(PR #410,感谢 @Convey123)。 - AI 购买高阶卡时适当降低与队友开局盟约抢卡的倾向,并在混合对局中发送有限的表情回应(PR #407、#444,感谢 @IceCodeNew、@DDDarkstar;可用 `SP_BOT_EMOTES=0` 关闭表情)。 -- 增加本机对局恢复入口,避免同一浏览器的闲置窗口妨碍恢复已关闭窗口的席位(#431)。浏览器支持时可添加到桌面,精简包也自带原创的系列通用要塞图标、maskable 安全区版本和 favicon(PR #413,感谢 @Cromemadnd)。 +- 增加本机对局恢复入口,避免同一浏览器的闲置窗口妨碍恢复已关闭窗口的席位(#431)。浏览器支持时可添加到桌面,沿用 0.2.x 的薄荷绿城堡并精修,精简包也自带该图标、maskable 安全区版本和 favicon(PR #413,感谢 @Cromemadnd)。 - 确认页可由房主发起全体真人一致同意的开局重刷,保留房间与个人配置;取消恢复剩余确认时间,成功后重新确认(#77、PR #408,感谢 @Siornya)。这是复刻版便捷功能。 - 修复模型加载较慢时丢失入场动画,按经过时间接续,攻击或死亡后不再补播过期入场(PR #416 提示的加载问题,感谢 @Cromemadnd)。 diff --git a/docs/ASSETS.md b/docs/ASSETS.md index 94635acf..0d934e25 100644 --- a/docs/ASSETS.md +++ b/docs/ASSETS.md @@ -8,15 +8,17 @@ The original project icons below are shipped static files, not downloaded game a ## Original project application icons (#413) -`public/icons/app.svg` is the original vector master for the **卫戍协议 series**, independent of any season. -Three battlements lead into a shield-shaped wall around one square tactical cell: defending a position on a board. -The only colours are charcoal `#242424` and off-white `#f2f2f2`; the mark contains no text, gradients or fine texture. -It contains no 「盟约」 lettering, Alliance badge or season-exclusive colours / elements. +By the maintainer's revised decision of 2026-10-10, **沿用 0.2.x 的薄荷绿城堡并精修**. +`public/icons/app.svg` retains the existing mint rook's three battlements, inset tower and splayed base. +The selected refinement (B) narrows the tower, balances the battlements, reduces the base thickness and adds breathing +room. On the 512 px canvas the mark spans x=128–384 and y=88–424; the tower is 176 px wide and the base is 32 px thick. +The original colours remain mint `#4ed8af` and dark `#0c0f0e`, with no text, gradients, gloss or texture. -**Originality statement (2026-10-10):** the SVG geometry was drawn from basic rectangles and polygons for this -project. No official logo, artwork, icon or font was used, downloaded, embedded, traced or redrawn to make it. +**Provenance / originality statement (2026-10-10):** this is a proportion refinement of this project's existing +`public/icons/app.svg` from before `ba5b998a`, expressed as basic rectangles and polygons. +No official logo, artwork, icon or font was used, downloaded, embedded, traced or redrawn for this refinement. The vector and exports are project-owned work under the repository's GPL-3.0-or-later licence, separate from the -Hypergryph / Yostar game assets below. The abstract fortification / tactical-board idea does not assert official branding. +Hypergryph / Yostar game assets below. The castle mark does not assert official branding. Rebuild offline with Python 3 and Pillow (the same image library used by local-extract): @@ -28,6 +30,8 @@ python3 tools/export-app-icons.py --out /tmp/stronghold-icons The script reads only the master SVG's flat rectangle / polygon geometry, renders at 8× and downsamples; unsupported SVG elements fail rather than silently disappearing. It needs no game art, fonts, browser or network. +ICO frames use the same native-size renders as the PNG favicons, with explicit 24-bit DIBs and opaque 1-bit masks +whose rows are padded to 4 bytes. This avoids malformed transparency masks in Pillow's RGB ICO export. | Files in `public/icons/` | Use | |---|---| @@ -37,7 +41,7 @@ SVG elements fail rather than silently disappearing. It needs no game art, fonts | `apple-touch-icon.png` | 180 px home-screen icon | | `app.svg` | Scalable favicon and editable source | -The maskable mark is scaled to 87.5% around the canvas centre. Its outermost vertex is about 34.8% of the canvas +The maskable mark is scaled to 87.5% around the canvas centre. Its outermost vertex is about 36.1% of the canvas width from that centre; even antialiased edge pixels stay inside the **40%-radius safe circle** specified by [Web Application Manifest §2.3](https://www.w3.org/TR/appmanifest/#icon-masks). The background remains opaque to every edge; launcher masks may remove the background outside the safe zone. @@ -46,8 +50,9 @@ favicons and Apple icon; `public/manifest.json` lists all four install icons. Al and included by the full and lite package allowlist; no changes to `data/assets.json` are needed. `test/ui/pwa-install-023.test.js` checks decoded pixels, mask safety and references; `test/package.test.js` checks -packaging inclusion. The local handoff holds old/new light/dark, small-size and phone-layout previews with their -render script. These are offline review renders, not evidence of browser or native OS installation. +packaging inclusion. The local handoff `claude-review-023/icon-v2/` holds the original and A/B/C SVGs, light/dark +256 px and 16/32/48/96 px comparisons, and phone-layout / mask previews with their render script. +These are offline review renders, not evidence of browser or native OS installation. ## Running diff --git a/docs/design/client.md b/docs/design/client.md index 40abede9..487d43e2 100644 --- a/docs/design/client.md +++ b/docs/design/client.md @@ -115,8 +115,11 @@ refused recovery can return to the previous identity and nickname. It does not i Without BroadcastChannel it refuses shared recovery. Server restart / expiry clears obsolete metadata on the next welcome. A spectator never registers a recoverable player seat. -`manifest.json` enables browser installation. The original season-independent fortress / shield mark in -`public/icons/app.svg` uses neutral greys and no official art, logos, icons, fonts or season-specific insignia. +`manifest.json` enables browser installation. The maintainer's revised decision of 2026-10-10 is to +**沿用 0.2.x 的薄荷绿城堡并精修**: `public/icons/app.svg` keeps the existing three-battlement rook, inset tower +and splayed base in mint `#4ed8af` on dark `#0c0f0e`. Refinement B narrows the tower, balances the battlements, +thins the base and increases breathing room. It has no lettering, gradients or gloss and uses no official art, +logos, icons or fonts. `tools/export-app-icons.py` exports 192/512 px PNGs for `any` and separate opaque `maskable` icons: the latter scale only the mark to 87.5%, keeping every foreground pixel inside the central circle of radius 40% of the canvas. The same master supplies 16/32/48 px PNG / ICO favicons and the 180 px Apple touch icon linked from `index.html`. diff --git a/docs/history/0.2.3.md b/docs/history/0.2.3.md index ed3510da..f695f7a1 100644 --- a/docs/history/0.2.3.md +++ b/docs/history/0.2.3.md @@ -275,19 +275,27 @@ Based on @Cromemadnd's proposal, with a one-use install controller. Buttons appe standalone/fullscreen and installed sessions hide them. Accept, dismiss, failure and duplicate clicks all consume the same offer once. See [the prompt API](https://developer.mozilla.org/en-US/docs/Web/API/BeforeInstallPromptEvent/prompt) and [installability requirements](https://developer.mozilla.org/en-US/docs/Web/Progressive_web_apps/Guides/Making_PWAs_installable). -The 2026-10-10 icon revision replaces the previous mint rook with an original, season-independent fortress / shield -and defended square. Its neutral grey palette and geometry use no official logos, artwork, icons or fonts and no -Alliance lettering or seasonal insignia. `public/icons/app.svg` is the master; `tools/export-app-icons.py` exports +The maintainer revised the icon direction on 2026-10-10: **沿用 0.2.x 的薄荷绿城堡并精修**. +The selected B refinement keeps the previous mint rook's three battlements, inset tower and splayed base, +with a narrower tower, balanced battlements, thinner base and more breathing room. It retains mint `#4ed8af` +on dark `#0c0f0e`, without text, gradients, gloss or official logos, artwork, icons or fonts. +The source is the project's `public/icons/app.svg` from before `ba5b998a`; the local `icon-v2` review includes +the original and A/B/C proposals at large and small sizes, on light/dark backgrounds and simulated home screens. +`public/icons/app.svg` is the selected B master; `tools/export-app-icons.py` exports 192/512 px `any` and separate opaque `maskable` PNGs (the mark stays inside the central 40%-radius safe circle), 16/32/48 px PNG / ICO favicons and a 180 px Apple touch icon. All ship outside optional game art, including lite packages. See ASSETS.md for the source, reproduction command and originality statement. +The offline revision also corrects the ICO writer's transparency masks: each 16/32/48 px frame now uses its own +native-size render and 4-byte-aligned opaque mask rows. Decoding previously exposed transparent holes in the 48 px +frame; the regression checks mask layout / opacity and compares every ICO colour pixel with its PNG counterpart. No service worker or offline cache is installed; the server is still required. The lobby keeps Settings immediately before How to Play even when the optional install/recovery buttons appear; the full browser run caught and corrected an initial insertion between that established pair. The settings-entry check tests the absent board-style/background-music option labels, allowing the text-size help to explain that the board keeps its scale. -Tests: offer lifecycle/disposal, decoded PNG dimensions / opacity / maskable safe zone, favicon references and packaging inclusion. +Tests: offer lifecycle/disposal, decoded PNG dimensions / opacity / mint palette / maskable safe zone, +three distinct battlements and an inset tower at 16/32/48 px, favicon references and packaging inclusion. The initial feature also had real Chrome manifest and installability checks, and a synthetic dismissed offer through the actual button. Initial incognito rejection was a test-environment limitation; the real installability check runs in Chrome's ordinary temporary profile. Native OS installation itself is not performed. diff --git a/public/icons/app-192.png b/public/icons/app-192.png index 4db28a8859cfb2bc51644f8c0d86504261524e97..2639b4e306c9b8be3cc6f8e64c5a3327762dcde7 100644 GIT binary patch literal 884 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 N#6-nKHvFA??mrnPM&JMd diff --git a/public/icons/app.svg b/public/icons/app.svg index eebb6f02..3680a014 100644 --- a/public/icons/app.svg +++ b/public/icons/app.svg @@ -1,10 +1,8 @@ - Stronghold Protocol — fortress and defended square - Original geometric project mark. No official artwork, lettering or seasonal insignia. - + Stronghold Protocol — refined mint rook + Refines the existing 0.2.x project castle: three battlements, inset tower and splayed base. Original project geometry; no official artwork, lettering, gradients or texture. + - - - + diff --git a/public/icons/apple-touch-icon.png b/public/icons/apple-touch-icon.png index 066f6cdc4e7de2759455dbf691e094d8885c59ff..9165f09555b18f60e27bfbb6f0d7738c565fe02b 100644 GIT binary patch literal 1021 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQWbof%%B1i(^Q|oVRz*XUT=i9Dg`@ z&olWW`#3Y1_cksF@DS|2keYX7YWI$pi?khMM0c(1R;rMVb3CwBfKy}2JqJf=pRj8! zok`4-T=xqJ8|*iJ|5>Nqc+Tb@1!--}#@ns-UVRYz?0%)_k%Rj;77KJXd&syvQV?RF zXwh-RQH4*bkQAZ!TbZ7zO*~AJWkpp*6*V&_PM-R-b?enKTlMzi>FUd;OYYoZ zI5$3K$1R=wxV?F-Q#z6}i~l|>kB@(QZ}DI8-@E6YfbF={@wodx9baM zPW(QvX5Y7q?BTOh-%s|BW|Y&$?Zy?gAIfAucXOXE&yP=ga_H9Y>+|ypldDd?TvwwK zeLSXB{;~f3y|qt+ZwB5f*77dAXGxqtSI8c}dGh7_!#91`FY0>u-9Cy}^^t|8&A*?g z^S@raH?8o)i^$arU1Zj7jsI46;rNGbJ&T*CuWi#Re37gX=C;-%ZD;?&ILW7vW}LNa zo0J;TlYH%$#Hwk5CVijn>V7?%Z6#l79N~7?A}V`--TwLyFK%}HmdKhOXw|p>zhBLV zqBXTVPfxFqJ09^{qDo_Nv&FIN$1iwF)*dljX4h7C>V;Zg;TOY_P&fa=KbPHKUvmsk zEfG6jDZe(_eXs7m%@#lGDT`G?$w7hdpS3l6a%b|UO?P&gzRdk-d(<#8a^L?yg|Dyu zecu23Rr0>|M$2wTuZ_O^cJ01jFO>TPr(8^ympx)Q%V^i@#Z@xO+KboQc^x-AWn}eg zah{B_-s2?s;>n(?S7pd7)_;7br26%}$#*x^0z=L1ltp>^bH|%@r;Zv1r;5B^93}${ zx2JYoM-4ZpPIG5ySmE7W8!2ZjtgjvJsfzKH5HBWNpn4inIl4Sd;YG zwSM_qXPcSN)X80U|BDt^<>yzW+6!D{t~}Z3L~1s`46rRv^wn2stb8nUUK^OL89ZJ6 KT-G@yGywoRp2hkA literal 1356 zcmeAS@N?(olHy`uVBq!ia0vp^TR@nD2}o{QKQWbofmPVk#WAE}&fB~89?F>vZ6EWm zuV!*ea7a{`pfn-ug4Wt?8%=s!#a{ZPCr=mYYF#Qei8)(Wr)TZjsbOK4mwJgh3b3>` zvWnZ}<;UkAPf@!+|9LpqI z%SG|JRpG^TnTs|pmUedxPJ_hFXI zRIjH$e%$zEw@BmF_uoehnZ3Qe&AtonogTP0tatOBI@{yt?aIo^;^N}|{aJA^p`gBg z{!5p|7d5sR&FO!fUE6c->32=nxG4`m*tTyyuRl>gwL+-fw$?>%>O+4SvoC@_Lyw(# zQaxl;QLD^YgJNOiPfBo7upS-BG*Nz@-U7sqEyzgq(R>^tyB!bt5 z{r+1g;%aI8@9f#L|Ni~EeEG77ym8;-xb@S&UVnB+>u^{7sb{r{Vd3G+trgdPKlouE zkM^|1Gycvv)pEp=zb!O1VxvrJ>=aJ*B~w2q+z4xPJso(Xt|rmx(I581hm3Th`jqqo zmT_i2wU;QHmUy>FWT*UpYsEsv=EPjdYr5_S#a~b6%oH_}Fq@wEvPfi){C*#g#OA~| zdWTd zN#&Sq-_gV#&8c2<{gz+Nl5}5^`Rn`c$h{&a3oP!;nsDUc!2&zJ-|8x*Wd=1Jho5nJ z#>ny1KiZJ%6!e|_@M?=UGbbGBKKQ|)@1~!}tBVOgB#*CAQ~8)}P}8^g1gB@M4A1$; z8fi{K{_M&f7N2HMxbpfShiab$m6!4uqM~((g8N* zI~JS#JcRBgw8$Nw@8co$DxrqMyzjia3TK6Z9Z;-Zz2)E!2buq~COm(h&i0tO@Xz89 zt-bs9u~oM}d?;Qa@d}dwbFf&AL7zkdJ!`wvs~-|s)Md-~Vgk5_A^p>XN>`~By|{2uHY7#J8{ z?K}PR)jMR7&)4sNxOfYv24VmceKp8NxN}H~AdB$ptBE<8B8wp6fn7|O$E0{C{Sz%d)(Sv@=c9JkLK3 zVB5CiICq!sx=skOuB((%2yrC&RnRmI0AtK`-J`vZB&7rp$FY<$2m%1U??+K|jCLNc zFP*08zR|X=VHk`t0992rO>->(@H{WeGC~LdrBqQA<2b%9oCN@ckUY1LOjbB!4SOL_t(|oMYtS=VPEHV4|t%45Jnh0a&n#vI_931_jH|9Jl9^}(~qB5>pQ_0`~lq*;KX zf|rX!QiNSh2w9LrQUvS{4uaYlvE~B~NfE0Jy$qy)edh5aDt{mR`SR`8+mFPX^6lYs z5-s@r?ObL%k#>B0`21XEJ82fcwf!Pw2SH`{;^5oE=WkD3#&5yP-6!!oVz{OvDgq{I z8ioywT0l_{3FxbdIho?uFCFN}B`-;i1p@kN`V&i-Ioa`Bz$VOZG`$j!9oXvwRsmkC z4ZQ?Shl5{lKSCby%3~nj2h5!8#GB3l_6II0>{aHUFW>IZ*}y;zfIWz)2$*PUI>V?1 b41@syJKI`?;DUzz00000NkvXXu0mjfwOF8t delta 377 zcmV-<0fzqM0+a)gB!62;L_t(|oMTi`QDLAZV4|t%45JndhB#os6k=v(78e(XbDuqX zMv@?53&h36w`|!0=NcIqkt8?>f(Q*XHa0RaFmQ2k5fmiT0$*QWA_a$OPG-R`{o%uh z{{DVE3Lud|z6IaDecQZwGlVT5AOMNg&tO2L1tbJfVPWAGFn@@OiXvXuKu0n!FR!q$ zFfLgI1qFf@Fk;qdQc_aL1;B#`4^mT8-@bi|Y5MHhvyrVQwy1da?Afwq%g_u`S64?? zV`5^0W_nLg53xRgfRd6DH*eldo;>;6w{Jx1BEbUkz(5xjl!H+V7zhCX Xgm{U^Jaz%A00000NkvXXu0mjfV8^0j diff --git a/public/icons/favicon-48.png b/public/icons/favicon-48.png index 22cac1e4bc643de4beed108ccf8bde1cd8bbc6c4..0b84e3710d6d5e00cc94ed2ffd2cf0838b7372bf 100644 GIT binary patch delta 397 zcmV;80doGi1f~O!BYy#HNkluCBpF&M zW6pw81ZEMKMPL?zSp;Sgm_=aLRNyBxE4OxW;y=?YpWBU>p?@O)ciScJz}j42INTjN z5&}sw>4iD}8B&#>M*d_&8!?@rTQ-=QH@&Z}S-HV!e=ACM2JwKFP7g**H?ArWh}|tC zFpIz}LgGNqyKO%{i(jc#t49OsIo*e&ilC`gt2fHkXg~nmc1OhIX+B*yG7tnzp)}zh zYPa2ayS@zzdVjsV8DEpkw=CvTP*CZHf=V|ORJx%_-jJF%i+fvPFEGEqBO7}3AT@8U zR*$EPg|HxVd2!{eG`@-cRPzf{Fi19Z5slvz4M@pm1T}qAG@zIZ{wdvax*zvXh$|`m rpE_yJ5P?|)W)YZ0U>5y1;Co-jXI_G*Fg^|d0000JhV|?GTTCLXW^?&tR*Y#Gb6)LeTi?jR{ zA&haYR=aN@gxp5dG%fTrBotl#8B>NpGE~umX0v&l1yyB~&t|jRwXW-l3uP`ubjW72 z@Smm(fn*?%3tKXf$HlxUM^& z&!ayv#q;k+yWM8Ka4Dsp=lQ;Wy;T(D=K?c4*L9iS?SB;^c0932hZnw*SFeQZ8w!PVGl6ns<7K?hl&bizBzTapx zglvQp;*9ErVB&5ai^oyD7EIiPm{G+E=50c7`SI23^$v%F=XsRUn4`Tx=)HXY@gr%z jLeKan^cswBLhyuNyn4lztM-!-00000NkvXXu0mjfC20jY diff --git a/public/icons/favicon.ico b/public/icons/favicon.ico index 1b3ebeee8a9b58c6173766c6a524ac96e08a2f20..dc41051cf945a61bad825ae74d0302de52825a53 100644 GIT binary patch literal 11502 zcmeHNK}#D!6n@34#;8>SiI_%gwNO#8ViBTJYo$e@P!W1)d#ifYlb$@4LcQivke-T# zif2!PwCA1*{+xcvYlq?ObhA6VYhwDs?CzVHZ{EE3&Ai>2%@WKZlYt9O-=pDmxxw?9R~&%0<6&v7lIo4p5vpD$0^s_eLZPlFgLq2>`YDr%3}j( zSL?xa`fJUJN8!f134sUr@r%m8@F@Ruv0^uQ|D_rNZZ|w!gM+sHCqJLhR7*}5hAaI7 z%ni}RiLZ>`wl&*w2>#=@#hGf6!O(Dg`4qAfKUC;__G)Z?b%X`TfWadcoK3PQ{2K?p z*Ls|laV+Af$7kb*m~2^`WkHha^kbk<%D>rr5G%#%C?rPhc(vtN`imodxINg-!{yIE zR%7W07`Lm4T|oK0$)ULVBX;4fobA~?dKk_O=75Lgritn|R5%6Y$4QGR%#djlL>Hnc=dL}v!2P+Y<+fkwBD&>3W_^F|B>mfm({RTu9+`HOARW}^%qV#C z&v#(K7;CemBbI(VAJLZ6()t~=wM$UW+SXp*HLzIv72Gil@W!BZ=lVVcuL$TZ4zo`w z>OQ4oCYU;+4EzraU>S+Gbg86N=n^XmvrzF6I?^7z-ovulgALK-!`FpW_<<5kp@b&E z82FWzJqb-p-5B=EkEM)^VUzG~Sj>v1S^VLU;xGwSYL6g9WR)L5h{!5Gf)J5aegq*R ztNaK;M7FK?!GZw~$EFV~0oboS>g(?jekITP8N;@vZc-9sQa1_TDR>N%7&T6vt0AKZwo@4O00Uz1fUP$!IDg21z!o$OZgM)vzNk~Wtw+2nZ!os$1-yRzq+uhxL^XAQ{s3>kR zI%^UT5KvH1Fg`y1k4>IDd6Jix=kM?D=nv$eprGdFW*k3#`gGyKg^rF6X;oWW8yFaf zgK7mxKR>_H(o!5|W@d;9e6l!o>eTG)EXJauA{9zA*#2mQc7FBU*D2V|6SCP7%m z$Rj=fG7z}oAa)c4yWXhAYgNjZbzuHonE`7ZvG7afr|QV}-=dDJYS>I?w;7Oi$U+ju z?KmqoWLdI?ckI|h_>r>s;K75FCr?t&$VJMHi_!FM!n44pN-r%fQAz){3B`9|Vd2`fYi=h( z@SQt%0=uxV5ZKi8SFc{Zc=3XIO`)fgxw$!vk_a0fz-Xd}XP~R#7cX9v4S;~JU%w_` zZ*Om3Umwc+{5(byA+MU68re8q=>nh7&`=<;RS=vwz$VVimoM3sVSD%P-Rz)m-n@Y# z=~s0@$_B7ij<|uGafAfI(#z1$(2*lY7z>%7OrG4_+((Zd5g7+kgEX?>sd0R!9LNtG zIPm=W^S5u`3UapIef#ziJTfx!H=B?GyV}8n2gk<77<6&tP?C_4Alsg^UL^jZLx(0N zCO}0OoN0)|`3BNpjEt}Ym6=J8nVFd&!2(yKqoc{m$u7p9oSc-bIYvZBM}vokSFc`W zk4KuCnwVPj2;h;C5rQM+LYk|J5515RA0ID+4t}U$qMkp09{dbbYHI4khY!~hAN1kd7n3}xo{;u3y1o^s+pRaq8H|8N=gblBUAn9)2A2_LeVM4!-o%x zCvO+LCnP2&8m}+bDXWI*O&4T=?BBmXA|isN%9XkyMk)rC1f!$pS4*gLK^7D`1Vx%~ z6^j_9f?;&@lt;!_;3-j_%7S8SR8LLC;|%b6V2S|)*7qpuCz=!d)O%E*`5xu*iE9Ed z888|6A2Sda7sr=Tj)}aKc|1+|J}u{Cj;c91Ii7+fw5XH1QanPGo)(2fDH}Jte|ma)e}BKiV5h7v zBxhit<=nY*DtEq6@JYoPQR@ZC85p5Ofl-P9QEP|9Khx}(ms<+5!538uHD4H4Y{-XB zh1?CLtE^sK)QGDp8hHNy5j+WLW^R6 zho|&z;?XmpA1{yala-al98 { +test('manifest provides opaque mint-rook 192/512 icons for both purposes with a safe maskable mark', () => { const m = JSON.parse(readFileSync(new URL('../../public/manifest.json', import.meta.url))); assert.equal(m.start_url, '/'); assert.equal(m.scope, '/'); assert.deepEqual(m.icons.map((i) => `${i.purpose}:${i.sizes}`).sort(), @@ -55,13 +55,14 @@ test('manifest provides opaque 192/512 icons for both purposes with a safe maska const size = Number(icon.sizes.split('x')[0]); const { w, h, rgba } = decodePng(bytes); assert.equal(w, size); assert.equal(h, size); - let foreground = 0; + let foreground = 0, solidMint = 0; for (let y = 0; y < h; y++) for (let x = 0; x < w; x++) { const i = (y * w + x) * 4; assert.equal(rgba[i + 3], 255, 'opaque edge-to-edge'); - assert.equal(rgba[i], rgba[i + 1], 'season-independent neutral palette'); - assert.equal(rgba[i], rgba[i + 2]); - if (rgba[i] === 36) continue; // #242424 background + const [r, g, b] = rgba.subarray(i, i + 3); + if (r === 12 && g === 15 && b === 14) continue; // Original #0c0f0e background + assert.ok(g >= b && b > r, 'mint palette, including antialiased edges'); + if (r === 78 && g === 216 && b === 175) solidMint++; // Original #4ed8af foreground++; if (icon.purpose === 'maskable') { // W3C safe zone is a circle, not merely a centred 80% square. @@ -70,6 +71,7 @@ test('manifest provides opaque 192/512 icons for both purposes with a safe maska } } assert.ok(foreground > size * size * 0.12, 'the exported mark is not empty or tiny'); + assert.ok(solidMint > size * size * 0.12, 'the original flat mint identity is retained'); } }); @@ -86,7 +88,7 @@ test('favicon and Apple links resolve to real sizes, including all legacy ICO fr assert.ok(link.includes(`sizes="${w}x${h}"`), href); } else if (href.endsWith('.svg')) { assert.match(bytes.toString(), /viewBox="0 0 512 512"/); - assert.doesNotMatch(bytes.toString(), /<(?:image|text|use|foreignObject)\b|(?:href|font-family)=/); + assert.doesNotMatch(bytes.toString(), /<(?:image|text|use|foreignObject|linearGradient|radialGradient|filter)\b|(?:href|font-family)=/); } else { assert.equal(bytes.readUInt16LE(2), 1); assert.equal(bytes.readUInt16LE(4), 3); @@ -95,11 +97,37 @@ test('favicon and Apple links resolve to real sizes, including all legacy ICO fr assert.equal(bytes[entry], size); assert.equal(bytes[entry + 1], size); const length = bytes.readUInt32LE(entry + 8), offset = bytes.readUInt32LE(entry + 12); assert.ok(length > 0 && offset >= 54 && offset + length <= bytes.length); + // Decode the legacy 24-bit DIB and its 1-bit transparency mask. Both row + // types need 4-byte alignment; malformed masks can punch holes in an icon. + assert.equal(bytes.readUInt32LE(offset), 40); + assert.equal(bytes.readInt32LE(offset + 4), size); + assert.equal(bytes.readInt32LE(offset + 8), size * 2); + assert.equal(bytes.readUInt16LE(offset + 14), 24); + assert.equal(bytes.readUInt32LE(offset + 16), 0); + const colorStride = Math.ceil(size * 3 / 4) * 4, maskStride = Math.ceil(size / 32) * 4; + assert.equal(length, 40 + (colorStride + maskStride) * size, `${size}px aligned ICO frame`); + const mask = bytes.subarray(offset + 40 + colorStride * size, offset + length); + assert.ok(mask.every((byte) => byte === 0), `${size}px ICO is fully opaque`); + const { rgba } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url))); + for (let y = 0; y < size; y++) for (let x = 0; x < size; x++) { + const bmp = offset + 40 + (size - 1 - y) * colorStride + x * 3; + const png = (y * size + x) * 4; + for (let c = 0; c < 3; c++) assert.equal(bytes[bmp + 2 - c], rgba[png + c], `${size}px ICO matches PNG`); + } } } } for (const size of [16, 32, 48]) { - const { w, h } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url))); + const { w, h, rgba } = decodePng(readFileSync(new URL(`../../public/icons/favicon-${size}.png`, import.meta.url))); assert.equal(w, size); assert.equal(h, size); + // At actual favicon sizes, the three battlements must stay separated and the + // tower must remain narrower than its base. Sample visible raster features. + const row = (y) => Array.from({ length: w }, (_, x) => rgba[(Math.floor(y * h) * w + x) * 4 + 1] > 100); + const runs = (pixels) => pixels.filter((on, x) => on && !pixels[x - 1]).length; + const crown = row(0.19), tower = row(0.56), base = row(0.79); + assert.equal(runs(crown), 3, `${size}px battlement gaps stay open`); + assert.equal(runs(tower), 1, `${size}px tower stays connected`); + assert.equal(runs(base), 1, `${size}px base stays connected`); + assert.ok(tower.filter(Boolean).length < base.filter(Boolean).length, `${size}px inset tower remains legible`); } }); diff --git a/tools/export-app-icons.py b/tools/export-app-icons.py index 58d08366..d2284a0d 100644 --- a/tools/export-app-icons.py +++ b/tools/export-app-icons.py @@ -6,7 +6,9 @@ only this source's flat rect/polygon vocabulary; unsupported SVG fails loudly. No fonts, game art, browser, network or platform image service is involved. """ import argparse +from io import BytesIO from pathlib import Path +import struct import xml.etree.ElementTree as ET from PIL import Image, ImageDraw @@ -54,6 +56,26 @@ def render(size, maskable=False): return image.resize((size, size), Image.Resampling.BOX) +def write_ico(path): + # Native-size renders preserve the same pixels as each PNG favicon. Write + # the DIBs explicitly: Pillow's RGB ICO writer does not pad 1-bit mask rows + # to 4 bytes, and can reuse the last size's mask for every smaller frame. + sizes = (16, 32, 48) + directory, frames = bytearray(), bytearray() + offset = 6 + 16 * len(sizes) + for size in sizes: + dib = BytesIO() + render(size).save(dib, format="DIB") + frame = bytearray(dib.getvalue()) + # ICO DIB height includes both the colour bitmap and the AND mask. + struct.pack_into("