@altpsyche/maths 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/README.md +124 -326
- package/dist/figure/animation.d.ts +6 -4
- package/dist/figure/animation.js +20 -4
- package/dist/figure/annotate.d.ts +3 -2
- package/dist/figure/annotate.js +3 -2
- package/dist/figure/axis.js +5 -8
- package/dist/figure/axis3.d.ts +8 -0
- package/dist/figure/axis3.js +13 -0
- package/dist/figure/boolean.js +2 -2
- package/dist/figure/equation.d.ts +1 -1
- package/dist/figure/equation.js +2 -2
- package/dist/figure/field.d.ts +6 -6
- package/dist/figure/field.js +5 -6
- package/dist/figure/field3.d.ts +50 -0
- package/dist/figure/field3.js +60 -0
- package/dist/figure/figure.d.ts +4 -4
- package/dist/figure/figure.js +4 -4
- package/dist/figure/frames.d.ts +57 -0
- package/dist/figure/frames.js +43 -0
- package/dist/figure/grid.d.ts +19 -0
- package/dist/figure/grid.js +32 -0
- package/dist/figure/inside.d.ts +2 -2
- package/dist/figure/inside.js +3 -4
- package/dist/figure/length.d.ts +1 -3
- package/dist/figure/length.js +7 -10
- package/dist/figure/mark.d.ts +7 -2
- package/dist/figure/node.js +2 -3
- package/dist/figure/path-data.js +2 -3
- package/dist/figure/path.d.ts +1 -1
- package/dist/figure/path.js +3 -3
- package/dist/figure/plot.d.ts +1 -1
- package/dist/figure/plot.js +9 -13
- package/dist/figure/scale.d.ts +2 -2
- package/dist/figure/scale.js +3 -3
- package/dist/figure/section.d.ts +5 -2
- package/dist/figure/section.js +9 -16
- package/dist/figure/space.d.ts +8 -91
- package/dist/figure/space.js +7 -110
- package/dist/figure/surface3.d.ts +62 -0
- package/dist/figure/surface3.js +58 -0
- package/dist/index.d.ts +19 -11
- package/dist/index.js +10 -6
- package/dist/paint/number.js +1 -2
- package/dist/paint/svg.d.ts +29 -4
- package/dist/paint/svg.js +49 -14
- package/dist/values/colour.d.ts +43 -0
- package/dist/values/colour.js +110 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* Nothing below the line may import anything above it.
|
|
9
9
|
*/
|
|
10
10
|
export { clamp, inverseLerp, lerp, remap } from './values/scalar.js';
|
|
11
|
+
export { colourOf, colourText, lerpColour } from './values/colour.js';
|
|
11
12
|
export { curveFor, easeIn, easeOut, linear, smoothstep } from './values/ease.js';
|
|
12
13
|
export { vec2 } from './values/vec2.js';
|
|
13
14
|
export { vec3 } from './values/vec3.js';
|
|
@@ -15,7 +16,7 @@ export { interval } from './values/interval.js';
|
|
|
15
16
|
export { mat3 } from './values/mat3.js';
|
|
16
17
|
export { mat4 } from './values/mat4.js';
|
|
17
18
|
export { SAME_TIME, keyAt, sampleTrack, sampleTracks, withKey, withoutKey } from './timing/track.js';
|
|
18
|
-
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect,
|
|
19
|
+
export { arc, circle, line, polygon, polyline, pointCount, pointOn, rect, splitCurve, straight, tangentOn, transformPath } from './figure/path.js';
|
|
19
20
|
export { pathFromData } from './figure/path-data.js';
|
|
20
21
|
export { TOLERANCE } from './figure/tolerance.js';
|
|
21
22
|
export { areaOf } from './figure/area.js';
|
|
@@ -30,10 +31,12 @@ export { vectorField } from './figure/field.js';
|
|
|
30
31
|
export { streamlineOf } from './figure/streamline.js';
|
|
31
32
|
export { axes, numberLine, numberPlane } from './figure/axis.js';
|
|
32
33
|
export { camera3, orthographic, perspective } from './figure/camera.js';
|
|
33
|
-
export { arrow3, dot3,
|
|
34
|
+
export { arrow3, dot3, polyline3, scene3, text3 } from './figure/space.js';
|
|
35
|
+
export { fieldArrows3, vectorField3 } from './figure/field3.js';
|
|
36
|
+
export { surface3, surfaceCells } from './figure/surface3.js';
|
|
34
37
|
export { axes3 } from './figure/axis3.js';
|
|
35
38
|
export { sectionOf } from './figure/section.js';
|
|
36
|
-
export { coordsOf, pointOf, scaleOf,
|
|
39
|
+
export { coordsOf, pointOf, scaleOf, toGraph, toUnits } from './figure/scale.js';
|
|
37
40
|
export { labelFor, tickStep, ticksOn } from './figure/ticks.js';
|
|
38
41
|
export { flatten, group, shape, text } from './figure/node.js';
|
|
39
42
|
export { circumscribe, countTo, fadeIn, fadeOut, fadeTo, draw, flash, growFrom, indicate, morph, morphEquation, moveAlong, moveBy, rotate, scale } from './figure/animation.js';
|
|
@@ -41,10 +44,11 @@ export { Timeline } from './figure/timeline.js';
|
|
|
41
44
|
export { lengthOf, pointAlong } from './figure/length.js';
|
|
42
45
|
export { trimPath } from './figure/trim.js';
|
|
43
46
|
export { alignPaths, lerpPath } from './figure/morph.js';
|
|
44
|
-
export {
|
|
45
|
-
export {
|
|
47
|
+
export { durationOf, isLoop, marksAt, sameMarks, viewAt } from './figure/figure.js';
|
|
48
|
+
export { frameTimesOf, framesOf } from './figure/frames.js';
|
|
49
|
+
export { paintSvg, pathToData, svgElements, svgMarkup } from './paint/svg.js';
|
|
46
50
|
export { paintCanvas } from './paint/canvas.js';
|
|
47
51
|
export { arrow, brace, bracePath, callout, dot } from './figure/annotate.js';
|
|
48
52
|
export { typesetElement } from './figure/typeset.js';
|
|
49
|
-
export { equationFromTex,
|
|
53
|
+
export { equationFromTex, equationNode, equationOf } from './figure/equation.js';
|
|
50
54
|
export { glyphToken, matchGlyphs } from './figure/equation-match.js';
|
package/dist/paint/number.js
CHANGED
|
@@ -8,8 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const PLACES = 3;
|
|
10
10
|
export function short(value) {
|
|
11
|
-
//
|
|
12
|
-
// negative zero is written as zero, because otherwise the same coordinate
|
|
11
|
+
// An integer keeps no decimal point and a negative zero is written as zero, or the same coordinate
|
|
13
12
|
// reached two ways would compare as two strings.
|
|
14
13
|
const rounded = Number(value.toFixed(PLACES));
|
|
15
14
|
return Object.is(rounded, -0) ? '0' : String(rounded);
|
package/dist/paint/svg.d.ts
CHANGED
|
@@ -23,18 +23,43 @@ export interface SvgElement {
|
|
|
23
23
|
attributes: Record<string, string>;
|
|
24
24
|
text?: string;
|
|
25
25
|
}
|
|
26
|
+
/** A colour per ground for one CSS custom property. A mark painted with
|
|
27
|
+
* `var(--name, colour)` takes the value of the ground it is read on, and the
|
|
28
|
+
* colour written inside the `var()` is what it falls back to. */
|
|
29
|
+
export interface SvgTheme {
|
|
30
|
+
[property: string]: {
|
|
31
|
+
light: string;
|
|
32
|
+
dark: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface SvgMarkupOptions {
|
|
36
|
+
/** Written into the markup as a `<style>` element, so one file is read on a
|
|
37
|
+
* light page and a dark one with no script and no page CSS. */
|
|
38
|
+
theme?: SvgTheme;
|
|
39
|
+
/**
|
|
40
|
+
* The smallest font size written, in the units painted into rather than in
|
|
41
|
+
* figure units. A view that fits a wide extent scales every figure unit down,
|
|
42
|
+
* so a glyph readable in one frame is not readable in a row of them.
|
|
43
|
+
*
|
|
44
|
+
* Every text size is multiplied by the one factor that brings the smallest of
|
|
45
|
+
* them to this, which holds the sizes in the ratios the figure gave them.
|
|
46
|
+
* Raising each size on its own to the floor would flatten two sizes that both
|
|
47
|
+
* fall under it into one.
|
|
48
|
+
*/
|
|
49
|
+
minTextSize?: number;
|
|
50
|
+
}
|
|
26
51
|
/** The `d` attribute: a move to the start, a cubic per segment, and a close
|
|
27
52
|
* where the subpath joins back. */
|
|
28
|
-
export declare function
|
|
53
|
+
export declare function pathToData(path: Path, view: Mat3): string;
|
|
29
54
|
/** Every mark described as an element, in the order they are drawn. */
|
|
30
|
-
export declare function svgElements(marks: readonly Mark[], view: Mat3): SvgElement[];
|
|
55
|
+
export declare function svgElements(marks: readonly Mark[], view: Mat3, options?: SvgMarkupOptions): SvgElement[];
|
|
31
56
|
/**
|
|
32
57
|
* A whole `<svg>` as text, for a page that has not run any script yet.
|
|
33
58
|
*
|
|
34
59
|
* It carries no width or height of its own and only a view box, so the element
|
|
35
60
|
* around it decides how big it is and the picture stays where it was put.
|
|
36
61
|
*/
|
|
37
|
-
export declare function svgMarkup(marks: readonly Mark[], view: Mat3, width: number, height: number): string;
|
|
62
|
+
export declare function svgMarkup(marks: readonly Mark[], view: Mat3, width: number, height: number, options?: SvgMarkupOptions): string;
|
|
38
63
|
/**
|
|
39
64
|
* Only what a painter needs from a document, named here rather than taken from
|
|
40
65
|
* the DOM types, so this package declares no browser library at all: it can be
|
|
@@ -66,4 +91,4 @@ export interface ElementMaker<Made extends PaintNode = PaintNode> {
|
|
|
66
91
|
* and matching them up first would cost more than it saved while adding a way for
|
|
67
92
|
* two frames to disagree.
|
|
68
93
|
*/
|
|
69
|
-
export declare function paintSvg<Made extends PaintNode>(into: PaintTarget<NoInfer<Made>>, marks: readonly Mark[], view: Mat3, maker: ElementMaker<Made
|
|
94
|
+
export declare function paintSvg<Made extends PaintNode>(into: PaintTarget<NoInfer<Made>>, marks: readonly Mark[], view: Mat3, maker: ElementMaker<Made>, options?: SvgMarkupOptions): void;
|
package/dist/paint/svg.js
CHANGED
|
@@ -17,7 +17,7 @@ import { mat3 } from '../values/mat3.js';
|
|
|
17
17
|
import { short } from './number.js';
|
|
18
18
|
/** The `d` attribute: a move to the start, a cubic per segment, and a close
|
|
19
19
|
* where the subpath joins back. */
|
|
20
|
-
export function
|
|
20
|
+
export function pathToData(path, view) {
|
|
21
21
|
const parts = [];
|
|
22
22
|
for (const subpath of path) {
|
|
23
23
|
const start = mat3.transformPoint(view, subpath.start);
|
|
@@ -36,7 +36,7 @@ export function pathData(path, view) {
|
|
|
36
36
|
function pathElement(mark, view, scale) {
|
|
37
37
|
const attributes = {
|
|
38
38
|
'data-mark': mark.id,
|
|
39
|
-
d:
|
|
39
|
+
d: pathToData(mark.path, view),
|
|
40
40
|
fill: mark.fill ? mark.fill.colour : 'none',
|
|
41
41
|
};
|
|
42
42
|
if (mark.fill?.rule === 'evenodd')
|
|
@@ -57,14 +57,14 @@ function pathElement(mark, view, scale) {
|
|
|
57
57
|
attributes.opacity = short(mark.opacity);
|
|
58
58
|
return { tag: 'path', attributes };
|
|
59
59
|
}
|
|
60
|
-
function textElement(mark, view, scale) {
|
|
60
|
+
function textElement(mark, view, scale, lift) {
|
|
61
61
|
const at = mat3.transformPoint(view, mark.at);
|
|
62
62
|
const attributes = {
|
|
63
63
|
'data-mark': mark.id,
|
|
64
64
|
x: short(at.x),
|
|
65
65
|
y: short(at.y),
|
|
66
66
|
'font-family': mark.family,
|
|
67
|
-
'font-size': short(mark.size * scale),
|
|
67
|
+
'font-size': short(mark.size * scale * lift),
|
|
68
68
|
fill: mark.fill.colour,
|
|
69
69
|
};
|
|
70
70
|
if (mark.weight !== undefined)
|
|
@@ -77,23 +77,59 @@ function textElement(mark, view, scale) {
|
|
|
77
77
|
attributes.opacity = short(mark.opacity);
|
|
78
78
|
return { tag: 'text', attributes, text: mark.text };
|
|
79
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* What every text size is multiplied by so the smallest of them reaches the
|
|
82
|
+
* floor, or one where they already do and where no text is drawn at all.
|
|
83
|
+
*/
|
|
84
|
+
function textLift(marks, scale, floor) {
|
|
85
|
+
if (floor <= 0)
|
|
86
|
+
return 1;
|
|
87
|
+
const written = marks.filter((mark) => mark.kind === 'text').map((mark) => mark.size * scale);
|
|
88
|
+
const smallest = Math.min(...written.filter((size) => size > 0));
|
|
89
|
+
if (!Number.isFinite(smallest))
|
|
90
|
+
return 1;
|
|
91
|
+
return Math.max(1, floor / smallest);
|
|
92
|
+
}
|
|
80
93
|
/** Every mark described as an element, in the order they are drawn. */
|
|
81
|
-
export function svgElements(marks, view) {
|
|
94
|
+
export function svgElements(marks, view, options = {}) {
|
|
82
95
|
const scale = mat3.scaleFactor(view);
|
|
83
|
-
|
|
96
|
+
const lift = textLift(marks, scale, options.minTextSize ?? 0);
|
|
97
|
+
return marks.map((mark) => mark.kind === 'path' ? pathElement(mark, view, scale) : textElement(mark, view, scale, lift));
|
|
84
98
|
}
|
|
85
99
|
/** The five characters that would otherwise close a tag or open an entity. */
|
|
86
100
|
function escaped(value) {
|
|
87
101
|
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
88
102
|
}
|
|
103
|
+
const PROPERTY = /^[A-Za-z0-9_-]+$/;
|
|
104
|
+
/** A value safe to write between `<style>` tags: no delimiter that would end the
|
|
105
|
+
* declaration or the element. */
|
|
106
|
+
function plainValue(value) {
|
|
107
|
+
return !/[<>&{};"]/.test(value);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* The theme as a `<style>` element, the light ground on `:root` and the dark one
|
|
111
|
+
* behind `prefers-color-scheme`.
|
|
112
|
+
*
|
|
113
|
+
* An entry whose name or either colour would need escaping is left out, which
|
|
114
|
+
* leaves the mark on the colour written inside its own `var()` rather than on a
|
|
115
|
+
* value that could close the element.
|
|
116
|
+
*/
|
|
117
|
+
function themeStyle(theme) {
|
|
118
|
+
const names = Object.keys(theme).filter((name) => PROPERTY.test(name) && plainValue(theme[name].light) && plainValue(theme[name].dark));
|
|
119
|
+
if (names.length === 0)
|
|
120
|
+
return '';
|
|
121
|
+
const block = (ground) => names.map((name) => `--${name}:${theme[name][ground]}`).join(';');
|
|
122
|
+
return `<style>:root{${block('light')}}@media(prefers-color-scheme:dark){:root{${block('dark')}}}</style>`;
|
|
123
|
+
}
|
|
89
124
|
/**
|
|
90
125
|
* A whole `<svg>` as text, for a page that has not run any script yet.
|
|
91
126
|
*
|
|
92
127
|
* It carries no width or height of its own and only a view box, so the element
|
|
93
128
|
* around it decides how big it is and the picture stays where it was put.
|
|
94
129
|
*/
|
|
95
|
-
export function svgMarkup(marks, view, width, height) {
|
|
96
|
-
const
|
|
130
|
+
export function svgMarkup(marks, view, width, height, options = {}) {
|
|
131
|
+
const style = options.theme ? themeStyle(options.theme) : '';
|
|
132
|
+
const body = svgElements(marks, view, options)
|
|
97
133
|
.map((element) => {
|
|
98
134
|
const attributes = Object.entries(element.attributes)
|
|
99
135
|
.map(([name, value]) => `${name}="${escaped(value)}"`)
|
|
@@ -103,7 +139,7 @@ export function svgMarkup(marks, view, width, height) {
|
|
|
103
139
|
return `<${element.tag} ${attributes}>${escaped(element.text)}</${element.tag}>`;
|
|
104
140
|
})
|
|
105
141
|
.join('');
|
|
106
|
-
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${short(width)} ${short(height)}">${body}</svg>`;
|
|
142
|
+
return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${short(width)} ${short(height)}">${style}${body}</svg>`;
|
|
107
143
|
}
|
|
108
144
|
const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
109
145
|
/**
|
|
@@ -115,11 +151,10 @@ const SVG_NAMESPACE = 'http://www.w3.org/2000/svg';
|
|
|
115
151
|
* two frames to disagree.
|
|
116
152
|
*/
|
|
117
153
|
export function paintSvg(
|
|
118
|
-
// The maker alone says what kind of element this is
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const elements = svgElements(marks, view);
|
|
154
|
+
// The maker alone says what kind of element this is: read from the target as well, a real element
|
|
155
|
+
// would offer the whole union its own call accepts, which is not one this painter can write to.
|
|
156
|
+
into, marks, view, maker, options = {}) {
|
|
157
|
+
const elements = svgElements(marks, view, options);
|
|
123
158
|
into.replaceChildren(...elements.map((element) => {
|
|
124
159
|
const node = maker.createElementNS(SVG_NAMESPACE, element.tag);
|
|
125
160
|
for (const [name, value] of Object.entries(element.attributes))
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a colour out of the text a figure is written with, so two of them can
|
|
3
|
+
* be walked between.
|
|
4
|
+
*
|
|
5
|
+
* A colour here is a string, which is what both painters take and what a CSS
|
|
6
|
+
* author already knows how to write. Nothing reads it back, so an animation that
|
|
7
|
+
* wants to cross-fade has nothing to work with. This reads the two forms a figure
|
|
8
|
+
* is actually handed, hex and `rgb()`, and refuses every other form rather than
|
|
9
|
+
* guessing at it: a named colour or an `hsl()` read as black is a wrong picture
|
|
10
|
+
* with nothing to say it went wrong.
|
|
11
|
+
*/
|
|
12
|
+
/** A colour read out of its text, each channel nothing to 255 and the alpha
|
|
13
|
+
* nothing to one. */
|
|
14
|
+
export interface Rgba {
|
|
15
|
+
r: number;
|
|
16
|
+
g: number;
|
|
17
|
+
b: number;
|
|
18
|
+
a: number;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* A colour read out of its text, or nothing where the form is one this does not
|
|
22
|
+
* read.
|
|
23
|
+
*
|
|
24
|
+
* Hex takes three, four, six or eight digits, which are the short forms with and
|
|
25
|
+
* without an alpha and the long ones. `rgb()` and `rgba()` take their channels
|
|
26
|
+
* separated by commas or by spaces, as numbers or as percentages, and either name
|
|
27
|
+
* takes an alpha, since CSS stopped keeping them apart.
|
|
28
|
+
*/
|
|
29
|
+
export declare function colourOf(colour: string): Rgba | undefined;
|
|
30
|
+
/** A colour written back out, as `rgb()` where it is opaque and `rgba()` where it
|
|
31
|
+
* is not, with the channels rounded so the same colour reached two ways is the
|
|
32
|
+
* same string. */
|
|
33
|
+
export declare function colourText({ r, g, b, a }: Rgba): string;
|
|
34
|
+
/**
|
|
35
|
+
* A colour a fraction of the way from one to another, or nothing where either
|
|
36
|
+
* end is a form `colourOf` does not read.
|
|
37
|
+
*
|
|
38
|
+
* The walk is straight through each channel in sRGB, which is what CSS mixes in
|
|
39
|
+
* when nothing names a space. Mixing in a space with even lightness would keep a
|
|
40
|
+
* mid-point from going dull, and that is a different call with a different name
|
|
41
|
+
* rather than a quiet change to this one.
|
|
42
|
+
*/
|
|
43
|
+
export declare function lerpColour(from: string, to: string, along: number): string | undefined;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reading a colour out of the text a figure is written with, so two of them can
|
|
3
|
+
* be walked between.
|
|
4
|
+
*
|
|
5
|
+
* A colour here is a string, which is what both painters take and what a CSS
|
|
6
|
+
* author already knows how to write. Nothing reads it back, so an animation that
|
|
7
|
+
* wants to cross-fade has nothing to work with. This reads the two forms a figure
|
|
8
|
+
* is actually handed, hex and `rgb()`, and refuses every other form rather than
|
|
9
|
+
* guessing at it: a named colour or an `hsl()` read as black is a wrong picture
|
|
10
|
+
* with nothing to say it went wrong.
|
|
11
|
+
*/
|
|
12
|
+
const HEX = /^#([0-9a-f]{3,8})$/i;
|
|
13
|
+
const FUNCTIONAL = /^rgba?\(([^)]*)\)$/i;
|
|
14
|
+
function channel(text, top) {
|
|
15
|
+
const trimmed = text.trim();
|
|
16
|
+
if (trimmed === '')
|
|
17
|
+
return undefined;
|
|
18
|
+
const percent = trimmed.endsWith('%');
|
|
19
|
+
const value = Number(percent ? trimmed.slice(0, -1) : trimmed);
|
|
20
|
+
if (!Number.isFinite(value))
|
|
21
|
+
return undefined;
|
|
22
|
+
return percent ? (value / 100) * top : value;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* A colour read out of its text, or nothing where the form is one this does not
|
|
26
|
+
* read.
|
|
27
|
+
*
|
|
28
|
+
* Hex takes three, four, six or eight digits, which are the short forms with and
|
|
29
|
+
* without an alpha and the long ones. `rgb()` and `rgba()` take their channels
|
|
30
|
+
* separated by commas or by spaces, as numbers or as percentages, and either name
|
|
31
|
+
* takes an alpha, since CSS stopped keeping them apart.
|
|
32
|
+
*/
|
|
33
|
+
export function colourOf(colour) {
|
|
34
|
+
const text = colour.trim();
|
|
35
|
+
const hex = HEX.exec(text);
|
|
36
|
+
if (hex) {
|
|
37
|
+
const digits = hex[1];
|
|
38
|
+
if (digits.length === 3 || digits.length === 4) {
|
|
39
|
+
const parts = [...digits].map((digit) => Number.parseInt(digit + digit, 16));
|
|
40
|
+
return { r: parts[0], g: parts[1], b: parts[2], a: parts[3] === undefined ? 1 : parts[3] / 255 };
|
|
41
|
+
}
|
|
42
|
+
if (digits.length === 6 || digits.length === 8) {
|
|
43
|
+
const parts = [0, 2, 4, 6]
|
|
44
|
+
.filter((at) => at < digits.length)
|
|
45
|
+
.map((at) => Number.parseInt(digits.slice(at, at + 2), 16));
|
|
46
|
+
return { r: parts[0], g: parts[1], b: parts[2], a: parts[3] === undefined ? 1 : parts[3] / 255 };
|
|
47
|
+
}
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
const call = FUNCTIONAL.exec(text);
|
|
51
|
+
if (!call)
|
|
52
|
+
return undefined;
|
|
53
|
+
const inside = call[1].includes(',') ? call[1].split(',') : call[1].replace('/', ' ').split(/\s+/);
|
|
54
|
+
const parts = inside.map((part) => part.trim()).filter((part) => part !== '');
|
|
55
|
+
if (parts.length < 3 || parts.length > 4)
|
|
56
|
+
return undefined;
|
|
57
|
+
const r = channel(parts[0], 255);
|
|
58
|
+
const g = channel(parts[1], 255);
|
|
59
|
+
const b = channel(parts[2], 255);
|
|
60
|
+
const a = parts[3] === undefined ? 1 : channel(parts[3], 1);
|
|
61
|
+
if (r === undefined || g === undefined || b === undefined || a === undefined)
|
|
62
|
+
return undefined;
|
|
63
|
+
return { r, g, b, a };
|
|
64
|
+
}
|
|
65
|
+
/** A colour written back out, as `rgb()` where it is opaque and `rgba()` where it
|
|
66
|
+
* is not, with the channels rounded so the same colour reached two ways is the
|
|
67
|
+
* same string. */
|
|
68
|
+
export function colourText({ r, g, b, a }) {
|
|
69
|
+
const whole = (value) => Math.round(Math.min(255, Math.max(0, value)));
|
|
70
|
+
const parts = `${whole(r)}, ${whole(g)}, ${whole(b)}`;
|
|
71
|
+
if (a >= 1)
|
|
72
|
+
return `rgb(${parts})`;
|
|
73
|
+
return `rgba(${parts}, ${Number(Math.min(1, Math.max(0, a)).toFixed(3))})`;
|
|
74
|
+
}
|
|
75
|
+
const VARIABLE = /^var\(\s*--[A-Za-z0-9_-]+\s*,\s*(.+)\)$/;
|
|
76
|
+
/**
|
|
77
|
+
* The colour a `var()` names as its fallback, or the text unchanged where it is
|
|
78
|
+
* not one.
|
|
79
|
+
*
|
|
80
|
+
* A themed colour is painted as `var(--ink, #1b1b1b)`, and the fallback is a
|
|
81
|
+
* colour written in a form `colourOf` already reads, so mixing from it is mixing
|
|
82
|
+
* from a stated value. The mix runs through that value on every ground, so a
|
|
83
|
+
* themed colour cross-faded on a dark page starts from its light end.
|
|
84
|
+
*/
|
|
85
|
+
function statedValue(colour) {
|
|
86
|
+
const call = VARIABLE.exec(colour.trim());
|
|
87
|
+
return call ? call[1].trim() : colour;
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A colour a fraction of the way from one to another, or nothing where either
|
|
91
|
+
* end is a form `colourOf` does not read.
|
|
92
|
+
*
|
|
93
|
+
* The walk is straight through each channel in sRGB, which is what CSS mixes in
|
|
94
|
+
* when nothing names a space. Mixing in a space with even lightness would keep a
|
|
95
|
+
* mid-point from going dull, and that is a different call with a different name
|
|
96
|
+
* rather than a quiet change to this one.
|
|
97
|
+
*/
|
|
98
|
+
export function lerpColour(from, to, along) {
|
|
99
|
+
const start = colourOf(statedValue(from));
|
|
100
|
+
const end = colourOf(statedValue(to));
|
|
101
|
+
if (!start || !end)
|
|
102
|
+
return undefined;
|
|
103
|
+
const at = (a, b) => a + (b - a) * along;
|
|
104
|
+
return colourText({
|
|
105
|
+
r: at(start.r, end.r),
|
|
106
|
+
g: at(start.g, end.g),
|
|
107
|
+
b: at(start.b, end.b),
|
|
108
|
+
a: at(start.a, end.a),
|
|
109
|
+
});
|
|
110
|
+
}
|
package/package.json
CHANGED