wallpapers: redesign LIGHT formula and retune all presets

LIGHT formula was rebuilt around an explicit ladder bg → RQ → S → SQ
with three configurable gaps and a single darkChroma anchor for SQ;
RQ and S chroma now interpolate linearly between bgC and darkChroma by
their L position. The previous fixed 1.4/2.2/3.3 multipliers and the
RQ-near-white / S-above-bg geometry are gone — sent messages now sit
visibly below bg and SQ is the deepest of the bubble cluster, with
pattern still gated by the patternDepth slider.

DARK and BLACK gain matching patternDepth / patternChroma / bgLOffset
parameters so the same dev-tool affordances exist there too. Toolbar
tinting moves off the constructor `hue` field — the panel reads the
hue from the stored preset background instead, so per-base hue tweaks
made via the dev tool actually reach the toolbar. LIGHT toolbar L/C
are tuned to a single (0.99, 0.025) anchor that produces a faint
preset tint without competing with bg; documentation explains the
gamut-squeeze rationale.

ChatWallpaper.kt is the result of running the new formula with the
founder-tuned slider state for every preset:
- LIGHT (all 6) — final hand-tuned colors per preset.
- DARK — kids / school / travel retuned; cats / flowers / hearts
  unchanged from prior round.
- BLACK — all 6 retuned (tint + bubble cluster); bg stays pure black
  via wallpaperBackgrounds() default since L=C=0 is hue-agnostic.

FormulaDevTools picks up new sliders for the LIGHT ladder gaps and
darkChroma; defaults are derived from each preset's stored colors so
Reset restores the actual ladder shape rather than a generic
0.5/1.0/1.0. patternDepth in all three formulas is now nullable with
default fallback for API symmetry. An "Old colors" hold-to-compare
button and an OLD_LIGHT_COLORS snapshot map are wired up but the UI
button is commented out — easy to re-enable for future A/B passes.

