Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"type": "patch",
"comment": "Adaptive UI: Fix circular palette reference",
"packageName": "@adaptive-web/adaptive-ui",
"email": "47367562+bheston@users.noreply.github.com",
"dependentChangeType": "patch"
}
1 change: 1 addition & 0 deletions packages/adaptive-ui/src/color/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export * from "./recipes/index.js";
export * from "./utilities/index.js";
export * from "./color.js";
export * from "./palette-base.js";
export * from "./palette-okhsl.js";
export * from "./palette-rgb.js";
export * from "./palette.js";
Expand Down
140 changes: 140 additions & 0 deletions packages/adaptive-ui/src/color/palette-base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { Color } from "./color.js";
import { Palette, PaletteDirection, PaletteDirectionValue, resolvePaletteDirection } from "./palette.js";
import { Swatch } from "./swatch.js";
import { binarySearch } from "./utilities/binary-search.js";
import { directionByIsDark } from "./utilities/direction-by-is-dark.js";
import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js";

/**
* A base {@link Palette} with a common implementation of the interface. Use PaletteRGB for an implementation
* of a palette generation algorithm that is ready to be used directly, or extend this class to generate custom Swatches.
*
* @public
*/
export class BasePalette<T extends Swatch> implements Palette<T> {
/**
* {@inheritdoc Palette.source}
*/
readonly source: Color;

/**
* {@inheritdoc Palette.swatches}
*/
readonly swatches: ReadonlyArray<T>;

/**
* An index pointer to the end of the palette.
*/
readonly lastIndex: number;

/**
* A copy of the `Swatch`es in reverse order, used for optimized searching.
*/
readonly reversedSwatches: ReadonlyArray<T>;

/**
* Cache from `relativeLuminance` to `Swatch` index in the `Palette`.
*/
readonly closestIndexCache = new Map<number, number>();

/**
* Creates a new Palette.
*
* @param source - The source color for the Palette
* @param swatches - All Swatches in the Palette
*/
constructor(source: Color, swatches: ReadonlyArray<T>) {
this.source = source;
this.swatches = swatches;

this.reversedSwatches = Object.freeze([...this.swatches].reverse());
this.lastIndex = this.swatches.length - 1;
}

/**
* {@inheritdoc Palette.colorContrast}
*/
colorContrast(
reference: RelativeLuminance,
contrastTarget: number,
initialSearchIndex?: number,
direction: PaletteDirection = directionByIsDark(reference)
): T {
if (initialSearchIndex === undefined) {
initialSearchIndex = this.closestIndexOf(reference);
}

let source: ReadonlyArray<T> = this.swatches;
const endSearchIndex = this.lastIndex;
let startSearchIndex = initialSearchIndex;

const condition = (value: T) => contrast(reference, value) >= contrastTarget;

if (direction === PaletteDirectionValue.lighter) {
source = this.reversedSwatches;
startSearchIndex = endSearchIndex - startSearchIndex;
}

return binarySearch(source, condition, startSearchIndex, endSearchIndex);
}

/**
* {@inheritdoc Palette.delta}
*/
delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T {
const dir = resolvePaletteDirection(direction);
return this.get(this.closestIndexOf(reference) + dir * delta);
}

/**
* {@inheritdoc Palette.closestIndexOf}
*/
closestIndexOf(reference: RelativeLuminance): number {
if (this.closestIndexCache.has(reference.relativeLuminance)) {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
return this.closestIndexCache.get(reference.relativeLuminance)!;
}

let index = this.swatches.indexOf(reference as T);

if (index !== -1) {
this.closestIndexCache.set(reference.relativeLuminance, index);
return index;
}

const closest = this.swatches.reduce((previous, next) =>
Math.abs(next.relativeLuminance - reference.relativeLuminance) <
Math.abs(previous.relativeLuminance - reference.relativeLuminance)
? next
: previous
);

index = this.swatches.indexOf(closest);
this.closestIndexCache.set(reference.relativeLuminance, index);

return index;
}

/**
* Ensures that an input number does not exceed a max value and is not less than a min value.
*
* @param i - the number to clamp
* @param min - the maximum (inclusive) value
* @param max - the minimum (inclusive) value
*/
private clamp(i: number, min: number, max: number): number {
if (isNaN(i) || i <= min) {
return min;
} else if (i >= max) {
return max;
}
return i;
}

/**
* {@inheritdoc Palette.get}
*/
get(index: number): T {
return this.swatches[index] || this.swatches[this.clamp(index, 0, this.lastIndex)];
}
}
2 changes: 1 addition & 1 deletion packages/adaptive-ui/src/color/palette-okhsl.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { clampChroma, interpolate, modeOkhsl, modeRgb, samples, useMode} from "culori/fn";
import { Color } from "./color.js";
import { BasePalette } from "./palette.js";
import { BasePalette } from "./palette-base.js";
import { Swatch } from "./swatch.js";
import { _black, _white } from "./utilities/color-constants.js";

