Skip to content

Commit 5b514fb

Browse files
authored
Adaptive UI: Added palette options for Okhsl (#223)
1 parent a0cb2d1 commit 5b514fb

5 files changed

Lines changed: 111 additions & 23 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"type": "minor",
3+
"comment": "Adaptive UI: Added palette options for Okhsl",
4+
"packageName": "@adaptive-web/adaptive-ui",
5+
"email": "47367562+bheston@users.noreply.github.com",
6+
"dependentChangeType": "patch"
7+
}

packages/adaptive-ui/docs/api-report.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,9 @@ export function createTokenNonCss<T>(name: string, type: DesignTokenType, intend
208208
// @public
209209
export function createTokenNumber(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedCSSDesignToken<number>;
210210

211+
// @public
212+
export function createTokenNumberNonStyling(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedDesignToken<number>;
213+
211214
// @public
212215
export function createTokenRecipe<TParam, TResult>(baseName: string, intendedFor: StyleProperty | StyleProperty[], evaluate: RecipeEvaluate<TParam, TResult>): TypedDesignToken<Recipe<TParam, TResult>>;
213216

@@ -438,7 +441,14 @@ export type PaletteDirectionValue = typeof PaletteDirectionValue[keyof typeof Pa
438441
// @public
439442
export class PaletteOkhsl extends BasePalette<Swatch> {
440443
// (undocumented)
441-
static from(source: Color | string): PaletteOkhsl;
444+
static from(source: Color | string, options?: Partial<PaletteOkhslOptions>): PaletteOkhsl;
445+
}
446+
447+
// @public
448+
export interface PaletteOkhslOptions {
449+
darkEndSaturation: number;
450+
lightEndSaturation: number;
451+
stepCount: number;
442452
}
443453

444454
// @public

packages/adaptive-ui/src/core/color/palette-okhsl.ts

Lines changed: 55 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { clampChroma, interpolate, modeOkhsl, modeRgb, samples, useMode} from "culori/fn";
1+
import { clampChroma, interpolate, modeOkhsl, modeRgb, samples, useMode } from "culori/fn";
22
import { Color } from "./color.js";
33
import { BasePalette } from "./palette-base.js";
44
import { Swatch } from "./swatch.js";
@@ -7,8 +7,43 @@ import { _black, _white } from "./utilities/color-constants.js";
77
const okhsl = useMode(modeOkhsl);
88
const rgb = useMode(modeRgb);
99

10-
const stepCount = 66;
11-
const threeSteps = (1 / stepCount) * 3;
10+
/**
11+
* Options to tailor the generation of PaletteOkhsl.
12+
*
13+
* @public
14+
*/
15+
export interface PaletteOkhslOptions {
16+
/**
17+
* The number of steps in the generated palette.
18+
*
19+
* @defaultValue 66
20+
*/
21+
stepCount: number;
22+
23+
/**
24+
* The saturation of the color at the light end of the palette.
25+
*
26+
* @remarks Decimal value between 0 and 1; 0 means no change.
27+
*
28+
* @defaultValue 0
29+
*/
30+
lightEndSaturation: number;
31+
32+
/**
33+
* The saturation of the color at the dark end of the palette.
34+
*
35+
* @remarks Decimal value between 0 and 1; 0 means no change.
36+
*
37+
* @defaultValue 0
38+
*/
39+
darkEndSaturation: number;
40+
}
41+
42+
const defaultPaletteOkhslOptions: PaletteOkhslOptions = {
43+
stepCount: 66,
44+
lightEndSaturation: 0,
45+
darkEndSaturation: 0,
46+
};
1247

1348
/**
1449
* An implementation of a {@link Palette} that uses the okhsl color model.
@@ -17,39 +52,46 @@ const threeSteps = (1 / stepCount) * 3;
1752
* @public
1853
*/
1954
export class PaletteOkhsl extends BasePalette<Swatch> {
20-
public static from(source: Color | string): PaletteOkhsl {
55+
public static from(source: Color | string, options?: Partial<PaletteOkhslOptions>): PaletteOkhsl {
2156
const color = source instanceof Color ? source : Color.parse(source);
2257
if (!color) {
2358
throw new Error(`Unable to parse source: ${source}`);
2459
}
2560

61+
const opts = (options === void 0 || options === null) ? defaultPaletteOkhslOptions : { ...defaultPaletteOkhslOptions, ...options };
62+
63+
const oneStep = (1 / opts.stepCount);
64+
const threeSteps = oneStep * 3;
65+
2666
const sourceHsl = okhsl(color.color);
2767

28-
const hi = Object.assign({}, sourceHsl, {l: 0.999});
29-
const lo = Object.assign({}, sourceHsl, {l: 0.02});
68+
const hiS = sourceHsl.s > 0 && opts.lightEndSaturation > 0 ? opts.lightEndSaturation : sourceHsl.s;
69+
const loS = sourceHsl.s > 0 && opts.darkEndSaturation > 0 ? opts.darkEndSaturation : sourceHsl.s;
70+
const hi = Object.assign({}, sourceHsl, { s: hiS, l: 1 - oneStep });
71+
const lo = Object.assign({}, sourceHsl, { s: loS, l: Math.max(oneStep, 0.04) }); // Minimum value to perceive difference
3072

3173
// Adjust the hi or lo end if the source color is too close to produce a good ramp.
3274
sourceHsl.l = Math.min(1 - threeSteps, sourceHsl.l);
3375
sourceHsl.l = Math.max(threeSteps, sourceHsl.l);
3476

77+
const rampCount = opts.stepCount - 2; // Ends fixed to white and black
3578
const y = 1 - sourceHsl.l; // Position for the source color in the ramp
36-
const stepCountLeft = Math.round(y * stepCount);
37-
const stepCountRight = stepCount - stepCountLeft + 1;
79+
const rampCountLeft = Math.round(y * rampCount);
80+
const rampCountRight = rampCount - rampCountLeft + 1;
3881
const colorsLeft: any = [hi, sourceHsl];
3982
const colorsRight: any = [sourceHsl, lo];
4083
const interpolateLeft = interpolate(colorsLeft, "okhsl");
4184
const interpolateRight = interpolate(colorsRight, "okhsl");
42-
const samplesLeft = samples(stepCountLeft).map(interpolateLeft);
43-
const samplesRight = samples(stepCountRight).map(interpolateRight);
85+
const samplesLeft = samples(rampCountLeft).map(interpolateLeft);
86+
const samplesRight = samples(rampCountRight).map(interpolateRight);
4487

4588
const ramp = [...samplesLeft, ...samplesRight.slice(1)];
46-
const swatches = ramp.map((value) =>
89+
const rampSwatches = ramp.map((value) =>
4790
Swatch.from(rgb(clampChroma(value, "okhsl")))
4891
);
4992

5093
// It's important that the ends are full white and black.
51-
swatches[0] = _white;
52-
swatches[swatches.length - 1] = _black;
94+
const swatches = [_white, ...rampSwatches, _black];
5395

5496
return new PaletteOkhsl(color, swatches);
5597
}

packages/adaptive-ui/src/core/token-helpers.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,18 @@ export function createTokenNumber(name: string, intendedFor?: StyleProperty | St
153153
return TypedCSSDesignToken.createTyped<number>(name, DesignTokenType.number, intendedFor);
154154
}
155155

156+
/**
157+
* Creates a DesignToken for number values that can be used by other DesignTokens, but not directly in styles.
158+
*
159+
* @param name - The token name in `css-identifier` casing.
160+
* @param intendedFor - The style properties where this token is intended to be used.
161+
*
162+
* @public
163+
*/
164+
export function createTokenNumberNonStyling(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedDesignToken<number> {
165+
return TypedDesignToken.createTyped<number>(name, DesignTokenType.number, intendedFor);
166+
}
167+
156168
/**
157169
* Creates a DesignToken that can be used as a fill in styles.
158170
*

packages/adaptive-ui/src/reference/palette.ts

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,33 @@
11
import type { DesignTokenResolver } from "@microsoft/fast-foundation";
22
import { DesignTokenType } from "../core/adaptive-design-tokens.js";
3-
import { Color, Palette, PaletteOkhsl } from "../core/color/index.js";
4-
import { createTokenColor, createTokenNonCss } from "../core/token-helpers.js";
3+
import { Color, Palette, PaletteOkhsl, PaletteOkhslOptions } from "../core/color/index.js";
4+
import { createTokenColor, createTokenNonCss, createTokenNumberNonStyling } from "../core/token-helpers.js";
55
import { StyleProperty } from "../core/modules/types.js";
66

7+
/** @public */
8+
export const paletteStepCount = createTokenNumberNonStyling("color.palette.stepCount").withDefault(66);
9+
10+
/** @public */
11+
export const paletteLightEndSaturation = createTokenNumberNonStyling("color.palette.lightEndSaturation").withDefault(0);
12+
13+
/** @public */
14+
export const paletteDarkEndSaturation = createTokenNumberNonStyling("color.palette.darkEndSaturation").withDefault(0);
15+
16+
const resolvePaletteOkhslOptions: (resolve: DesignTokenResolver) => PaletteOkhslOptions = (resolve: DesignTokenResolver) => {
17+
return {
18+
stepCount: resolve(paletteStepCount),
19+
lightEndSaturation: resolve(paletteLightEndSaturation),
20+
darkEndSaturation: resolve(paletteDarkEndSaturation),
21+
};
22+
};
23+
724
/** @public */
825
export const neutralBaseColor = createTokenColor("color.neutral.base", StyleProperty.backgroundFill).withDefault(Color.parse("#808080")!);
926

1027
/** @public */
1128
export const neutralPalette = createTokenNonCss<Palette>("color.neutral.palette", DesignTokenType.palette).withDefault(
1229
(resolve: DesignTokenResolver) =>
13-
PaletteOkhsl.from(resolve(neutralBaseColor))
30+
PaletteOkhsl.from(resolve(neutralBaseColor), resolvePaletteOkhslOptions(resolve))
1431
);
1532

1633
/** @public */
@@ -19,7 +36,7 @@ export const accentBaseColor = createTokenColor("color.accent.base", StyleProper
1936
/** @public */
2037
export const accentPalette = createTokenNonCss<Palette>("color.accent.palette", DesignTokenType.palette).withDefault(
2138
(resolve: DesignTokenResolver) =>
22-
PaletteOkhsl.from(resolve(accentBaseColor))
39+
PaletteOkhsl.from(resolve(accentBaseColor), resolvePaletteOkhslOptions(resolve))
2340
);
2441

2542
/** @public */
@@ -28,7 +45,7 @@ export const highlightBaseColor = createTokenColor("color.highlight.base", Style
2845
/** @public */
2946
export const highlightPalette = createTokenNonCss<Palette>("color.highlight.palette", DesignTokenType.palette).withDefault(
3047
(resolve: DesignTokenResolver) =>
31-
PaletteOkhsl.from(resolve(highlightBaseColor))
48+
PaletteOkhsl.from(resolve(highlightBaseColor), resolvePaletteOkhslOptions(resolve))
3249
);
3350

3451
/** @public */
@@ -37,7 +54,7 @@ export const criticalBaseColor = createTokenColor("color.critical.base", StylePr
3754
/** @public */
3855
export const criticalPalette = createTokenNonCss<Palette>("color.critical.palette", DesignTokenType.palette).withDefault(
3956
(resolve: DesignTokenResolver) =>
40-
PaletteOkhsl.from(resolve(criticalBaseColor))
57+
PaletteOkhsl.from(resolve(criticalBaseColor), resolvePaletteOkhslOptions(resolve))
4158
);
4259

4360
/** @public */
@@ -46,7 +63,7 @@ export const warningBaseColor = createTokenColor("color.warning.base", StyleProp
4663
/** @public */
4764
export const warningPalette = createTokenNonCss<Palette>("color.warning.palette", DesignTokenType.palette).withDefault(
4865
(resolve: DesignTokenResolver) =>
49-
PaletteOkhsl.from(resolve(warningBaseColor))
66+
PaletteOkhsl.from(resolve(warningBaseColor), resolvePaletteOkhslOptions(resolve))
5067
);
5168

5269
/** @public */
@@ -55,7 +72,7 @@ export const successBaseColor = createTokenColor("color.success.base", StyleProp
5572
/** @public */
5673
export const successPalette = createTokenNonCss<Palette>("color.success.palette", DesignTokenType.palette).withDefault(
5774
(resolve: DesignTokenResolver) =>
58-
PaletteOkhsl.from(resolve(successBaseColor))
75+
PaletteOkhsl.from(resolve(successBaseColor), resolvePaletteOkhslOptions(resolve))
5976
);
6077

6178
/** @public */
@@ -64,7 +81,7 @@ export const infoBaseColor = createTokenColor("color.info.base", StyleProperty.b
6481
/** @public */
6582
export const infoPalette = createTokenNonCss<Palette>("color.info.palette", DesignTokenType.palette).withDefault(
6683
(resolve: DesignTokenResolver) =>
67-
PaletteOkhsl.from(resolve(infoBaseColor))
84+
PaletteOkhsl.from(resolve(infoBaseColor), resolvePaletteOkhslOptions(resolve))
6885
);
6986

7087
/**

0 commit comments

Comments
 (0)