scroll-arrows 0.1.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/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/chunk-HLZXSGP5.js +557 -0
- package/dist/index.cjs +572 -0
- package/dist/index.d.cts +57 -0
- package/dist/index.d.ts +57 -0
- package/dist/index.js +11 -0
- package/dist/react.cjs +593 -0
- package/dist/react.d.cts +21 -0
- package/dist/react.d.ts +21 -0
- package/dist/react.js +31 -0
- package/dist/types-DehQP2Hx.d.cts +104 -0
- package/dist/types-DehQP2Hx.d.ts +104 -0
- package/package.json +84 -0
package/dist/react.cjs
ADDED
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var react = require('react');
|
|
4
|
+
var rough = require('roughjs');
|
|
5
|
+
|
|
6
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
7
|
+
|
|
8
|
+
var rough__default = /*#__PURE__*/_interopDefault(rough);
|
|
9
|
+
|
|
10
|
+
// src/react.tsx
|
|
11
|
+
|
|
12
|
+
// src/geometry.ts
|
|
13
|
+
var SIDES = [
|
|
14
|
+
"top",
|
|
15
|
+
"bottom",
|
|
16
|
+
"left",
|
|
17
|
+
"right"
|
|
18
|
+
];
|
|
19
|
+
function docRect(el) {
|
|
20
|
+
const r2 = el.getBoundingClientRect();
|
|
21
|
+
return {
|
|
22
|
+
left: r2.left + window.scrollX,
|
|
23
|
+
top: r2.top + window.scrollY,
|
|
24
|
+
width: r2.width,
|
|
25
|
+
height: r2.height
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function center(r2) {
|
|
29
|
+
return { x: r2.left + r2.width / 2, y: r2.top + r2.height / 2 };
|
|
30
|
+
}
|
|
31
|
+
function socketPoint(r2, side) {
|
|
32
|
+
const c = center(r2);
|
|
33
|
+
switch (side) {
|
|
34
|
+
case "top":
|
|
35
|
+
return { x: c.x, y: r2.top };
|
|
36
|
+
case "bottom":
|
|
37
|
+
return { x: c.x, y: r2.top + r2.height };
|
|
38
|
+
case "left":
|
|
39
|
+
return { x: r2.left, y: c.y };
|
|
40
|
+
case "right":
|
|
41
|
+
return { x: r2.left + r2.width, y: c.y };
|
|
42
|
+
default:
|
|
43
|
+
return c;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
function socketNormal(side) {
|
|
47
|
+
switch (side) {
|
|
48
|
+
case "top":
|
|
49
|
+
return { x: 0, y: -1 };
|
|
50
|
+
case "bottom":
|
|
51
|
+
return { x: 0, y: 1 };
|
|
52
|
+
case "left":
|
|
53
|
+
return { x: -1, y: 0 };
|
|
54
|
+
case "right":
|
|
55
|
+
return { x: 1, y: 0 };
|
|
56
|
+
default:
|
|
57
|
+
return { x: 0, y: 0 };
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function autoSide(self, other) {
|
|
61
|
+
const target = center(other);
|
|
62
|
+
let best = "right";
|
|
63
|
+
let bestDist = Infinity;
|
|
64
|
+
for (const side of SIDES) {
|
|
65
|
+
const p = socketPoint(self, side);
|
|
66
|
+
const d = (p.x - target.x) ** 2 + (p.y - target.y) ** 2;
|
|
67
|
+
if (d < bestDist) {
|
|
68
|
+
bestDist = d;
|
|
69
|
+
best = side;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return best;
|
|
73
|
+
}
|
|
74
|
+
function resolveEndpoints(startRect, endRect, startSocket, endSocket) {
|
|
75
|
+
const s = startSocket === "auto" ? autoSide(startRect, endRect) : startSocket;
|
|
76
|
+
const e = endSocket === "auto" ? autoSide(endRect, startRect) : endSocket;
|
|
77
|
+
return {
|
|
78
|
+
start: socketPoint(startRect, s),
|
|
79
|
+
end: socketPoint(endRect, e),
|
|
80
|
+
startNormal: socketNormal(s),
|
|
81
|
+
endNormal: socketNormal(e)
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function buildPath(ep, curvature, belly = { x: 0, y: 0 }) {
|
|
85
|
+
const { start, end, startNormal, endNormal } = ep;
|
|
86
|
+
const dx = end.x - start.x;
|
|
87
|
+
const dy = end.y - start.y;
|
|
88
|
+
const dist = Math.hypot(dx, dy) || 1;
|
|
89
|
+
const reach = dist * (0.3 + curvature * 0.4);
|
|
90
|
+
const sn = startNormal.x || startNormal.y ? startNormal : unit(dx, dy);
|
|
91
|
+
const en = endNormal.x || endNormal.y ? endNormal : unit(-dx, -dy);
|
|
92
|
+
const c1 = { x: start.x + sn.x * reach + belly.x, y: start.y + sn.y * reach + belly.y };
|
|
93
|
+
const c2 = { x: end.x + en.x * reach + belly.x, y: end.y + en.y * reach + belly.y };
|
|
94
|
+
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)}`;
|
|
95
|
+
}
|
|
96
|
+
function routeOffset(start, end, obstacles, padding = 14) {
|
|
97
|
+
const dx = end.x - start.x;
|
|
98
|
+
const dy = end.y - start.y;
|
|
99
|
+
const len = Math.hypot(dx, dy) || 1;
|
|
100
|
+
const t = { x: dx / len, y: dy / len };
|
|
101
|
+
const n = { x: dy / len, y: -dx / len };
|
|
102
|
+
let best = { x: 0, y: 0 };
|
|
103
|
+
let bestMag = 0;
|
|
104
|
+
for (const b of obstacles) {
|
|
105
|
+
const cx = b.left + b.width / 2;
|
|
106
|
+
const cy = b.top + b.height / 2;
|
|
107
|
+
const relx = cx - start.x;
|
|
108
|
+
const rely = cy - start.y;
|
|
109
|
+
const u = (relx * t.x + rely * t.y) / len;
|
|
110
|
+
if (u < 0 || u > 1) continue;
|
|
111
|
+
const signed = relx * n.x + rely * n.y;
|
|
112
|
+
const radius = Math.abs(b.width / 2 * n.x) + Math.abs(b.height / 2 * n.y);
|
|
113
|
+
const clearance = radius + padding;
|
|
114
|
+
if (Math.abs(signed) >= clearance) continue;
|
|
115
|
+
const sign = signed > 0 ? -1 : 1;
|
|
116
|
+
const offset = signed + sign * clearance;
|
|
117
|
+
if (Math.abs(offset) > bestMag) {
|
|
118
|
+
bestMag = Math.abs(offset);
|
|
119
|
+
best = { x: n.x * offset, y: n.y * offset };
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return best;
|
|
123
|
+
}
|
|
124
|
+
function arrowHeadPath(tip, dir, size) {
|
|
125
|
+
const a = Math.atan2(dir.y, dir.x);
|
|
126
|
+
const spread = Math.PI / 7;
|
|
127
|
+
const p1 = {
|
|
128
|
+
x: tip.x - size * Math.cos(a - spread),
|
|
129
|
+
y: tip.y - size * Math.sin(a - spread)
|
|
130
|
+
};
|
|
131
|
+
const p2 = {
|
|
132
|
+
x: tip.x - size * Math.cos(a + spread),
|
|
133
|
+
y: tip.y - size * Math.sin(a + spread)
|
|
134
|
+
};
|
|
135
|
+
return `M ${r(p1.x)} ${r(p1.y)} L ${r(tip.x)} ${r(tip.y)} L ${r(p2.x)} ${r(p2.y)}`;
|
|
136
|
+
}
|
|
137
|
+
function endTangent(ep) {
|
|
138
|
+
const { end, endNormal } = ep;
|
|
139
|
+
if (endNormal.x || endNormal.y)
|
|
140
|
+
return { x: -endNormal.x + 0, y: -endNormal.y + 0 };
|
|
141
|
+
return unit(end.x - ep.start.x, end.y - ep.start.y);
|
|
142
|
+
}
|
|
143
|
+
function startTangent(ep) {
|
|
144
|
+
const { startNormal } = ep;
|
|
145
|
+
if (startNormal.x || startNormal.y)
|
|
146
|
+
return { x: -startNormal.x + 0, y: -startNormal.y + 0 };
|
|
147
|
+
return unit(ep.start.x - ep.end.x, ep.start.y - ep.end.y);
|
|
148
|
+
}
|
|
149
|
+
function unitNormal(a, b) {
|
|
150
|
+
const dx = b.x - a.x;
|
|
151
|
+
const dy = b.y - a.y;
|
|
152
|
+
const m = Math.hypot(dx, dy);
|
|
153
|
+
if (m === 0) return { x: 0, y: 0 };
|
|
154
|
+
return { x: dy / m + 0, y: -dx / m + 0 };
|
|
155
|
+
}
|
|
156
|
+
function unit(x, y) {
|
|
157
|
+
const m = Math.hypot(x, y) || 1;
|
|
158
|
+
return { x: x / m, y: y / m };
|
|
159
|
+
}
|
|
160
|
+
function r(n) {
|
|
161
|
+
return Math.round(n * 100) / 100;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/progress.ts
|
|
165
|
+
function easeInOutCubic(t) {
|
|
166
|
+
return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
|
|
167
|
+
}
|
|
168
|
+
function clamp01(t) {
|
|
169
|
+
return t < 0 ? 0 : t > 1 ? 1 : t;
|
|
170
|
+
}
|
|
171
|
+
function scrollProgress(targetRect, range) {
|
|
172
|
+
const vh = window.innerHeight || 1;
|
|
173
|
+
const topFrac = (targetRect.top - window.scrollY) / vh;
|
|
174
|
+
const [enter, leave] = range;
|
|
175
|
+
return clamp01((enter - topFrac) / (enter - leave || 1));
|
|
176
|
+
}
|
|
177
|
+
function midpointRect(a, b) {
|
|
178
|
+
const ra = docRect(a);
|
|
179
|
+
const rb = docRect(b);
|
|
180
|
+
const ca = { x: ra.left + ra.width / 2, y: ra.top + ra.height / 2 };
|
|
181
|
+
const cb = { x: rb.left + rb.width / 2, y: rb.top + rb.height / 2 };
|
|
182
|
+
return {
|
|
183
|
+
left: (ca.x + cb.x) / 2,
|
|
184
|
+
top: (ca.y + cb.y) / 2,
|
|
185
|
+
width: 0,
|
|
186
|
+
height: 0
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// src/overlay.ts
|
|
191
|
+
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
192
|
+
var overlays = /* @__PURE__ */ new WeakMap();
|
|
193
|
+
function getOverlay(container) {
|
|
194
|
+
var _a;
|
|
195
|
+
let svg = overlays.get(container);
|
|
196
|
+
if (svg && svg.isConnected) return svg;
|
|
197
|
+
svg = document.createElementNS(SVG_NS, "svg");
|
|
198
|
+
svg.setAttribute("data-scroll-arrows", "");
|
|
199
|
+
Object.assign(svg.style, {
|
|
200
|
+
position: "absolute",
|
|
201
|
+
left: "0",
|
|
202
|
+
top: "0",
|
|
203
|
+
width: "100%",
|
|
204
|
+
height: "100%",
|
|
205
|
+
overflow: "visible",
|
|
206
|
+
pointerEvents: "none",
|
|
207
|
+
zIndex: "9999"
|
|
208
|
+
});
|
|
209
|
+
if (container === document.body) {
|
|
210
|
+
(_a = document.body.style).position || (_a.position = "relative");
|
|
211
|
+
} else {
|
|
212
|
+
const pos = getComputedStyle(container).position;
|
|
213
|
+
if (pos === "static") container.style.position = "relative";
|
|
214
|
+
}
|
|
215
|
+
container.appendChild(svg);
|
|
216
|
+
overlays.set(container, svg);
|
|
217
|
+
return svg;
|
|
218
|
+
}
|
|
219
|
+
function overlayOrigin(svg) {
|
|
220
|
+
const r2 = svg.getBoundingClientRect();
|
|
221
|
+
return { x: r2.left + window.scrollX, y: r2.top + window.scrollY };
|
|
222
|
+
}
|
|
223
|
+
function createGroup() {
|
|
224
|
+
return document.createElementNS(SVG_NS, "g");
|
|
225
|
+
}
|
|
226
|
+
function createSvgEl(tag) {
|
|
227
|
+
return document.createElementNS(SVG_NS, tag);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// src/roughness.ts
|
|
231
|
+
function mapRoughness(roughness, curvatureOverride, stroke, strokeWidth, seed, anchorEnds = true) {
|
|
232
|
+
const r2 = clamp012(roughness);
|
|
233
|
+
return {
|
|
234
|
+
curvature: curvatureOverride ?? r2 * 0.6,
|
|
235
|
+
rough: {
|
|
236
|
+
roughness: r2 * 3.5,
|
|
237
|
+
bowing: r2 * 3,
|
|
238
|
+
maxRandomnessOffset: r2 * 4,
|
|
239
|
+
// Clean end of the spectrum: a single, near-exact stroke.
|
|
240
|
+
disableMultiStroke: r2 < 0.15,
|
|
241
|
+
// Pin endpoints to the sockets so the arrow always lands on its anchors,
|
|
242
|
+
// even when the middle of the stroke gets scratchy.
|
|
243
|
+
preserveVertices: anchorEnds,
|
|
244
|
+
stroke,
|
|
245
|
+
strokeWidth,
|
|
246
|
+
seed: seed | 0,
|
|
247
|
+
fill: "none"
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function deriveSeed(a, b) {
|
|
252
|
+
let h = 2166136261;
|
|
253
|
+
const s = a + "->" + b;
|
|
254
|
+
for (let i = 0; i < s.length; i++) {
|
|
255
|
+
h ^= s.charCodeAt(i);
|
|
256
|
+
h = Math.imul(h, 16777619);
|
|
257
|
+
}
|
|
258
|
+
return (h >>> 0) % 1e5;
|
|
259
|
+
}
|
|
260
|
+
function clamp012(t) {
|
|
261
|
+
return t < 0 ? 0 : t > 1 ? 1 : t;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// src/draw.ts
|
|
265
|
+
function lengths(segs) {
|
|
266
|
+
let lineLen = 0;
|
|
267
|
+
let headLen = 0;
|
|
268
|
+
for (const s of segs) {
|
|
269
|
+
if (s.kind === "line") lineLen = Math.max(lineLen, s.len);
|
|
270
|
+
else headLen += s.len;
|
|
271
|
+
}
|
|
272
|
+
return { lineLen, headLen };
|
|
273
|
+
}
|
|
274
|
+
function dashOffsets(segs, eased) {
|
|
275
|
+
const { lineLen, headLen } = lengths(segs);
|
|
276
|
+
const total = lineLen + headLen || 1;
|
|
277
|
+
const drawn = clamp01(eased) * total;
|
|
278
|
+
const lp = lineLen > 0 ? clamp01(drawn / lineLen) : 1;
|
|
279
|
+
let headDrawn = Math.max(0, drawn - lineLen);
|
|
280
|
+
return segs.map((seg) => {
|
|
281
|
+
if (seg.kind === "line") return seg.len * (1 - lp);
|
|
282
|
+
const show = Math.max(0, Math.min(seg.len, headDrawn));
|
|
283
|
+
headDrawn -= seg.len;
|
|
284
|
+
return seg.len - show;
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
function lineProgress(segs, eased) {
|
|
288
|
+
const { lineLen, headLen } = lengths(segs);
|
|
289
|
+
const total = lineLen + headLen || 1;
|
|
290
|
+
const drawn = clamp01(eased) * total;
|
|
291
|
+
return lineLen > 0 ? clamp01(drawn / lineLen) : 1;
|
|
292
|
+
}
|
|
293
|
+
function labelOpacity(lineProg, labelAt, fade = 0.08) {
|
|
294
|
+
return clamp01((lineProg - clamp01(labelAt)) / (fade || 1));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// src/scroll-arrow.ts
|
|
298
|
+
var ScrollArrow = class {
|
|
299
|
+
constructor(options) {
|
|
300
|
+
this.group = createGroup();
|
|
301
|
+
/**
|
|
302
|
+
* Drawable segments. Line strokes (rough.js emits 1-2 overlapping ones) share
|
|
303
|
+
* a leading edge so they grow as a single pen tip; heads draw after the line.
|
|
304
|
+
*/
|
|
305
|
+
this.segments = [];
|
|
306
|
+
/** Representative line stroke + label nodes, when a label is set. */
|
|
307
|
+
this.lineEl = null;
|
|
308
|
+
this.labelEl = null;
|
|
309
|
+
this.labelBgEl = null;
|
|
310
|
+
this.rafId = 0;
|
|
311
|
+
this.destroyed = false;
|
|
312
|
+
this.onScroll = () => {
|
|
313
|
+
if (this.rafId) return;
|
|
314
|
+
this.rafId = requestAnimationFrame(() => {
|
|
315
|
+
this.rafId = 0;
|
|
316
|
+
this.update();
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
this.opts = {
|
|
320
|
+
roughness: 0.5,
|
|
321
|
+
strokeWidth: 2,
|
|
322
|
+
head: "end",
|
|
323
|
+
headSize: 14,
|
|
324
|
+
speed: 1,
|
|
325
|
+
easing: easeInOutCubic,
|
|
326
|
+
progress: 0,
|
|
327
|
+
...options
|
|
328
|
+
};
|
|
329
|
+
this.container = options.container ?? document.body;
|
|
330
|
+
this.svg = getOverlay(this.container);
|
|
331
|
+
this.rc = rough__default.default.svg(this.svg);
|
|
332
|
+
this.svg.appendChild(this.group);
|
|
333
|
+
this.progress = clamp01(this.opts.progress);
|
|
334
|
+
this.refs = this.resolveRefs();
|
|
335
|
+
this.stroke = options.stroke ?? getComputedStyle(this.container).color ?? "#222";
|
|
336
|
+
this.seed = options.seed ?? deriveSeed(refKey(options.start), refKey(options.end));
|
|
337
|
+
this.render();
|
|
338
|
+
this.bind();
|
|
339
|
+
this.update();
|
|
340
|
+
}
|
|
341
|
+
/** Manually set draw progress (0..1). Only meaningful when scroll is false. */
|
|
342
|
+
setProgress(p) {
|
|
343
|
+
this.progress = clamp01(p);
|
|
344
|
+
this.applyProgress();
|
|
345
|
+
}
|
|
346
|
+
/** Recompute geometry (call after layout changes you control). */
|
|
347
|
+
refresh() {
|
|
348
|
+
this.render();
|
|
349
|
+
this.update();
|
|
350
|
+
}
|
|
351
|
+
destroy() {
|
|
352
|
+
this.destroyed = true;
|
|
353
|
+
this.ro?.disconnect();
|
|
354
|
+
window.removeEventListener("scroll", this.onScroll, true);
|
|
355
|
+
window.removeEventListener("resize", this.onScroll);
|
|
356
|
+
cancelAnimationFrame(this.rafId);
|
|
357
|
+
this.group.remove();
|
|
358
|
+
}
|
|
359
|
+
// --- internals ---------------------------------------------------------
|
|
360
|
+
resolveRefs() {
|
|
361
|
+
const start = resolve(this.opts.start);
|
|
362
|
+
const end = resolve(this.opts.end);
|
|
363
|
+
if (!start || !end) {
|
|
364
|
+
throw new Error("[scroll-arrows] start/end element not found");
|
|
365
|
+
}
|
|
366
|
+
const scroll = this.opts.scroll;
|
|
367
|
+
let target = null;
|
|
368
|
+
if (scroll && scroll !== void 0 && scroll.target) {
|
|
369
|
+
target = resolve(scroll.target);
|
|
370
|
+
}
|
|
371
|
+
return { start, end, target };
|
|
372
|
+
}
|
|
373
|
+
resolveAvoid() {
|
|
374
|
+
const a = this.opts.avoid;
|
|
375
|
+
if (!a) return [];
|
|
376
|
+
const list = Array.isArray(a) ? a : [a];
|
|
377
|
+
return list.map(resolve).filter((el) => el !== null);
|
|
378
|
+
}
|
|
379
|
+
computeEndpoints() {
|
|
380
|
+
const sr = docRect(this.refs.start);
|
|
381
|
+
const er = docRect(this.refs.end);
|
|
382
|
+
const ss = this.opts.startSocket ?? "auto";
|
|
383
|
+
const es = this.opts.endSocket ?? "auto";
|
|
384
|
+
return resolveEndpoints(sr, er, ss, es);
|
|
385
|
+
}
|
|
386
|
+
render() {
|
|
387
|
+
while (this.group.firstChild) this.group.removeChild(this.group.firstChild);
|
|
388
|
+
this.segments = [];
|
|
389
|
+
const ep = this.computeEndpoints();
|
|
390
|
+
const origin = overlayOrigin(this.svg);
|
|
391
|
+
const shift = (e) => ({
|
|
392
|
+
start: { x: e.start.x - origin.x, y: e.start.y - origin.y },
|
|
393
|
+
end: { x: e.end.x - origin.x, y: e.end.y - origin.y },
|
|
394
|
+
startNormal: e.startNormal,
|
|
395
|
+
endNormal: e.endNormal
|
|
396
|
+
});
|
|
397
|
+
const local = shift(ep);
|
|
398
|
+
const { rough: roughOpts, curvature } = mapRoughness(
|
|
399
|
+
this.opts.roughness,
|
|
400
|
+
this.opts.curvature,
|
|
401
|
+
this.stroke,
|
|
402
|
+
this.opts.strokeWidth,
|
|
403
|
+
this.seed,
|
|
404
|
+
this.opts.anchorEnds ?? true
|
|
405
|
+
);
|
|
406
|
+
const obstacles = this.resolveAvoid().map((el) => {
|
|
407
|
+
const dr = docRect(el);
|
|
408
|
+
return {
|
|
409
|
+
left: dr.left - origin.x,
|
|
410
|
+
top: dr.top - origin.y,
|
|
411
|
+
width: dr.width,
|
|
412
|
+
height: dr.height
|
|
413
|
+
};
|
|
414
|
+
});
|
|
415
|
+
const clear = routeOffset(
|
|
416
|
+
local.start,
|
|
417
|
+
local.end,
|
|
418
|
+
obstacles,
|
|
419
|
+
this.opts.avoidPadding ?? 14
|
|
420
|
+
);
|
|
421
|
+
const BOW = 1.6;
|
|
422
|
+
const belly = { x: clear.x * BOW, y: clear.y * BOW };
|
|
423
|
+
const d = buildPath(local, curvature, belly);
|
|
424
|
+
this.appendDrawable(this.rc.path(d, roughOpts), "line");
|
|
425
|
+
const head = this.opts.head;
|
|
426
|
+
const size = this.opts.headSize;
|
|
427
|
+
if (head === "end" || head === "both") {
|
|
428
|
+
const dir = endTangent(local);
|
|
429
|
+
this.appendDrawable(
|
|
430
|
+
this.rc.path(arrowHeadPath(local.end, dir, size), roughOpts),
|
|
431
|
+
"head"
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
if (head === "start" || head === "both") {
|
|
435
|
+
const dir = startTangent(local);
|
|
436
|
+
this.appendDrawable(
|
|
437
|
+
this.rc.path(arrowHeadPath(local.start, dir, size), roughOpts),
|
|
438
|
+
"head"
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
this.lineEl = null;
|
|
442
|
+
let longest = 0;
|
|
443
|
+
for (const seg of this.segments) {
|
|
444
|
+
seg.len = seg.el.getTotalLength();
|
|
445
|
+
seg.el.style.strokeDasharray = String(seg.len);
|
|
446
|
+
seg.el.style.strokeDashoffset = String(seg.len);
|
|
447
|
+
if (seg.kind === "line" && seg.len >= longest) {
|
|
448
|
+
longest = seg.len;
|
|
449
|
+
this.lineEl = seg.el;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
this.renderLabel();
|
|
453
|
+
this.applyProgress();
|
|
454
|
+
}
|
|
455
|
+
/** Place the label at a point along the line, with a masking background. */
|
|
456
|
+
renderLabel() {
|
|
457
|
+
this.labelEl = null;
|
|
458
|
+
this.labelBgEl = null;
|
|
459
|
+
const text = this.opts.label;
|
|
460
|
+
if (!text || !this.lineEl) return;
|
|
461
|
+
const total = this.lineEl.getTotalLength();
|
|
462
|
+
const at = clampAt(this.opts.labelAt ?? 0.5);
|
|
463
|
+
const pt = this.lineEl.getPointAtLength(at * total);
|
|
464
|
+
const offset = this.opts.labelOffset ?? 0;
|
|
465
|
+
let x = pt.x;
|
|
466
|
+
let y = pt.y;
|
|
467
|
+
if (offset && total > 0) {
|
|
468
|
+
const eps = Math.min(1, total / 2);
|
|
469
|
+
const before = this.lineEl.getPointAtLength(Math.max(0, at * total - eps));
|
|
470
|
+
const after = this.lineEl.getPointAtLength(Math.min(total, at * total + eps));
|
|
471
|
+
const n = unitNormal(before, after);
|
|
472
|
+
x += n.x * offset;
|
|
473
|
+
y += n.y * offset;
|
|
474
|
+
}
|
|
475
|
+
const label = createSvgEl("text");
|
|
476
|
+
label.textContent = text;
|
|
477
|
+
label.setAttribute("x", String(x));
|
|
478
|
+
label.setAttribute("y", String(y));
|
|
479
|
+
label.setAttribute("text-anchor", "middle");
|
|
480
|
+
label.setAttribute("dominant-baseline", "central");
|
|
481
|
+
label.style.font = this.opts.font ?? "600 16px sans-serif";
|
|
482
|
+
label.style.fill = this.opts.labelColor ?? this.stroke;
|
|
483
|
+
this.group.appendChild(label);
|
|
484
|
+
const bgColor = this.opts.labelBackground ?? getComputedStyle(this.container).backgroundColor;
|
|
485
|
+
if (bgColor && bgColor !== "none") {
|
|
486
|
+
const pad = 6;
|
|
487
|
+
const box = label.getBBox();
|
|
488
|
+
const rect = createSvgEl("rect");
|
|
489
|
+
rect.setAttribute("x", String(box.x - pad));
|
|
490
|
+
rect.setAttribute("y", String(box.y - pad / 2));
|
|
491
|
+
rect.setAttribute("width", String(box.width + pad * 2));
|
|
492
|
+
rect.setAttribute("height", String(box.height + pad));
|
|
493
|
+
rect.setAttribute("rx", "4");
|
|
494
|
+
rect.setAttribute("fill", bgColor);
|
|
495
|
+
this.group.insertBefore(rect, label);
|
|
496
|
+
this.labelBgEl = rect;
|
|
497
|
+
}
|
|
498
|
+
this.labelEl = label;
|
|
499
|
+
}
|
|
500
|
+
/** roughjs returns a <g> of one or more <path>; collect them in order. */
|
|
501
|
+
appendDrawable(g, kind) {
|
|
502
|
+
const paths = g.querySelectorAll("path");
|
|
503
|
+
paths.forEach((p) => {
|
|
504
|
+
const el = p;
|
|
505
|
+
el.setAttribute("fill", "none");
|
|
506
|
+
this.group.appendChild(el);
|
|
507
|
+
this.segments.push({ el, len: 0, kind });
|
|
508
|
+
});
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Reveal the line as one growing pen tip (all line sub-strokes advance to the
|
|
512
|
+
* same leading fraction together), then draw the arrowhead(s) sequentially
|
|
513
|
+
* once the line is complete.
|
|
514
|
+
*/
|
|
515
|
+
applyProgress() {
|
|
516
|
+
const eased = this.opts.easing(clamp01(this.progress));
|
|
517
|
+
const offsets = dashOffsets(this.segments, eased);
|
|
518
|
+
this.segments.forEach((seg, i) => {
|
|
519
|
+
seg.el.style.strokeDashoffset = String(offsets[i]);
|
|
520
|
+
});
|
|
521
|
+
if (this.labelEl) {
|
|
522
|
+
const op = labelOpacity(
|
|
523
|
+
lineProgress(this.segments, eased),
|
|
524
|
+
this.opts.labelAt ?? 0.5
|
|
525
|
+
);
|
|
526
|
+
this.labelEl.style.opacity = String(op);
|
|
527
|
+
if (this.labelBgEl) this.labelBgEl.style.opacity = String(op);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
bind() {
|
|
531
|
+
const targets = [this.refs.start, this.refs.end, ...this.resolveAvoid()];
|
|
532
|
+
if (this.refs.target) targets.push(this.refs.target);
|
|
533
|
+
this.ro = new ResizeObserver(() => this.render());
|
|
534
|
+
targets.forEach((t) => this.ro.observe(t));
|
|
535
|
+
if (this.opts.scroll !== false) {
|
|
536
|
+
window.addEventListener("scroll", this.onScroll, true);
|
|
537
|
+
window.addEventListener("resize", this.onScroll);
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
update() {
|
|
541
|
+
if (this.destroyed) return;
|
|
542
|
+
if (this.opts.scroll === false) {
|
|
543
|
+
this.applyProgress();
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const scroll = this.opts.scroll ?? {};
|
|
547
|
+
const range = scroll.range ?? [0.85, 0.35];
|
|
548
|
+
const targetRect = this.refs.target ? docRect(this.refs.target) : midpointRect(this.refs.start, this.refs.end);
|
|
549
|
+
const raw = scrollProgress(targetRect, range);
|
|
550
|
+
this.progress = clamp01(raw * this.opts.speed);
|
|
551
|
+
this.applyProgress();
|
|
552
|
+
}
|
|
553
|
+
};
|
|
554
|
+
function resolve(ref) {
|
|
555
|
+
return typeof ref === "string" ? document.querySelector(ref) : ref;
|
|
556
|
+
}
|
|
557
|
+
function refKey(ref) {
|
|
558
|
+
return typeof ref === "string" ? ref : ref.tagName + (ref.id ? "#" + ref.id : "");
|
|
559
|
+
}
|
|
560
|
+
function clampAt(t) {
|
|
561
|
+
return t < 0 ? 0 : t > 1 ? 1 : t;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// src/react.tsx
|
|
565
|
+
function read(a) {
|
|
566
|
+
if (typeof a === "string") return a;
|
|
567
|
+
if (a instanceof Element) return a;
|
|
568
|
+
return a.current;
|
|
569
|
+
}
|
|
570
|
+
function useScrollArrow(options) {
|
|
571
|
+
const arrowRef = react.useRef(null);
|
|
572
|
+
const { start, end, deps = [], ...rest } = options;
|
|
573
|
+
react.useEffect(() => {
|
|
574
|
+
const s = read(start);
|
|
575
|
+
const e = read(end);
|
|
576
|
+
if (!s || !e) return;
|
|
577
|
+
const arrow = new ScrollArrow({ ...rest, start: s, end: e });
|
|
578
|
+
arrowRef.current = arrow;
|
|
579
|
+
return () => {
|
|
580
|
+
arrow.destroy();
|
|
581
|
+
arrowRef.current = null;
|
|
582
|
+
};
|
|
583
|
+
}, deps);
|
|
584
|
+
}
|
|
585
|
+
function ScrollArrowLine(props) {
|
|
586
|
+
useScrollArrow(props);
|
|
587
|
+
return null;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
exports.ScrollArrowLine = ScrollArrowLine;
|
|
591
|
+
exports.useScrollArrow = useScrollArrow;
|
|
592
|
+
//# sourceMappingURL=react.cjs.map
|
|
593
|
+
//# sourceMappingURL=react.cjs.map
|
package/dist/react.d.cts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { S as ScrollArrowOptions } from './types-DehQP2Hx.cjs';
|
|
3
|
+
|
|
4
|
+
type Anchor = RefObject<Element | null> | Element | string;
|
|
5
|
+
interface UseScrollArrowOptions extends Omit<ScrollArrowOptions, "start" | "end"> {
|
|
6
|
+
start: Anchor;
|
|
7
|
+
end: Anchor;
|
|
8
|
+
/** Re-create the arrow when any value here changes. */
|
|
9
|
+
deps?: unknown[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Imperatively manage a ScrollArrow tied to two element refs. Returns nothing;
|
|
13
|
+
* the arrow lives in an overlay <svg>, not the React tree.
|
|
14
|
+
*/
|
|
15
|
+
declare function useScrollArrow(options: UseScrollArrowOptions): void;
|
|
16
|
+
interface ScrollArrowProps extends UseScrollArrowOptions {
|
|
17
|
+
}
|
|
18
|
+
/** Declarative component form. Renders nothing itself. */
|
|
19
|
+
declare function ScrollArrowLine(props: ScrollArrowProps): null;
|
|
20
|
+
|
|
21
|
+
export { ScrollArrowLine, ScrollArrowOptions, type ScrollArrowProps, type UseScrollArrowOptions, useScrollArrow };
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { RefObject } from 'react';
|
|
2
|
+
import { S as ScrollArrowOptions } from './types-DehQP2Hx.js';
|
|
3
|
+
|
|
4
|
+
type Anchor = RefObject<Element | null> | Element | string;
|
|
5
|
+
interface UseScrollArrowOptions extends Omit<ScrollArrowOptions, "start" | "end"> {
|
|
6
|
+
start: Anchor;
|
|
7
|
+
end: Anchor;
|
|
8
|
+
/** Re-create the arrow when any value here changes. */
|
|
9
|
+
deps?: unknown[];
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Imperatively manage a ScrollArrow tied to two element refs. Returns nothing;
|
|
13
|
+
* the arrow lives in an overlay <svg>, not the React tree.
|
|
14
|
+
*/
|
|
15
|
+
declare function useScrollArrow(options: UseScrollArrowOptions): void;
|
|
16
|
+
interface ScrollArrowProps extends UseScrollArrowOptions {
|
|
17
|
+
}
|
|
18
|
+
/** Declarative component form. Renders nothing itself. */
|
|
19
|
+
declare function ScrollArrowLine(props: ScrollArrowProps): null;
|
|
20
|
+
|
|
21
|
+
export { ScrollArrowLine, ScrollArrowOptions, type ScrollArrowProps, type UseScrollArrowOptions, useScrollArrow };
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { ScrollArrow } from './chunk-HLZXSGP5.js';
|
|
2
|
+
import { useRef, useEffect } from 'react';
|
|
3
|
+
|
|
4
|
+
function read(a) {
|
|
5
|
+
if (typeof a === "string") return a;
|
|
6
|
+
if (a instanceof Element) return a;
|
|
7
|
+
return a.current;
|
|
8
|
+
}
|
|
9
|
+
function useScrollArrow(options) {
|
|
10
|
+
const arrowRef = useRef(null);
|
|
11
|
+
const { start, end, deps = [], ...rest } = options;
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
const s = read(start);
|
|
14
|
+
const e = read(end);
|
|
15
|
+
if (!s || !e) return;
|
|
16
|
+
const arrow = new ScrollArrow({ ...rest, start: s, end: e });
|
|
17
|
+
arrowRef.current = arrow;
|
|
18
|
+
return () => {
|
|
19
|
+
arrow.destroy();
|
|
20
|
+
arrowRef.current = null;
|
|
21
|
+
};
|
|
22
|
+
}, deps);
|
|
23
|
+
}
|
|
24
|
+
function ScrollArrowLine(props) {
|
|
25
|
+
useScrollArrow(props);
|
|
26
|
+
return null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { ScrollArrowLine, useScrollArrow };
|
|
30
|
+
//# sourceMappingURL=react.js.map
|
|
31
|
+
//# sourceMappingURL=react.js.map
|