@promptctl/rich-js 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/color.d.ts +41 -1
- package/dist/core/color.js +107 -5
- package/dist/core/oklch.d.ts +34 -0
- package/dist/core/oklch.js +52 -4
- package/dist/core/strip.d.ts +19 -2
- package/dist/core/strip.js +46 -10
- package/dist/core/style.d.ts +2 -1
- package/dist/core/style.js +1 -1
- package/dist/core/text.js +3 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/template-bindings/color-funcs.d.ts +12 -2
- package/dist/template-bindings/color-funcs.js +12 -4
- package/dist/template-bindings/index.d.ts +7 -5
- package/dist/template-bindings/index.js +9 -6
- package/dist/themes/colorMath.d.ts +11 -19
- package/dist/themes/colorMath.js +38 -31
- package/package.json +1 -1
package/dist/core/color.d.ts
CHANGED
|
@@ -31,11 +31,34 @@ export declare class ColorRgba {
|
|
|
31
31
|
*/
|
|
32
32
|
compositeOver(bg: ColorRgba): ColorRgba;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* WCAG 2.x relative luminance (0..1) of an opaque color. The single
|
|
36
|
+
* luminance function in the codebase — `contrastFor`, `contrastRatio`, and
|
|
37
|
+
* any caller that needs to reason about readability all funnel through it.
|
|
38
|
+
* [LAW:one-source-of-truth]
|
|
39
|
+
*/
|
|
40
|
+
export declare function relativeLuminance(c: ColorRgba): number;
|
|
41
|
+
/**
|
|
42
|
+
* WCAG 2.x contrast ratio between two colors, in [1, 21]. Symmetric — the
|
|
43
|
+
* order of arguments does not matter. 4.5 is the AA threshold for normal
|
|
44
|
+
* text, 3.0 for large text.
|
|
45
|
+
*
|
|
46
|
+
* Assumes opaque inputs: alpha is ignored, since the displayed contrast of a
|
|
47
|
+
* translucent color depends on what it composites over. For a translucent
|
|
48
|
+
* foreground, flatten it first (or use `ensureContrast`, which does).
|
|
49
|
+
*/
|
|
50
|
+
export declare function contrastRatio(a: ColorRgba, b: ColorRgba): number;
|
|
34
51
|
export declare class ColorTable {
|
|
35
52
|
private readonly colors;
|
|
53
|
+
private readonly firstIndex;
|
|
36
54
|
private readonly matchCache;
|
|
37
|
-
|
|
55
|
+
private readonly readableCache;
|
|
56
|
+
constructor(colors: ColorRgba[], firstIndex?: number);
|
|
38
57
|
get(index: number): ColorRgba;
|
|
58
|
+
private luminanceCache;
|
|
59
|
+
/** Each entry's relative luminance, computed once per table. */
|
|
60
|
+
private luminances;
|
|
61
|
+
/** How many entries the table holds (terminal indices `firstIndex`…). */
|
|
39
62
|
get size(): number;
|
|
40
63
|
/**
|
|
41
64
|
* Finds the nearest table index to the given color (Euclidean RGB distance, cached).
|
|
@@ -43,6 +66,16 @@ export declare class ColorTable {
|
|
|
43
66
|
* don't collide, but is not used in the distance metric.
|
|
44
67
|
*/
|
|
45
68
|
match(value: ColorRgba): number;
|
|
69
|
+
/**
|
|
70
|
+
* The nearest entry to `value` (the distance `match` uses) among those that
|
|
71
|
+
* clear `minRatio` against `on`; when none does, the entry with the most
|
|
72
|
+
* contrast against `on`. Text downgraded on its own background: `match`
|
|
73
|
+
* moves text and background independently, and two independent roundings
|
|
74
|
+
* can meet in the middle, so a pair that read at 4.5:1 in truecolor can
|
|
75
|
+
* draw at 2:1. [LAW:dataflow-not-control-flow] One scan scores every entry;
|
|
76
|
+
* the ratio decides which one wins.
|
|
77
|
+
*/
|
|
78
|
+
matchReadable(value: ColorRgba, on: ColorRgba, minRatio: number): number;
|
|
46
79
|
}
|
|
47
80
|
export declare enum ColorDepth {
|
|
48
81
|
DEFAULT = 0,
|
|
@@ -153,6 +186,13 @@ export declare class TerminalTheme {
|
|
|
153
186
|
}
|
|
154
187
|
export declare const STANDARD_TABLE: ColorTable;
|
|
155
188
|
export declare const EIGHT_BIT_TABLE: ColorTable;
|
|
189
|
+
/**
|
|
190
|
+
* What a downgrade to 256 colours may choose: the cube and the grey ramp,
|
|
191
|
+
* indices 16–255. Indices 0–15 are the terminal's own ANSI colours, which
|
|
192
|
+
* every theme redefines, so their RGB is unknown and a match against them is
|
|
193
|
+
* a guess; Python Rich never picks them either.
|
|
194
|
+
*/
|
|
195
|
+
export declare const EIGHT_BIT_DOWNGRADE_TABLE: ColorTable;
|
|
156
196
|
export declare const WINDOWS_TABLE: ColorTable;
|
|
157
197
|
export declare const ANSI_COLOR_NAMES: Record<string, number>;
|
|
158
198
|
/**
|
package/dist/core/color.js
CHANGED
|
@@ -65,16 +65,74 @@ export class ColorRgba {
|
|
|
65
65
|
return new ColorRgba(Math.round(bg.red + (this.red - bg.red) * t), Math.round(bg.green + (this.green - bg.green) * t), Math.round(bg.blue + (this.blue - bg.blue) * t), 1);
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
|
+
/**
|
|
69
|
+
* WCAG 2.x relative luminance (0..1) of an opaque color. The single
|
|
70
|
+
* luminance function in the codebase — `contrastFor`, `contrastRatio`, and
|
|
71
|
+
* any caller that needs to reason about readability all funnel through it.
|
|
72
|
+
* [LAW:one-source-of-truth]
|
|
73
|
+
*/
|
|
74
|
+
export function relativeLuminance(c) {
|
|
75
|
+
const ch = (v) => {
|
|
76
|
+
const x = v / 255;
|
|
77
|
+
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
|
|
78
|
+
};
|
|
79
|
+
return 0.2126 * ch(c.red) + 0.7152 * ch(c.green) + 0.0722 * ch(c.blue);
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* WCAG 2.x contrast ratio between two colors, in [1, 21]. Symmetric — the
|
|
83
|
+
* order of arguments does not matter. 4.5 is the AA threshold for normal
|
|
84
|
+
* text, 3.0 for large text.
|
|
85
|
+
*
|
|
86
|
+
* Assumes opaque inputs: alpha is ignored, since the displayed contrast of a
|
|
87
|
+
* translucent color depends on what it composites over. For a translucent
|
|
88
|
+
* foreground, flatten it first (or use `ensureContrast`, which does).
|
|
89
|
+
*/
|
|
90
|
+
export function contrastRatio(a, b) {
|
|
91
|
+
return luminanceRatio(relativeLuminance(a), relativeLuminance(b));
|
|
92
|
+
}
|
|
93
|
+
// [LAW:single-enforcer] The WCAG ratio over two relative luminances — the one
|
|
94
|
+
// formula `contrastRatio` and `ColorTable.matchReadable` (which caches its
|
|
95
|
+
// entries' luminances) both measure with.
|
|
96
|
+
function luminanceRatio(la, lb) {
|
|
97
|
+
return (Math.max(la, lb) + 0.05) / (Math.min(la, lb) + 0.05);
|
|
98
|
+
}
|
|
68
99
|
// --- ColorTable ---
|
|
100
|
+
/**
|
|
101
|
+
* An indexed palette: entry `i` of `colors` is terminal index `firstIndex + i`.
|
|
102
|
+
* `firstIndex` lets a table hold only the part of a palette a downgrade may
|
|
103
|
+
* choose (the 256-colour cube and grey ramp start at 16) while every index it
|
|
104
|
+
* reports is the terminal's own.
|
|
105
|
+
*/
|
|
106
|
+
// [LAW:single-enforcer] The one size policy for ColorTable's memos: a key is
|
|
107
|
+
// derived from colours a long-running host computes without end (ramp stops,
|
|
108
|
+
// mixes), so an unbounded map grows with every render. Clearing at the cap is
|
|
109
|
+
// the policy `cellLen` already uses; a refill costs one table scan per key.
|
|
110
|
+
const TABLE_CACHE_MAX = 4096;
|
|
111
|
+
function remember(cache, key, index) {
|
|
112
|
+
if (cache.size >= TABLE_CACHE_MAX)
|
|
113
|
+
cache.clear();
|
|
114
|
+
cache.set(key, index);
|
|
115
|
+
return index;
|
|
116
|
+
}
|
|
69
117
|
export class ColorTable {
|
|
70
118
|
colors;
|
|
119
|
+
firstIndex;
|
|
71
120
|
matchCache = new Map();
|
|
72
|
-
|
|
121
|
+
readableCache = new Map();
|
|
122
|
+
constructor(colors, firstIndex = 0) {
|
|
73
123
|
this.colors = colors;
|
|
124
|
+
this.firstIndex = firstIndex;
|
|
74
125
|
}
|
|
75
126
|
get(index) {
|
|
76
|
-
return this.colors[index];
|
|
127
|
+
return this.colors[index - this.firstIndex];
|
|
128
|
+
}
|
|
129
|
+
luminanceCache;
|
|
130
|
+
/** Each entry's relative luminance, computed once per table. */
|
|
131
|
+
luminances() {
|
|
132
|
+
this.luminanceCache ??= this.colors.map(relativeLuminance);
|
|
133
|
+
return this.luminanceCache;
|
|
77
134
|
}
|
|
135
|
+
/** How many entries the table holds (terminal indices `firstIndex`…). */
|
|
78
136
|
get size() {
|
|
79
137
|
return this.colors.length;
|
|
80
138
|
}
|
|
@@ -101,8 +159,45 @@ export class ColorTable {
|
|
|
101
159
|
bestIndex = i;
|
|
102
160
|
}
|
|
103
161
|
}
|
|
104
|
-
this.matchCache
|
|
105
|
-
|
|
162
|
+
return remember(this.matchCache, key, this.firstIndex + bestIndex);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The nearest entry to `value` (the distance `match` uses) among those that
|
|
166
|
+
* clear `minRatio` against `on`; when none does, the entry with the most
|
|
167
|
+
* contrast against `on`. Text downgraded on its own background: `match`
|
|
168
|
+
* moves text and background independently, and two independent roundings
|
|
169
|
+
* can meet in the middle, so a pair that read at 4.5:1 in truecolor can
|
|
170
|
+
* draw at 2:1. [LAW:dataflow-not-control-flow] One scan scores every entry;
|
|
171
|
+
* the ratio decides which one wins.
|
|
172
|
+
*/
|
|
173
|
+
matchReadable(value, on, minRatio) {
|
|
174
|
+
const key = `${value.red},${value.green},${value.blue}|${on.red},${on.green},${on.blue}|${minRatio}`;
|
|
175
|
+
const cached = this.readableCache.get(key);
|
|
176
|
+
if (cached !== undefined)
|
|
177
|
+
return cached;
|
|
178
|
+
const lOn = relativeLuminance(on);
|
|
179
|
+
let best = 0;
|
|
180
|
+
let bestPasses = false;
|
|
181
|
+
let bestScore = -Infinity;
|
|
182
|
+
for (let i = 0; i < this.colors.length; i++) {
|
|
183
|
+
const c = this.colors[i];
|
|
184
|
+
const lc = this.luminances()[i];
|
|
185
|
+
const ratio = luminanceRatio(lc, lOn);
|
|
186
|
+
const passes = ratio >= minRatio;
|
|
187
|
+
const dr = c.red - value.red;
|
|
188
|
+
const dg = c.green - value.green;
|
|
189
|
+
const db = c.blue - value.blue;
|
|
190
|
+
// A passing entry scores by closeness; a failing one only by contrast,
|
|
191
|
+
// and loses to every passing one.
|
|
192
|
+
const score = passes ? -(dr * dr + dg * dg + db * db) : ratio;
|
|
193
|
+
if ((passes && !bestPasses) ||
|
|
194
|
+
(passes === bestPasses && score > bestScore)) {
|
|
195
|
+
best = i;
|
|
196
|
+
bestPasses = passes;
|
|
197
|
+
bestScore = score;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return remember(this.readableCache, key, this.firstIndex + best);
|
|
106
201
|
}
|
|
107
202
|
}
|
|
108
203
|
// --- Enums ---
|
|
@@ -370,7 +465,7 @@ export class ColorSpec {
|
|
|
370
465
|
const triplet = this.getTruecolor();
|
|
371
466
|
switch (targetSystem) {
|
|
372
467
|
case ColorDepth.EIGHT_BIT: {
|
|
373
|
-
const index =
|
|
468
|
+
const index = EIGHT_BIT_DOWNGRADE_TABLE.match(triplet);
|
|
374
469
|
return ColorSpec.fromAnsi(index);
|
|
375
470
|
}
|
|
376
471
|
case ColorDepth.STANDARD: {
|
|
@@ -537,6 +632,13 @@ function buildWindowsTable() {
|
|
|
537
632
|
}
|
|
538
633
|
export const STANDARD_TABLE = new ColorTable(buildStandard16());
|
|
539
634
|
export const EIGHT_BIT_TABLE = new ColorTable(build256Table());
|
|
635
|
+
/**
|
|
636
|
+
* What a downgrade to 256 colours may choose: the cube and the grey ramp,
|
|
637
|
+
* indices 16–255. Indices 0–15 are the terminal's own ANSI colours, which
|
|
638
|
+
* every theme redefines, so their RGB is unknown and a match against them is
|
|
639
|
+
* a guess; Python Rich never picks them either.
|
|
640
|
+
*/
|
|
641
|
+
export const EIGHT_BIT_DOWNGRADE_TABLE = new ColorTable(build256Table().slice(16), 16);
|
|
540
642
|
export const WINDOWS_TABLE = new ColorTable(buildWindowsTable());
|
|
541
643
|
// --- Internal fallback theme ---
|
|
542
644
|
//
|
package/dist/core/oklch.d.ts
CHANGED
|
@@ -41,6 +41,13 @@ export interface ThemeKey {
|
|
|
41
41
|
/** Additive on lightness; applied *after* the scale. */
|
|
42
42
|
readonly lightnessShift: number;
|
|
43
43
|
}
|
|
44
|
+
/** How far `Oklch.mixAxes` moves each axis toward its target, each in [0, 1]. */
|
|
45
|
+
export interface OklchWeights {
|
|
46
|
+
readonly l: number;
|
|
47
|
+
readonly c: number;
|
|
48
|
+
readonly h: number;
|
|
49
|
+
readonly alpha: number;
|
|
50
|
+
}
|
|
44
51
|
export declare const IDENTITY: ThemeKey;
|
|
45
52
|
/**
|
|
46
53
|
* Flip lightness around the midpoint (`L' = 1 - L`) with hue and chroma
|
|
@@ -70,6 +77,7 @@ export declare function isIdentityKey(k: ThemeKey): boolean;
|
|
|
70
77
|
* sRGB-quantization step normalizes them.
|
|
71
78
|
*/
|
|
72
79
|
export declare class Oklch {
|
|
80
|
+
#private;
|
|
73
81
|
readonly l: number;
|
|
74
82
|
readonly c: number;
|
|
75
83
|
readonly h: number;
|
|
@@ -115,6 +123,32 @@ export declare class Oklch {
|
|
|
115
123
|
* there too.
|
|
116
124
|
*/
|
|
117
125
|
mix(toward: Oklch, t: number): Oklch;
|
|
126
|
+
/**
|
|
127
|
+
* `mix` with each axis moved its own share of the way: `weights.l` of the
|
|
128
|
+
* lightness gap, `weights.c` of the chroma gap, and so on, each in [0, 1].
|
|
129
|
+
* `mix(toward, t)` is `mixAxes` with every weight `t` — one interpolation,
|
|
130
|
+
* so the shorter-arc hue and the powerless-endpoint rule are the same
|
|
131
|
+
* whichever is called.
|
|
132
|
+
*
|
|
133
|
+
* It exists because perceptual axes are independent: a tint can take most of
|
|
134
|
+
* a hue's colourfulness while keeping close to the lightness it started at,
|
|
135
|
+
* which a single `t` cannot say — raising `t` for chroma drags lightness with
|
|
136
|
+
* it. Every weight is required: an axis left out would need a default, and
|
|
137
|
+
* "unchanged" (0) and "same as the others" are both plausible readings.
|
|
138
|
+
*
|
|
139
|
+
* A weak `h` beside a strong `c` shows the starting colour's hue at high
|
|
140
|
+
* chroma. The powerless rule covers only a truly achromatic start, so a
|
|
141
|
+
* near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
|
|
142
|
+
* noise in proportion. To land on the target's hue, pass `h: 1`.
|
|
143
|
+
*/
|
|
144
|
+
mixAxes(toward: Oklch, weights: OklchWeights): Oklch;
|
|
145
|
+
/**
|
|
146
|
+
* ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
|
|
147
|
+
* (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
|
|
148
|
+
* resolves. Alpha is not a coordinate of the space and does not count.
|
|
149
|
+
* Symmetric and pure.
|
|
150
|
+
*/
|
|
151
|
+
deltaE(other: Oklch): number;
|
|
118
152
|
/** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
|
|
119
153
|
* already-normalized values so this never sees out-of-range inputs. */
|
|
120
154
|
private toLinearRgb;
|
package/dist/core/oklch.js
CHANGED
|
@@ -20,7 +20,11 @@
|
|
|
20
20
|
* nothing else from inside the project. Nothing in core/ depends back on
|
|
21
21
|
* this file.
|
|
22
22
|
*/
|
|
23
|
+
var _a;
|
|
23
24
|
import { ColorRgba } from "./color.js";
|
|
25
|
+
// The axes `mixAxes` checks, named once so a weights object's own keys — a
|
|
26
|
+
// missing one, or an extra field riding along — never decide what is checked.
|
|
27
|
+
const OKLCH_AXES = ["l", "c", "h", "alpha"];
|
|
24
28
|
export const IDENTITY = Object.freeze({
|
|
25
29
|
hueShift: 0,
|
|
26
30
|
chromaScale: 1,
|
|
@@ -144,7 +148,7 @@ export class Oklch {
|
|
|
144
148
|
const bLab = 0.0259040371 * lCube + 0.7827717662 * mCube - 0.8086757660 * sCube;
|
|
145
149
|
const C = Math.sqrt(aLab * aLab + bLab * bLab);
|
|
146
150
|
const H = wrapHue(Math.atan2(bLab, aLab) * (180 / Math.PI));
|
|
147
|
-
return new
|
|
151
|
+
return new _a(L, C, hueOf(C, H), a);
|
|
148
152
|
}
|
|
149
153
|
/**
|
|
150
154
|
* Polar → OKLab → linear → sRGB (with chroma-bisection gamut clamping).
|
|
@@ -181,7 +185,7 @@ export class Oklch {
|
|
|
181
185
|
const newL = clamp01(this.l * k.lightnessScale + k.lightnessShift);
|
|
182
186
|
const newC = Math.max(0, this.c * k.chromaScale);
|
|
183
187
|
const newH = wrapHue(this.h + k.hueShift);
|
|
184
|
-
return new
|
|
188
|
+
return new _a(newL, newC, hueOf(newC, newH), this.alpha);
|
|
185
189
|
}
|
|
186
190
|
/**
|
|
187
191
|
* The color `t` of the way from this one toward `toward`, in OKLCH.
|
|
@@ -208,6 +212,38 @@ export class Oklch {
|
|
|
208
212
|
if (!(t >= 0 && t <= 1)) {
|
|
209
213
|
throw new RangeError(`Oklch.mix: t must be in [0, 1]; got ${t}`);
|
|
210
214
|
}
|
|
215
|
+
return this.#interpolate(toward, t, t, t, t);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* `mix` with each axis moved its own share of the way: `weights.l` of the
|
|
219
|
+
* lightness gap, `weights.c` of the chroma gap, and so on, each in [0, 1].
|
|
220
|
+
* `mix(toward, t)` is `mixAxes` with every weight `t` — one interpolation,
|
|
221
|
+
* so the shorter-arc hue and the powerless-endpoint rule are the same
|
|
222
|
+
* whichever is called.
|
|
223
|
+
*
|
|
224
|
+
* It exists because perceptual axes are independent: a tint can take most of
|
|
225
|
+
* a hue's colourfulness while keeping close to the lightness it started at,
|
|
226
|
+
* which a single `t` cannot say — raising `t` for chroma drags lightness with
|
|
227
|
+
* it. Every weight is required: an axis left out would need a default, and
|
|
228
|
+
* "unchanged" (0) and "same as the others" are both plausible readings.
|
|
229
|
+
*
|
|
230
|
+
* A weak `h` beside a strong `c` shows the starting colour's hue at high
|
|
231
|
+
* chroma. The powerless rule covers only a truly achromatic start, so a
|
|
232
|
+
* near-grey whose hue is noise (a theme surface at c ≈ 0.004) keeps that
|
|
233
|
+
* noise in proportion. To land on the target's hue, pass `h: 1`.
|
|
234
|
+
*/
|
|
235
|
+
mixAxes(toward, weights) {
|
|
236
|
+
for (const axis of OKLCH_AXES) {
|
|
237
|
+
const w = weights[axis];
|
|
238
|
+
if (!(w >= 0 && w <= 1)) {
|
|
239
|
+
throw new RangeError(`Oklch.mixAxes: ${axis} weight must be in [0, 1]; got ${w}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return this.#interpolate(toward, weights.l, weights.c, weights.h, weights.alpha);
|
|
243
|
+
}
|
|
244
|
+
// The one interpolation both entry points share; each has already checked
|
|
245
|
+
// its own weights, so this is the arithmetic alone.
|
|
246
|
+
#interpolate(toward, wl, wc, wh, walpha) {
|
|
211
247
|
const thisHasHue = this.c >= ACHROMATIC_EPS;
|
|
212
248
|
const towardHasHue = toward.c >= ACHROMATIC_EPS;
|
|
213
249
|
const fromH = wrapHue(thisHasHue ? this.h : towardHasHue ? toward.h : 0);
|
|
@@ -219,8 +255,19 @@ export class Oklch {
|
|
|
219
255
|
const arc = toH - fromH;
|
|
220
256
|
const fromU = arc < -180 ? fromH - 360 : fromH;
|
|
221
257
|
const toU = arc > 180 ? toH - 360 : toH;
|
|
222
|
-
const c = lerp(this.c, toward.c,
|
|
223
|
-
return new
|
|
258
|
+
const c = lerp(this.c, toward.c, wc);
|
|
259
|
+
return new _a(lerp(this.l, toward.l, wl), c, hueOf(c, wrapHue(lerp(fromU, toU, wh))), lerp(this.alpha, toward.alpha, walpha));
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* ΔE_OK — the Euclidean distance between this colour and `other` in OKLab
|
|
263
|
+
* (CSS Color 4's `deltaEOK`), where ~0.02 is the smallest difference the eye
|
|
264
|
+
* resolves. Alpha is not a coordinate of the space and does not count.
|
|
265
|
+
* Symmetric and pure.
|
|
266
|
+
*/
|
|
267
|
+
deltaE(other) {
|
|
268
|
+
const a = (this.h * Math.PI) / 180;
|
|
269
|
+
const b = (other.h * Math.PI) / 180;
|
|
270
|
+
return Math.hypot(this.l - other.l, this.c * Math.cos(a) - other.c * Math.cos(b), this.c * Math.sin(a) - other.c * Math.sin(b));
|
|
224
271
|
}
|
|
225
272
|
/** Linear-sRGB coordinates for an explicit (l, C, h). Pure; `toRgba` passes
|
|
226
273
|
* already-normalized values so this never sees out-of-range inputs. */
|
|
@@ -262,3 +309,4 @@ export class Oklch {
|
|
|
262
309
|
return lo;
|
|
263
310
|
}
|
|
264
311
|
}
|
|
312
|
+
_a = Oklch;
|
package/dist/core/strip.d.ts
CHANGED
|
@@ -58,12 +58,29 @@ export declare class Strip<T extends StyledRenderable = StyledRenderable> implem
|
|
|
58
58
|
constructor(items: readonly T[], joiner: Joiner<T>);
|
|
59
59
|
render(options: RenderOptions): Iterable<Segment>;
|
|
60
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* The least ΔE_OK two neighbouring backgrounds must differ by for the powerline
|
|
63
|
+
* arrow between them to be seen. Below it the arrow is drawn in a colour the
|
|
64
|
+
* eye cannot tell from its own background, so the joiner draws the divider
|
|
65
|
+
* instead. Twice the ~.02 threshold of a visible difference, because a seam is
|
|
66
|
+
* one cell wide.
|
|
67
|
+
*/
|
|
68
|
+
export declare const SEAM_MIN_DELTA_E = 0.04;
|
|
61
69
|
export interface PowerlineJoinerOptions {
|
|
62
|
-
/** Glyph used for every join
|
|
63
|
-
glyph
|
|
70
|
+
/** Glyph used for every join. */
|
|
71
|
+
glyph: string;
|
|
72
|
+
/**
|
|
73
|
+
* Glyph drawn between neighbours whose backgrounds the eye cannot tell apart,
|
|
74
|
+
* in the left item's text colour — the arrow itself would vanish into the
|
|
75
|
+
* shared background.
|
|
76
|
+
*/
|
|
77
|
+
divider: string;
|
|
64
78
|
}
|
|
79
|
+
/** The powerline pair: U+E0B0 (right-arrow) divided by U+E0B1 (thin right-arrow). */
|
|
80
|
+
export declare const POWERLINE_JOINER_GLYPHS: Readonly<PowerlineJoinerOptions>;
|
|
65
81
|
export declare class PowerlineJoiner<T extends StyledRenderable = StyledRenderable> implements Joiner<T> {
|
|
66
82
|
private readonly _glyph;
|
|
83
|
+
private readonly _divider;
|
|
67
84
|
constructor(options?: PowerlineJoinerOptions);
|
|
68
85
|
join(left: T | null, right: T | null): Renderable;
|
|
69
86
|
}
|
package/dist/core/strip.js
CHANGED
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
* styling, so the cell type does too.
|
|
27
27
|
*/
|
|
28
28
|
import { Segment } from "./segment.js";
|
|
29
|
-
import { Style } from "./style.js";
|
|
29
|
+
import { Style, SURFACE_BLACK } from "./style.js";
|
|
30
30
|
import { ColorSpec, blendRgb } from "./color.js";
|
|
31
|
+
import { Oklch } from "./oklch.js";
|
|
31
32
|
// --- Strip ---
|
|
32
33
|
export class Strip {
|
|
33
34
|
items;
|
|
@@ -100,10 +101,38 @@ function bgAsFg(edge) {
|
|
|
100
101
|
function paintableBg(bg) {
|
|
101
102
|
return bg !== undefined && !bg.isDefault ? bg : undefined;
|
|
102
103
|
}
|
|
104
|
+
// --- PowerlineJoiner ---
|
|
105
|
+
/**
|
|
106
|
+
* The least ΔE_OK two neighbouring backgrounds must differ by for the powerline
|
|
107
|
+
* arrow between them to be seen. Below it the arrow is drawn in a colour the
|
|
108
|
+
* eye cannot tell from its own background, so the joiner draws the divider
|
|
109
|
+
* instead. Twice the ~.02 threshold of a visible difference, because a seam is
|
|
110
|
+
* one cell wide.
|
|
111
|
+
*/
|
|
112
|
+
export const SEAM_MIN_DELTA_E = 0.04;
|
|
113
|
+
// Two backgrounds the eye cannot tell apart. What is measured is what is
|
|
114
|
+
// drawn: each colour flattened onto the substrate Style.toSgrCodes flattens it
|
|
115
|
+
// onto, so two alphas over one RGB are the two greys they render as. A palette
|
|
116
|
+
// colour has no value here, so two of them are the same only when they are the
|
|
117
|
+
// same palette slot — `type` + `number`, never the name, which spells one slot
|
|
118
|
+
// many ways ("red", "color(1)").
|
|
119
|
+
function indistinct(a, b) {
|
|
120
|
+
if (b === undefined)
|
|
121
|
+
return false;
|
|
122
|
+
const av = a.flattenAlpha(SURFACE_BLACK).value;
|
|
123
|
+
const bv = b.flattenAlpha(SURFACE_BLACK).value;
|
|
124
|
+
return av !== undefined && bv !== undefined
|
|
125
|
+
? Oklch.fromRgba(av).deltaE(Oklch.fromRgba(bv)) < SEAM_MIN_DELTA_E
|
|
126
|
+
: a.type === b.type && a.number === b.number;
|
|
127
|
+
}
|
|
128
|
+
/** The powerline pair: U+E0B0 (right-arrow) divided by U+E0B1 (thin right-arrow). */
|
|
129
|
+
export const POWERLINE_JOINER_GLYPHS = Object.freeze({ glyph: "\ue0b0", divider: "\ue0b1" });
|
|
103
130
|
export class PowerlineJoiner {
|
|
104
131
|
_glyph;
|
|
105
|
-
|
|
106
|
-
|
|
132
|
+
_divider;
|
|
133
|
+
constructor(options = POWERLINE_JOINER_GLYPHS) {
|
|
134
|
+
this._glyph = options.glyph;
|
|
135
|
+
this._divider = options.divider;
|
|
107
136
|
}
|
|
108
137
|
join(left, right) {
|
|
109
138
|
// [LAW:dataflow-not-control-flow] One expression for all three positions
|
|
@@ -117,20 +146,27 @@ export class PowerlineJoiner {
|
|
|
117
146
|
// colourless arrow is not drawn.)
|
|
118
147
|
// • no right bg — the end cap — bleeds the left colour out over the
|
|
119
148
|
// terminal background (fg = left bg, no bg).
|
|
120
|
-
// Equal REAL bgs still emit:
|
|
121
|
-
//
|
|
122
|
-
//
|
|
123
|
-
//
|
|
124
|
-
//
|
|
149
|
+
// Equal REAL bgs still emit: a same-bg seam between two distinct items is a
|
|
150
|
+
// structural boundary, never suppressed. The arrow would be drawn in its own
|
|
151
|
+
// background colour there and vanish, so the seam is the DIVIDER instead, in
|
|
152
|
+
// the left item's text colour — the vim-airline convention for neighbours
|
|
153
|
+
// that share a background. "Equal" is perceptual (SEAM_MIN_DELTA_E): an
|
|
154
|
+
// arrow a hair off its background is as invisible as one exactly on it.
|
|
155
|
+
// Background colour is paint, not structure; its ABSENCE (nothing to paint)
|
|
156
|
+
// is the only thing that elides the separator, and that is paint logic.
|
|
125
157
|
// "Absent" = no bg OR the terminal default (transparent) — paintableBg folds
|
|
126
158
|
// both to undefined so an explicit `… on default` cannot smuggle a separator.
|
|
127
159
|
const glyph = this._glyph;
|
|
160
|
+
const divider = this._divider;
|
|
128
161
|
return deferred(function* (options) {
|
|
129
|
-
const
|
|
162
|
+
const leftEdge = left?.edgeStyle("right", options);
|
|
163
|
+
const leftBg = paintableBg(leftEdge?.bgcolor);
|
|
130
164
|
if (leftBg === undefined)
|
|
131
165
|
return;
|
|
132
166
|
const rightBg = paintableBg(right?.edgeStyle("left", options).bgcolor);
|
|
133
|
-
yield
|
|
167
|
+
yield indistinct(leftBg, rightBg)
|
|
168
|
+
? new Segment(divider, new Style({ color: leftEdge?.color, bgcolor: rightBg }))
|
|
169
|
+
: new Segment(glyph, new Style({ color: leftBg, bgcolor: rightBg }));
|
|
134
170
|
});
|
|
135
171
|
}
|
|
136
172
|
}
|
package/dist/core/style.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Immutable style descriptors — colors, text attributes, links, metadata.
|
|
3
3
|
*/
|
|
4
|
-
import { ColorSpec, ColorDepth } from "./color.js";
|
|
4
|
+
import { ColorRgba, ColorSpec, ColorDepth } from "./color.js";
|
|
5
|
+
export declare const SURFACE_BLACK: ColorRgba;
|
|
5
6
|
/**
|
|
6
7
|
* Canonical text-attribute inventory. Single source of truth consumed by
|
|
7
8
|
* `Style.parse` / `Style.toString` and the template bindings — adding an
|
package/dist/core/style.js
CHANGED
|
@@ -8,7 +8,7 @@ import { OSC8_CLOSE, osc8Open } from "./osc8.js";
|
|
|
8
8
|
// canonical canvas color (black), inlined to avoid pulling in any preset
|
|
9
9
|
// theme constants. Preset themes live in `src/themes/` and depend on core,
|
|
10
10
|
// never the reverse.
|
|
11
|
-
const SURFACE_BLACK = new ColorRgba(0, 0, 0);
|
|
11
|
+
export const SURFACE_BLACK = new ColorRgba(0, 0, 0);
|
|
12
12
|
// --- Attribute definitions ---
|
|
13
13
|
/**
|
|
14
14
|
* Canonical text-attribute inventory. Single source of truth consumed by
|
package/dist/core/text.js
CHANGED
|
@@ -48,8 +48,9 @@ function hangingWhitespace(line) {
|
|
|
48
48
|
// [LAW:single-enforcer] RichText is the data-model trust boundary for link
|
|
49
49
|
// URLs: a `Style` is sanitized as it enters (`admitStyle`), and the `Style` a
|
|
50
50
|
// stored string resolves to is sanitized as it leaves for a render
|
|
51
|
-
// (`resolveStyle`). Wire-byte safety is enforced separately
|
|
52
|
-
//
|
|
51
|
+
// (`resolveStyle`). Wire-byte safety is enforced separately by `osc8Open`
|
|
52
|
+
// (core/osc8.ts), which every link producer calls and which applies the same
|
|
53
|
+
// `stripOscTerminators`.
|
|
53
54
|
function sanitizeStyleLink(style) {
|
|
54
55
|
const link = style.link;
|
|
55
56
|
if (!link)
|
package/dist/index.d.ts
CHANGED
|
@@ -3,7 +3,7 @@ export type { CellCol, CodeUnit, CodePoint } from "./core/cells.js";
|
|
|
3
3
|
export { ColorRgba, ColorTable, ColorDepth, ColorSpec, ColorParseError, TerminalTheme, parseRgbHex, parseRgbaHex, blendRgb, resolveColorSystem, detectColorSystem, STANDARD_TABLE, EIGHT_BIT_TABLE, WINDOWS_TABLE, ANSI_COLOR_NAMES, } from "./core/color.js";
|
|
4
4
|
export type { DetectColorOptions } from "./core/color.js";
|
|
5
5
|
export { Oklch, IDENTITY, INVERT_LIGHTNESS, isIdentityKey, } from "./core/oklch.js";
|
|
6
|
-
export type { ThemeKey } from "./core/oklch.js";
|
|
6
|
+
export type { ThemeKey, OklchWeights } from "./core/oklch.js";
|
|
7
7
|
export { Palette } from "./themes/palette.js";
|
|
8
8
|
export { resolveColorRef, parseHexColor, ColorRefError, HEX_COLOR_RE, } from "./themes/colorRef.js";
|
|
9
9
|
export { buildPalette } from "./themes/buildPalette.js";
|
|
@@ -28,7 +28,7 @@ export type { Unsubscribe } from "./core/subscription.js";
|
|
|
28
28
|
export { Measurement, measureRenderables } from "./core/measure.js";
|
|
29
29
|
export { Span, RichText } from "./core/text.js";
|
|
30
30
|
export type { RichTextOptions } from "./core/text.js";
|
|
31
|
-
export { Strip, PowerlineJoiner, CapsuleJoiner, PlainJoiner, GradientJoiner, } from "./core/strip.js";
|
|
31
|
+
export { Strip, PowerlineJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER_GLYPHS, CapsuleJoiner, PlainJoiner, GradientJoiner, } from "./core/strip.js";
|
|
32
32
|
export type { StyledRenderable, Joiner, PowerlineJoinerOptions, CapsuleJoinerOptions, PlainJoinerOptions, GradientJoinerOptions, } from "./core/strip.js";
|
|
33
33
|
export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
|
|
34
34
|
export type { RenderToStringOptions } from "./core/render.js";
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ export { Measurement, measureRenderables } from "./core/measure.js";
|
|
|
38
38
|
// Text
|
|
39
39
|
export { Span, RichText } from "./core/text.js";
|
|
40
40
|
// Strip + Joiner
|
|
41
|
-
export { Strip, PowerlineJoiner, CapsuleJoiner, PlainJoiner, GradientJoiner, } from "./core/strip.js";
|
|
41
|
+
export { Strip, PowerlineJoiner, SEAM_MIN_DELTA_E, POWERLINE_JOINER_GLYPHS, CapsuleJoiner, PlainJoiner, GradientJoiner, } from "./core/strip.js";
|
|
42
42
|
// renderToString — stateless one-shot emission
|
|
43
43
|
export { renderToString, segmentToString, segmentsToString, } from "./core/render.js";
|
|
44
44
|
// OSC 8 hyperlink wire grammar — for consumers that read rendered bytes back
|
|
@@ -55,7 +55,13 @@
|
|
|
55
55
|
* from that point on the math is pure. A consumer with no theme system at all still gets
|
|
56
56
|
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
57
|
*/
|
|
58
|
-
import type { FuncMap } from "@promptctl/go-template-js";
|
|
58
|
+
import type { FuncMap, TemplateFunc } from "@promptctl/go-template-js";
|
|
59
|
+
import { ColorDepth } from "../core/color.js";
|
|
60
|
+
/**
|
|
61
|
+
* The `readableOn` binding, measuring at the depth `drawnAt` names when it is
|
|
62
|
+
* evaluated — a consumer that renders at a per-call depth registers its own.
|
|
63
|
+
*/
|
|
64
|
+
export declare const readableOnFunc: (drawnAt: () => ColorDepth) => TemplateFunc;
|
|
59
65
|
/**
|
|
60
66
|
* The palette-free color vocabulary: every function takes colors and returns a
|
|
61
67
|
* color, so they compose by nesting.
|
|
@@ -73,5 +79,9 @@ import type { FuncMap } from "@promptctl/go-template-js";
|
|
|
73
79
|
*
|
|
74
80
|
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
75
81
|
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
82
|
+
*
|
|
83
|
+
* @param drawnAt the depth the terminal will draw at, read on every
|
|
84
|
+
* `readableOn` evaluation so a host that learns its depth per render passes
|
|
85
|
+
* one getter; defaults to truecolor.
|
|
76
86
|
*/
|
|
77
|
-
export declare function colorFuncs(): FuncMap;
|
|
87
|
+
export declare function colorFuncs(drawnAt?: () => ColorDepth): FuncMap;
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
* from that point on the math is pure. A consumer with no theme system at all still gets
|
|
56
56
|
* the full color vocabulary by feeding it hex literals. [LAW:one-way-deps]
|
|
57
57
|
*/
|
|
58
|
-
import { blendRgb } from "../core/color.js";
|
|
58
|
+
import { blendRgb, ColorDepth } from "../core/color.js";
|
|
59
59
|
import { Oklch, IDENTITY } from "../core/oklch.js";
|
|
60
60
|
import { HEX_COLOR_RE, parseHexColor } from "../themes/colorRef.js";
|
|
61
61
|
import { darken, contrastFor, ensureContrast } from "../themes/colorMath.js";
|
|
@@ -147,7 +147,11 @@ const RATIO_RANGE = {
|
|
|
147
147
|
note: "4.5 = AA body text, 3 = AA large text",
|
|
148
148
|
};
|
|
149
149
|
const contrastOnFunc = colorFunc(["string"], ((bgHex) => contrastFor(asColor(bgHex, "contrastOn")).hex));
|
|
150
|
-
|
|
150
|
+
/**
|
|
151
|
+
* The `readableOn` binding, measuring at the depth `drawnAt` names when it is
|
|
152
|
+
* evaluated — a consumer that renders at a per-call depth registers its own.
|
|
153
|
+
*/
|
|
154
|
+
export const readableOnFunc = (drawnAt) => colorFunc(["string", "string", "float"], ((fgHex, bgHex, ratio) => ensureContrast(asColor(fgHex, "readableOn"), asColor(bgHex, "readableOn"), asAmount(ratio, "readableOn", "ratio", RATIO_RANGE), drawnAt()).hex));
|
|
151
155
|
// --- OKLCH axes ---
|
|
152
156
|
//
|
|
153
157
|
// [LAW:one-source-of-truth] One function per `ThemeKey` axis, and the table is
|
|
@@ -198,14 +202,18 @@ function oklchAxisFuncs() {
|
|
|
198
202
|
*
|
|
199
203
|
* Pair with `paletteFuncs()` to name colors from a theme, and with
|
|
200
204
|
* `richTextStyleFuncs()`'s `fg`/`bg` to paint them onto text.
|
|
205
|
+
*
|
|
206
|
+
* @param drawnAt the depth the terminal will draw at, read on every
|
|
207
|
+
* `readableOn` evaluation so a host that learns its depth per render passes
|
|
208
|
+
* one getter; defaults to truecolor.
|
|
201
209
|
*/
|
|
202
|
-
export function colorFuncs() {
|
|
210
|
+
export function colorFuncs(drawnAt = () => ColorDepth.TRUECOLOR) {
|
|
203
211
|
return {
|
|
204
212
|
darken: darkenFunc,
|
|
205
213
|
lighten: lightenFunc,
|
|
206
214
|
mix: mixFunc,
|
|
207
215
|
contrastOn: contrastOnFunc,
|
|
208
|
-
readableOn: readableOnFunc,
|
|
216
|
+
readableOn: readableOnFunc(drawnAt),
|
|
209
217
|
...oklchAxisFuncs(),
|
|
210
218
|
};
|
|
211
219
|
}
|
|
@@ -36,13 +36,15 @@
|
|
|
36
36
|
import { type Engine, type FuncMap } from "@promptctl/go-template-js";
|
|
37
37
|
import { RichText } from "../core/text.js";
|
|
38
38
|
import { Segment } from "../core/segment.js";
|
|
39
|
+
import { ColorDepth } from "../core/color.js";
|
|
39
40
|
export { paletteFuncs } from "./palette-funcs.js";
|
|
40
|
-
export { colorFuncs } from "./color-funcs.js";
|
|
41
|
+
export { colorFuncs, readableOnFunc } from "./color-funcs.js";
|
|
41
42
|
/**
|
|
42
43
|
* Funcs registered by the rich-js binding — the colour sinks, the palette-free
|
|
43
|
-
* colour math, text attributes, and the `link` cell-splitter.
|
|
44
|
-
*
|
|
45
|
-
* `
|
|
44
|
+
* colour math, text attributes, and the `link` cell-splitter. Nothing here
|
|
45
|
+
* needs a theme; the two that do (`color`, `ramp`) ship separately via
|
|
46
|
+
* `paletteFuncs(getPalette)`, merged consumer-side. `drawnAt` is forwarded to
|
|
47
|
+
* `colorFuncs` — the depth `readableOn` measures at, truecolor when omitted.
|
|
46
48
|
*
|
|
47
49
|
* `FuncMap` is not parameterised over `T` in `@promptctl/go-template-js` — the engine's
|
|
48
50
|
* `T` lives on the `Engine`/`EngineConfig`, and per-function input/output
|
|
@@ -55,7 +57,7 @@ export { colorFuncs } from "./color-funcs.js";
|
|
|
55
57
|
* into a wider engine must keep `T = RichText`; merging into an engine
|
|
56
58
|
* whose `T` is something else will compile but fail at evaluation time.
|
|
57
59
|
*/
|
|
58
|
-
export declare function richTextFuncs(): FuncMap;
|
|
60
|
+
export declare function richTextFuncs(drawnAt?: () => ColorDepth): FuncMap;
|
|
59
61
|
/**
|
|
60
62
|
* Construct an `Engine<RichText>` with the rich-js style-function set
|
|
61
63
|
* registered. Consumers that already manage their own engine should call
|
|
@@ -40,12 +40,13 @@ import { Segment } from "../core/segment.js";
|
|
|
40
40
|
import { richTextStyleFuncs } from "./style-funcs.js";
|
|
41
41
|
import { colorFuncs } from "./color-funcs.js";
|
|
42
42
|
export { paletteFuncs } from "./palette-funcs.js";
|
|
43
|
-
export { colorFuncs } from "./color-funcs.js";
|
|
43
|
+
export { colorFuncs, readableOnFunc } from "./color-funcs.js";
|
|
44
44
|
/**
|
|
45
45
|
* Funcs registered by the rich-js binding — the colour sinks, the palette-free
|
|
46
|
-
* colour math, text attributes, and the `link` cell-splitter.
|
|
47
|
-
*
|
|
48
|
-
* `
|
|
46
|
+
* colour math, text attributes, and the `link` cell-splitter. Nothing here
|
|
47
|
+
* needs a theme; the two that do (`color`, `ramp`) ship separately via
|
|
48
|
+
* `paletteFuncs(getPalette)`, merged consumer-side. `drawnAt` is forwarded to
|
|
49
|
+
* `colorFuncs` — the depth `readableOn` measures at, truecolor when omitted.
|
|
49
50
|
*
|
|
50
51
|
* `FuncMap` is not parameterised over `T` in `@promptctl/go-template-js` — the engine's
|
|
51
52
|
* `T` lives on the `Engine`/`EngineConfig`, and per-function input/output
|
|
@@ -58,8 +59,10 @@ export { colorFuncs } from "./color-funcs.js";
|
|
|
58
59
|
* into a wider engine must keep `T = RichText`; merging into an engine
|
|
59
60
|
* whose `T` is something else will compile but fail at evaluation time.
|
|
60
61
|
*/
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
// [LAW:one-source-of-truth] The truecolor default lives on `colorFuncs` alone;
|
|
63
|
+
// an omitted `drawnAt` is forwarded as omitted.
|
|
64
|
+
export function richTextFuncs(drawnAt) {
|
|
65
|
+
return { ...richTextStyleFuncs(), ...colorFuncs(drawnAt) };
|
|
63
66
|
}
|
|
64
67
|
/**
|
|
65
68
|
* Construct an `Engine<RichText>` with the rich-js style-function set
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorRgba } from "../core/color.js";
|
|
1
|
+
import { ColorDepth, ColorRgba, contrastRatio, relativeLuminance } from "../core/color.js";
|
|
2
2
|
/**
|
|
3
3
|
* Darken a color by N levels, where each level reduces HSL lightness by 10%.
|
|
4
4
|
* Negative levels lighten. Level 0 returns an equivalent triplet (after the
|
|
@@ -20,23 +20,7 @@ export declare function alphaBlend(fg: ColorRgba, bg: ColorRgba, alpha: number):
|
|
|
20
20
|
* where black and white are equally readable).
|
|
21
21
|
*/
|
|
22
22
|
export declare function contrastFor(bg: ColorRgba): ColorRgba;
|
|
23
|
-
|
|
24
|
-
* WCAG 2.x relative luminance (0..1) of an opaque color. The single
|
|
25
|
-
* luminance function in the codebase — `contrastFor`, `contrastRatio`, and
|
|
26
|
-
* any caller that needs to reason about readability all funnel through it.
|
|
27
|
-
* [LAW:one-source-of-truth]
|
|
28
|
-
*/
|
|
29
|
-
export declare function relativeLuminance(c: ColorRgba): number;
|
|
30
|
-
/**
|
|
31
|
-
* WCAG 2.x contrast ratio between two colors, in [1, 21]. Symmetric — the
|
|
32
|
-
* order of arguments does not matter. 4.5 is the AA threshold for normal
|
|
33
|
-
* text, 3.0 for large text.
|
|
34
|
-
*
|
|
35
|
-
* Assumes opaque inputs: alpha is ignored, since the displayed contrast of a
|
|
36
|
-
* translucent color depends on what it composites over. For a translucent
|
|
37
|
-
* foreground, flatten it first (or use `ensureContrast`, which does).
|
|
38
|
-
*/
|
|
39
|
-
export declare function contrastRatio(a: ColorRgba, b: ColorRgba): number;
|
|
23
|
+
export { relativeLuminance, contrastRatio };
|
|
40
24
|
/**
|
|
41
25
|
* Return a foreground guaranteed to clear `minRatio` against `bg`, keeping the
|
|
42
26
|
* color *recognizably itself*. If the themed `fg` already passes it is returned
|
|
@@ -53,10 +37,18 @@ export declare function contrastRatio(a: ColorRgba, b: ColorRgba): number;
|
|
|
53
37
|
* actually sees and the returned color is opaque. `bg` is treated as the
|
|
54
38
|
* opaque substrate.
|
|
55
39
|
*
|
|
40
|
+
* `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
|
|
41
|
+
* the terminal rounds text and background independently, and two roundings
|
|
42
|
+
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
43
|
+
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
44
|
+
* that clears it (whose own rounding is itself). Every other depth draws a
|
|
45
|
+
* colour the chosen one IS (truecolor) or one only the terminal knows (ANSI).
|
|
46
|
+
*
|
|
56
47
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
57
48
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
58
49
|
* unreadable state never reaches output. [LAW:dataflow-not-control-flow] the
|
|
59
50
|
* function always runs; the measured ratio (data) decides how far the
|
|
60
51
|
* lightness moves — there is no caller-side "should I check contrast" branch.
|
|
61
52
|
*/
|
|
62
|
-
export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number
|
|
53
|
+
export declare function ensureContrast(fg: ColorRgba, bg: ColorRgba, minRatio?: number, // WCAG AA for normal text
|
|
54
|
+
drawnAt?: ColorDepth): ColorRgba;
|
package/dist/themes/colorMath.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ColorRgba, blendRgb } from "../core/color.js";
|
|
1
|
+
import { ColorDepth, ColorRgba, EIGHT_BIT_DOWNGRADE_TABLE, blendRgb, contrastRatio, relativeLuminance, } from "../core/color.js";
|
|
2
2
|
import { Oklch } from "../core/oklch.js";
|
|
3
3
|
const LEVEL_STEP = 0.1;
|
|
4
4
|
function rgbToHsl(c) {
|
|
@@ -90,35 +90,11 @@ export function contrastFor(bg) {
|
|
|
90
90
|
? new ColorRgba(0, 0, 0)
|
|
91
91
|
: new ColorRgba(255, 255, 255);
|
|
92
92
|
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
*/
|
|
99
|
-
export function relativeLuminance(c) {
|
|
100
|
-
const ch = (v) => {
|
|
101
|
-
const x = v / 255;
|
|
102
|
-
return x <= 0.03928 ? x / 12.92 : Math.pow((x + 0.055) / 1.055, 2.4);
|
|
103
|
-
};
|
|
104
|
-
return 0.2126 * ch(c.red) + 0.7152 * ch(c.green) + 0.0722 * ch(c.blue);
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* WCAG 2.x contrast ratio between two colors, in [1, 21]. Symmetric — the
|
|
108
|
-
* order of arguments does not matter. 4.5 is the AA threshold for normal
|
|
109
|
-
* text, 3.0 for large text.
|
|
110
|
-
*
|
|
111
|
-
* Assumes opaque inputs: alpha is ignored, since the displayed contrast of a
|
|
112
|
-
* translucent color depends on what it composites over. For a translucent
|
|
113
|
-
* foreground, flatten it first (or use `ensureContrast`, which does).
|
|
114
|
-
*/
|
|
115
|
-
export function contrastRatio(a, b) {
|
|
116
|
-
const la = relativeLuminance(a);
|
|
117
|
-
const lb = relativeLuminance(b);
|
|
118
|
-
const hi = la > lb ? la : lb;
|
|
119
|
-
const lo = la > lb ? lb : la;
|
|
120
|
-
return (hi + 0.05) / (lo + 0.05);
|
|
121
|
-
}
|
|
93
|
+
// [LAW:one-way-deps] The WCAG measures live in core/color.ts, beside the
|
|
94
|
+
// ColorTable whose `matchReadable` needs them to pick a drawn text colour, and
|
|
95
|
+
// below the theme math here. Re-exported so this module stays the colour-math
|
|
96
|
+
// surface.
|
|
97
|
+
export { relativeLuminance, contrastRatio };
|
|
122
98
|
// Iterations for the lightness bisection below. 20 resolves L to ~1e-6 — far
|
|
123
99
|
// finer than 8-bit quantization or the eye.
|
|
124
100
|
const CONTRAST_ITERS = 20;
|
|
@@ -138,13 +114,44 @@ const CONTRAST_ITERS = 20;
|
|
|
138
114
|
* actually sees and the returned color is opaque. `bg` is treated as the
|
|
139
115
|
* opaque substrate.
|
|
140
116
|
*
|
|
117
|
+
* `drawnAt` is the depth the terminal will draw the pair at. At 256 colours
|
|
118
|
+
* the terminal rounds text and background independently, and two roundings
|
|
119
|
+
* can meet in the middle, so the ratio is measured on the drawn pair: a
|
|
120
|
+
* colour that loses the floor there is replaced by the nearest cube/grey entry
|
|
121
|
+
* that clears it (whose own rounding is itself). Every other depth draws a
|
|
122
|
+
* colour the chosen one IS (truecolor) or one only the terminal knows (ANSI).
|
|
123
|
+
*
|
|
141
124
|
* [LAW:single-enforcer] The one place "is this text readable, and if not fix
|
|
142
125
|
* it" is decided. Callers route every fg/bg pair through here and the
|
|
143
126
|
* unreadable state never reaches output. [LAW:dataflow-not-control-flow] the
|
|
144
127
|
* function always runs; the measured ratio (data) decides how far the
|
|
145
128
|
* lightness moves — there is no caller-side "should I check contrast" branch.
|
|
146
129
|
*/
|
|
147
|
-
export function ensureContrast(fg, bg, minRatio = 4.5
|
|
130
|
+
export function ensureContrast(fg, bg, minRatio = 4.5, // WCAG AA for normal text
|
|
131
|
+
drawnAt = ColorDepth.TRUECOLOR) {
|
|
132
|
+
const chosen = ensureTruecolorContrast(fg, bg, minRatio);
|
|
133
|
+
// [LAW:dataflow-not-control-flow] The depth names the table the terminal
|
|
134
|
+
// draws from; only one whose entries have a known RGB can be measured.
|
|
135
|
+
const table = MEASURABLE_DOWNGRADE[drawnAt];
|
|
136
|
+
if (table === undefined)
|
|
137
|
+
return chosen;
|
|
138
|
+
const drawnBg = table.get(table.match(bg));
|
|
139
|
+
const drawn = table.get(table.match(chosen));
|
|
140
|
+
if (contrastRatio(drawn, drawnBg) >= minRatio)
|
|
141
|
+
return chosen;
|
|
142
|
+
return table.get(table.matchReadable(chosen, drawnBg, minRatio));
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* The downgrade tables whose entries the terminal draws at a known RGB, by the
|
|
146
|
+
* depth that draws from them. 256 colours is the one: its cube and grey ramp
|
|
147
|
+
* are fixed by xterm. ANSI 0–15 and the default colour are the terminal
|
|
148
|
+
* theme's own, so text drawn there has no ratio to keep; truecolor draws the
|
|
149
|
+
* chosen colour itself.
|
|
150
|
+
*/
|
|
151
|
+
const MEASURABLE_DOWNGRADE = {
|
|
152
|
+
[ColorDepth.EIGHT_BIT]: EIGHT_BIT_DOWNGRADE_TABLE,
|
|
153
|
+
};
|
|
154
|
+
function ensureTruecolorContrast(fg, bg, minRatio) {
|
|
148
155
|
// Flatten translucency so the guarantee holds for the displayed color, not
|
|
149
156
|
// the raw bytes (e.g. a "#FFFFFF60" text-disabled over a light surface).
|
|
150
157
|
const opaqueFg = fg.compositeOver(bg);
|