Set Default Theme in chat customization now passes the SCHOOL preset's
recalibrated colors instead of null, so the default override matches
the new wallpaper instead of the pre-rewrite base palette.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
another-simple-pixel
2026-05-14 13:48:23 -07:00
co-authored by Claude Opus 4.6
parent e178e8924e
commit 03a5eb377e
3 changed files with 262 additions and 131 deletions
@@ -64,9 +64,25 @@ fun panelBackgroundColor(): Color {
private fun currentWallpaperPanelTint(state: ThemeManager.ActiveTheme): Color? {
val type = state.wallpaper.type as? WallpaperType.Preset ?: return null
val preset = PresetWallpaper.from(type.filename) ?: return null
val hue = preset.hue(state.base)
// Read hue from the stored preset bg for this base — that's the source of truth
// after the LIGHT formula reshuffle. (Old code used preset.hue(state.base), which
// returns the constructor field that wasn't updated in lockstep with the bg colors.)
val bg = preset.background[state.base] ?: return null
val hue = bg.toOklch().H
// Toolbar tint values (single L per mode, hue from preset bg, small chroma):
// LIGHT L=0.99 C=0.025 — just below pure white, so chroma can survive the
// high-L gamut squeeze and produce a faint preset
// tint instead of flat white. Yellow/green hues
// keep most of the chroma; red/blue hues clamp at
// their (smaller) gamut edge — still enough to
// distinguish presets without competing with bg.
// DARK L=0.1822 C=0.01 — same idea inverted: just above pure black with a
// barely-perceptible hue tint that aligns the
// toolbar with the dark wallpaper.
// BLACK and SIMPLEX get no preset tint — BLACK keeps pure dark, SIMPLEX has
// its own gradient panel.
return when (state.base) {
DefaultTheme.LIGHT -> oklch(1.0f, 0.03f, hue)
DefaultTheme.LIGHT -> oklch(0.99f, 0.025f, hue)
DefaultTheme.DARK -> oklch(0.1822f, 0.01f, hue)
else -> null
}
@@ -218,66 +234,68 @@ data class FormulaResult(
// ─── LIGHT ───
// Lightness ladder (top → bottom):
// R = 1 receivedTint (faint tint, near-white)
// bg = bgL (anchor)
// RQ = bgL rqGap·step (default 0.5)
// S = bgL (rqGap + sGap)·step (default sGap = 1.0)
// SQ = bgL (rqGap + sGap + sqGap)·step (default sqGap = 1.0)
// pattern = bgL patternDepth·step (depth slider)
//
// Step is the contrast-ladder unit: bigger step → wider total ladder.
// rqGap / sGap / sqGap let the founder retune individual rungs.
//
// Chroma: bgC for bg, darkChroma for SQ (the darkest below-bg slot). RQ and S
// linearly interpolate between bgC and darkChroma by their L position relative
// to the bg→SQ axis. So darkChroma=0 → SQ pure grey, S/RQ approach bg's chroma
// proportionally; darkChroma > bgC → ladder gets more saturated as it darkens.
// Pattern uses patternChroma slider (default 2.6·bgC). All chromas clamped to
// gamut.
fun generateSchemeLight(
hue: Float,
bgL: Float,
bgC: Float,
step: Float,
patternDepth: Float = 2.5f,
patternDepth: Float? = null,
patternChroma: Float? = null,
receivedTint: Float = 0.005f,
bgLOffset: Float = 0f,
rqGap: Float = 0.5f,
sGap: Float = 1.0f,
sqGap: Float = 1.0f,
darkChroma: Float? = null,
saturationScale: Float = 1f,
contrastScale: Float = 1f,
patternIntensity: Float = 1f,
): FormulaResult {
val effBgC = bgC * saturationScale
val effStep = step * contrastScale
val effDepth = patternDepth * patternIntensity
val effPatternC = patternChroma?.let { it * patternIntensity }
val maxBg = maxChroma(bgL, hue)
val satRatio = if (maxBg > 0f) effBgC / maxBg else 0f
val loRatio = 0.35f
val effDepth = (patternDepth ?: 3.5f) * patternIntensity
val effPatternC = patternChroma?.let { it * patternIntensity } ?: (effBgC * 2.6f)
val effDarkC = (darkChroma ?: (bgC * 3.3f)) * saturationScale
data class Raw(val name: String, val L: Float)
val slots = listOf(
Raw("receivedMessage", 1f - receivedTint),
Raw("receivedQuote", 1f - effStep),
Raw("sentMessage", bgL + effStep / 3f),
Raw("background", (bgL + bgLOffset).coerceIn(0f, 1f)),
Raw("sentQuote", bgL - effStep),
Raw("pattern", bgL - effDepth * effStep),
)
fun clamp(L: Float, c: Float) = min(c, maxChroma(L, hue))
fun lerp(a: Float, b: Float, t: Float) = a + t * (b - a)
val computed = mutableMapOf<String, FormulaSlot>()
for (slot in slots) {
val maxC = maxChroma(slot.L, hue)
val c = when (slot.name) {
"receivedMessage" -> maxC
"background" -> effBgC
"pattern" -> if (effPatternC != null) min(effPatternC, maxC) else {
if (slot.L >= bgL) maxC * satRatio
else {
val t = if (effStep > 0f) min(1f, (bgL - slot.L) / (2.5f * effStep)) else 0f
maxC * (satRatio - (satRatio - loRatio) * t)
}
}
else -> if (slot.L >= bgL) maxC * satRatio
else {
val t = if (effStep > 0f) min(1f, (bgL - slot.L) / (2.5f * effStep)) else 0f
maxC * (satRatio - (satRatio - loRatio) * t)
}
}
computed[slot.name] = FormulaSlot(slot.L, min(c, maxC), hue)
}
val rL = 1f - receivedTint
val bgLEff = (bgL + bgLOffset).coerceIn(0f, 1f)
val rqL = bgL - rqGap * effStep
val sL = bgL - (rqGap + sGap) * effStep
val sqL = bgL - (rqGap + sGap + sqGap) * effStep
val ptL = bgL - effDepth * effStep
// Chroma interpolation: bgC at bg, effDarkC at SQ; RQ and S sit on the line.
val totalGap = rqGap + sGap + sqGap
val tRQ = if (totalGap > 0f) rqGap / totalGap else 0f
val tS = if (totalGap > 0f) (rqGap + sGap) / totalGap else 0f
return FormulaResult(
background = computed["background"]!!,
pattern = computed["pattern"]!!,
sentMessage = computed["sentMessage"]!!,
sentQuote = computed["sentQuote"]!!,
receivedMessage = computed["receivedMessage"]!!,
receivedQuote = computed["receivedQuote"]!!,
receivedMessage = FormulaSlot(rL, maxChroma(rL, hue), hue),
background = FormulaSlot(bgLEff, clamp(bgLEff, effBgC), hue),
receivedQuote = FormulaSlot(rqL, clamp(rqL, lerp(effBgC, effDarkC, tRQ)), hue),
sentMessage = FormulaSlot(sL, clamp(sL, lerp(effBgC, effDarkC, tS)), hue),
sentQuote = FormulaSlot(sqL, clamp(sqL, effDarkC), hue),
pattern = FormulaSlot(ptL, clamp(ptL, effPatternC), hue),
)
}
@@ -34,21 +34,21 @@ enum class PresetWallpaper(
) {
CATS(MR.images.wallpaper_cats, "cats", 0.5f, 77f, 0.8172f,
wallpaperBackgrounds(
light = oklch(0.97393197f, 0.04026258f, 94.315125f),
light = oklch(0.9838867f, 0.021334622f, 95.39152f),
dark = oklch(0.1800f, 0.0250f, 77f),
),
_tint = mapOf(
DefaultTheme.LIGHT to oklch(0.9049206f, 0.07393196f, 94.315125f),
DefaultTheme.LIGHT to oklch(0.92822266f, 0.05571678f, 95.39152f),
DefaultTheme.DARK to oklch(0.2940f, 0.0567f, 77f),
DefaultTheme.SIMPLEX to oklch(0.3797781f, 0.06842897f, 88.88896f), // #ff51400f
DefaultTheme.BLACK to oklch(0.3600f, 0.0866f, 77f)
DefaultTheme.BLACK to oklch(0.36010742f, 0.07737845f, 64.32541f)
),
_colors = mapOf(
DefaultTheme.LIGHT to ResolvedColors(
sentMessage = oklch(0.97874475f, 0.03285575f, 94.315125f),
sentQuote = oklch(0.95411396f, 0.05781051f, 94.315125f),
receivedMessage = oklch(0.9946289f, 0.00832808f, 94.315125f),
receivedQuote = oklch(0.9815269f, 0.028569698f, 94.315125f),
sentMessage = oklch(0.95458984f, 0.05769427f, 95.39152f),
sentQuote = oklch(0.9350586f, 0.081934035f, 95.39152f),
receivedMessage = oklch(0.9941406f, 0.009641647f, 95.39152f),
receivedQuote = oklch(0.9741211f, 0.033454504f, 95.39152f),
),
DefaultTheme.DARK to ResolvedColors(
sentMessage = oklch(0.3130f, 0.0630f, 77f),
@@ -63,30 +63,30 @@ enum class PresetWallpaper(
receivedQuote = oklch(0.332832f, 0.005361989f, 91.54412f), // #ff373633
),
DefaultTheme.BLACK to ResolvedColors(
sentMessage = oklch(0.24f, 0.0596f, 70f),
sentQuote = oklch(0.33f, 0.0815f, 70f),
receivedMessage = oklch(0.12f, 0f, 0f),
receivedQuote = oklch(0.22f, 0f, 0f),
sentMessage = oklch(0.24145508f, 0.054447953f, 64.32541f),
sentQuote = oklch(0.33200073f, 0.07415811f, 64.32541f),
receivedMessage = oklch(0.12072754f, 0.0f, 64.32541f),
receivedQuote = oklch(0.22133382f, 0.0f, 64.32541f),
),
),
),
FLOWERS(MR.images.wallpaper_flowers, "flowers", 0.5f, 130f, 1.3553f,
wallpaperBackgrounds(
light = oklch(0.9506836f, 0.035730995f, 129.80244f),
light = oklch(0.9838867f, 0.02136218f, 130.21953f),
dark = oklch(0.1800f, 0.0250f, 130f),
),
_tint = mapOf(
DefaultTheme.LIGHT to oklch(0.85039175f, 0.09360638f, 129.80244f),
DefaultTheme.LIGHT to oklch(0.91506225f, 0.054591343f, 130.21953f),
DefaultTheme.DARK to oklch(0.3130f, 0.0567f, 130f),
DefaultTheme.SIMPLEX to oklch(0.4415422f, 0.1170956f, 133.8571f), // #ff36600f
DefaultTheme.BLACK to oklch(0.3600f, 0.1133f, 130f)
DefaultTheme.BLACK to oklch(0.36254883f, 0.10072228f, 132.04506f)
),
_colors = mapOf(
DefaultTheme.LIGHT to ResolvedColors(
sentMessage = oklch(0.96106595f, 0.027363745f, 129.80244f),
sentQuote = oklch(0.9195365f, 0.06853871f, 129.80244f),
receivedMessage = oklch(0.9946289f, 0.011358023f, 129.80244f),
receivedQuote = oklch(0.9688529f, 0.021421315f, 129.80244f),
sentMessage = oklch(0.9542969f, 0.067714185f, 130.21953f),
sentQuote = oklch(0.9345703f, 0.09861551f, 130.21953f),
receivedMessage = oklch(0.9951172f, 0.010222077f, 130.21953f),
receivedQuote = oklch(0.97402346f, 0.036812846f, 130.21953f),
),
DefaultTheme.DARK to ResolvedColors(
sentMessage = oklch(0.3130f, 0.0630f, 130f),
@@ -101,30 +101,30 @@ enum class PresetWallpaper(
receivedQuote = oklch(0.3334174f, 0.007411477f, 128.7105f), // #ff353733
),
DefaultTheme.BLACK to ResolvedColors(
sentMessage = oklch(0.24f, 0.0756f, 130f),
sentQuote = oklch(0.33f, 0.1029f, 130f),
receivedMessage = oklch(0.12f, 0f, 0f),
receivedQuote = oklch(0.22f, 0f, 0f),
sentMessage = oklch(0.24267578f, 0.06739509f, 132.04506f),
sentQuote = oklch(0.3336792f, 0.091792114f, 132.04506f),
receivedMessage = oklch(0.12133789f, 0.0f, 132.04506f),
receivedQuote = oklch(0.2224528f, 0.0f, 132.04506f),
),
),
),
HEARTS(MR.images.wallpaper_hearts, "hearts", 0.5f, 15f, 1.0504f,
wallpaperBackgrounds(
light = oklch(0.9560547f, 0.021765456f, 13.841402f),
light = oklch(0.9780521f, 0.010869741f, 10.569774f),
dark = oklch(0.1800f, 0.0250f, 5f),
),
_tint = mapOf(
DefaultTheme.LIGHT to oklch(0.90485084f, 0.04976797f, 13.841402f),
DefaultTheme.LIGHT to oklch(0.9399476f, 0.030744314f, 10.569774f),
DefaultTheme.DARK to oklch(0.2940f, 0.0567f, 5f),
DefaultTheme.SIMPLEX to oklch(0.2574974f, 0.07614605f, 24.19117f), // #ff411010
DefaultTheme.BLACK to oklch(0.3600f, 0.1630f, 5f)
DefaultTheme.BLACK to oklch(0.30926332f, 0.12386322f, 7.8465395f)
),
_colors = mapOf(
DefaultTheme.LIGHT to ResolvedColors(
sentMessage = oklch(0.96500653f, 0.017198082f, 13.841402f),
sentQuote = oklch(0.9291992f, 0.026614813f, 13.841402f),
receivedMessage = oklch(0.99560547f, 0.002124548f, 13.841402f),
receivedQuote = oklch(0.97314453f, 0.013107679f, 13.841402f),
sentMessage = oklch(0.95283204f, 0.023875237f, 10.569774f),
sentQuote = oklch(0.9364258f, 0.032649755f, 10.569774f),
receivedMessage = oklch(0.99121094f, 0.004304409f, 10.569774f),
receivedQuote = oklch(0.9665039f, 0.016754389f, 10.569774f),
),
DefaultTheme.DARK to ResolvedColors(
sentMessage = oklch(0.3130f, 0.0630f, 5f),
@@ -139,36 +139,36 @@ enum class PresetWallpaper(
receivedQuote = oklch(0.3352158f, 0.008515606f, 17.58481f), // #ff3b3535
),
DefaultTheme.BLACK to ResolvedColors(
sentMessage = oklch(0.24f, 0.1087f, 5f),
sentQuote = oklch(0.33f, 0.1480f, 5f),
receivedMessage = oklch(0.12f, 0f, 0f),
receivedQuote = oklch(0.22f, 0f, 0f),
sentMessage = oklch(0.24780273f, 0.085917726f, 7.8465395f),
sentQuote = oklch(0.34072876f, 0.117019944f, 7.8465395f),
receivedMessage = oklch(0.12390137f, 0.0f, 7.8465395f),
receivedQuote = oklch(0.22715251f, 0.0f, 7.8465395f),
),
),
),
KIDS(MR.images.wallpaper_kids, "kids", 0.5f, 200f, 0.7723f,
wallpaperBackgrounds(
light = oklch(0.9628906f, 0.023496835f, 198.73976f),
dark = oklch(0.1800f, 0.0250f, 200f),
light = oklch(0.98472977f, 0.01831758f, 200.66322f),
dark = oklch(0.18017578f, 0.025585549f, 199.76416f),
),
_tint = mapOf(
DefaultTheme.LIGHT to oklch(0.8909232f, 0.055181965f, 198.73976f),
DefaultTheme.DARK to oklch(0.2560f, 0.0567f, 200f),
DefaultTheme.LIGHT to oklch(0.9382386f, 0.035245255f, 200.66322f),
DefaultTheme.DARK to oklch(0.2585798f, 0.043961763f, 199.76416f),
DefaultTheme.SIMPLEX to oklch(0.3716418f, 0.05389406f, 217.7104f), // #ff184753
DefaultTheme.BLACK to oklch(0.3000f, 0.0684f, 200f)
DefaultTheme.BLACK to oklch(0.2880891f, 0.049433947f, 192.22177f)
),
_colors = mapOf(
DefaultTheme.LIGHT to ResolvedColors(
sentMessage = oklch(0.9716797f, 0.017883832f, 198.73976f),
sentQuote = oklch(0.93652344f, 0.036067758f, 198.73976f),
receivedMessage = oklch(0.9946289f, 0.0069881678f, 198.73976f),
receivedQuote = oklch(0.9736328f, 0.016644018f, 198.73976f),
sentMessage = oklch(0.9599414f, 0.029920436f, 200.66322f),
sentQuote = oklch(0.9388867f, 0.038093355f, 200.66322f),
receivedMessage = oklch(0.9941406f, 0.007004261f, 200.66322f),
receivedQuote = oklch(0.97333986f, 0.024719488f, 200.66322f),
),
DefaultTheme.DARK to ResolvedColors(
sentMessage = oklch(0.3130f, 0.0630f, 200f),
sentQuote = oklch(0.3700f, 0.0725f, 200f),
receivedMessage = oklch(0.2560f, 0.0278f, 200f),
receivedQuote = oklch(0.2940f, 0.0320f, 200f),
sentMessage = oklch(0.3173828f, 0.05395502f, 199.76416f),
sentQuote = oklch(0.37618583f, 0.062117502f, 199.76416f),
receivedMessage = oklch(0.2585798f, 0.028428389f, 199.76416f),
receivedQuote = oklch(0.29778183f, 0.032692645f, 199.76416f),
),
DefaultTheme.SIMPLEX to ResolvedColors(
sentMessage = oklch(0.3662882f, 0.04909204f, 191.2229f), // #ff1a4745
@@ -177,36 +177,36 @@ enum class PresetWallpaper(
receivedQuote = oklch(0.3451987f, 0.004436687f, 174.2088f), // #ff373a39
),
DefaultTheme.BLACK to ResolvedColors(
sentMessage = oklch(0.24f, 0.0555f, 192f),
sentQuote = oklch(0.33f, 0.0756f, 192f),
receivedMessage = oklch(0.12f, 0f, 0f),
receivedQuote = oklch(0.22f, 0f, 0f),
sentMessage = oklch(0.24645996f, 0.042290688f, 192.22177f),
sentQuote = oklch(0.33888245f, 0.057626568f, 192.22177f),
receivedMessage = oklch(0.12322998f, 0.0f, 192.22177f),
receivedQuote = oklch(0.22592163f, 0.0f, 192.22177f),
),
),
),
SCHOOL(MR.images.wallpaper_school, "school", 0.5f, 239f, 0.7950f,
wallpaperBackgrounds(
light = oklch(0.9604492f, 0.025148988f, 225.3911f),
dark = oklch(0.1800f, 0.0250f, 249f),
light = oklch(0.9837532f, 0.010169387f, 225.85442f),
dark = oklch(0.17883301f, 0.024886385f, 251.75946f),
),
_tint = mapOf(
DefaultTheme.LIGHT to oklch(0.9019366f, 0.04948576f, 225.3911f),
DefaultTheme.DARK to oklch(0.2560f, 0.0567f, 249f),
DefaultTheme.LIGHT to oklch(0.94544077f, 0.029549051f, 225.85442f),
DefaultTheme.DARK to oklch(0.2515276f, 0.05627329f, 251.75946f),
DefaultTheme.SIMPLEX to oklch(0.2929108f, 0.05102392f, 240.8139f), // #ff112f43
DefaultTheme.BLACK to oklch(0.3000f, 0.1070f, 249f)
DefaultTheme.BLACK to oklch(0.27668023f, 0.09167254f, 255.17474f)
),
_colors = mapOf(
DefaultTheme.LIGHT to ResolvedColors(
sentMessage = oklch(0.96875f, 0.019812047f, 225.3911f),
sentQuote = oklch(0.9355469f, 0.036309462f, 225.3911f),
receivedMessage = oklch(0.99560547f, 0.0027611256f, 225.3911f),
receivedQuote = oklch(0.97509766f, 0.015755296f, 225.3911f),
sentMessage = oklch(0.95765626f, 0.026757836f, 225.85442f),
sentQuote = oklch(0.93779296f, 0.038093355f, 225.85442f),
receivedMessage = oklch(0.9941406f, 0.0036548972f, 225.85442f),
receivedQuote = oklch(0.9731055f, 0.016901612f, 225.85442f),
),
DefaultTheme.DARK to ResolvedColors(
sentMessage = oklch(0.3130f, 0.0630f, 249f),
sentQuote = oklch(0.3700f, 0.0725f, 249f),
receivedMessage = oklch(0.2560f, 0.0278f, 249f),
receivedQuote = oklch(0.2940f, 0.0320f, 249f),
sentMessage = oklch(0.31274414f, 0.06245613f, 251.75946f),
sentQuote = oklch(0.37013462f, 0.07182455f, 251.75946f),
receivedMessage = oklch(0.25535366f, 0.02765154f, 251.75946f),
receivedQuote = oklch(0.29361397f, 0.031799268f, 251.75946f),
),
DefaultTheme.SIMPLEX to ResolvedColors(
sentMessage = oklch(0.4187f, 0.1388f, 262.50f), // #204797
@@ -215,36 +215,36 @@ enum class PresetWallpaper(
receivedQuote = oklch(0.3023f, 0.0900f, 267.90f), // #1b2a5b
),
DefaultTheme.BLACK to ResolvedColors(
sentMessage = oklch(0.24f, 0.0856f, 249f),
sentQuote = oklch(0.33f, 0.1166f, 249f),
receivedMessage = oklch(0.12f, 0f, 0f),
receivedQuote = oklch(0.22f, 0f, 0f),
sentMessage = oklch(0.24328613f, 0.08060831f, 255.17474f),
sentQuote = oklch(0.33451843f, 0.10981243f, 255.17474f),
receivedMessage = oklch(0.12164307f, 0.0f, 255.17474f),
receivedQuote = oklch(0.22301228f, 0.0f, 255.17474f),
),
),
),
TRAVEL(MR.images.wallpaper_travel, "travel", 0.5f, 315f, 1.2099f,
wallpaperBackgrounds(
light = oklch(0.95996094f, 0.023496835f, 322.59464f),
dark = oklch(0.1800f, 0.0250f, 315f),
light = oklch(0.9835678f, 0.01424849f, 325.37686f),
dark = oklch(0.17980957f, 0.025278661f, 316.56534f),
),
_tint = mapOf(
DefaultTheme.LIGHT to oklch(0.9120599f, 0.056131333f, 322.59464f),
DefaultTheme.DARK to oklch(0.2750f, 0.0567f, 315f),
DefaultTheme.LIGHT to oklch(0.9464822f, 0.035007913f, 325.37686f),
DefaultTheme.DARK to oklch(0.27441406f, 0.0701345f, 316.56534f),
DefaultTheme.SIMPLEX to oklch(0.2948376f, 0.08277514f, 302.7197f), // #ff35204e
DefaultTheme.BLACK to oklch(0.3000f, 0.1579f, 315f)
DefaultTheme.BLACK to oklch(0.2714336f, 0.1344262f, 313.97916f)
),
_colors = mapOf(
DefaultTheme.LIGHT to ResolvedColors(
sentMessage = oklch(0.96809894f, 0.01866399f, 322.59464f),
sentQuote = oklch(0.9355469f, 0.030384803f, 322.59464f),
receivedMessage = oklch(0.99560547f, 0.0035372972f, 322.59464f),
receivedQuote = oklch(0.97558594f, 0.014230033f, 322.59464f),
sentMessage = oklch(0.9571094f, 0.032885246f, 325.37686f),
sentQuote = oklch(0.9408496f, 0.043789558f, 325.37686f),
receivedMessage = oklch(0.9921875f, 0.0067495108f, 325.37686f),
receivedQuote = oklch(0.9701172f, 0.024161799f, 325.37686f),
),
DefaultTheme.DARK to ResolvedColors(
sentMessage = oklch(0.3130f, 0.0630f, 315f),
sentQuote = oklch(0.3700f, 0.0725f, 315f),
receivedMessage = oklch(0.2560f, 0.0278f, 315f),
receivedQuote = oklch(0.2940f, 0.0320f, 315f),
sentMessage = oklch(0.31201172f, 0.066930376f, 316.56534f),
sentQuote = oklch(0.36866978f, 0.07696993f, 316.56534f),
receivedMessage = oklch(0.25535366f, 0.028087402f, 316.56534f),
receivedQuote = oklch(0.2931257f, 0.03230051f, 316.56534f),
),
DefaultTheme.SIMPLEX to ResolvedColors(
sentMessage = oklch(0.3234681f, 0.09690244f, 299.9634f), // #ff3C255D
@@ -253,10 +253,10 @@ enum class PresetWallpaper(
receivedQuote = oklch(0.355058f, 0.03791292f, 286.3773f), // #ff3A394F
),
DefaultTheme.BLACK to ResolvedColors(
sentMessage = oklch(0.24f, 0.1263f, 315f),
sentQuote = oklch(0.33f, 0.1721f, 315f),
receivedMessage = oklch(0.12f, 0f, 0f),
receivedQuote = oklch(0.22f, 0f, 0f),
sentMessage = oklch(0.2446289f, 0.09382912f, 313.97916f),
sentQuote = oklch(0.33636475f, 0.12779526f, 313.97916f),
receivedMessage = oklch(0.12231445f, 0.0f, 313.97916f),
receivedQuote = oklch(0.22424316f, 0.0f, 313.97916f),
),
),
);
@@ -8,6 +8,7 @@ import SectionItemViewWithoutMinPadding
import SectionSpacer
import SectionView
import androidx.compose.foundation.*
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.text.BasicTextField
@@ -22,6 +23,7 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.*
import androidx.compose.ui.graphics.*
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.TextFieldValue
@@ -68,6 +70,61 @@ private val formulaSavedParams = mutableStateMapOf<String, Float>()
// Desktop: use fast scaling during slider drag, SCALE_SMOOTH on release.
private var patternScaleDragging by mutableStateOf(false)
// Snapshot of LIGHT preset colors before the formula rewrite (the values that
// were stored in ChatWallpaper.kt prior to today's pass). Used by the "Old
// colors" hold-to-compare button so the founder can see the previous palette
// without depending on the previous formula's behaviour.
private val OLD_LIGHT_COLORS: Map<String, FormulaResult> = mapOf(
"cats" to FormulaResult(
background = FormulaSlot(0.97393197f, 0.04026258f, 94.315125f),
pattern = FormulaSlot(0.9049206f, 0.07393196f, 94.315125f),
sentMessage = FormulaSlot(0.97874475f, 0.03285575f, 94.315125f),
sentQuote = FormulaSlot(0.95411396f, 0.05781051f, 94.315125f),
receivedMessage = FormulaSlot(0.9946289f, 0.00832808f, 94.315125f),
receivedQuote = FormulaSlot(0.9815269f, 0.028569698f, 94.315125f),
),
"flowers" to FormulaResult(
background = FormulaSlot(0.9506836f, 0.035730995f, 129.80244f),
pattern = FormulaSlot(0.85039175f, 0.09360638f, 129.80244f),
sentMessage = FormulaSlot(0.96106595f, 0.027363745f, 129.80244f),
sentQuote = FormulaSlot(0.9195365f, 0.06853871f, 129.80244f),
receivedMessage = FormulaSlot(0.9946289f, 0.011358023f, 129.80244f),
receivedQuote = FormulaSlot(0.9688529f, 0.021421315f, 129.80244f),
),
"hearts" to FormulaResult(
background = FormulaSlot(0.9560547f, 0.021765456f, 13.841402f),
pattern = FormulaSlot(0.90485084f, 0.04976797f, 13.841402f),
sentMessage = FormulaSlot(0.96500653f, 0.017198082f, 13.841402f),
sentQuote = FormulaSlot(0.9291992f, 0.026614813f, 13.841402f),
receivedMessage = FormulaSlot(0.99560547f, 0.002124548f, 13.841402f),
receivedQuote = FormulaSlot(0.97314453f, 0.013107679f, 13.841402f),
),
"kids" to FormulaResult(
background = FormulaSlot(0.9628906f, 0.023496835f, 198.73976f),
pattern = FormulaSlot(0.8909232f, 0.055181965f, 198.73976f),
sentMessage = FormulaSlot(0.9716797f, 0.017883832f, 198.73976f),
sentQuote = FormulaSlot(0.93652344f, 0.036067758f, 198.73976f),
receivedMessage = FormulaSlot(0.9946289f, 0.0069881678f, 198.73976f),
receivedQuote = FormulaSlot(0.9736328f, 0.016644018f, 198.73976f),
),
"school" to FormulaResult(
background = FormulaSlot(0.9604492f, 0.025148988f, 225.3911f),
pattern = FormulaSlot(0.9019366f, 0.04948576f, 225.3911f),
sentMessage = FormulaSlot(0.96875f, 0.019812047f, 225.3911f),
sentQuote = FormulaSlot(0.9355469f, 0.036309462f, 225.3911f),
receivedMessage = FormulaSlot(0.99560547f, 0.0027611256f, 225.3911f),
receivedQuote = FormulaSlot(0.97509766f, 0.015755296f, 225.3911f),
),
"travel" to FormulaResult(
background = FormulaSlot(0.95996094f, 0.023496835f, 322.59464f),
pattern = FormulaSlot(0.9120599f, 0.056131333f, 322.59464f),
sentMessage = FormulaSlot(0.96809894f, 0.01866399f, 322.59464f),
sentQuote = FormulaSlot(0.9355469f, 0.030384803f, 322.59464f),
receivedMessage = FormulaSlot(0.99560547f, 0.0035372972f, 322.59464f),
receivedQuote = FormulaSlot(0.97558594f, 0.014230033f, 322.59464f),
),
)
// ───────────────────────────────────────────────────────────────────────────
object AppearanceScope {
@@ -768,14 +825,23 @@ object AppearanceScope {
val sm = colors?.sentMessage?.toOklch() ?: bg
val sq = colors?.sentQuote?.toOklch() ?: bg
if (isLight) {
val step = bg.L - sq.L
// New LIGHT formula: SQ sits at bgL 2.5·step, so step = (bgL sqL) / 2.5.
// patternDepth and rqGap/sGap/sqGap are derived in the same step units
// so a Reset on a tuned preset restores its actual ladder shape, not a
// flat 0.5/1.0/1.0 default. darkChroma is SQ.C directly.
val step = (bg.L - sq.L) / 2.5f
val rm = colors?.receivedMessage?.toOklch()
val rq = colors?.receivedQuote?.toOklch() ?: bg
mapOf(
"hue" to bg.H, "bgL" to bg.L, "bgC" to bg.C,
"step" to step,
"patternDepth" to if (step > 0f) (bg.L - tint.L) / step else 0f,
"patternChroma" to tint.C,
"receivedTint" to if (rm != null && rm.L < 1f) 1f - rm.L else 0.005f,
"darkChroma" to sq.C,
"rqGap" to if (step > 0f) (bg.L - rq.L) / step else 0.5f,
"sGap" to if (step > 0f) (rq.L - sm.L) / step else 1.0f,
"sqGap" to if (step > 0f) (sm.L - sq.L) / step else 1.0f,
)
} else {
val step = if (isBlack) sm.L / 6f else (sm.L - bg.L) / 3.5f
@@ -808,13 +874,29 @@ object AppearanceScope {
val bgLOffset = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("bgLOffset") ?: 0f) }
val mutedChroma = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("mutedChroma") ?: defaults["mutedChroma"] ?: 0f) }
val colorChroma = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("colorChroma") ?: defaults["colorChroma"] ?: 0f) }
val rqGap = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("rqGap") ?: defaults["rqGap"] ?: 0.5f) }
val sGap = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("sGap") ?: defaults["sGap"] ?: 1.0f) }
val sqGap = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("sqGap") ?: defaults["sqGap"] ?: 1.0f) }
val darkChroma = remember(preset, baseTheme, resetKey) { mutableFloatStateOf(saved("darkChroma") ?: defaults["darkChroma"] ?: 0f) }
// Hold-to-compare: when true, LIGHT result switches to the snapshot of
// pre-rewrite preset colors (OLD_LIGHT_COLORS). DARK/BLACK have no snapshot
// — flag is no-op there.
val comparingOld = remember { mutableStateOf(false) }
val oldColors = OLD_LIGHT_COLORS[preset.filename]
// Compute formula result (O(1) math, no need to memoize)
val result = when (baseTheme) {
val result = if (comparingOld.value && baseTheme == DefaultTheme.LIGHT && oldColors != null) {
oldColors
} else when (baseTheme) {
DefaultTheme.LIGHT -> generateSchemeLight(
hue.floatValue, bgL.floatValue, bgC.floatValue, step.floatValue,
patternDepth.floatValue, patternChromaVal.floatValue, receivedTint.floatValue,
bgLOffset.floatValue,
rqGap = rqGap.floatValue,
sGap = sGap.floatValue,
sqGap = sqGap.floatValue,
darkChroma = darkChroma.floatValue,
)
DefaultTheme.BLACK -> generateSchemeBlack(
hue.floatValue, step.floatValue, colorChroma.floatValue,
@@ -851,8 +933,35 @@ object AppearanceScope {
savedParams["${pk}bgLOffset"] = bgLOffset.floatValue
savedParams["${pk}mutedChroma"] = mutedChroma.floatValue
savedParams["${pk}colorChroma"] = colorChroma.floatValue
savedParams["${pk}rqGap"] = rqGap.floatValue
savedParams["${pk}sGap"] = sGap.floatValue
savedParams["${pk}sqGap"] = sqGap.floatValue
savedParams["${pk}darkChroma"] = darkChroma.floatValue
}
// Old colors compare button — disabled (uncomment to re-enable A/B compare).
// if (baseTheme == DefaultTheme.LIGHT && oldColors != null) {
// Box(
// Modifier
// .fillMaxWidth()
// .sizeIn(minHeight = DEFAULT_MIN_SECTION_ITEM_HEIGHT)
// .pointerInput(Unit) {
// detectTapGestures(onPress = {
// comparingOld.value = true
// tryAwaitRelease()
// comparingOld.value = false
// })
// }
// .padding(horizontal = DEFAULT_PADDING, vertical = DEFAULT_PADDING_HALF),
// contentAlignment = Alignment.CenterStart,
// ) {
// Text(
// if (comparingOld.value) "Old colors — release to return" else "Old colors — hold to compare",
// color = colors.primary,
// )
// }
// }
SectionView("FORMULA: ${preset.filename.uppercase()} / ${baseTheme.name}") {
when (baseTheme) {
DefaultTheme.LIGHT -> {
@@ -860,8 +969,12 @@ object AppearanceScope {
FormulaSlider("Lightness", bgL, 0.85f..1f)
FormulaSlider("BG Lightness", bgLOffset, -0.05f..0.05f)
FormulaSlider("Chroma", bgC, 0f..0.10f)
FormulaSlider("Contrast", step, 0.01f..0.10f)
FormulaSlider("Contrast", step, 0.005f..0.05f)
FormulaSlider("Received tint", receivedTint, 0f..0.07f)
FormulaSlider("RQ gap", rqGap, 0f..3f)
FormulaSlider("S gap", sGap, 0f..3f)
FormulaSlider("SQ gap", sqGap, 0f..3f)
FormulaSlider("Dark chroma", darkChroma, 0f..0.15f)
FormulaSlider("Pattern depth", patternDepth, 0f..10f)
FormulaSlider("Pattern chroma", patternChromaVal, 0f..0.15f)
}