From f36f4d607a0c34c69b7877437190bee22a7b3ebd Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 31 Mar 2026 14:55:52 +0200 Subject: [PATCH 1/2] feat(shadow): add darkShadowCurve for dark-scheme alpha dampening Apply a power curve to shadow alpha in dark scheme, compressing low/mid intensities while preserving alphaMax at full intensity. Made-with: Cursor --- .changeset/quiet-lions-dance.md | 5 ++ README.md | 2 + src/glaze.test.ts | 104 ++++++++++++++++++++++++++++++++ src/glaze.ts | 21 ++++++- src/types.ts | 8 +++ 5 files changed, 138 insertions(+), 2 deletions(-) create mode 100644 .changeset/quiet-lions-dance.md diff --git a/.changeset/quiet-lions-dance.md b/.changeset/quiet-lions-dance.md new file mode 100644 index 0000000..01a72db --- /dev/null +++ b/.changeset/quiet-lions-dance.md @@ -0,0 +1,5 @@ +--- +'@tenphi/glaze': patch +--- + +Add `darkShadowCurve` tuning parameter to dampen shadow alpha in dark scheme via a power curve, keeping low/mid intensities closer to their light-mode counterparts while preserving full alphaMax at high intensity. diff --git a/README.md b/README.md index df11eab..dee5fcb 100644 --- a/README.md +++ b/README.md @@ -390,6 +390,7 @@ Available tuning parameters: | `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness | | `alphaMax` | 0.6 | Asymptotic maximum alpha | | `bgHueBlend` | 0.2 | Blend weight pulling pigment hue toward bg hue | +| `darkShadowCurve` | 0.5 | Power curve for dark-scheme alpha (0-1). Lower = more dampening; 1 = no dampening | ### Standalone Shadow Computation @@ -963,6 +964,7 @@ glaze.configure({ shadowTuning: { // Default tuning for all shadow colors alphaMax: 0.6, bgHueBlend: 0.2, + darkShadowCurve: 0.5, // Power curve for dark-scheme alpha dampening (0-1) }, }); ``` diff --git a/src/glaze.test.ts b/src/glaze.test.ts index ca15378..c32202a 100644 --- a/src/glaze.test.ts +++ b/src/glaze.test.ts @@ -2317,6 +2317,110 @@ describe('glaze', () => { expect(shadow.light.alpha).toBeLessThan(0.3 + 0.001); }); + + it('darkShadowCurve=1 preserves old behavior (no dampening)', () => { + glaze.configure({ shadowTuning: { darkShadowCurve: 1 } }); + + const theme = glaze(280, 80); + theme.colors({ + surface: { lightness: 95 }, + text: { lightness: 15, base: 'surface', contrast: 'AA' }, + 'shadow-md': { + type: 'shadow', + bg: 'surface', + fg: 'text', + intensity: 10, + }, + }); + + const resolved = theme.resolve(); + const shadow = resolved.get('shadow-md')!; + + expect(shadow.dark.alpha).toBeGreaterThan(shadow.light.alpha * 2); + }); + + it('default darkShadowCurve dampens dark alpha', () => { + const themeUndampened = glaze(280, 80); + themeUndampened.colors({ + surface: { lightness: 95 }, + text: { lightness: 15, base: 'surface', contrast: 'AA' }, + 'shadow-md': { + type: 'shadow', + bg: 'surface', + fg: 'text', + intensity: 12, + tuning: { darkShadowCurve: 1 }, + }, + }); + + const themeDampened = glaze(280, 80); + themeDampened.colors({ + surface: { lightness: 95 }, + text: { lightness: 15, base: 'surface', contrast: 'AA' }, + 'shadow-md': { + type: 'shadow', + bg: 'surface', + fg: 'text', + intensity: 12, + }, + }); + + const undampened = themeUndampened.resolve().get('shadow-md')!; + const dampened = themeDampened.resolve().get('shadow-md')!; + + expect(undampened.light.alpha).toBeCloseTo(dampened.light.alpha, 6); + expect(dampened.dark.alpha).toBeLessThan(undampened.dark.alpha); + }); + + it('darkShadowCurve preserves endpoints (0 and alphaMax)', () => { + const theme = glaze(0, 0); + theme.colors({ + white: { lightness: 100, mode: 'static' }, + black: { lightness: 0, mode: 'static' }, + 'shadow-zero': { + type: 'shadow', + bg: 'white', + fg: 'black', + intensity: 0, + }, + 'shadow-full': { + type: 'shadow', + bg: 'white', + fg: 'black', + intensity: 100, + }, + }); + + const resolved = theme.resolve(); + + expect(resolved.get('shadow-zero')!.dark.alpha).toBe(0); + expect(resolved.get('shadow-full')!.dark.alpha).toBeCloseTo(1.0, 6); + }); + + it('per-color darkShadowCurve override works', () => { + const theme = glaze(280, 80); + theme.colors({ + surface: { lightness: 95 }, + 'shadow-default': { + type: 'shadow', + bg: 'surface', + intensity: 12, + }, + 'shadow-aggressive': { + type: 'shadow', + bg: 'surface', + intensity: 12, + tuning: { darkShadowCurve: 0.25 }, + }, + }); + + const resolved = theme.resolve(); + const def = resolved.get('shadow-default')!; + const agg = resolved.get('shadow-aggressive')!; + + expect(def.light.alpha).toBeCloseTo(agg.light.alpha, 6); + expect(agg.dark.alpha).toBeLessThan(def.dark.alpha); + }); }); describe('shadow validation', () => { diff --git a/src/glaze.ts b/src/glaze.ts index 217cd22..860eeed 100644 --- a/src/glaze.ts +++ b/src/glaze.ts @@ -103,6 +103,7 @@ const DEFAULT_SHADOW_TUNING: Required = { minGapTarget: 0.05, alphaMax: 1.0, bgHueBlend: 0.2, + darkShadowCurve: 0.5, }; function resolveShadowTuning(perColor?: ShadowTuning): Required { @@ -696,7 +697,15 @@ function resolveShadowForScheme( : pairNormal(def.intensity); const tuning = resolveShadowTuning(def.tuning); - return computeShadow(bgVariant, fgVariant, intensity, tuning); + const result = computeShadow(bgVariant, fgVariant, intensity, tuning); + + if (isDark && tuning.darkShadowCurve < 1 && result.alpha > 0) { + const normalized = result.alpha / tuning.alphaMax; + const exponent = 1 / tuning.darkShadowCurve; + result.alpha = tuning.alphaMax * Math.pow(normalized, exponent); + } + + return result; } function variantToLinearRgb(v: ResolvedColorVariant): LinearRgb { @@ -1632,12 +1641,20 @@ glaze.shadow = function shadow(input: GlazeShadowInput): ResolvedColorVariant { const bg = parseOkhslInput(input.bg); const fg = input.fg ? parseOkhslInput(input.fg) : undefined; const tuning = resolveShadowTuning(input.tuning); - return computeShadow( + const result = computeShadow( { ...bg, alpha: 1 }, fg ? { ...fg, alpha: 1 } : undefined, input.intensity, tuning, ); + + if (input.dark && tuning.darkShadowCurve < 1 && result.alpha > 0) { + const normalized = result.alpha / tuning.alphaMax; + const exponent = 1 / tuning.darkShadowCurve; + result.alpha = tuning.alphaMax * Math.pow(normalized, exponent); + } + + return result; }; /** diff --git a/src/types.ts b/src/types.ts index 98b185b..27ba52a 100644 --- a/src/types.ts +++ b/src/types.ts @@ -108,6 +108,12 @@ export interface ShadowTuning { * 0 = pure fg hue, 1 = pure bg hue. Default: 0.2. */ bgHueBlend?: number; + /** + * Power curve for dark-scheme shadow alpha (0-1). Default: 0.5. + * Lower values compress low/mid-intensity shadows more aggressively. + * 1.0 = no dampening (identity). + */ + darkShadowCurve?: number; } export interface ShadowColorDef { @@ -276,6 +282,8 @@ export interface GlazeShadowInput { /** Intensity 0-100. */ intensity: number; tuning?: ShadowTuning; + /** Whether to apply dark-scheme dampening. Default: false. */ + dark?: boolean; } // ============================================================================ From 7452fa153897cba09adbb10913c4040f9e4a769c Mon Sep 17 00:00:00 2001 From: Andrey Yamanov Date: Tue, 31 Mar 2026 15:34:50 +0200 Subject: [PATCH 2/2] fix(shadow): change darkShadowCurve default from 0.5 to 0.4 Made-with: Cursor --- README.md | 4 ++-- src/glaze.ts | 2 +- src/types.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index dee5fcb..2d23ccd 100644 --- a/README.md +++ b/README.md @@ -390,7 +390,7 @@ Available tuning parameters: | `minGapTarget` | 0.05 | Target minimum gap between pigment and bg lightness | | `alphaMax` | 0.6 | Asymptotic maximum alpha | | `bgHueBlend` | 0.2 | Blend weight pulling pigment hue toward bg hue | -| `darkShadowCurve` | 0.5 | Power curve for dark-scheme alpha (0-1). Lower = more dampening; 1 = no dampening | +| `darkShadowCurve` | 0.4 | Power curve for dark-scheme alpha (0-1). Lower = more dampening; 1 = no dampening | ### Standalone Shadow Computation @@ -964,7 +964,7 @@ glaze.configure({ shadowTuning: { // Default tuning for all shadow colors alphaMax: 0.6, bgHueBlend: 0.2, - darkShadowCurve: 0.5, // Power curve for dark-scheme alpha dampening (0-1) + darkShadowCurve: 0.4, // Power curve for dark-scheme alpha dampening (0-1) }, }); ``` diff --git a/src/glaze.ts b/src/glaze.ts index 860eeed..d8ec4fd 100644 --- a/src/glaze.ts +++ b/src/glaze.ts @@ -103,7 +103,7 @@ const DEFAULT_SHADOW_TUNING: Required = { minGapTarget: 0.05, alphaMax: 1.0, bgHueBlend: 0.2, - darkShadowCurve: 0.5, + darkShadowCurve: 0.4, }; function resolveShadowTuning(perColor?: ShadowTuning): Required { diff --git a/src/types.ts b/src/types.ts index 27ba52a..39c92a7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -109,7 +109,7 @@ export interface ShadowTuning { */ bgHueBlend?: number; /** - * Power curve for dark-scheme shadow alpha (0-1). Default: 0.5. + * Power curve for dark-scheme shadow alpha (0-1). Default: 0.4. * Lower values compress low/mid-intensity shadows more aggressively. * 1.0 = no dampening (identity). */