@fusion.dev/helix 0.181.2 → 0.182.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.
@@ -1,7 +1,7 @@
1
1
  export const MAX_POINTS = 96;
2
2
  const NARROW = 760;
3
- const GLOW = { bend: 1, ribbon: 0.36, waves: 0.7 };
4
- const WIDTH = { bend: 0.12, ribbon: 0.24, waves: 0.06 };
3
+ const GLOW = { bend: 1, ribbon: 0.36, sweep: 0.8, waves: 0.7 };
4
+ const WIDTH = { bend: 0.12, ribbon: 0.18, sweep: 0.16, waves: 0.06 };
5
5
  const BEND_AT = 0.5;
6
6
  const BEND_EXIT = 0.3;
7
7
  export const TURN = 0.5;
@@ -9,13 +9,13 @@ const bend = (top, height, at, narrow, width, reach) => {
9
9
  const x = narrow ? Math.max(at, 0.8) : at;
10
10
  const y = top + height * 0.58;
11
11
  const out = (down) => x + (down * down * height) / (2 * TURN) / width;
12
+ const pinch = (down) => 1 - Math.exp(-((down / 0.32) ** 2));
12
13
  const points = [];
13
14
  for (let step = -7; step * 0.15 <= reach + 1e-9; step++) {
14
15
  const down = step * 0.15;
15
16
  const upper = down < 0;
16
- const wide = upper
17
- ? 0.02 + (WIDTH.bend - 0.02) * Math.min(1, -down / 0.6)
18
- : 0.02 + (WIDTH.bend * 1.4 - 0.02) * (down / reach);
17
+ const far = upper ? WIDTH.bend : WIDTH.bend + WIDTH.bend * 0.4 * (down / reach);
18
+ const wide = 0.02 + (far - 0.02) * pinch(down);
19
19
  const glow = upper ? Math.max(0, 1 + down * 1.07) : 1 - down * 0.73;
20
20
  points.push([out(down), y + down * height, wide, GLOW.bend * glow]);
21
21
  }
@@ -55,7 +55,17 @@ const wave = (y, height, fromRight, narrow, width, from) => {
55
55
  }
56
56
  return points;
57
57
  };
58
- const swingOf = (height, width, narrow) => Math.min(narrow ? 0.26 : 0.3, (height * TURN) / width);
58
+ export const SPREAD = 1.35;
59
+ const REACH = 1.3;
60
+ const WOBBLE = 40;
61
+ const reachOf = (wide, width) => wide * SPREAD * width * REACH + WOBBLE;
62
+ const PERIOD = 1.7;
63
+ export const SHAPE = { lean: 0.1, period: 1.4, sweep: 0.4, swing: 0.6 };
64
+ const swingOf = (height, width, narrow, shape = SHAPE) => {
65
+ const reach = reachOf((narrow ? 0.45 : WIDTH.ribbon) * 1.1, width);
66
+ const gentle = (PERIOD * shape.period * height) ** 2 / (Math.PI ** 2 * reach * 1.15);
67
+ return Math.min((narrow ? 0.26 : 0.3) * shape.swing, (height * TURN) / width, gentle / width);
68
+ };
59
69
  const RIBBON_STEP = 0.35;
60
70
  const bridge = (from, to, first, last) => {
61
71
  const length = to[1] - from[1];
@@ -82,15 +92,15 @@ const bridge = (from, to, first, last) => {
82
92
  }
83
93
  return out;
84
94
  };
