diff --git a/.gitignore b/.gitignore index c9d2543..beeecff 100644 --- a/.gitignore +++ b/.gitignore @@ -25,3 +25,7 @@ app/build/ # Kotlin .kotlin/ + +# Логотип: промежуточные генерации (тяжёлые), ключ — вне репо +design/logos/gen/ +design/logos/ref_*.png diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e6377e0..a3e1fb7 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -36,12 +36,50 @@ android:name=".MainActivity" android:exported="true" android:theme="@style/Theme.Radiola.Splash" - android:configChanges="orientation|screenSize|smallestScreenSize"> - - - - - + android:configChanges="orientation|screenSize|smallestScreenSize" /> + + + + + + + + + + + + + + + + + + + + + + + + + + R.drawable.logo_forest + OCEAN -> R.drawable.logo_ocean + SUNSET -> R.drawable.logo_sunset + AMETHYST -> R.drawable.logo_amethyst + NEON -> R.drawable.logo_neon + AMBER -> R.drawable.logo_amber + ICE -> R.drawable.logo_ice + ROSE -> R.drawable.logo_rose + } + companion object { /** Палитра по сохранённому id (фолбэк — «Лес»). */ fun fromId(id: String?): ThemePalette = entries.firstOrNull { it.id == id } ?: FOREST diff --git a/app/src/main/java/com/radiola/ui/theme/Theme.kt b/app/src/main/java/com/radiola/ui/theme/Theme.kt index b26527b..729d7ed 100644 --- a/app/src/main/java/com/radiola/ui/theme/Theme.kt +++ b/app/src/main/java/com/radiola/ui/theme/Theme.kt @@ -33,10 +33,14 @@ data class RadiolaColors( // По умолчанию — фирменная палитра «Лес». val LocalRadiolaColors = staticCompositionLocalOf { ThemePalette.FOREST.colors } +// Текущая палитра целиком — чтобы лого/иконка выбирали свой перекрашенный вариант. +val LocalThemePalette = staticCompositionLocalOf { ThemePalette.FOREST } object RadiolaTheme { val colors: RadiolaColors @Composable get() = LocalRadiolaColors.current + val palette: ThemePalette + @Composable get() = LocalThemePalette.current } // Material ColorScheme из наших токенов — чтобы Material-компоненты тоже следовали палитре. @@ -64,7 +68,10 @@ fun RadiolaTheme( ) { // Приложение всегда тёмное; палитра выбирается пользователем. val colors = palette.colors - CompositionLocalProvider(LocalRadiolaColors provides colors) { + CompositionLocalProvider( + LocalRadiolaColors provides colors, + LocalThemePalette provides palette + ) { MaterialTheme( colorScheme = schemeOf(colors), typography = Typography, diff --git a/app/src/main/java/com/radiola/util/LauncherIconManager.kt b/app/src/main/java/com/radiola/util/LauncherIconManager.kt new file mode 100644 index 0000000..cabff4d --- /dev/null +++ b/app/src/main/java/com/radiola/util/LauncherIconManager.kt @@ -0,0 +1,66 @@ +package com.radiola.util + +import android.content.ComponentName +import android.content.Context +import android.content.pm.PackageManager +import com.radiola.ui.theme.ThemePalette +import dagger.hilt.android.qualifiers.ApplicationContext +import javax.inject.Inject +import javax.inject.Singleton + +/** + * Переключает иконку лаунчера под выбранную цветовую тему. Реализовано через + * activity-alias в манифесте (по одному на тему): включаем alias выбранной темы и + * выключаем остальные — ровно ОДИН активен всегда (иначе приложение пропадёт с + * рабочего стола). На части лаунчеров/ColorOS иконка обновляется с задержкой — + * это особенность системы, не баг. + */ +@Singleton +class LauncherIconManager @Inject constructor( + @ApplicationContext private val context: Context +) { + private fun aliasSuffix(p: ThemePalette) = when (p) { + ThemePalette.FOREST -> "Forest" + ThemePalette.OCEAN -> "Ocean" + ThemePalette.SUNSET -> "Sunset" + ThemePalette.AMETHYST -> "Amethyst" + ThemePalette.NEON -> "Neon" + ThemePalette.AMBER -> "Amber" + ThemePalette.ICE -> "Ice" + ThemePalette.ROSE -> "Rose" + } + + private fun component(p: ThemePalette) = + ComponentName(context.packageName, "${context.packageName}.MainAlias${aliasSuffix(p)}") + + /** Меняет иконку только если она ещё не соответствует теме (без лишних миганий). */ + fun applyIfNeeded(palette: ThemePalette) { + val prefs = context.getSharedPreferences("radiola_prefs", Context.MODE_PRIVATE) + // Дефолт манифеста — forest (его alias enabled=true). + if (prefs.getString("icon_alias", "forest") == palette.id) return + apply(palette) + prefs.edit().putString("icon_alias", palette.id).apply() + } + + private fun apply(palette: ThemePalette) { + val pm = context.packageManager + // Сначала включаем нужный — чтобы ни на миг не остаться без launcher-иконки. + runCatching { + pm.setComponentEnabledSetting( + component(palette), + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + } + for (p in ThemePalette.entries) { + if (p == palette) continue + runCatching { + pm.setComponentEnabledSetting( + component(p), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } + } +} diff --git a/app/src/main/res/drawable-nodpi/ic_fg_amber.png b/app/src/main/res/drawable-nodpi/ic_fg_amber.png new file mode 100644 index 0000000..f61a85d Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_amber.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_amethyst.png b/app/src/main/res/drawable-nodpi/ic_fg_amethyst.png new file mode 100644 index 0000000..4a6d912 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_amethyst.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_forest.png b/app/src/main/res/drawable-nodpi/ic_fg_forest.png new file mode 100644 index 0000000..13985e4 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_forest.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_ice.png b/app/src/main/res/drawable-nodpi/ic_fg_ice.png new file mode 100644 index 0000000..b0b9d48 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_ice.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_neon.png b/app/src/main/res/drawable-nodpi/ic_fg_neon.png new file mode 100644 index 0000000..e3d00a7 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_neon.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_ocean.png b/app/src/main/res/drawable-nodpi/ic_fg_ocean.png new file mode 100644 index 0000000..ba07bdc Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_ocean.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_rose.png b/app/src/main/res/drawable-nodpi/ic_fg_rose.png new file mode 100644 index 0000000..89edb71 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_rose.png differ diff --git a/app/src/main/res/drawable-nodpi/ic_fg_sunset.png b/app/src/main/res/drawable-nodpi/ic_fg_sunset.png new file mode 100644 index 0000000..f5b4479 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/ic_fg_sunset.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_amber.png b/app/src/main/res/drawable-nodpi/logo_amber.png new file mode 100644 index 0000000..75072c0 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_amber.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_amethyst.png b/app/src/main/res/drawable-nodpi/logo_amethyst.png new file mode 100644 index 0000000..fdf8d20 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_amethyst.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_forest.png b/app/src/main/res/drawable-nodpi/logo_forest.png new file mode 100644 index 0000000..13e1986 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_forest.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_ice.png b/app/src/main/res/drawable-nodpi/logo_ice.png new file mode 100644 index 0000000..230ff0a Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_ice.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_neon.png b/app/src/main/res/drawable-nodpi/logo_neon.png new file mode 100644 index 0000000..41bb34a Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_neon.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_ocean.png b/app/src/main/res/drawable-nodpi/logo_ocean.png new file mode 100644 index 0000000..3dd2d3a Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_ocean.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_rose.png b/app/src/main/res/drawable-nodpi/logo_rose.png new file mode 100644 index 0000000..7fba574 Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_rose.png differ diff --git a/app/src/main/res/drawable-nodpi/logo_sunset.png b/app/src/main/res/drawable-nodpi/logo_sunset.png new file mode 100644 index 0000000..ce0cb6d Binary files /dev/null and b/app/src/main/res/drawable-nodpi/logo_sunset.png differ diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml new file mode 100644 index 0000000..279f7bc --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber_round.xml new file mode 100644 index 0000000..279f7bc --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amethyst.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amethyst.xml new file mode 100644 index 0000000..8afb6f2 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amethyst.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amethyst_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amethyst_round.xml new file mode 100644 index 0000000..8afb6f2 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_amethyst_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest.xml new file mode 100644 index 0000000..6840300 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest_round.xml new file mode 100644 index 0000000..6840300 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_forest_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice.xml new file mode 100644 index 0000000..470f0e1 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice_round.xml new file mode 100644 index 0000000..470f0e1 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml new file mode 100644 index 0000000..ca6c953 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon_round.xml new file mode 100644 index 0000000..ca6c953 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml new file mode 100644 index 0000000..7d788fa --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean_round.xml new file mode 100644 index 0000000..7d788fa --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose.xml new file mode 100644 index 0000000..1e2976d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose_round.xml new file mode 100644 index 0000000..1e2976d --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset.xml new file mode 100644 index 0000000..e145e77 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset_round.xml new file mode 100644 index 0000000..e145e77 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_sunset_round.xml @@ -0,0 +1,5 @@ + + + + + diff --git a/app/src/main/res/values/ic_colors.xml b/app/src/main/res/values/ic_colors.xml new file mode 100644 index 0000000..bca7175 --- /dev/null +++ b/app/src/main/res/values/ic_colors.xml @@ -0,0 +1,11 @@ + + + #0C1410 + #0A0F1A + #1A0F0C + #120E1A + #0D0A12 + #14110A + #0C1014 + #160E12 + diff --git a/design/logos/PHILOSOPHY.md b/design/logos/PHILOSOPHY.md new file mode 100644 index 0000000..6572532 --- /dev/null +++ b/design/logos/PHILOSOPHY.md @@ -0,0 +1,13 @@ +# Lumen Relay — дизайн-философия марки radiOLA + +**Движение:** *Lumen Relay* — «светящаяся ретрансляция». Свет как сигнал, форма как звук. + +Идентичность radiOLA рождается из одной идеи: **невидимая волна, ставшая осязаемым объёмом**. Эфир нельзя увидеть — но его можно вылепить. Буква «R» здесь не надпись, а маленькая скульптура: толстая, округлая, отлитая будто из тёплого зелёного стекла и подсвеченная изнутри. Это не плоский знак, а предмет, который хочется взять в руку — глянцевый, дружелюбный, уверенный. Каждая грань должна выглядеть так, словно над ней работали часами: выверенная фаска, честная тень, чистый блик. + +**Свет и материал.** Палитра — единый лаймово-зелёный луч от почти белого верха (#D8FF7A) к глубокому травяному низу (#5E9A30), посаженный на угольно-зелёную ночь (#0C1410). Верх каждой формы ловит свет, низ уходит в тень — так рождается объём без единого лишнего цвета. Материал читается как полупрозрачный леденец: сочный, влажный, с одним точным бликом, а не десятком случайных. + +**Объём и грань.** Глубина строится честно — изометрический сдвиг вниз-вправо, боковая грань на тон темнее, мягкая контактная тень под объектом. Никакого дешёвого «выдавливания фильтром»: экструзия выверена по шагам, как у мастера, который чувствует, где металл должен поймать свет. Скруглённые углы, пухлые штрихи — характер мультяшный, но исполнение ювелирное. + +**Ритм радио.** Сигнал входит в композицию минимальным жестом — концентрические дуги эфира, волна-эквалайзер, бороздки винила, штрихи частотной шкалы. Один мотив на знак, не больше: радио должно *чувствоваться*, а не объясняться. Эти элементы — тихий аккомпанемент главной форме, тонкие и точные, как засечки на приборе. + +**Композиция и тишина.** Каждая иконка — квадрат с воздухом по краям, центрированная масса, спокойный тёмный фон с лёгким сиянием за объектом. Текст почти отсутствует: только имя бренда шёпотом под формой. Информация живёт в свете и объёме, не в словах. Результат должен выглядеть как работа дизайнера на вершине ремесла — кристально чистая, без единой случайной точки, готовая к витрине. diff --git a/design/logos/build.py b/design/logos/build.py new file mode 100644 index 0000000..d127d60 --- /dev/null +++ b/design/logos/build.py @@ -0,0 +1,152 @@ +# -*- 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'' + return f'{arcs}' + +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='' + bars="" + for i,h in enumerate(hs): + x=x0+i*(bw+gap) + bars+=f'' + return f'{grad}{bars}' + +def motif_vinyl(): + cx,cy=132,372; grooves="" + disc=f'' + for r in range(128,44,-15): + grooves+=f'' + label=f'' + shine=f'' + return f'{disc}{grooves}{label}{shine}' + +def motif_antenna(): + x,y=398,158 + mast=f''.replace("ef","ef") + mast=f'' + waves="" + for i,r in enumerate([30,52,76]): + op=0.9-0.22*i; sw=8-2*i + waves+=f'' + waves+=f'' + tip=f'' + return f'{waves}{mast}{tip}' + +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'' + base=f'' + kx=x0+(x1-x0)*0.63 + knob=f'' + return f'{base}{ticks}{knob}' + +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""" +
+
+ {motif} +
+
R
+
+
""" + +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""" + {tile(motif,sy)}""" + 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"""
{num}
+
{tile(motif,sy)}
+
{name}{sub}
""" + +sheet=f""" +

