@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.cjs
CHANGED
|
@@ -28,6 +28,265 @@ __export(react_exports, {
|
|
|
28
28
|
module.exports = __toCommonJS(react_exports);
|
|
29
29
|
var import_react = require("react");
|
|
30
30
|
|
|
31
|
+
// src/animation.js
|
|
32
|
+
var STYLE_MARKER = "data-freehand-animation";
|
|
33
|
+
var CSS = `
|
|
34
|
+
@keyframes freehand-arrow-dash {
|
|
35
|
+
from { stroke-dashoffset: var(--freehand-dash, 0px); }
|
|
36
|
+
to { stroke-dashoffset: 0; }
|
|
37
|
+
}
|
|
38
|
+
@keyframes freehand-arrow-drift {
|
|
39
|
+
0%, 100% { transform: translate(0px, 0px); }
|
|
40
|
+
50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
41
|
+
}
|
|
42
|
+
@keyframes freehand-arrow-settle {
|
|
43
|
+
from { transform: translate(0px, 0px); }
|
|
44
|
+
to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
45
|
+
}
|
|
46
|
+
@media (prefers-reduced-motion: reduce) {
|
|
47
|
+
.freehand-arrow, .freehand-arrow path { animation: none !important; }
|
|
48
|
+
.freehand-arrow path { stroke-dashoffset: 0 !important; }
|
|
49
|
+
}
|
|
50
|
+
`;
|
|
51
|
+
var ARROW_ANIMATION_DEFAULTS = {
|
|
52
|
+
/** Reveal the stroke with a dash sweep. */
|
|
53
|
+
draw: true,
|
|
54
|
+
/** Lean the arrow toward its tip. */
|
|
55
|
+
drift: true,
|
|
56
|
+
/** Multiplier over every duration - 2 plays twice as fast. */
|
|
57
|
+
speed: 1,
|
|
58
|
+
/** How long the shaft takes to draw, ms, before `speed`. */
|
|
59
|
+
duration: 900,
|
|
60
|
+
/** Wait before the first stroke appears, ms. */
|
|
61
|
+
delay: 150,
|
|
62
|
+
/** How far the arrow leans, px. */
|
|
63
|
+
distance: 5,
|
|
64
|
+
/** One full lean-and-return, ms. */
|
|
65
|
+
driftDuration: 2200,
|
|
66
|
+
/** Keep leaning. `false` settles once and stays put. */
|
|
67
|
+
repeat: true
|
|
68
|
+
};
|
|
69
|
+
function positive(value, fallback) {
|
|
70
|
+
const n = Number(value);
|
|
71
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
72
|
+
}
|
|
73
|
+
function atLeastZero(value, fallback) {
|
|
74
|
+
const n = Number(value);
|
|
75
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
76
|
+
}
|
|
77
|
+
function normalizeArrowAnimation(config) {
|
|
78
|
+
if (config === false || config === null) return false;
|
|
79
|
+
const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
|
|
80
|
+
const animation = {
|
|
81
|
+
draw: settings.draw !== false,
|
|
82
|
+
drift: settings.drift !== false,
|
|
83
|
+
speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
|
|
84
|
+
duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
|
|
85
|
+
delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
|
|
86
|
+
distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
|
|
87
|
+
driftDuration: positive(
|
|
88
|
+
settings.driftDuration,
|
|
89
|
+
ARROW_ANIMATION_DEFAULTS.driftDuration
|
|
90
|
+
),
|
|
91
|
+
repeat: settings.repeat !== false
|
|
92
|
+
};
|
|
93
|
+
if (!animation.draw && (!animation.drift || !animation.distance)) return false;
|
|
94
|
+
return animation;
|
|
95
|
+
}
|
|
96
|
+
var FLOW_PERIOD = 260;
|
|
97
|
+
function arrowTiming(animation) {
|
|
98
|
+
const duration = animation.duration / animation.speed;
|
|
99
|
+
const delay = animation.delay / animation.speed;
|
|
100
|
+
const drawn = animation.draw ? duration : 0;
|
|
101
|
+
return {
|
|
102
|
+
shaft: { duration, delay },
|
|
103
|
+
head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
|
|
104
|
+
flow: { duration: FLOW_PERIOD / animation.speed, delay },
|
|
105
|
+
drift: {
|
|
106
|
+
duration: animation.driftDuration / animation.speed,
|
|
107
|
+
delay: delay + drawn * 1.04
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
var injected = false;
|
|
112
|
+
function ensureArrowAnimationStyles() {
|
|
113
|
+
if (injected) return;
|
|
114
|
+
if (typeof document === "undefined" || !document.head) return;
|
|
115
|
+
injected = true;
|
|
116
|
+
if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
|
|
117
|
+
const style = document.createElement("style");
|
|
118
|
+
style.setAttribute(STYLE_MARKER, "");
|
|
119
|
+
style.textContent = CSS;
|
|
120
|
+
document.head.appendChild(style);
|
|
121
|
+
}
|
|
122
|
+
function time(ms) {
|
|
123
|
+
return `${Math.round(ms)}ms`;
|
|
124
|
+
}
|
|
125
|
+
function setAnimation(element, spec) {
|
|
126
|
+
const parts = [
|
|
127
|
+
spec.name,
|
|
128
|
+
time(spec.duration),
|
|
129
|
+
spec.easing,
|
|
130
|
+
time(spec.delay),
|
|
131
|
+
spec.iterations ?? "1"
|
|
132
|
+
];
|
|
133
|
+
if (spec.fill) parts.push(spec.fill);
|
|
134
|
+
element.style.animation = parts.join(" ");
|
|
135
|
+
}
|
|
136
|
+
function pathLength(path) {
|
|
137
|
+
try {
|
|
138
|
+
const length = path.getTotalLength();
|
|
139
|
+
return Number.isFinite(length) && length > 0 ? length : 0;
|
|
140
|
+
} catch {
|
|
141
|
+
return 0;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function dashPeriod(path) {
|
|
145
|
+
const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
|
|
146
|
+
if (!pattern.length) return 0;
|
|
147
|
+
const sum = pattern.reduce((total, n) => total + n, 0);
|
|
148
|
+
return pattern.length % 2 === 0 ? sum : sum * 2;
|
|
149
|
+
}
|
|
150
|
+
function applyDash(path, timing, elapsed, flow) {
|
|
151
|
+
const length = pathLength(path);
|
|
152
|
+
if (!length) return;
|
|
153
|
+
const period = dashPeriod(path);
|
|
154
|
+
if (period) {
|
|
155
|
+
path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
|
|
156
|
+
setAnimation(path, {
|
|
157
|
+
name: "freehand-arrow-dash",
|
|
158
|
+
duration: flow.duration,
|
|
159
|
+
easing: "linear",
|
|
160
|
+
delay: flow.delay - elapsed,
|
|
161
|
+
iterations: "infinite"
|
|
162
|
+
});
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
|
|
166
|
+
path.style.strokeDasharray = `${length.toFixed(2)}px`;
|
|
167
|
+
setAnimation(path, {
|
|
168
|
+
name: "freehand-arrow-dash",
|
|
169
|
+
duration: timing.duration,
|
|
170
|
+
easing: "ease-out",
|
|
171
|
+
delay: timing.delay - elapsed,
|
|
172
|
+
fill: "both"
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
function applyDrift(group, parts, timing, elapsed, animation) {
|
|
176
|
+
const dx = parts.end.x - parts.start.x;
|
|
177
|
+
const dy = parts.end.y - parts.start.y;
|
|
178
|
+
const length = Math.hypot(dx, dy);
|
|
179
|
+
if (!length) return;
|
|
180
|
+
const scale = animation.distance / length;
|
|
181
|
+
group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
|
|
182
|
+
group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
|
|
183
|
+
setAnimation(
|
|
184
|
+
group,
|
|
185
|
+
animation.repeat ? {
|
|
186
|
+
name: "freehand-arrow-drift",
|
|
187
|
+
duration: timing.duration,
|
|
188
|
+
easing: "ease-in-out",
|
|
189
|
+
delay: timing.delay - elapsed,
|
|
190
|
+
iterations: "infinite"
|
|
191
|
+
} : {
|
|
192
|
+
// A single lean is a settle, not a round trip, so it takes half a cycle
|
|
193
|
+
name: "freehand-arrow-settle",
|
|
194
|
+
duration: timing.duration * 0.5,
|
|
195
|
+
easing: "ease-out",
|
|
196
|
+
delay: timing.delay - elapsed,
|
|
197
|
+
fill: "both"
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
function animateArrow(group, parts, animation, elapsed = 0) {
|
|
202
|
+
if (!animation || !group) return;
|
|
203
|
+
ensureArrowAnimationStyles();
|
|
204
|
+
group.setAttribute("class", "freehand-arrow");
|
|
205
|
+
const timing = arrowTiming(animation);
|
|
206
|
+
const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
|
|
207
|
+
if (animation.draw) {
|
|
208
|
+
for (const path of parts.shaft) {
|
|
209
|
+
applyDash(path, timing.shaft, age, timing.flow);
|
|
210
|
+
}
|
|
211
|
+
for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
|
|
212
|
+
}
|
|
213
|
+
if (animation.drift && animation.distance > 0) {
|
|
214
|
+
applyDrift(group, parts, timing.drift, age, animation);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// src/locale.js
|
|
219
|
+
var DEFAULT_KEY = "default";
|
|
220
|
+
var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
|
|
221
|
+
var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
|
|
222
|
+
var LTR_STRONG = /[A-Za-zÀ-֏ऀ-Ⰰ-豈-ﬗA-Za-z]/;
|
|
223
|
+
function normalizeTag(tag) {
|
|
224
|
+
if (typeof tag !== "string") return "";
|
|
225
|
+
return tag.trim().replace(/_/g, "-").toLowerCase();
|
|
226
|
+
}
|
|
227
|
+
function isLocaleMap(value) {
|
|
228
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
229
|
+
const entries = Object.entries(value);
|
|
230
|
+
if (!entries.length) return false;
|
|
231
|
+
return entries.every(([key, text]) => {
|
|
232
|
+
if (typeof text !== "string") return false;
|
|
233
|
+
const tag = normalizeTag(key);
|
|
234
|
+
return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
function preferredLocales(override) {
|
|
238
|
+
const tags = [];
|
|
239
|
+
const add = (value) => {
|
|
240
|
+
const tag = normalizeTag(value);
|
|
241
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
242
|
+
};
|
|
243
|
+
if (Array.isArray(override)) override.forEach(add);
|
|
244
|
+
else add(override);
|
|
245
|
+
const nav = typeof navigator === "undefined" ? null : navigator;
|
|
246
|
+
if (nav) {
|
|
247
|
+
if (Array.isArray(nav.languages)) nav.languages.forEach(add);
|
|
248
|
+
add(nav.language);
|
|
249
|
+
}
|
|
250
|
+
if (typeof document !== "undefined") add(document.documentElement?.lang);
|
|
251
|
+
return tags;
|
|
252
|
+
}
|
|
253
|
+
function matchLocale(tags, preferred) {
|
|
254
|
+
const available = tags.map(normalizeTag);
|
|
255
|
+
for (const want of preferred) {
|
|
256
|
+
let tag = want;
|
|
257
|
+
while (tag) {
|
|
258
|
+
const exact = available.indexOf(tag);
|
|
259
|
+
if (exact !== -1) return exact;
|
|
260
|
+
const cut = tag.lastIndexOf("-");
|
|
261
|
+
tag = cut > 0 ? tag.slice(0, cut) : "";
|
|
262
|
+
}
|
|
263
|
+
const base = want.split("-")[0];
|
|
264
|
+
const sibling = available.findIndex((key) => key.split("-")[0] === base);
|
|
265
|
+
if (sibling !== -1) return sibling;
|
|
266
|
+
}
|
|
267
|
+
return -1;
|
|
268
|
+
}
|
|
269
|
+
function resolveLocalizedText(text, locale = null) {
|
|
270
|
+
if (typeof text === "string") return { text, locale: null };
|
|
271
|
+
if (!isLocaleMap(text)) return { text: "", locale: null };
|
|
272
|
+
const keys = Object.keys(text);
|
|
273
|
+
const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
|
|
274
|
+
const index = matchLocale(tags, preferredLocales(locale));
|
|
275
|
+
if (index !== -1) {
|
|
276
|
+
return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
|
|
277
|
+
}
|
|
278
|
+
const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
|
|
279
|
+
if (fallback) return { text: text[fallback], locale: null };
|
|
280
|
+
return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
|
|
281
|
+
}
|
|
282
|
+
function isRtlText(text) {
|
|
283
|
+
if (typeof text !== "string") return false;
|
|
284
|
+
const rtl = text.search(RTL_STRONG);
|
|
285
|
+
if (rtl === -1) return false;
|
|
286
|
+
const ltr = text.search(LTR_STRONG);
|
|
287
|
+
return ltr === -1 || rtl < ltr;
|
|
288
|
+
}
|
|
289
|
+
|
|
31
290
|
// src/utils.js
|
|
32
291
|
var DEFAULT_OPTIONS = {
|
|
33
292
|
border: true,
|
|
@@ -46,7 +305,17 @@ var DEFAULT_OPTIONS = {
|
|
|
46
305
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
47
306
|
fontFamily: null,
|
|
48
307
|
/** Fetch Caveat when the page has not provided it. */
|
|
49
|
-
autoLoadFont: true
|
|
308
|
+
autoLoadFont: true,
|
|
309
|
+
/**
|
|
310
|
+
* Language for a note written in more than one. Omit to follow the visitor's
|
|
311
|
+
* own languages, which is what their operating system is set to.
|
|
312
|
+
*/
|
|
313
|
+
locale: null,
|
|
314
|
+
/**
|
|
315
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
316
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
317
|
+
*/
|
|
318
|
+
zIndex: null
|
|
50
319
|
};
|
|
51
320
|
function resolveElement(target) {
|
|
52
321
|
if (!target) return null;
|
|
@@ -60,8 +329,15 @@ function resolveElement(target) {
|
|
|
60
329
|
}
|
|
61
330
|
function mergeOptions(options = {}) {
|
|
62
331
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
63
|
-
if (merged.note
|
|
64
|
-
merged.note
|
|
332
|
+
if (merged.note) {
|
|
333
|
+
const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
|
|
334
|
+
merged.note = {
|
|
335
|
+
...config,
|
|
336
|
+
position: config.position ?? "top-right",
|
|
337
|
+
// The note carries the chosen language, so `renderAnnotation` can resolve
|
|
338
|
+
// a multilingual note on its own.
|
|
339
|
+
locale: config.locale ?? merged.locale ?? null
|
|
340
|
+
};
|
|
65
341
|
}
|
|
66
342
|
if (merged.arrow) {
|
|
67
343
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -71,7 +347,10 @@ function mergeOptions(options = {}) {
|
|
|
71
347
|
// explicit `from` is honoured whether or not there is a note.
|
|
72
348
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
73
349
|
to: config.to ?? "edge",
|
|
74
|
-
style: config.style ?? "curved"
|
|
350
|
+
style: config.style ?? "curved",
|
|
351
|
+
// Arrows draw themselves on and lean toward their target by default;
|
|
352
|
+
// `animate: false` opts out, an object tunes speed and gestures
|
|
353
|
+
animate: normalizeArrowAnimation(config.animate)
|
|
75
354
|
};
|
|
76
355
|
}
|
|
77
356
|
if (merged.decorations === true) {
|
|
@@ -139,6 +418,40 @@ function getScrollableAncestors(element) {
|
|
|
139
418
|
ancestors.push(window);
|
|
140
419
|
return ancestors;
|
|
141
420
|
}
|
|
421
|
+
function resolveOverlayZIndex(element, override) {
|
|
422
|
+
if (override != null) return override;
|
|
423
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
424
|
+
if (zIndex !== "auto") {
|
|
425
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
426
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
427
|
+
}
|
|
428
|
+
let node = element.parentElement;
|
|
429
|
+
while (node && node !== document.documentElement) {
|
|
430
|
+
const style = getComputedStyle(node);
|
|
431
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
432
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
433
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
434
|
+
}
|
|
435
|
+
node = node.parentElement;
|
|
436
|
+
}
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
439
|
+
function insertOverlaySvg(svg, element) {
|
|
440
|
+
const parent = element.parentNode;
|
|
441
|
+
if (!parent) {
|
|
442
|
+
document.body.appendChild(svg);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
446
|
+
}
|
|
447
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
448
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
449
|
+
if (zIndex == null) {
|
|
450
|
+
svg.style.zIndex = "";
|
|
451
|
+
} else {
|
|
452
|
+
svg.style.zIndex = String(zIndex);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
142
455
|
|
|
143
456
|
// src/geometry.js
|
|
144
457
|
function averageCornerRadius(element) {
|
|
@@ -286,6 +599,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
286
599
|
|
|
287
600
|
// src/renderer.js
|
|
288
601
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
602
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
289
603
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
290
604
|
var NOTE_FONT_SIZE = 19;
|
|
291
605
|
function clearSvg(svg) {
|
|
@@ -531,6 +845,15 @@ function appendPath(svg, d, style) {
|
|
|
531
845
|
svg.appendChild(path);
|
|
532
846
|
return path;
|
|
533
847
|
}
|
|
848
|
+
function appendGlow(svg, radius, style) {
|
|
849
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
850
|
+
circle.setAttribute("r", fmt(radius));
|
|
851
|
+
circle.setAttribute("fill", style.color);
|
|
852
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
853
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
854
|
+
svg.appendChild(circle);
|
|
855
|
+
return circle;
|
|
856
|
+
}
|
|
534
857
|
function rectPerimeter(width, height, radius) {
|
|
535
858
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
536
859
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -617,20 +940,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
617
940
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
618
941
|
};
|
|
619
942
|
}
|
|
620
|
-
|
|
943
|
+
var STRADDLE = 0.75;
|
|
944
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
621
945
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
622
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
623
946
|
const knot = 0.46 + random() * 0.12;
|
|
624
947
|
const points = [];
|
|
625
948
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
626
949
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
627
950
|
const pivot = at(knot);
|
|
628
|
-
const
|
|
951
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
952
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
953
|
+
const heading = normalize(derivative);
|
|
629
954
|
const normal = { x: -heading.y, y: heading.x };
|
|
630
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
631
955
|
const centre = {
|
|
632
|
-
x: pivot.x + normal.x * radius * side,
|
|
633
|
-
y: pivot.y + normal.y * radius * side
|
|
956
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
957
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
634
958
|
};
|
|
635
959
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
636
960
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -638,15 +962,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
638
962
|
for (let i = 1; i <= steps; i++) {
|
|
639
963
|
const progress = i / steps;
|
|
640
964
|
const angle = from + turns * progress * side;
|
|
965
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
641
966
|
const drift = radius * 0.55 * progress;
|
|
642
967
|
points.push({
|
|
643
|
-
x: centre.x + Math.cos(angle) *
|
|
644
|
-
y: centre.y + Math.sin(angle) *
|
|
968
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
969
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
645
970
|
});
|
|
646
971
|
}
|
|
972
|
+
const exit = points[points.length - 1];
|
|
973
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
974
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
975
|
+
const rejoin = at(resume);
|
|
976
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
977
|
+
const settle = 6;
|
|
647
978
|
const tail = 14;
|
|
648
979
|
for (let i = 1; i <= tail; i++) {
|
|
649
|
-
|
|
980
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
981
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
982
|
+
points.push({
|
|
983
|
+
x: point.x + normal.x * residual * fade,
|
|
984
|
+
y: point.y + normal.y * residual * fade
|
|
985
|
+
});
|
|
650
986
|
}
|
|
651
987
|
return points;
|
|
652
988
|
}
|
|
@@ -720,21 +1056,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
720
1056
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
721
1057
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
722
1058
|
const spread = 0.46 + random() * 0.1;
|
|
1059
|
+
const arms = [];
|
|
723
1060
|
for (const side of [-1, 1]) {
|
|
724
1061
|
const armAngle = angle + spread * side;
|
|
725
1062
|
const tail = {
|
|
726
1063
|
x: tip.x - length * Math.cos(armAngle),
|
|
727
1064
|
y: tip.y - length * Math.sin(armAngle)
|
|
728
1065
|
};
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
1066
|
+
arms.push(
|
|
1067
|
+
appendPath(
|
|
1068
|
+
svg,
|
|
1069
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1070
|
+
steps: 2,
|
|
1071
|
+
bow: 0.35 * side
|
|
1072
|
+
}),
|
|
1073
|
+
{ ...style, dashed: false }
|
|
1074
|
+
)
|
|
736
1075
|
);
|
|
737
1076
|
}
|
|
1077
|
+
return arms;
|
|
1078
|
+
}
|
|
1079
|
+
function bowOverTheTop(perpendicular) {
|
|
1080
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1081
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
738
1082
|
}
|
|
739
1083
|
function boundsOf(points) {
|
|
740
1084
|
const xs = points.map((p) => p.x);
|
|
@@ -746,7 +1090,7 @@ function boundsOf(points) {
|
|
|
746
1090
|
height: Math.max(...ys) - Math.min(...ys)
|
|
747
1091
|
};
|
|
748
1092
|
}
|
|
749
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1093
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
750
1094
|
const random = createRandom(seed + 17);
|
|
751
1095
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
752
1096
|
const dx = end.x - start.x;
|
|
@@ -755,6 +1099,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
755
1099
|
const dashed = arrowOptions.style === "dotted";
|
|
756
1100
|
let tipAngle = Math.atan2(dy, dx);
|
|
757
1101
|
let shaft;
|
|
1102
|
+
let ghost = null;
|
|
758
1103
|
const hull = [start, end];
|
|
759
1104
|
if (arrowOptions.style === "straight") {
|
|
760
1105
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -768,7 +1113,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
768
1113
|
y: rect.y + rect.height / 2 - mid.y
|
|
769
1114
|
};
|
|
770
1115
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
771
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1116
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1117
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1118
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1119
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1120
|
+
bowOverTheTop(perpendicular)
|
|
1121
|
+
);
|
|
772
1122
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
773
1123
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
774
1124
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -783,34 +1133,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
783
1133
|
{ x: c2x, y: c2y },
|
|
784
1134
|
end,
|
|
785
1135
|
radius,
|
|
786
|
-
random
|
|
1136
|
+
random,
|
|
1137
|
+
-sign
|
|
787
1138
|
);
|
|
788
1139
|
shaft = catmullRomPath(points, false);
|
|
789
1140
|
hull.push(...points);
|
|
790
1141
|
const last = points[points.length - 1];
|
|
791
1142
|
const prior = points[points.length - 2] ?? start;
|
|
792
1143
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1144
|
+
} else {
|
|
1145
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1146
|
+
if (style.roughness >= 1.1) {
|
|
1147
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1148
|
+
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)}`;
|
|
1149
|
+
}
|
|
1150
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
796
1151
|
}
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
1152
|
+
}
|
|
1153
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1154
|
+
svg.appendChild(group);
|
|
1155
|
+
const shaftPaths = [];
|
|
1156
|
+
if (ghost) {
|
|
1157
|
+
shaftPaths.push(
|
|
1158
|
+
appendPath(group, ghost, {
|
|
802
1159
|
...style,
|
|
803
1160
|
opacity: style.opacity * 0.3,
|
|
804
1161
|
strokeWidth: style.strokeWidth * 0.7,
|
|
805
1162
|
dashed
|
|
806
|
-
})
|
|
807
|
-
|
|
808
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1163
|
+
})
|
|
1164
|
+
);
|
|
809
1165
|
}
|
|
810
|
-
appendPath(
|
|
811
|
-
drawArrowHead(
|
|
1166
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1167
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1168
|
+
animateArrow(
|
|
1169
|
+
group,
|
|
1170
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1171
|
+
arrowOptions.animate,
|
|
1172
|
+
context.elapsed
|
|
1173
|
+
);
|
|
812
1174
|
const bounds = boundsOf(hull);
|
|
813
|
-
return { start, end, bounds };
|
|
1175
|
+
return { start, end, bounds, group };
|
|
814
1176
|
}
|
|
815
1177
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
816
1178
|
if (width <= 4) return;
|
|
@@ -877,6 +1239,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
877
1239
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
878
1240
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
879
1241
|
const lineHeight = fontSize * 1.05;
|
|
1242
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
880
1243
|
const group = document.createElementNS(SVG_NS, "g");
|
|
881
1244
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
882
1245
|
textEl.setAttribute("x", "0");
|
|
@@ -889,6 +1252,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
889
1252
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
890
1253
|
textEl.setAttribute("text-anchor", "start");
|
|
891
1254
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1255
|
+
if (rtl) {
|
|
1256
|
+
textEl.setAttribute("direction", "rtl");
|
|
1257
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1258
|
+
}
|
|
1259
|
+
if (options.locale) {
|
|
1260
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1261
|
+
}
|
|
892
1262
|
const tspans = lines.map((line, index) => {
|
|
893
1263
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
894
1264
|
tspan.textContent = line;
|
|
@@ -917,15 +1287,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
917
1287
|
height,
|
|
918
1288
|
ascent,
|
|
919
1289
|
descent,
|
|
920
|
-
fontSize
|
|
1290
|
+
fontSize,
|
|
1291
|
+
rtl
|
|
921
1292
|
};
|
|
922
1293
|
}
|
|
923
1294
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
924
1295
|
const random = createRandom(seed + 55);
|
|
925
1296
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
926
|
-
note.
|
|
1297
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1298
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
927
1299
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
928
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1300
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
929
1301
|
const cx = box.x + box.width / 2;
|
|
930
1302
|
const cy = box.y + box.height / 2;
|
|
931
1303
|
note.group.setAttribute(
|
|
@@ -934,9 +1306,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
934
1306
|
);
|
|
935
1307
|
if (options.underline !== false) {
|
|
936
1308
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1309
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
937
1310
|
drawUnderline(
|
|
938
1311
|
note.group,
|
|
939
|
-
|
|
1312
|
+
underlineX,
|
|
940
1313
|
box.y + box.height + 2,
|
|
941
1314
|
lastWidth - 2,
|
|
942
1315
|
{
|
|
@@ -964,7 +1337,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
964
1337
|
svg.style.height = `${height}px`;
|
|
965
1338
|
svg.style.pointerEvents = "none";
|
|
966
1339
|
svg.style.overflow = "visible";
|
|
967
|
-
svg.style.
|
|
1340
|
+
svg.style.willChange = "transform";
|
|
968
1341
|
return svg;
|
|
969
1342
|
}
|
|
970
1343
|
|
|
@@ -1011,12 +1384,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1011
1384
|
return { ...box, x: box.x + dx };
|
|
1012
1385
|
}
|
|
1013
1386
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
1014
|
-
|
|
1387
|
+
const { text, locale } = resolveLocalizedText(
|
|
1388
|
+
noteOptions?.text,
|
|
1389
|
+
noteOptions?.locale
|
|
1390
|
+
);
|
|
1391
|
+
if (!text) return null;
|
|
1015
1392
|
const band = layout.band ?? null;
|
|
1016
1393
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1017
1394
|
const random = createRandom(seed + 9);
|
|
1018
|
-
const note = createNoteText(svg,
|
|
1019
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1395
|
+
const note = createNoteText(svg, text, style, {
|
|
1396
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1397
|
+
locale
|
|
1020
1398
|
});
|
|
1021
1399
|
const tilt = (random() - 0.45) * 7;
|
|
1022
1400
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1058,6 +1436,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1058
1436
|
strokeWidth: style.strokeWidth * scale
|
|
1059
1437
|
});
|
|
1060
1438
|
}
|
|
1439
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1440
|
+
const count = 6;
|
|
1441
|
+
const points = [];
|
|
1442
|
+
for (let i = 0; i < count; i++) {
|
|
1443
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1444
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1445
|
+
points.push({
|
|
1446
|
+
x: x + Math.cos(angle) * r,
|
|
1447
|
+
y: y + Math.sin(angle) * r
|
|
1448
|
+
});
|
|
1449
|
+
}
|
|
1450
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1451
|
+
...style,
|
|
1452
|
+
fill: style.color,
|
|
1453
|
+
strokeWidth: 0
|
|
1454
|
+
});
|
|
1455
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1456
|
+
return blob;
|
|
1457
|
+
}
|
|
1061
1458
|
function drawHeart(parent, size, style, random) {
|
|
1062
1459
|
const s = size;
|
|
1063
1460
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1081,26 +1478,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1081
1478
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1082
1479
|
"Z"
|
|
1083
1480
|
].join(" ");
|
|
1084
|
-
appendPath(parent, path, style);
|
|
1481
|
+
return appendPath(parent, path, style);
|
|
1085
1482
|
}
|
|
1086
1483
|
function drawSparkle(parent, size, style, random) {
|
|
1087
1484
|
twinkle(
|
|
1088
1485
|
parent,
|
|
1089
|
-
size * (0.
|
|
1486
|
+
size * (0.62 + random() * 0.12),
|
|
1090
1487
|
size,
|
|
1091
|
-
size * (0.
|
|
1488
|
+
size * (0.14 + random() * 0.08),
|
|
1092
1489
|
style
|
|
1093
1490
|
);
|
|
1094
1491
|
}
|
|
1095
1492
|
function drawStar(parent, size, style, random) {
|
|
1096
1493
|
twinkle(
|
|
1097
1494
|
parent,
|
|
1098
|
-
size * (0.
|
|
1099
|
-
size,
|
|
1100
|
-
size * (0.
|
|
1495
|
+
size * (0.62 + random() * 0.12),
|
|
1496
|
+
size * (0.8 + random() * 0.12),
|
|
1497
|
+
size * (0.14 + random() * 0.08),
|
|
1101
1498
|
style
|
|
1102
1499
|
);
|
|
1103
1500
|
}
|
|
1501
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1502
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1503
|
+
color: style.color,
|
|
1504
|
+
opacity: style.opacity * 0.22
|
|
1505
|
+
});
|
|
1506
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1507
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1508
|
+
const bluntness = size * 0.19;
|
|
1509
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1510
|
+
...style,
|
|
1511
|
+
fill: style.color,
|
|
1512
|
+
strokeWidth: bluntness
|
|
1513
|
+
});
|
|
1514
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1515
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1516
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1517
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1518
|
+
const corners = [
|
|
1519
|
+
{ x: 0, y: -(armY + gap) },
|
|
1520
|
+
{ x: armX + gap, y: 0 },
|
|
1521
|
+
{ x: 0, y: armY + gap },
|
|
1522
|
+
{ x: -(armX + gap), y: 0 }
|
|
1523
|
+
];
|
|
1524
|
+
for (const { x, y } of corners) {
|
|
1525
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1526
|
+
drawFleck(
|
|
1527
|
+
parent,
|
|
1528
|
+
x * jitter,
|
|
1529
|
+
y * jitter,
|
|
1530
|
+
size * (0.1 + random() * 0.03),
|
|
1531
|
+
style,
|
|
1532
|
+
random
|
|
1533
|
+
);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1104
1536
|
function drawTwinkle(parent, size, style, random) {
|
|
1105
1537
|
const count = 5;
|
|
1106
1538
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1208,46 +1640,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1208
1640
|
);
|
|
1209
1641
|
}
|
|
1210
1642
|
}
|
|
1211
|
-
function drawDots(parent, size, style, random) {
|
|
1212
|
-
let x = -size * 0.7;
|
|
1213
|
-
for (let i = 0; i < 3; i++) {
|
|
1214
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1215
|
-
x += size * (0.6 + random() * 0.25);
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
function drawStroke(parent, size, style, random) {
|
|
1219
|
-
appendPath(
|
|
1220
|
-
parent,
|
|
1221
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1222
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1223
|
-
steps: 4
|
|
1224
|
-
}),
|
|
1225
|
-
style
|
|
1226
|
-
);
|
|
1227
|
-
}
|
|
1228
|
-
function drawSteam(parent, size, style, random) {
|
|
1229
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1230
|
-
for (let i = 0; i < 2; i++) {
|
|
1231
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1232
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1233
|
-
appendPath(
|
|
1234
|
-
parent,
|
|
1235
|
-
`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)}`,
|
|
1236
|
-
style
|
|
1237
|
-
);
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
1643
|
var DECORATION_DRAWERS = {
|
|
1241
1644
|
heart: drawHeart,
|
|
1242
1645
|
sparkle: drawSparkle,
|
|
1243
1646
|
star: drawStar,
|
|
1244
1647
|
twinkle: drawTwinkle,
|
|
1648
|
+
glimmer: drawGlimmer,
|
|
1245
1649
|
smiley: drawSmiley,
|
|
1246
1650
|
emphasis: drawEmphasis,
|
|
1247
|
-
arcs: drawArcs
|
|
1248
|
-
dots: drawDots,
|
|
1249
|
-
stroke: drawStroke,
|
|
1250
|
-
steam: drawSteam
|
|
1651
|
+
arcs: drawArcs
|
|
1251
1652
|
};
|
|
1252
1653
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1253
1654
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1258,12 +1659,10 @@ var TYPE_TRAITS = {
|
|
|
1258
1659
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1259
1660
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1260
1661
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1662
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1261
1663
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1262
1664
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1263
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1264
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1265
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1266
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1665
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1267
1666
|
};
|
|
1268
1667
|
var DEFAULT_TYPES = [
|
|
1269
1668
|
"twinkle",
|
|
@@ -1466,6 +1865,9 @@ function ensureHandwrittenFont() {
|
|
|
1466
1865
|
|
|
1467
1866
|
// src/doodle.js
|
|
1468
1867
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1868
|
+
function now() {
|
|
1869
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1870
|
+
}
|
|
1469
1871
|
function localBand(overlayRect, inset = 8) {
|
|
1470
1872
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1471
1873
|
return {
|
|
@@ -1482,19 +1884,24 @@ var DoodleOverlay = class {
|
|
|
1482
1884
|
this.element = element;
|
|
1483
1885
|
this.options = options;
|
|
1484
1886
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1887
|
+
this.startedAt = now();
|
|
1485
1888
|
this.svg = null;
|
|
1486
1889
|
this.resizeObserver = null;
|
|
1487
1890
|
this.mutationObserver = null;
|
|
1488
1891
|
this.pendingFrame = null;
|
|
1489
|
-
this.
|
|
1490
|
-
this.
|
|
1892
|
+
this.pendingRepositionFrame = null;
|
|
1893
|
+
this.basePosition = { x: 0, y: 0 };
|
|
1894
|
+
this.listeners = [];
|
|
1895
|
+
this.onScroll = this.scheduleReposition.bind(this);
|
|
1491
1896
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1897
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1492
1898
|
this.childElements = [];
|
|
1493
1899
|
this.mount();
|
|
1494
1900
|
}
|
|
1495
1901
|
mount() {
|
|
1496
1902
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1497
|
-
|
|
1903
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1904
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1498
1905
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1499
1906
|
this.resizeObserver.observe(this.element);
|
|
1500
1907
|
if (this.options.children) {
|
|
@@ -1508,10 +1915,20 @@ var DoodleOverlay = class {
|
|
|
1508
1915
|
});
|
|
1509
1916
|
for (const target of getScrollableAncestors(this.element)) {
|
|
1510
1917
|
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1511
|
-
this.
|
|
1918
|
+
this.listeners.push({ target, type: "scroll", handler: this.onScroll });
|
|
1512
1919
|
}
|
|
1513
1920
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1514
|
-
this.
|
|
1921
|
+
this.listeners.push({
|
|
1922
|
+
target: window,
|
|
1923
|
+
type: "resize",
|
|
1924
|
+
handler: this.onResize
|
|
1925
|
+
});
|
|
1926
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1927
|
+
this.listeners.push({
|
|
1928
|
+
target: window,
|
|
1929
|
+
type: "languagechange",
|
|
1930
|
+
handler: this.onLanguageChange
|
|
1931
|
+
});
|
|
1515
1932
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1516
1933
|
ensureHandwrittenFont();
|
|
1517
1934
|
}
|
|
@@ -1535,6 +1952,46 @@ var DoodleOverlay = class {
|
|
|
1535
1952
|
this.update();
|
|
1536
1953
|
});
|
|
1537
1954
|
}
|
|
1955
|
+
scheduleReposition() {
|
|
1956
|
+
if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
|
|
1957
|
+
return;
|
|
1958
|
+
}
|
|
1959
|
+
this.pendingRepositionFrame = scheduleFrame(() => {
|
|
1960
|
+
this.pendingRepositionFrame = null;
|
|
1961
|
+
this.reposition();
|
|
1962
|
+
});
|
|
1963
|
+
}
|
|
1964
|
+
/**
|
|
1965
|
+
* Cheap scroll-driven path: slide the overlay to the element's new
|
|
1966
|
+
* viewport position without touching its contents. Scrolling can't change
|
|
1967
|
+
* an element's size, so the rects, viewBox and every hand-drawn stroke
|
|
1968
|
+
* inside them are still valid - only the overlay's position needs to move.
|
|
1969
|
+
*
|
|
1970
|
+
* That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
|
|
1971
|
+
* same box-geometry pass as layout, so the browser re-checks layout on
|
|
1972
|
+
* every write; `transform` is composited, so the scroll path never
|
|
1973
|
+
* touches layout at all.
|
|
1974
|
+
*/
|
|
1975
|
+
reposition() {
|
|
1976
|
+
if (!this.svg || !this.element.isConnected) return;
|
|
1977
|
+
const { padding, radius } = this.options;
|
|
1978
|
+
const targets = this.getTargets();
|
|
1979
|
+
const margin = this.overlayMargin();
|
|
1980
|
+
const absoluteRects = targets.map(
|
|
1981
|
+
(target) => getElementBounds(target, padding, radius)
|
|
1982
|
+
);
|
|
1983
|
+
const overlayRect = unionRects(absoluteRects, margin);
|
|
1984
|
+
const currentWidth = parseFloat(this.svg.style.width) || 0;
|
|
1985
|
+
const currentHeight = parseFloat(this.svg.style.height) || 0;
|
|
1986
|
+
const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
|
|
1987
|
+
if (resized) {
|
|
1988
|
+
this.update();
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
const dx = overlayRect.x - this.basePosition.x;
|
|
1992
|
+
const dy = overlayRect.y - this.basePosition.y;
|
|
1993
|
+
this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
1994
|
+
}
|
|
1538
1995
|
getTargets() {
|
|
1539
1996
|
if (this.options.children) {
|
|
1540
1997
|
const nodes = this.element.querySelectorAll(this.options.children);
|
|
@@ -1547,11 +2004,13 @@ var DoodleOverlay = class {
|
|
|
1547
2004
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1548
2005
|
const targets = this.getTargets();
|
|
1549
2006
|
const margin = this.overlayMargin();
|
|
2007
|
+
const elapsed = now() - this.startedAt;
|
|
1550
2008
|
const absoluteRects = targets.map(
|
|
1551
2009
|
(target) => getElementBounds(target, padding, radius)
|
|
1552
2010
|
);
|
|
1553
2011
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1554
2012
|
clearSvg(this.svg);
|
|
2013
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1555
2014
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1556
2015
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1557
2016
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
@@ -1559,6 +2018,8 @@ var DoodleOverlay = class {
|
|
|
1559
2018
|
this.svg.style.top = `${overlayRect.y}px`;
|
|
1560
2019
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1561
2020
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
2021
|
+
this.svg.style.transform = "";
|
|
2022
|
+
this.basePosition = { x: overlayRect.x, y: overlayRect.y };
|
|
1562
2023
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
1563
2024
|
const noteStyle = {
|
|
1564
2025
|
color,
|
|
@@ -1598,7 +2059,8 @@ var DoodleOverlay = class {
|
|
|
1598
2059
|
this.options.arrow,
|
|
1599
2060
|
strokeStyle,
|
|
1600
2061
|
seed,
|
|
1601
|
-
note ? note.box : null
|
|
2062
|
+
note ? note.box : null,
|
|
2063
|
+
{ elapsed }
|
|
1602
2064
|
) : null;
|
|
1603
2065
|
if (this.options.decorations) {
|
|
1604
2066
|
renderDecorations(
|
|
@@ -1625,7 +2087,7 @@ var DoodleOverlay = class {
|
|
|
1625
2087
|
overlayMargin() {
|
|
1626
2088
|
const { note, arrow, decorations } = this.options;
|
|
1627
2089
|
if (!note && !arrow && !decorations) return 48;
|
|
1628
|
-
const text =
|
|
2090
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1629
2091
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1630
2092
|
}
|
|
1631
2093
|
destroy() {
|
|
@@ -1633,6 +2095,10 @@ var DoodleOverlay = class {
|
|
|
1633
2095
|
cancelFrame(this.pendingFrame);
|
|
1634
2096
|
this.pendingFrame = null;
|
|
1635
2097
|
}
|
|
2098
|
+
if (this.pendingRepositionFrame != null) {
|
|
2099
|
+
cancelFrame(this.pendingRepositionFrame);
|
|
2100
|
+
this.pendingRepositionFrame = null;
|
|
2101
|
+
}
|
|
1636
2102
|
if (this.resizeObserver) {
|
|
1637
2103
|
this.resizeObserver.disconnect();
|
|
1638
2104
|
this.resizeObserver = null;
|
|
@@ -1645,10 +2111,10 @@ var DoodleOverlay = class {
|
|
|
1645
2111
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1646
2112
|
this.onFontsLoaded = null;
|
|
1647
2113
|
}
|
|
1648
|
-
for (const { target, type } of this.
|
|
1649
|
-
target.removeEventListener(type,
|
|
2114
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2115
|
+
target.removeEventListener(type, handler);
|
|
1650
2116
|
}
|
|
1651
|
-
this.
|
|
2117
|
+
this.listeners = [];
|
|
1652
2118
|
if (this.svg?.parentNode) {
|
|
1653
2119
|
this.svg.parentNode.removeChild(this.svg);
|
|
1654
2120
|
}
|
|
@@ -1689,7 +2155,8 @@ var OPTION_KEYS = [
|
|
|
1689
2155
|
"decorations",
|
|
1690
2156
|
"addBreaks",
|
|
1691
2157
|
"fontFamily",
|
|
1692
|
-
"autoLoadFont"
|
|
2158
|
+
"autoLoadFont",
|
|
2159
|
+
"locale"
|
|
1693
2160
|
];
|
|
1694
2161
|
function collectOptions(props) {
|
|
1695
2162
|
const options = {};
|
|
@@ -1741,6 +2208,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1741
2208
|
addBreaks,
|
|
1742
2209
|
fontFamily,
|
|
1743
2210
|
autoLoadFont,
|
|
2211
|
+
locale,
|
|
1744
2212
|
childSelector,
|
|
1745
2213
|
...rest
|
|
1746
2214
|
} = props;
|
|
@@ -1760,6 +2228,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1760
2228
|
addBreaks,
|
|
1761
2229
|
fontFamily,
|
|
1762
2230
|
autoLoadFont,
|
|
2231
|
+
locale,
|
|
1763
2232
|
childSelector
|
|
1764
2233
|
}),
|
|
1765
2234
|
[
|
|
@@ -1777,6 +2246,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1777
2246
|
JSON.stringify(addBreaks ?? null),
|
|
1778
2247
|
fontFamily,
|
|
1779
2248
|
autoLoadFont,
|
|
2249
|
+
JSON.stringify(locale ?? null),
|
|
1780
2250
|
childSelector
|
|
1781
2251
|
]
|
|
1782
2252
|
);
|