formula dev tools: add pattern + bg-lightness controls for dark/black

LIGHT had three pattern/background controls — Pattern depth, Pattern
chroma, BG Lightness — that DARK and BLACK didn't expose. Without them
the founder couldn't tune the wallpaper pattern's brightness/saturation
or nudge the background's lightness independently of the bubble cluster
in those themes. Add the same three sliders to DARK and BLACK.

generateSchemeDark / generateSchemeBlack gain three new optional params
that pass through generateDarkFromSlots:
- patternDepth: when set, replaces the slot's hard-coded lightnessMult
  (and patternIntensity multiplier) directly, matching LIGHT's semantic
  of "multiplier on effStep".
- patternChroma: when set, overrides the pattern slot's chroma — taking
  precedence over BLACK's max-pin behaviour so the user can pull it back
  from the gamut edge.
- bgLOffset: nudges only the background slot's L (other slots stay at
  their formula-computed L), mirroring LIGHT.

null defaults preserve the existing behaviour, so nothing changes for
callers that don't pass the new params.

Defaults derivation in FormulaDevTools picks up patternDepth and
patternChroma from the preset's stored tint slot (same shape as LIGHT,
with the sign of the depth swapped because dark themes have the pattern
brighter than the bg, not darker). bgLOffset stays at its existing 0f
fallback.

Slider ranges: BLACK pattern depth uses 0..15 because the default
lightnessMult for BLACK pattern is 9.0 — the LIGHT/DARK 0..10 wouldn't
give meaningful headroom.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
another-simple-pixel
2026-05-13 14:48:23 -07:00
parent 143653005d
commit e178e8924e
2 changed files with 34 additions and 5 deletions
@@ -309,25 +309,32 @@ fun generateSchemeDark(
step: Float = 0.038f,
mutedChroma: Float = 0.020f,
colorChroma: Float = 0.063f,
patternDepth: Float? = null,
patternChroma: Float? = null,
bgLOffset: Float = 0f,
saturationScale: Float = 1f,
contrastScale: Float = 1f,
patternIntensity: Float = 1f,
): FormulaResult = generateDarkFromSlots(hue, bgL, step, mutedChroma, colorChroma, DARK_SLOTS, false, saturationScale, contrastScale, patternIntensity)
): FormulaResult = generateDarkFromSlots(hue, bgL, step, mutedChroma, colorChroma, DARK_SLOTS, false, patternDepth, patternChroma, bgLOffset, saturationScale, contrastScale, patternIntensity)
fun generateSchemeBlack(
hue: Float,
step: Float = 0.04f,
colorChroma: Float = 0.0522f,
patternDepth: Float? = null,
patternChroma: Float? = null,
bgLOffset: Float = 0f,
saturationScale: Float = 1f,
contrastScale: Float = 1f,
patternIntensity: Float = 1f,
): FormulaResult = generateDarkFromSlots(hue, 0f, step, 0f, colorChroma, BLACK_SLOTS, true, saturationScale, contrastScale, patternIntensity)
): FormulaResult = generateDarkFromSlots(hue, 0f, step, 0f, colorChroma, BLACK_SLOTS, true, patternDepth, patternChroma, bgLOffset, saturationScale, contrastScale, patternIntensity)
private fun generateDarkFromSlots(
hue: Float, bgL: Float, step: Float,
mutedChroma: Float, colorChroma: Float,
slotDefs: Map<String, DarkSlotDef>,
patternPinsToP3: Boolean,
patternDepth: Float?, patternChroma: Float?, bgLOffset: Float,
saturationScale: Float, contrastScale: Float, patternIntensity: Float,
): FormulaResult {
val effStep = step * contrastScale
@@ -345,12 +352,20 @@ private fun generateDarkFromSlots(
var lMult = def.lightnessMult
var baseC = baselineC[name]!!
if (name == "pattern") {
lMult *= patternIntensity
baseC = bgCAnchor + (baseC - bgCAnchor) * patternIntensity
// patternDepth slider overrides slot's lightnessMult (and patternIntensity)
// directly; same semantic as LIGHT's patternDepth — multiplier on effStep.
lMult = patternDepth ?: (lMult * patternIntensity)
baseC = patternChroma ?: (bgCAnchor + (baseC - bgCAnchor) * patternIntensity)
}
val L = bgL + lMult * effStep
// bgLOffset nudges only the background slot, leaving bubbles/pattern at their
// formula-computed L (mirrors LIGHT's bgLOffset behaviour).
val L = if (name == "background") (bgL + bgLOffset).coerceIn(0f, 1f)
else bgL + lMult * effStep
val C = when {
name == "background" -> bgCAnchor
// patternChroma slider takes precedence over BLACK's max-pin behavior so
// the user can dial it back from the gamut edge.
name == "pattern" && patternChroma != null -> patternChroma
name == "pattern" && patternPinsToP3 -> maxChroma(L, hue)
else -> bgCAnchor + (baseC - bgCAnchor) * saturationScale
}.let { min(it, maxChroma(L, hue)) }
@@ -784,6 +784,8 @@ object AppearanceScope {
"hue" to bg.H.let { if (isBlack) sm.H else it },
"bgL" to bg.L, "step" to step,
"mutedChroma" to mutedC, "colorChroma" to sm.C,
"patternDepth" to if (step > 0f) (tint.L - bg.L) / step else 0f,
"patternChroma" to tint.C,
)
}
}
@@ -816,10 +818,16 @@ object AppearanceScope {
)
DefaultTheme.BLACK -> generateSchemeBlack(
hue.floatValue, step.floatValue, colorChroma.floatValue,
patternDepth = patternDepth.floatValue,
patternChroma = patternChromaVal.floatValue,
bgLOffset = bgLOffset.floatValue,
)
else -> generateSchemeDark(
hue.floatValue, bgL.floatValue, step.floatValue,
mutedChroma.floatValue, colorChroma.floatValue,
patternDepth = patternDepth.floatValue,
patternChroma = patternChromaVal.floatValue,
bgLOffset = bgLOffset.floatValue,
)
}
@@ -859,15 +867,21 @@ object AppearanceScope {
}
DefaultTheme.BLACK -> {
FormulaSlider("Hue", hue, 0f..360f)
FormulaSlider("BG Lightness", bgLOffset, -0.05f..0.05f)
FormulaSlider("Contrast", step, 0.01f..0.10f)
FormulaSlider("Accent chroma", colorChroma, 0f..0.20f)
FormulaSlider("Pattern depth", patternDepth, 0f..15f)
FormulaSlider("Pattern chroma", patternChromaVal, 0f..0.15f)
}
else -> {
FormulaSlider("Hue", hue, 0f..360f)
FormulaSlider("Lightness", bgL, 0.05f..0.30f)
FormulaSlider("BG Lightness", bgLOffset, -0.05f..0.05f)
FormulaSlider("Contrast", step, 0.01f..0.10f)
FormulaSlider("Accent chroma", colorChroma, 0f..0.20f)
FormulaSlider("Secondary chroma", mutedChroma, 0f..0.05f)
FormulaSlider("Pattern depth", patternDepth, 0f..10f)
FormulaSlider("Pattern chroma", patternChromaVal, 0f..0.15f)
}
}
SectionItemView({