feat(splash+icon): фон иконки-градиент под тему + темо-зависимый сплэш

- Подложка adaptive-иконки: градиент под акцент темы + радиальное свечение + мягкая
  тень от логотипа (ic_bg_<тема>, было плоским цветом). Иконку-лого не трогал.
- Сплэш под выбранную тему: системный сплэш Android 12+ нельзя перекрасить под выбор
  пользователя (alias-тема на ColorOS игнорится), поэтому системный = просто тёмный
  (splash_transparent), а красивый сплэш рисуем сами на Compose (SplashOverlay):
  3D-лого + акцентное свечение + тень + анимация, цвет берём из текущей темы.
- Тему на старте читаем синхронно из SharedPreferences (мгновенно, без блокировки кадра).
- Ускорен холодный старт до первого кадра 1.48с→1.11с: сплэш рисуется на первом
  дешёвом кадре, тяжёлый контент (ViewModels/плеер) композится под ним; старт
  PlayerService уведён с критического пути. Остаток — оверхед debug-сборки.
This commit is contained in:
nk
2026-06-07 16:57:01 +03:00
parent 01729e0a52
commit d63c1d4187
40 changed files with 293 additions and 29 deletions

73
design/logos/gen_bg.py Normal file
View File

@@ -0,0 +1,73 @@
# -*- coding: utf-8 -*-
"""Фоны adaptive-иконки: градиент под акцент темы + радиальное свечение + тень от логотипа."""
from PIL import Image, ImageDraw, ImageFilter
import numpy as np, os
TH = r"C:\radiOLA\design\logos\gen\themed"
DRW = r"C:\radiOLA\app\src\main\res\drawable-nodpi"
PREV = os.environ["LOCALAPPDATA"] + r"\Temp\radiola_bg_preview.png"
S = 432
THEMES = [
("forest", (0xA8,0xE0,0x5F), (0x0C,0x14,0x10)),
("ocean", (0x4F,0xD6,0xE0), (0x0A,0x0F,0x1A)),
("sunset", (0xFF,0x8A,0x5B), (0x1A,0x0F,0x0C)),
("amethyst",(0xB3,0x88,0xFF), (0x12,0x0E,0x1A)),
("neon", (0xFF,0x4D,0x9D), (0x0D,0x0A,0x12)),
("amber", (0xFF,0xC2,0x47), (0x14,0x11,0x0A)),
("ice", (0x7F,0xB3,0xFF), (0x0C,0x10,0x14)),
("rose", (0xFF,0x7E,0xA8), (0x16,0x0E,0x12)),
]
def lerp(a,b,t): return tuple(a[i]+(b[i]-a[i])*t for i in range(3))
yy,xx = np.mgrid[0:S,0:S]
cx,cy=S*0.5,S*0.46
rad = np.sqrt((xx-cx)**2+(yy-cy)**2)/(S*0.62) # 0 в центре → ~1 к краю
diag = (xx+yy)/(2*S) # 0 верх-лево → 1 низ-право
def make_bg(accent, bg):
acc=np.array(accent,float); base=np.array(bg,float)
# 1) база: лёгкий диагональный градиент (верх чуть светлее с оттенком акцента, низ темнее)
top = lerp(bg, accent, 0.10); top=np.array([min(255,c*1.15) for c in top])
bot = np.array([c*0.7 for c in bg])
g = diag[...,None]
img = top*(1-g) + bot*g
# 2) радиальное акцентное свечение в центре
glow = np.clip(1-rad,0,1)**1.7
img = img + (acc-img)*(glow[...,None]*0.30)
# 3) виньетка по углам
vig = np.clip(rad-0.6,0,1)
img = img*(1-vig[...,None]*0.35)
return Image.fromarray(np.clip(img,0,255).astype("uint8"),"RGB").convert("RGBA")
def r_shadow(theme):
"""Мягкая тень от логотипа (силуэт R), запечённая в фон."""
logo=Image.open(os.path.join(TH,f"logo_{theme}.png")).convert("RGBA")
logo=logo.crop(logo.getbbox())
target=196; sc=target/max(logo.size)
logo=logo.resize((int(logo.width*sc),int(logo.height*sc)))
sh=Image.new("RGBA",(S,S),(0,0,0,0))
# силуэт из альфы
sil=Image.new("RGBA",(S,S),(0,0,0,0))
a=logo.split()[3]
blk=Image.new("RGBA",logo.size,(0,0,0,150))
sil.paste(blk,((S-logo.width)//2+7,(S-logo.height)//2+14),a) # сдвиг вниз-вправо
sil=sil.filter(ImageFilter.GaussianBlur(16))
return sil
cols=4; rows=2; pad=16
sheet=Image.new("RGB",(cols*S//2+(cols+1)*pad, rows*S//2+(rows+1)*pad),(28,28,32))
for i,(name,acc,bg) in enumerate(THEMES):
base=make_bg(acc,bg)
base.alpha_composite(r_shadow(name))
base.convert("RGB").save(os.path.join(DRW,f"ic_bg_{name}.png"))
# превью: фон + foreground + круглая маска
fg=Image.open(os.path.join(DRW,f"ic_fg_{name}.png")).convert("RGBA")
full=base.copy(); full.alpha_composite(fg)
mask=Image.new("L",(S,S),0); ImageDraw.Draw(mask).ellipse([0,0,S,S],fill=255)
out=Image.new("RGB",(S,S),(28,28,32)); out.paste(full.convert("RGB"),(0,0),mask)
out=out.resize((S//2,S//2))
r=i//cols; c=i%cols; x=pad+c*(S//2+pad); y=pad+r*(S//2+pad)
sheet.paste(out,(x,y))
sheet.save(PREV)
print("bg generated + preview", PREV)