@fusion.dev/helix 0.181.1 → 0.182.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/custom-elements.json +213 -13
- package/dist/bar-chart/root/bar-chart-root.css.js +2 -2
- package/dist/strands/path.d.ts +51 -2
- package/dist/strands/path.js +255 -42
- package/dist/strands/renderer.d.ts +10 -0
- package/dist/strands/renderer.js +5 -2
- package/dist/strands/root/strands-root.d.ts +29 -6
- package/dist/strands/root/strands-root.js +38 -4
- package/dist/strands/shader.d.ts +5 -3
- package/dist/strands/shader.js +122 -18
- package/dist/strands/stop/strands-stop.d.ts +5 -3
- package/lint.json +1 -1
- package/package.json +6 -6
- package/types/jsx.d.ts +90 -24
- package/types/vue.d.ts +25 -5
package/dist/strands/path.js
CHANGED
|
@@ -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.
|
|
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
|
|
17
|
-
|
|
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
|
|
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 / (
|
|
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
|
-
|
|
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:
|
|
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 /
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
172
|
-
const
|
|
173
|
-
const
|
|
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
|
|
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
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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
|
|
354
|
+
const carries = joined !== undefined && ribbonAfter(stop.top + height * 0.5, placed[index + 1]);
|
|
194
355
|
const end = points.at(-1);
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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. */
|
package/dist/strands/renderer.js
CHANGED
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
import {
|
|
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
|
-
* **
|
|
19
|
-
* arms opening to the right. `ribbon`, the default, is a
|
|
20
|
-
* long curves. `
|
|
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
|
|
107
|
-
* waving as one, to `1`,
|
|
108
|
-
*
|
|
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') ||
|
|
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);
|