@juspay/svelte-ui-components 4.32.1 → 4.33.1

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.
@@ -0,0 +1,201 @@
1
+ /**
2
+ * The "form" modes: composing, breathing and shaping. Each one reads as a
3
+ * single evolving surface or ring rather than a swarm of independent points,
4
+ * so the motion stays legible down at 20px instead of dissolving into noise
5
+ * the way a large loose point cloud would.
6
+ *
7
+ * All three share one placement idiom: build each dot in its own object
8
+ * space (a band or ring parametrised by its index, deformed however that
9
+ * mode's intent calls for), spin the whole thing with `rotateY` for a sense
10
+ * of turning, tilt it with `rotateX` so it reads in 3D, then `project`. That
11
+ * split keeps "the shape's own motion" and "the camera's fixed angle on it"
12
+ * from tangling together in the per-dot math.
13
+ */
14
+ import { easeInOutCubic, hashUnit, modeSizing, project, rotateX, rotateY, rotateZ } from '../space';
15
+ const TWO_PI = Math.PI * 2;
16
+ /** The shared "reads as a form inside a square" contract: radius as a share of `size`. */
17
+ const FORM_RADIUS_RATIO = 0.4;
18
+ const clamp01 = (value) => Math.max(0, Math.min(1, value));
19
+ /** Nearer dots read as more saturated tint; `paintFrame` turns low ink into more of `tint`. */
20
+ const depthToInk = (depth) => clamp01(1 - depth);
21
+ /** Farther dots stay at partial opacity rather than fading to nothing, so no instant reads blank. */
22
+ const depthToAlpha = (depth) => clamp01(0.5 + depth * 0.5);
23
+ /** Nearer dots draw a touch larger, reinforcing the depth `paintFrame`'s draw order already implies. */
24
+ const depthToRadiusFactor = (depth) => 0.7 + depth * 0.3;
25
+ /** Builds one `Dot` from a projected point; `jitter` gives per-dot size variety from `hashUnit`. */
26
+ const dotFrom = (projected, baseRadius, sizing, jitter) => ({
27
+ x: projected.x,
28
+ y: projected.y,
29
+ depth: projected.depth,
30
+ radius: sizing.radius(baseRadius) * depthToRadiusFactor(projected.depth) * jitter,
31
+ ink: depthToInk(projected.depth),
32
+ alpha: depthToAlpha(projected.depth)
33
+ });
34
+ /** Turns an object-space point into a placed, projected `Dot` via the shared spin/tilt/project steps. */
35
+ const place = (point, spinAngle, tiltAngle, originPx, worldRadius, baseRadius, sizing, jitter) => {
36
+ const spun = rotateY(point, spinAngle);
37
+ const tilted = rotateX(spun, tiltAngle);
38
+ const projected = project(tilted, originPx, worldRadius);
39
+ return dotFrom(projected, baseRadius, sizing, jitter);
40
+ };
41
+ const EMPTY_STROKES = [];
42
+ // --- composing ---------------------------------------------------------
43
+ const RIBBON_BASE_COUNT = 260;
44
+ const RIBBON_BASE_DOT_RADIUS = 2.2;
45
+ const RIBBON_THREADS = { 64: 7, 32: 5, 20: 4 };
46
+ const RIBBON_HALF_WIDTH = 0.4;
47
+ /** The ribbon's own great circle sits tilted off the spin axis by this much,
48
+ * so spinning genuinely sweeps its width through the frame -- a band that
49
+ * happened to lie flat on the equator would look identical at every spin
50
+ * angle, since an equatorial ring is rotationally symmetric about that axis. */
51
+ const RIBBON_AXIS_TILT = 0.68;
52
+ const RIBBON_RIPPLE_WAVES = 3;
53
+ const RIBBON_RIPPLE_SPEED = 1.15;
54
+ const RIBBON_RIPPLE_AMPLITUDE = 0.5;
55
+ const RIBBON_THREAD_JITTER = 0.1;
56
+ const RIBBON_SPIN_SPEED = 0.48;
57
+ const RIBBON_CAMERA_TILT = 0.45;
58
+ /** Edge threads only dim toward the background; nothing here shrinks a dot's
59
+ * radius, so the ribbon keeps a full-width row of legible marks even where a
60
+ * combined ink/alpha/radius fade would collapse an edge row to a speck. */
61
+ const RIBBON_EDGE_ALPHA_FLOOR = 0.5;
62
+ const RIBBON_EDGE_INK_LIFT = 0.22;
63
+ /**
64
+ * A ribbon of dots looped once around the sphere on its own tilted great
65
+ * circle -- distinct from the spin axis -- rippling along its length like a
66
+ * travelling wave through cloth while the whole ribbon turns.
67
+ */
68
+ export const composing = (t, ctx) => {
69
+ const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
70
+ const worldRadius = ctx.size * FORM_RADIUS_RATIO;
71
+ const originPx = ctx.size / 2;
72
+ const threads = RIBBON_THREADS[ctx.size];
73
+ const perThread = Math.max(1, Math.ceil(sizing.count(RIBBON_BASE_COUNT) / threads));
74
+ const total = threads * perThread;
75
+ const dots = [];
76
+ for (let index = 0; index < total; index += 1) {
77
+ const thread = index % threads;
78
+ const step = Math.floor(index / threads);
79
+ // w ranges -1 (one edge thread) .. 0 (centre) .. 1 (other edge thread).
80
+ const w = threads > 1 ? (thread / (threads - 1)) * 2 - 1 : 0;
81
+ const u = (step / perThread) * TWO_PI;
82
+ const ripple = Math.sin(u * RIBBON_RIPPLE_WAVES + t * RIBBON_RIPPLE_SPEED) * RIBBON_RIPPLE_AMPLITUDE;
83
+ // A standard spherical parametrisation keeps every point exactly on a
84
+ // sphere of `worldRadius` regardless of `phi`, so the ribbon stays safely
85
+ // inside the canvas no matter how far the ripple bends it.
86
+ const phi = w * RIBBON_HALF_WIDTH + ripple;
87
+ const onSphere = {
88
+ x: Math.cos(phi) * Math.cos(u) * worldRadius,
89
+ y: Math.sin(phi) * worldRadius,
90
+ z: Math.cos(phi) * Math.sin(u) * worldRadius
91
+ };
92
+ const onAxis = rotateZ(onSphere, RIBBON_AXIS_TILT);
93
+ const jitter = 1 - RIBBON_THREAD_JITTER / 2 + hashUnit(index * 7 + 1) * RIBBON_THREAD_JITTER;
94
+ const placed = place(onAxis, t * RIBBON_SPIN_SPEED, RIBBON_CAMERA_TILT, originPx, worldRadius, RIBBON_BASE_DOT_RADIUS, sizing, jitter);
95
+ const edgeFeather = Math.abs(w);
96
+ dots.push({
97
+ ...placed,
98
+ ink: clamp01(placed.ink + edgeFeather * RIBBON_EDGE_INK_LIFT),
99
+ alpha: clamp01(placed.alpha * (1 - edgeFeather * (1 - RIBBON_EDGE_ALPHA_FLOOR)))
100
+ });
101
+ }
102
+ return { dots, strokes: EMPTY_STROKES };
103
+ };
104
+ // --- breathing -----------------------------------------------------------
105
+ const BREATHING_BASE_COUNT = 210;
106
+ const BREATHING_BASE_DOT_RADIUS = 1.7;
107
+ /** Sits below composing/shaping's ~0.4: at the peak of its breath this ring
108
+ * already swells by its amplitude plus its shape wobble below, so a resting
109
+ * radius this much smaller still lands at about the same on-screen extent as
110
+ * its siblings' fixed radius, without the peak pushing past the canvas. */
111
+ const BREATHING_WORLD_RADIUS_RATIO = 0.34;
112
+ const BREATHING_AMPLITUDE = 0.09;
113
+ const BREATHING_SPEED = 0.55;
114
+ const BREATHING_SHAPE_HARMONIC = 5;
115
+ const BREATHING_SHAPE_SPEED = 0.22;
116
+ const BREATHING_SHAPE_AMPLITUDE = 0.035;
117
+ const BREATHING_SPIN_SPEED = 0.12;
118
+ const BREATHING_TILT = 0.62;
119
+ /**
120
+ * A single ring that swells and relaxes on a slow sine, with a faint,
121
+ * higher-harmonic wobble layered on top so its outline never reads as a
122
+ * perfectly static circle even at rest.
123
+ */
124
+ export const breathing = (t, ctx) => {
125
+ const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
126
+ const worldRadius = ctx.size * BREATHING_WORLD_RADIUS_RATIO;
127
+ const originPx = ctx.size / 2;
128
+ const count = sizing.count(BREATHING_BASE_COUNT);
129
+ const breathe = Math.sin(t * BREATHING_SPEED);
130
+ const dots = [];
131
+ for (let index = 0; index < count; index += 1) {
132
+ const angle = (index / count) * TWO_PI;
133
+ const wobble = Math.sin(angle * BREATHING_SHAPE_HARMONIC + t * BREATHING_SHAPE_SPEED) *
134
+ BREATHING_SHAPE_AMPLITUDE;
135
+ const radiusFactor = 1 + breathe * BREATHING_AMPLITUDE + wobble;
136
+ const point = {
137
+ x: Math.cos(angle) * radiusFactor * worldRadius,
138
+ y: 0,
139
+ z: Math.sin(angle) * radiusFactor * worldRadius
140
+ };
141
+ const jitter = 0.88 + hashUnit(index * 11 + 5) * 0.24;
142
+ dots.push(place(point, t * BREATHING_SPIN_SPEED, BREATHING_TILT, originPx, worldRadius, BREATHING_BASE_DOT_RADIUS, sizing, jitter));
143
+ }
144
+ return { dots, strokes: EMPTY_STROKES };
145
+ };
146
+ // --- shaping ---------------------------------------------------------------
147
+ const SHAPING_BASE_COUNT = 210;
148
+ const SHAPING_BASE_DOT_RADIUS = 1.7;
149
+ const SHAPING_CYCLE_SECONDS = 12;
150
+ // One full turn per morph cycle, so the shape and its orientation both land
151
+ // back where they started at the same instant -- a true seamless loop.
152
+ const SHAPING_SPIN_SPEED = (2 * Math.PI) / SHAPING_CYCLE_SECONDS;
153
+ const SHAPING_TILT = 0.6;
154
+ /** The radial distance of a regular `sides`-gon's straight edge at `angle`, circumradius 1. */
155
+ const polygonRadius = (angle, sides) => {
156
+ const segment = TWO_PI / sides;
157
+ const half = segment / 2;
158
+ const wrapped = ((angle % segment) + segment) % segment;
159
+ return Math.cos(half) / Math.cos(wrapped - half);
160
+ };
161
+ const lerp = (from, to, amount) => from + (to - from) * amount;
162
+ /**
163
+ * A ring that morphs circle to triangle to square and back over a fixed
164
+ * cycle, each leg eased so the shape comes to rest at every corner instead
165
+ * of snapping into it -- which also makes the wrap from the last leg back to
166
+ * the first exactly continuous.
167
+ */
168
+ export const shaping = (t, ctx) => {
169
+ const sizing = modeSizing(ctx.size, ctx.density, ctx.dotScale);
170
+ const worldRadius = ctx.size * FORM_RADIUS_RATIO;
171
+ const originPx = ctx.size / 2;
172
+ const count = sizing.count(SHAPING_BASE_COUNT);
173
+ const cyclePos = (((t / SHAPING_CYCLE_SECONDS) % 1) + 1) % 1;
174
+ const phaseFloat = cyclePos * 3;
175
+ const phaseIndex = Math.min(2, Math.floor(phaseFloat));
176
+ const eased = easeInOutCubic(phaseFloat - phaseIndex);
177
+ const dots = [];
178
+ for (let index = 0; index < count; index += 1) {
179
+ const angle = (index / count) * TWO_PI;
180
+ const triangleR = polygonRadius(angle, 3);
181
+ const squareR = polygonRadius(angle, 4);
182
+ let radiusFactor;
183
+ if (phaseIndex === 0) {
184
+ radiusFactor = lerp(1, triangleR, eased);
185
+ }
186
+ else if (phaseIndex === 1) {
187
+ radiusFactor = lerp(triangleR, squareR, eased);
188
+ }
189
+ else {
190
+ radiusFactor = lerp(squareR, 1, eased);
191
+ }
192
+ const point = {
193
+ x: Math.cos(angle) * radiusFactor * worldRadius,
194
+ y: 0,
195
+ z: Math.sin(angle) * radiusFactor * worldRadius
196
+ };
197
+ const jitter = 0.9 + hashUnit(index * 17 + 9) * 0.2;
198
+ dots.push(place(point, t * SHAPING_SPIN_SPEED, SHAPING_TILT, originPx, worldRadius, SHAPING_BASE_DOT_RADIUS, sizing, jitter));
199
+ }
200
+ return { dots, strokes: EMPTY_STROKES };
201
+ };
@@ -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;