@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.
- package/dist/ThinkingOrb/ThinkingOrb.svelte +357 -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 +30 -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 +1035 -433
- package/package.json +1 -1
|
@@ -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;
|