scroll-arrows 0.1.3 → 0.3.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/README.md +123 -5
- package/dist/{chunk-GKWBGFLA.js → chunk-GX722UDR.js} +314 -52
- package/dist/index.cjs +316 -49
- package/dist/index.d.cts +78 -3
- package/dist/index.d.ts +78 -3
- package/dist/index.js +6 -3
- package/dist/react.cjs +336 -49
- package/dist/react.d.cts +20 -2
- package/dist/react.d.ts +20 -2
- package/dist/react.js +25 -2
- package/dist/types-CgHPWybd.d.cts +196 -0
- package/dist/types-CgHPWybd.d.ts +196 -0
- package/package.json +1 -1
- package/dist/types-CDd8JqZX.d.cts +0 -104
- package/dist/types-CDd8JqZX.d.ts +0 -104
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { S as ScrollArrowOptions } from './types-
|
|
2
|
-
export { A as ArrowHead, E as ElementRef, P as Point,
|
|
1
|
+
import { S as ScrollArrowOptions, a as ScrollArrowGroupOptions } from './types-CgHPWybd.js';
|
|
2
|
+
export { A as ArrowHead, E as ElementRef, L as LabelPosition, P as Point, b as ScrollOptions, c as Socket } from './types-CgHPWybd.js';
|
|
3
3
|
|
|
4
4
|
/** A single hand-drawn arrow that draws itself between two elements on scroll. */
|
|
5
5
|
declare class ScrollArrow {
|
|
@@ -18,13 +18,38 @@ declare class ScrollArrow {
|
|
|
18
18
|
private segments;
|
|
19
19
|
/** Representative line stroke + label nodes, when a label is set. */
|
|
20
20
|
private lineEl;
|
|
21
|
+
/**
|
|
22
|
+
* The smooth ideal path `d` (pre-roughjs). Label placement measures against
|
|
23
|
+
* this, not `lineEl`: rough.js bakes its double stroke into one path with two
|
|
24
|
+
* subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
|
|
25
|
+
* put `labelAt` at twice its intended fraction.
|
|
26
|
+
*/
|
|
27
|
+
private lineD;
|
|
21
28
|
private labelEl;
|
|
22
29
|
private labelBgEl;
|
|
30
|
+
/** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
|
|
31
|
+
private resolvedLabelAt;
|
|
23
32
|
private progress;
|
|
24
33
|
private ro?;
|
|
34
|
+
/**
|
|
35
|
+
* Lazily armed only while an anchor is hidden/zero-size, so the common path
|
|
36
|
+
* (both anchors visible) wires no extra observer. Fires render() on reveal.
|
|
37
|
+
*/
|
|
38
|
+
private revealObs?;
|
|
25
39
|
private rafId;
|
|
26
40
|
private destroyed;
|
|
41
|
+
/** Render static + fully drawn: user prefers reduced motion and we honor it. */
|
|
42
|
+
private reducedMotion;
|
|
43
|
+
/** When false the arrow is hidden and skips all draw/scroll work (no teardown). */
|
|
44
|
+
private enabled;
|
|
27
45
|
constructor(options: ScrollArrowOptions);
|
|
46
|
+
/**
|
|
47
|
+
* Suspend or restore the arrow without tearing it down. Disabling hides it and
|
|
48
|
+
* stops all draw/scroll work; enabling shows it and recomputes geometry (so it
|
|
49
|
+
* reflects any layout change that happened while hidden). Idempotent. Wire it
|
|
50
|
+
* to `matchMedia` to switch arrows off below a breakpoint.
|
|
51
|
+
*/
|
|
52
|
+
setEnabled(on: boolean): void;
|
|
28
53
|
/** Manually set draw progress (0..1). Only meaningful when scroll is false. */
|
|
29
54
|
setProgress(p: number): void;
|
|
30
55
|
/** Recompute geometry (call after layout changes you control). */
|
|
@@ -44,14 +69,64 @@ declare class ScrollArrow {
|
|
|
44
69
|
* once the line is complete.
|
|
45
70
|
*/
|
|
46
71
|
private applyProgress;
|
|
72
|
+
/**
|
|
73
|
+
* Watch the anchors for the moment a hidden one becomes laid out, then redraw.
|
|
74
|
+
* IntersectionObserver fires when a previously `display:none` element gains a
|
|
75
|
+
* box — a transition ResizeObserver does not reliably report. Armed lazily and
|
|
76
|
+
* idempotently; torn down by the next successful render(). Guarded so it no-ops
|
|
77
|
+
* in environments without IntersectionObserver (older engines, some SSR/jsdom).
|
|
78
|
+
*/
|
|
79
|
+
private armReveal;
|
|
80
|
+
private teardownReveal;
|
|
81
|
+
private bind;
|
|
82
|
+
private onScroll;
|
|
83
|
+
private update;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* A coordinated set of arrows that draw in a staggered sequence off one shared
|
|
88
|
+
* scroll trigger (or manual `setProgress`). Owns its arrows; `destroy()` tears
|
|
89
|
+
* them all down.
|
|
90
|
+
*/
|
|
91
|
+
declare class ScrollArrowGroup {
|
|
92
|
+
private opts;
|
|
93
|
+
private arrows;
|
|
94
|
+
private windows;
|
|
95
|
+
private elements;
|
|
96
|
+
private target;
|
|
97
|
+
private progress;
|
|
98
|
+
private rafId;
|
|
99
|
+
private destroyed;
|
|
100
|
+
/** Render every arrow static + fully drawn when reduced motion is honored. */
|
|
101
|
+
private reducedMotion;
|
|
102
|
+
/** When false every arrow is hidden and the group does no scroll work. */
|
|
103
|
+
private enabled;
|
|
104
|
+
constructor(options: ScrollArrowGroupOptions);
|
|
105
|
+
/** Manually set group progress (0..1). Only meaningful when scroll is false. */
|
|
106
|
+
setProgress(p: number): void;
|
|
107
|
+
/** Recompute geometry for every arrow (call after layout changes). */
|
|
108
|
+
refresh(): void;
|
|
109
|
+
/**
|
|
110
|
+
* Suspend or restore the whole group without tearing it down. Hides every
|
|
111
|
+
* arrow and stops scroll work when off; restores and recomputes when on.
|
|
112
|
+
* Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
|
|
113
|
+
*/
|
|
114
|
+
setEnabled(on: boolean): void;
|
|
115
|
+
destroy(): void;
|
|
47
116
|
private bind;
|
|
48
117
|
private onScroll;
|
|
49
118
|
private update;
|
|
119
|
+
/** Push each arrow to its sliced local progress for the current group value. */
|
|
120
|
+
private distribute;
|
|
121
|
+
/** Synthetic rect spanning every endpoint, used as the default trigger. */
|
|
122
|
+
private groupRect;
|
|
50
123
|
}
|
|
51
124
|
|
|
52
125
|
declare function easeInOutCubic(t: number): number;
|
|
53
126
|
|
|
54
127
|
/** Convenience factory. `const a = scrollArrow({ start, end })`. */
|
|
55
128
|
declare function scrollArrow(options: ScrollArrowOptions): ScrollArrow;
|
|
129
|
+
/** Convenience factory. `const g = scrollArrowGroup({ arrows: [...] })`. */
|
|
130
|
+
declare function scrollArrowGroup(options: ScrollArrowGroupOptions): ScrollArrowGroup;
|
|
56
131
|
|
|
57
|
-
export { ScrollArrow, ScrollArrowOptions, easeInOutCubic, scrollArrow };
|
|
132
|
+
export { ScrollArrow, ScrollArrowGroup, ScrollArrowGroupOptions, ScrollArrowOptions, easeInOutCubic, scrollArrow, scrollArrowGroup };
|
package/dist/index.js
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
-
import { ScrollArrow } from './chunk-
|
|
2
|
-
export { ScrollArrow, easeInOutCubic } from './chunk-
|
|
1
|
+
import { ScrollArrow, ScrollArrowGroup } from './chunk-GX722UDR.js';
|
|
2
|
+
export { ScrollArrow, ScrollArrowGroup, easeInOutCubic } from './chunk-GX722UDR.js';
|
|
3
3
|
|
|
4
4
|
// src/index.ts
|
|
5
5
|
function scrollArrow(options) {
|
|
6
6
|
return new ScrollArrow(options);
|
|
7
7
|
}
|
|
8
|
+
function scrollArrowGroup(options) {
|
|
9
|
+
return new ScrollArrowGroup(options);
|
|
10
|
+
}
|
|
8
11
|
|
|
9
|
-
export { scrollArrow };
|
|
12
|
+
export { scrollArrow, scrollArrowGroup };
|
|
10
13
|
//# sourceMappingURL=index.js.map
|
|
11
14
|
//# sourceMappingURL=index.js.map
|
package/dist/react.cjs
CHANGED
|
@@ -25,20 +25,24 @@ function docRect(el) {
|
|
|
25
25
|
height: r2.height
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
|
+
function isDegenerateRect(r2) {
|
|
29
|
+
return r2.width <= 0 || r2.height <= 0;
|
|
30
|
+
}
|
|
28
31
|
function center(r2) {
|
|
29
32
|
return { x: r2.left + r2.width / 2, y: r2.top + r2.height / 2 };
|
|
30
33
|
}
|
|
31
|
-
function socketPoint(r2, side) {
|
|
34
|
+
function socketPoint(r2, side, offset = 0) {
|
|
32
35
|
const c = center(r2);
|
|
36
|
+
const o = offset < -0.5 ? -0.5 : offset > 0.5 ? 0.5 : offset;
|
|
33
37
|
switch (side) {
|
|
34
38
|
case "top":
|
|
35
|
-
return { x: c.x, y: r2.top };
|
|
39
|
+
return { x: c.x + o * r2.width, y: r2.top };
|
|
36
40
|
case "bottom":
|
|
37
|
-
return { x: c.x, y: r2.top + r2.height };
|
|
41
|
+
return { x: c.x + o * r2.width, y: r2.top + r2.height };
|
|
38
42
|
case "left":
|
|
39
|
-
return { x: r2.left, y: c.y };
|
|
43
|
+
return { x: r2.left, y: c.y + o * r2.height };
|
|
40
44
|
case "right":
|
|
41
|
-
return { x: r2.left + r2.width, y: c.y };
|
|
45
|
+
return { x: r2.left + r2.width, y: c.y + o * r2.height };
|
|
42
46
|
default:
|
|
43
47
|
return c;
|
|
44
48
|
}
|
|
@@ -71,12 +75,12 @@ function autoSide(self, other) {
|
|
|
71
75
|
}
|
|
72
76
|
return best;
|
|
73
77
|
}
|
|
74
|
-
function resolveEndpoints(startRect, endRect, startSocket, endSocket) {
|
|
78
|
+
function resolveEndpoints(startRect, endRect, startSocket, endSocket, startOffset = 0, endOffset = 0) {
|
|
75
79
|
const s = startSocket === "auto" ? autoSide(startRect, endRect) : startSocket;
|
|
76
80
|
const e = endSocket === "auto" ? autoSide(endRect, startRect) : endSocket;
|
|
77
81
|
return {
|
|
78
|
-
start: socketPoint(startRect, s),
|
|
79
|
-
end: socketPoint(endRect, e),
|
|
82
|
+
start: socketPoint(startRect, s, startOffset),
|
|
83
|
+
end: socketPoint(endRect, e, endOffset),
|
|
80
84
|
startNormal: socketNormal(s),
|
|
81
85
|
endNormal: socketNormal(e)
|
|
82
86
|
};
|
|
@@ -99,6 +103,26 @@ function buildPath(ep, curvature, belly = { x: 0, y: 0 }) {
|
|
|
99
103
|
};
|
|
100
104
|
return `M ${r(start.x)} ${r(start.y)} C ${r(c1.x)} ${r(c1.y)} ${r(c2.x)} ${r(c2.y)} ${r(end.x)} ${r(end.y)}`;
|
|
101
105
|
}
|
|
106
|
+
function buildElbowPath(ep) {
|
|
107
|
+
const { start: s, end: e, startNormal: sn, endNormal: en } = ep;
|
|
108
|
+
const dx = e.x - s.x;
|
|
109
|
+
const dy = e.y - s.y;
|
|
110
|
+
const startVertical = sn.y !== 0 || sn.x === 0 && Math.abs(dy) >= Math.abs(dx);
|
|
111
|
+
const endVertical = en.y !== 0 || en.x === 0 && Math.abs(dy) >= Math.abs(dx);
|
|
112
|
+
let pts;
|
|
113
|
+
if (startVertical && endVertical) {
|
|
114
|
+
const midY = (s.y + e.y) / 2;
|
|
115
|
+
pts = [s, { x: s.x, y: midY }, { x: e.x, y: midY }, e];
|
|
116
|
+
} else if (!startVertical && !endVertical) {
|
|
117
|
+
const midX = (s.x + e.x) / 2;
|
|
118
|
+
pts = [s, { x: midX, y: s.y }, { x: midX, y: e.y }, e];
|
|
119
|
+
} else if (startVertical) {
|
|
120
|
+
pts = [s, { x: s.x, y: e.y }, e];
|
|
121
|
+
} else {
|
|
122
|
+
pts = [s, { x: e.x, y: s.y }, e];
|
|
123
|
+
}
|
|
124
|
+
return `M ${r(pts[0].x)} ${r(pts[0].y)}` + pts.slice(1).map((p) => ` L ${r(p.x)} ${r(p.y)}`).join("");
|
|
125
|
+
}
|
|
102
126
|
function routeOffset(start, end, obstacles, padding = 14) {
|
|
103
127
|
const dx = end.x - start.x;
|
|
104
128
|
const dy = end.y - start.y;
|
|
@@ -174,12 +198,29 @@ function easeInOutCubic(t) {
|
|
|
174
198
|
function clamp01(t) {
|
|
175
199
|
return t < 0 ? 0 : t > 1 ? 1 : t;
|
|
176
200
|
}
|
|
201
|
+
function prefersReducedMotion() {
|
|
202
|
+
if (typeof window === "undefined" || typeof window.matchMedia !== "function") {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
206
|
+
}
|
|
177
207
|
function scrollProgress(targetRect, range) {
|
|
178
208
|
const vh = window.innerHeight || 1;
|
|
179
209
|
const topFrac = (targetRect.top - window.scrollY) / vh;
|
|
180
210
|
const [enter, leave] = range;
|
|
181
211
|
return clamp01((enter - topFrac) / (enter - leave || 1));
|
|
182
212
|
}
|
|
213
|
+
function staggerWindows(n, stagger) {
|
|
214
|
+
if (n <= 0) return [];
|
|
215
|
+
const s = clamp01(stagger);
|
|
216
|
+
const span = 1 / (1 + (n - 1) * s);
|
|
217
|
+
const step = span * s;
|
|
218
|
+
return Array.from({ length: n }, (_, i) => ({ start: i * step, span }));
|
|
219
|
+
}
|
|
220
|
+
function windowProgress(p, w) {
|
|
221
|
+
if (w.span <= 0) return p > w.start ? 1 : 0;
|
|
222
|
+
return clamp01((p - w.start) / w.span);
|
|
223
|
+
}
|
|
183
224
|
function midpointRect(a, b) {
|
|
184
225
|
const ra = docRect(a);
|
|
185
226
|
const rb = docRect(b);
|
|
@@ -269,6 +310,25 @@ function clamp012(t) {
|
|
|
269
310
|
}
|
|
270
311
|
|
|
271
312
|
// src/draw.ts
|
|
313
|
+
var LABEL_KEYWORDS = {
|
|
314
|
+
start: 0,
|
|
315
|
+
middle: 0.5,
|
|
316
|
+
end: 1
|
|
317
|
+
};
|
|
318
|
+
function resolveLabelAt(value, fallback = 0.5) {
|
|
319
|
+
if (value == null) return fallback;
|
|
320
|
+
if (typeof value === "number") {
|
|
321
|
+
return Number.isFinite(value) ? clamp01(value) : fallback;
|
|
322
|
+
}
|
|
323
|
+
const key = value.trim().toLowerCase();
|
|
324
|
+
const keyword = LABEL_KEYWORDS[key];
|
|
325
|
+
if (keyword !== void 0) return keyword;
|
|
326
|
+
if (key.endsWith("%")) {
|
|
327
|
+
const n = Number.parseFloat(key.slice(0, -1));
|
|
328
|
+
return Number.isFinite(n) ? clamp01(n / 100) : fallback;
|
|
329
|
+
}
|
|
330
|
+
return fallback;
|
|
331
|
+
}
|
|
272
332
|
function lengths(segs) {
|
|
273
333
|
let lineLen = 0;
|
|
274
334
|
let headLen = 0;
|
|
@@ -298,7 +358,8 @@ function lineProgress(segs, eased) {
|
|
|
298
358
|
return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
|
|
299
359
|
}
|
|
300
360
|
function labelOpacity(lineProg, labelAt, fade = 0.08) {
|
|
301
|
-
|
|
361
|
+
const start = Math.min(clamp01(labelAt), 1 - fade);
|
|
362
|
+
return clamp01((lineProg - start) / (fade || 1));
|
|
302
363
|
}
|
|
303
364
|
|
|
304
365
|
// src/scroll-arrow.ts
|
|
@@ -312,8 +373,17 @@ var ScrollArrow = class {
|
|
|
312
373
|
this.segments = [];
|
|
313
374
|
/** Representative line stroke + label nodes, when a label is set. */
|
|
314
375
|
this.lineEl = null;
|
|
376
|
+
/**
|
|
377
|
+
* The smooth ideal path `d` (pre-roughjs). Label placement measures against
|
|
378
|
+
* this, not `lineEl`: rough.js bakes its double stroke into one path with two
|
|
379
|
+
* subpaths, so `lineEl.getTotalLength()` is ~2x the visible curve and would
|
|
380
|
+
* put `labelAt` at twice its intended fraction.
|
|
381
|
+
*/
|
|
382
|
+
this.lineD = "";
|
|
315
383
|
this.labelEl = null;
|
|
316
384
|
this.labelBgEl = null;
|
|
385
|
+
/** `opts.labelAt` resolved to a 0..1 fraction; cached by renderLabel for the draw loop. */
|
|
386
|
+
this.resolvedLabelAt = 0.5;
|
|
317
387
|
this.rafId = 0;
|
|
318
388
|
this.destroyed = false;
|
|
319
389
|
this.onScroll = () => {
|
|
@@ -337,14 +407,36 @@ var ScrollArrow = class {
|
|
|
337
407
|
this.svg = getOverlay(this.container);
|
|
338
408
|
this.rc = rough__default.default.svg(this.svg);
|
|
339
409
|
this.svg.appendChild(this.group);
|
|
340
|
-
this.
|
|
410
|
+
this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
|
|
411
|
+
this.progress = this.reducedMotion ? 1 : clamp01(this.opts.progress);
|
|
341
412
|
this.refs = this.resolveRefs();
|
|
342
413
|
this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
|
|
343
414
|
this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
|
|
415
|
+
this.enabled = this.opts.enabled ?? true;
|
|
416
|
+
if (!this.enabled) this.group.style.display = "none";
|
|
344
417
|
this.render();
|
|
345
418
|
this.bind();
|
|
346
419
|
this.update();
|
|
347
420
|
}
|
|
421
|
+
/**
|
|
422
|
+
* Suspend or restore the arrow without tearing it down. Disabling hides it and
|
|
423
|
+
* stops all draw/scroll work; enabling shows it and recomputes geometry (so it
|
|
424
|
+
* reflects any layout change that happened while hidden). Idempotent. Wire it
|
|
425
|
+
* to `matchMedia` to switch arrows off below a breakpoint.
|
|
426
|
+
*/
|
|
427
|
+
setEnabled(on) {
|
|
428
|
+
if (on === this.enabled || this.destroyed) return;
|
|
429
|
+
this.enabled = on;
|
|
430
|
+
if (on) {
|
|
431
|
+
this.group.style.display = "";
|
|
432
|
+
this.render();
|
|
433
|
+
this.update();
|
|
434
|
+
} else {
|
|
435
|
+
this.group.style.display = "none";
|
|
436
|
+
cancelAnimationFrame(this.rafId);
|
|
437
|
+
this.rafId = 0;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
348
440
|
/** Manually set draw progress (0..1). Only meaningful when scroll is false. */
|
|
349
441
|
setProgress(p) {
|
|
350
442
|
this.progress = clamp01(p);
|
|
@@ -358,6 +450,7 @@ var ScrollArrow = class {
|
|
|
358
450
|
destroy() {
|
|
359
451
|
this.destroyed = true;
|
|
360
452
|
this.ro?.disconnect();
|
|
453
|
+
this.teardownReveal();
|
|
361
454
|
window.removeEventListener("scroll", this.onScroll, true);
|
|
362
455
|
window.removeEventListener("resize", this.onScroll);
|
|
363
456
|
cancelAnimationFrame(this.rafId);
|
|
@@ -383,17 +476,30 @@ var ScrollArrow = class {
|
|
|
383
476
|
const list = Array.isArray(a) ? a : [a];
|
|
384
477
|
return list.map(resolve).filter((el) => el !== null);
|
|
385
478
|
}
|
|
386
|
-
computeEndpoints() {
|
|
387
|
-
const sr = docRect(this.refs.start);
|
|
388
|
-
const er = docRect(this.refs.end);
|
|
479
|
+
computeEndpoints(sr, er) {
|
|
389
480
|
const ss = this.opts.startSocket ?? "auto";
|
|
390
481
|
const es = this.opts.endSocket ?? "auto";
|
|
391
|
-
return resolveEndpoints(
|
|
482
|
+
return resolveEndpoints(
|
|
483
|
+
sr,
|
|
484
|
+
er,
|
|
485
|
+
ss,
|
|
486
|
+
es,
|
|
487
|
+
this.opts.startSocketOffset ?? 0,
|
|
488
|
+
this.opts.endSocketOffset ?? 0
|
|
489
|
+
);
|
|
392
490
|
}
|
|
393
491
|
render() {
|
|
394
492
|
while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
|
|
395
493
|
this.segments = [];
|
|
396
|
-
|
|
494
|
+
if (!this.enabled) return;
|
|
495
|
+
const sr = docRect(this.refs.start);
|
|
496
|
+
const er = docRect(this.refs.end);
|
|
497
|
+
if (isDegenerateRect(sr) || isDegenerateRect(er)) {
|
|
498
|
+
this.armReveal();
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
this.teardownReveal();
|
|
502
|
+
const ep = this.computeEndpoints(sr, er);
|
|
397
503
|
const origin = overlayOrigin(this.svg);
|
|
398
504
|
const shift = (e) => ({
|
|
399
505
|
start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
|
|
@@ -410,24 +516,30 @@ var ScrollArrow = class {
|
|
|
410
516
|
this.seed,
|
|
411
517
|
this.opts.anchorEnds ?? true
|
|
412
518
|
);
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
519
|
+
let d;
|
|
520
|
+
if (this.opts.route === "elbow") {
|
|
521
|
+
d = buildElbowPath(local);
|
|
522
|
+
} else {
|
|
523
|
+
const obstacles = this.resolveAvoid().map((el) => {
|
|
524
|
+
const dr = docRect(el);
|
|
525
|
+
return {
|
|
526
|
+
left: dr.left - origin.x,
|
|
527
|
+
top: dr.top - origin.y,
|
|
528
|
+
width: dr.width,
|
|
529
|
+
height: dr.height
|
|
530
|
+
};
|
|
531
|
+
});
|
|
532
|
+
const clear = routeOffset(
|
|
533
|
+
local.start,
|
|
534
|
+
local.end,
|
|
535
|
+
obstacles,
|
|
536
|
+
this.opts.avoidPadding ?? 14
|
|
537
|
+
);
|
|
538
|
+
const BOW = 1.6;
|
|
539
|
+
const belly = { x: clear.x * BOW, y: clear.y * BOW };
|
|
540
|
+
d = buildPath(local, curvature, belly);
|
|
541
|
+
}
|
|
542
|
+
this.lineD = d;
|
|
431
543
|
this.appendDrawable(this.rc.path(d, roughOpts), "line");
|
|
432
544
|
const head = this.opts.head;
|
|
433
545
|
const size = this.opts.headSize;
|
|
@@ -464,25 +576,26 @@ var ScrollArrow = class {
|
|
|
464
576
|
this.labelEl = null;
|
|
465
577
|
this.labelBgEl = null;
|
|
466
578
|
const text = this.opts.label;
|
|
467
|
-
if (!text || !this.lineEl) return;
|
|
468
|
-
const
|
|
469
|
-
|
|
470
|
-
const
|
|
579
|
+
if (!text || !this.lineEl || !this.lineD) return;
|
|
580
|
+
const at = resolveLabelAt(this.opts.labelAt);
|
|
581
|
+
this.resolvedLabelAt = at;
|
|
582
|
+
const measure = createSvgEl("path");
|
|
583
|
+
measure.setAttribute("d", this.lineD);
|
|
584
|
+
this.group.appendChild(measure);
|
|
585
|
+
const total = measure.getTotalLength();
|
|
586
|
+
const pt = measure.getPointAtLength(at * total);
|
|
471
587
|
const offset = this.opts.labelOffset ?? 0;
|
|
472
588
|
let x = pt.x;
|
|
473
589
|
let y = pt.y;
|
|
474
590
|
if (offset && total > 0) {
|
|
475
591
|
const eps = Math.min(1, total / 2);
|
|
476
|
-
const before =
|
|
477
|
-
|
|
478
|
-
);
|
|
479
|
-
const after = this.lineEl.getPointAtLength(
|
|
480
|
-
Math.min(total, at * total + eps)
|
|
481
|
-
);
|
|
592
|
+
const before = measure.getPointAtLength(Math.max(0, at * total - eps));
|
|
593
|
+
const after = measure.getPointAtLength(Math.min(total, at * total + eps));
|
|
482
594
|
const n = unitNormal(before, after);
|
|
483
595
|
x += n.x * offset;
|
|
484
596
|
y += n.y * offset;
|
|
485
597
|
}
|
|
598
|
+
this.group.removeChild(measure);
|
|
486
599
|
const label = createSvgEl("text");
|
|
487
600
|
label.textContent = text;
|
|
488
601
|
label.setAttribute("x", String(x));
|
|
@@ -532,25 +645,43 @@ var ScrollArrow = class {
|
|
|
532
645
|
if (this.labelEl) {
|
|
533
646
|
const op = labelOpacity(
|
|
534
647
|
lineProgress(this.segments, eased),
|
|
535
|
-
this.
|
|
648
|
+
this.resolvedLabelAt
|
|
536
649
|
);
|
|
537
650
|
this.labelEl.style.opacity = String(op);
|
|
538
651
|
if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
|
|
539
652
|
}
|
|
540
653
|
}
|
|
654
|
+
/**
|
|
655
|
+
* Watch the anchors for the moment a hidden one becomes laid out, then redraw.
|
|
656
|
+
* IntersectionObserver fires when a previously `display:none` element gains a
|
|
657
|
+
* box — a transition ResizeObserver does not reliably report. Armed lazily and
|
|
658
|
+
* idempotently; torn down by the next successful render(). Guarded so it no-ops
|
|
659
|
+
* in environments without IntersectionObserver (older engines, some SSR/jsdom).
|
|
660
|
+
*/
|
|
661
|
+
armReveal() {
|
|
662
|
+
if (this.revealObs || this.destroyed) return;
|
|
663
|
+
if (typeof IntersectionObserver === "undefined") return;
|
|
664
|
+
this.revealObs = new IntersectionObserver(() => this.render());
|
|
665
|
+
this.revealObs.observe(this.refs.start);
|
|
666
|
+
this.revealObs.observe(this.refs.end);
|
|
667
|
+
}
|
|
668
|
+
teardownReveal() {
|
|
669
|
+
this.revealObs?.disconnect();
|
|
670
|
+
this.revealObs = void 0;
|
|
671
|
+
}
|
|
541
672
|
bind() {
|
|
542
673
|
const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
|
|
543
674
|
if (this.refs.target) targets.push(this.refs.target);
|
|
544
675
|
this.ro = new ResizeObserver(() => this.render());
|
|
545
676
|
targets.forEach((t) => this.ro.observe(t));
|
|
546
|
-
if (this.opts.scroll !== false) {
|
|
677
|
+
if (this.opts.scroll !== false && !this.reducedMotion) {
|
|
547
678
|
window.addEventListener("scroll", this.onScroll, true);
|
|
548
679
|
window.addEventListener("resize", this.onScroll);
|
|
549
680
|
}
|
|
550
681
|
}
|
|
551
682
|
update() {
|
|
552
|
-
if (this.destroyed) return;
|
|
553
|
-
if (this.opts.scroll === false) {
|
|
683
|
+
if (this.destroyed || !this.enabled) return;
|
|
684
|
+
if (this.opts.scroll === false || this.reducedMotion) {
|
|
554
685
|
this.applyProgress();
|
|
555
686
|
return;
|
|
556
687
|
}
|
|
@@ -568,8 +699,139 @@ function resolve(ref) {
|
|
|
568
699
|
function refKey(ref) {
|
|
569
700
|
return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
|
|
570
701
|
}
|
|
571
|
-
|
|
572
|
-
|
|
702
|
+
|
|
703
|
+
// src/group.ts
|
|
704
|
+
var ScrollArrowGroup = class {
|
|
705
|
+
constructor(options) {
|
|
706
|
+
this.elements = [];
|
|
707
|
+
this.target = null;
|
|
708
|
+
this.progress = 0;
|
|
709
|
+
this.rafId = 0;
|
|
710
|
+
this.destroyed = false;
|
|
711
|
+
this.onScroll = () => {
|
|
712
|
+
if (this.rafId) return;
|
|
713
|
+
this.rafId = requestAnimationFrame(() => {
|
|
714
|
+
this.rafId = 0;
|
|
715
|
+
this.update();
|
|
716
|
+
});
|
|
717
|
+
};
|
|
718
|
+
if (!options.arrows || options.arrows.length === 0) {
|
|
719
|
+
throw new Error("[scroll-arrows] group needs at least one arrow");
|
|
720
|
+
}
|
|
721
|
+
this.opts = {
|
|
722
|
+
stagger: 1,
|
|
723
|
+
speed: 1,
|
|
724
|
+
easing: (t) => t,
|
|
725
|
+
...options
|
|
726
|
+
};
|
|
727
|
+
this.reducedMotion = this.opts.respectReducedMotion !== false && prefersReducedMotion();
|
|
728
|
+
this.enabled = this.opts.enabled ?? true;
|
|
729
|
+
this.arrows = options.arrows.map(
|
|
730
|
+
(a) => new ScrollArrow({
|
|
731
|
+
...a,
|
|
732
|
+
scroll: false,
|
|
733
|
+
progress: 0,
|
|
734
|
+
respectReducedMotion: false,
|
|
735
|
+
enabled: this.enabled
|
|
736
|
+
})
|
|
737
|
+
);
|
|
738
|
+
this.windows = staggerWindows(this.arrows.length, this.opts.stagger);
|
|
739
|
+
for (const a of options.arrows) {
|
|
740
|
+
const s = resolve2(a.start);
|
|
741
|
+
const e = resolve2(a.end);
|
|
742
|
+
if (s) this.elements.push(s);
|
|
743
|
+
if (e) this.elements.push(e);
|
|
744
|
+
}
|
|
745
|
+
const scroll = this.opts.scroll;
|
|
746
|
+
if (scroll !== false && scroll?.target) {
|
|
747
|
+
this.target = resolve2(scroll.target);
|
|
748
|
+
}
|
|
749
|
+
if (this.reducedMotion) this.progress = 1;
|
|
750
|
+
this.bind();
|
|
751
|
+
this.update();
|
|
752
|
+
}
|
|
753
|
+
/** Manually set group progress (0..1). Only meaningful when scroll is false. */
|
|
754
|
+
setProgress(p) {
|
|
755
|
+
this.progress = clamp01(p);
|
|
756
|
+
this.distribute();
|
|
757
|
+
}
|
|
758
|
+
/** Recompute geometry for every arrow (call after layout changes). */
|
|
759
|
+
refresh() {
|
|
760
|
+
this.arrows.forEach((a) => a.refresh());
|
|
761
|
+
this.update();
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Suspend or restore the whole group without tearing it down. Hides every
|
|
765
|
+
* arrow and stops scroll work when off; restores and recomputes when on.
|
|
766
|
+
* Idempotent. Wire to `matchMedia` for breakpoint-aware diagrams.
|
|
767
|
+
*/
|
|
768
|
+
setEnabled(on) {
|
|
769
|
+
if (on === this.enabled || this.destroyed) return;
|
|
770
|
+
this.enabled = on;
|
|
771
|
+
this.arrows.forEach((a) => a.setEnabled(on));
|
|
772
|
+
if (on) {
|
|
773
|
+
this.bind();
|
|
774
|
+
this.update();
|
|
775
|
+
} else {
|
|
776
|
+
window.removeEventListener("scroll", this.onScroll, true);
|
|
777
|
+
window.removeEventListener("resize", this.onScroll);
|
|
778
|
+
cancelAnimationFrame(this.rafId);
|
|
779
|
+
this.rafId = 0;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
destroy() {
|
|
783
|
+
this.destroyed = true;
|
|
784
|
+
window.removeEventListener("scroll", this.onScroll, true);
|
|
785
|
+
window.removeEventListener("resize", this.onScroll);
|
|
786
|
+
cancelAnimationFrame(this.rafId);
|
|
787
|
+
this.arrows.forEach((a) => a.destroy());
|
|
788
|
+
}
|
|
789
|
+
// --- internals ---------------------------------------------------------
|
|
790
|
+
bind() {
|
|
791
|
+
if (this.opts.scroll === false || this.reducedMotion || !this.enabled)
|
|
792
|
+
return;
|
|
793
|
+
window.addEventListener("scroll", this.onScroll, true);
|
|
794
|
+
window.addEventListener("resize", this.onScroll);
|
|
795
|
+
}
|
|
796
|
+
update() {
|
|
797
|
+
if (this.destroyed || !this.enabled) return;
|
|
798
|
+
if (this.opts.scroll === false || this.reducedMotion) {
|
|
799
|
+
this.distribute();
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const scroll = this.opts.scroll ?? {};
|
|
803
|
+
const range = scroll.range ?? [0.85, 0.35];
|
|
804
|
+
const rect = this.target ? docRect(this.target) : this.groupRect();
|
|
805
|
+
const raw = scrollProgress(rect, range);
|
|
806
|
+
this.progress = clamp01(raw * this.opts.speed);
|
|
807
|
+
this.distribute();
|
|
808
|
+
}
|
|
809
|
+
/** Push each arrow to its sliced local progress for the current group value. */
|
|
810
|
+
distribute() {
|
|
811
|
+
const eased = clamp01(this.opts.easing(this.progress));
|
|
812
|
+
this.arrows.forEach((arrow, i) => {
|
|
813
|
+
arrow.setProgress(windowProgress(eased, this.windows[i]));
|
|
814
|
+
});
|
|
815
|
+
}
|
|
816
|
+
/** Synthetic rect spanning every endpoint, used as the default trigger. */
|
|
817
|
+
groupRect() {
|
|
818
|
+
const rects = this.elements.map(docRect);
|
|
819
|
+
if (rects.length === 0) return { left: 0, top: 0, width: 0, height: 0 };
|
|
820
|
+
let left = Infinity;
|
|
821
|
+
let top = Infinity;
|
|
822
|
+
let right = -Infinity;
|
|
823
|
+
let bottom = -Infinity;
|
|
824
|
+
for (const r2 of rects) {
|
|
825
|
+
left = Math.min(left, r2.left);
|
|
826
|
+
top = Math.min(top, r2.top);
|
|
827
|
+
right = Math.max(right, r2.left + r2.width);
|
|
828
|
+
bottom = Math.max(bottom, r2.top + r2.height);
|
|
829
|
+
}
|
|
830
|
+
return { left, top, width: right - left, height: bottom - top };
|
|
831
|
+
}
|
|
832
|
+
};
|
|
833
|
+
function resolve2(ref) {
|
|
834
|
+
return typeof ref === "string" ? document.querySelector(ref) : ref;
|
|
573
835
|
}
|
|
574
836
|
|
|
575
837
|
// src/react.tsx
|
|
@@ -597,8 +859,33 @@ function ScrollArrowLine(props) {
|
|
|
597
859
|
useScrollArrow(props);
|
|
598
860
|
return null;
|
|
599
861
|
}
|
|
862
|
+
function useScrollArrowGroup(options) {
|
|
863
|
+
const groupRef = react.useRef(null);
|
|
864
|
+
const { arrows, deps = [], ...rest } = options;
|
|
865
|
+
react.useEffect(() => {
|
|
866
|
+
const resolved = arrows.map((a) => {
|
|
867
|
+
const s = read(a.start);
|
|
868
|
+
const e = read(a.end);
|
|
869
|
+
if (!s || !e) return null;
|
|
870
|
+
return { ...a, start: s, end: e };
|
|
871
|
+
}).filter((a) => a !== null);
|
|
872
|
+
if (resolved.length === 0) return;
|
|
873
|
+
const group = new ScrollArrowGroup({ ...rest, arrows: resolved });
|
|
874
|
+
groupRef.current = group;
|
|
875
|
+
return () => {
|
|
876
|
+
group.destroy();
|
|
877
|
+
groupRef.current = null;
|
|
878
|
+
};
|
|
879
|
+
}, deps);
|
|
880
|
+
}
|
|
881
|
+
function ScrollArrowGroupLines(props) {
|
|
882
|
+
useScrollArrowGroup(props);
|
|
883
|
+
return null;
|
|
884
|
+
}
|
|
600
885
|
|
|
886
|
+
exports.ScrollArrowGroupLines = ScrollArrowGroupLines;
|
|
601
887
|
exports.ScrollArrowLine = ScrollArrowLine;
|
|
602
888
|
exports.useScrollArrow = useScrollArrow;
|
|
889
|
+
exports.useScrollArrowGroup = useScrollArrowGroup;
|
|
603
890
|
//# sourceMappingURL=react.cjs.map
|
|
604
891
|
//# sourceMappingURL=react.cjs.map
|