Skip to content

Commit

Permalink
Adaptive UI: Added palette options for Okhsl (#223)
Browse files Browse the repository at this point in the history
  • Loading branch information
bheston authored Aug 23, 2024
1 parent a0cb2d1 commit 5b514fb
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "minor",
"comment": "Adaptive UI: Added palette options for Okhsl",
"packageName": "@adaptive-web/adaptive-ui",
"email": "47367562+bheston@users.noreply.github.com",
"dependentChangeType": "patch"
}
12 changes: 11 additions & 1 deletion packages/adaptive-ui/docs/api-report.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,9 @@ export function createTokenNonCss<T>(name: string, type: DesignTokenType, intend
// @public
export function createTokenNumber(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedCSSDesignToken<number>;

// @public
export function createTokenNumberNonStyling(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedDesignToken<number>;

// @public
export function createTokenRecipe<TParam, TResult>(baseName: string, intendedFor: StyleProperty | StyleProperty[], evaluate: RecipeEvaluate<TParam, TResult>): TypedDesignToken<Recipe<TParam, TResult>>;

Expand Down Expand Up @@ -438,7 +441,14 @@ export type PaletteDirectionValue = typeof PaletteDirectionValue[keyof typeof Pa
// @public
export class PaletteOkhsl extends BasePalette<Swatch> {
// (undocumented)
static from(source: Color | string): PaletteOkhsl;
static from(source: Color | string, options?: Partial<PaletteOkhslOptions>): PaletteOkhsl;
}

// @public
export interface PaletteOkhslOptions {
darkEndSaturation: number;
lightEndSaturation: number;
stepCount: number;
}

// @public
Expand Down
68 changes: 55 additions & 13 deletions packages/adaptive-ui/src/core/color/palette-okhsl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { clampChroma, interpolate, modeOkhsl, modeRgb, samples, useMode} from "culori/fn";
import { clampChroma, interpolate, modeOkhsl, modeRgb, samples, useMode } from "culori/fn";
import { Color } from "./color.js";
import { BasePalette } from "./palette-base.js";
import { Swatch } from "./swatch.js";
Expand All @@ -7,8 +7,43 @@ import { _black, _white } from "./utilities/color-constants.js";
const okhsl = useMode(modeOkhsl);
const rgb = useMode(modeRgb);

const stepCount = 66;
const threeSteps = (1 / stepCount) * 3;
/**
* Options to tailor the generation of PaletteOkhsl.
*
* @public
*/
export interface PaletteOkhslOptions {
/**
* The number of steps in the generated palette.
*
* @defaultValue 66
*/
stepCount: number;

/**
* The saturation of the color at the light end of the palette.
*
* @remarks Decimal value between 0 and 1; 0 means no change.
*
* @defaultValue 0
*/
lightEndSaturation: number;

/**
* The saturation of the color at the dark end of the palette.
*
* @remarks Decimal value between 0 and 1; 0 means no change.
*
* @defaultValue 0
*/
darkEndSaturation: number;
}

const defaultPaletteOkhslOptions: PaletteOkhslOptions = {
stepCount: 66,
lightEndSaturation: 0,
darkEndSaturation: 0,
};

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

const opts = (options === void 0 || options === null) ? defaultPaletteOkhslOptions : { ...defaultPaletteOkhslOptions, ...options };

const oneStep = (1 / opts.stepCount);
const threeSteps = oneStep * 3;

const sourceHsl = okhsl(color.color);

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

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

const rampCount = opts.stepCount - 2; // Ends fixed to white and black
const y = 1 - sourceHsl.l; // Position for the source color in the ramp
const stepCountLeft = Math.round(y * stepCount);
const stepCountRight = stepCount - stepCountLeft + 1;
const rampCountLeft = Math.round(y * rampCount);
const rampCountRight = rampCount - rampCountLeft + 1;
const colorsLeft: any = [hi, sourceHsl];
const colorsRight: any = [sourceHsl, lo];
const interpolateLeft = interpolate(colorsLeft, "okhsl");
const interpolateRight = interpolate(colorsRight, "okhsl");
const samplesLeft = samples(stepCountLeft).map(interpolateLeft);
const samplesRight = samples(stepCountRight).map(interpolateRight);
const samplesLeft = samples(rampCountLeft).map(interpolateLeft);
const samplesRight = samples(rampCountRight).map(interpolateRight);

const ramp = [...samplesLeft, ...samplesRight.slice(1)];
const swatches = ramp.map((value) =>
const rampSwatches = ramp.map((value) =>
Swatch.from(rgb(clampChroma(value, "okhsl")))
);

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

return new PaletteOkhsl(color, swatches);
}
Expand Down
12 changes: 12 additions & 0 deletions packages/adaptive-ui/src/core/token-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,18 @@ export function createTokenNumber(name: string, intendedFor?: StyleProperty | St
return TypedCSSDesignToken.createTyped<number>(name, DesignTokenType.number, intendedFor);
}

/**
* Creates a DesignToken for number values that can be used by other DesignTokens, but not directly in styles.
*
* @param name - The token name in `css-identifier` casing.
* @param intendedFor - The style properties where this token is intended to be used.
*
* @public
*/
export function createTokenNumberNonStyling(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedDesignToken<number> {
return TypedDesignToken.createTyped<number>(name, DesignTokenType.number, intendedFor);
}

/**
* Creates a DesignToken that can be used as a fill in styles.
*
Expand Down
35 changes: 26 additions & 9 deletions packages/adaptive-ui/src/reference/palette.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import type { DesignTokenResolver } from "@microsoft/fast-foundation";
import { DesignTokenType } from "../core/adaptive-design-tokens.js";
import { Color, Palette, PaletteOkhsl } from "../core/color/index.js";
import { createTokenColor, createTokenNonCss } from "../core/token-helpers.js";
import { Color, Palette, PaletteOkhsl, PaletteOkhslOptions } from "../core/color/index.js";
import { createTokenColor, createTokenNonCss, createTokenNumberNonStyling } from "../core/token-helpers.js";
import { StyleProperty } from "../core/modules/types.js";

/** @public */
export const paletteStepCount = createTokenNumberNonStyling("color.palette.stepCount").withDefault(66);

/** @public */
export const paletteLightEndSaturation = createTokenNumberNonStyling("color.palette.lightEndSaturation").withDefault(0);

/** @public */
export const paletteDarkEndSaturation = createTokenNumberNonStyling("color.palette.darkEndSaturation").withDefault(0);

const resolvePaletteOkhslOptions: (resolve: DesignTokenResolver) => PaletteOkhslOptions = (resolve: DesignTokenResolver) => {
return {
stepCount: resolve(paletteStepCount),
lightEndSaturation: resolve(paletteLightEndSaturation),
darkEndSaturation: resolve(paletteDarkEndSaturation),
};
};

/** @public */
export const neutralBaseColor = createTokenColor("color.neutral.base", StyleProperty.backgroundFill).withDefault(Color.parse("#808080")!);

/** @public */
export const neutralPalette = createTokenNonCss<Palette>("color.neutral.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(neutralBaseColor))
PaletteOkhsl.from(resolve(neutralBaseColor), resolvePaletteOkhslOptions(resolve))
);

/** @public */
Expand All @@ -19,7 +36,7 @@ export const accentBaseColor = createTokenColor("color.accent.base", StyleProper
/** @public */
export const accentPalette = createTokenNonCss<Palette>("color.accent.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(accentBaseColor))
PaletteOkhsl.from(resolve(accentBaseColor), resolvePaletteOkhslOptions(resolve))
);

/** @public */
Expand All @@ -28,7 +45,7 @@ export const highlightBaseColor = createTokenColor("color.highlight.base", Style
/** @public */
export const highlightPalette = createTokenNonCss<Palette>("color.highlight.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(highlightBaseColor))
PaletteOkhsl.from(resolve(highlightBaseColor), resolvePaletteOkhslOptions(resolve))
);

/** @public */
Expand All @@ -37,7 +54,7 @@ export const criticalBaseColor = createTokenColor("color.critical.base", StylePr
/** @public */
export const criticalPalette = createTokenNonCss<Palette>("color.critical.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(criticalBaseColor))
PaletteOkhsl.from(resolve(criticalBaseColor), resolvePaletteOkhslOptions(resolve))
);

/** @public */
Expand All @@ -46,7 +63,7 @@ export const warningBaseColor = createTokenColor("color.warning.base", StyleProp
/** @public */
export const warningPalette = createTokenNonCss<Palette>("color.warning.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(warningBaseColor))
PaletteOkhsl.from(resolve(warningBaseColor), resolvePaletteOkhslOptions(resolve))
);

/** @public */
Expand All @@ -55,7 +72,7 @@ export const successBaseColor = createTokenColor("color.success.base", StyleProp
/** @public */
export const successPalette = createTokenNonCss<Palette>("color.success.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(successBaseColor))
PaletteOkhsl.from(resolve(successBaseColor), resolvePaletteOkhslOptions(resolve))
);

/** @public */
Expand All @@ -64,7 +81,7 @@ export const infoBaseColor = createTokenColor("color.info.base", StyleProperty.b
/** @public */
export const infoPalette = createTokenNonCss<Palette>("color.info.palette", DesignTokenType.palette).withDefault(
(resolve: DesignTokenResolver) =>
PaletteOkhsl.from(resolve(infoBaseColor))
PaletteOkhsl.from(resolve(infoBaseColor), resolvePaletteOkhslOptions(resolve))
);

/**
Expand Down

0 comments on commit 5b514fb

Please sign in to comment.