-
Notifications
You must be signed in to change notification settings - Fork 601
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: refactor color recipes (#4623)
* refactor color recipes away from DesignSystem data structure * rename dir * cleanup * factor binary-search out to it's own file * updating code docs * Change files * fixing binary-search * Update packages/web-components/fast-components/src/color-vNext/palette.ts Co-authored-by: Brian Heston <[email protected]> * addressing feedback * adding readme * pretty pretty closes #3833 Co-authored-by: nicholasrice <[email protected]> Co-authored-by: Brian Heston <[email protected]>
- Loading branch information
1 parent
dbc4e48
commit d6d4bbc
Showing
40 changed files
with
1,060 additions
and
7 deletions.
There are no files selected for viewing
6 changes: 6 additions & 0 deletions
6
change/@microsoft-fast-components-11d3d100-a9cd-47cf-adda-e2b76137e8fa.json
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
{ | ||
"type": "minor", | ||
"packageName": "@microsoft/fast-components", | ||
"email": "[email protected]", | ||
"dependentChangeType": "patch" | ||
} |
28 changes: 28 additions & 0 deletions
28
packages/web-components/fast-components/src/color-vNext/README.md
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
# FAST Color Recipes | ||
|
||
Color recipes are named colors who's value is algorithmically defined from a variety of inputs. `@microsoft/fast-components` relies on these recipes heavily to achieve expressive theming options while maintaining color accessability targets. | ||
|
||
|
||
## Swatch | ||
A Swatch is a representation of a color that has a `relativeLuminance` value and a method to convert the swatch to a color string. It is used by recipes to determine which colors to use for UI. | ||
|
||
### SwatchRGB | ||
A concrete implementation of `Swatch`, it is a swatch with red, green, and blue 64bit color channels . | ||
|
||
**Example: Creating a SwatchRGB** | ||
```ts | ||
import { SwatchRGB } from "@microsoft/fast-components"; | ||
|
||
const red = new SwatchRGB(1, 0, 0); | ||
``` | ||
|
||
## Palette | ||
A palette is a collection `Swatch` instances, ordered by relative luminance, and provides mechanisms to safely retrieve swatches by index and by target contrast ratios. It also contains a `source` color, which is the color from which the palette is | ||
|
||
### PaletteRGB | ||
An implementation of `Palette` of `SwatchRGB` instances. | ||
|
||
```ts | ||
// Create a palette from the red swatch | ||
const palette = PaletteRGB.from(red): | ||
``` |
145 changes: 145 additions & 0 deletions
145
packages/web-components/fast-components/src/color-vNext/palette.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
import { | ||
clamp, | ||
ComponentStateColorPalette, | ||
parseColorHexRGB, | ||
} from "@microsoft/fast-colors"; | ||
import { Swatch, SwatchRGB } from "./swatch"; | ||
import { binarySearch } from "./utilities/binary-search"; | ||
import { directionByIsDark } from "./utilities/direction-by-is-dark"; | ||
import { contrast, RelativeLuminance } from "./utilities/relative-luminance"; | ||
|
||
/** | ||
* A collection of {@link Swatch} instances | ||
* @public | ||
*/ | ||
export interface Palette<T extends Swatch = Swatch> { | ||
readonly source: T; | ||
readonly swatches: ReadonlyArray<T>; | ||
|
||
/** | ||
* Returns a swatch from the palette that most closely matches | ||
* the contrast ratio provided to a provided reference. | ||
*/ | ||
colorContrast( | ||
reference: Swatch, | ||
contrast: number, | ||
initialIndex?: number, | ||
direction?: 1 | -1 | ||
): Swatch; | ||
|
||
/** | ||
* Returns the index of the palette that most closely matches | ||
* the relativeLuminance of the provided swatch | ||
*/ | ||
closestIndexOf(reference: RelativeLuminance): number; | ||
|
||
/** | ||
* Gets a swatch by index. Index is clamped to the limits | ||
* of the palette so a Swatch will always be returned. | ||
*/ | ||
get(index: number): T; | ||
} | ||
|
||
/** | ||
* A {@link Palette} representing RGB swatch values. | ||
* @public | ||
*/ | ||
export class PaletteRGB implements Palette<SwatchRGB> { | ||
/** | ||
* {@inheritdoc Palette.source} | ||
*/ | ||
public readonly source: SwatchRGB; | ||
public readonly swatches: ReadonlyArray<SwatchRGB>; | ||
private lastIndex: number; | ||
private reversedSwatches: ReadonlyArray<SwatchRGB>; | ||
/** | ||
* | ||
* @param source - The source color for the palette | ||
* @param swatches - All swatches in the palette | ||
*/ | ||
constructor(source: SwatchRGB, swatches: ReadonlyArray<SwatchRGB>) { | ||
this.source = source; | ||
this.swatches = swatches; | ||
|
||
this.reversedSwatches = Object.freeze([...this.swatches].reverse()); | ||
this.lastIndex = this.swatches.length - 1; | ||
} | ||
|
||
/** | ||
* {@inheritdoc Palette.colorContrast} | ||
*/ | ||
public colorContrast( | ||
reference: Swatch, | ||
contrastTarget: number, | ||
initialSearchIndex?: number, | ||
direction?: 1 | -1 | ||
): SwatchRGB { | ||
if (initialSearchIndex === undefined) { | ||
initialSearchIndex = this.closestIndexOf(reference); | ||
} | ||
|
||
let source: ReadonlyArray<SwatchRGB> = this.swatches; | ||
const endSearchIndex = this.lastIndex; | ||
let startSearchIndex = initialSearchIndex; | ||
|
||
if (direction === undefined) { | ||
direction = directionByIsDark(reference); | ||
} | ||
|
||
const condition = (value: SwatchRGB) => | ||
contrast(reference, value) >= contrastTarget; | ||
|
||
if (direction === -1) { | ||
source = this.reversedSwatches; | ||
startSearchIndex = endSearchIndex - startSearchIndex; | ||
} | ||
|
||
return binarySearch(source, condition, startSearchIndex, endSearchIndex); | ||
} | ||
|
||
/** | ||
* {@inheritdoc Palette.get} | ||
*/ | ||
public get(index: number): SwatchRGB { | ||
return this.swatches[index] || this.swatches[clamp(index, 0, this.lastIndex)]; | ||
} | ||
|
||
/** | ||
* {@inheritdoc Palette.closestIndexOf} | ||
*/ | ||
public closestIndexOf(reference: Swatch): number { | ||
const index = this.swatches.indexOf(reference as SwatchRGB); | ||
|
||
if (index !== -1) { | ||
return index; | ||
} | ||
|
||
const closest = this.swatches.reduce((previous, next) => | ||
Math.abs(next.relativeLuminance - reference.relativeLuminance) < | ||
Math.abs(previous.relativeLuminance - reference.relativeLuminance) | ||
? next | ||
: previous | ||
); | ||
|
||
return this.swatches.indexOf(closest); | ||
} | ||
|
||
/** | ||
* Create a color palette from a provided swatch | ||
* @param source - The source swatch to create a palette from | ||
* @returns | ||
*/ | ||
static from(source: SwatchRGB) { | ||
return new PaletteRGB( | ||
source, | ||
Object.freeze( | ||
new ComponentStateColorPalette({ | ||
baseColor: source, | ||
}).palette.map(x => { | ||
const _x = parseColorHexRGB(x.toStringHexRGB())!; | ||
return new SwatchRGB(_x.r, _x.g, _x.b); | ||
}) | ||
) | ||
); | ||
} | ||
} |
60 changes: 60 additions & 0 deletions
60
packages/web-components/fast-components/src/color-vNext/recipes/accent-fill.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
import { inRange } from "lodash"; | ||
import { Palette } from "../palette"; | ||
import { Swatch } from "../swatch"; | ||
import { isDark } from "../utilities/is-dark"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function accentFill( | ||
palette: Palette, | ||
neutralPalette: Palette, | ||
reference: Swatch, | ||
textColor: Swatch, | ||
contrastTarget: number, | ||
hoverDelta: number, | ||
activeDelta: number, | ||
focusDelta: number, | ||
selectedDelta: number, | ||
neutralFillRestDelta: number, | ||
neutralFillHoverDelta: number, | ||
neutralFillActiveDelta: number | ||
) { | ||
const accent = palette.source; | ||
const referenceIndex = neutralPalette.closestIndexOf(reference); | ||
const swapThreshold = Math.max( | ||
neutralFillRestDelta, | ||
neutralFillHoverDelta, | ||
neutralFillActiveDelta | ||
); | ||
const direction = referenceIndex >= swapThreshold ? -1 : 1; | ||
const paletteLength = palette.swatches.length; | ||
const maxIndex = paletteLength - 1; | ||
const accentIndex = palette.closestIndexOf(accent); | ||
let accessibleOffset = 0; | ||
|
||
while ( | ||
accessibleOffset < direction * hoverDelta && | ||
inRange(accentIndex + accessibleOffset + direction, 0, paletteLength) && | ||
textColor.contrast(palette.get(accentIndex + accessibleOffset + direction)) >= | ||
contrastTarget && | ||
inRange(accentIndex + accessibleOffset + direction + direction, 0, maxIndex) | ||
) { | ||
accessibleOffset += direction; | ||
} | ||
|
||
const hoverIndex = accentIndex + accessibleOffset; | ||
const restIndex = hoverIndex + direction * -1 * hoverDelta; | ||
const activeIndex = restIndex + direction * activeDelta; | ||
const focusIndex = restIndex + direction * focusDelta; | ||
const selectedIndex = | ||
restIndex + (isDark(reference) ? selectedDelta * -1 : selectedDelta); | ||
|
||
return { | ||
rest: palette.get(restIndex), | ||
hover: palette.get(hoverIndex), | ||
active: palette.get(activeIndex), | ||
focus: palette.get(focusIndex), | ||
selected: palette.get(selectedIndex), | ||
}; | ||
} |
9 changes: 9 additions & 0 deletions
9
packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground-cut.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
import { Swatch } from "../swatch"; | ||
import { black, white } from "../utilities/color-constants"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function accentForegroundCut(reference: Swatch, contrastTarget: number) { | ||
return reference.contrast(white) >= contrastTarget ? white : black; | ||
} |
56 changes: 56 additions & 0 deletions
56
packages/web-components/fast-components/src/color-vNext/recipes/accent-foreground.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import { Palette } from "../palette"; | ||
import { Swatch } from "../swatch"; | ||
import { directionByIsDark } from "../utilities/direction-by-is-dark"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function accentForeground( | ||
palette: Palette, | ||
reference: Swatch, | ||
contrastTarget: number, | ||
restDelta: number, | ||
hoverDelta: number, | ||
activeDelta: number, | ||
focusDelta: number | ||
) { | ||
const accent = palette.source; | ||
const accentIndex = palette.closestIndexOf(accent); | ||
const direction = directionByIsDark(reference); | ||
const startIndex = | ||
accentIndex + | ||
(direction === 1 | ||
? Math.min(restDelta, hoverDelta) | ||
: Math.max(direction * restDelta, direction * hoverDelta)); | ||
const accessibleSwatch = palette.colorContrast( | ||
reference, | ||
contrastTarget, | ||
startIndex, | ||
direction | ||
); | ||
const accessibleIndex1 = palette.closestIndexOf(accessibleSwatch); | ||
const accessibleIndex2 = | ||
accessibleIndex1 + direction * Math.abs(restDelta - hoverDelta); | ||
const indexOneIsRestState = | ||
direction === 1 | ||
? restDelta < hoverDelta | ||
: direction * restDelta > direction * hoverDelta; | ||
|
||
let restIndex: number; | ||
let hoverIndex: number; | ||
|
||
if (indexOneIsRestState) { | ||
restIndex = accessibleIndex1; | ||
hoverIndex = accessibleIndex2; | ||
} else { | ||
restIndex = accessibleIndex2; | ||
hoverIndex = accessibleIndex1; | ||
} | ||
|
||
return { | ||
rest: palette.get(restIndex), | ||
hover: palette.get(hoverIndex), | ||
active: palette.get(restIndex + direction * activeDelta), | ||
focus: palette.get(restIndex + direction * focusDelta), | ||
}; | ||
} |
17 changes: 17 additions & 0 deletions
17
packages/web-components/fast-components/src/color-vNext/recipes/neutral-divider.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
import { Swatch } from "../swatch"; | ||
import { Palette } from "../palette"; | ||
import { directionByIsDark } from "../utilities/direction-by-is-dark"; | ||
|
||
/** | ||
* The neutralDivider color recipe | ||
* @param palette - The palette to operate on | ||
* @param reference - The reference color | ||
* @param delta - The offset from the reference | ||
* | ||
* @internal | ||
*/ | ||
export function neutralDivider(palette: Palette, reference: Swatch, delta: number) { | ||
return palette.get( | ||
palette.closestIndexOf(reference) + directionByIsDark(reference) * delta | ||
); | ||
} |
11 changes: 11 additions & 0 deletions
11
packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-card.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { Palette } from "../palette"; | ||
import { Swatch } from "../swatch"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function neutralFillCard(palette: Palette, reference: Swatch, delta: number) { | ||
const referenceIndex = palette.closestIndexOf(reference); | ||
|
||
return palette.get(referenceIndex - (referenceIndex < delta ? delta * -1 : delta)); | ||
} |
27 changes: 27 additions & 0 deletions
27
packages/web-components/fast-components/src/color-vNext/recipes/neutral-fill-input.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
import { Palette } from "../palette"; | ||
import { Swatch } from "../swatch"; | ||
import { directionByIsDark } from "../utilities/direction-by-is-dark"; | ||
|
||
/** | ||
* @internal | ||
*/ | ||
export function neutralFillInput( | ||
palette: Palette, | ||
reference: Swatch, | ||
restDelta: number, | ||
hoverDelta: number, | ||
activeDelta: number, | ||
focusDelta: number, | ||
selectedDelta: number | ||
) { | ||
const direction = directionByIsDark(reference); | ||
const referenceIndex = palette.closestIndexOf(reference); | ||
|
||
return { | ||
rest: palette.get(referenceIndex - direction * restDelta), | ||
hover: palette.get(referenceIndex - direction * hoverDelta), | ||
active: palette.get(referenceIndex - direction * activeDelta), | ||
focus: palette.get(referenceIndex - direction * focusDelta), | ||
selected: palette.get(referenceIndex - direction * selectedDelta), | ||
}; | ||
} |
Oops, something went wrong.