@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.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,
|
|
@@ -47,6 +306,11 @@ var DEFAULT_OPTIONS = {
|
|
|
47
306
|
fontFamily: null,
|
|
48
307
|
/** Fetch Caveat when the page has not provided it. */
|
|
49
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,
|
|
50
314
|
/**
|
|
51
315
|
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
52
316
|
* doodles stay above the annotated element but below modals and other UI.
|
|
@@ -65,8 +329,15 @@ function resolveElement(target) {
|
|
|
65
329
|
}
|
|
66
330
|
function mergeOptions(options = {}) {
|
|
67
331
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
68
|
-
if (merged.note
|
|
69
|
-
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
|
+
};
|
|
70
341
|
}
|
|
71
342
|
if (merged.arrow) {
|
|
72
343
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -76,7 +347,10 @@ function mergeOptions(options = {}) {
|
|
|
76
347
|
// explicit `from` is honoured whether or not there is a note.
|
|
77
348
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
78
349
|
to: config.to ?? "edge",
|
|
79
|
-
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)
|
|
80
354
|
};
|
|
81
355
|
}
|
|
82
356
|
if (merged.decorations === true) {
|
|
@@ -130,20 +404,6 @@ function isValidPosition(position) {
|
|
|
130
404
|
"center"
|
|
131
405
|
].includes(position);
|
|
132
406
|
}
|
|
133
|
-
function getScrollableAncestors(element) {
|
|
134
|
-
const ancestors = [];
|
|
135
|
-
let node = element.parentElement;
|
|
136
|
-
while (node && node !== document.documentElement) {
|
|
137
|
-
const style = getComputedStyle(node);
|
|
138
|
-
const overflow = style.overflow + style.overflowY + style.overflowX;
|
|
139
|
-
if (/(auto|scroll|overlay)/.test(overflow)) {
|
|
140
|
-
ancestors.push(node);
|
|
141
|
-
}
|
|
142
|
-
node = node.parentElement;
|
|
143
|
-
}
|
|
144
|
-
ancestors.push(window);
|
|
145
|
-
return ancestors;
|
|
146
|
-
}
|
|
147
407
|
function resolveOverlayZIndex(element, override) {
|
|
148
408
|
if (override != null) return override;
|
|
149
409
|
const { zIndex, position } = getComputedStyle(element);
|
|
@@ -162,13 +422,45 @@ function resolveOverlayZIndex(element, override) {
|
|
|
162
422
|
}
|
|
163
423
|
return null;
|
|
164
424
|
}
|
|
425
|
+
var ITEM_PLACEMENT_PROPERTIES = [
|
|
426
|
+
"order",
|
|
427
|
+
"flexGrow",
|
|
428
|
+
"flexShrink",
|
|
429
|
+
"flexBasis",
|
|
430
|
+
"alignSelf",
|
|
431
|
+
"justifySelf",
|
|
432
|
+
"gridColumn",
|
|
433
|
+
"gridRow",
|
|
434
|
+
"gridArea"
|
|
435
|
+
];
|
|
436
|
+
function isInlineDisplay(computed) {
|
|
437
|
+
return computed.display.startsWith("inline");
|
|
438
|
+
}
|
|
165
439
|
function insertOverlaySvg(svg, element) {
|
|
166
440
|
const parent = element.parentNode;
|
|
167
441
|
if (!parent) {
|
|
168
442
|
document.body.appendChild(svg);
|
|
169
|
-
return;
|
|
443
|
+
return null;
|
|
444
|
+
}
|
|
445
|
+
const wrapper = document.createElement("div");
|
|
446
|
+
wrapper.style.position = "relative";
|
|
447
|
+
const computed = getComputedStyle(element);
|
|
448
|
+
wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
|
|
449
|
+
for (const property of ITEM_PLACEMENT_PROPERTIES) {
|
|
450
|
+
const value = computed[property];
|
|
451
|
+
if (value) wrapper.style[property] = value;
|
|
452
|
+
}
|
|
453
|
+
parent.replaceChild(wrapper, element);
|
|
454
|
+
wrapper.appendChild(element);
|
|
455
|
+
wrapper.appendChild(svg);
|
|
456
|
+
return wrapper;
|
|
457
|
+
}
|
|
458
|
+
function removeOverlaySvg(svg, wrapper, element) {
|
|
459
|
+
if (svg.parentNode) {
|
|
460
|
+
svg.parentNode.removeChild(svg);
|
|
170
461
|
}
|
|
171
|
-
|
|
462
|
+
if (!wrapper || !wrapper.parentNode) return;
|
|
463
|
+
wrapper.parentNode.replaceChild(element, wrapper);
|
|
172
464
|
}
|
|
173
465
|
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
174
466
|
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
@@ -325,6 +617,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
325
617
|
|
|
326
618
|
// src/renderer.js
|
|
327
619
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
620
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
328
621
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
329
622
|
var NOTE_FONT_SIZE = 19;
|
|
330
623
|
function clearSvg(svg) {
|
|
@@ -570,6 +863,15 @@ function appendPath(svg, d, style) {
|
|
|
570
863
|
svg.appendChild(path);
|
|
571
864
|
return path;
|
|
572
865
|
}
|
|
866
|
+
function appendGlow(svg, radius, style) {
|
|
867
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
868
|
+
circle.setAttribute("r", fmt(radius));
|
|
869
|
+
circle.setAttribute("fill", style.color);
|
|
870
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
871
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
872
|
+
svg.appendChild(circle);
|
|
873
|
+
return circle;
|
|
874
|
+
}
|
|
573
875
|
function rectPerimeter(width, height, radius) {
|
|
574
876
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
575
877
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -656,20 +958,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
656
958
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
657
959
|
};
|
|
658
960
|
}
|
|
659
|
-
|
|
961
|
+
var STRADDLE = 0.75;
|
|
962
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
660
963
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
661
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
662
964
|
const knot = 0.46 + random() * 0.12;
|
|
663
965
|
const points = [];
|
|
664
966
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
665
967
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
666
968
|
const pivot = at(knot);
|
|
667
|
-
const
|
|
969
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
970
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
971
|
+
const heading = normalize(derivative);
|
|
668
972
|
const normal = { x: -heading.y, y: heading.x };
|
|
669
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
670
973
|
const centre = {
|
|
671
|
-
x: pivot.x + normal.x * radius * side,
|
|
672
|
-
y: pivot.y + normal.y * radius * side
|
|
974
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
975
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
673
976
|
};
|
|
674
977
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
675
978
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -677,15 +980,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
677
980
|
for (let i = 1; i <= steps; i++) {
|
|
678
981
|
const progress = i / steps;
|
|
679
982
|
const angle = from + turns * progress * side;
|
|
983
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
680
984
|
const drift = radius * 0.55 * progress;
|
|
681
985
|
points.push({
|
|
682
|
-
x: centre.x + Math.cos(angle) *
|
|
683
|
-
y: centre.y + Math.sin(angle) *
|
|
986
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
987
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
684
988
|
});
|
|
685
989
|
}
|
|
990
|
+
const exit = points[points.length - 1];
|
|
991
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
992
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
993
|
+
const rejoin = at(resume);
|
|
994
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
995
|
+
const settle = 6;
|
|
686
996
|
const tail = 14;
|
|
687
997
|
for (let i = 1; i <= tail; i++) {
|
|
688
|
-
|
|
998
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
999
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
1000
|
+
points.push({
|
|
1001
|
+
x: point.x + normal.x * residual * fade,
|
|
1002
|
+
y: point.y + normal.y * residual * fade
|
|
1003
|
+
});
|
|
689
1004
|
}
|
|
690
1005
|
return points;
|
|
691
1006
|
}
|
|
@@ -759,21 +1074,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
759
1074
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
760
1075
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
761
1076
|
const spread = 0.46 + random() * 0.1;
|
|
1077
|
+
const arms = [];
|
|
762
1078
|
for (const side of [-1, 1]) {
|
|
763
1079
|
const armAngle = angle + spread * side;
|
|
764
1080
|
const tail = {
|
|
765
1081
|
x: tip.x - length * Math.cos(armAngle),
|
|
766
1082
|
y: tip.y - length * Math.sin(armAngle)
|
|
767
1083
|
};
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
1084
|
+
arms.push(
|
|
1085
|
+
appendPath(
|
|
1086
|
+
svg,
|
|
1087
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1088
|
+
steps: 2,
|
|
1089
|
+
bow: 0.35 * side
|
|
1090
|
+
}),
|
|
1091
|
+
{ ...style, dashed: false }
|
|
1092
|
+
)
|
|
775
1093
|
);
|
|
776
1094
|
}
|
|
1095
|
+
return arms;
|
|
1096
|
+
}
|
|
1097
|
+
function bowOverTheTop(perpendicular) {
|
|
1098
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1099
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
777
1100
|
}
|
|
778
1101
|
function boundsOf(points) {
|
|
779
1102
|
const xs = points.map((p) => p.x);
|
|
@@ -785,7 +1108,7 @@ function boundsOf(points) {
|
|
|
785
1108
|
height: Math.max(...ys) - Math.min(...ys)
|
|
786
1109
|
};
|
|
787
1110
|
}
|
|
788
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1111
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
789
1112
|
const random = createRandom(seed + 17);
|
|
790
1113
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
791
1114
|
const dx = end.x - start.x;
|
|
@@ -794,6 +1117,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
794
1117
|
const dashed = arrowOptions.style === "dotted";
|
|
795
1118
|
let tipAngle = Math.atan2(dy, dx);
|
|
796
1119
|
let shaft;
|
|
1120
|
+
let ghost = null;
|
|
797
1121
|
const hull = [start, end];
|
|
798
1122
|
if (arrowOptions.style === "straight") {
|
|
799
1123
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -807,7 +1131,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
807
1131
|
y: rect.y + rect.height / 2 - mid.y
|
|
808
1132
|
};
|
|
809
1133
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
810
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1134
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1135
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1136
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1137
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1138
|
+
bowOverTheTop(perpendicular)
|
|
1139
|
+
);
|
|
811
1140
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
812
1141
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
813
1142
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -822,34 +1151,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
822
1151
|
{ x: c2x, y: c2y },
|
|
823
1152
|
end,
|
|
824
1153
|
radius,
|
|
825
|
-
random
|
|
1154
|
+
random,
|
|
1155
|
+
-sign
|
|
826
1156
|
);
|
|
827
1157
|
shaft = catmullRomPath(points, false);
|
|
828
1158
|
hull.push(...points);
|
|
829
1159
|
const last = points[points.length - 1];
|
|
830
1160
|
const prior = points[points.length - 2] ?? start;
|
|
831
1161
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
1162
|
+
} else {
|
|
1163
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1164
|
+
if (style.roughness >= 1.1) {
|
|
1165
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1166
|
+
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)}`;
|
|
1167
|
+
}
|
|
1168
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
835
1169
|
}
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1170
|
+
}
|
|
1171
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1172
|
+
svg.appendChild(group);
|
|
1173
|
+
const shaftPaths = [];
|
|
1174
|
+
if (ghost) {
|
|
1175
|
+
shaftPaths.push(
|
|
1176
|
+
appendPath(group, ghost, {
|
|
841
1177
|
...style,
|
|
842
1178
|
opacity: style.opacity * 0.3,
|
|
843
1179
|
strokeWidth: style.strokeWidth * 0.7,
|
|
844
1180
|
dashed
|
|
845
|
-
})
|
|
846
|
-
|
|
847
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1181
|
+
})
|
|
1182
|
+
);
|
|
848
1183
|
}
|
|
849
|
-
appendPath(
|
|
850
|
-
drawArrowHead(
|
|
1184
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1185
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1186
|
+
animateArrow(
|
|
1187
|
+
group,
|
|
1188
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1189
|
+
arrowOptions.animate,
|
|
1190
|
+
context.elapsed
|
|
1191
|
+
);
|
|
851
1192
|
const bounds = boundsOf(hull);
|
|
852
|
-
return { start, end, bounds };
|
|
1193
|
+
return { start, end, bounds, group };
|
|
853
1194
|
}
|
|
854
1195
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
855
1196
|
if (width <= 4) return;
|
|
@@ -916,6 +1257,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
916
1257
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
917
1258
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
918
1259
|
const lineHeight = fontSize * 1.05;
|
|
1260
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
919
1261
|
const group = document.createElementNS(SVG_NS, "g");
|
|
920
1262
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
921
1263
|
textEl.setAttribute("x", "0");
|
|
@@ -928,6 +1270,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
928
1270
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
929
1271
|
textEl.setAttribute("text-anchor", "start");
|
|
930
1272
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1273
|
+
if (rtl) {
|
|
1274
|
+
textEl.setAttribute("direction", "rtl");
|
|
1275
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1276
|
+
}
|
|
1277
|
+
if (options.locale) {
|
|
1278
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1279
|
+
}
|
|
931
1280
|
const tspans = lines.map((line, index) => {
|
|
932
1281
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
933
1282
|
tspan.textContent = line;
|
|
@@ -956,15 +1305,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
956
1305
|
height,
|
|
957
1306
|
ascent,
|
|
958
1307
|
descent,
|
|
959
|
-
fontSize
|
|
1308
|
+
fontSize,
|
|
1309
|
+
rtl
|
|
960
1310
|
};
|
|
961
1311
|
}
|
|
962
1312
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
963
1313
|
const random = createRandom(seed + 55);
|
|
964
1314
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
965
|
-
note.
|
|
1315
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1316
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
966
1317
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
967
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1318
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
968
1319
|
const cx = box.x + box.width / 2;
|
|
969
1320
|
const cy = box.y + box.height / 2;
|
|
970
1321
|
note.group.setAttribute(
|
|
@@ -973,9 +1324,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
973
1324
|
);
|
|
974
1325
|
if (options.underline !== false) {
|
|
975
1326
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1327
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
976
1328
|
drawUnderline(
|
|
977
1329
|
note.group,
|
|
978
|
-
|
|
1330
|
+
underlineX,
|
|
979
1331
|
box.y + box.height + 2,
|
|
980
1332
|
lastWidth - 2,
|
|
981
1333
|
{
|
|
@@ -996,7 +1348,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
996
1348
|
svg.setAttribute("width", String(width));
|
|
997
1349
|
svg.setAttribute("height", String(height));
|
|
998
1350
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
999
|
-
svg.style.position = "
|
|
1351
|
+
svg.style.position = "absolute";
|
|
1000
1352
|
svg.style.left = `${left}px`;
|
|
1001
1353
|
svg.style.top = `${top}px`;
|
|
1002
1354
|
svg.style.width = `${width}px`;
|
|
@@ -1049,12 +1401,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1049
1401
|
return { ...box, x: box.x + dx };
|
|
1050
1402
|
}
|
|
1051
1403
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
1052
|
-
|
|
1404
|
+
const { text, locale } = resolveLocalizedText(
|
|
1405
|
+
noteOptions?.text,
|
|
1406
|
+
noteOptions?.locale
|
|
1407
|
+
);
|
|
1408
|
+
if (!text) return null;
|
|
1053
1409
|
const band = layout.band ?? null;
|
|
1054
1410
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1055
1411
|
const random = createRandom(seed + 9);
|
|
1056
|
-
const note = createNoteText(svg,
|
|
1057
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1412
|
+
const note = createNoteText(svg, text, style, {
|
|
1413
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1414
|
+
locale
|
|
1058
1415
|
});
|
|
1059
1416
|
const tilt = (random() - 0.45) * 7;
|
|
1060
1417
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1096,6 +1453,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1096
1453
|
strokeWidth: style.strokeWidth * scale
|
|
1097
1454
|
});
|
|
1098
1455
|
}
|
|
1456
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1457
|
+
const count = 6;
|
|
1458
|
+
const points = [];
|
|
1459
|
+
for (let i = 0; i < count; i++) {
|
|
1460
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1461
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1462
|
+
points.push({
|
|
1463
|
+
x: x + Math.cos(angle) * r,
|
|
1464
|
+
y: y + Math.sin(angle) * r
|
|
1465
|
+
});
|
|
1466
|
+
}
|
|
1467
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1468
|
+
...style,
|
|
1469
|
+
fill: style.color,
|
|
1470
|
+
strokeWidth: 0
|
|
1471
|
+
});
|
|
1472
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1473
|
+
return blob;
|
|
1474
|
+
}
|
|
1099
1475
|
function drawHeart(parent, size, style, random) {
|
|
1100
1476
|
const s = size;
|
|
1101
1477
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1119,26 +1495,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1119
1495
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1120
1496
|
"Z"
|
|
1121
1497
|
].join(" ");
|
|
1122
|
-
appendPath(parent, path, style);
|
|
1498
|
+
return appendPath(parent, path, style);
|
|
1123
1499
|
}
|
|
1124
1500
|
function drawSparkle(parent, size, style, random) {
|
|
1125
1501
|
twinkle(
|
|
1126
1502
|
parent,
|
|
1127
|
-
size * (0.
|
|
1503
|
+
size * (0.62 + random() * 0.12),
|
|
1128
1504
|
size,
|
|
1129
|
-
size * (0.
|
|
1505
|
+
size * (0.14 + random() * 0.08),
|
|
1130
1506
|
style
|
|
1131
1507
|
);
|
|
1132
1508
|
}
|
|
1133
1509
|
function drawStar(parent, size, style, random) {
|
|
1134
1510
|
twinkle(
|
|
1135
1511
|
parent,
|
|
1136
|
-
size * (0.
|
|
1137
|
-
size,
|
|
1138
|
-
size * (0.
|
|
1512
|
+
size * (0.62 + random() * 0.12),
|
|
1513
|
+
size * (0.8 + random() * 0.12),
|
|
1514
|
+
size * (0.14 + random() * 0.08),
|
|
1139
1515
|
style
|
|
1140
1516
|
);
|
|
1141
1517
|
}
|
|
1518
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1519
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1520
|
+
color: style.color,
|
|
1521
|
+
opacity: style.opacity * 0.22
|
|
1522
|
+
});
|
|
1523
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1524
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1525
|
+
const bluntness = size * 0.19;
|
|
1526
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1527
|
+
...style,
|
|
1528
|
+
fill: style.color,
|
|
1529
|
+
strokeWidth: bluntness
|
|
1530
|
+
});
|
|
1531
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1532
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1533
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1534
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1535
|
+
const corners = [
|
|
1536
|
+
{ x: 0, y: -(armY + gap) },
|
|
1537
|
+
{ x: armX + gap, y: 0 },
|
|
1538
|
+
{ x: 0, y: armY + gap },
|
|
1539
|
+
{ x: -(armX + gap), y: 0 }
|
|
1540
|
+
];
|
|
1541
|
+
for (const { x, y } of corners) {
|
|
1542
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1543
|
+
drawFleck(
|
|
1544
|
+
parent,
|
|
1545
|
+
x * jitter,
|
|
1546
|
+
y * jitter,
|
|
1547
|
+
size * (0.1 + random() * 0.03),
|
|
1548
|
+
style,
|
|
1549
|
+
random
|
|
1550
|
+
);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1142
1553
|
function drawTwinkle(parent, size, style, random) {
|
|
1143
1554
|
const count = 5;
|
|
1144
1555
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1246,46 +1657,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1246
1657
|
);
|
|
1247
1658
|
}
|
|
1248
1659
|
}
|
|
1249
|
-
function drawDots(parent, size, style, random) {
|
|
1250
|
-
let x = -size * 0.7;
|
|
1251
|
-
for (let i = 0; i < 3; i++) {
|
|
1252
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1253
|
-
x += size * (0.6 + random() * 0.25);
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
function drawStroke(parent, size, style, random) {
|
|
1257
|
-
appendPath(
|
|
1258
|
-
parent,
|
|
1259
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1260
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1261
|
-
steps: 4
|
|
1262
|
-
}),
|
|
1263
|
-
style
|
|
1264
|
-
);
|
|
1265
|
-
}
|
|
1266
|
-
function drawSteam(parent, size, style, random) {
|
|
1267
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1268
|
-
for (let i = 0; i < 2; i++) {
|
|
1269
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1270
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1271
|
-
appendPath(
|
|
1272
|
-
parent,
|
|
1273
|
-
`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)}`,
|
|
1274
|
-
style
|
|
1275
|
-
);
|
|
1276
|
-
}
|
|
1277
|
-
}
|
|
1278
1660
|
var DECORATION_DRAWERS = {
|
|
1279
1661
|
heart: drawHeart,
|
|
1280
1662
|
sparkle: drawSparkle,
|
|
1281
1663
|
star: drawStar,
|
|
1282
1664
|
twinkle: drawTwinkle,
|
|
1665
|
+
glimmer: drawGlimmer,
|
|
1283
1666
|
smiley: drawSmiley,
|
|
1284
1667
|
emphasis: drawEmphasis,
|
|
1285
|
-
arcs: drawArcs
|
|
1286
|
-
dots: drawDots,
|
|
1287
|
-
stroke: drawStroke,
|
|
1288
|
-
steam: drawSteam
|
|
1668
|
+
arcs: drawArcs
|
|
1289
1669
|
};
|
|
1290
1670
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1291
1671
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1296,12 +1676,10 @@ var TYPE_TRAITS = {
|
|
|
1296
1676
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1297
1677
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1298
1678
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1679
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1299
1680
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1300
1681
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1301
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1302
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1303
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1304
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1682
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1305
1683
|
};
|
|
1306
1684
|
var DEFAULT_TYPES = [
|
|
1307
1685
|
"twinkle",
|
|
@@ -1504,6 +1882,9 @@ function ensureHandwrittenFont() {
|
|
|
1504
1882
|
|
|
1505
1883
|
// src/doodle.js
|
|
1506
1884
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1885
|
+
function now() {
|
|
1886
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1887
|
+
}
|
|
1507
1888
|
function localBand(overlayRect, inset = 8) {
|
|
1508
1889
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1509
1890
|
return {
|
|
@@ -1520,19 +1901,21 @@ var DoodleOverlay = class {
|
|
|
1520
1901
|
this.element = element;
|
|
1521
1902
|
this.options = options;
|
|
1522
1903
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1904
|
+
this.startedAt = now();
|
|
1523
1905
|
this.svg = null;
|
|
1906
|
+
this.wrapper = null;
|
|
1524
1907
|
this.resizeObserver = null;
|
|
1525
1908
|
this.mutationObserver = null;
|
|
1526
1909
|
this.pendingFrame = null;
|
|
1527
|
-
this.
|
|
1528
|
-
this.onScroll = this.scheduleUpdate.bind(this);
|
|
1910
|
+
this.listeners = [];
|
|
1529
1911
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1912
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1530
1913
|
this.childElements = [];
|
|
1531
1914
|
this.mount();
|
|
1532
1915
|
}
|
|
1533
1916
|
mount() {
|
|
1534
1917
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1535
|
-
insertOverlaySvg(this.svg, this.element);
|
|
1918
|
+
this.wrapper = insertOverlaySvg(this.svg, this.element);
|
|
1536
1919
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1537
1920
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1538
1921
|
this.resizeObserver.observe(this.element);
|
|
@@ -1545,12 +1928,18 @@ var DoodleOverlay = class {
|
|
|
1545
1928
|
subtree: true,
|
|
1546
1929
|
attributes: true
|
|
1547
1930
|
});
|
|
1548
|
-
for (const target of getScrollableAncestors(this.element)) {
|
|
1549
|
-
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1550
|
-
this.scrollTargets.push({ target, type: "scroll" });
|
|
1551
|
-
}
|
|
1552
1931
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1553
|
-
this.
|
|
1932
|
+
this.listeners.push({
|
|
1933
|
+
target: window,
|
|
1934
|
+
type: "resize",
|
|
1935
|
+
handler: this.onResize
|
|
1936
|
+
});
|
|
1937
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1938
|
+
this.listeners.push({
|
|
1939
|
+
target: window,
|
|
1940
|
+
type: "languagechange",
|
|
1941
|
+
handler: this.onLanguageChange
|
|
1942
|
+
});
|
|
1554
1943
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1555
1944
|
ensureHandwrittenFont();
|
|
1556
1945
|
}
|
|
@@ -1586,17 +1975,19 @@ var DoodleOverlay = class {
|
|
|
1586
1975
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1587
1976
|
const targets = this.getTargets();
|
|
1588
1977
|
const margin = this.overlayMargin();
|
|
1978
|
+
const elapsed = now() - this.startedAt;
|
|
1589
1979
|
const absoluteRects = targets.map(
|
|
1590
1980
|
(target) => getElementBounds(target, padding, radius)
|
|
1591
1981
|
);
|
|
1592
1982
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1593
1983
|
clearSvg(this.svg);
|
|
1594
1984
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1985
|
+
const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1595
1986
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1596
1987
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1597
1988
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
1598
|
-
this.svg.style.left = `${overlayRect.x}px`;
|
|
1599
|
-
this.svg.style.top = `${overlayRect.y}px`;
|
|
1989
|
+
this.svg.style.left = `${overlayRect.x - origin.left}px`;
|
|
1990
|
+
this.svg.style.top = `${overlayRect.y - origin.top}px`;
|
|
1600
1991
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1601
1992
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
1602
1993
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
@@ -1638,7 +2029,8 @@ var DoodleOverlay = class {
|
|
|
1638
2029
|
this.options.arrow,
|
|
1639
2030
|
strokeStyle,
|
|
1640
2031
|
seed,
|
|
1641
|
-
note ? note.box : null
|
|
2032
|
+
note ? note.box : null,
|
|
2033
|
+
{ elapsed }
|
|
1642
2034
|
) : null;
|
|
1643
2035
|
if (this.options.decorations) {
|
|
1644
2036
|
renderDecorations(
|
|
@@ -1665,7 +2057,7 @@ var DoodleOverlay = class {
|
|
|
1665
2057
|
overlayMargin() {
|
|
1666
2058
|
const { note, arrow, decorations } = this.options;
|
|
1667
2059
|
if (!note && !arrow && !decorations) return 48;
|
|
1668
|
-
const text =
|
|
2060
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1669
2061
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1670
2062
|
}
|
|
1671
2063
|
destroy() {
|
|
@@ -1685,14 +2077,15 @@ var DoodleOverlay = class {
|
|
|
1685
2077
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1686
2078
|
this.onFontsLoaded = null;
|
|
1687
2079
|
}
|
|
1688
|
-
for (const { target, type } of this.
|
|
1689
|
-
target.removeEventListener(type,
|
|
2080
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2081
|
+
target.removeEventListener(type, handler);
|
|
1690
2082
|
}
|
|
1691
|
-
this.
|
|
1692
|
-
if (this.svg
|
|
1693
|
-
this.svg.
|
|
2083
|
+
this.listeners = [];
|
|
2084
|
+
if (this.svg) {
|
|
2085
|
+
removeOverlaySvg(this.svg, this.wrapper, this.element);
|
|
1694
2086
|
}
|
|
1695
2087
|
this.svg = null;
|
|
2088
|
+
this.wrapper = null;
|
|
1696
2089
|
instances.delete(this.element);
|
|
1697
2090
|
}
|
|
1698
2091
|
};
|
|
@@ -1729,7 +2122,8 @@ var OPTION_KEYS = [
|
|
|
1729
2122
|
"decorations",
|
|
1730
2123
|
"addBreaks",
|
|
1731
2124
|
"fontFamily",
|
|
1732
|
-
"autoLoadFont"
|
|
2125
|
+
"autoLoadFont",
|
|
2126
|
+
"locale"
|
|
1733
2127
|
];
|
|
1734
2128
|
function collectOptions(props) {
|
|
1735
2129
|
const options = {};
|
|
@@ -1781,6 +2175,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1781
2175
|
addBreaks,
|
|
1782
2176
|
fontFamily,
|
|
1783
2177
|
autoLoadFont,
|
|
2178
|
+
locale,
|
|
1784
2179
|
childSelector,
|
|
1785
2180
|
...rest
|
|
1786
2181
|
} = props;
|
|
@@ -1800,6 +2195,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1800
2195
|
addBreaks,
|
|
1801
2196
|
fontFamily,
|
|
1802
2197
|
autoLoadFont,
|
|
2198
|
+
locale,
|
|
1803
2199
|
childSelector
|
|
1804
2200
|
}),
|
|
1805
2201
|
[
|
|
@@ -1817,6 +2213,7 @@ var Doodle = (0, import_react.forwardRef)(function Doodle2(props, forwardedRef)
|
|
|
1817
2213
|
JSON.stringify(addBreaks ?? null),
|
|
1818
2214
|
fontFamily,
|
|
1819
2215
|
autoLoadFont,
|
|
2216
|
+
JSON.stringify(locale ?? null),
|
|
1820
2217
|
childSelector
|
|
1821
2218
|
]
|
|
1822
2219
|
);
|