Логотип: монограмма-R пользователя отрендерена в матовый 3D через routerai (gpt-5.4-image), один мастер перекрашен под 8 тем (recolor по яркости, форма идентична). - Внутри приложения: AppMark показывает перекрашенный 3D-логотип текущей палитры (LocalThemePalette + ThemePalette.logoRes, drawable logo_<тема>). - Иконка лаунчера следует теме: 8 adaptive-иконок (ic_fg_<тема> + ic_bg_<тема>) и 8 activity-alias в манифесте; LauncherIconManager включает alias выбранной темы, гасит остальные (ровно один активен, guard против лишних миганий). Переключение — в MainActivity по LaunchedEffect(paletteId). На ColorOS иконка может обновляться с задержкой — особенность системы. Скрипты генерации в design/logos (ключ routerai — вне репо, ~/.routerai_key).
153 lines
8.5 KiB
Python
153 lines
8.5 KiB
Python
# -*- coding: utf-8 -*-
|
|
import base64, os
|
|
FONTS = r"C:\Users\nk\.claude\skills\canvas-design\canvas-fonts"
|
|
OUT = r"C:\radiOLA\design\logos"
|
|
|
|
def b64(p):
|
|
with open(p, "rb") as f:
|
|
return base64.b64encode(f.read()).decode()
|
|
|
|
erica = b64(os.path.join(FONTS, "EricaOne-Regular.ttf"))
|
|
outfit = b64(os.path.join(FONTS, "Outfit-Bold.ttf"))
|
|
|
|
FONT_CSS = f"""
|
|
@font-face {{ font-family:'Erica'; src:url(data:font/ttf;base64,{erica}) format('truetype'); }}
|
|
@font-face {{ font-family:'Outfit'; src:url(data:font/ttf;base64,{outfit}) format('truetype'); }}
|
|
"""
|
|
|
|
# Изометрическая экструзия вниз-вправо: сочная зелёная боковая грань (как материал в тени)
|
|
def extr(steps=18, dx=1.0, dy=1.1):
|
|
parts=[]
|
|
for i in range(1, steps+1):
|
|
t=i/steps
|
|
# грань — насыщенный зелёный, плавно темнеющий ко дну (не чёрный)
|
|
r=int(0x52-0x22*t); g=int(0x8c-0x40*t); b=int(0x2c-0x14*t)
|
|
parts.append(f"{dx*i:.1f}px {dy*i:.1f}px 0 rgb({r},{g},{b})")
|
|
parts.append(f"{dx*steps+6:.0f}px {dy*steps+12:.0f}px 22px rgba(0,0,0,.5)") # контактная тень
|
|
return ",".join(parts)
|
|
|
|
EXTR = extr()
|
|
GLOW = "#C2F25B"
|
|
|
|
def motif_broadcast():
|
|
cx,cy=408,116; arcs=""
|
|
for i,r in enumerate([36,66,98]):
|
|
op=0.95-0.22*i
|
|
arcs+=f'<path d="M {cx-r} {cy} A {r} {r} 0 0 1 {cx} {cy-r}" fill="none" stroke="{GLOW}" stroke-width="{11-2*i}" stroke-linecap="round" opacity="{op}"/>'
|
|
return f'<svg width="512" height="512" style="position:absolute;inset:0">{arcs}<circle cx="{cx}" cy="{cy}" r="11" fill="{GLOW}"/></svg>'
|
|
|
|
def motif_wave():
|
|
hs=[28,58,96,134,168,134,96,168,120,70,36]; bw=22; gap=11
|
|
total=len(hs)*(bw+gap)-gap; x0=(512-total)/2; base=452
|
|
grad='<defs><linearGradient id="wg" x1="0" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#E9FF95"/><stop offset="1" stop-color="#6FA53C"/></linearGradient></defs>'
|
|
bars=""
|
|
for i,h in enumerate(hs):
|
|
x=x0+i*(bw+gap)
|
|
bars+=f'<rect x="{x:.0f}" y="{base-h}" width="{bw}" height="{h}" rx="10" fill="url(#wg)"/>'
|
|
return f'<svg width="512" height="512" style="position:absolute;inset:0">{grad}{bars}</svg>'
|
|
|
|
def motif_vinyl():
|
|
cx,cy=132,372; grooves=""
|
|
disc=f'<circle cx="{cx}" cy="{cy}" r="138" fill="#0c1810" stroke="#2b4321" stroke-width="2"/>'
|
|
for r in range(128,44,-15):
|
|
grooves+=f'<circle cx="{cx}" cy="{cy}" r="{r}" fill="none" stroke="#23381c" stroke-width="3"/>'
|
|
label=f'<circle cx="{cx}" cy="{cy}" r="38" fill="{GLOW}"/><circle cx="{cx}" cy="{cy}" r="7" fill="#0c1410"/>'
|
|
shine=f'<path d="M {cx-92} {cy-92} A 130 130 0 0 1 {cx+30} {cy-128}" fill="none" stroke="rgba(255,255,255,.16)" stroke-width="10" stroke-linecap="round"/>'
|
|
return f'<svg width="512" height="512" style="position:absolute;inset:0">{disc}{grooves}{label}{shine}</svg>'
|
|
|
|
def motif_antenna():
|
|
x,y=398,158
|
|
mast=f'<path d="M {x} {y} L {x-18} {y+128} M {x} {y} L {x+18} {y+128} M {x-10} {y+62} L {x+10} {y+62}" stroke="#d6 efb0" stroke-width="8" stroke-linecap="round" fill="none"/>'.replace("ef","ef")
|
|
mast=f'<path d="M {x} {y} L {x-18} {y+128} M {x} {y} L {x+18} {y+128} M {x-10} {y+62} L {x+10} {y+62}" stroke="#d6efb0" stroke-width="8" stroke-linecap="round" fill="none"/>'
|
|
waves=""
|
|
for i,r in enumerate([30,52,76]):
|
|
op=0.9-0.22*i; sw=8-2*i
|
|
waves+=f'<path d="M {x-r} {y-8} A {r} {r} 0 0 1 {x-r*0.18:.0f} {y-8-r*0.96:.0f}" fill="none" stroke="{GLOW}" stroke-width="{sw}" stroke-linecap="round" opacity="{op}"/>'
|
|
waves+=f'<path d="M {x+r} {y-8} A {r} {r} 0 0 0 {x+r*0.18:.0f} {y-8-r*0.96:.0f}" fill="none" stroke="{GLOW}" stroke-width="{sw}" stroke-linecap="round" opacity="{op}"/>'
|
|
tip=f'<circle cx="{x}" cy="{y-8}" r="9" fill="{GLOW}"/>'
|
|
return f'<svg width="512" height="512" style="position:absolute;inset:0">{waves}{mast}{tip}</svg>'
|
|
|
|
def motif_tuner():
|
|
y=452; x0=92; x1=420; n=23; ticks=""
|
|
for i in range(n):
|
|
x=x0+(x1-x0)*i/(n-1); big=(i%4==0); h=24 if big else 13
|
|
ticks+=f'<line x1="{x:.0f}" y1="{y-h}" x2="{x:.0f}" y2="{y}" stroke="#8aa86c" stroke-width="{3 if big else 2}" stroke-linecap="round"/>'
|
|
base=f'<line x1="{x0}" y1="{y}" x2="{x1}" y2="{y}" stroke="#36532c" stroke-width="3" stroke-linecap="round"/>'
|
|
kx=x0+(x1-x0)*0.63
|
|
knob=f'<line x1="{kx:.0f}" y1="{y-38}" x2="{kx:.0f}" y2="{y+10}" stroke="{GLOW}" stroke-width="7" stroke-linecap="round"/><circle cx="{kx:.0f}" cy="{y-46}" r="12" fill="{GLOW}"/>'
|
|
return f'<svg width="512" height="512" style="position:absolute;inset:0">{base}{ticks}{knob}</svg>'
|
|
|
|
VARIANTS = [
|
|
("01","Эфир","дуги вещания",motif_broadcast(),0),
|
|
("02","Волна","эквалайзер",motif_wave(),-26),
|
|
("03","Винил","пластинка + R",motif_vinyl(),-6),
|
|
("04","Антенна","вышка + сигнал",motif_antenna(),6),
|
|
("05","Тюнер","частотная шкала",motif_tuner(),-30),
|
|
]
|
|
|
|
def tile(motif, shift_y):
|
|
return f"""
|
|
<div class="tile">
|
|
<div class="bg"></div><div class="glow"></div>
|
|
{motif}
|
|
<div class="rwrap" style="transform:translateY({shift_y}px)">
|
|
<div class="r">R</div>
|
|
</div>
|
|
</div>"""
|
|
|
|
TILE_CSS = f"""
|
|
*{{margin:0;padding:0;box-sizing:border-box}}
|
|
{FONT_CSS}
|
|
.tile{{position:relative;width:512px;height:512px;overflow:hidden;border-radius:22.5%;background:#0c1410}}
|
|
.bg{{position:absolute;inset:0;background:radial-gradient(125% 105% at 50% -5%, #1a2c1d 0%, #0e1712 50%, #060c08 100%);}}
|
|
.glow{{position:absolute;left:48%;top:46%;width:88%;height:88%;transform:translate(-50%,-50%);
|
|
background:radial-gradient(circle, rgba(176,236,98,.42) 0%, rgba(140,210,70,.12) 38%, rgba(168,224,95,0) 64%);}}
|
|
.rwrap{{position:absolute;inset:0;display:flex;align-items:center;justify-content:center}}
|
|
.r{{font-family:'Erica';font-size:284px;line-height:.78;
|
|
background:
|
|
linear-gradient(176deg, rgba(255,255,255,.72) 0%, rgba(255,255,255,.16) 22%, rgba(255,255,255,0) 40%),
|
|
linear-gradient(150deg,#F0FFAE 0%,#CFF96E 24%,#B3EC5E 46%,#9BD94F 68%,#84C53F 100%);
|
|
-webkit-background-clip:text;background-clip:text;color:transparent;
|
|
text-shadow:{EXTR};}}
|
|
"""
|
|
|
|
for num,name,sub,motif,sy in VARIANTS:
|
|
html=f"""<!doctype html><html><head><meta charset="utf-8"><style>
|
|
html,body{{background:#0c1410;width:512px;height:512px;overflow:hidden}}{TILE_CSS}</style></head>
|
|
<body>{tile(motif,sy)}</body></html>"""
|
|
open(os.path.join(OUT,f"icon_{num}.html"),"w",encoding="utf-8").write(html)
|
|
|
|
cards=""
|
|
for num,name,sub,motif,sy in VARIANTS:
|
|
cards+=f"""<div class="card"><div class="num">{num}</div>
|
|
<div class="icon"><div class="scaler">{tile(motif,sy)}</div></div>
|
|
<div class="cap"><b>{name}</b><span>{sub}</span></div></div>"""
|
|
|
|
sheet=f"""<!doctype html><html><head><meta charset="utf-8"><style>
|
|
{TILE_CSS}
|
|
body{{background:#060c08;width:1760px;min-height:1080px;font-family:'Outfit';
|
|
background-image:radial-gradient(130% 80% at 50% -12%, #13201610 0%, #060c08 60%);}}
|
|
.head{{padding:64px 80px 6px;display:flex;align-items:baseline;gap:26px;position:relative}}
|
|
.head h1{{font-family:'Erica';font-size:76px;color:#fff;letter-spacing:1px;line-height:1}}
|
|
.head h1 span{{color:#A8E05F}}
|
|
.head .t{{color:#85a08b;font-size:22px;font-weight:600}}
|
|
.movement{{position:absolute;right:80px;top:80px;color:#41604a;font-size:15px;letter-spacing:4px;text-transform:uppercase}}
|
|
.grid{{display:flex;flex-wrap:wrap;gap:40px;justify-content:center;padding:48px 70px 24px}}
|
|
.card{{width:300px;position:relative}}
|
|
.icon{{width:300px;height:300px;border-radius:67px;overflow:hidden;
|
|
box-shadow:0 26px 64px rgba(0,0,0,.6),0 0 0 1px rgba(168,224,95,.12);}}
|
|
.scaler{{width:512px;height:512px;transform:scale(.5859);transform-origin:top left}}
|
|
.num{{position:absolute;top:-4px;left:4px;color:#36513a;font-family:'Erica';font-size:32px;z-index:3}}
|
|
.cap{{margin-top:20px;text-align:center}}
|
|
.cap b{{display:block;color:#eef8e2;font-size:23px;font-weight:700}}
|
|
.cap span{{display:block;color:#76916f;font-size:15px;margin-top:3px}}
|
|
.foot{{text-align:center;color:#41604a;font-size:14px;letter-spacing:5px;text-transform:uppercase;padding:20px 0 50px}}
|
|
</style></head><body>
|
|
<div class="head"><h1>radi<span>OLA</span></h1><div class="t">5 концепций знака · объёмная «R» · радио-мотив</div>
|
|
<div class="movement">Lumen Relay</div></div>
|
|
<div class="grid">{cards}</div>
|
|
<div class="foot">— выбери номер, доведу до финала —</div>
|
|
</body></html>"""
|
|
open(os.path.join(OUT,"sheet.html"),"w",encoding="utf-8").write(sheet)
|
|
print("ok")
|