@juspay/svelte-ui-components 4.32.1 → 4.33.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/ThinkingOrb/ThinkingOrb.svelte +344 -0
- package/dist/ThinkingOrb/ThinkingOrb.svelte.d.ts +4 -0
- package/dist/ThinkingOrb/orb/gravity.d.ts +30 -0
- package/dist/ThinkingOrb/orb/gravity.js +64 -0
- package/dist/ThinkingOrb/orb/modes/form.d.ts +33 -0
- package/dist/ThinkingOrb/orb/modes/form.js +201 -0
- package/dist/ThinkingOrb/orb/modes/index.d.ts +9 -0
- package/dist/ThinkingOrb/orb/modes/index.js +20 -0
- package/dist/ThinkingOrb/orb/modes/network.d.ts +12 -0
- package/dist/ThinkingOrb/orb/modes/network.js +181 -0
- package/dist/ThinkingOrb/orb/modes/sphere.d.ts +11 -0
- package/dist/ThinkingOrb/orb/modes/sphere.js +364 -0
- package/dist/ThinkingOrb/orb/paint.d.ts +25 -0
- package/dist/ThinkingOrb/orb/paint.js +47 -0
- package/dist/ThinkingOrb/orb/space.d.ts +63 -0
- package/dist/ThinkingOrb/orb/space.js +115 -0
- package/dist/ThinkingOrb/orb/types.d.ts +52 -0
- package/dist/ThinkingOrb/orb/types.js +7 -0
- package/dist/ThinkingOrb/properties.d.ts +68 -0
- package/dist/ThinkingOrb/properties.js +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist-codemod/codemod/migration-assets/legacy-display.css +2 -1
- package/dist-codemod/codemod/migration-assets/wc-display.json +5 -0
- package/dist-wc/index.js +1044 -433
- package/package.json +1 -1
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place that maps a public `OrbState` name to the mode function
|
|
3
|
+
* that draws it. `ThinkingOrb.svelte` only ever imports `MODES`, never a mode
|
|
4
|
+
* file directly, so adding a state is a one-line change here plus a new
|
|
5
|
+
* export in whichever mode file it belongs to.
|
|
6
|
+
*/
|
|
7
|
+
import type { OrbState } from '../../properties';
|
|
8
|
+
import type { ModeFn } from '../types';
|
|
9
|
+
export declare const MODES: Record<OrbState, ModeFn>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The single place that maps a public `OrbState` name to the mode function
|
|
3
|
+
* that draws it. `ThinkingOrb.svelte` only ever imports `MODES`, never a mode
|
|
4
|
+
* file directly, so adding a state is a one-line change here plus a new
|
|
5
|
+
* export in whichever mode file it belongs to.
|
|
6
|
+
*/
|
|
7
|
+
import { listening, searching, solving, working } from './sphere';
|
|
8
|
+
import { connecting, weaving } from './network';
|
|
9
|
+
import { breathing, composing, shaping } from './form';
|
|
10
|
+
export const MODES = {
|
|
11
|
+
working,
|
|
12
|
+
searching,
|
|
13
|
+
solving,
|
|
14
|
+
listening,
|
|
15
|
+
connecting,
|
|
16
|
+
weaving,
|
|
17
|
+
composing,
|
|
18
|
+
breathing,
|
|
19
|
+
shaping
|
|
20
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two "network" modes: `connecting` draws a small graph of nodes on a
|
|
3
|
+
* sphere with pulses running its edges, and `weaving` draws three strands
|
|
4
|
+
* braided around a sphere. Both are built from the same handful of points
|
|
5
|
+
* placed on a sphere and rotated over time, so they share their sizing and
|
|
6
|
+
* projection setup below.
|
|
7
|
+
*/
|
|
8
|
+
import type { ModeFn } from '../types';
|
|
9
|
+
/** A few nodes on a sphere; lines to near neighbours carry small travelling pulses. */
|
|
10
|
+
export declare const connecting: ModeFn;
|
|
11
|
+
/** Three strands wrapped around a sphere, crossing as they spiral from top to bottom. */
|
|
12
|
+
export declare const weaving: ModeFn;
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two "network" modes: `connecting` draws a small graph of nodes on a
|
|
3
|
+
* sphere with pulses running its edges, and `weaving` draws three strands
|
|
4
|
+
* braided around a sphere. Both are built from the same handful of points
|
|
5
|
+
* placed on a sphere and rotated over time, so they share their sizing and
|
|
6
|
+
* projection setup below.
|
|
7
|
+
*/
|
|
8
|
+
import { hashUnit, modeSizing, project, rotateX, rotateY, sampleSphere } from '../space';
|
|
9
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
10
|
+
/** Both modes read as a sphere of this radius, centred in the canvas -- the spec's ~40% of `size`. */
|
|
11
|
+
const SPHERE_RADIUS_RATIO = 0.4;
|
|
12
|
+
/** The count/radius scaler, canvas centre and world sphere radius every mode below draws into. */
|
|
13
|
+
const sphereGeometry = (ctx) => ({
|
|
14
|
+
sizing: modeSizing(ctx.size, ctx.density, ctx.dotScale),
|
|
15
|
+
originPx: ctx.size / 2,
|
|
16
|
+
sphereRadius: ctx.size * SPHERE_RADIUS_RATIO
|
|
17
|
+
});
|
|
18
|
+
const squaredDistance = (a, b) => {
|
|
19
|
+
const dx = a.x - b.x;
|
|
20
|
+
const dy = a.y - b.y;
|
|
21
|
+
const dz = a.z - b.z;
|
|
22
|
+
return dx * dx + dy * dy + dz * dz;
|
|
23
|
+
};
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// connecting -- a few nodes on a sphere, linked to their nearest neighbours,
|
|
26
|
+
// with small bright pulses running along the links.
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
const NODE_BASE_COUNT = 16;
|
|
29
|
+
const MIN_NODE_COUNT = 6;
|
|
30
|
+
const NEIGHBOR_COUNT = 3;
|
|
31
|
+
const NODE_BASE_RADIUS = 3.4;
|
|
32
|
+
const PULSE_BASE_RADIUS = 2;
|
|
33
|
+
const EDGE_BASE_WIDTH = 1.1;
|
|
34
|
+
const NODE_ROTATE_SPEED = 0.35;
|
|
35
|
+
const NODE_TILT = 0.45;
|
|
36
|
+
const EDGE_FLICKER_SPEED = 0.5;
|
|
37
|
+
const EDGE_MIN_ALPHA = 0.25;
|
|
38
|
+
const PULSE_SPEED = 0.6;
|
|
39
|
+
/**
|
|
40
|
+
* Every unordered pair `(i, j)` where `j` is one of `i`'s `perNode` closest
|
|
41
|
+
* points (by straight-line distance), deduplicated so a mutual pair is only
|
|
42
|
+
* one edge. `points` are pre-rotation world positions: a rigid rotation
|
|
43
|
+
* never changes which points are nearest, so the graph itself can be built
|
|
44
|
+
* once per frame from them and stay stable as the sphere spins.
|
|
45
|
+
*/
|
|
46
|
+
const nearestNeighborEdges = (points, perNode) => {
|
|
47
|
+
const indices = points.map((_, index) => index);
|
|
48
|
+
const candidates = indices.flatMap((i) => indices
|
|
49
|
+
.filter((j) => j !== i)
|
|
50
|
+
.sort((a, b) => squaredDistance(points[i], points[a]) - squaredDistance(points[i], points[b]))
|
|
51
|
+
.slice(0, perNode)
|
|
52
|
+
.map((j) => (i < j ? [i, j] : [j, i])));
|
|
53
|
+
return candidates.filter(([a, b], index) => candidates.findIndex(([x, y]) => x === a && y === b) === index);
|
|
54
|
+
};
|
|
55
|
+
/** A few nodes on a sphere; lines to near neighbours carry small travelling pulses. */
|
|
56
|
+
export const connecting = (t, ctx) => {
|
|
57
|
+
const { sizing, originPx, sphereRadius } = sphereGeometry(ctx);
|
|
58
|
+
const nodeCount = Math.max(MIN_NODE_COUNT, sizing.count(NODE_BASE_COUNT));
|
|
59
|
+
const basePoints = Array.from({ length: nodeCount }, (_, index) => sampleSphere(index, nodeCount, sphereRadius));
|
|
60
|
+
const projected = basePoints
|
|
61
|
+
.map((point) => rotateX(point, NODE_TILT))
|
|
62
|
+
.map((point) => rotateY(point, t * NODE_ROTATE_SPEED))
|
|
63
|
+
.map((point) => project(point, originPx, sphereRadius));
|
|
64
|
+
const nodeDots = projected.map((p) => ({
|
|
65
|
+
x: p.x,
|
|
66
|
+
y: p.y,
|
|
67
|
+
depth: p.depth,
|
|
68
|
+
radius: sizing.radius(NODE_BASE_RADIUS),
|
|
69
|
+
ink: clamp01(1 - p.depth * 0.85),
|
|
70
|
+
alpha: 0.5 + p.depth * 0.45
|
|
71
|
+
}));
|
|
72
|
+
const edges = nearestNeighborEdges(basePoints, NEIGHBOR_COUNT);
|
|
73
|
+
const strokes = edges.map(([a, b], index) => {
|
|
74
|
+
const pa = projected[a];
|
|
75
|
+
const pb = projected[b];
|
|
76
|
+
const depth = (pa.depth + pb.depth) / 2;
|
|
77
|
+
const flicker = 0.5 +
|
|
78
|
+
0.5 *
|
|
79
|
+
Math.sin(t * EDGE_FLICKER_SPEED * Math.PI * 2 + hashUnit(index * 97 + 11) * Math.PI * 2);
|
|
80
|
+
return {
|
|
81
|
+
x1: pa.x,
|
|
82
|
+
y1: pa.y,
|
|
83
|
+
x2: pb.x,
|
|
84
|
+
y2: pb.y,
|
|
85
|
+
depth,
|
|
86
|
+
width: sizing.radius(EDGE_BASE_WIDTH),
|
|
87
|
+
ink: clamp01(1 - depth * 0.7),
|
|
88
|
+
alpha: EDGE_MIN_ALPHA + (1 - EDGE_MIN_ALPHA) * flicker
|
|
89
|
+
};
|
|
90
|
+
});
|
|
91
|
+
const pulses = edges.map(([a, b], index) => {
|
|
92
|
+
const pa = projected[a];
|
|
93
|
+
const pb = projected[b];
|
|
94
|
+
const travel = 0.5 + 0.5 * Math.sin(t * PULSE_SPEED * Math.PI * 2 + hashUnit(index * 53 + 7) * Math.PI * 2);
|
|
95
|
+
return {
|
|
96
|
+
x: pa.x + (pb.x - pa.x) * travel,
|
|
97
|
+
y: pa.y + (pb.y - pa.y) * travel,
|
|
98
|
+
depth: pa.depth + (pb.depth - pa.depth) * travel,
|
|
99
|
+
radius: sizing.radius(PULSE_BASE_RADIUS),
|
|
100
|
+
ink: 0.05,
|
|
101
|
+
alpha: 0.9
|
|
102
|
+
};
|
|
103
|
+
});
|
|
104
|
+
return { dots: [...nodeDots, ...pulses], strokes };
|
|
105
|
+
};
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
// weaving -- three strands spiralling from top to bottom around a sphere, offset
|
|
108
|
+
// in phase so they interleave; the depth sort in `paintFrame` is what makes
|
|
109
|
+
// them cross over and under each other.
|
|
110
|
+
// ---------------------------------------------------------------------------
|
|
111
|
+
const STRAND_COUNT = 3;
|
|
112
|
+
const STRAND_POINTS_BASE_COUNT = 26;
|
|
113
|
+
const MIN_STRAND_POINTS = 10;
|
|
114
|
+
/** Lower than a bare spiral would use: the braid swing below is what supplies
|
|
115
|
+
* the winding, so the longitude term only needs to carry the strands most of
|
|
116
|
+
* the way around, not add its own extra turns on top. */
|
|
117
|
+
const STRAND_WRAPS = 1.1;
|
|
118
|
+
const STRAND_ROTATE_SPEED = 0.3;
|
|
119
|
+
const STRAND_TILT = 0.5;
|
|
120
|
+
const STRAND_DOT_BASE_RADIUS = 1.6;
|
|
121
|
+
const STRAND_STROKE_BASE_WIDTH = 1.4;
|
|
122
|
+
/** How many times a strand swings past its neighbours along its own pole-to-
|
|
123
|
+
* pole length -- enough that a still frame always lands inside more than one
|
|
124
|
+
* crossing, not so many the braid blurs into texture. */
|
|
125
|
+
const STRAND_BRAID_CYCLES = 2.5;
|
|
126
|
+
/** A constant 120-degree offset alone keeps the three strands in the same
|
|
127
|
+
* relative order the whole way, which reads as one thick spiral rather than
|
|
128
|
+
* three distinct ones. Two strands only visibly swap places, i.e. actually
|
|
129
|
+
* cross, once the swing below overtakes half their 120-degree separation;
|
|
130
|
+
* this sits comfortably past that so every pair of strands crosses, not just
|
|
131
|
+
* wobbles toward each other. */
|
|
132
|
+
const STRAND_BRAID_SWING = 1.3;
|
|
133
|
+
const strandBaseAngle = (strand) => (strand / STRAND_COUNT) * Math.PI * 2;
|
|
134
|
+
/**
|
|
135
|
+
* The `pointIndex`-th of `pointsPerStrand` points of `strand`, spiralling
|
|
136
|
+
* from the top of the sphere to the bottom. Each strand's longitude oscillates around its own
|
|
137
|
+
* 120-degree slot (the braid swing) on top of the shared spiral term, so
|
|
138
|
+
* strands actually change places rather than staying a fixed distance apart;
|
|
139
|
+
* folding `t` into the shared longitude term spins the whole braid rigidly,
|
|
140
|
+
* which is what keeps every point exactly on the sphere at every instant.
|
|
141
|
+
*/
|
|
142
|
+
const strandPoint = (strand, pointIndex, pointsPerStrand, sphereRadius, t) => {
|
|
143
|
+
const u = pointIndex / (pointsPerStrand - 1);
|
|
144
|
+
const phi = u * Math.PI - Math.PI / 2;
|
|
145
|
+
const y = sphereRadius * Math.sin(phi);
|
|
146
|
+
const ringRadius = sphereRadius * Math.cos(phi);
|
|
147
|
+
const base = strandBaseAngle(strand);
|
|
148
|
+
const swing = STRAND_BRAID_SWING * Math.sin(u * STRAND_BRAID_CYCLES * Math.PI * 2 + base);
|
|
149
|
+
const theta = u * STRAND_WRAPS * Math.PI * 2 + base + swing + t * STRAND_ROTATE_SPEED;
|
|
150
|
+
const spun = { x: ringRadius * Math.cos(theta), y, z: ringRadius * Math.sin(theta) };
|
|
151
|
+
return rotateX(spun, STRAND_TILT);
|
|
152
|
+
};
|
|
153
|
+
/** Three strands wrapped around a sphere, crossing as they spiral from top to bottom. */
|
|
154
|
+
export const weaving = (t, ctx) => {
|
|
155
|
+
const { sizing, originPx, sphereRadius } = sphereGeometry(ctx);
|
|
156
|
+
const pointsPerStrand = Math.max(MIN_STRAND_POINTS, sizing.count(STRAND_POINTS_BASE_COUNT));
|
|
157
|
+
const strands = Array.from({ length: STRAND_COUNT }, (_, strand) => Array.from({ length: pointsPerStrand }, (_, pointIndex) => project(strandPoint(strand, pointIndex, pointsPerStrand, sphereRadius, t), originPx, sphereRadius)));
|
|
158
|
+
const dots = strands.flatMap((points, strand) => points.map((p) => ({
|
|
159
|
+
x: p.x,
|
|
160
|
+
y: p.y,
|
|
161
|
+
depth: p.depth,
|
|
162
|
+
radius: sizing.radius(STRAND_DOT_BASE_RADIUS),
|
|
163
|
+
ink: clamp01(1 - p.depth * 0.8 + strand * 0.08),
|
|
164
|
+
alpha: 0.55 + p.depth * 0.4
|
|
165
|
+
})));
|
|
166
|
+
const strokes = strands.flatMap((points, strand) => points.slice(1).map((p, i) => {
|
|
167
|
+
const previous = points[i];
|
|
168
|
+
const depth = (previous.depth + p.depth) / 2;
|
|
169
|
+
return {
|
|
170
|
+
x1: previous.x,
|
|
171
|
+
y1: previous.y,
|
|
172
|
+
x2: p.x,
|
|
173
|
+
y2: p.y,
|
|
174
|
+
depth,
|
|
175
|
+
width: sizing.radius(STRAND_STROKE_BASE_WIDTH),
|
|
176
|
+
ink: clamp01(1 - depth * 0.75 + strand * 0.06),
|
|
177
|
+
alpha: 0.6 + depth * 0.35
|
|
178
|
+
};
|
|
179
|
+
}));
|
|
180
|
+
return { dots, strokes };
|
|
181
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sphere-family modes: marks that read as points spread across, or orbiting,
|
|
3
|
+
* a solid sphere. Each mode is a pure function of `(t, ctx)` -- the geometry,
|
|
4
|
+
* tuning constants and small helpers below are local to this file so every
|
|
5
|
+
* mode's numbers stay easy to read next to the mode that uses them.
|
|
6
|
+
*/
|
|
7
|
+
import type { ModeFn } from '../types';
|
|
8
|
+
export declare const working: ModeFn;
|
|
9
|
+
export declare const searching: ModeFn;
|
|
10
|
+
export declare const solving: ModeFn;
|
|
11
|
+
export declare const listening: ModeFn;
|
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sphere-family modes: marks that read as points spread across, or orbiting,
|
|
3
|
+
* a solid sphere. Each mode is a pure function of `(t, ctx)` -- the geometry,
|
|
4
|
+
* tuning constants and small helpers below are local to this file so every
|
|
5
|
+
* mode's numbers stay easy to read next to the mode that uses them.
|
|
6
|
+
*/
|
|
7
|
+
import { easeInOutCubic, easeOutCubic, hashUnit, modeSizing, project, rotateX, rotateY, rotateZ, sampleSphere } from '../space';
|
|
8
|
+
const TWO_PI = Math.PI * 2;
|
|
9
|
+
/**
|
|
10
|
+
* `project`'s perspective makes a near point's on-screen radius bulge past
|
|
11
|
+
* its world radius, so every mode keeps its sphere a little inside the 40%
|
|
12
|
+
* mark to leave that bulge room before the canvas edge.
|
|
13
|
+
*/
|
|
14
|
+
const WORLD_RADIUS_RATIO = 0.38;
|
|
15
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
16
|
+
const clamp = (value, min, max) => Math.max(min, Math.min(max, value));
|
|
17
|
+
const originOf = (ctx) => ctx.size / 2;
|
|
18
|
+
const worldRadiusOf = (ctx) => ctx.size * WORLD_RADIUS_RATIO;
|
|
19
|
+
/** Near marks read brighter (low ink); far ones fade toward the background. */
|
|
20
|
+
const INK_NEAR = 0.05;
|
|
21
|
+
const INK_FAR = 0.72;
|
|
22
|
+
const shadeByDepth = (depth) => INK_FAR - (INK_FAR - INK_NEAR) * clamp01(depth);
|
|
23
|
+
const toDot = (at, radius, ink, alpha) => ({
|
|
24
|
+
x: at.x,
|
|
25
|
+
y: at.y,
|
|
26
|
+
depth: at.depth,
|
|
27
|
+
radius,
|
|
28
|
+
ink: clamp01(ink),
|
|
29
|
+
alpha: clamp01(alpha)
|
|
30
|
+
});
|
|
31
|
+
const toStroke = (from, to, width, ink, alpha) => ({
|
|
32
|
+
x1: from.x,
|
|
33
|
+
y1: from.y,
|
|
34
|
+
x2: to.x,
|
|
35
|
+
y2: to.y,
|
|
36
|
+
depth: (from.depth + to.depth) / 2,
|
|
37
|
+
width,
|
|
38
|
+
ink: clamp01(ink),
|
|
39
|
+
alpha: clamp01(alpha)
|
|
40
|
+
});
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
// working -- particles travelling several tilted orbits around a centre
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
/** Particles travel in short trailing arcs ("trains") rather than scattering
|
|
45
|
+
* independently around their orbit -- a lone random phase per particle reads
|
|
46
|
+
* as noise once density thins the count out at 32/20px, while a handful of
|
|
47
|
+
* particles held close together always reads as motion along a path. */
|
|
48
|
+
const WORKING_TRAIN_BASE_COUNT = 22;
|
|
49
|
+
/** Keeps at least this many short arcs on screen at each size. At 32/64px the
|
|
50
|
+
* area scaler alone already clears this floor, so it only bites at 20px. More
|
|
51
|
+
* than one train per orbit (WORKING_ORBIT_COUNT = 6) matters because depth
|
|
52
|
+
* shading fades whichever half of each orbit is on the far side of the
|
|
53
|
+
* sphere at a given instant almost to the background -- one train per orbit
|
|
54
|
+
* left whole quadrants with no train currently on the near side to fill
|
|
55
|
+
* them; two independently-phased laps around the six orbits keeps at least
|
|
56
|
+
* a couple of quadrants covered at every instant, not just on average. */
|
|
57
|
+
const WORKING_MIN_TRAINS = { 64: 3, 32: 3, 20: 12 };
|
|
58
|
+
const WORKING_TRAIN_LENGTH = 5;
|
|
59
|
+
/** How far apart, in radians, a train's particles trail behind its lead --
|
|
60
|
+
* wide enough to read as a short curved arc, short enough to stay a cluster
|
|
61
|
+
* rather than smearing back into the full-orbit scatter this replaces. */
|
|
62
|
+
const WORKING_TRAIN_ARC_SPAN = 0.5;
|
|
63
|
+
const WORKING_BASE_RADIUS_PX = 1.6;
|
|
64
|
+
const WORKING_ORBIT_COUNT = 6;
|
|
65
|
+
const WORKING_ANGULAR_SPEED = 1.1;
|
|
66
|
+
/** How far an orbit's `weight` can push its dots' ink away from the shared
|
|
67
|
+
* depth shading, so each of the six paths reads as its own band rather than
|
|
68
|
+
* all of them blurring into one undifferentiated cloud. */
|
|
69
|
+
const WORKING_ORBIT_INK_SPREAD = 0.3;
|
|
70
|
+
/** However low an orbit's `weight` sits, its dots never fade past this floor. */
|
|
71
|
+
const WORKING_ORBIT_ALPHA_FLOOR = 0.65;
|
|
72
|
+
/** A train's trailing-most particle never fades past this floor, so the arc's
|
|
73
|
+
* tail stays legible instead of thinning away to nothing. */
|
|
74
|
+
const WORKING_TRAIL_ALPHA_FLOOR = 0.35;
|
|
75
|
+
/** Every particle's own orbital plane, radius, pace and shading weight, keyed by its orbit. */
|
|
76
|
+
const workingOrbit = (orbit) => {
|
|
77
|
+
const seed = orbit * 31 + 7;
|
|
78
|
+
return {
|
|
79
|
+
tilt: (hashUnit(seed) - 0.5) * Math.PI,
|
|
80
|
+
roll: hashUnit(seed + 3) * TWO_PI,
|
|
81
|
+
orbitRadiusFactor: 0.5 + 0.45 * hashUnit(seed + 5),
|
|
82
|
+
speedFactor: 0.6 + 1.3 * hashUnit(seed + 9),
|
|
83
|
+
weight: 0.35 + 0.65 * hashUnit(seed + 15)
|
|
84
|
+
};
|
|
85
|
+
};
|
|
86
|
+
export const working = (t, ctx) => {
|
|
87
|
+
const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
|
|
88
|
+
const origin = originOf(ctx);
|
|
89
|
+
const worldRadius = worldRadiusOf(ctx);
|
|
90
|
+
const trainCount = Math.max(WORKING_MIN_TRAINS[ctx.size], sizing.count(WORKING_TRAIN_BASE_COUNT));
|
|
91
|
+
const arcStep = WORKING_TRAIN_ARC_SPAN / (WORKING_TRAIN_LENGTH - 1);
|
|
92
|
+
const dots = [];
|
|
93
|
+
for (let train = 0; train < trainCount; train += 1) {
|
|
94
|
+
const orbit = workingOrbit(train % WORKING_ORBIT_COUNT);
|
|
95
|
+
const leadPhase = hashUnit(train * 5 + 1) * TWO_PI;
|
|
96
|
+
const leadAngle = leadPhase + t * WORKING_ANGULAR_SPEED * orbit.speedFactor;
|
|
97
|
+
const orbitRadius = worldRadius * orbit.orbitRadiusFactor;
|
|
98
|
+
for (let slot = 0; slot < WORKING_TRAIN_LENGTH; slot += 1) {
|
|
99
|
+
const angle = leadAngle - slot * arcStep;
|
|
100
|
+
const planar = {
|
|
101
|
+
x: Math.cos(angle) * orbitRadius,
|
|
102
|
+
y: 0,
|
|
103
|
+
z: Math.sin(angle) * orbitRadius
|
|
104
|
+
};
|
|
105
|
+
const tilted = rotateX(planar, orbit.tilt);
|
|
106
|
+
const point = rotateZ(tilted, orbit.roll);
|
|
107
|
+
const projected = project(point, origin, worldRadius);
|
|
108
|
+
const seed = train * 13 + slot * 7 + 2;
|
|
109
|
+
const trail = 1 - slot / (WORKING_TRAIN_LENGTH - 1);
|
|
110
|
+
const ink = shadeByDepth(projected.depth) +
|
|
111
|
+
(hashUnit(seed) - 0.5) * 0.08 -
|
|
112
|
+
(orbit.weight - 0.5) * WORKING_ORBIT_INK_SPREAD;
|
|
113
|
+
const alpha = (WORKING_TRAIL_ALPHA_FLOOR + (1 - WORKING_TRAIL_ALPHA_FLOOR) * trail) *
|
|
114
|
+
(WORKING_ORBIT_ALPHA_FLOOR + (1 - WORKING_ORBIT_ALPHA_FLOOR) * orbit.weight);
|
|
115
|
+
const radius = sizing.radius(WORKING_BASE_RADIUS_PX) * (0.75 + 0.5 * hashUnit(seed + 3));
|
|
116
|
+
dots.push(toDot(projected, radius, ink, alpha));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return { dots, strokes: [] };
|
|
120
|
+
};
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// searching -- a dotted globe with a meridian band sweeping around it
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
const SEARCHING_BASE_COUNT = 150;
|
|
125
|
+
const SEARCHING_BASE_RADIUS_PX = 1.5;
|
|
126
|
+
const SEARCHING_BAND_HALF_WIDTH = 0.42;
|
|
127
|
+
const SEARCHING_SWEEP_SPEED = 1.15;
|
|
128
|
+
const SEARCHING_SPIN_SPEED = 0.18;
|
|
129
|
+
const SEARCHING_TILT = -0.55;
|
|
130
|
+
const SEARCHING_ARC_SEGMENTS = 10;
|
|
131
|
+
/** How much of the pole-to-pole range the band's guide arc spans, short of
|
|
132
|
+
* the poles themselves so its end segments stay distinct marks rather than
|
|
133
|
+
* converging to a single point. */
|
|
134
|
+
const SEARCHING_ARC_LATITUDE_SPAN = 0.92;
|
|
135
|
+
/** How much darker, more opaque and larger a dot gets the closer the sweep
|
|
136
|
+
* passes over it, so the band reads as a clear lit stripe against the rest
|
|
137
|
+
* of the dotted globe instead of a faint gradient. */
|
|
138
|
+
const SEARCHING_BAND_INK_BOOST = 0.75;
|
|
139
|
+
const SEARCHING_BAND_ALPHA_BOOST = 0.35;
|
|
140
|
+
const SEARCHING_BAND_RADIUS_BOOST = 1.3;
|
|
141
|
+
/** The guide arc itself: wide, dark and opaque so the sweep's leading edge
|
|
142
|
+
* is legible on its own, not just via the dots it lifts. */
|
|
143
|
+
const SEARCHING_ARC_STROKE_RADIUS = 2.2;
|
|
144
|
+
const SEARCHING_ARC_INK = 0;
|
|
145
|
+
const SEARCHING_ARC_ALPHA = 0.7;
|
|
146
|
+
/** Shortest signed-free distance between two angles, always in 0..π. */
|
|
147
|
+
const angleDistance = (a, b) => {
|
|
148
|
+
const diff = Math.abs(a - b) % TWO_PI;
|
|
149
|
+
return diff > Math.PI ? TWO_PI - diff : diff;
|
|
150
|
+
};
|
|
151
|
+
export const searching = (t, ctx) => {
|
|
152
|
+
const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
|
|
153
|
+
const origin = originOf(ctx);
|
|
154
|
+
const worldRadius = worldRadiusOf(ctx);
|
|
155
|
+
const count = sizing.count(SEARCHING_BASE_COUNT);
|
|
156
|
+
const sweepAngle = t * SEARCHING_SWEEP_SPEED;
|
|
157
|
+
const dots = Array.from({ length: count }, (_unused, i) => {
|
|
158
|
+
const surface = sampleSphere(i, count, worldRadius);
|
|
159
|
+
const tilted = rotateX(surface, SEARCHING_TILT);
|
|
160
|
+
const point = rotateY(tilted, t * SEARCHING_SPIN_SPEED);
|
|
161
|
+
const longitude = Math.atan2(point.z, point.x);
|
|
162
|
+
const distance = angleDistance(longitude, sweepAngle);
|
|
163
|
+
const lift = easeOutCubic(clamp01(1 - distance / SEARCHING_BAND_HALF_WIDTH));
|
|
164
|
+
const projected = project(point, origin, worldRadius);
|
|
165
|
+
const ink = shadeByDepth(projected.depth) - lift * SEARCHING_BAND_INK_BOOST;
|
|
166
|
+
const alpha = 0.55 + projected.depth * 0.25 + lift * SEARCHING_BAND_ALPHA_BOOST;
|
|
167
|
+
const radius = sizing.radius(SEARCHING_BASE_RADIUS_PX) * (1 + lift * SEARCHING_BAND_RADIUS_BOOST);
|
|
168
|
+
return toDot(projected, radius, ink, alpha);
|
|
169
|
+
});
|
|
170
|
+
const arcPoints = Array.from({ length: SEARCHING_ARC_SEGMENTS + 1 }, (_unused, j) => {
|
|
171
|
+
const latFrac = j / SEARCHING_ARC_SEGMENTS;
|
|
172
|
+
const phi = (latFrac - 0.5) * Math.PI * SEARCHING_ARC_LATITUDE_SPAN;
|
|
173
|
+
const point = {
|
|
174
|
+
x: Math.cos(phi) * Math.cos(sweepAngle) * worldRadius,
|
|
175
|
+
y: Math.sin(phi) * worldRadius,
|
|
176
|
+
z: Math.cos(phi) * Math.sin(sweepAngle) * worldRadius
|
|
177
|
+
};
|
|
178
|
+
return project(point, origin, worldRadius);
|
|
179
|
+
});
|
|
180
|
+
const strokeWidth = sizing.radius(SEARCHING_ARC_STROKE_RADIUS);
|
|
181
|
+
const strokes = Array.from({ length: SEARCHING_ARC_SEGMENTS }, (_unused, j) => toStroke(arcPoints[j], arcPoints[j + 1], strokeWidth, SEARCHING_ARC_INK, SEARCHING_ARC_ALPHA));
|
|
182
|
+
return { dots, strokes };
|
|
183
|
+
};
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
// solving -- tiers that snap a quarter turn out of alignment, hold scrambled,
|
|
186
|
+
// then resolve back, on a loop. Landmark dots and fixed seam rings make the
|
|
187
|
+
// misalignment legible: an otherwise-uniform, evenly-sampled dot texture
|
|
188
|
+
// looks identical under any rotation about the spin axis, so turning a
|
|
189
|
+
// "band" of it invisibly wouldn't read as scrambling at all.
|
|
190
|
+
// ---------------------------------------------------------------------------
|
|
191
|
+
const SOLVING_BASE_COUNT = 130;
|
|
192
|
+
const SOLVING_BASE_RADIUS_PX = 1.5;
|
|
193
|
+
const SOLVING_TIER_COUNT = 4;
|
|
194
|
+
const SOLVING_TURN_DURATION = 0.16;
|
|
195
|
+
const SOLVING_TURN_GAP = 0.3;
|
|
196
|
+
const SOLVING_HOLD_GAP = 0.55;
|
|
197
|
+
const SOLVING_TILT = -0.5;
|
|
198
|
+
/** Each tier carries this many oversized landmark dots at fixed, evenly
|
|
199
|
+
* spaced longitudes, so a quarter turn visibly displaces a recognisable
|
|
200
|
+
* cluster relative to its neighbours instead of only reshuffling texture
|
|
201
|
+
* that looks the same before and after. */
|
|
202
|
+
const SOLVING_LANDMARKS_PER_TIER = 3;
|
|
203
|
+
const SOLVING_LANDMARK_RADIUS_PX = 3.1;
|
|
204
|
+
const SOLVING_LANDMARK_INK_BOOST = 0.45;
|
|
205
|
+
const SOLVING_LANDMARK_ALPHA = 0.9;
|
|
206
|
+
/** A static ring of short strokes at each cut between tiers, unaffected by
|
|
207
|
+
* any turn, so the "cut into bands" structure reads even while the puzzle
|
|
208
|
+
* sits solved and nothing is moving. */
|
|
209
|
+
const SOLVING_SEAM_SEGMENTS = 12;
|
|
210
|
+
const SOLVING_SEAM_STROKE_RADIUS = 1.05;
|
|
211
|
+
const SOLVING_SEAM_INK = 0.15;
|
|
212
|
+
const SOLVING_SEAM_ALPHA = 0.5;
|
|
213
|
+
const solvingTurnStart = (tier) => tier * SOLVING_TURN_GAP;
|
|
214
|
+
const solvingTurnEnd = (tier) => solvingTurnStart(tier) + SOLVING_TURN_DURATION;
|
|
215
|
+
const SOLVING_HOLD_START = solvingTurnEnd(SOLVING_TIER_COUNT - 1) + SOLVING_HOLD_GAP;
|
|
216
|
+
const solvingReturnStart = (tier) => SOLVING_HOLD_START + (SOLVING_TIER_COUNT - 1 - tier) * SOLVING_TURN_GAP;
|
|
217
|
+
const solvingReturnEnd = (tier) => solvingReturnStart(tier) + SOLVING_TURN_DURATION;
|
|
218
|
+
/** Full snap-scramble-then-resolve length; the mode repeats every this many seconds. */
|
|
219
|
+
const SOLVING_CYCLE = solvingReturnEnd(0) + SOLVING_HOLD_GAP;
|
|
220
|
+
/** 0 (solved) snaps out to 1 (quarter-turned) and back. The snap out eases
|
|
221
|
+
* only at its landing (a fast start reads as a sudden turn); the return to
|
|
222
|
+
* solved eases at both ends, reading as a calmer resolve. */
|
|
223
|
+
const solvingTierTurn = (tier, cyclePos) => {
|
|
224
|
+
const turnStart = solvingTurnStart(tier);
|
|
225
|
+
const turnEnd = solvingTurnEnd(tier);
|
|
226
|
+
const returnStart = solvingReturnStart(tier);
|
|
227
|
+
const returnEnd = solvingReturnEnd(tier);
|
|
228
|
+
if (cyclePos <= turnStart) {
|
|
229
|
+
return 0;
|
|
230
|
+
}
|
|
231
|
+
if (cyclePos < turnEnd) {
|
|
232
|
+
return easeOutCubic((cyclePos - turnStart) / SOLVING_TURN_DURATION);
|
|
233
|
+
}
|
|
234
|
+
if (cyclePos < returnStart) {
|
|
235
|
+
return 1;
|
|
236
|
+
}
|
|
237
|
+
if (cyclePos < returnEnd) {
|
|
238
|
+
return 1 - easeInOutCubic((cyclePos - returnStart) / SOLVING_TURN_DURATION);
|
|
239
|
+
}
|
|
240
|
+
return 0;
|
|
241
|
+
};
|
|
242
|
+
const solvingTierOf = (yUnit) => clamp(Math.floor(((1 - yUnit) / 2) * SOLVING_TIER_COUNT), 0, SOLVING_TIER_COUNT - 1);
|
|
243
|
+
const solvingTierDirection = (tier) => (hashUnit(tier * 53 + 21) < 0.5 ? -1 : 1);
|
|
244
|
+
/** The y-unit (-1..1) at the centre of a tier's latitude band. */
|
|
245
|
+
const solvingTierCenterYUnit = (tier) => 1 - (2 * tier + 1) / SOLVING_TIER_COUNT;
|
|
246
|
+
/** The y-unit (-1..1) of the fixed cut between one tier and the next. */
|
|
247
|
+
const solvingSeamYUnit = (seam) => 1 - (2 * (seam + 1)) / SOLVING_TIER_COUNT;
|
|
248
|
+
const solvingTurnAngle = (tier, cyclePos) => solvingTierTurn(tier, cyclePos) * solvingTierDirection(tier) * (Math.PI / 2);
|
|
249
|
+
export const solving = (t, ctx) => {
|
|
250
|
+
const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
|
|
251
|
+
const origin = originOf(ctx);
|
|
252
|
+
const worldRadius = worldRadiusOf(ctx);
|
|
253
|
+
const count = sizing.count(SOLVING_BASE_COUNT);
|
|
254
|
+
const cyclePos = t - SOLVING_CYCLE * Math.floor(t / SOLVING_CYCLE);
|
|
255
|
+
const texture = Array.from({ length: count }, (_unused, i) => {
|
|
256
|
+
const surface = sampleSphere(i, count, worldRadius);
|
|
257
|
+
const yUnit = worldRadius > 0 ? surface.y / worldRadius : 0;
|
|
258
|
+
const tier = solvingTierOf(yUnit);
|
|
259
|
+
const turned = rotateY(surface, solvingTurnAngle(tier, cyclePos));
|
|
260
|
+
const point = rotateX(turned, SOLVING_TILT);
|
|
261
|
+
const projected = project(point, origin, worldRadius);
|
|
262
|
+
const ink = shadeByDepth(projected.depth);
|
|
263
|
+
const alpha = 0.55 + 0.3 * hashUnit(i * 3 + 9);
|
|
264
|
+
const radius = sizing.radius(SOLVING_BASE_RADIUS_PX);
|
|
265
|
+
return toDot(projected, radius, ink, alpha);
|
|
266
|
+
});
|
|
267
|
+
const landmarks = Array.from({ length: SOLVING_TIER_COUNT * SOLVING_LANDMARKS_PER_TIER }, (_unused, i) => {
|
|
268
|
+
const tier = Math.floor(i / SOLVING_LANDMARKS_PER_TIER);
|
|
269
|
+
const slot = i % SOLVING_LANDMARKS_PER_TIER;
|
|
270
|
+
const yUnit = solvingTierCenterYUnit(tier);
|
|
271
|
+
const y = yUnit * worldRadius;
|
|
272
|
+
const ringRadius = Math.sqrt(Math.max(0, 1 - yUnit * yUnit)) * worldRadius;
|
|
273
|
+
const phase = hashUnit(tier * 29 + 5) * TWO_PI;
|
|
274
|
+
const longitude = phase + (slot / SOLVING_LANDMARKS_PER_TIER) * TWO_PI;
|
|
275
|
+
const base = {
|
|
276
|
+
x: Math.cos(longitude) * ringRadius,
|
|
277
|
+
y,
|
|
278
|
+
z: Math.sin(longitude) * ringRadius
|
|
279
|
+
};
|
|
280
|
+
const turned = rotateY(base, solvingTurnAngle(tier, cyclePos));
|
|
281
|
+
const point = rotateX(turned, SOLVING_TILT);
|
|
282
|
+
const projected = project(point, origin, worldRadius);
|
|
283
|
+
const ink = shadeByDepth(projected.depth) - SOLVING_LANDMARK_INK_BOOST;
|
|
284
|
+
return toDot(projected, sizing.radius(SOLVING_LANDMARK_RADIUS_PX), ink, SOLVING_LANDMARK_ALPHA);
|
|
285
|
+
});
|
|
286
|
+
const seamWidth = sizing.radius(SOLVING_SEAM_STROKE_RADIUS);
|
|
287
|
+
const seams = Array.from({ length: SOLVING_TIER_COUNT - 1 }, (_unused, seam) => {
|
|
288
|
+
const yUnit = solvingSeamYUnit(seam);
|
|
289
|
+
const y = yUnit * worldRadius;
|
|
290
|
+
const ringRadius = Math.sqrt(Math.max(0, 1 - yUnit * yUnit)) * worldRadius;
|
|
291
|
+
const ringPoints = Array.from({ length: SOLVING_SEAM_SEGMENTS + 1 }, (_unusedPoint, j) => {
|
|
292
|
+
const angle = (j / SOLVING_SEAM_SEGMENTS) * TWO_PI;
|
|
293
|
+
const point = {
|
|
294
|
+
x: Math.cos(angle) * ringRadius,
|
|
295
|
+
y,
|
|
296
|
+
z: Math.sin(angle) * ringRadius
|
|
297
|
+
};
|
|
298
|
+
return project(rotateX(point, SOLVING_TILT), origin, worldRadius);
|
|
299
|
+
});
|
|
300
|
+
return Array.from({ length: SOLVING_SEAM_SEGMENTS }, (_unusedSegment, j) => toStroke(ringPoints[j], ringPoints[j + 1], seamWidth, SOLVING_SEAM_INK, SOLVING_SEAM_ALPHA));
|
|
301
|
+
}).flat();
|
|
302
|
+
return { dots: [...texture, ...landmarks], strokes: seams };
|
|
303
|
+
};
|
|
304
|
+
// ---------------------------------------------------------------------------
|
|
305
|
+
// listening -- stacked horizontal rings with a wave rolling through them
|
|
306
|
+
// ---------------------------------------------------------------------------
|
|
307
|
+
/** Fewer, fatter rings at the smaller presets read as distinct stacked bands;
|
|
308
|
+
* six thin rings at 20px used to compress into a single smudge. */
|
|
309
|
+
const LISTENING_RING_COUNT = { 64: 5, 32: 4, 20: 3 };
|
|
310
|
+
const LISTENING_DOTS_PER_RING_BASE = 26;
|
|
311
|
+
/** A ring below this many dots reads as a loose cluster of points rather than
|
|
312
|
+
* a line, especially once it is a small ellipse at 20px. */
|
|
313
|
+
const LISTENING_MIN_DOTS_PER_RING = 10;
|
|
314
|
+
const LISTENING_BASE_RADIUS_PX = 1.5;
|
|
315
|
+
const LISTENING_WAVE_SPEED = 0.6;
|
|
316
|
+
const LISTENING_SWELL = 0.55;
|
|
317
|
+
const LISTENING_LIFT_INK = 0.45;
|
|
318
|
+
const LISTENING_SPIN_SPEED = 0.12;
|
|
319
|
+
/** A ring's own vertical thickness once tilted is (its radius) x sin(tilt); a
|
|
320
|
+
* ring near the equator is nearly as wide as the sphere itself, so even this
|
|
321
|
+
* shallow a tilt must stay well under the gap between ring centres below or
|
|
322
|
+
* neighbouring rings' ellipses swallow the empty space between them. Kept
|
|
323
|
+
* shallow rather than flattened to zero so a ring still reads as an ellipse
|
|
324
|
+
* seen slightly from above, not a flat line. */
|
|
325
|
+
const LISTENING_TILT = -0.13;
|
|
326
|
+
/** Rings are stacked between +/- this fraction of the world radius, short of
|
|
327
|
+
* the poles so the top and bottom rings stay full circles, not points. */
|
|
328
|
+
const LISTENING_Y_SPAN = 0.82;
|
|
329
|
+
const listeningRing = (ring, ringCount, worldRadius) => {
|
|
330
|
+
const yUnit = (-1 + (2 * ring + 1) / ringCount) * LISTENING_Y_SPAN;
|
|
331
|
+
const y = yUnit * worldRadius;
|
|
332
|
+
const ringRadius = Math.sqrt(Math.max(0, worldRadius * worldRadius - y * y));
|
|
333
|
+
const phase = hashUnit(ring * 17 + 3) * TWO_PI;
|
|
334
|
+
const wave = (t) => (Math.sin(TWO_PI * (ring / ringCount - t * LISTENING_WAVE_SPEED)) + 1) / 2;
|
|
335
|
+
return { y, ringRadius, phase, wave };
|
|
336
|
+
};
|
|
337
|
+
export const listening = (t, ctx) => {
|
|
338
|
+
const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
|
|
339
|
+
const origin = originOf(ctx);
|
|
340
|
+
const worldRadius = worldRadiusOf(ctx);
|
|
341
|
+
const ringCount = LISTENING_RING_COUNT[ctx.size];
|
|
342
|
+
const totalBase = ringCount * LISTENING_DOTS_PER_RING_BASE;
|
|
343
|
+
const dotsPerRing = Math.max(LISTENING_MIN_DOTS_PER_RING, Math.round(sizing.count(totalBase) / ringCount));
|
|
344
|
+
const dots = Array.from({ length: ringCount }, (_unused, r) => {
|
|
345
|
+
const ring = listeningRing(r, ringCount, worldRadius);
|
|
346
|
+
const wave = ring.wave(t);
|
|
347
|
+
return Array.from({ length: dotsPerRing }, (_unusedDot, k) => {
|
|
348
|
+
const angle = (k / dotsPerRing) * TWO_PI + ring.phase;
|
|
349
|
+
const planar = {
|
|
350
|
+
x: Math.cos(angle) * ring.ringRadius,
|
|
351
|
+
y: ring.y,
|
|
352
|
+
z: Math.sin(angle) * ring.ringRadius
|
|
353
|
+
};
|
|
354
|
+
const tilted = rotateX(planar, LISTENING_TILT);
|
|
355
|
+
const point = rotateY(tilted, t * LISTENING_SPIN_SPEED);
|
|
356
|
+
const projected = project(point, origin, worldRadius);
|
|
357
|
+
const ink = shadeByDepth(projected.depth) - wave * LISTENING_LIFT_INK;
|
|
358
|
+
const alpha = 0.6 + wave * 0.3;
|
|
359
|
+
const radius = sizing.radius(LISTENING_BASE_RADIUS_PX) * (1 + wave * LISTENING_SWELL);
|
|
360
|
+
return toDot(projected, radius, ink, alpha);
|
|
361
|
+
});
|
|
362
|
+
}).flat();
|
|
363
|
+
return { dots, strokes: [] };
|
|
364
|
+
};
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turns one `Frame` into canvas calls. The only place that knows about theme
|
|
3
|
+
* and colour -- a mode never sees `dark` or `tint`, only the 0..1 `ink` it
|
|
4
|
+
* assigns each mark.
|
|
5
|
+
*/
|
|
6
|
+
import type { Rgb } from '../../types';
|
|
7
|
+
import type { Frame } from './types';
|
|
8
|
+
/** What `paintFrame` needs beyond the frame itself: the theme and the base colour. */
|
|
9
|
+
export type PaintOptions = {
|
|
10
|
+
dark: boolean;
|
|
11
|
+
tint: Rgb;
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Maps `ink` (0 = full `tint`) to an exact, testable colour string: fading
|
|
15
|
+
* toward white in light theme and toward black in dark theme, so a mark
|
|
16
|
+
* further away can fade into either background without ever reading as the
|
|
17
|
+
* wrong theme's ink.
|
|
18
|
+
*/
|
|
19
|
+
export declare const inkColor: (tint: Rgb, ink: number, dark: boolean) => string;
|
|
20
|
+
/**
|
|
21
|
+
* Clears the canvas and draws a `Frame`: every stroke, then every dot, each
|
|
22
|
+
* group sorted far-to-near by `depth` so nearer marks are painted last and
|
|
23
|
+
* sit on top.
|
|
24
|
+
*/
|
|
25
|
+
export declare const paintFrame: (ctx: CanvasRenderingContext2D, frame: Frame, options: PaintOptions) => void;
|