feat(brand): новый 3D-логотип (монограмма R) + лого/иконка под цветовую тему
Логотип: монограмма-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).
4
.gitignore
vendored
@@ -25,3 +25,7 @@ app/build/
|
||||
|
||||
# Kotlin
|
||||
.kotlin/
|
||||
|
||||
# Логотип: промежуточные генерации (тяжёлые), ключ — вне репо
|
||||
design/logos/gen/
|
||||
design/logos/ref_*.png
|
||||
|
||||
@@ -36,12 +36,50 @@
|
||||
android:name=".MainActivity"
|
||||
android:exported="true"
|
||||
android:theme="@style/Theme.Radiola.Splash"
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize">
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
android:configChanges="orientation|screenSize|smallestScreenSize" />
|
||||
|
||||
<!-- Иконка лаунчера под цветовую тему: всегда включён ровно ОДИН alias.
|
||||
Переключается в рантайме (LauncherIconManager) при смене темы. -->
|
||||
<activity-alias android:name=".MainAliasForest" android:enabled="true" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_forest" android:roundIcon="@mipmap/ic_launcher_forest_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasOcean" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_ocean" android:roundIcon="@mipmap/ic_launcher_ocean_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasSunset" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_sunset" android:roundIcon="@mipmap/ic_launcher_sunset_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasAmethyst" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_amethyst" android:roundIcon="@mipmap/ic_launcher_amethyst_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasNeon" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_neon" android:roundIcon="@mipmap/ic_launcher_neon_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasAmber" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_amber" android:roundIcon="@mipmap/ic_launcher_amber_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasIce" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_ice" android:roundIcon="@mipmap/ic_launcher_ice_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
<activity-alias android:name=".MainAliasRose" android:enabled="false" android:exported="true"
|
||||
android:targetActivity=".MainActivity" android:label="@string/app_name"
|
||||
android:icon="@mipmap/ic_launcher_rose" android:roundIcon="@mipmap/ic_launcher_rose_round">
|
||||
<intent-filter><action android:name="android.intent.action.MAIN"/><category android:name="android.intent.category.LAUNCHER"/></intent-filter>
|
||||
</activity-alias>
|
||||
|
||||
<service
|
||||
android:name=".service.PlayerService"
|
||||
|
||||
@@ -50,6 +50,9 @@ class MainActivity : ComponentActivity() {
|
||||
@Inject
|
||||
lateinit var settingsRepository: com.radiola.domain.repository.SettingsRepository
|
||||
|
||||
@Inject
|
||||
lateinit var launcherIconManager: com.radiola.util.LauncherIconManager
|
||||
|
||||
// После ответа на запрос уведомлений — просим исключение из оптимизации батареи.
|
||||
private val notifPermLauncher = registerForActivityResult(
|
||||
androidx.activity.result.contract.ActivityResultContracts.RequestPermission()
|
||||
@@ -67,6 +70,10 @@ class MainActivity : ComponentActivity() {
|
||||
setContent {
|
||||
// Выбранная цветовая тема (мгновенно перекрашивает всё приложение).
|
||||
val paletteId by settingsRepository.getThemePalette().collectAsState(initial = "forest")
|
||||
// Иконка лаунчера следует теме (срабатывает на старте и при смене темы).
|
||||
LaunchedEffect(paletteId) {
|
||||
launcherIconManager.applyIfNeeded(com.radiola.ui.theme.ThemePalette.fromId(paletteId))
|
||||
}
|
||||
RadiolaTheme(palette = com.radiola.ui.theme.ThemePalette.fromId(paletteId)) {
|
||||
val navController = rememberNavController()
|
||||
val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true)
|
||||
|
||||
@@ -23,30 +23,20 @@ import androidx.compose.material3.Text
|
||||
fun brandGradient(): Brush = Brush.linearGradient(listOf(BrandGradientStart, BrandGradientEnd))
|
||||
|
||||
/**
|
||||
* Иконка-марка приложения: градиентный squircle с монограммой «R».
|
||||
* Марка приложения: объёмная 3D-монограмма «R», перекрашенная под текущую тему.
|
||||
*/
|
||||
@Composable
|
||||
fun AppMark(
|
||||
size: Dp = 76.dp,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val radius = (size.value * 0.29f).dp
|
||||
val colors = RadiolaTheme.colors
|
||||
Box(
|
||||
modifier = modifier
|
||||
.size(size)
|
||||
.clip(RoundedCornerShape(radius))
|
||||
.background(colors.brandGradient),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "R",
|
||||
color = colors.bgBase,
|
||||
fontWeight = FontWeight.Black,
|
||||
fontSize = (size.value * 0.62f).sp
|
||||
val palette = RadiolaTheme.palette
|
||||
androidx.compose.foundation.Image(
|
||||
painter = androidx.compose.ui.res.painterResource(palette.logoRes),
|
||||
contentDescription = "radiOLA",
|
||||
modifier = modifier.size(size)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Текстовый логотип «radiOLA»: «radi» основным цветом, «OLA» акцентом.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.radiola.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import com.radiola.R
|
||||
|
||||
/**
|
||||
* Цветовые темы приложения. radiOLA всегда тёмная — палитры различаются оттенком
|
||||
@@ -112,6 +113,19 @@ enum class ThemePalette(
|
||||
),
|
||||
;
|
||||
|
||||
/** Перекрашенный под эту тему 3D-логотип (drawable). */
|
||||
val logoRes: Int
|
||||
get() = when (this) {
|
||||
FOREST -> 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
|
||||
|
||||
@@ -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,
|
||||
|
||||
66
app/src/main/java/com/radiola/util/LauncherIconManager.kt
Normal file
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
app/src/main/res/drawable-nodpi/ic_fg_amber.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_amethyst.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_forest.png
Normal file
|
After Width: | Height: | Size: 58 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_ice.png
Normal file
|
After Width: | Height: | Size: 53 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_neon.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_ocean.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_rose.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
BIN
app/src/main/res/drawable-nodpi/ic_fg_sunset.png
Normal file
|
After Width: | Height: | Size: 55 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_amber.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_amethyst.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_forest.png
Normal file
|
After Width: | Height: | Size: 89 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_ice.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_neon.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_ocean.png
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_rose.png
Normal file
|
After Width: | Height: | Size: 82 KiB |
BIN
app/src/main/res/drawable-nodpi/logo_sunset.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_amber.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_amber"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_amber"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_amber"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_amber"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_amethyst"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_amethyst"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_amethyst"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_amethyst"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_forest"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_forest"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_forest"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_forest"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_ice.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_ice"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_ice"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_ice"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_ice"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_neon.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_neon"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_neon"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_neon"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_neon"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_ocean.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_ocean"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_ocean"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_ocean"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_ocean"/>
|
||||
</adaptive-icon>
|
||||
5
app/src/main/res/mipmap-anydpi-v26/ic_launcher_rose.xml
Normal file
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_rose"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_rose"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_rose"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_rose"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_sunset"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_sunset"/>
|
||||
</adaptive-icon>
|
||||
@@ -0,0 +1,5 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@color/ic_bg_sunset"/>
|
||||
<foreground android:drawable="@drawable/ic_fg_sunset"/>
|
||||
</adaptive-icon>
|
||||
11
app/src/main/res/values/ic_colors.xml
Normal file
@@ -0,0 +1,11 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<color name="ic_bg_forest">#0C1410</color>
|
||||
<color name="ic_bg_ocean">#0A0F1A</color>
|
||||
<color name="ic_bg_sunset">#1A0F0C</color>
|
||||
<color name="ic_bg_amethyst">#120E1A</color>
|
||||
<color name="ic_bg_neon">#0D0A12</color>
|
||||
<color name="ic_bg_amber">#14110A</color>
|
||||
<color name="ic_bg_ice">#0C1014</color>
|
||||
<color name="ic_bg_rose">#160E12</color>
|
||||
</resources>
|
||||
13
design/logos/PHILOSOPHY.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Lumen Relay — дизайн-философия марки radiOLA
|
||||
|
||||
**Движение:** *Lumen Relay* — «светящаяся ретрансляция». Свет как сигнал, форма как звук.
|
||||
|
||||
Идентичность radiOLA рождается из одной идеи: **невидимая волна, ставшая осязаемым объёмом**. Эфир нельзя увидеть — но его можно вылепить. Буква «R» здесь не надпись, а маленькая скульптура: толстая, округлая, отлитая будто из тёплого зелёного стекла и подсвеченная изнутри. Это не плоский знак, а предмет, который хочется взять в руку — глянцевый, дружелюбный, уверенный. Каждая грань должна выглядеть так, словно над ней работали часами: выверенная фаска, честная тень, чистый блик.
|
||||
|
||||
**Свет и материал.** Палитра — единый лаймово-зелёный луч от почти белого верха (#D8FF7A) к глубокому травяному низу (#5E9A30), посаженный на угольно-зелёную ночь (#0C1410). Верх каждой формы ловит свет, низ уходит в тень — так рождается объём без единого лишнего цвета. Материал читается как полупрозрачный леденец: сочный, влажный, с одним точным бликом, а не десятком случайных.
|
||||
|
||||
**Объём и грань.** Глубина строится честно — изометрический сдвиг вниз-вправо, боковая грань на тон темнее, мягкая контактная тень под объектом. Никакого дешёвого «выдавливания фильтром»: экструзия выверена по шагам, как у мастера, который чувствует, где металл должен поймать свет. Скруглённые углы, пухлые штрихи — характер мультяшный, но исполнение ювелирное.
|
||||
|
||||
**Ритм радио.** Сигнал входит в композицию минимальным жестом — концентрические дуги эфира, волна-эквалайзер, бороздки винила, штрихи частотной шкалы. Один мотив на знак, не больше: радио должно *чувствоваться*, а не объясняться. Эти элементы — тихий аккомпанемент главной форме, тонкие и точные, как засечки на приборе.
|
||||
|
||||
**Композиция и тишина.** Каждая иконка — квадрат с воздухом по краям, центрированная масса, спокойный тёмный фон с лёгким сиянием за объектом. Текст почти отсутствует: только имя бренда шёпотом под формой. Информация живёт в свете и объёме, не в словах. Результат должен выглядеть как работа дизайнера на вершине ремесла — кристально чистая, без единой случайной точки, готовая к витрине.
|
||||
152
design/logos/build.py
Normal file
@@ -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'<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")
|
||||
74
design/logos/recolor.py
Normal file
@@ -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)
|
||||
137
design/logos/routerai.py
Normal file
@@ -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 <img> — рендер монограммы в 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 <stem> "<prompt>" <img1> [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,}', '<BASE64>', json.dumps(resp)))
|
||||
print("STRUCTURE:", json.dumps(skel, ensure_ascii=False)[:1500])
|
||||
print("SAVED:", save_images(resp, stem))
|
||||