From a0fa0455edeb84e2720b8279019756cef18221e3 Mon Sep 17 00:00:00 2001 From: Tim Kainz Date: Sun, 5 Apr 2026 15:10:05 +0200 Subject: [PATCH] Add favicon generation script and use favicon --- public/favicon.ico | Bin 15086 -> 6143 bytes src/generate-logo.py | 367 +++++++++++++++++++++++++++++++++++++++++++ src/logos/.gitignore | 2 + 3 files changed, 369 insertions(+) create mode 100644 src/generate-logo.py create mode 100644 src/logos/.gitignore diff --git a/public/favicon.ico b/public/favicon.ico index 57614f9c967596fad0a3989bec2b1deff33034f6..82a000e72b5abfe48e88024ec913c0b6d44f0225 100644 GIT binary patch literal 6143 zcmai1XE+o9+`l__oKemQ33qfHvI-%>*&`0wTck)?**RzLNRpM6LdIDc8Am9}%$|2P zN%lINw|9JaKfKTLe)^B!|M`FX4FCWLpaMup0RB}3KpqSL+y(#u%F6#e3sM6Bp8s|@ z+<(u`mjHkaIshQnKwt9`9p}Fea7kNB&FEkK_W>XQ{dW&A1D!+z0AP@|8rs-Dd(-l# zD=V6H^HT=cv*y+<8mhoTlD7F>TqYPWYoAkB>IdsoQ5 z{^t9%bzV0uHTtm!u0f98!$x7EAO-3^jnbjr>N^{H&f&@Ss%=k5)Q=Tknz4GvC-fr1 zw2}L10IpDHH`a3TMq)M45>(0&>%cF7au#4J7Fc`HFFdlR#S^Th^|f_>Eyj5(+~|oAUZ$d6{48axSwc zBy%zBb$na5V&Pi(&{b9WX18_aAPpC1jzXA0W2ZlC^Cl>nfg@+WLh9L)~7Fa zC1ou1S-)?P?IS7FY3BA#qkL79qu13H2{qM3-2Ov&e?qi3U*zMO0S?2^Bsu{)-Hc>9 z1=Q(hjON^*dgP$z@=nq9Mfv_qU!&;!h-qdCBErhDZ_rb8e2i{vVUEYv9FaHU%EI@0X9dw zvBu4jPecHuU>|E?32DhL6OMa`wBSD{1`*Ih+POz3eh+iQh=G*TsEr!2jr-{6Dlw6; zw)L#(O)Sldi_k+qwt{2I?#i8Vsx~427&yLe+~;oWGYkDw-#qooKL{^ z_V5XL`#+U1LtNk1Wmpb3rJ`I+E8)7ACk{GW>kn=ac@3WwTt zFw^`ewO+*t$;&~cx>xDv2q2zrN~ga|^t{b;s&+9u05pjS6a&9;7^lL}PHjpjBV-PP z*?75F6>AKfHHx{3nScO_XPC_b*;oGI&-NHc)rL(LJdPZb}JD|40F#t*)*quxfwFB@O%h&NiYU655{Z4p4_yTOR|E{V}RI`kqG~BpDKD z2`77f2WXv+GXS5dXcY)9MZs#OHAbHX{aCKH`FrSw%SXBo1Wx(3m<=qu_|+R-OnYWk zyz|&3TbZ8!-2D4)t6KIT45EyqWAcy;pWb`6u?%WREcvs2?r=K38K}oJd!5Yc54Hp~ zI4@e5Ue%3h6XqBrN44wXo&=Xn6@6ayjNGCZ>R8=(> zn$!b+amX|M-&6_mb>ceUUb}l=pX*;|Qr#-9X=8d^uIIxLudU+bp>~X{GrqKvM*_yk zWhkHWanj^&kz)v&i~iDiXT!Uvjl@ z-qKxKhXg;uqyQ4Siwe{(F9H!xda%f(B`p*M3|ZfPeHl`Q{7S zdP-43T^wILwy<{jD0gaB#u|KavYWVt+V)ez9-;(W%kh4fQVtsw4bgr0URV3L^0hP6 zt{rnQ!yIF{lDBZW()+;jnxA|*)G4EF#V<|c6=~+O@QcRDH9X^-&MqBeYr}GOw`AEh z<%>YKd2eSLvct|-i3FBV!eLJ?e(11PUuVMdN;-d{4&h|S@Fq#Fjf>ZQuudC7f5@9) zStU<3zCLC-kz(kPt$1U9((>RqUgmZq7T|wbVPeb9mN-Jr-FQH->@B930Q{O->De6a zOX*9YzfKw&--m>7-q#S^>C z{-~$cO&^R6(8(yncIIvL-Np2`Sk-+w>yHT|j7EmQFBw;<9SdhHJvt}XBo}pRLf!zZ ztS(j3WlPFk5fQcCm)riZwptx>y1BFCHf0X*?a-h~#>xD1I)KjSxndT>(<1dN-XyC6 zJ51QsD}yDtH#qUsm94Q7#4AOJ-EJxKxd7ze(ag}HkpZaEU0>kQTFaN5jRPkxe{v}% z`6MePPyNsQ!<)Wt>~f!%s%xDaFm#HC{M2?U_z)3$Dv&XdSI* zA0JGL`5Q@LJTf;KDTRVOU>2rBXI(X%v~Br(YoL0|bjPo>V%{Z4RE!BG-i#+J#1#|& zs0geYA=C5lAfL$I_A#jS*6>~;lbf}#9o<O=1P18$c{gsEK zbqqfz%Mfkv&mj;hsvOS(MhW}2jCo}v=in%+kSLW+>acg5+cbhzW4Nl&5Qh);`Rl#6 znkDr@O}d`7iUo)VXqz65T^%{}mC172$v#DF@OMUCx^~-bqSB!w%I`9jnYv7Uu2#jD z+obK^%Q9VOS4=8jVJQ!~$wL}v%TNb@R5!6D_hLF4-o+np-ypl7+ubT*XdfZr@#IlmlsM>n&{?=D1@UYAr-1JL-ACaR&-d=h%`LyTz#cT4g5kuggn=0LhfaC-G zn`7o`IrX8A8_Fs%0ih0?OGR4lX#Wy~Wn8E*$P4;dUGc__Bf3yt0U5v6s;n}2`bz~G z2iMW!7*9V?T`CP7vr2XIyU`wl7at|$(B9}o3F3Ih@YI4*TF75k_MP#-rljn=?UBcw zyP`a5H>&SToH9Y#4g4QpWqZI;`L`hLK$UGk?@U60_d?@`=3z5$Dr$roEy(Wiy1l|# zjW1XE#pTI~K!bw8V2FZB_qF|9aFZ@?PW*EndF`736=-CnEw4kpXdA8szII*2IfP-x^HZajtS>*DEUJd3`NfNz{t#m?Ow62NX_*tBXyVBR24& zVWUyF;yYDKsF;iGq>E$IG7k4v$|g4l8!j35j&}qKaU5Qsi+m0Lef`@t`?T68kzqFY z1`I#3^T-bSLZ;GYw3|vOvVpRogbw|54i$(I!VDsWmN1~J-HiPAr>gar7)z(39R6la zj*lqjE1;KO*t>Bb-CVkEdAFs^Aq(fN&BaGzfRypz%Jh*Ztp@{(cgvPAkMDl51*D4> zswIPp*aew!C9NXk?!h$W#f^jv%^=R>H}FC7qA|KTpMO;BKW&?=z8vL5C@afGF5NBz1wHf zYgOzo@+zI041@l36g!kp75urSZDNL8BN8IaT}N@WNu0d^4bY3+AF&r~i_6PrC)v;- zEJGMrH)LpCBGL7$Iw_vt)hJp`9$~UZqbpl#r+89nxZ{sP>c9Jh^q(j$#n=y zQIK&yFPI=>h^M9g7^Rw?af4hMv$Lp?Atfc&&L56r)Rs1@5=QQX%&hapd8eUg)*s9X z(%XB!2kZ4i=^@g=A(=<(hYPm2+oDc1QrDDcZu|Y&q!{Y9+6UEc;T6wO!t+gc()MUa zu0*SU%Y}_yrfSOhtCW6hQiaVuGYg@d5c-Z}LQaVI-lI=vlrHxQpC;ESKg)UVqXA23 zZ?q&ovYcV%w$NA<-B>Okoa{z6p&ajz*`XdUKyI z7IE@?Pl}`dz8Cve|1*955<)CuCx@EsrOQ;{o^>|qZ zzSB^n6QXH4d)~U-EE04vx+(n@THFwV4JtQ{O8VL13SVJN!w%s0%$zeG!d8s*15Wj? zvtI?}T@zwu8saz_*s2&;6f0%HmHdA1s3sL zNQ@~Zg)1q(T?bxH3VjC_5k`Pc&3NcX)hd-z;NIs2y$7Y9yfgXFO+c0XW-@=VZ|>yJ z7gJzw+kMmAlQKOz9UEPF{=8*TRmdl|IQpHB9a}7tC$s?H-v25`S+Ux_bq)k}SE~uL za+M3YS;RH41O!ZA2r?q0OX-q3H+;^i;cS>#%~|{depq!_;a&bqo)k<4^pO7K`9kWC!YLeR{^X+T+#Q z(-C{tA(3F1I8G-vPot-HP#4)-1O2`F44B>VdCG)-V+NJ!dGym%Viin75}gb z9`~qfUXNRinfl|A^5jD2fFO-!2+fh5I-t=3=VR$l79byWm=SW+@@yTK<5-kRb19TV zuL}i`AT8C)-^8=cBflXR2V_O2c&gkaV>~n^28p%B^d2Ve1`_tMJciX|GmfYski?cQ zJP>>b`t?(u#0Ymb+n0>b8J`e*kx>J??mh|E4;{h#%@UKbbN~n|4*Wi!F+SWY!+$iY7#q!&ib_12Ddy;z` z&qY9pfY1d45*%eN%xr_m|6^R;h>bgtVNo9GgS7zoSw#rulmnKkv8+M?tlBt+o0$Cw zS{(tXW=F&UUc(M2!2CNu5rn#fx8+o@7G)jqd?3$!#9;N8*5r~-#+r$6-rPxY{9Wbe z8aO7Pz-4MRi3?@I{sl`_CnF-4>*(E%NYB~EJD7@(cDjMYo1j|iUpmn7%1CCVq5*YJ zLfFP?7DOUCEdAl}+j~8uZp;BLW~MWE61midyKKZ|_JfYkT;u!nDeVmbO;=dLHN-kP zZdQC;p=6f}5gse{?uGt;K9Cje{XqkICL#4$R6M&ET^#bcTl_QDlris)qhev(9yp(f zRA{$eAq9ZkldJs33T7S(3^Am}M(e&ZKf<-Nyzqp(*P;&R4LKq^&OU)Bu{U+SP$@1z6U0?303Ueq(1zL=H>@cO`-rlFNeqU$(@OIfJwVdqr+(cM|+Kva0(`rAd`eWcDp)fx%K4$o+pphaFae(lfje1)3h($GM-@0u2 aKcmaP43jN`1@$}O9!TeO{Q2K0>puXL_J&jd literal 15086 zcmd^G33O9Omi+`8$@{|M-I6TH3wzF-p5CV8o}7f~KxR60LK+ApEFB<$bcciv%@SmA zV{n>g85YMFFeU*Uvl=i4v)C*qgnb;$GQ=3XTe9{Y%c`mO%su)noNCCQ*@t1WXn|B(hQ7i~ zrUK8|pUkD6#lNo!bt$6)jR!&C?`P5G(`e((P($RaLeq+o0Vd~f11;qB05kdbAOm?r zXv~GYr_sibQO9NGTCdT;+G(!{4Xs@4fPak8#L8PjgJwcs-Mm#nR_Z0s&u?nDX5^~@ z+A6?}g0|=4e_LoE69pPFO`yCD@BCjgKpzMH0O4Xs{Ahc?K3HC5;l=f zg>}alhBXX&);z$E-wai+9TTRtBX-bWYY@cl$@YN#gMd~tM_5lj6W%8ah4;uZ;jP@Q zVbuel1rPA?2@x9Y+u?e`l{Z4ngfG5q5BLH5QsEu4GVpt{KIp1?U)=3+KQ;%7ec8l* zdV=zZgN5>O3G(3L2fqj3;oBbZZw$Ij@`Juz@?+yy#OPw)>#wsTewVgTK9BGt5AbZ&?K&B3GVF&yu?@(Xj3fR3n+ZP0%+wo)D9_xp>Z$`A4 zfV>}NWjO#3lqumR0`gvnffd9Ka}JJMuHS&|55-*mCD#8e^anA<+sFZVaJe7{=p*oX zE_Uv?1>e~ga=seYzh{9P+n5<+7&9}&(kwqSaz;1aD|YM3HBiy<))4~QJSIryyqp| z8nGc(8>3(_nEI4n)n7j(&d4idW1tVLjZ7QbNLXg;LB ziHsS5pXHEjGJZb59KcvS~wv;uZR-+4qEqow`;JCfB*+b^UL^3!?;-^F%yt=VjU|v z39SSqKcRu_NVvz!zJzL0CceJaS6%!(eMshPv_0U5G`~!a#I$qI5Ic(>IONej@aH=f z)($TAT#1I{iCS4f{D2+ApS=$3E7}5=+y(rA9mM#;Cky%b*Gi0KfFA`ofKTzu`AV-9 znW|y@19rrZ*!N2AvDi<_ZeR3O2R{#dh1#3-d%$k${Rx42h+i&GZo5!C^dSL34*AKp z27mTd>k>?V&X;Nl%GZ(>0s`1UN~Hfyj>KPjtnc|)xM@{H_B9rNr~LuH`Gr5_am&Ep zTjZA8hljNj5H1Ipm-uD9rC}U{-vR!eay5&6x6FkfupdpT*84MVwGpdd(}ib)zZ3Ky z7C$pnjc82(W_y_F{PhYj?o!@3__UUvpX)v69aBSzYj3 zdi}YQkKs^SyXyFG2LTRz9{(w}y~!`{EuAaUr6G1M{*%c+kP1olW9z23dSH!G4_HSK zzae-DF$OGR{ofP*!$a(r^5Go>I3SObVI6FLY)N@o<*gl0&kLo-OT{Tl*7nCz>Iq=? zcigIDHtj|H;6sR?or8Wd_a4996GI*CXGU}o;D9`^FM!AT1pBY~?|4h^61BY#_yIfO zKO?E0 zJ{Pc`9rVEI&$xxXu`<5E)&+m(7zX^v0rqofLs&bnQT(1baQkAr^kEsk)15vlzAZ-l z@OO9RF<+IiJ*O@HE256gCt!bF=NM*vh|WVWmjVawcNoksRTMvR03H{p@cjwKh(CL4 z7_PB(dM=kO)!s4fW!1p0f93YN@?ZSG` z$B!JaAJCtW$B97}HNO9(x-t30&E}Mo1UPi@Av%uHj~?T|!4JLwV;KCx8xO#b9IlUW zI6+{a@Wj|<2Y=U;a@vXbxqZNngH8^}LleE_4*0&O7#3iGxfJ%Id>+sb;7{L=aIic8 z|EW|{{S)J-wr@;3PmlxRXU8!e2gm_%s|ReH!reFcY8%$Hl4M5>;6^UDUUae?kOy#h zk~6Ee_@ZAn48Bab__^bNmQ~+k=02jz)e0d9Z3>G?RGG!65?d1>9}7iG17?P*=GUV-#SbLRw)Hu{zx*azHxWkGNTWl@HeWjA?39Ia|sCi{e;!^`1Oec zb>Z|b65OM*;eC=ZLSy?_fg$&^2xI>qSLA2G*$nA3GEnp3$N-)46`|36m*sc#4%C|h zBN<2U;7k>&G_wL4=Ve5z`ubVD&*Hxi)r@{4RCDw7U_D`lbC(9&pG5C*z#W>8>HU)h z!h3g?2UL&sS!oY5$3?VlA0Me9W5e~V;2jds*fz^updz#AJ%G8w2V}AEE?E^=MK%Xt z__Bx1cr7+DQmuHmzn*|hh%~eEc9@m05@clWfpEFcr+06%0&dZJH&@8^&@*$qR@}o3 z@Tuuh2FsLz^zH+dN&T&?0G3I?MpmYJ;GP$J!EzjeM#YLJ!W$}MVNb0^HfOA>5Fe~UNn%Zk(PT@~9}1dt)1UQ zU*B5K?Dl#G74qmg|2>^>0WtLX#Jz{lO4NT`NYB*(L#D|5IpXr9v&7a@YsGp3vLR7L zHYGHZg7{ie6n~2p$6Yz>=^cEg7tEgk-1YRl%-s7^cbqFb(U7&Dp78+&ut5!Tn(hER z|Gp4Ed@CnOPeAe|N>U(dB;SZ?NU^AzoD^UAH_vamp6Ws}{|mSq`^+VP1g~2B{%N-!mWz<`)G)>V-<`9`L4?3dM%Qh6<@kba+m`JS{Ya@9Fq*m6$$ zA1%Ogc~VRH33|S9l%CNb4zM%k^EIpqY}@h{w(aBcJ9c05oiZx#SK9t->5lSI`=&l~ z+-Ic)a{FbBhXV$Xt!WRd`R#Jk-$+_Z52rS>?Vpt2IK<84|E-SBEoIw>cs=a{BlQ7O z-?{Fy_M&84&9|KM5wt~)*!~i~E=(6m8(uCO)I=)M?)&sRbzH$9Rovzd?ZEY}GqX+~ zFbEbLz`BZ49=2Yh-|<`waK-_4!7`ro@zlC|r&I4fc4oyb+m=|c8)8%tZ-z5FwhzDt zL5kB@u53`d@%nHl0Sp)Dw`(QU&>vujEn?GPEXUW!Wi<+4e%BORl&BIH+SwRcbS}X@ z01Pk|vA%OdJKAs17zSXtO55k!;%m9>1eW9LnyAX4uj7@${O6cfii`49qTNItzny5J zH&Gj`e}o}?xjQ}r?LrI%FjUd@xflT3|7LA|ka%Q3i}a8gVm<`HIWoJGH=$EGClX^C0lysQJ>UO(q&;`T#8txuoQ_{l^kEV9CAdXuU1Ghg8 zN_6hHFuy&1x24q5-(Z7;!poYdt*`UTdrQOIQ!2O7_+AHV2hgXaEz7)>$LEdG z<8vE^Tw$|YwZHZDPM!SNOAWG$?J)MdmEk{U!!$M#fp7*Wo}jJ$Q(=8>R`Ats?e|VU?Zt7Cdh%AdnfyN3MBWw{ z$OnREvPf7%z6`#2##_7id|H%Y{vV^vWXb?5d5?a_y&t3@p9t$ncHj-NBdo&X{wrfJ zamN)VMYROYh_SvjJ=Xd!Ga?PY_$;*L=SxFte!4O6%0HEh%iZ4=gvns7IWIyJHa|hT z2;1+e)`TvbNb3-0z&DD_)Jomsg-7p_Uh`wjGnU1urmv1_oVqRg#=C?e?!7DgtqojU zWoAB($&53;TsXu^@2;8M`#z{=rPy?JqgYM0CDf4v@z=ZD|ItJ&8%_7A#K?S{wjxgd z?xA6JdJojrWpB7fr2p_MSsU4(R7=XGS0+Eg#xR=j>`H@R9{XjwBmqAiOxOL` zt?XK-iTEOWV}f>Pz3H-s*>W z4~8C&Xq25UQ^xH6H9kY_RM1$ch+%YLF72AA7^b{~VNTG}Tj#qZltz5Q=qxR`&oIlW Nr__JTFzvMr^FKp4S3v*( diff --git a/src/generate-logo.py b/src/generate-logo.py new file mode 100644 index 0000000..1dc1920 --- /dev/null +++ b/src/generate-logo.py @@ -0,0 +1,367 @@ +#!/usr/bin/env python3 +""" +Logo & Favicon Generator +Extracted from CSS color scheme: + Background: #161b22 (dark navy) + Border: #30363d (dark grey) + Primary text: #e6edf3 (near-white) + Secondary: #c9d1d9 (light grey) + Muted: #8b949e (medium grey) + Accent: #2f81f7 (vivid blue) + +Outputs: + logo.png — 600 × 180 px (wide wordmark) + logo@2x.png — 1200 × 360 px (retina) + favicon.ico — 16×16, 32×32, 48×48 multi-size ICO + favicon-32.png + favicon-16.png +""" + +from PIL import Image, ImageDraw, ImageFont +import math +import os +import struct +import io + +# ── Palette ──────────────────────────────────────────────────────────────────── +BG = (22, 27, 34, 255) # #161b22 +BORDER = (48, 54, 61, 255) # #30363d +TEXT_PRI = (230, 237, 243, 255) # #e6edf3 +TEXT_SEC = (201, 209, 217, 255) # #c9d1d9 +TEXT_MUTE = (139, 148, 158, 255) # #8b949e +ACCENT = (47, 129, 247, 255) # #2f81f7 +ACCENT_DIM= (47, 129, 247, 36) # translucent blue fill + +# ── Helpers ──────────────────────────────────────────────────────────────────── + +def lerp_color(a, b, t): + return tuple(int(a[i] + (b[i] - a[i]) * t) for i in range(4)) + + +def draw_rounded_rect(draw, xy, radius, fill=None, outline=None, width=1): + x0, y0, x1, y1 = xy + draw.rounded_rectangle([x0, y0, x1, y1], radius=radius, fill=fill, + outline=outline, width=width) + + +def add_glow(img, center, radius, color, intensity=0.55): + """Soft radial glow overlay using additive blending.""" + glow = Image.new("RGBA", img.size, (0, 0, 0, 0)) + draw = ImageDraw.Draw(glow) + cx, cy = center + steps = 18 + for i in range(steps, 0, -1): + t = i / steps + r = int(radius * t) + alpha = int(intensity * (1 - t) ** 0.6 * 255) + c = (*color[:3], alpha) + draw.ellipse([cx - r, cy - r, cx + r, cy + r], fill=c) + return Image.alpha_composite(img, glow) + + +def make_font(size): + """Try to load a system sans-serif; fall back to PIL default.""" + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Bold.ttf", + "/usr/share/fonts/truetype/freefont/FreeSansBold.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Bold.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-B.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "C:/Windows/Fonts/arialbd.ttf", + "C:/Windows/Fonts/calibrib.ttf", + ] + for path in candidates: + if os.path.exists(path): + try: + return ImageFont.truetype(path, size) + except OSError: + pass + + # Ask fontconfig for whatever sans-serif is available + try: + import subprocess + result = subprocess.run( + ["fc-match", "--format=%{file}", "sans-serif:bold"], + capture_output=True, text=True + ) + path = result.stdout.strip() + if path: + return ImageFont.truetype(path, size) + except Exception: + pass + return ImageFont.load_default(size) + + +def make_font_regular(size): + candidates = [ + "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf", + "/usr/share/fonts/truetype/liberation/LiberationSans-Regular.ttf", + "/usr/share/fonts/truetype/freefont/FreeSans.ttf", + "/usr/share/fonts/truetype/noto/NotoSans-Regular.ttf", + "/usr/share/fonts/truetype/ubuntu/Ubuntu-R.ttf", + "/System/Library/Fonts/Helvetica.ttc", + "C:/Windows/Fonts/arial.ttf", + "C:/Windows/Fonts/calibri.ttf", + ] + for path in candidates: + if os.path.exists(path): + try: + return ImageFont.truetype(path, size) + except OSError: + pass + # Ask fontconfig for whatever sans-serif is available + try: + import subprocess + result = subprocess.run( + ["fc-match", "--format=%{file}", "sans-serif"], + capture_output=True, text=True + ) + path = result.stdout.strip() + if path: + return ImageFont.truetype(path, size) + except Exception: + pass + return ImageFont.load_default(size) + +# ── Icon mark (the "gem" shape) ──────────────────────────────────────────────── + +def draw_icon_mark(draw, img, cx, cy, size): + """ + Draw a stylised hexagonal mark: + - dark fill with blue border + - inner accent diamond + - subtle glow + """ + s = size + # Flat-top hexagon vertices + hex_pts = [ + (cx + s * math.cos(math.radians(a)), + cy + s * math.sin(math.radians(a))) + for a in range(0, 360, 60) + ] + + # Fill with subtle gradient-like layering + for layer in range(int(s), 0, -1): + t = layer / s + col = lerp_color( + (35, 43, 54, 255), + (22, 27, 34, 255), + t + ) + pts = [ + (cx + layer * math.cos(math.radians(a)), + cy + layer * math.sin(math.radians(a))) + for a in range(0, 360, 60) + ] + draw.polygon(pts, fill=col) + + # Border + draw.polygon(hex_pts, outline=ACCENT[:3] + (200,), width=max(2, size // 18)) + + # Inner diamond + d = s * 0.42 + diamond = [ + (cx, cy - d), + (cx + d, cy), + (cx, cy + d), + (cx - d, cy), + ] + draw.polygon(diamond, fill=ACCENT[:3] + (230,)) + + # Tiny highlight dot + h = s * 0.12 + draw.ellipse([cx - h, cy - h - d * 0.28, + cx + h, cy + h - d * 0.28], + fill=(255, 255, 255, 180)) + + return img + +# ── Logo ─────────────────────────────────────────────────────────────────────── + +def build_logo(width=600, height=180, scale=1): + W, H = width * scale, height * scale + img = Image.new("RGBA", (W, H), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + pad = int(16 * scale) + radius = int(20 * scale) + + # Background card + draw_rounded_rect(draw, + [pad, pad, W - pad, H - pad], + radius=radius, + fill=BG, + outline=BORDER[:3], + width=max(1, scale)) + + icon_cx = int(H * 0.5) + icon_cy = int(H * 0.5) + icon_r = int(H * 0.28) + + # Glow clipped to card bounds + card = img.crop([pad, pad, W - pad, H - pad]) + glow_cx = icon_cx - pad + glow_cy = icon_cy - pad + card = add_glow(card, (glow_cx, glow_cy), int(icon_r * 1.6), ACCENT, 0.28) + img.paste(card, (pad, pad)) + + draw = ImageDraw.Draw(img) + + # Icon mark + draw_icon_mark(draw, img, icon_cx, icon_cy, icon_r) + draw = ImageDraw.Draw(img) # refresh after composite + + # Text area + text_x = icon_cx + icon_r + int(22 * scale) + brand_font = make_font(int(38 * scale)) + sub_font = make_font_regular(int(16 * scale)) + + brand_text = "YourBrand" + sub_text = "Your tagline here" + + # Brand name + draw.text((text_x, int(H * 0.22)), brand_text, + font=brand_font, fill=TEXT_PRI) + + # Accent underline + bbox = draw.textbbox((text_x, int(H * 0.22)), brand_text, font=brand_font) + uw = bbox[2] - bbox[0] + uy = bbox[3] + int(4 * scale) + + draw.rectangle([text_x, uy, text_x + uw, uy + max(2, int(3 * scale))], + fill=ACCENT[:3]) + + # Subtitle + draw.text((text_x, int(H * 0.63)), sub_text, + font=sub_font, fill=TEXT_MUTE) + + # Thin right-side accent bar + bar_x = W - pad - max(3, int(4 * scale)) + draw.rectangle([bar_x, pad + radius, + bar_x + max(3, int(4 * scale)), H - pad - radius], + fill=ACCENT[:3] + (120,)) + + return img + +# ── Favicon mark ─────────────────────────────────────────────────────────────── + +def build_favicon_image(size): + img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + + pad = max(1, size // 24) + radius = max(2, size // 8) + + # Background + draw_rounded_rect(draw, + [pad, pad, size - pad, size - pad], + radius=radius, + fill=BG, + outline=BORDER[:3], + width=max(1, size // 24)) + + cx, cy = size // 2, size // 2 + icon_r = int(size * 0.30) + + card = img.crop([pad, pad, size - pad, size - pad]) + card = add_glow(card, (cx - pad, cy - pad), int(icon_r * 1.6), ACCENT, 0.28) + img.paste(card, (pad, pad)) + + draw = ImageDraw.Draw(img) + draw_icon_mark(draw, img, cx, cy, icon_r) + + return img + +# ── ICO writer ───────────────────────────────────────────────────────────────── + +def save_ico(images_dict, path): + """ + images_dict: {size: PIL.Image, ...} e.g. {16: img16, 32: img32, 48: img48} + """ + sizes = sorted(images_dict.keys()) + entries = [] + png_data_list = [] + + for s in sizes: + img = images_dict[s].convert("RGBA") + buf = io.BytesIO() + img.save(buf, format="PNG") + data = buf.getvalue() + png_data_list.append(data) + entries.append((s, len(data))) + + # ICO header: RESERVED(2) TYPE(2) COUNT(2) + header = struct.pack("