@aznabee/freehand-ui 0.1.4 → 0.1.6
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 +117 -9
- package/dist/freehand-ui.cjs +564 -98
- package/dist/freehand-ui.js +564 -98
- package/dist/index.d.ts +61 -6
- package/dist/react.cjs +569 -99
- package/dist/react.d.ts +2 -0
- package/dist/react.js +569 -99
- package/package.json +1 -1
package/dist/react.js
CHANGED
|
@@ -11,6 +11,265 @@ import {
|
|
|
11
11
|
useRef
|
|
12
12
|
} from "react";
|
|
13
13
|
|
|
14
|
+
// src/animation.js
|
|
15
|
+
var STYLE_MARKER = "data-freehand-animation";
|
|
16
|
+
var CSS = `
|
|
17
|
+
@keyframes freehand-arrow-dash {
|
|
18
|
+
from { stroke-dashoffset: var(--freehand-dash, 0px); }
|
|
19
|
+
to { stroke-dashoffset: 0; }
|
|
20
|
+
}
|
|
21
|
+
@keyframes freehand-arrow-drift {
|
|
22
|
+
0%, 100% { transform: translate(0px, 0px); }
|
|
23
|
+
50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
24
|
+
}
|
|
25
|
+
@keyframes freehand-arrow-settle {
|
|
26
|
+
from { transform: translate(0px, 0px); }
|
|
27
|
+
to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
28
|
+
}
|
|
29
|
+
@media (prefers-reduced-motion: reduce) {
|
|
30
|
+
.freehand-arrow, .freehand-arrow path { animation: none !important; }
|
|
31
|
+
.freehand-arrow path { stroke-dashoffset: 0 !important; }
|
|
32
|
+
}
|
|
33
|
+
`;
|
|
34
|
+
var ARROW_ANIMATION_DEFAULTS = {
|
|
35
|
+
/** Reveal the stroke with a dash sweep. */
|
|
36
|
+
draw: true,
|
|
37
|
+
/** Lean the arrow toward its tip. */
|
|
38
|
+
drift: true,
|
|
39
|
+
/** Multiplier over every duration - 2 plays twice as fast. */
|
|
40
|
+
speed: 1,
|
|
41
|
+
/** How long the shaft takes to draw, ms, before `speed`. */
|
|
42
|
+
duration: 900,
|
|
43
|
+
/** Wait before the first stroke appears, ms. */
|
|
44
|
+
delay: 150,
|
|
45
|
+
/** How far the arrow leans, px. */
|
|
46
|
+
distance: 5,
|
|
47
|
+
/** One full lean-and-return, ms. */
|
|
48
|
+
driftDuration: 2200,
|
|
49
|
+
/** Keep leaning. `false` settles once and stays put. */
|
|
50
|
+
repeat: true
|
|
51
|
+
};
|
|
52
|
+
function positive(value, fallback) {
|
|
53
|
+
const n = Number(value);
|
|
54
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
55
|
+
}
|
|
56
|
+
function atLeastZero(value, fallback) {
|
|
57
|
+
const n = Number(value);
|
|
58
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
59
|
+
}
|
|
60
|
+
function normalizeArrowAnimation(config) {
|
|
61
|
+
if (config === false || config === null) return false;
|
|
62
|
+
const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
|
|
63
|
+
const animation = {
|
|
64
|
+
draw: settings.draw !== false,
|
|
65
|
+
drift: settings.drift !== false,
|
|
66
|
+
speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
|
|
67
|
+
duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
|
|
68
|
+
delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
|
|
69
|
+
distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
|
|
70
|
+
driftDuration: positive(
|
|
71
|
+
settings.driftDuration,
|
|
72
|
+
ARROW_ANIMATION_DEFAULTS.driftDuration
|
|
73
|
+
),
|
|
74
|
+
repeat: settings.repeat !== false
|
|
75
|
+
};
|
|
76
|
+
if (!animation.draw && (!animation.drift || !animation.distance)) return false;
|
|
77
|
+
return animation;
|
|
78
|
+
}
|
|
79
|
+
var FLOW_PERIOD = 260;
|
|
80
|
+
function arrowTiming(animation) {
|
|
81
|
+
const duration = animation.duration / animation.speed;
|
|
82
|
+
const delay = animation.delay / animation.speed;
|
|
83
|
+
const drawn = animation.draw ? duration : 0;
|
|
84
|
+
return {
|
|
85
|
+
shaft: { duration, delay },
|
|
86
|
+
head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
|
|
87
|
+
flow: { duration: FLOW_PERIOD / animation.speed, delay },
|
|
88
|
+
drift: {
|
|
89
|
+
duration: animation.driftDuration / animation.speed,
|
|
90
|
+
delay: delay + drawn * 1.04
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
var injected = false;
|
|
95
|
+
function ensureArrowAnimationStyles() {
|
|
96
|
+
if (injected) return;
|
|
97
|
+
if (typeof document === "undefined" || !document.head) return;
|
|
98
|
+
injected = true;
|
|
99
|
+
if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
|
|
100
|
+
const style = document.createElement("style");
|
|
101
|
+
style.setAttribute(STYLE_MARKER, "");
|
|
102
|
+
style.textContent = CSS;
|
|
103
|
+
document.head.appendChild(style);
|
|
104
|
+
}
|
|
105
|
+
function time(ms) {
|
|
106
|
+
return `${Math.round(ms)}ms`;
|
|
107
|
+
}
|
|
108
|
+
function setAnimation(element, spec) {
|
|
109
|
+
const parts = [
|
|
110
|
+
spec.name,
|
|
111
|
+
time(spec.duration),
|
|
112
|
+
spec.easing,
|
|
113
|
+
time(spec.delay),
|
|
114
|
+
spec.iterations ?? "1"
|
|
115
|
+
];
|
|
116
|
+
if (spec.fill) parts.push(spec.fill);
|
|
117
|
+
element.style.animation = parts.join(" ");
|
|
118
|
+
}
|
|
119
|
+
function pathLength(path) {
|
|
120
|
+
try {
|
|
121
|
+
const length = path.getTotalLength();
|
|
122
|
+
return Number.isFinite(length) && length > 0 ? length : 0;
|
|
123
|
+
} catch {
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
function dashPeriod(path) {
|
|
128
|
+
const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
|
|
129
|
+
if (!pattern.length) return 0;
|
|
130
|
+
const sum = pattern.reduce((total, n) => total + n, 0);
|
|
131
|
+
return pattern.length % 2 === 0 ? sum : sum * 2;
|
|
132
|
+
}
|
|
133
|
+
function applyDash(path, timing, elapsed, flow) {
|
|
134
|
+
const length = pathLength(path);
|
|
135
|
+
if (!length) return;
|
|
136
|
+
const period = dashPeriod(path);
|
|
137
|
+
if (period) {
|
|
138
|
+
path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
|
|
139
|
+
setAnimation(path, {
|
|
140
|
+
name: "freehand-arrow-dash",
|
|
141
|
+
duration: flow.duration,
|
|
142
|
+
easing: "linear",
|
|
143
|
+
delay: flow.delay - elapsed,
|
|
144
|
+
iterations: "infinite"
|
|
145
|
+
});
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
|
|
149
|
+
path.style.strokeDasharray = `${length.toFixed(2)}px`;
|
|
150
|
+
setAnimation(path, {
|
|
151
|
+
name: "freehand-arrow-dash",
|
|
152
|
+
duration: timing.duration,
|
|
153
|
+
easing: "ease-out",
|
|
154
|
+
delay: timing.delay - elapsed,
|
|
155
|
+
fill: "both"
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
function applyDrift(group, parts, timing, elapsed, animation) {
|
|
159
|
+
const dx = parts.end.x - parts.start.x;
|
|
160
|
+
const dy = parts.end.y - parts.start.y;
|
|
161
|
+
const length = Math.hypot(dx, dy);
|
|
162
|
+
if (!length) return;
|
|
163
|
+
const scale = animation.distance / length;
|
|
164
|
+
group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
|
|
165
|
+
group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
|
|
166
|
+
setAnimation(
|
|
167
|
+
group,
|
|
168
|
+
animation.repeat ? {
|
|
169
|
+
name: "freehand-arrow-drift",
|
|
170
|
+
duration: timing.duration,
|
|
171
|
+
easing: "ease-in-out",
|
|
172
|
+
delay: timing.delay - elapsed,
|
|
173
|
+
iterations: "infinite"
|
|
174
|
+
} : {
|
|
175
|
+
// A single lean is a settle, not a round trip, so it takes half a cycle
|
|
176
|
+
name: "freehand-arrow-settle",
|
|
177
|
+
duration: timing.duration * 0.5,
|
|
178
|
+
easing: "ease-out",
|
|
179
|
+
delay: timing.delay - elapsed,
|
|
180
|
+
fill: "both"
|
|
181
|
+
}
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
function animateArrow(group, parts, animation, elapsed = 0) {
|
|
185
|
+
if (!animation || !group) return;
|
|
186
|
+
ensureArrowAnimationStyles();
|
|
187
|
+
group.setAttribute("class", "freehand-arrow");
|
|
188
|
+
const timing = arrowTiming(animation);
|
|
189
|
+
const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
|
|
190
|
+
if (animation.draw) {
|
|
191
|
+
for (const path of parts.shaft) {
|
|
192
|
+
applyDash(path, timing.shaft, age, timing.flow);
|
|
193
|
+
}
|
|
194
|
+
for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
|
|
195
|
+
}
|
|
196
|
+
if (animation.drift && animation.distance > 0) {
|
|
197
|
+
applyDrift(group, parts, timing.drift, age, animation);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/locale.js
|
|
202
|
+
var DEFAULT_KEY = "default";
|
|
203
|
+
var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
|
|
204
|
+
var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
|
|
205
|
+
var LTR_STRONG = /[A-Za-zÀ-֏ऀ-Ⰰ-豈-ﬗA-Za-z]/;
|
|
206
|
+
function normalizeTag(tag) {
|
|
207
|
+
if (typeof tag !== "string") return "";
|
|
208
|
+
return tag.trim().replace(/_/g, "-").toLowerCase();
|
|
209
|
+
}
|
|
210
|
+
function isLocaleMap(value) {
|
|
211
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
212
|
+
const entries = Object.entries(value);
|
|
213
|
+
if (!entries.length) return false;
|
|
214
|
+
return entries.every(([key, text]) => {
|
|
215
|
+
if (typeof text !== "string") return false;
|
|
216
|
+
const tag = normalizeTag(key);
|
|
217
|
+
return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
function preferredLocales(override) {
|
|
221
|
+
const tags = [];
|
|
222
|
+
const add = (value) => {
|
|
223
|
+
const tag = normalizeTag(value);
|
|
224
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
225
|
+
};
|
|
226
|
+
if (Array.isArray(override)) override.forEach(add);
|
|
227
|
+
else add(override);
|
|
228
|
+
const nav = typeof navigator === "undefined" ? null : navigator;
|
|
229
|
+
if (nav) {
|
|
230
|
+
if (Array.isArray(nav.languages)) nav.languages.forEach(add);
|
|
231
|
+
add(nav.language);
|
|
232
|
+
}
|
|
233
|
+
if (typeof document !== "undefined") add(document.documentElement?.lang);
|
|
234
|
+
return tags;
|
|
235
|
+
}
|
|
236
|
+
function matchLocale(tags, preferred) {
|
|
237
|
+
const available = tags.map(normalizeTag);
|
|
238
|
+
for (const want of preferred) {
|
|
239
|
+
let tag = want;
|
|
240
|
+
while (tag) {
|
|
241
|
+
const exact = available.indexOf(tag);
|
|
242
|
+
if (exact !== -1) return exact;
|
|
243
|
+
const cut = tag.lastIndexOf("-");
|
|
244
|
+
tag = cut > 0 ? tag.slice(0, cut) : "";
|
|
245
|
+
}
|
|
246
|
+
const base = want.split("-")[0];
|
|
247
|
+
const sibling = available.findIndex((key) => key.split("-")[0] === base);
|
|
248
|
+
if (sibling !== -1) return sibling;
|
|
249
|
+
}
|
|
250
|
+
return -1;
|
|
251
|
+
}
|
|
252
|
+
function resolveLocalizedText(text, locale = null) {
|
|
253
|
+
if (typeof text === "string") return { text, locale: null };
|
|
254
|
+
if (!isLocaleMap(text)) return { text: "", locale: null };
|
|
255
|
+
const keys = Object.keys(text);
|
|
256
|
+
const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
|
|
257
|
+
const index = matchLocale(tags, preferredLocales(locale));
|
|
258
|
+
if (index !== -1) {
|
|
259
|
+
return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
|
|
260
|
+
}
|
|
261
|
+
const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
|
|
262
|
+
if (fallback) return { text: text[fallback], locale: null };
|
|
263
|
+
return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
|
|
264
|
+
}
|
|
265
|
+
function isRtlText(text) {
|
|
266
|
+
if (typeof text !== "string") return false;
|
|
267
|
+
const rtl = text.search(RTL_STRONG);
|
|
268
|
+
if (rtl === -1) return false;
|
|
269
|
+
const ltr = text.search(LTR_STRONG);
|
|
270
|
+
return ltr === -1 || rtl < ltr;
|
|
271
|
+
}
|
|
272
|
+
|
|
14
273
|
// src/utils.js
|
|
15
274
|
var DEFAULT_OPTIONS = {
|
|
16
275
|
border: true,
|
|
@@ -29,7 +288,17 @@ var DEFAULT_OPTIONS = {
|
|
|
29
288
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
30
289
|
fontFamily: null,
|
|
31
290
|
/** Fetch Caveat when the page has not provided it. */
|
|
32
|
-
autoLoadFont: true
|
|
291
|
+
autoLoadFont: true,
|
|
292
|
+
/**
|
|
293
|
+
* Language for a note written in more than one. Omit to follow the visitor's
|
|
294
|
+
* own languages, which is what their operating system is set to.
|
|
295
|
+
*/
|
|
296
|
+
locale: null,
|
|
297
|
+
/**
|
|
298
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
299
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
300
|
+
*/
|
|
301
|
+
zIndex: null
|
|
33
302
|
};
|
|
34
303
|
function resolveElement(target) {
|
|
35
304
|
if (!target) return null;
|
|
@@ -43,8 +312,15 @@ function resolveElement(target) {
|
|
|
43
312
|
}
|
|
44
313
|
function mergeOptions(options = {}) {
|
|
45
314
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
46
|
-
if (merged.note
|
|
47
|
-
merged.note
|
|
315
|
+
if (merged.note) {
|
|
316
|
+
const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
|
|
317
|
+
merged.note = {
|
|
318
|
+
...config,
|
|
319
|
+
position: config.position ?? "top-right",
|
|
320
|
+
// The note carries the chosen language, so `renderAnnotation` can resolve
|
|
321
|
+
// a multilingual note on its own.
|
|
322
|
+
locale: config.locale ?? merged.locale ?? null
|
|
323
|
+
};
|
|
48
324
|
}
|
|
49
325
|
if (merged.arrow) {
|
|
50
326
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -54,7 +330,10 @@ function mergeOptions(options = {}) {
|
|
|
54
330
|
// explicit `from` is honoured whether or not there is a note.
|
|
55
331
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
56
332
|
to: config.to ?? "edge",
|
|
57
|
-
style: config.style ?? "curved"
|
|
333
|
+
style: config.style ?? "curved",
|
|
334
|
+
// Arrows draw themselves on and lean toward their target by default;
|
|
335
|
+
// `animate: false` opts out, an object tunes speed and gestures
|
|
336
|
+
animate: normalizeArrowAnimation(config.animate)
|
|
58
337
|
};
|
|
59
338
|
}
|
|
60
339
|
if (merged.decorations === true) {
|
|
@@ -122,6 +401,40 @@ function getScrollableAncestors(element) {
|
|
|
122
401
|
ancestors.push(window);
|
|
123
402
|
return ancestors;
|
|
124
403
|
}
|
|
404
|
+
function resolveOverlayZIndex(element, override) {
|
|
405
|
+
if (override != null) return override;
|
|
406
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
407
|
+
if (zIndex !== "auto") {
|
|
408
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
409
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
410
|
+
}
|
|
411
|
+
let node = element.parentElement;
|
|
412
|
+
while (node && node !== document.documentElement) {
|
|
413
|
+
const style = getComputedStyle(node);
|
|
414
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
415
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
416
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
417
|
+
}
|
|
418
|
+
node = node.parentElement;
|
|
419
|
+
}
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
function insertOverlaySvg(svg, element) {
|
|
423
|
+
const parent = element.parentNode;
|
|
424
|
+
if (!parent) {
|
|
425
|
+
document.body.appendChild(svg);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
429
|
+
}
|
|
430
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
431
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
432
|
+
if (zIndex == null) {
|
|
433
|
+
svg.style.zIndex = "";
|
|
434
|
+
} else {
|
|
435
|
+
svg.style.zIndex = String(zIndex);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
125
438
|
|
|
126
439
|
// src/geometry.js
|
|
127
440
|
function averageCornerRadius(element) {
|
|
@@ -269,6 +582,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
269
582
|
|
|
270
583
|
// src/renderer.js
|
|
271
584
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
585
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
272
586
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
273
587
|
var NOTE_FONT_SIZE = 19;
|
|
274
588
|
function clearSvg(svg) {
|
|
@@ -514,6 +828,15 @@ function appendPath(svg, d, style) {
|
|
|
514
828
|
svg.appendChild(path);
|
|
515
829
|
return path;
|
|
516
830
|
}
|
|
831
|
+
function appendGlow(svg, radius, style) {
|
|
832
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
833
|
+
circle.setAttribute("r", fmt(radius));
|
|
834
|
+
circle.setAttribute("fill", style.color);
|
|
835
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
836
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
837
|
+
svg.appendChild(circle);
|
|
838
|
+
return circle;
|
|
839
|
+
}
|
|
517
840
|
function rectPerimeter(width, height, radius) {
|
|
518
841
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
519
842
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -600,20 +923,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
600
923
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
601
924
|
};
|
|
602
925
|
}
|
|
603
|
-
|
|
926
|
+
var STRADDLE = 0.75;
|
|
927
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
604
928
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
605
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
606
929
|
const knot = 0.46 + random() * 0.12;
|
|
607
930
|
const points = [];
|
|
608
931
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
609
932
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
610
933
|
const pivot = at(knot);
|
|
611
|
-
const
|
|
934
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
935
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
936
|
+
const heading = normalize(derivative);
|
|
612
937
|
const normal = { x: -heading.y, y: heading.x };
|
|
613
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
614
938
|
const centre = {
|
|
615
|
-
x: pivot.x + normal.x * radius * side,
|
|
616
|
-
y: pivot.y + normal.y * radius * side
|
|
939
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
940
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
617
941
|
};
|
|
618
942
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
619
943
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -621,15 +945,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
621
945
|
for (let i = 1; i <= steps; i++) {
|
|
622
946
|
const progress = i / steps;
|
|
623
947
|
const angle = from + turns * progress * side;
|
|
948
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
624
949
|
const drift = radius * 0.55 * progress;
|
|
625
950
|
points.push({
|
|
626
|
-
x: centre.x + Math.cos(angle) *
|
|
627
|
-
y: centre.y + Math.sin(angle) *
|
|
951
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
952
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
628
953
|
});
|
|
629
954
|
}
|
|
955
|
+
const exit = points[points.length - 1];
|
|
956
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
957
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
958
|
+
const rejoin = at(resume);
|
|
959
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
960
|
+
const settle = 6;
|
|
630
961
|
const tail = 14;
|
|
631
962
|
for (let i = 1; i <= tail; i++) {
|
|
632
|
-
|
|
963
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
964
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
965
|
+
points.push({
|
|
966
|
+
x: point.x + normal.x * residual * fade,
|
|
967
|
+
y: point.y + normal.y * residual * fade
|
|
968
|
+
});
|
|
633
969
|
}
|
|
634
970
|
return points;
|
|
635
971
|
}
|
|
@@ -703,21 +1039,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
703
1039
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
704
1040
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
705
1041
|
const spread = 0.46 + random() * 0.1;
|
|
1042
|
+
const arms = [];
|
|
706
1043
|
for (const side of [-1, 1]) {
|
|
707
1044
|
const armAngle = angle + spread * side;
|
|
708
1045
|
const tail = {
|
|
709
1046
|
x: tip.x - length * Math.cos(armAngle),
|
|
710
1047
|
y: tip.y - length * Math.sin(armAngle)
|
|
711
1048
|
};
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
1049
|
+
arms.push(
|
|
1050
|
+
appendPath(
|
|
1051
|
+
svg,
|
|
1052
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1053
|
+
steps: 2,
|
|
1054
|
+
bow: 0.35 * side
|
|
1055
|
+
}),
|
|
1056
|
+
{ ...style, dashed: false }
|
|
1057
|
+
)
|
|
719
1058
|
);
|
|
720
1059
|
}
|
|
1060
|
+
return arms;
|
|
1061
|
+
}
|
|
1062
|
+
function bowOverTheTop(perpendicular) {
|
|
1063
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1064
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
721
1065
|
}
|
|
722
1066
|
function boundsOf(points) {
|
|
723
1067
|
const xs = points.map((p) => p.x);
|
|
@@ -729,7 +1073,7 @@ function boundsOf(points) {
|
|
|
729
1073
|
height: Math.max(...ys) - Math.min(...ys)
|
|
730
1074
|
};
|
|
731
1075
|
}
|
|
732
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1076
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
733
1077
|
const random = createRandom(seed + 17);
|
|
734
1078
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
735
1079
|
const dx = end.x - start.x;
|
|
@@ -738,6 +1082,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
738
1082
|
const dashed = arrowOptions.style === "dotted";
|
|
739
1083
|
let tipAngle = Math.atan2(dy, dx);
|
|
740
1084
|
let shaft;
|
|
1085
|
+
let ghost = null;
|
|
741
1086
|
const hull = [start, end];
|
|
742
1087
|
if (arrowOptions.style === "straight") {
|
|
743
1088
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -751,7 +1096,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
751
1096
|
y: rect.y + rect.height / 2 - mid.y
|
|
752
1097
|
};
|
|
753
1098
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
754
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1099
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1100
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1101
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1102
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1103
|
+
bowOverTheTop(perpendicular)
|
|
1104
|
+
);
|
|
755
1105
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
756
1106
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
757
1107
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -766,34 +1116,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
766
1116
|
{ x: c2x, y: c2y },
|
|
767
1117
|
end,
|
|
768
1118
|
radius,
|
|
769
|
-
random
|
|
1119
|
+
random,
|
|
1120
|
+
-sign
|
|
770
1121
|
);
|
|
771
1122
|
shaft = catmullRomPath(points, false);
|
|
772
1123
|
hull.push(...points);
|
|
773
1124
|
const last = points[points.length - 1];
|
|
774
1125
|
const prior = points[points.length - 2] ?? start;
|
|
775
1126
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
1127
|
+
} else {
|
|
1128
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1129
|
+
if (style.roughness >= 1.1) {
|
|
1130
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1131
|
+
ghost = `M ${fmt(start.x + drift())} ${fmt(start.y + drift())} C ${fmt(c1x + drift())} ${fmt(c1y + drift())} ${fmt(c2x + drift())} ${fmt(c2y + drift())} ${fmt(end.x + drift() * 0.4)} ${fmt(end.y + drift() * 0.4)}`;
|
|
1132
|
+
}
|
|
1133
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
779
1134
|
}
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
1135
|
+
}
|
|
1136
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1137
|
+
svg.appendChild(group);
|
|
1138
|
+
const shaftPaths = [];
|
|
1139
|
+
if (ghost) {
|
|
1140
|
+
shaftPaths.push(
|
|
1141
|
+
appendPath(group, ghost, {
|
|
785
1142
|
...style,
|
|
786
1143
|
opacity: style.opacity * 0.3,
|
|
787
1144
|
strokeWidth: style.strokeWidth * 0.7,
|
|
788
1145
|
dashed
|
|
789
|
-
})
|
|
790
|
-
|
|
791
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1146
|
+
})
|
|
1147
|
+
);
|
|
792
1148
|
}
|
|
793
|
-
appendPath(
|
|
794
|
-
drawArrowHead(
|
|
1149
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1150
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1151
|
+
animateArrow(
|
|
1152
|
+
group,
|
|
1153
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1154
|
+
arrowOptions.animate,
|
|
1155
|
+
context.elapsed
|
|
1156
|
+
);
|
|
795
1157
|
const bounds = boundsOf(hull);
|
|
796
|
-
return { start, end, bounds };
|
|
1158
|
+
return { start, end, bounds, group };
|
|
797
1159
|
}
|
|
798
1160
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
799
1161
|
if (width <= 4) return;
|
|
@@ -860,6 +1222,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
860
1222
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
861
1223
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
862
1224
|
const lineHeight = fontSize * 1.05;
|
|
1225
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
863
1226
|
const group = document.createElementNS(SVG_NS, "g");
|
|
864
1227
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
865
1228
|
textEl.setAttribute("x", "0");
|
|
@@ -872,6 +1235,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
872
1235
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
873
1236
|
textEl.setAttribute("text-anchor", "start");
|
|
874
1237
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1238
|
+
if (rtl) {
|
|
1239
|
+
textEl.setAttribute("direction", "rtl");
|
|
1240
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1241
|
+
}
|
|
1242
|
+
if (options.locale) {
|
|
1243
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1244
|
+
}
|
|
875
1245
|
const tspans = lines.map((line, index) => {
|
|
876
1246
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
877
1247
|
tspan.textContent = line;
|
|
@@ -900,15 +1270,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
900
1270
|
height,
|
|
901
1271
|
ascent,
|
|
902
1272
|
descent,
|
|
903
|
-
fontSize
|
|
1273
|
+
fontSize,
|
|
1274
|
+
rtl
|
|
904
1275
|
};
|
|
905
1276
|
}
|
|
906
1277
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
907
1278
|
const random = createRandom(seed + 55);
|
|
908
1279
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
909
|
-
note.
|
|
1280
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1281
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
910
1282
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
911
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1283
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
912
1284
|
const cx = box.x + box.width / 2;
|
|
913
1285
|
const cy = box.y + box.height / 2;
|
|
914
1286
|
note.group.setAttribute(
|
|
@@ -917,9 +1289,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
917
1289
|
);
|
|
918
1290
|
if (options.underline !== false) {
|
|
919
1291
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1292
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
920
1293
|
drawUnderline(
|
|
921
1294
|
note.group,
|
|
922
|
-
|
|
1295
|
+
underlineX,
|
|
923
1296
|
box.y + box.height + 2,
|
|
924
1297
|
lastWidth - 2,
|
|
925
1298
|
{
|
|
@@ -947,7 +1320,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
947
1320
|
svg.style.height = `${height}px`;
|
|
948
1321
|
svg.style.pointerEvents = "none";
|
|
949
1322
|
svg.style.overflow = "visible";
|
|
950
|
-
svg.style.
|
|
1323
|
+
svg.style.willChange = "transform";
|
|
951
1324
|
return svg;
|
|
952
1325
|
}
|
|
953
1326
|
|
|
@@ -994,12 +1367,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
994
1367
|
return { ...box, x: box.x + dx };
|
|
995
1368
|
}
|
|
996
1369
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
997
|
-
|
|
1370
|
+
const { text, locale } = resolveLocalizedText(
|
|
1371
|
+
noteOptions?.text,
|
|
1372
|
+
noteOptions?.locale
|
|
1373
|
+
);
|
|
1374
|
+
if (!text) return null;
|
|
998
1375
|
const band = layout.band ?? null;
|
|
999
1376
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1000
1377
|
const random = createRandom(seed + 9);
|
|
1001
|
-
const note = createNoteText(svg,
|
|
1002
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1378
|
+
const note = createNoteText(svg, text, style, {
|
|
1379
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1380
|
+
locale
|
|
1003
1381
|
});
|
|
1004
1382
|
const tilt = (random() - 0.45) * 7;
|
|
1005
1383
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1041,6 +1419,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1041
1419
|
strokeWidth: style.strokeWidth * scale
|
|
1042
1420
|
});
|
|
1043
1421
|
}
|
|
1422
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1423
|
+
const count = 6;
|
|
1424
|
+
const points = [];
|
|
1425
|
+
for (let i = 0; i < count; i++) {
|
|
1426
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1427
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1428
|
+
points.push({
|
|
1429
|
+
x: x + Math.cos(angle) * r,
|
|
1430
|
+
y: y + Math.sin(angle) * r
|
|
1431
|
+
});
|
|
1432
|
+
}
|
|
1433
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1434
|
+
...style,
|
|
1435
|
+
fill: style.color,
|
|
1436
|
+
strokeWidth: 0
|
|
1437
|
+
});
|
|
1438
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1439
|
+
return blob;
|
|
1440
|
+
}
|
|
1044
1441
|
function drawHeart(parent, size, style, random) {
|
|
1045
1442
|
const s = size;
|
|
1046
1443
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1064,26 +1461,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1064
1461
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1065
1462
|
"Z"
|
|
1066
1463
|
].join(" ");
|
|
1067
|
-
appendPath(parent, path, style);
|
|
1464
|
+
return appendPath(parent, path, style);
|
|
1068
1465
|
}
|
|
1069
1466
|
function drawSparkle(parent, size, style, random) {
|
|
1070
1467
|
twinkle(
|
|
1071
1468
|
parent,
|
|
1072
|
-
size * (0.
|
|
1469
|
+
size * (0.62 + random() * 0.12),
|
|
1073
1470
|
size,
|
|
1074
|
-
size * (0.
|
|
1471
|
+
size * (0.14 + random() * 0.08),
|
|
1075
1472
|
style
|
|
1076
1473
|
);
|
|
1077
1474
|
}
|
|
1078
1475
|
function drawStar(parent, size, style, random) {
|
|
1079
1476
|
twinkle(
|
|
1080
1477
|
parent,
|
|
1081
|
-
size * (0.
|
|
1082
|
-
size,
|
|
1083
|
-
size * (0.
|
|
1478
|
+
size * (0.62 + random() * 0.12),
|
|
1479
|
+
size * (0.8 + random() * 0.12),
|
|
1480
|
+
size * (0.14 + random() * 0.08),
|
|
1084
1481
|
style
|
|
1085
1482
|
);
|
|
1086
1483
|
}
|
|
1484
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1485
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1486
|
+
color: style.color,
|
|
1487
|
+
opacity: style.opacity * 0.22
|
|
1488
|
+
});
|
|
1489
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1490
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1491
|
+
const bluntness = size * 0.19;
|
|
1492
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1493
|
+
...style,
|
|
1494
|
+
fill: style.color,
|
|
1495
|
+
strokeWidth: bluntness
|
|
1496
|
+
});
|
|
1497
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1498
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1499
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1500
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1501
|
+
const corners = [
|
|
1502
|
+
{ x: 0, y: -(armY + gap) },
|
|
1503
|
+
{ x: armX + gap, y: 0 },
|
|
1504
|
+
{ x: 0, y: armY + gap },
|
|
1505
|
+
{ x: -(armX + gap), y: 0 }
|
|
1506
|
+
];
|
|
1507
|
+
for (const { x, y } of corners) {
|
|
1508
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1509
|
+
drawFleck(
|
|
1510
|
+
parent,
|
|
1511
|
+
x * jitter,
|
|
1512
|
+
y * jitter,
|
|
1513
|
+
size * (0.1 + random() * 0.03),
|
|
1514
|
+
style,
|
|
1515
|
+
random
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1087
1519
|
function drawTwinkle(parent, size, style, random) {
|
|
1088
1520
|
const count = 5;
|
|
1089
1521
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1191,46 +1623,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1191
1623
|
);
|
|
1192
1624
|
}
|
|
1193
1625
|
}
|
|
1194
|
-
function drawDots(parent, size, style, random) {
|
|
1195
|
-
let x = -size * 0.7;
|
|
1196
|
-
for (let i = 0; i < 3; i++) {
|
|
1197
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1198
|
-
x += size * (0.6 + random() * 0.25);
|
|
1199
|
-
}
|
|
1200
|
-
}
|
|
1201
|
-
function drawStroke(parent, size, style, random) {
|
|
1202
|
-
appendPath(
|
|
1203
|
-
parent,
|
|
1204
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1205
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1206
|
-
steps: 4
|
|
1207
|
-
}),
|
|
1208
|
-
style
|
|
1209
|
-
);
|
|
1210
|
-
}
|
|
1211
|
-
function drawSteam(parent, size, style, random) {
|
|
1212
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1213
|
-
for (let i = 0; i < 2; i++) {
|
|
1214
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1215
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1216
|
-
appendPath(
|
|
1217
|
-
parent,
|
|
1218
|
-
`M ${x.toFixed(2)} ${base.toFixed(2)} Q ${(x + lean * size * 0.52).toFixed(2)} ${(base - size * 0.62).toFixed(2)} ${(x + lean * size * 0.06).toFixed(2)} ${(base - size * 1.3).toFixed(2)}`,
|
|
1219
|
-
style
|
|
1220
|
-
);
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
1626
|
var DECORATION_DRAWERS = {
|
|
1224
1627
|
heart: drawHeart,
|
|
1225
1628
|
sparkle: drawSparkle,
|
|
1226
1629
|
star: drawStar,
|
|
1227
1630
|
twinkle: drawTwinkle,
|
|
1631
|
+
glimmer: drawGlimmer,
|
|
1228
1632
|
smiley: drawSmiley,
|
|
1229
1633
|
emphasis: drawEmphasis,
|
|
1230
|
-
arcs: drawArcs
|
|
1231
|
-
dots: drawDots,
|
|
1232
|
-
stroke: drawStroke,
|
|
1233
|
-
steam: drawSteam
|
|
1634
|
+
arcs: drawArcs
|
|
1234
1635
|
};
|
|
1235
1636
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1236
1637
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1241,12 +1642,10 @@ var TYPE_TRAITS = {
|
|
|
1241
1642
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1242
1643
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1243
1644
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1645
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1244
1646
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1245
1647
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1246
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1247
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1248
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1249
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1648
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1250
1649
|
};
|
|
1251
1650
|
var DEFAULT_TYPES = [
|
|
1252
1651
|
"twinkle",
|
|
@@ -1449,6 +1848,9 @@ function ensureHandwrittenFont() {
|
|
|
1449
1848
|
|
|
1450
1849
|
// src/doodle.js
|
|
1451
1850
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1851
|
+
function now() {
|
|
1852
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1853
|
+
}
|
|
1452
1854
|
function localBand(overlayRect, inset = 8) {
|
|
1453
1855
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1454
1856
|
return {
|
|
@@ -1465,19 +1867,24 @@ var DoodleOverlay = class {
|
|
|
1465
1867
|
this.element = element;
|
|
1466
1868
|
this.options = options;
|
|
1467
1869
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1870
|
+
this.startedAt = now();
|
|
1468
1871
|
this.svg = null;
|
|
1469
1872
|
this.resizeObserver = null;
|
|
1470
1873
|
this.mutationObserver = null;
|
|
1471
1874
|
this.pendingFrame = null;
|
|
1472
|
-
this.
|
|
1473
|
-
this.
|
|
1875
|
+
this.pendingRepositionFrame = null;
|
|
1876
|
+
this.basePosition = { x: 0, y: 0 };
|
|
1877
|
+
this.listeners = [];
|
|
1878
|
+
this.onScroll = this.scheduleReposition.bind(this);
|
|
1474
1879
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1880
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1475
1881
|
this.childElements = [];
|
|
1476
1882
|
this.mount();
|
|
1477
1883
|
}
|
|
1478
1884
|
mount() {
|
|
1479
1885
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1480
|
-
|
|
1886
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1887
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1481
1888
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1482
1889
|
this.resizeObserver.observe(this.element);
|
|
1483
1890
|
if (this.options.children) {
|
|
@@ -1491,10 +1898,20 @@ var DoodleOverlay = class {
|
|
|
1491
1898
|
});
|
|
1492
1899
|
for (const target of getScrollableAncestors(this.element)) {
|
|
1493
1900
|
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1494
|
-
this.
|
|
1901
|
+
this.listeners.push({ target, type: "scroll", handler: this.onScroll });
|
|
1495
1902
|
}
|
|
1496
1903
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1497
|
-
this.
|
|
1904
|
+
this.listeners.push({
|
|
1905
|
+
target: window,
|
|
1906
|
+
type: "resize",
|
|
1907
|
+
handler: this.onResize
|
|
1908
|
+
});
|
|
1909
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1910
|
+
this.listeners.push({
|
|
1911
|
+
target: window,
|
|
1912
|
+
type: "languagechange",
|
|
1913
|
+
handler: this.onLanguageChange
|
|
1914
|
+
});
|
|
1498
1915
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1499
1916
|
ensureHandwrittenFont();
|
|
1500
1917
|
}
|
|
@@ -1518,6 +1935,46 @@ var DoodleOverlay = class {
|
|
|
1518
1935
|
this.update();
|
|
1519
1936
|
});
|
|
1520
1937
|
}
|
|
1938
|
+
scheduleReposition() {
|
|
1939
|
+
if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
|
|
1940
|
+
return;
|
|
1941
|
+
}
|
|
1942
|
+
this.pendingRepositionFrame = scheduleFrame(() => {
|
|
1943
|
+
this.pendingRepositionFrame = null;
|
|
1944
|
+
this.reposition();
|
|
1945
|
+
});
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Cheap scroll-driven path: slide the overlay to the element's new
|
|
1949
|
+
* viewport position without touching its contents. Scrolling can't change
|
|
1950
|
+
* an element's size, so the rects, viewBox and every hand-drawn stroke
|
|
1951
|
+
* inside them are still valid - only the overlay's position needs to move.
|
|
1952
|
+
*
|
|
1953
|
+
* That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
|
|
1954
|
+
* same box-geometry pass as layout, so the browser re-checks layout on
|
|
1955
|
+
* every write; `transform` is composited, so the scroll path never
|
|
1956
|
+
* touches layout at all.
|
|
1957
|
+
*/
|
|
1958
|
+
reposition() {
|
|
1959
|
+
if (!this.svg || !this.element.isConnected) return;
|
|
1960
|
+
const { padding, radius } = this.options;
|
|
1961
|
+
const targets = this.getTargets();
|
|
1962
|
+
const margin = this.overlayMargin();
|
|
1963
|
+
const absoluteRects = targets.map(
|
|
1964
|
+
(target) => getElementBounds(target, padding, radius)
|
|
1965
|
+
);
|
|
1966
|
+
const overlayRect = unionRects(absoluteRects, margin);
|
|
1967
|
+
const currentWidth = parseFloat(this.svg.style.width) || 0;
|
|
1968
|
+
const currentHeight = parseFloat(this.svg.style.height) || 0;
|
|
1969
|
+
const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
|
|
1970
|
+
if (resized) {
|
|
1971
|
+
this.update();
|
|
1972
|
+
return;
|
|
1973
|
+
}
|
|
1974
|
+
const dx = overlayRect.x - this.basePosition.x;
|
|
1975
|
+
const dy = overlayRect.y - this.basePosition.y;
|
|
1976
|
+
this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
1977
|
+
}
|
|
1521
1978
|
getTargets() {
|
|
1522
1979
|
if (this.options.children) {
|
|
1523
1980
|
const nodes = this.element.querySelectorAll(this.options.children);
|
|
@@ -1530,11 +1987,13 @@ var DoodleOverlay = class {
|
|
|
1530
1987
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1531
1988
|
const targets = this.getTargets();
|
|
1532
1989
|
const margin = this.overlayMargin();
|
|
1990
|
+
const elapsed = now() - this.startedAt;
|
|
1533
1991
|
const absoluteRects = targets.map(
|
|
1534
1992
|
(target) => getElementBounds(target, padding, radius)
|
|
1535
1993
|
);
|
|
1536
1994
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1537
1995
|
clearSvg(this.svg);
|
|
1996
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1538
1997
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1539
1998
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1540
1999
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
@@ -1542,6 +2001,8 @@ var DoodleOverlay = class {
|
|
|
1542
2001
|
this.svg.style.top = `${overlayRect.y}px`;
|
|
1543
2002
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1544
2003
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
2004
|
+
this.svg.style.transform = "";
|
|
2005
|
+
this.basePosition = { x: overlayRect.x, y: overlayRect.y };
|
|
1545
2006
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
1546
2007
|
const noteStyle = {
|
|
1547
2008
|
color,
|
|
@@ -1581,7 +2042,8 @@ var DoodleOverlay = class {
|
|
|
1581
2042
|
this.options.arrow,
|
|
1582
2043
|
strokeStyle,
|
|
1583
2044
|
seed,
|
|
1584
|
-
note ? note.box : null
|
|
2045
|
+
note ? note.box : null,
|
|
2046
|
+
{ elapsed }
|
|
1585
2047
|
) : null;
|
|
1586
2048
|
if (this.options.decorations) {
|
|
1587
2049
|
renderDecorations(
|
|
@@ -1608,7 +2070,7 @@ var DoodleOverlay = class {
|
|
|
1608
2070
|
overlayMargin() {
|
|
1609
2071
|
const { note, arrow, decorations } = this.options;
|
|
1610
2072
|
if (!note && !arrow && !decorations) return 48;
|
|
1611
|
-
const text =
|
|
2073
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1612
2074
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1613
2075
|
}
|
|
1614
2076
|
destroy() {
|
|
@@ -1616,6 +2078,10 @@ var DoodleOverlay = class {
|
|
|
1616
2078
|
cancelFrame(this.pendingFrame);
|
|
1617
2079
|
this.pendingFrame = null;
|
|
1618
2080
|
}
|
|
2081
|
+
if (this.pendingRepositionFrame != null) {
|
|
2082
|
+
cancelFrame(this.pendingRepositionFrame);
|
|
2083
|
+
this.pendingRepositionFrame = null;
|
|
2084
|
+
}
|
|
1619
2085
|
if (this.resizeObserver) {
|
|
1620
2086
|
this.resizeObserver.disconnect();
|
|
1621
2087
|
this.resizeObserver = null;
|
|
@@ -1628,10 +2094,10 @@ var DoodleOverlay = class {
|
|
|
1628
2094
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1629
2095
|
this.onFontsLoaded = null;
|
|
1630
2096
|
}
|
|
1631
|
-
for (const { target, type } of this.
|
|
1632
|
-
target.removeEventListener(type,
|
|
2097
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2098
|
+
target.removeEventListener(type, handler);
|
|
1633
2099
|
}
|
|
1634
|
-
this.
|
|
2100
|
+
this.listeners = [];
|
|
1635
2101
|
if (this.svg?.parentNode) {
|
|
1636
2102
|
this.svg.parentNode.removeChild(this.svg);
|
|
1637
2103
|
}
|
|
@@ -1672,7 +2138,8 @@ var OPTION_KEYS = [
|
|
|
1672
2138
|
"decorations",
|
|
1673
2139
|
"addBreaks",
|
|
1674
2140
|
"fontFamily",
|
|
1675
|
-
"autoLoadFont"
|
|
2141
|
+
"autoLoadFont",
|
|
2142
|
+
"locale"
|
|
1676
2143
|
];
|
|
1677
2144
|
function collectOptions(props) {
|
|
1678
2145
|
const options = {};
|
|
@@ -1724,6 +2191,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1724
2191
|
addBreaks,
|
|
1725
2192
|
fontFamily,
|
|
1726
2193
|
autoLoadFont,
|
|
2194
|
+
locale,
|
|
1727
2195
|
childSelector,
|
|
1728
2196
|
...rest
|
|
1729
2197
|
} = props;
|
|
@@ -1743,6 +2211,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1743
2211
|
addBreaks,
|
|
1744
2212
|
fontFamily,
|
|
1745
2213
|
autoLoadFont,
|
|
2214
|
+
locale,
|
|
1746
2215
|
childSelector
|
|
1747
2216
|
}),
|
|
1748
2217
|
[
|
|
@@ -1760,6 +2229,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1760
2229
|
JSON.stringify(addBreaks ?? null),
|
|
1761
2230
|
fontFamily,
|
|
1762
2231
|
autoLoadFont,
|
|
2232
|
+
JSON.stringify(locale ?? null),
|
|
1763
2233
|
childSelector
|
|
1764
2234
|
]
|
|
1765
2235
|
);
|