From 5b514fba68e3c1e52cf513e3d33a48c6b0acdf12 Mon Sep 17 00:00:00 2001 From: Brian Heston <47367562+bheston@users.noreply.github.com> Date: Fri, 23 Aug 2024 13:14:37 -0700 Subject: [PATCH] Adaptive UI: Added palette options for Okhsl (#223) --- ...-e0d5b689-bc54-4ebd-a792-599d0516bcb0.json | 7 ++ packages/adaptive-ui/docs/api-report.md | 12 +++- .../src/core/color/palette-okhsl.ts | 68 +++++++++++++++---- .../adaptive-ui/src/core/token-helpers.ts | 12 ++++ packages/adaptive-ui/src/reference/palette.ts | 35 +++++++--- 5 files changed, 111 insertions(+), 23 deletions(-) create mode 100644 change/@adaptive-web-adaptive-ui-e0d5b689-bc54-4ebd-a792-599d0516bcb0.json diff --git a/change/@adaptive-web-adaptive-ui-e0d5b689-bc54-4ebd-a792-599d0516bcb0.json b/change/@adaptive-web-adaptive-ui-e0d5b689-bc54-4ebd-a792-599d0516bcb0.json new file mode 100644 index 00000000..efdaea90 --- /dev/null +++ b/change/@adaptive-web-adaptive-ui-e0d5b689-bc54-4ebd-a792-599d0516bcb0.json @@ -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" +} diff --git a/packages/adaptive-ui/docs/api-report.md b/packages/adaptive-ui/docs/api-report.md index 101f7e0a..04ed6e5e 100644 --- a/packages/adaptive-ui/docs/api-report.md +++ b/packages/adaptive-ui/docs/api-report.md @@ -208,6 +208,9 @@ export function createTokenNonCss(name: string, type: DesignTokenType, intend // @public export function createTokenNumber(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedCSSDesignToken; +// @public +export function createTokenNumberNonStyling(name: string, intendedFor?: StyleProperty | StyleProperty[]): TypedDesignToken; + // @public export function createTokenRecipe(baseName: string, intendedFor: StyleProperty | StyleProperty[], evaluate: RecipeEvaluate): TypedDesignToken>; @@ -438,7 +441,14 @@ export type PaletteDirectionValue = typeof PaletteDirectionValue[keyof typeof Pa // @public export class PaletteOkhsl extends BasePalette { // (undocumented) - static from(source: Color | string): PaletteOkhsl; + static from(source: Color | string, options?: Partial): PaletteOkhsl; +} + +// @public +export interface PaletteOkhslOptions { + darkEndSaturation: number; + lightEndSaturation: number; + stepCount: number; } // @public diff --git a/packages/adaptive-ui/src/core/color/palette-okhsl.ts b/packages/adaptive-ui/src/core/color/palette-okhsl.ts index 8a3f4e8b..e772a7eb 100644 --- a/packages/adaptive-ui/src/core/color/palette-okhsl.ts +++ b/packages/adaptive-ui/src/core/color/palette-okhsl.ts @@ -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"; @@ -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. @@ -17,39 +52,46 @@ const threeSteps = (1 / stepCount) * 3; * @public */ export class PaletteOkhsl extends BasePalette { - public static from(source: Color | string): PaletteOkhsl { + public static from(source: Color | string, options?: Partial): 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); } diff --git a/packages/adaptive-ui/src/core/token-helpers.ts b/packages/adaptive-ui/src/core/token-helpers.ts index 5ea25b13..9ecfa335 100644 --- a/packages/adaptive-ui/src/core/token-helpers.ts +++ b/packages/adaptive-ui/src/core/token-helpers.ts @@ -153,6 +153,18 @@ export function createTokenNumber(name: string, intendedFor?: StyleProperty | St return TypedCSSDesignToken.createTyped(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 { + return TypedDesignToken.createTyped(name, DesignTokenType.number, intendedFor); +} + /** * Creates a DesignToken that can be used as a fill in styles. * diff --git a/packages/adaptive-ui/src/reference/palette.ts b/packages/adaptive-ui/src/reference/palette.ts index afd2d9ac..d010b638 100644 --- a/packages/adaptive-ui/src/reference/palette.ts +++ b/packages/adaptive-ui/src/reference/palette.ts @@ -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("color.neutral.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(neutralBaseColor)) + PaletteOkhsl.from(resolve(neutralBaseColor), resolvePaletteOkhslOptions(resolve)) ); /** @public */ @@ -19,7 +36,7 @@ export const accentBaseColor = createTokenColor("color.accent.base", StyleProper /** @public */ export const accentPalette = createTokenNonCss("color.accent.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(accentBaseColor)) + PaletteOkhsl.from(resolve(accentBaseColor), resolvePaletteOkhslOptions(resolve)) ); /** @public */ @@ -28,7 +45,7 @@ export const highlightBaseColor = createTokenColor("color.highlight.base", Style /** @public */ export const highlightPalette = createTokenNonCss("color.highlight.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(highlightBaseColor)) + PaletteOkhsl.from(resolve(highlightBaseColor), resolvePaletteOkhslOptions(resolve)) ); /** @public */ @@ -37,7 +54,7 @@ export const criticalBaseColor = createTokenColor("color.critical.base", StylePr /** @public */ export const criticalPalette = createTokenNonCss("color.critical.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(criticalBaseColor)) + PaletteOkhsl.from(resolve(criticalBaseColor), resolvePaletteOkhslOptions(resolve)) ); /** @public */ @@ -46,7 +63,7 @@ export const warningBaseColor = createTokenColor("color.warning.base", StyleProp /** @public */ export const warningPalette = createTokenNonCss("color.warning.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(warningBaseColor)) + PaletteOkhsl.from(resolve(warningBaseColor), resolvePaletteOkhslOptions(resolve)) ); /** @public */ @@ -55,7 +72,7 @@ export const successBaseColor = createTokenColor("color.success.base", StyleProp /** @public */ export const successPalette = createTokenNonCss("color.success.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(successBaseColor)) + PaletteOkhsl.from(resolve(successBaseColor), resolvePaletteOkhslOptions(resolve)) ); /** @public */ @@ -64,7 +81,7 @@ export const infoBaseColor = createTokenColor("color.info.base", StyleProperty.b /** @public */ export const infoPalette = createTokenNonCss("color.info.palette", DesignTokenType.palette).withDefault( (resolve: DesignTokenResolver) => - PaletteOkhsl.from(resolve(infoBaseColor)) + PaletteOkhsl.from(resolve(infoBaseColor), resolvePaletteOkhslOptions(resolve)) ); /**