Expand Down
2 changes: 1 addition & 1 deletion packages/adaptive-ui/src/color/palette-rgb.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { clampRgb, type Hsl, interpolate, modeHsl, modeLab, modeRgb, type Rgb, useMode } from "culori/fn";
import { BasePalette } from "./palette.js";
import { BasePalette } from "./palette-base.js";
import { Swatch } from "./swatch.js";
import { contrast } from "./utilities/relative-luminance.js";
import { _black, _white } from "./utilities/color-constants.js";
Expand Down
138 changes: 1 addition & 137 deletions packages/adaptive-ui/src/color/palette.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
import { Color } from "./color.js";
import { Swatch } from "./swatch.js";
import { binarySearch } from "./utilities/binary-search.js";
import { directionByIsDark } from "./utilities/direction-by-is-dark.js";
import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js";
import { RelativeLuminance } from "./utilities/relative-luminance.js";

/**
* Directional values for navigating {@link Swatch}es in {@link Palette}.
Expand Down Expand Up @@ -111,137 +109,3 @@ export interface Palette<T extends Swatch = Swatch> {
*/
get(index: number): T;
}

/**
* A base {@link Palette} with a common implementation of the interface. Use PaletteRGB for an implementation
* of a palette generation algorithm that is ready to be used directly, or extend this class to generate custom Swatches.
*
* @public
*/
export class BasePalette<T extends Swatch> implements Palette<T> {
/**
* {@inheritdoc Palette.source}
*/
readonly source: Color;

/**
* {@inheritdoc Palette.swatches}
*/
readonly swatches: ReadonlyArray<T>;

/**
* An index pointer to the end of the palette.
*/
readonly lastIndex: number;

/**
* A copy of the `Swatch`es in reverse order, used for optimized searching.
*/
readonly reversedSwatches: ReadonlyArray<T>;

/**
* Cache from `relativeLuminance` to `Swatch` index in the `Palette`.
*/
readonly closestIndexCache = new Map<number, number>();

/**
* Creates a new Palette.
*
* @param source - The source color for the Palette
* @param swatches - All Swatches in the Palette
*/
constructor(source: Color, swatches: ReadonlyArray<T>) {
this.source = source;
this.swatches = swatches;

this.reversedSwatches = Object.freeze([...this.swatches].reverse());
this.lastIndex = this.swatches.length - 1;
}

/**
* {@inheritdoc Palette.colorContrast}
*/
colorContrast(
reference: RelativeLuminance,
contrastTarget: number,
initialSearchIndex?: number,
direction: PaletteDirection = directionByIsDark(reference)
): T {
if (initialSearchIndex === undefined) {
initialSearchIndex = this.closestIndexOf(reference);
}

let source: ReadonlyArray<T> = this.swatches;
const endSearchIndex = this.lastIndex;
let startSearchIndex = initialSearchIndex;

const condition = (value: T) => contrast(reference, value) >= contrastTarget;

if (direction === PaletteDirectionValue.lighter) {
source = this.reversedSwatches;
startSearchIndex = endSearchIndex - startSearchIndex;
}

return binarySearch(source, condition, startSearchIndex, endSearchIndex);
}

/**
* {@inheritdoc Palette.delta}
*/
delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T {
const dir = resolvePaletteDirection(direction);
return this.get(this.closestIndexOf(reference) + dir * delta);
}

/**
* {@inheritdoc Palette.closestIndexOf}
*/
closestIndexOf(reference: RelativeLuminance): number {
if (this.closestIndexCache.has(reference.relativeLuminance)) {
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
return this.closestIndexCache.get(reference.relativeLuminance)!;
}

let index = this.swatches.indexOf(reference as T);

if (index !== -1) {
this.closestIndexCache.set(reference.relativeLuminance, index);
return index;
}

const closest = this.swatches.reduce((previous, next) =>
Math.abs(next.relativeLuminance - reference.relativeLuminance) <
Math.abs(previous.relativeLuminance - reference.relativeLuminance)
? next
: previous
);

index = this.swatches.indexOf(closest);
this.closestIndexCache.set(reference.relativeLuminance, index);

return index;
}

/**
* Ensures that an input number does not exceed a max value and is not less than a min value.
*
* @param i - the number to clamp
* @param min - the maximum (inclusive) value
* @param max - the minimum (inclusive) value
*/
private clamp(i: number, min: number, max: number): number {
if (isNaN(i) || i <= min) {
return min;
} else if (i >= max) {
return max;
}
return i;
}

/**
* {@inheritdoc Palette.get}
*/
get(index: number): T {
return this.swatches[index] || this.swatches[this.clamp(index, 0, this.lastIndex)];
}
}