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