Skip to content

Commit 4397a79

Browse files
authored
Adaptive UI: Fix circular palette reference (#180)
1 parent 2f5a03d commit 4397a79

File tree

6 files changed

+151
-139
lines changed

6 files changed

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

packages/adaptive-ui/src/color/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from "./recipes/index.js";
22
export * from "./utilities/index.js";
33
export * from "./color.js";
4+
export * from "./palette-base.js";
45
export * from "./palette-okhsl.js";
56
export * from "./palette-rgb.js";
67
export * from "./palette.js";
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
import { Color } from "./color.js";
2+
import { Palette, PaletteDirection, PaletteDirectionValue, resolvePaletteDirection } from "./palette.js";
3+
import { Swatch } from "./swatch.js";
4+
import { binarySearch } from "./utilities/binary-search.js";
5+
import { directionByIsDark } from "./utilities/direction-by-is-dark.js";
6+
import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js";
7+
8+
/**
9+
* A base {@link Palette} with a common implementation of the interface. Use PaletteRGB for an implementation
10+
* of a palette generation algorithm that is ready to be used directly, or extend this class to generate custom Swatches.
11+
*
12+
* @public
13+
*/
14+
export class BasePalette<T extends Swatch> implements Palette<T> {
15+
/**
16+
* {@inheritdoc Palette.source}
17+
*/
18+
readonly source: Color;
19+
20+
/**
21+
* {@inheritdoc Palette.swatches}
22+
*/
23+
readonly swatches: ReadonlyArray<T>;
24+
25+
/**
26+
* An index pointer to the end of the palette.
27+
*/
28+
readonly lastIndex: number;
29+
30+
/**
31+
* A copy of the `Swatch`es in reverse order, used for optimized searching.
32+
*/
33+
readonly reversedSwatches: ReadonlyArray<T>;
34+
35+
/**
36+
* Cache from `relativeLuminance` to `Swatch` index in the `Palette`.
37+
*/
38+
readonly closestIndexCache = new Map<number, number>();
39+
40+
/**
41+
* Creates a new Palette.
42+
*
43+
* @param source - The source color for the Palette
44+
* @param swatches - All Swatches in the Palette
45+
*/
46+
constructor(source: Color, swatches: ReadonlyArray<T>) {
47+
this.source = source;
48+
this.swatches = swatches;
49+
50+
this.reversedSwatches = Object.freeze([...this.swatches].reverse());
51+
this.lastIndex = this.swatches.length - 1;
52+
}
53+
54+
/**
55+
* {@inheritdoc Palette.colorContrast}
56+
*/
57+
colorContrast(
58+
reference: RelativeLuminance,
59+
contrastTarget: number,
60+
initialSearchIndex?: number,
61+
direction: PaletteDirection = directionByIsDark(reference)
62+
): T {
63+
if (initialSearchIndex === undefined) {
64+
initialSearchIndex = this.closestIndexOf(reference);
65+
}
66+
67+
let source: ReadonlyArray<T> = this.swatches;
68+
const endSearchIndex = this.lastIndex;
69+
let startSearchIndex = initialSearchIndex;
70+
71+
const condition = (value: T) => contrast(reference, value) >= contrastTarget;
72+
73+
if (direction === PaletteDirectionValue.lighter) {
74+
source = this.reversedSwatches;
75+
startSearchIndex = endSearchIndex - startSearchIndex;
76+
}
77+
78+
return binarySearch(source, condition, startSearchIndex, endSearchIndex);
79+
}
80+
81+
/**
82+
* {@inheritdoc Palette.delta}
83+
*/
84+
delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T {
85+
const dir = resolvePaletteDirection(direction);
86+
return this.get(this.closestIndexOf(reference) + dir * delta);
87+
}
88+
89+
/**
90+
* {@inheritdoc Palette.closestIndexOf}
91+
*/
92+
closestIndexOf(reference: RelativeLuminance): number {
93+
if (this.closestIndexCache.has(reference.relativeLuminance)) {
94+
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
95+
return this.closestIndexCache.get(reference.relativeLuminance)!;
96+
}
97+
98+
let index = this.swatches.indexOf(reference as T);
99+
100+
if (index !== -1) {
101+
this.closestIndexCache.set(reference.relativeLuminance, index);
102+
return index;
103+
}
104+
105+
const closest = this.swatches.reduce((previous, next) =>
106+
Math.abs(next.relativeLuminance - reference.relativeLuminance) <
107+
Math.abs(previous.relativeLuminance - reference.relativeLuminance)
108+
? next
109+
: previous
110+
);
111+
112+
index = this.swatches.indexOf(closest);
113+
this.closestIndexCache.set(reference.relativeLuminance, index);
114+
115+
return index;
116+
}
117+
118+
/**
119+
* Ensures that an input number does not exceed a max value and is not less than a min value.
120+
*
121+
* @param i - the number to clamp
122+
* @param min - the maximum (inclusive) value
123+
* @param max - the minimum (inclusive) value
124+
*/
125+
private clamp(i: number, min: number, max: number): number {
126+
if (isNaN(i) || i <= min) {
127+
return min;
128+
} else if (i >= max) {
129+
return max;
130+
}
131+
return i;
132+
}
133+
134+
/**
135+
* {@inheritdoc Palette.get}
136+
*/
137+
get(index: number): T {
138+
return this.swatches[index] || this.swatches[this.clamp(index, 0, this.lastIndex)];
139+
}
140+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { clampChroma, interpolate, modeOkhsl, modeRgb, samples, useMode} from "culori/fn";
22
import { Color } from "./color.js";
3-
import { BasePalette } from "./palette.js";
3+
import { BasePalette } from "./palette-base.js";
44
import { Swatch } from "./swatch.js";
55
import { _black, _white } from "./utilities/color-constants.js";
66

packages/adaptive-ui/src/color/palette-rgb.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { clampRgb, type Hsl, interpolate, modeHsl, modeLab, modeRgb, type Rgb, useMode } from "culori/fn";
2-
import { BasePalette } from "./palette.js";
2+
import { BasePalette } from "./palette-base.js";
33
import { Swatch } from "./swatch.js";
44
import { contrast } from "./utilities/relative-luminance.js";
55
import { _black, _white } from "./utilities/color-constants.js";
Lines changed: 1 addition & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
import { Color } from "./color.js";
22
import { Swatch } from "./swatch.js";
3-
import { binarySearch } from "./utilities/binary-search.js";
4-
import { directionByIsDark } from "./utilities/direction-by-is-dark.js";
5-
import { contrast, RelativeLuminance } from "./utilities/relative-luminance.js";
3+
import { RelativeLuminance } from "./utilities/relative-luminance.js";
64

75
/**
86
* Directional values for navigating {@link Swatch}es in {@link Palette}.
@@ -111,137 +109,3 @@ export interface Palette<T extends Swatch = Swatch> {
111109
*/
112110
get(index: number): T;
113111
}
114-
115-
/**
116-
* A base {@link Palette} with a common implementation of the interface. Use PaletteRGB for an implementation
117-
* of a palette generation algorithm that is ready to be used directly, or extend this class to generate custom Swatches.
118-
*
119-
* @public
120-
*/
121-
export class BasePalette<T extends Swatch> implements Palette<T> {
122-
/**
123-
* {@inheritdoc Palette.source}
124-
*/
125-
readonly source: Color;
126-
127-
/**
128-
* {@inheritdoc Palette.swatches}
129-
*/
130-
readonly swatches: ReadonlyArray<T>;
131-
132-
/**
133-
* An index pointer to the end of the palette.
134-
*/
135-
readonly lastIndex: number;
136-
137-
/**
138-
* A copy of the `Swatch`es in reverse order, used for optimized searching.
139-
*/
140-
readonly reversedSwatches: ReadonlyArray<T>;
141-
142-
/**
143-
* Cache from `relativeLuminance` to `Swatch` index in the `Palette`.
144-
*/
145-
readonly closestIndexCache = new Map<number, number>();
146-
147-
/**
148-
* Creates a new Palette.
149-
*
150-
* @param source - The source color for the Palette
151-
* @param swatches - All Swatches in the Palette
152-
*/
153-
constructor(source: Color, swatches: ReadonlyArray<T>) {
154-
this.source = source;
155-
this.swatches = swatches;
156-
157-
this.reversedSwatches = Object.freeze([...this.swatches].reverse());
158-
this.lastIndex = this.swatches.length - 1;
159-
}
160-
161-
/**
162-
* {@inheritdoc Palette.colorContrast}
163-
*/
164-
colorContrast(
165-
reference: RelativeLuminance,
166-
contrastTarget: number,
167-
initialSearchIndex?: number,
168-
direction: PaletteDirection = directionByIsDark(reference)
169-
): T {
170-
if (initialSearchIndex === undefined) {
171-
initialSearchIndex = this.closestIndexOf(reference);
172-
}
173-
174-
let source: ReadonlyArray<T> = this.swatches;
175-
const endSearchIndex = this.lastIndex;
176-
let startSearchIndex = initialSearchIndex;
177-
178-
const condition = (value: T) => contrast(reference, value) >= contrastTarget;
179-
180-
if (direction === PaletteDirectionValue.lighter) {
181-
source = this.reversedSwatches;
182-
startSearchIndex = endSearchIndex - startSearchIndex;
183-
}
184-
185-
return binarySearch(source, condition, startSearchIndex, endSearchIndex);
186-
}
187-
188-
/**
189-
* {@inheritdoc Palette.delta}
190-
*/
191-
delta(reference: RelativeLuminance, delta: number, direction: PaletteDirection): T {
192-
const dir = resolvePaletteDirection(direction);
193-
return this.get(this.closestIndexOf(reference) + dir * delta);
194-
}
195-
196-
/**
197-
* {@inheritdoc Palette.closestIndexOf}
198-
*/
199-
closestIndexOf(reference: RelativeLuminance): number {
200-
if (this.closestIndexCache.has(reference.relativeLuminance)) {
201-
/* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
202-
return this.closestIndexCache.get(reference.relativeLuminance)!;
203-
}
204-
205-
let index = this.swatches.indexOf(reference as T);
206-
207-
if (index !== -1) {
208-
this.closestIndexCache.set(reference.relativeLuminance, index);
209-
return index;
210-
}
211-
212-
const closest = this.swatches.reduce((previous, next) =>
213-
Math.abs(next.relativeLuminance - reference.relativeLuminance) <
214-
Math.abs(previous.relativeLuminance - reference.relativeLuminance)
215-
? next
216-
: previous
217-
);
218-
219-
index = this.swatches.indexOf(closest);
220-
this.closestIndexCache.set(reference.relativeLuminance, index);
221-
222-
return index;
223-
}
224-
225-
/**
226-
* Ensures that an input number does not exceed a max value and is not less than a min value.
227-
*
228-
* @param i - the number to clamp
229-
* @param min - the maximum (inclusive) value
230-
* @param max - the minimum (inclusive) value
231-
*/
232-
private clamp(i: number, min: number, max: number): number {
233-
if (isNaN(i) || i <= min) {
234-
return min;
235-
} else if (i >= max) {
236-
return max;
237-
}
238-
return i;
239-
}
240-
241-
/**
242-
* {@inheritdoc Palette.get}
243-
*/
244-
get(index: number): T {
245-
return this.swatches[index] || this.swatches[this.clamp(index, 0, this.lastIndex)];
246-
}
247-
}

0 commit comments

Comments
 (0)