Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/quiet-lions-dance.md
Original file line number Diff line number Diff line change
@@ -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.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.4 | Power curve for dark-scheme alpha (0-1). Lower = more dampening; 1 = no dampening |

### Standalone Shadow Computation

Expand Down Expand Up @@ -963,6 +964,7 @@ glaze.configure({
shadowTuning: { // Default tuning for all shadow colors
alphaMax: 0.6,
bgHueBlend: 0.2,
darkShadowCurve: 0.4, // Power curve for dark-scheme alpha dampening (0-1)
},
});
```
Expand Down
104 changes: 104 additions & 0 deletions src/glaze.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
21 changes: 19 additions & 2 deletions src/glaze.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ const DEFAULT_SHADOW_TUNING: Required<ShadowTuning> = {
minGapTarget: 0.05,
alphaMax: 1.0,
bgHueBlend: 0.2,
darkShadowCurve: 0.4,
};

function resolveShadowTuning(perColor?: ShadowTuning): Required<ShadowTuning> {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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;
};

/**
Expand Down
8 changes: 8 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.4.
* Lower values compress low/mid-intensity shadows more aggressively.
* 1.0 = no dampening (identity).
*/
darkShadowCurve?: number;
}

export interface ShadowColorDef {
Expand Down Expand Up @@ -276,6 +282,8 @@ export interface GlazeShadowInput {
/** Intensity 0-100. */
intensity: number;
tuning?: ShadowTuning;
/** Whether to apply dark-scheme dampening. Default: false. */
dark?: boolean;
}

// ============================================================================
Expand Down
Loading