85
- const ribbon = (from, to, height, at, phase, narrow, width) => {
95
+ const ribbon = (from, to, height, at, phase, narrow, width, shape = SHAPE) => {
86
96
  const points = [];
87
97
  const step = height * RIBBON_STEP;
88
- const swing = swingOf(height, width, narrow);
98
+ const swing = swingOf(height, width, narrow, shape);
89
99
  const wide = narrow ? 0.45 : WIDTH.ribbon;
90
100
  const glow = narrow ? GLOW.ribbon * 0.65 : GLOW.ribbon;
91
101
  let a = phase;
92
102
  for (let y = from; y < to; y += step) {
93
- a += (step / (1.7 * height)) * Math.PI;
103
+ a += (step / (PERIOD * shape.period * height)) * Math.PI;
94
104
  points.push([at + swing * Math.sin(a), y, wide * (0.8 + 0.3 * Math.sin(a * 0.63 + 0.4)), glow]);
95
105
  }
96
106
  return { phase: a, points };
@@ -103,12 +113,77 @@ export const boxPath = (variant, at, height, width) => {
103
113
  if (variant === 'waves') {
104
114
  return wave(height * 0.5, height, false, narrow, width);
105
115
  }
116
+ if (variant === 'sweep') {
117
+ return pagePath([{ at, height: height * 0.6, left: 0.08, right: 0.92, top: height * 0.4, variant: 'sweep' }], 'ribbon', width, height, height);
118
+ }
106
119
  const start = -height * 0.4;
107
120
  const lead = [at ?? 0.5, start - height * 0.4, WIDTH.ribbon, 0];
108
121
  const body = ribbon(start, height * 1.4, height, at ?? 0.5, 0, narrow, width).points.map(([x, y, w]) => [x, y, w, narrow ? GLOW.ribbon * 0.65 * 1.4 : GLOW.ribbon * 1.4]);
109
122
  return [lead, ...body];
110
123
  };
111
- export const pagePath = (stops, fill, width, height, page) => {
124
+ const tightestIn = (points, i, width) => {
125
+ const n = points.length;
126
+ const at = (k, c) => points[Math.max(0, Math.min(n - 1, k))]?.[c] ?? 0;
127
+ const sample = (f) => {
128
+ const along = (c) => catmull(at(i - 1, c), at(i, c), at(i + 1, c), at(i + 2, c), f);
129
+ return [along(0) * width, along(1), along(2), along(3)];
130
+ };
131
+ let radius = Number.POSITIVE_INFINITY;
132
+ let reach = 0;
133
+ for (let k = 1; k < 8; k++) {
134
+ const [ax, ay] = sample((k - 0.5) / 8);
135
+ const [bx, by, wide, glow] = sample(k / 8);
136
+ const [cx, cy] = sample((k + 0.5) / 8);
137
+ if (glow < 0.1 || bx < -0.1 * width || bx > 1.1 * width) {
138
+ continue;
139
+ }
140
+ const area = Math.abs((bx - ax) * (cy - ay) - (by - ay) * (cx - ax)) / 2;
141
+ const r = area < 1e-6
142
+ ? Number.POSITIVE_INFINITY
143
+ : (Math.hypot(bx - ax, by - ay) *
144
+ Math.hypot(cx - bx, cy - by) *
145
+ Math.hypot(cx - ax, cy - ay)) /
146
+ (4 * area);
147
+ if (r < radius) {
148
+ radius = r;
149
+ reach = reachOf(Math.max(0, wide), width);
150
+ }
151
+ }
152
+ return { radius, reach };
153
+ };
154
+ const relax = (points, width, height, kept) => {
155
+ const out = points.map((point) => [...point]);
156
+ const floor = 0.3 * Math.min(width, height);
157
+ const ease = (i) => {
158
+ const a = out[i - 1];
159
+ const b = out[i];
160
+ const c = out[i + 1];
161
+ if (a && b && c) {
162
+ out[i] = [b[0] + ((a[0] + c[0]) / 2 - b[0]) / 4, b[1], b[2], b[3]];
163
+ }
164
+ };
165
+ for (let pass = 0; pass < 80; pass++) {
166
+ let eased = false;
167
+ for (let i = 1; i < out.length - 2; i++) {
168
+ if (kept.has(i) || kept.has(i + 1)) {
169
+ continue;
170
+ }
171
+ const { radius, reach } = tightestIn(out, i, width);
172
+ if (radius < Math.max(reach * 1.1, floor)) {
173
+ ease(i);
174
+ ease(i + 1);
175
+ eased = true;
176
+ }
177
+ }
178
+ if (!eased) {
179
+ break;
180
+ }
181
+ }
182
+ return out;
183
+ };
184
+ export const pagePath = (stops, fill, width, height, page, shape = SHAPE) => {
185
+ const period = PERIOD * shape.period;
186
+ const home = 0.5 + shape.lean;
112
187
  const narrow = width < NARROW;
113
188
  const placed = [...stops].sort((one, two) => one.top - two.top);
114
189
  if (placed.length === 0) {
@@ -117,26 +192,30 @@ export const pagePath = (stops, fill, width, height, page) => {
117
192
  placed.push({ at: undefined, height, top: y, variant: 'waves' });
118
193
  }
119
194
  }
120
- else if (fill === 'bend') {
121
- placed.push({ at: undefined, height, top: 0, variant: 'bend' });
195
+ else if (fill === 'bend' || fill === 'sweep') {
196
+ placed.push({ at: undefined, height, top: 0, variant: fill });
122
197
  }
123
198
  }
124
199
  const points = [[0.6, -height * 0.9, WIDTH.ribbon, 0]];
125
- const turn = (RIBBON_STEP / 1.7) * Math.PI;
200
+ const turn = (RIBBON_STEP / period) * Math.PI;
126
201
  let phase = 1.2;
127
202
  let cursor = 0;
128
203
  let fromRight = true;
129
204
  let previous;
130
205
  let joined;
206
+ let axis = home;
207
+ let carried = false;
208
+ const kept = new Set();
131
209
  const gap = (to, next) => {
132
- if (to - cursor < height * 0.7) {
210
+ if (to - cursor < height * (carried ? 0.4 : 0.7)) {
133
211
  return;
134
212
  }
135
- if (previous === 'waves' && next === 'waves' && to - cursor < height * 1.5) {
213
+ if (previous === 'waves' && next === 'waves' && !carried && to - cursor < height * 1.5) {
136
214
  return;
137
215
  }
138
216
  const end = next === 'waves' ? to - height * 0.95 : to - height * 0.45;
139
- const stretch = ribbon(cursor + height * 0.2, end, height, 0.5, phase, narrow, width);
217
+ const stretch = ribbon(cursor + height * 0.2, end, height, axis, phase, narrow, width, shape);
218
+ axis = home;
140
219
  phase = stretch.phase;
141
220
  let run = stretch.points;
142
221
  let heading;
@@ -152,7 +231,7 @@ export const pagePath = (stops, fill, width, height, page) => {
152
231
  }
153
232
  }
154
233
  }
155
- if (previous === 'waves' && run.length > 1) {
234
+ if (previous === 'waves' && !carried && run.length > 1) {
156
235
  run = run.map((point, k) => k < 3 ? [point[0], point[1], point[2], point[3] * Math.max(0, k - 1) * 0.5] : point);
157
236
  }
158
237
  points.push(...run);
@@ -162,26 +241,77 @@ export const pagePath = (stops, fill, width, height, page) => {
162
241
  joined = last[0];
163
242
  }
164
243
  };
165
- for (const stop of placed) {
244
+ const carryFrom = (way, from, light, spacing) => {
245
+ const bottom = points.at(-1);
246
+ const swing = swingOf(height, width, narrow, shape);
247
+ const wide = narrow ? 0.45 : WIDTH.ribbon;
248
+ axis = bottom[0] - way * swing;
249
+ let phaseAt = way > 0 ? Math.PI / 2 : Math.PI * 1.5;
250
+ let y = bottom[1];
251
+ let gap = spacing;
252
+ const ramp = Math.ceil(Math.log((height * RIBBON_STEP) / spacing) / Math.log(1.33));
253
+ let k = 0;
254
+ while (gap < height * RIBBON_STEP) {
255
+ y += gap;
256
+ phaseAt += (gap / (period * height)) * Math.PI;
257
+ k += 1;
258
+ const f = Math.min(1, k / Math.max(ramp, 1));
259
+ points.push([
260
+ axis + swing * Math.sin(phaseAt),
261
+ y,
262
+ from + (wide - from) * f,
263
+ light + (GLOW.ribbon - light) * f,
264
+ ]);
265
+ gap *= 1.33;
266
+ }
267
+ phase = phaseAt;
268
+ cursor = y + height * RIBBON_STEP - height * 0.2;
269
+ fromRight = way > 0;
270
+ carried = true;
271
+ };
272
+ const ribbonAfter = (from, next) => next !== undefined &&
273
+ next.top - from >= height * 0.7 &&
274
+ !(next.variant === 'waves' && next.top - from < height * 1.5);
275
+ for (const [index, stop] of placed.entries()) {
166
276
  joined = undefined;
167
- gap(stop.top, stop.variant);
277
+ if (stop.variant !== 'sweep') {
278
+ gap(stop.top, stop.variant);
279
+ }
280
+ carried = false;
168
281
  if (stop.variant === 'bend') {
169
282
  const span = Math.min(stop.height, height);
170
283
  const at = stop.at ?? BEND_AT;
171
- const slope = BEND_EXIT / TURN;
172
- const swing = swingOf(height, width, narrow);
173
- const steepest = (swing * width * Math.PI) / (1.7 * height);
284
+ const swing = swingOf(height, width, narrow, shape);
285
+ const steepest = (swing * width * Math.PI) / (period * height);
286
+ const reach = Math.max(0.15, Math.floor(Math.min(BEND_EXIT, steepest * TURN) / 0.15) * 0.15);
287
+ const slope = reach / TURN;
174
288
  const a = Math.acos(Math.min(1, slope / steepest));
175
- const x = 0.5 + swing * Math.sin(a);
176
- const bent = bend(stop.top, span, at, narrow, width, narrow ? 0.6 : BEND_EXIT);
289
+ const bent = bend(stop.top, span, at, narrow, width, narrow ? 0.6 : reach);
177
290
  const exit = bent.at(-1);
178
291
  points.push(...bent);
179
- if (!narrow && x > exit[0]) {
180
- const drop = Math.max(height * 0.3, ((x - exit[0]) * width) / slope);
181
- const start = [x, exit[1] + drop, WIDTH.ribbon, GLOW.ribbon];
182
- points.push(...bridge(exit, start, span * 0.15, height * RIBBON_STEP));
183
- cursor = start[1] - height * 0.2;
184
- phase = a - turn;
292
+ if (!narrow) {
293
+ axis = Math.min(1.08 - swing, Math.max(swing - 0.08, exit[0] - swing * Math.sin(a)));
294
+ let phaseAt = Math.asin(Math.max(-1, Math.min(1, (exit[0] - axis) / swing)));
295
+ phaseAt = Math.max(phaseAt, Math.min(a, Math.PI / 2));
296
+ let y = exit[1];
297
+ let spacing = span * 0.15;
298
+ let k = 0;
299
+ const steps = Math.ceil(Math.log((height * RIBBON_STEP) / spacing) / Math.log(1.33));
300
+ while (spacing < height * RIBBON_STEP) {
301
+ y += spacing;
302
+ phaseAt += (spacing / (period * height)) * Math.PI;
303
+ k += 1;
304
+ const f = Math.min(1, k / Math.max(steps, 1));
305
+ points.push([
306
+ axis + swing * Math.sin(phaseAt),
307
+ y,
308
+ exit[2] + (WIDTH.ribbon - exit[2]) * f,
309
+ exit[3] + (GLOW.ribbon - exit[3]) * f,
310
+ ]);
311
+ spacing *= 1.33;
312
+ }
313
+ cursor = y + height * RIBBON_STEP - height * 0.2;
314
+ phase = phaseAt;
185
315
  }
186
316
  else {
187
317
  cursor = exit[1] + height * 0.1;
@@ -189,20 +319,83 @@ export const pagePath = (stops, fill, width, height, page) => {
189
319
  }
190
320
  fromRight = true;
191
321
  }
322
+ else if (stop.variant === 'sweep') {
323
+ const right = (stop.at ?? 0.9) >= 0.5;
324
+ const way = right ? -1 : 1;
325
+ const from = right ? 0.98 : 0.02;
326
+ const land = right ? (stop.left ?? 0.06) : (stop.right ?? 0.94);
327
+ const full = narrow ? 0.3 : WIDTH.sweep;
328
+ const glow = narrow ? GLOW.sweep * 0.65 : GLOW.sweep;
329
+ const begins = Math.min(stop.top, 0) - height * 0.35;
330
+ const descent = stop.top - begins;
331
+ const across = (land - from) * width;
332
+ const allowed = (radius) => Math.max(0.006, Math.min(full, (radius / 1.15 - WOBBLE) / (SPREAD * width * REACH)));
333
+ const radiusAt = (f) => {
334
+ const slope = ((across * Math.PI) / (2 * descent)) * Math.sin(Math.PI * f);
335
+ const bend = Math.abs(((across * Math.PI ** 2) / (2 * descent ** 2)) * Math.cos(Math.PI * f));
336
+ return bend < 1e-9 ? Number.POSITIVE_INFINITY : (1 + slope * slope) ** 1.5 / bend;
337
+ };
338
+ const spacing = height * 0.08;
339
+ const count = Math.max(8, Math.ceil(descent / spacing));
340
+ points.push([from, begins - height * 0.4, allowed(radiusAt(0)), 0]);
341
+ for (let k = 0; k <= count; k++) {
342
+ const f = k / count;
343
+ kept.add(points.length);
344
+ points.push([
345
+ from + ((land - from) * (1 - Math.cos(Math.PI * f))) / 2,
346
+ begins + descent * f,
347
+ allowed(radiusAt(f)),
348
+ glow * Math.min(1, (k + 1) / 3),
349
+ ]);
350
+ }
351
+ carryFrom(way, allowed(radiusAt(1)), glow, descent / count);
352
+ }
192
353
  else if (stop.variant === 'waves') {
193
- const crossing = wave(stop.top, height, fromRight, narrow, width, joined);
354
+ const carries = joined !== undefined && ribbonAfter(stop.top + height * 0.5, placed[index + 1]);
194
355
  const end = points.at(-1);
195
- const lead = crossing[0];
196
- if (joined !== undefined && end && lead) {
197
- points.push(...bridge(end, lead, height * RIBBON_STEP, height * 0.26));
356
+ if (carries && end) {
357
+ const way = end[0] < 0.5 ? 1 : -1;
358
+ const thin = narrow ? WIDTH.waves * 2 : WIDTH.waves;
359
+ const glow = narrow ? GLOW.waves * 0.65 : GLOW.waves;
360
+ const blend = (a, b, f) => a + (b - a) * f;
361
+ const from = end[0];
362
+ const to = 0.5 + way * shape.sweep;
363
+ const amplitude = (Math.abs(to - from) * width) / 2;
364
+ const radius = Math.max(reachOf(thin, width), 0.3 * Math.min(width, height)) * 1.15;
365
+ const descent = Math.PI * Math.sqrt(radius * amplitude);
366
+ const begins = Math.max(end[1] + height * 0.3, stop.top - descent / 2);
367
+ const spacing = height * 0.12;
368
+ const top = [from, begins, thin, glow * 0.7];
369
+ const mirror = ([x, y, w, g]) => [x, -y, w, g];
370
+ points.push(...bridge(mirror(top), mirror(end), spacing, height * RIBBON_STEP)
371
+ .map(mirror)
372
+ .reverse(), top);
373
+ const count = Math.max(4, Math.ceil(descent / spacing));
374
+ for (let k = 1; k <= count; k++) {
375
+ const f = k / count;
376
+ points.push([
377
+ from + ((to - from) * (1 - Math.cos(Math.PI * f))) / 2,
378
+ begins + descent * f,
379
+ thin,
380
+ blend(glow * 0.7, glow, Math.sin(Math.PI * f)),
381
+ ]);
382
+ }
383
+ carryFrom(way, thin, glow * 0.7, spacing);
384
+ }
385
+ else {
386
+ const crossing = wave(stop.top, height, fromRight, narrow, width, joined);
387
+ const lead = crossing[0];
388
+ if (joined !== undefined && end && lead) {
389
+ points.push(...bridge(end, lead, height * RIBBON_STEP, height * 0.26));
390
+ }
391
+ points.push(...crossing);
392
+ phase =
393
+ joined === undefined
394
+ ? (fromRight ? Math.PI * 1.5 : Math.PI / 2) - turn
395
+ : (fromRight ? Math.PI / 2 : Math.PI * 1.5) - turn;
396
+ fromRight = joined === undefined ? !fromRight : fromRight;
397
+ cursor = stop.top + height * (joined === undefined ? 0.35 : 0.5);
198
398
  }
199
- points.push(...crossing);
200
- phase =
201
- joined === undefined
202
- ? (fromRight ? Math.PI * 1.5 : Math.PI / 2) - turn
203
- : (fromRight ? Math.PI / 2 : Math.PI * 1.5) - turn;
204
- fromRight = joined === undefined ? !fromRight : fromRight;
205
- cursor = stop.top + height * (joined === undefined ? 0.35 : 0.5);
206
399
  }
207
400
  else {
208
401
  const y = stop.top + Math.min(stop.height, height) / 2;
@@ -218,7 +411,7 @@ export const pagePath = (stops, fill, width, height, page) => {
218
411
  const before = points.at(-2) ?? last;
219
412
  const on = [2 * last[0] - before[0], 2 * last[1] - before[1], last[2], 0];
220
413
  points.push(on, [on[0], Math.max(on[1], page) + height, last[2], 0]);
221
- return points.slice(0, MAX_POINTS);
414
+ return relax(points.slice(0, MAX_POINTS), width, height, kept);
222
415
  };
223
416
  export const catmull = (p0, p1, p2, p3, f) => {
224
417
  const f2 = f * f;
@@ -229,6 +422,26 @@ export const catmull = (p0, p1, p2, p3, f) => {
229
422
  (2 * p0 - 5 * p1 + 4 * p2 - p3) * f2 +
230
423
  (-p0 + 3 * p1 - 3 * p2 + p3) * f3));
231
424
  };
425
+ export const arcLengths = (points, width) => {
426
+ const n = points.length;
427
+ const out = new Float32Array(n);
428
+ const at = (i, c) => points[Math.max(0, Math.min(n - 1, i))]?.[c] ?? 0;
429
+ let total = 0;
430
+ for (let i = 0; i < n - 1; i++) {
431
+ let x = at(i, 0) * width;
432
+ let y = at(i, 1);
433
+ for (let k = 1; k <= 16; k++) {
434
+ const f = k / 16;
435
+ const nx = catmull(at(i - 1, 0), at(i, 0), at(i + 1, 0), at(i + 2, 0), f) * width;
436
+ const ny = catmull(at(i - 1, 1), at(i, 1), at(i + 1, 1), at(i + 2, 1), f);
437
+ total += Math.hypot(nx - x, ny - y);
438
+ x = nx;
439
+ y = ny;
440
+ }
441
+ out[i + 1] = total;
442
+ }
443
+ return out;
444
+ };
232
445
  export const lookup = (points) => {
233
446
  const n = points.length;
234
447
  const samples = Math.max(1, (n - 1) * 24);
@@ -1,4 +1,6 @@
1
1
  import type { StrandsPalette } from './palette.js';
2
+ import { MAX_QUIET } from './shader.js';
3
+ export { MAX_QUIET };
2
4
  /** The most strands a ribbon draws, at `density` 1. */
3
5
  export declare const MAX_STRANDS = 340;
4
6
  /** The fewest, at `density` 0. Fewer stops reading as a ribbon at all. */
@@ -19,6 +21,8 @@ export interface StrandsFrame {
19
21
  depth: number;
20
22
  /** How many specks of dust drift around the strands. */
21
23
  dust: number;
24
+ /** How far along the path each point is, in CSS pixels, padded like `points`. */
25
+ lengths: Float32Array;
22
26
  /** How differently each strand moves, from `0`, as one, to `1`. */
23
27
  drift: number;
24
28
  /** The device pixels per CSS pixel the buffer was sized at. */
@@ -27,6 +31,12 @@ export interface StrandsFrame {
27
31
  gain: number;
28
32
  /** The path, four floats a point, padded to the uniform array's length. */
29
33
  points: Float32Array;
34
+ /**
35
+ * The rects the light dims behind, as left, top, right and bottom in the
36
+ * path's pixels, four floats each for `MAX_QUIET` rects. An unused one is
37
+ * all zeros.
38
+ */
39
+ quiet: Float32Array;
30
40
  /** The stretch of the path on screen, as a range of its parameter. */
31
41
  range: readonly [number, number];
32
42
  /** How far the page is scrolled, in CSS pixels, or `0` inside a box. */
@@ -1,10 +1,11 @@
1
- import { BLUR_FRAGMENT, COMPOSITE_FRAGMENT, DOWN_FRAGMENT, DUST_FRAGMENT, DUST_VERTEX, LINE_FRAGMENT, LINE_VERTEX, QUAD_VERTEX, } from './shader.js';
1
+ import { SPREAD } from './path.js';
2
+ import { BLUR_FRAGMENT, COMPOSITE_FRAGMENT, DOWN_FRAGMENT, DUST_FRAGMENT, DUST_VERTEX, LINE_FRAGMENT, LINE_VERTEX, MAX_QUIET, QUAD_VERTEX, } from './shader.js';
3
+ export { MAX_QUIET };
2
4
  export const MAX_STRANDS = 340;
3
5
  export const MIN_STRANDS = 60;
4
6
  const SEGMENTS = 320;
5
7
  export const DUST = 600;
6
8
  export const MAX_DUST = DUST * 4;
7
- const SPREAD = 1.35;
8
9
  const BLOOM = 0.75;
9
10
  const GAIN = 1.615;
10
11
  export class StrandsRenderer {
@@ -172,6 +173,8 @@ export class StrandsRenderer {
172
173
  const { gl } = this;
173
174
  gl.useProgram(program.program);
174
175
  gl.uniform4fv(program.uniforms.get('uPts') ?? null, frame.points);
176
+ gl.uniform4fv(program.uniforms.get('uLen') ?? null, frame.lengths);
177
+ gl.uniform4fv(program.uniforms.get('uQuiet') ?? null, frame.quiet);
175
178
  gl.uniform1i(program.uniforms.get('uCount') ?? null, frame.count);
176
179
  gl.uniform2f(program.uniforms.get('uView') ?? null, frame.view[0], frame.view[1]);
177
180
  gl.uniform1f(program.uniforms.get('uScroll') ?? null, frame.scroll);
@@ -15,9 +15,15 @@ import { type StrandsVariant } from '../path.js';
15
15
  * `--helix-color-background`. On a light page the light becomes ink, because
16
16
  * light added to near-white cannot be seen.
17
17
  *
18
- * **Three forms, set with `variant`.** `bend` turns bright strands through one tight point, with its
19
- * arms opening to the right. `ribbon`, the default, is a broad, soft ribbon in
20
- * long curves. `waves` is sharp strands crossing the full width.
18
+ * **Four forms, set with `variant`.** `bend` turns bright strands through one
19
+ * tight point, with its arms opening to the right. `ribbon`, the default, is a
20
+ * broad, soft ribbon in long curves. `sweep` enters at a top corner, the one
21
+ * `at` is nearer, and crosses in one diagonal to the far side. `waves` is sharp
22
+ * strands crossing the full width.
23
+ *
24
+ * **Quiet behind words.** `quiet` names elements, such as a hero's words, that
25
+ * the light dims behind to a faint glow, so a bright ribbon can cross the page
26
+ * under them and they stay crisp.
21
27
  *
22
28
  * **In its own box, by default.** The element fills the box it is given, the
23
29
  * way an image does, and `at` places the bend's apex or the ribbon's axis across
@@ -103,9 +109,10 @@ export declare class HelixStrands extends HelixElement {
103
109
  */
104
110
  depth: number;
105
111
  /**
106
- * How differently each strand moves, from `0`, the whole ribbon rolling and
107
- * waving as one, to `1`, each strand at its own pace and angle, crossing
108
- * over the others. Clamped to that range.
112
+ * How differently the strands move across the ribbon, from `0`, the whole
113
+ * ribbon rolling and waving as one, to `1`, its edges flowing at their own
114
+ * pace past its middle. Neighbors always move together, so no strand leaves
115
+ * its lane. Clamped to that range.
109
116
  */
110
117
  drift: number;
111
118
  /**
@@ -116,6 +123,13 @@ export declare class HelixStrands extends HelixElement {
116
123
  dust: number;
117
124
  /** Overall brightness, as a multiplier on the light. */
118
125
  gain: number;
126
+ /**
127
+ * The ids of elements the light dims behind, separated by spaces, such as
128
+ * a hero's words. Inside each, the ribbon keeps a faint glow, so words over
129
+ * it stay crisp and nothing busy runs under them, and it eases back to full
130
+ * a short way past each edge. Up to four; ids that name nothing are skipped.
131
+ */
132
+ quiet?: string;
119
133
  /**
120
134
  * The share of strands drawn out of focus where the ribbon is wide, from `0`,
121
135
  * every strand sharp, to `1`, every one soft. Clamped to that range. A tight
@@ -165,6 +179,10 @@ export declare class HelixStrands extends HelixElement {
165
179
  /** The path, padded to the shader's array, and how many points are in use. */
166
180
  private readonly points;
167
181
  private count;
182
+ /** How far along the path each point is, in pixels, four to a slot as the shader reads them. */
183
+ private lengths;
184
+ /** The rects the light dims behind, in the path's pixels, from `quiet`. */
185
+ private readonly quietRects;
168
186
  /** In `travel`, the path's `y` at even steps of its parameter. */
169
187
  private table;
170
188
  /** In `travel`, the scroll position the ribbon is drawn at, easing toward the page's. */
@@ -223,6 +241,11 @@ export declare class HelixStrands extends HelixElement {
223
241
  * on any change that could move the path rather than working out which did.
224
242
  */
225
243
  private layout;
244
+ /**
245
+ * The rects of the elements `quiet` names: on the page in `travel`, where
246
+ * the path is laid in page pixels, and inside the box otherwise.
247
+ */
248
+ private measureQuiet;
226
249
  /** The stops inside, each with its target's place on the page. A stop naming nothing is skipped. */
227
250
  private placedStops;
228
251
  /**
@@ -9,8 +9,8 @@ import { customElement, drawsInSoftware, elementById, HelixElement, HelixStrands
9
9
  import { html } from 'lit';
10
10
  import { property } from 'lit/decorators.js';
11
11
  import { paletteOf, RESTING_PALETTE } from '../palette.js';
12
- import { boxPath, lookup, MAX_POINTS, pagePath, visibleRange, } from '../path.js';
13
- import { DUST, MAX_DUST, MAX_STRANDS, MIN_STRANDS, StrandsRenderer } from '../renderer.js';
12
+ import { arcLengths, boxPath, lookup, MAX_POINTS, pagePath, visibleRange, } from '../path.js';
13
+ import { DUST, MAX_DUST, MAX_QUIET, MAX_STRANDS, MIN_STRANDS, StrandsRenderer, } from '../renderer.js';
14
14
  import { styles } from './strands-root.css.js';
15
15
  const MAX_CANVAS_EDGE = 4096;
16
16
  const MAX_CANVAS_PIXELS = 4_194_304;
@@ -45,6 +45,8 @@ let HelixStrands = class HelixStrands extends HelixElement {
45
45
  this.box = { height: 1, width: 1 };
46
46
  this.points = new Float32Array(MAX_POINTS * 4);
47
47
  this.count = 0;
48
+ this.lengths = new Float32Array(MAX_POINTS);
49
+ this.quietRects = new Float32Array(MAX_QUIET * 4);
48
50
  this.table = new Float32Array(1);
49
51
  this.offset = 0;
50
52
  this.opened = false;
@@ -142,7 +144,10 @@ let HelixStrands = class HelixStrands extends HelixElement {
142
144
  if (changed.has('travel') && this.hasUpdated && this.canvas) {
143
145
  this.watchPage(this.travel);
144
146
  }
145
- if (changed.has('variant') || changed.has('at') || changed.has('travel')) {
147
+ if (changed.has('variant') ||
148
+ changed.has('at') ||
149
+ changed.has('travel') ||
150
+ changed.has('quiet')) {
146
151
  this.layout();
147
152
  }
148
153
  this.paintOnce();
@@ -316,6 +321,28 @@ let HelixStrands = class HelixStrands extends HelixElement {
316
321
  this.points.set(point, i * 4);
317
322
  });
318
323
  this.count = path.length;
324
+ this.lengths.fill(0);
325
+ this.lengths.set(arcLengths(path, this.box.width));
326
+ this.measureQuiet();
327
+ }
328
+ measureQuiet() {
329
+ this.quietRects.fill(0);
330
+ const origin = this.travel ? { left: 0, top: -window.scrollY } : this.getBoundingClientRect();
331
+ let slot = 0;
332
+ for (const name of (this.quiet ?? '').split(/\s+/)) {
333
+ const target = name ? elementById(this, name) : null;
334
+ if (!target || slot >= MAX_QUIET) {
335
+ continue;
336
+ }
337
+ const rect = target.getBoundingClientRect();
338
+ this.quietRects.set([
339
+ rect.left - origin.left,
340
+ rect.top - origin.top,
341
+ rect.right - origin.left,
342
+ rect.bottom - origin.top,
343
+ ], slot * 4);
344
+ slot += 1;
345
+ }
319
346
  }
320
347
  placedStops() {
321
348
  const placed = [];
@@ -332,8 +359,10 @@ let HelixStrands = class HelixStrands extends HelixElement {
332
359
  placed.push({
333
360
  at: Number.isFinite(at) ? at : undefined,
334
361
  height: rect.height,
362
+ left: this.box.width ? rect.left / this.box.width : undefined,
363
+ right: this.box.width ? rect.right / this.box.width : undefined,
335
364
  top: rect.top + scroll,
336
- variant: variant === 'bend' || variant === 'waves' ? variant : 'ribbon',
365
+ variant: variant === 'bend' || variant === 'sweep' || variant === 'waves' ? variant : 'ribbon',
337
366
  });
338
367
  }
339
368
  return placed;
@@ -380,7 +409,9 @@ let HelixStrands = class HelixStrands extends HelixElement {
380
409
  drift: share(this.drift),
381
410
  dust: Math.round(Math.min(MAX_DUST, Math.max(0, this.dust * DUST))),
382
411
  gain: Math.max(0, this.gain),
412
+ lengths: this.lengths,
383
413
  points: this.points,
414
+ quiet: this.quietRects,
384
415
  range,
385
416
  scroll: this.travel ? this.offset : 0,
386
417
  softness: Math.min(1, Math.max(0, this.softness)),
@@ -472,6 +503,9 @@ __decorate([
472
503
  __decorate([
473
504
  property({ type: Number })
474
505
  ], HelixStrands.prototype, "gain", void 0);
506
+ __decorate([
507
+ property()
508
+ ], HelixStrands.prototype, "quiet", void 0);
475
509
  __decorate([
476
510
  property({ type: Number })
477
511
  ], HelixStrands.prototype, "softness", void 0);