@aznabee/freehand-ui 0.1.5 → 0.1.7
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 +121 -11
- package/dist/freehand-ui.cjs +513 -120
- package/dist/freehand-ui.js +513 -120
- package/dist/index.d.ts +56 -6
- package/dist/react.cjs +518 -121
- package/dist/react.d.ts +2 -0
- package/dist/react.js +518 -121
- 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,
|
|
@@ -30,6 +289,11 @@ var DEFAULT_OPTIONS = {
|
|
|
30
289
|
fontFamily: null,
|
|
31
290
|
/** Fetch Caveat when the page has not provided it. */
|
|
32
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,
|
|
33
297
|
/**
|
|
34
298
|
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
35
299
|
* doodles stay above the annotated element but below modals and other UI.
|
|
@@ -48,8 +312,15 @@ function resolveElement(target) {
|
|
|
48
312
|
}
|
|
49
313
|
function mergeOptions(options = {}) {
|
|
50
314
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
51
|
-
if (merged.note
|
|
52
|
-
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
|
+
};
|
|
53
324
|
}
|
|
54
325
|
if (merged.arrow) {
|
|
55
326
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -59,7 +330,10 @@ function mergeOptions(options = {}) {
|
|
|
59
330
|
// explicit `from` is honoured whether or not there is a note.
|
|
60
331
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
61
332
|
to: config.to ?? "edge",
|
|
62
|
-
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)
|
|
63
337
|
};
|
|
64
338
|
}
|
|
65
339
|
if (merged.decorations === true) {
|
|
@@ -113,20 +387,6 @@ function isValidPosition(position) {
|
|
|
113
387
|
"center"
|
|
114
388
|
].includes(position);
|
|
115
389
|
}
|
|
116
|
-
function getScrollableAncestors(element) {
|
|
117
|
-
const ancestors = [];
|
|
118
|
-
let node = element.parentElement;
|
|
119
|
-
while (node && node !== document.documentElement) {
|
|
120
|
-
const style = getComputedStyle(node);
|
|
121
|
-
const overflow = style.overflow + style.overflowY + style.overflowX;
|
|
122
|
-
if (/(auto|scroll|overlay)/.test(overflow)) {
|
|
123
|
-
ancestors.push(node);
|
|
124
|
-
}
|
|
125
|
-
node = node.parentElement;
|
|
126
|
-
}
|
|
127
|
-
ancestors.push(window);
|
|
128
|
-
return ancestors;
|
|
129
|
-
}
|
|
130
390
|
function resolveOverlayZIndex(element, override) {
|
|
131
391
|
if (override != null) return override;
|
|
132
392
|
const { zIndex, position } = getComputedStyle(element);
|
|
@@ -145,13 +405,45 @@ function resolveOverlayZIndex(element, override) {
|
|
|
145
405
|
}
|
|
146
406
|
return null;
|
|
147
407
|
}
|
|
408
|
+
var ITEM_PLACEMENT_PROPERTIES = [
|
|
409
|
+
"order",
|
|
410
|
+
"flexGrow",
|
|
411
|
+
"flexShrink",
|
|
412
|
+
"flexBasis",
|
|
413
|
+
"alignSelf",
|
|
414
|
+
"justifySelf",
|
|
415
|
+
"gridColumn",
|
|
416
|
+
"gridRow",
|
|
417
|
+
"gridArea"
|
|
418
|
+
];
|
|
419
|
+
function isInlineDisplay(computed) {
|
|
420
|
+
return computed.display.startsWith("inline");
|
|
421
|
+
}
|
|
148
422
|
function insertOverlaySvg(svg, element) {
|
|
149
423
|
const parent = element.parentNode;
|
|
150
424
|
if (!parent) {
|
|
151
425
|
document.body.appendChild(svg);
|
|
152
|
-
return;
|
|
426
|
+
return null;
|
|
427
|
+
}
|
|
428
|
+
const wrapper = document.createElement("div");
|
|
429
|
+
wrapper.style.position = "relative";
|
|
430
|
+
const computed = getComputedStyle(element);
|
|
431
|
+
wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
|
|
432
|
+
for (const property of ITEM_PLACEMENT_PROPERTIES) {
|
|
433
|
+
const value = computed[property];
|
|
434
|
+
if (value) wrapper.style[property] = value;
|
|
435
|
+
}
|
|
436
|
+
parent.replaceChild(wrapper, element);
|
|
437
|
+
wrapper.appendChild(element);
|
|
438
|
+
wrapper.appendChild(svg);
|
|
439
|
+
return wrapper;
|
|
440
|
+
}
|
|
441
|
+
function removeOverlaySvg(svg, wrapper, element) {
|
|
442
|
+
if (svg.parentNode) {
|
|
443
|
+
svg.parentNode.removeChild(svg);
|
|
153
444
|
}
|
|
154
|
-
|
|
445
|
+
if (!wrapper || !wrapper.parentNode) return;
|
|
446
|
+
wrapper.parentNode.replaceChild(element, wrapper);
|
|
155
447
|
}
|
|
156
448
|
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
157
449
|
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
@@ -308,6 +600,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
308
600
|
|
|
309
601
|
// src/renderer.js
|
|
310
602
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
603
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
311
604
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
312
605
|
var NOTE_FONT_SIZE = 19;
|
|
313
606
|
function clearSvg(svg) {
|
|
@@ -553,6 +846,15 @@ function appendPath(svg, d, style) {
|
|
|
553
846
|
svg.appendChild(path);
|
|
554
847
|
return path;
|
|
555
848
|
}
|
|
849
|
+
function appendGlow(svg, radius, style) {
|
|
850
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
851
|
+
circle.setAttribute("r", fmt(radius));
|
|
852
|
+
circle.setAttribute("fill", style.color);
|
|
853
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
854
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
855
|
+
svg.appendChild(circle);
|
|
856
|
+
return circle;
|
|
857
|
+
}
|
|
556
858
|
function rectPerimeter(width, height, radius) {
|
|
557
859
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
558
860
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -639,20 +941,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
639
941
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
640
942
|
};
|
|
641
943
|
}
|
|
642
|
-
|
|
944
|
+
var STRADDLE = 0.75;
|
|
945
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
643
946
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
644
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
645
947
|
const knot = 0.46 + random() * 0.12;
|
|
646
948
|
const points = [];
|
|
647
949
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
648
950
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
649
951
|
const pivot = at(knot);
|
|
650
|
-
const
|
|
952
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
953
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
954
|
+
const heading = normalize(derivative);
|
|
651
955
|
const normal = { x: -heading.y, y: heading.x };
|
|
652
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
653
956
|
const centre = {
|
|
654
|
-
x: pivot.x + normal.x * radius * side,
|
|
655
|
-
y: pivot.y + normal.y * radius * side
|
|
957
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
958
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
656
959
|
};
|
|
657
960
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
658
961
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -660,15 +963,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
660
963
|
for (let i = 1; i <= steps; i++) {
|
|
661
964
|
const progress = i / steps;
|
|
662
965
|
const angle = from + turns * progress * side;
|
|
966
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
663
967
|
const drift = radius * 0.55 * progress;
|
|
664
968
|
points.push({
|
|
665
|
-
x: centre.x + Math.cos(angle) *
|
|
666
|
-
y: centre.y + Math.sin(angle) *
|
|
969
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
970
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
667
971
|
});
|
|
668
972
|
}
|
|
973
|
+
const exit = points[points.length - 1];
|
|
974
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
975
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
976
|
+
const rejoin = at(resume);
|
|
977
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
978
|
+
const settle = 6;
|
|
669
979
|
const tail = 14;
|
|
670
980
|
for (let i = 1; i <= tail; i++) {
|
|
671
|
-
|
|
981
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
982
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
983
|
+
points.push({
|
|
984
|
+
x: point.x + normal.x * residual * fade,
|
|
985
|
+
y: point.y + normal.y * residual * fade
|
|
986
|
+
});
|
|
672
987
|
}
|
|
673
988
|
return points;
|
|
674
989
|
}
|
|
@@ -742,21 +1057,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
742
1057
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
743
1058
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
744
1059
|
const spread = 0.46 + random() * 0.1;
|
|
1060
|
+
const arms = [];
|
|
745
1061
|
for (const side of [-1, 1]) {
|
|
746
1062
|
const armAngle = angle + spread * side;
|
|
747
1063
|
const tail = {
|
|
748
1064
|
x: tip.x - length * Math.cos(armAngle),
|
|
749
1065
|
y: tip.y - length * Math.sin(armAngle)
|
|
750
1066
|
};
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
1067
|
+
arms.push(
|
|
1068
|
+
appendPath(
|
|
1069
|
+
svg,
|
|
1070
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1071
|
+
steps: 2,
|
|
1072
|
+
bow: 0.35 * side
|
|
1073
|
+
}),
|
|
1074
|
+
{ ...style, dashed: false }
|
|
1075
|
+
)
|
|
758
1076
|
);
|
|
759
1077
|
}
|
|
1078
|
+
return arms;
|
|
1079
|
+
}
|
|
1080
|
+
function bowOverTheTop(perpendicular) {
|
|
1081
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1082
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
760
1083
|
}
|
|
761
1084
|
function boundsOf(points) {
|
|
762
1085
|
const xs = points.map((p) => p.x);
|
|
@@ -768,7 +1091,7 @@ function boundsOf(points) {
|
|
|
768
1091
|
height: Math.max(...ys) - Math.min(...ys)
|
|
769
1092
|
};
|
|
770
1093
|
}
|
|
771
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1094
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
772
1095
|
const random = createRandom(seed + 17);
|
|
773
1096
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
774
1097
|
const dx = end.x - start.x;
|
|
@@ -777,6 +1100,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
777
1100
|
const dashed = arrowOptions.style === "dotted";
|
|
778
1101
|
let tipAngle = Math.atan2(dy, dx);
|
|
779
1102
|
let shaft;
|
|
1103
|
+
let ghost = null;
|
|
780
1104
|
const hull = [start, end];
|
|
781
1105
|
if (arrowOptions.style === "straight") {
|
|
782
1106
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -790,7 +1114,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
790
1114
|
y: rect.y + rect.height / 2 - mid.y
|
|
791
1115
|
};
|
|
792
1116
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
793
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1117
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1118
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1119
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1120
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1121
|
+
bowOverTheTop(perpendicular)
|
|
1122
|
+
);
|
|
794
1123
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
795
1124
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
796
1125
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -805,34 +1134,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
805
1134
|
{ x: c2x, y: c2y },
|
|
806
1135
|
end,
|
|
807
1136
|
radius,
|
|
808
|
-
random
|
|
1137
|
+
random,
|
|
1138
|
+
-sign
|
|
809
1139
|
);
|
|
810
1140
|
shaft = catmullRomPath(points, false);
|
|
811
1141
|
hull.push(...points);
|
|
812
1142
|
const last = points[points.length - 1];
|
|
813
1143
|
const prior = points[points.length - 2] ?? start;
|
|
814
1144
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
1145
|
+
} else {
|
|
1146
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1147
|
+
if (style.roughness >= 1.1) {
|
|
1148
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1149
|
+
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)}`;
|
|
1150
|
+
}
|
|
1151
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
818
1152
|
}
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
1153
|
+
}
|
|
1154
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1155
|
+
svg.appendChild(group);
|
|
1156
|
+
const shaftPaths = [];
|
|
1157
|
+
if (ghost) {
|
|
1158
|
+
shaftPaths.push(
|
|
1159
|
+
appendPath(group, ghost, {
|
|
824
1160
|
...style,
|
|
825
1161
|
opacity: style.opacity * 0.3,
|
|
826
1162
|
strokeWidth: style.strokeWidth * 0.7,
|
|
827
1163
|
dashed
|
|
828
|
-
})
|
|
829
|
-
|
|
830
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1164
|
+
})
|
|
1165
|
+
);
|
|
831
1166
|
}
|
|
832
|
-
appendPath(
|
|
833
|
-
drawArrowHead(
|
|
1167
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1168
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1169
|
+
animateArrow(
|
|
1170
|
+
group,
|
|
1171
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1172
|
+
arrowOptions.animate,
|
|
1173
|
+
context.elapsed
|
|
1174
|
+
);
|
|
834
1175
|
const bounds = boundsOf(hull);
|
|
835
|
-
return { start, end, bounds };
|
|
1176
|
+
return { start, end, bounds, group };
|
|
836
1177
|
}
|
|
837
1178
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
838
1179
|
if (width <= 4) return;
|
|
@@ -899,6 +1240,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
899
1240
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
900
1241
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
901
1242
|
const lineHeight = fontSize * 1.05;
|
|
1243
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
902
1244
|
const group = document.createElementNS(SVG_NS, "g");
|
|
903
1245
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
904
1246
|
textEl.setAttribute("x", "0");
|
|
@@ -911,6 +1253,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
911
1253
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
912
1254
|
textEl.setAttribute("text-anchor", "start");
|
|
913
1255
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1256
|
+
if (rtl) {
|
|
1257
|
+
textEl.setAttribute("direction", "rtl");
|
|
1258
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1259
|
+
}
|
|
1260
|
+
if (options.locale) {
|
|
1261
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1262
|
+
}
|
|
914
1263
|
const tspans = lines.map((line, index) => {
|
|
915
1264
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
916
1265
|
tspan.textContent = line;
|
|
@@ -939,15 +1288,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
939
1288
|
height,
|
|
940
1289
|
ascent,
|
|
941
1290
|
descent,
|
|
942
|
-
fontSize
|
|
1291
|
+
fontSize,
|
|
1292
|
+
rtl
|
|
943
1293
|
};
|
|
944
1294
|
}
|
|
945
1295
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
946
1296
|
const random = createRandom(seed + 55);
|
|
947
1297
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
948
|
-
note.
|
|
1298
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1299
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
949
1300
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
950
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1301
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
951
1302
|
const cx = box.x + box.width / 2;
|
|
952
1303
|
const cy = box.y + box.height / 2;
|
|
953
1304
|
note.group.setAttribute(
|
|
@@ -956,9 +1307,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
956
1307
|
);
|
|
957
1308
|
if (options.underline !== false) {
|
|
958
1309
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1310
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
959
1311
|
drawUnderline(
|
|
960
1312
|
note.group,
|
|
961
|
-
|
|
1313
|
+
underlineX,
|
|
962
1314
|
box.y + box.height + 2,
|
|
963
1315
|
lastWidth - 2,
|
|
964
1316
|
{
|
|
@@ -979,7 +1331,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
979
1331
|
svg.setAttribute("width", String(width));
|
|
980
1332
|
svg.setAttribute("height", String(height));
|
|
981
1333
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
982
|
-
svg.style.position = "
|
|
1334
|
+
svg.style.position = "absolute";
|
|
983
1335
|
svg.style.left = `${left}px`;
|
|
984
1336
|
svg.style.top = `${top}px`;
|
|
985
1337
|
svg.style.width = `${width}px`;
|
|
@@ -1032,12 +1384,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1032
1384
|
return { ...box, x: box.x + dx };
|
|
1033
1385
|
}
|
|
1034
1386
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
1035
|
-
|
|
1387
|
+
const { text, locale } = resolveLocalizedText(
|
|
1388
|
+
noteOptions?.text,
|
|
1389
|
+
noteOptions?.locale
|
|
1390
|
+
);
|
|
1391
|
+
if (!text) return null;
|
|
1036
1392
|
const band = layout.band ?? null;
|
|
1037
1393
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1038
1394
|
const random = createRandom(seed + 9);
|
|
1039
|
-
const note = createNoteText(svg,
|
|
1040
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1395
|
+
const note = createNoteText(svg, text, style, {
|
|
1396
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1397
|
+
locale
|
|
1041
1398
|
});
|
|
1042
1399
|
const tilt = (random() - 0.45) * 7;
|
|
1043
1400
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1079,6 +1436,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1079
1436
|
strokeWidth: style.strokeWidth * scale
|
|
1080
1437
|
});
|
|
1081
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
|
+
}
|
|
1082
1458
|
function drawHeart(parent, size, style, random) {
|
|
1083
1459
|
const s = size;
|
|
1084
1460
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1102,26 +1478,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1102
1478
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1103
1479
|
"Z"
|
|
1104
1480
|
].join(" ");
|
|
1105
|
-
appendPath(parent, path, style);
|
|
1481
|
+
return appendPath(parent, path, style);
|
|
1106
1482
|
}
|
|
1107
1483
|
function drawSparkle(parent, size, style, random) {
|
|
1108
1484
|
twinkle(
|
|
1109
1485
|
parent,
|
|
1110
|
-
size * (0.
|
|
1486
|
+
size * (0.62 + random() * 0.12),
|
|
1111
1487
|
size,
|
|
1112
|
-
size * (0.
|
|
1488
|
+
size * (0.14 + random() * 0.08),
|
|
1113
1489
|
style
|
|
1114
1490
|
);
|
|
1115
1491
|
}
|
|
1116
1492
|
function drawStar(parent, size, style, random) {
|
|
1117
1493
|
twinkle(
|
|
1118
1494
|
parent,
|
|
1119
|
-
size * (0.
|
|
1120
|
-
size,
|
|
1121
|
-
size * (0.
|
|
1495
|
+
size * (0.62 + random() * 0.12),
|
|
1496
|
+
size * (0.8 + random() * 0.12),
|
|
1497
|
+
size * (0.14 + random() * 0.08),
|
|
1122
1498
|
style
|
|
1123
1499
|
);
|
|
1124
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
|
+
}
|
|
1125
1536
|
function drawTwinkle(parent, size, style, random) {
|
|
1126
1537
|
const count = 5;
|
|
1127
1538
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1229,46 +1640,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1229
1640
|
);
|
|
1230
1641
|
}
|
|
1231
1642
|
}
|
|
1232
|
-
function drawDots(parent, size, style, random) {
|
|
1233
|
-
let x = -size * 0.7;
|
|
1234
|
-
for (let i = 0; i < 3; i++) {
|
|
1235
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1236
|
-
x += size * (0.6 + random() * 0.25);
|
|
1237
|
-
}
|
|
1238
|
-
}
|
|
1239
|
-
function drawStroke(parent, size, style, random) {
|
|
1240
|
-
appendPath(
|
|
1241
|
-
parent,
|
|
1242
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1243
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1244
|
-
steps: 4
|
|
1245
|
-
}),
|
|
1246
|
-
style
|
|
1247
|
-
);
|
|
1248
|
-
}
|
|
1249
|
-
function drawSteam(parent, size, style, random) {
|
|
1250
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1251
|
-
for (let i = 0; i < 2; i++) {
|
|
1252
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1253
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1254
|
-
appendPath(
|
|
1255
|
-
parent,
|
|
1256
|
-
`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)}`,
|
|
1257
|
-
style
|
|
1258
|
-
);
|
|
1259
|
-
}
|
|
1260
|
-
}
|
|
1261
1643
|
var DECORATION_DRAWERS = {
|
|
1262
1644
|
heart: drawHeart,
|
|
1263
1645
|
sparkle: drawSparkle,
|
|
1264
1646
|
star: drawStar,
|
|
1265
1647
|
twinkle: drawTwinkle,
|
|
1648
|
+
glimmer: drawGlimmer,
|
|
1266
1649
|
smiley: drawSmiley,
|
|
1267
1650
|
emphasis: drawEmphasis,
|
|
1268
|
-
arcs: drawArcs
|
|
1269
|
-
dots: drawDots,
|
|
1270
|
-
stroke: drawStroke,
|
|
1271
|
-
steam: drawSteam
|
|
1651
|
+
arcs: drawArcs
|
|
1272
1652
|
};
|
|
1273
1653
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1274
1654
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1279,12 +1659,10 @@ var TYPE_TRAITS = {
|
|
|
1279
1659
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1280
1660
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1281
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 },
|
|
1282
1663
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1283
1664
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1284
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1285
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1286
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1287
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1665
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1288
1666
|
};
|
|
1289
1667
|
var DEFAULT_TYPES = [
|
|
1290
1668
|
"twinkle",
|
|
@@ -1487,6 +1865,9 @@ function ensureHandwrittenFont() {
|
|
|
1487
1865
|
|
|
1488
1866
|
// src/doodle.js
|
|
1489
1867
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1868
|
+
function now() {
|
|
1869
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1870
|
+
}
|
|
1490
1871
|
function localBand(overlayRect, inset = 8) {
|
|
1491
1872
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1492
1873
|
return {
|
|
@@ -1503,19 +1884,21 @@ var DoodleOverlay = class {
|
|
|
1503
1884
|
this.element = element;
|
|
1504
1885
|
this.options = options;
|
|
1505
1886
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1887
|
+
this.startedAt = now();
|
|
1506
1888
|
this.svg = null;
|
|
1889
|
+
this.wrapper = null;
|
|
1507
1890
|
this.resizeObserver = null;
|
|
1508
1891
|
this.mutationObserver = null;
|
|
1509
1892
|
this.pendingFrame = null;
|
|
1510
|
-
this.
|
|
1511
|
-
this.onScroll = this.scheduleUpdate.bind(this);
|
|
1893
|
+
this.listeners = [];
|
|
1512
1894
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1895
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1513
1896
|
this.childElements = [];
|
|
1514
1897
|
this.mount();
|
|
1515
1898
|
}
|
|
1516
1899
|
mount() {
|
|
1517
1900
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1518
|
-
insertOverlaySvg(this.svg, this.element);
|
|
1901
|
+
this.wrapper = insertOverlaySvg(this.svg, this.element);
|
|
1519
1902
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1520
1903
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1521
1904
|
this.resizeObserver.observe(this.element);
|
|
@@ -1528,12 +1911,18 @@ var DoodleOverlay = class {
|
|
|
1528
1911
|
subtree: true,
|
|
1529
1912
|
attributes: true
|
|
1530
1913
|
});
|
|
1531
|
-
for (const target of getScrollableAncestors(this.element)) {
|
|
1532
|
-
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1533
|
-
this.scrollTargets.push({ target, type: "scroll" });
|
|
1534
|
-
}
|
|
1535
1914
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1536
|
-
this.
|
|
1915
|
+
this.listeners.push({
|
|
1916
|
+
target: window,
|
|
1917
|
+
type: "resize",
|
|
1918
|
+
handler: this.onResize
|
|
1919
|
+
});
|
|
1920
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1921
|
+
this.listeners.push({
|
|
1922
|
+
target: window,
|
|
1923
|
+
type: "languagechange",
|
|
1924
|
+
handler: this.onLanguageChange
|
|
1925
|
+
});
|
|
1537
1926
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1538
1927
|
ensureHandwrittenFont();
|
|
1539
1928
|
}
|
|
@@ -1569,17 +1958,19 @@ var DoodleOverlay = class {
|
|
|
1569
1958
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1570
1959
|
const targets = this.getTargets();
|
|
1571
1960
|
const margin = this.overlayMargin();
|
|
1961
|
+
const elapsed = now() - this.startedAt;
|
|
1572
1962
|
const absoluteRects = targets.map(
|
|
1573
1963
|
(target) => getElementBounds(target, padding, radius)
|
|
1574
1964
|
);
|
|
1575
1965
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1576
1966
|
clearSvg(this.svg);
|
|
1577
1967
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1968
|
+
const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1578
1969
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1579
1970
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1580
1971
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
1581
|
-
this.svg.style.left = `${overlayRect.x}px`;
|
|
1582
|
-
this.svg.style.top = `${overlayRect.y}px`;
|
|
1972
|
+
this.svg.style.left = `${overlayRect.x - origin.left}px`;
|
|
1973
|
+
this.svg.style.top = `${overlayRect.y - origin.top}px`;
|
|
1583
1974
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1584
1975
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
1585
1976
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
@@ -1621,7 +2012,8 @@ var DoodleOverlay = class {
|
|
|
1621
2012
|
this.options.arrow,
|
|
1622
2013
|
strokeStyle,
|
|
1623
2014
|
seed,
|
|
1624
|
-
note ? note.box : null
|
|
2015
|
+
note ? note.box : null,
|
|
2016
|
+
{ elapsed }
|
|
1625
2017
|
) : null;
|
|
1626
2018
|
if (this.options.decorations) {
|
|
1627
2019
|
renderDecorations(
|
|
@@ -1648,7 +2040,7 @@ var DoodleOverlay = class {
|
|
|
1648
2040
|
overlayMargin() {
|
|
1649
2041
|
const { note, arrow, decorations } = this.options;
|
|
1650
2042
|
if (!note && !arrow && !decorations) return 48;
|
|
1651
|
-
const text =
|
|
2043
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1652
2044
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1653
2045
|
}
|
|
1654
2046
|
destroy() {
|
|
@@ -1668,14 +2060,15 @@ var DoodleOverlay = class {
|
|
|
1668
2060
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1669
2061
|
this.onFontsLoaded = null;
|
|
1670
2062
|
}
|
|
1671
|
-
for (const { target, type } of this.
|
|
1672
|
-
target.removeEventListener(type,
|
|
2063
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2064
|
+
target.removeEventListener(type, handler);
|
|
1673
2065
|
}
|
|
1674
|
-
this.
|
|
1675
|
-
if (this.svg
|
|
1676
|
-
this.svg.
|
|
2066
|
+
this.listeners = [];
|
|
2067
|
+
if (this.svg) {
|
|
2068
|
+
removeOverlaySvg(this.svg, this.wrapper, this.element);
|
|
1677
2069
|
}
|
|
1678
2070
|
this.svg = null;
|
|
2071
|
+
this.wrapper = null;
|
|
1679
2072
|
instances.delete(this.element);
|
|
1680
2073
|
}
|
|
1681
2074
|
};
|
|
@@ -1712,7 +2105,8 @@ var OPTION_KEYS = [
|
|
|
1712
2105
|
"decorations",
|
|
1713
2106
|
"addBreaks",
|
|
1714
2107
|
"fontFamily",
|
|
1715
|
-
"autoLoadFont"
|
|
2108
|
+
"autoLoadFont",
|
|
2109
|
+
"locale"
|
|
1716
2110
|
];
|
|
1717
2111
|
function collectOptions(props) {
|
|
1718
2112
|
const options = {};
|
|
@@ -1764,6 +2158,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1764
2158
|
addBreaks,
|
|
1765
2159
|
fontFamily,
|
|
1766
2160
|
autoLoadFont,
|
|
2161
|
+
locale,
|
|
1767
2162
|
childSelector,
|
|
1768
2163
|
...rest
|
|
1769
2164
|
} = props;
|
|
@@ -1783,6 +2178,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1783
2178
|
addBreaks,
|
|
1784
2179
|
fontFamily,
|
|
1785
2180
|
autoLoadFont,
|
|
2181
|
+
locale,
|
|
1786
2182
|
childSelector
|
|
1787
2183
|
}),
|
|
1788
2184
|
[
|
|
@@ -1800,6 +2196,7 @@ var Doodle = forwardRef(function Doodle2(props, forwardedRef) {
|
|
|
1800
2196
|
JSON.stringify(addBreaks ?? null),
|
|
1801
2197
|
fontFamily,
|
|
1802
2198
|
autoLoadFont,
|
|
2199
|
+
JSON.stringify(locale ?? null),
|
|
1803
2200
|
childSelector
|
|
1804
2201
|
]
|
|
1805
2202
|
);
|