radiOLA

5 концепций знака · объёмная «R» · радио-мотив
+
Lumen Relay
+
{cards}
+
— выбери номер, доведу до финала —
+""" +open(os.path.join(OUT,"sheet.html"),"w",encoding="utf-8").write(sheet) +print("ok") diff --git a/design/logos/recolor.py b/design/logos/recolor.py new file mode 100644 index 0000000..c1885cb --- /dev/null +++ b/design/logos/recolor.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +"""Один мастер-логотип → 8 тем. Изолируем букву по насыщенности, перекрашиваем по яркости.""" +from PIL import Image, ImageOps, ImageFilter, ImageDraw, ImageFont +import os + +GEN = r"C:\radiOLA\design\logos\gen" +MASTER = os.path.join(GEN, "master_matte_0.png") +OUTDIR = os.path.join(GEN, "themed"); os.makedirs(OUTDIR, exist_ok=True) + +# (id, акцент, фон темы) — как в ThemePalette +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(int(a[i]+(b[i]-a[i])*t) for i in range(3)) + +im = Image.open(MASTER).convert("RGB") +hsv = im.convert("HSV") +H,S,V = hsv.split() + +# --- альфа-маска: буква насыщенная, шахматка-фон серый (низкая S) --- +alpha = S.point(lambda s: 0 if s < 40 else min(255, int((s-40)*4))) +alpha = alpha.filter(ImageFilter.MaxFilter(3)).filter(ImageFilter.GaussianBlur(0.8)) + +# --- яркость буквы (3D-тени), нормализуем в рабочий диапазон --- +L = ImageOps.grayscale(im) +# статистика только по букве +import numpy as np +la = np.asarray(L, dtype=float); aa = np.asarray(alpha, dtype=float)/255.0 +mask = aa > 0.5 +if mask.sum() > 0: + lo, hi = np.percentile(la[mask], 4), np.percentile(la[mask], 96) +else: + lo, hi = 40, 150 +la = np.clip((la - lo)/(hi - lo), 0, 1) # 0..1 нормализованная яркость буквы +Ln = Image.fromarray((la*255).astype("uint8"), "L") + +def themed(accent): + dark = lerp(accent, (0,0,0), 0.55) # тень/боковая грань + light = lerp(accent, (255,255,255), 0.30) # свет фронта + col = ImageOps.colorize(Ln, black=dark, white=light, mid=accent).convert("RGBA") + col.putalpha(alpha) + return col + +# отдельные прозрачные PNG + превью-лист на фоне темы +cell=300; pad=26; cols=4; rows=2 +W=cols*cell+(cols+1)*pad; Hh=rows*(cell+40)+(rows+1)*pad+56 +sheet=Image.new("RGB",(W,Hh),(8,12,10)); d=ImageDraw.Draw(sheet) +def f(s): + try: return ImageFont.truetype(r"C:\Windows\Fonts\segoeui.ttf",s) + except: return ImageFont.load_default() +d.text((pad,18),"radiOLA — один логотип, перекрашен под 8 тем",font=f(26),fill=(234,246,223)) + +for i,(name,acc,bg) in enumerate(THEMES): + logo = themed(acc) + logo.save(os.path.join(OUTDIR, f"logo_{name}.png")) + # превью на фоне темы + tile = Image.new("RGBA",(cell,cell), bg+(255,)) + lg = logo.resize((int(cell*0.74),)*2) + tile.alpha_composite(lg, ((cell-lg.width)//2,(cell-lg.height)//2)) + r=i//cols; c=i%cols; x=pad+c*(cell+pad); y=56+pad+r*(cell+40+pad) + sheet.paste(tile.convert("RGB"),(x,y)) + d.text((x+6,y+cell+8),name,font=f(20),fill=(150,170,150)) + +out=os.path.join(GEN,"_themed_sheet.png"); sheet.save(out) +import shutil; shutil.copy(out, os.path.join(os.environ["LOCALAPPDATA"],"Temp","radiola_themed.png")) +print("saved", len(THEMES), "themed logos ->", OUTDIR) diff --git a/design/logos/routerai.py b/design/logos/routerai.py new file mode 100644 index 0000000..6372967 --- /dev/null +++ b/design/logos/routerai.py @@ -0,0 +1,137 @@ +# -*- coding: utf-8 -*- +"""Генерация логотипа radiOLA через routerai.ru (OpenAI-совместимый, gpt-5.4-image-2).""" +import os, sys, json, base64, urllib.request, re + +KEY = open(os.path.expanduser("~/.routerai_key")).read().strip() +BASE = "https://routerai.ru/api/v1" +OUT = r"C:\radiOLA\design\logos\gen" +os.makedirs(OUT, exist_ok=True) + +def _data_uri(path): + ext = os.path.splitext(path)[1].lstrip(".").lower() or "png" + if ext == "jpg": ext = "jpeg" + return f"data:image/{ext};base64," + base64.b64encode(open(path, "rb").read()).decode() + +def call(model, prompt, ref_images=None): + if ref_images: + content = [{"type": "text", "text": prompt}] + for p in ref_images: + content.append({"type": "image_url", "image_url": {"url": _data_uri(p)}}) + else: + content = prompt + body = json.dumps({ + "model": model, + "modalities": ["image", "text"], + "messages": [{"role": "user", "content": content}], + }).encode("utf-8") + req = urllib.request.Request(BASE + "/chat/completions", data=body, headers={ + "Authorization": "Bearer " + KEY, + "Content-Type": "application/json", + }) + with urllib.request.urlopen(req, timeout=180) as r: + return json.loads(r.read().decode("utf-8")) + +def save_images(resp, stem): + """Ищем картинки в ответе (разные возможные места) и сохраняем.""" + saved = [] + raw = json.dumps(resp) + # 1) message.images[].image_url.url (data:image/...;base64,...) + msg = (resp.get("choices") or [{}])[0].get("message", {}) or {} + cands = [] + for im in (msg.get("images") or []): + u = (im.get("image_url") or {}).get("url") or im.get("url") + if u: cands.append(u) + # 2) content может быть списком с image_url + c = msg.get("content") + if isinstance(c, list): + for part in c: + if isinstance(part, dict): + u = (part.get("image_url") or {}).get("url") + if u: cands.append(u) + # 3) любые data:image в сыром json + cands += re.findall(r'data:image/[^"\\]+;base64,[A-Za-z0-9+/=]+', raw) + seen=set() + for i, u in enumerate(cands): + if u in seen: continue + seen.add(u) + m = re.search(r'base64,([A-Za-z0-9+/=]+)', u) + if not m: continue + p = os.path.join(OUT, f"{stem}_{i}.png") + open(p, "wb").write(base64.b64decode(m.group(1))) + saved.append(p) + return saved + +STYLE = ("App icon design, a single chunky glossy 3D cartoon letter R, vibrant emerald and " + "lime green with bright glassy highlights and smooth rounded bevels, playful premium mascot " + "style, centered on a deep dark teal-green background with a soft green glow, no text, " + "square 1:1 composition, crisp high detail. ") + +VARIANTS = { + "v1_radio": STYLE + "A small retro radio is built into the left stem of the R: round tuning " + "knobs and a horizontal speaker grille, integrated cleanly into the letter.", + "v2_waves": STYLE + "Concentric broadcast signal arcs and a tiny antenna emit from the top-right " + "of the R, like an on-air radio broadcast.", + "v3_vinyl": STYLE + "A glossy black vinyl record with a glowing green center label is tucked " + "behind the lower-left of the R, hinting at music radio.", + "v4_eq": STYLE + "A row of glowing green equalizer sound bars of varying heights runs along " + "the bottom, and the 3D R rises above them.", + "v5_dial": STYLE + "A retro radio tuning dial with a frequency scale and a glowing needle " + "wraps around the base of the R.", +} + +if __name__ == "__main__" and len(sys.argv) > 1 and sys.argv[1] == "batch": + model = "openai/gpt-5.4-image-2" + for stem, prompt in VARIANTS.items(): + try: + resp = call(model, prompt) + s = save_images(resp, stem) + print(stem, "->", s if s else "NO IMAGE", flush=True) + except Exception as e: + print(stem, "ERROR", e, flush=True) + sys.exit(0) + +# режим: python routerai.py refbatch — рендер монограммы в 3D, 4 стиля +if __name__ == "__main__" and len(sys.argv) > 1 and sys.argv[1] == "refbatch": + img = sys.argv[2] + KEEP = ("Recreate the EXACT letter-R monogram shown in the reference image — keep its identical " + "shape, proportions and thick ribbon construction, do not redesign the letterform. ") + BG = " Centered on a dark teal-green background with a subtle green glow, app icon, no text, square 1:1, crisp high detail." + PR = { + "r3d_matte": KEEP + "Render it as a chunky 3D isometric extruded block letter with clean matte " + "emerald-green faces and a darker green extruded side, soft simple shading, modern minimal 3D." + BG, + "r3d_gloss": KEEP + "Render it as a glossy 3D isometric extruded letter, vibrant emerald and lime " + "green with bright glassy highlights and smooth rounded bevels, premium look." + BG, + "r3d_grad": KEEP + "Render it as a 3D isometric extruded letter with a smooth lime-to-green gradient " + "front face, a clean darker-green side, gentle top light, modern." + BG, + "r3d_neon": KEEP + "Render it as a sleek 3D isometric extruded letter in bright lime green with a " + "soft neon glow rim, dark glossy side, futuristic clean." + BG, + } + for stem, prompt in PR.items(): + try: + resp = call("openai/gpt-5.4-image-2", prompt, ref_images=[img]) + print(stem, "->", save_images(resp, stem) or "NO IMAGE", flush=True) + except Exception as e: + print(stem, "ERROR", e, flush=True) + sys.exit(0) + +# режим: python routerai.py ref "" [img2 ...] +if __name__ == "__main__" and len(sys.argv) > 1 and sys.argv[1] == "ref": + stem = sys.argv[2]; prompt = sys.argv[3]; refs = sys.argv[4:] + resp = call("openai/gpt-5.4-image-2", prompt, ref_images=refs) + print("SAVED:", save_images(resp, stem)) + sys.exit(0) + +if __name__ == "__main__": + model = sys.argv[1] if len(sys.argv) > 1 else "openai/gpt-5.4-image-2" + stem = sys.argv[2] if len(sys.argv) > 2 else "probe" + prompt = sys.argv[3] if len(sys.argv) > 3 else ( + "App icon, a chunky glossy 3D cartoon letter 'R' in vibrant emerald and lime green " + "with a bright glassy top highlight and a clean beveled extrusion. A small retro radio " + "is built into the left stem of the R: round tuning knobs and horizontal speaker-grille " + "lines, subtle. Centered on a dark teal-green background with a soft green glow. Bold, " + "modern, playful, high detail, no text, square 1:1.") + resp = call(model, prompt) + # дамп структуры без гигантского base64 + skel = json.loads(re.sub(r'[A-Za-z0-9+/=]{200,}', '', json.dumps(resp))) + print("STRUCTURE:", json.dumps(skel, ensure_ascii=False)[:1500]) + print("SAVED:", save_images(resp, stem))