@juspay/svelte-ui-components 4.32.0 → 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/ChatComposer/ChatComposer.svelte +4 -0
- package/dist/Snippet/Snippet.svelte +7 -1
- package/dist/Table/Table.svelte +1 -0
- 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 +1053 -436
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -0,0 +1,47 @@
|
|
|
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
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
7
|
+
/**
|
|
8
|
+
* Maps `ink` (0 = full `tint`) to an exact, testable colour string: fading
|
|
9
|
+
* toward white in light theme and toward black in dark theme, so a mark
|
|
10
|
+
* further away can fade into either background without ever reading as the
|
|
11
|
+
* wrong theme's ink.
|
|
12
|
+
*/
|
|
13
|
+
export const inkColor = (tint, ink, dark) => {
|
|
14
|
+
const amount = clamp01(ink);
|
|
15
|
+
const fadeTo = dark ? 0 : 255;
|
|
16
|
+
const r = Math.round(tint.r + (fadeTo - tint.r) * amount);
|
|
17
|
+
const g = Math.round(tint.g + (fadeTo - tint.g) * amount);
|
|
18
|
+
const b = Math.round(tint.b + (fadeTo - tint.b) * amount);
|
|
19
|
+
return `rgb(${r} ${g} ${b})`;
|
|
20
|
+
};
|
|
21
|
+
/**
|
|
22
|
+
* Clears the canvas and draws a `Frame`: every stroke, then every dot, each
|
|
23
|
+
* group sorted far-to-near by `depth` so nearer marks are painted last and
|
|
24
|
+
* sit on top.
|
|
25
|
+
*/
|
|
26
|
+
export const paintFrame = (ctx, frame, options) => {
|
|
27
|
+
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
28
|
+
const strokes = [...frame.strokes].sort((a, b) => a.depth - b.depth);
|
|
29
|
+
for (const stroke of strokes) {
|
|
30
|
+
ctx.strokeStyle = inkColor(options.tint, stroke.ink, options.dark);
|
|
31
|
+
ctx.globalAlpha = clamp01(stroke.alpha);
|
|
32
|
+
ctx.lineWidth = stroke.width;
|
|
33
|
+
ctx.beginPath();
|
|
34
|
+
ctx.moveTo(stroke.x1, stroke.y1);
|
|
35
|
+
ctx.lineTo(stroke.x2, stroke.y2);
|
|
36
|
+
ctx.stroke();
|
|
37
|
+
}
|
|
38
|
+
const dots = [...frame.dots].sort((a, b) => a.depth - b.depth);
|
|
39
|
+
for (const dot of dots) {
|
|
40
|
+
ctx.fillStyle = inkColor(options.tint, dot.ink, options.dark);
|
|
41
|
+
ctx.globalAlpha = clamp01(dot.alpha);
|
|
42
|
+
ctx.beginPath();
|
|
43
|
+
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
|
|
44
|
+
ctx.fill();
|
|
45
|
+
}
|
|
46
|
+
ctx.globalAlpha = 1;
|
|
47
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure 3D helpers shared by every mode: rotation, perspective projection,
|
|
3
|
+
* even sphere sampling, easing and a seeded per-index hash. Nothing here
|
|
4
|
+
* touches a canvas or keeps state, so a mode stays a plain function of
|
|
5
|
+
* `(t, ctx)` and every helper below is trivial to unit test in isolation.
|
|
6
|
+
*/
|
|
7
|
+
import type { ModeSize } from './types';
|
|
8
|
+
/** A point in the mode's own 3D world space, before projection to the canvas. */
|
|
9
|
+
export type Point3 = {
|
|
10
|
+
x: number;
|
|
11
|
+
y: number;
|
|
12
|
+
z: number;
|
|
13
|
+
};
|
|
14
|
+
/** A `Point3` projected onto the canvas: screen px plus normalised depth and raw scale. */
|
|
15
|
+
export type Projected = {
|
|
16
|
+
x: number;
|
|
17
|
+
y: number;
|
|
18
|
+
depth: number;
|
|
19
|
+
scale: number;
|
|
20
|
+
};
|
|
21
|
+
/** The two scalers `modeSizing` hands back: multiply a mode's own base counts and radii by them. */
|
|
22
|
+
export type ModeSizing = {
|
|
23
|
+
count: (base: number) => number;
|
|
24
|
+
radius: (base: number) => number;
|
|
25
|
+
};
|
|
26
|
+
/** Rotates a point about the X axis (mixes y and z). */
|
|
27
|
+
export declare const rotateX: (point: Point3, angle: number) => Point3;
|
|
28
|
+
/** Rotates a point about the Y axis (mixes x and z). */
|
|
29
|
+
export declare const rotateY: (point: Point3, angle: number) => Point3;
|
|
30
|
+
/** Rotates a point about the Z axis (mixes x and y). */
|
|
31
|
+
export declare const rotateZ: (point: Point3, angle: number) => Point3;
|
|
32
|
+
/**
|
|
33
|
+
* Projects a world point with perspective onto a canvas centred at
|
|
34
|
+
* `(originPx, originPx)`. `worldRadius` is the extent the point's `z` is
|
|
35
|
+
* expected to range over (typically the sphere/shape radius the mode drew it
|
|
36
|
+
* at), and is what turns `z` into a 0..1 `depth`: nearer (more negative `z`)
|
|
37
|
+
* is 1, farther is 0.
|
|
38
|
+
*/
|
|
39
|
+
export declare const project: (point: Point3, originPx: number, worldRadius: number) => Projected;
|
|
40
|
+
/**
|
|
41
|
+
* The `index`-th of `count` points spread evenly over a sphere of `radius`,
|
|
42
|
+
* via a golden-angle spiral. Deterministic in `index` and `count` alone, and
|
|
43
|
+
* even across the whole surface rather than crowding the poles the way a
|
|
44
|
+
* uniformly-sampled latitude/longitude grid would.
|
|
45
|
+
*/
|
|
46
|
+
export declare const sampleSphere: (index: number, count: number, radius: number) => Point3;
|
|
47
|
+
/** Cubic ease, slow-fast-slow, for a motion that starts and ends at rest. */
|
|
48
|
+
export declare const easeInOutCubic: (t: number) => number;
|
|
49
|
+
/** Cubic ease-out, fast-then-slow, for a motion that only needs to settle at the end. */
|
|
50
|
+
export declare const easeOutCubic: (t: number) => number;
|
|
51
|
+
/**
|
|
52
|
+
* A deterministic 0..1 value for an integer seed (a dot's index is the usual
|
|
53
|
+
* one). Gives every mode per-dot variety -- a phase offset, a size jitter --
|
|
54
|
+
* without `Math.random`, so the same `(t, ctx)` always paints the same frame.
|
|
55
|
+
*/
|
|
56
|
+
export declare const hashUnit: (seed: number) => number;
|
|
57
|
+
/**
|
|
58
|
+
* Turns `(size, density, dotScale)` into a count scaler and a radius scaler,
|
|
59
|
+
* so every mode derives its dot counts and radii the same way instead of
|
|
60
|
+
* keeping its own per-size table. Counts scale with area (`size` squared);
|
|
61
|
+
* radii grow sub-linearly, with a floor that keeps dots visible at 20px.
|
|
62
|
+
*/
|
|
63
|
+
export declare const modeSizing: (size: ModeSize, density: number, dotScale: number) => ModeSizing;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure 3D helpers shared by every mode: rotation, perspective projection,
|
|
3
|
+
* even sphere sampling, easing and a seeded per-index hash. Nothing here
|
|
4
|
+
* touches a canvas or keeps state, so a mode stays a plain function of
|
|
5
|
+
* `(t, ctx)` and every helper below is trivial to unit test in isolation.
|
|
6
|
+
*/
|
|
7
|
+
const clamp01 = (value) => Math.max(0, Math.min(1, value));
|
|
8
|
+
/** Rotates a point about the X axis (mixes y and z). */
|
|
9
|
+
export const rotateX = (point, angle) => {
|
|
10
|
+
const cos = Math.cos(angle);
|
|
11
|
+
const sin = Math.sin(angle);
|
|
12
|
+
return {
|
|
13
|
+
x: point.x,
|
|
14
|
+
y: point.y * cos - point.z * sin,
|
|
15
|
+
z: point.y * sin + point.z * cos
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
/** Rotates a point about the Y axis (mixes x and z). */
|
|
19
|
+
export const rotateY = (point, angle) => {
|
|
20
|
+
const cos = Math.cos(angle);
|
|
21
|
+
const sin = Math.sin(angle);
|
|
22
|
+
return {
|
|
23
|
+
x: point.x * cos + point.z * sin,
|
|
24
|
+
y: point.y,
|
|
25
|
+
z: point.z * cos - point.x * sin
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
/** Rotates a point about the Z axis (mixes x and y). */
|
|
29
|
+
export const rotateZ = (point, angle) => {
|
|
30
|
+
const cos = Math.cos(angle);
|
|
31
|
+
const sin = Math.sin(angle);
|
|
32
|
+
return {
|
|
33
|
+
x: point.x * cos - point.y * sin,
|
|
34
|
+
y: point.x * sin + point.y * cos,
|
|
35
|
+
z: point.z
|
|
36
|
+
};
|
|
37
|
+
};
|
|
38
|
+
const FOCAL_LENGTH_PX = 320;
|
|
39
|
+
/**
|
|
40
|
+
* Projects a world point with perspective onto a canvas centred at
|
|
41
|
+
* `(originPx, originPx)`. `worldRadius` is the extent the point's `z` is
|
|
42
|
+
* expected to range over (typically the sphere/shape radius the mode drew it
|
|
43
|
+
* at), and is what turns `z` into a 0..1 `depth`: nearer (more negative `z`)
|
|
44
|
+
* is 1, farther is 0.
|
|
45
|
+
*/
|
|
46
|
+
export const project = (point, originPx, worldRadius) => {
|
|
47
|
+
const safeRadius = worldRadius > 0 ? worldRadius : 1;
|
|
48
|
+
const perspective = FOCAL_LENGTH_PX / (FOCAL_LENGTH_PX + point.z);
|
|
49
|
+
return {
|
|
50
|
+
x: originPx + point.x * perspective,
|
|
51
|
+
y: originPx + point.y * perspective,
|
|
52
|
+
depth: clamp01((safeRadius - point.z) / (safeRadius * 2)),
|
|
53
|
+
scale: perspective
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
const GOLDEN_ANGLE = Math.PI * (3 - Math.sqrt(5));
|
|
57
|
+
/**
|
|
58
|
+
* The `index`-th of `count` points spread evenly over a sphere of `radius`,
|
|
59
|
+
* via a golden-angle spiral. Deterministic in `index` and `count` alone, and
|
|
60
|
+
* even across the whole surface rather than crowding the poles the way a
|
|
61
|
+
* uniformly-sampled latitude/longitude grid would.
|
|
62
|
+
*/
|
|
63
|
+
export const sampleSphere = (index, count, radius) => {
|
|
64
|
+
const safeCount = count > 0 ? Math.floor(count) : 1;
|
|
65
|
+
const wrapped = ((Math.floor(index) % safeCount) + safeCount) % safeCount;
|
|
66
|
+
const y = 1 - (2 * wrapped + 1) / safeCount;
|
|
67
|
+
const ringRadius = Math.sqrt(Math.max(0, 1 - y * y));
|
|
68
|
+
const angle = wrapped * GOLDEN_ANGLE;
|
|
69
|
+
return {
|
|
70
|
+
x: Math.cos(angle) * ringRadius * radius,
|
|
71
|
+
y: y * radius,
|
|
72
|
+
z: Math.sin(angle) * ringRadius * radius
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/** Cubic ease, slow-fast-slow, for a motion that starts and ends at rest. */
|
|
76
|
+
export const easeInOutCubic = (t) => {
|
|
77
|
+
const clamped = clamp01(t);
|
|
78
|
+
return clamped < 0.5 ? 4 * clamped ** 3 : 1 - (-2 * clamped + 2) ** 3 / 2;
|
|
79
|
+
};
|
|
80
|
+
/** Cubic ease-out, fast-then-slow, for a motion that only needs to settle at the end. */
|
|
81
|
+
export const easeOutCubic = (t) => {
|
|
82
|
+
const clamped = clamp01(t);
|
|
83
|
+
return 1 - (1 - clamped) ** 3;
|
|
84
|
+
};
|
|
85
|
+
/**
|
|
86
|
+
* A deterministic 0..1 value for an integer seed (a dot's index is the usual
|
|
87
|
+
* one). Gives every mode per-dot variety -- a phase offset, a size jitter --
|
|
88
|
+
* without `Math.random`, so the same `(t, ctx)` always paints the same frame.
|
|
89
|
+
*/
|
|
90
|
+
export const hashUnit = (seed) => {
|
|
91
|
+
let hash = (seed | 0) >>> 0;
|
|
92
|
+
hash = Math.imul(hash ^ (hash >>> 16), 0x45d9f3b);
|
|
93
|
+
hash = Math.imul(hash ^ (hash >>> 16), 0x45d9f3b);
|
|
94
|
+
hash = (hash ^ (hash >>> 16)) >>> 0;
|
|
95
|
+
return hash / 4294967296;
|
|
96
|
+
};
|
|
97
|
+
const REFERENCE_SIZE = 64;
|
|
98
|
+
const RADIUS_GROWTH_EXPONENT = 0.6;
|
|
99
|
+
const MIN_DOT_RADIUS_PX = 0.6;
|
|
100
|
+
/**
|
|
101
|
+
* Turns `(size, density, dotScale)` into a count scaler and a radius scaler,
|
|
102
|
+
* so every mode derives its dot counts and radii the same way instead of
|
|
103
|
+
* keeping its own per-size table. Counts scale with area (`size` squared);
|
|
104
|
+
* radii grow sub-linearly, with a floor that keeps dots visible at 20px.
|
|
105
|
+
*/
|
|
106
|
+
export const modeSizing = (size, density, dotScale) => {
|
|
107
|
+
const sizeRatio = size / REFERENCE_SIZE;
|
|
108
|
+
const areaRatio = sizeRatio * sizeRatio;
|
|
109
|
+
const safeDensity = density > 0 ? density : 0.1;
|
|
110
|
+
const safeDotScale = dotScale > 0 ? dotScale : 0.1;
|
|
111
|
+
return {
|
|
112
|
+
count: (base) => Math.max(1, Math.round(base * areaRatio * safeDensity)),
|
|
113
|
+
radius: (base) => Math.max(MIN_DOT_RADIUS_PX, base * sizeRatio ** RADIUS_GROWTH_EXPONENT * safeDotScale)
|
|
114
|
+
};
|
|
115
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared vocabulary between `ThinkingOrb`'s modes, its painter and its
|
|
3
|
+
* gravity pass. A mode never touches a canvas directly -- it only ever
|
|
4
|
+
* describes one instant as a `Frame`, so it stays a pure, testable function
|
|
5
|
+
* of `(t, ctx)` with nothing environment-specific to mock.
|
|
6
|
+
*/
|
|
7
|
+
/** One canvas size `ThinkingOrb` renders at; a mode adapts to whichever it is given. */
|
|
8
|
+
export type ModeSize = 64 | 32 | 20;
|
|
9
|
+
/**
|
|
10
|
+
* One drawn point. `x`/`y` are canvas CSS px, `depth` is 0 (far) to 1 (near),
|
|
11
|
+
* `ink` is 0 to 1 and is turned into a colour by `paintFrame`, and `alpha` is
|
|
12
|
+
* 0 to 1 opacity.
|
|
13
|
+
*/
|
|
14
|
+
export type Dot = {
|
|
15
|
+
x: number;
|
|
16
|
+
y: number;
|
|
17
|
+
depth: number;
|
|
18
|
+
radius: number;
|
|
19
|
+
ink: number;
|
|
20
|
+
alpha: number;
|
|
21
|
+
};
|
|
22
|
+
/** One drawn line segment. Same coordinate, depth, ink and alpha units as `Dot`. */
|
|
23
|
+
export type Stroke = {
|
|
24
|
+
x1: number;
|
|
25
|
+
y1: number;
|
|
26
|
+
x2: number;
|
|
27
|
+
y2: number;
|
|
28
|
+
depth: number;
|
|
29
|
+
width: number;
|
|
30
|
+
ink: number;
|
|
31
|
+
alpha: number;
|
|
32
|
+
};
|
|
33
|
+
/** Everything to paint for one instant: strokes and dots, in no particular order. */
|
|
34
|
+
export type Frame = {
|
|
35
|
+
dots: readonly Dot[];
|
|
36
|
+
strokes: readonly Stroke[];
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* What a mode renders at and how strongly. `density` and `dotScale` are
|
|
40
|
+
* multipliers already floored at 0.1 by the component, never negative or zero.
|
|
41
|
+
*/
|
|
42
|
+
export type ModeContext = {
|
|
43
|
+
size: ModeSize;
|
|
44
|
+
density: number;
|
|
45
|
+
dotScale: number;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* A mode: a pure function of elapsed, already speed-scaled seconds and the
|
|
49
|
+
* context it draws into. Must be non-blank, finite and in-bounds at every `t`,
|
|
50
|
+
* including `t = 0`.
|
|
51
|
+
*/
|
|
52
|
+
export type ModeFn = (t: number, ctx: ModeContext) => Frame;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shared vocabulary between `ThinkingOrb`'s modes, its painter and its
|
|
3
|
+
* gravity pass. A mode never touches a canvas directly -- it only ever
|
|
4
|
+
* describes one instant as a `Frame`, so it stays a pure, testable function
|
|
5
|
+
* of `(t, ctx)` with nothing environment-specific to mock.
|
|
6
|
+
*/
|
|
7
|
+
export {};
|