@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/freehand-ui.cjs
CHANGED
|
@@ -23,6 +23,265 @@ __export(index_exports, {
|
|
|
23
23
|
});
|
|
24
24
|
module.exports = __toCommonJS(index_exports);
|
|
25
25
|
|
|
26
|
+
// src/animation.js
|
|
27
|
+
var STYLE_MARKER = "data-freehand-animation";
|
|
28
|
+
var CSS = `
|
|
29
|
+
@keyframes freehand-arrow-dash {
|
|
30
|
+
from { stroke-dashoffset: var(--freehand-dash, 0px); }
|
|
31
|
+
to { stroke-dashoffset: 0; }
|
|
32
|
+
}
|
|
33
|
+
@keyframes freehand-arrow-drift {
|
|
34
|
+
0%, 100% { transform: translate(0px, 0px); }
|
|
35
|
+
50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
36
|
+
}
|
|
37
|
+
@keyframes freehand-arrow-settle {
|
|
38
|
+
from { transform: translate(0px, 0px); }
|
|
39
|
+
to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
40
|
+
}
|
|
41
|
+
@media (prefers-reduced-motion: reduce) {
|
|
42
|
+
.freehand-arrow, .freehand-arrow path { animation: none !important; }
|
|
43
|
+
.freehand-arrow path { stroke-dashoffset: 0 !important; }
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
var ARROW_ANIMATION_DEFAULTS = {
|
|
47
|
+
/** Reveal the stroke with a dash sweep. */
|
|
48
|
+
draw: true,
|
|
49
|
+
/** Lean the arrow toward its tip. */
|
|
50
|
+
drift: true,
|
|
51
|
+
/** Multiplier over every duration - 2 plays twice as fast. */
|
|
52
|
+
speed: 1,
|
|
53
|
+
/** How long the shaft takes to draw, ms, before `speed`. */
|
|
54
|
+
duration: 900,
|
|
55
|
+
/** Wait before the first stroke appears, ms. */
|
|
56
|
+
delay: 150,
|
|
57
|
+
/** How far the arrow leans, px. */
|
|
58
|
+
distance: 5,
|
|
59
|
+
/** One full lean-and-return, ms. */
|
|
60
|
+
driftDuration: 2200,
|
|
61
|
+
/** Keep leaning. `false` settles once and stays put. */
|
|
62
|
+
repeat: true
|
|
63
|
+
};
|
|
64
|
+
function positive(value, fallback) {
|
|
65
|
+
const n = Number(value);
|
|
66
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
67
|
+
}
|
|
68
|
+
function atLeastZero(value, fallback) {
|
|
69
|
+
const n = Number(value);
|
|
70
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
71
|
+
}
|
|
72
|
+
function normalizeArrowAnimation(config) {
|
|
73
|
+
if (config === false || config === null) return false;
|
|
74
|
+
const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
|
|
75
|
+
const animation = {
|
|
76
|
+
draw: settings.draw !== false,
|
|
77
|
+
drift: settings.drift !== false,
|
|
78
|
+
speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
|
|
79
|
+
duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
|
|
80
|
+
delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
|
|
81
|
+
distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
|
|
82
|
+
driftDuration: positive(
|
|
83
|
+
settings.driftDuration,
|
|
84
|
+
ARROW_ANIMATION_DEFAULTS.driftDuration
|
|
85
|
+
),
|
|
86
|
+
repeat: settings.repeat !== false
|
|
87
|
+
};
|
|
88
|
+
if (!animation.draw && (!animation.drift || !animation.distance)) return false;
|
|
89
|
+
return animation;
|
|
90
|
+
}
|
|
91
|
+
var FLOW_PERIOD = 260;
|
|
92
|
+
function arrowTiming(animation) {
|
|
93
|
+
const duration = animation.duration / animation.speed;
|
|
94
|
+
const delay = animation.delay / animation.speed;
|
|
95
|
+
const drawn = animation.draw ? duration : 0;
|
|
96
|
+
return {
|
|
97
|
+
shaft: { duration, delay },
|
|
98
|
+
head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
|
|
99
|
+
flow: { duration: FLOW_PERIOD / animation.speed, delay },
|
|
100
|
+
drift: {
|
|
101
|
+
duration: animation.driftDuration / animation.speed,
|
|
102
|
+
delay: delay + drawn * 1.04
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
var injected = false;
|
|
107
|
+
function ensureArrowAnimationStyles() {
|
|
108
|
+
if (injected) return;
|
|
109
|
+
if (typeof document === "undefined" || !document.head) return;
|
|
110
|
+
injected = true;
|
|
111
|
+
if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
|
|
112
|
+
const style = document.createElement("style");
|
|
113
|
+
style.setAttribute(STYLE_MARKER, "");
|
|
114
|
+
style.textContent = CSS;
|
|
115
|
+
document.head.appendChild(style);
|
|
116
|
+
}
|
|
117
|
+
function time(ms) {
|
|
118
|
+
return `${Math.round(ms)}ms`;
|
|
119
|
+
}
|
|
120
|
+
function setAnimation(element, spec) {
|
|
121
|
+
const parts = [
|
|
122
|
+
spec.name,
|
|
123
|
+
time(spec.duration),
|
|
124
|
+
spec.easing,
|
|
125
|
+
time(spec.delay),
|
|
126
|
+
spec.iterations ?? "1"
|
|
127
|
+
];
|
|
128
|
+
if (spec.fill) parts.push(spec.fill);
|
|
129
|
+
element.style.animation = parts.join(" ");
|
|
130
|
+
}
|
|
131
|
+
function pathLength(path) {
|
|
132
|
+
try {
|
|
133
|
+
const length = path.getTotalLength();
|
|
134
|
+
return Number.isFinite(length) && length > 0 ? length : 0;
|
|
135
|
+
} catch {
|
|
136
|
+
return 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function dashPeriod(path) {
|
|
140
|
+
const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
|
|
141
|
+
if (!pattern.length) return 0;
|
|
142
|
+
const sum = pattern.reduce((total, n) => total + n, 0);
|
|
143
|
+
return pattern.length % 2 === 0 ? sum : sum * 2;
|
|
144
|
+
}
|
|
145
|
+
function applyDash(path, timing, elapsed, flow) {
|
|
146
|
+
const length = pathLength(path);
|
|
147
|
+
if (!length) return;
|
|
148
|
+
const period = dashPeriod(path);
|
|
149
|
+
if (period) {
|
|
150
|
+
path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
|
|
151
|
+
setAnimation(path, {
|
|
152
|
+
name: "freehand-arrow-dash",
|
|
153
|
+
duration: flow.duration,
|
|
154
|
+
easing: "linear",
|
|
155
|
+
delay: flow.delay - elapsed,
|
|
156
|
+
iterations: "infinite"
|
|
157
|
+
});
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
|
|
161
|
+
path.style.strokeDasharray = `${length.toFixed(2)}px`;
|
|
162
|
+
setAnimation(path, {
|
|
163
|
+
name: "freehand-arrow-dash",
|
|
164
|
+
duration: timing.duration,
|
|
165
|
+
easing: "ease-out",
|
|
166
|
+
delay: timing.delay - elapsed,
|
|
167
|
+
fill: "both"
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
function applyDrift(group, parts, timing, elapsed, animation) {
|
|
171
|
+
const dx = parts.end.x - parts.start.x;
|
|
172
|
+
const dy = parts.end.y - parts.start.y;
|
|
173
|
+
const length = Math.hypot(dx, dy);
|
|
174
|
+
if (!length) return;
|
|
175
|
+
const scale = animation.distance / length;
|
|
176
|
+
group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
|
|
177
|
+
group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
|
|
178
|
+
setAnimation(
|
|
179
|
+
group,
|
|
180
|
+
animation.repeat ? {
|
|
181
|
+
name: "freehand-arrow-drift",
|
|
182
|
+
duration: timing.duration,
|
|
183
|
+
easing: "ease-in-out",
|
|
184
|
+
delay: timing.delay - elapsed,
|
|
185
|
+
iterations: "infinite"
|
|
186
|
+
} : {
|
|
187
|
+
// A single lean is a settle, not a round trip, so it takes half a cycle
|
|
188
|
+
name: "freehand-arrow-settle",
|
|
189
|
+
duration: timing.duration * 0.5,
|
|
190
|
+
easing: "ease-out",
|
|
191
|
+
delay: timing.delay - elapsed,
|
|
192
|
+
fill: "both"
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
function animateArrow(group, parts, animation, elapsed = 0) {
|
|
197
|
+
if (!animation || !group) return;
|
|
198
|
+
ensureArrowAnimationStyles();
|
|
199
|
+
group.setAttribute("class", "freehand-arrow");
|
|
200
|
+
const timing = arrowTiming(animation);
|
|
201
|
+
const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
|
|
202
|
+
if (animation.draw) {
|
|
203
|
+
for (const path of parts.shaft) {
|
|
204
|
+
applyDash(path, timing.shaft, age, timing.flow);
|
|
205
|
+
}
|
|
206
|
+
for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
|
|
207
|
+
}
|
|
208
|
+
if (animation.drift && animation.distance > 0) {
|
|
209
|
+
applyDrift(group, parts, timing.drift, age, animation);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// src/locale.js
|
|
214
|
+
var DEFAULT_KEY = "default";
|
|
215
|
+
var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
|
|
216
|
+
var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
|
|
217
|
+
var LTR_STRONG = /[A-Za-zÀ-֏ऀ-Ⰰ-豈-ﬗA-Za-z]/;
|
|
218
|
+
function normalizeTag(tag) {
|
|
219
|
+
if (typeof tag !== "string") return "";
|
|
220
|
+
return tag.trim().replace(/_/g, "-").toLowerCase();
|
|
221
|
+
}
|
|
222
|
+
function isLocaleMap(value) {
|
|
223
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
224
|
+
const entries = Object.entries(value);
|
|
225
|
+
if (!entries.length) return false;
|
|
226
|
+
return entries.every(([key, text]) => {
|
|
227
|
+
if (typeof text !== "string") return false;
|
|
228
|
+
const tag = normalizeTag(key);
|
|
229
|
+
return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
function preferredLocales(override) {
|
|
233
|
+
const tags = [];
|
|
234
|
+
const add = (value) => {
|
|
235
|
+
const tag = normalizeTag(value);
|
|
236
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
237
|
+
};
|
|
238
|
+
if (Array.isArray(override)) override.forEach(add);
|
|
239
|
+
else add(override);
|
|
240
|
+
const nav = typeof navigator === "undefined" ? null : navigator;
|
|
241
|
+
if (nav) {
|
|
242
|
+
if (Array.isArray(nav.languages)) nav.languages.forEach(add);
|
|
243
|
+
add(nav.language);
|
|
244
|
+
}
|
|
245
|
+
if (typeof document !== "undefined") add(document.documentElement?.lang);
|
|
246
|
+
return tags;
|
|
247
|
+
}
|
|
248
|
+
function matchLocale(tags, preferred) {
|
|
249
|
+
const available = tags.map(normalizeTag);
|
|
250
|
+
for (const want of preferred) {
|
|
251
|
+
let tag = want;
|
|
252
|
+
while (tag) {
|
|
253
|
+
const exact = available.indexOf(tag);
|
|
254
|
+
if (exact !== -1) return exact;
|
|
255
|
+
const cut = tag.lastIndexOf("-");
|
|
256
|
+
tag = cut > 0 ? tag.slice(0, cut) : "";
|
|
257
|
+
}
|
|
258
|
+
const base = want.split("-")[0];
|
|
259
|
+
const sibling = available.findIndex((key) => key.split("-")[0] === base);
|
|
260
|
+
if (sibling !== -1) return sibling;
|
|
261
|
+
}
|
|
262
|
+
return -1;
|
|
263
|
+
}
|
|
264
|
+
function resolveLocalizedText(text, locale = null) {
|
|
265
|
+
if (typeof text === "string") return { text, locale: null };
|
|
266
|
+
if (!isLocaleMap(text)) return { text: "", locale: null };
|
|
267
|
+
const keys = Object.keys(text);
|
|
268
|
+
const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
|
|
269
|
+
const index = matchLocale(tags, preferredLocales(locale));
|
|
270
|
+
if (index !== -1) {
|
|
271
|
+
return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
|
|
272
|
+
}
|
|
273
|
+
const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
|
|
274
|
+
if (fallback) return { text: text[fallback], locale: null };
|
|
275
|
+
return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
|
|
276
|
+
}
|
|
277
|
+
function isRtlText(text) {
|
|
278
|
+
if (typeof text !== "string") return false;
|
|
279
|
+
const rtl = text.search(RTL_STRONG);
|
|
280
|
+
if (rtl === -1) return false;
|
|
281
|
+
const ltr = text.search(LTR_STRONG);
|
|
282
|
+
return ltr === -1 || rtl < ltr;
|
|
283
|
+
}
|
|
284
|
+
|
|
26
285
|
// src/utils.js
|
|
27
286
|
var DEFAULT_OPTIONS = {
|
|
28
287
|
border: true,
|
|
@@ -42,6 +301,11 @@ var DEFAULT_OPTIONS = {
|
|
|
42
301
|
fontFamily: null,
|
|
43
302
|
/** Fetch Caveat when the page has not provided it. */
|
|
44
303
|
autoLoadFont: true,
|
|
304
|
+
/**
|
|
305
|
+
* Language for a note written in more than one. Omit to follow the visitor's
|
|
306
|
+
* own languages, which is what their operating system is set to.
|
|
307
|
+
*/
|
|
308
|
+
locale: null,
|
|
45
309
|
/**
|
|
46
310
|
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
47
311
|
* doodles stay above the annotated element but below modals and other UI.
|
|
@@ -60,8 +324,15 @@ function resolveElement(target) {
|
|
|
60
324
|
}
|
|
61
325
|
function mergeOptions(options = {}) {
|
|
62
326
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
63
|
-
if (merged.note
|
|
64
|
-
merged.note
|
|
327
|
+
if (merged.note) {
|
|
328
|
+
const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
|
|
329
|
+
merged.note = {
|
|
330
|
+
...config,
|
|
331
|
+
position: config.position ?? "top-right",
|
|
332
|
+
// The note carries the chosen language, so `renderAnnotation` can resolve
|
|
333
|
+
// a multilingual note on its own.
|
|
334
|
+
locale: config.locale ?? merged.locale ?? null
|
|
335
|
+
};
|
|
65
336
|
}
|
|
66
337
|
if (merged.arrow) {
|
|
67
338
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -71,7 +342,10 @@ function mergeOptions(options = {}) {
|
|
|
71
342
|
// explicit `from` is honoured whether or not there is a note.
|
|
72
343
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
73
344
|
to: config.to ?? "edge",
|
|
74
|
-
style: config.style ?? "curved"
|
|
345
|
+
style: config.style ?? "curved",
|
|
346
|
+
// Arrows draw themselves on and lean toward their target by default;
|
|
347
|
+
// `animate: false` opts out, an object tunes speed and gestures
|
|
348
|
+
animate: normalizeArrowAnimation(config.animate)
|
|
75
349
|
};
|
|
76
350
|
}
|
|
77
351
|
if (merged.decorations === true) {
|
|
@@ -125,20 +399,6 @@ function isValidPosition(position) {
|
|
|
125
399
|
"center"
|
|
126
400
|
].includes(position);
|
|
127
401
|
}
|
|
128
|
-
function getScrollableAncestors(element) {
|
|
129
|
-
const ancestors = [];
|
|
130
|
-
let node = element.parentElement;
|
|
131
|
-
while (node && node !== document.documentElement) {
|
|
132
|
-
const style = getComputedStyle(node);
|
|
133
|
-
const overflow = style.overflow + style.overflowY + style.overflowX;
|
|
134
|
-
if (/(auto|scroll|overlay)/.test(overflow)) {
|
|
135
|
-
ancestors.push(node);
|
|
136
|
-
}
|
|
137
|
-
node = node.parentElement;
|
|
138
|
-
}
|
|
139
|
-
ancestors.push(window);
|
|
140
|
-
return ancestors;
|
|
141
|
-
}
|
|
142
402
|
function resolveOverlayZIndex(element, override) {
|
|
143
403
|
if (override != null) return override;
|
|
144
404
|
const { zIndex, position } = getComputedStyle(element);
|
|
@@ -157,13 +417,45 @@ function resolveOverlayZIndex(element, override) {
|
|
|
157
417
|
}
|
|
158
418
|
return null;
|
|
159
419
|
}
|
|
420
|
+
var ITEM_PLACEMENT_PROPERTIES = [
|
|
421
|
+
"order",
|
|
422
|
+
"flexGrow",
|
|
423
|
+
"flexShrink",
|
|
424
|
+
"flexBasis",
|
|
425
|
+
"alignSelf",
|
|
426
|
+
"justifySelf",
|
|
427
|
+
"gridColumn",
|
|
428
|
+
"gridRow",
|
|
429
|
+
"gridArea"
|
|
430
|
+
];
|
|
431
|
+
function isInlineDisplay(computed) {
|
|
432
|
+
return computed.display.startsWith("inline");
|
|
433
|
+
}
|
|
160
434
|
function insertOverlaySvg(svg, element) {
|
|
161
435
|
const parent = element.parentNode;
|
|
162
436
|
if (!parent) {
|
|
163
437
|
document.body.appendChild(svg);
|
|
164
|
-
return;
|
|
438
|
+
return null;
|
|
439
|
+
}
|
|
440
|
+
const wrapper = document.createElement("div");
|
|
441
|
+
wrapper.style.position = "relative";
|
|
442
|
+
const computed = getComputedStyle(element);
|
|
443
|
+
wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
|
|
444
|
+
for (const property of ITEM_PLACEMENT_PROPERTIES) {
|
|
445
|
+
const value = computed[property];
|
|
446
|
+
if (value) wrapper.style[property] = value;
|
|
447
|
+
}
|
|
448
|
+
parent.replaceChild(wrapper, element);
|
|
449
|
+
wrapper.appendChild(element);
|
|
450
|
+
wrapper.appendChild(svg);
|
|
451
|
+
return wrapper;
|
|
452
|
+
}
|
|
453
|
+
function removeOverlaySvg(svg, wrapper, element) {
|
|
454
|
+
if (svg.parentNode) {
|
|
455
|
+
svg.parentNode.removeChild(svg);
|
|
165
456
|
}
|
|
166
|
-
|
|
457
|
+
if (!wrapper || !wrapper.parentNode) return;
|
|
458
|
+
wrapper.parentNode.replaceChild(element, wrapper);
|
|
167
459
|
}
|
|
168
460
|
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
169
461
|
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
@@ -320,6 +612,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
320
612
|
|
|
321
613
|
// src/renderer.js
|
|
322
614
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
615
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
323
616
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
324
617
|
var NOTE_FONT_SIZE = 19;
|
|
325
618
|
function clearSvg(svg) {
|
|
@@ -565,6 +858,15 @@ function appendPath(svg, d, style) {
|
|
|
565
858
|
svg.appendChild(path);
|
|
566
859
|
return path;
|
|
567
860
|
}
|
|
861
|
+
function appendGlow(svg, radius, style) {
|
|
862
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
863
|
+
circle.setAttribute("r", fmt(radius));
|
|
864
|
+
circle.setAttribute("fill", style.color);
|
|
865
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
866
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
867
|
+
svg.appendChild(circle);
|
|
868
|
+
return circle;
|
|
869
|
+
}
|
|
568
870
|
function rectPerimeter(width, height, radius) {
|
|
569
871
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
570
872
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -651,20 +953,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
651
953
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
652
954
|
};
|
|
653
955
|
}
|
|
654
|
-
|
|
956
|
+
var STRADDLE = 0.75;
|
|
957
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
655
958
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
656
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
657
959
|
const knot = 0.46 + random() * 0.12;
|
|
658
960
|
const points = [];
|
|
659
961
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
660
962
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
661
963
|
const pivot = at(knot);
|
|
662
|
-
const
|
|
964
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
965
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
966
|
+
const heading = normalize(derivative);
|
|
663
967
|
const normal = { x: -heading.y, y: heading.x };
|
|
664
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
665
968
|
const centre = {
|
|
666
|
-
x: pivot.x + normal.x * radius * side,
|
|
667
|
-
y: pivot.y + normal.y * radius * side
|
|
969
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
970
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
668
971
|
};
|
|
669
972
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
670
973
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -672,15 +975,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
672
975
|
for (let i = 1; i <= steps; i++) {
|
|
673
976
|
const progress = i / steps;
|
|
674
977
|
const angle = from + turns * progress * side;
|
|
978
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
675
979
|
const drift = radius * 0.55 * progress;
|
|
676
980
|
points.push({
|
|
677
|
-
x: centre.x + Math.cos(angle) *
|
|
678
|
-
y: centre.y + Math.sin(angle) *
|
|
981
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
982
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
679
983
|
});
|
|
680
984
|
}
|
|
985
|
+
const exit = points[points.length - 1];
|
|
986
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
987
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
988
|
+
const rejoin = at(resume);
|
|
989
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
990
|
+
const settle = 6;
|
|
681
991
|
const tail = 14;
|
|
682
992
|
for (let i = 1; i <= tail; i++) {
|
|
683
|
-
|
|
993
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
994
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
995
|
+
points.push({
|
|
996
|
+
x: point.x + normal.x * residual * fade,
|
|
997
|
+
y: point.y + normal.y * residual * fade
|
|
998
|
+
});
|
|
684
999
|
}
|
|
685
1000
|
return points;
|
|
686
1001
|
}
|
|
@@ -754,21 +1069,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
754
1069
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
755
1070
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
756
1071
|
const spread = 0.46 + random() * 0.1;
|
|
1072
|
+
const arms = [];
|
|
757
1073
|
for (const side of [-1, 1]) {
|
|
758
1074
|
const armAngle = angle + spread * side;
|
|
759
1075
|
const tail = {
|
|
760
1076
|
x: tip.x - length * Math.cos(armAngle),
|
|
761
1077
|
y: tip.y - length * Math.sin(armAngle)
|
|
762
1078
|
};
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
1079
|
+
arms.push(
|
|
1080
|
+
appendPath(
|
|
1081
|
+
svg,
|
|
1082
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1083
|
+
steps: 2,
|
|
1084
|
+
bow: 0.35 * side
|
|
1085
|
+
}),
|
|
1086
|
+
{ ...style, dashed: false }
|
|
1087
|
+
)
|
|
770
1088
|
);
|
|
771
1089
|
}
|
|
1090
|
+
return arms;
|
|
1091
|
+
}
|
|
1092
|
+
function bowOverTheTop(perpendicular) {
|
|
1093
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1094
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
772
1095
|
}
|
|
773
1096
|
function boundsOf(points) {
|
|
774
1097
|
const xs = points.map((p) => p.x);
|
|
@@ -780,7 +1103,7 @@ function boundsOf(points) {
|
|
|
780
1103
|
height: Math.max(...ys) - Math.min(...ys)
|
|
781
1104
|
};
|
|
782
1105
|
}
|
|
783
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1106
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
784
1107
|
const random = createRandom(seed + 17);
|
|
785
1108
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
786
1109
|
const dx = end.x - start.x;
|
|
@@ -789,6 +1112,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
789
1112
|
const dashed = arrowOptions.style === "dotted";
|
|
790
1113
|
let tipAngle = Math.atan2(dy, dx);
|
|
791
1114
|
let shaft;
|
|
1115
|
+
let ghost = null;
|
|
792
1116
|
const hull = [start, end];
|
|
793
1117
|
if (arrowOptions.style === "straight") {
|
|
794
1118
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -802,7 +1126,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
802
1126
|
y: rect.y + rect.height / 2 - mid.y
|
|
803
1127
|
};
|
|
804
1128
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
805
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1129
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1130
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1131
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1132
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1133
|
+
bowOverTheTop(perpendicular)
|
|
1134
|
+
);
|
|
806
1135
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
807
1136
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
808
1137
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -817,34 +1146,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
817
1146
|
{ x: c2x, y: c2y },
|
|
818
1147
|
end,
|
|
819
1148
|
radius,
|
|
820
|
-
random
|
|
1149
|
+
random,
|
|
1150
|
+
-sign
|
|
821
1151
|
);
|
|
822
1152
|
shaft = catmullRomPath(points, false);
|
|
823
1153
|
hull.push(...points);
|
|
824
1154
|
const last = points[points.length - 1];
|
|
825
1155
|
const prior = points[points.length - 2] ?? start;
|
|
826
1156
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1157
|
+
} else {
|
|
1158
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1159
|
+
if (style.roughness >= 1.1) {
|
|
1160
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1161
|
+
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)}`;
|
|
1162
|
+
}
|
|
1163
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
830
1164
|
}
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
1165
|
+
}
|
|
1166
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1167
|
+
svg.appendChild(group);
|
|
1168
|
+
const shaftPaths = [];
|
|
1169
|
+
if (ghost) {
|
|
1170
|
+
shaftPaths.push(
|
|
1171
|
+
appendPath(group, ghost, {
|
|
836
1172
|
...style,
|
|
837
1173
|
opacity: style.opacity * 0.3,
|
|
838
1174
|
strokeWidth: style.strokeWidth * 0.7,
|
|
839
1175
|
dashed
|
|
840
|
-
})
|
|
841
|
-
|
|
842
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1176
|
+
})
|
|
1177
|
+
);
|
|
843
1178
|
}
|
|
844
|
-
appendPath(
|
|
845
|
-
drawArrowHead(
|
|
1179
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1180
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1181
|
+
animateArrow(
|
|
1182
|
+
group,
|
|
1183
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1184
|
+
arrowOptions.animate,
|
|
1185
|
+
context.elapsed
|
|
1186
|
+
);
|
|
846
1187
|
const bounds = boundsOf(hull);
|
|
847
|
-
return { start, end, bounds };
|
|
1188
|
+
return { start, end, bounds, group };
|
|
848
1189
|
}
|
|
849
1190
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
850
1191
|
if (width <= 4) return;
|
|
@@ -911,6 +1252,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
911
1252
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
912
1253
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
913
1254
|
const lineHeight = fontSize * 1.05;
|
|
1255
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
914
1256
|
const group = document.createElementNS(SVG_NS, "g");
|
|
915
1257
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
916
1258
|
textEl.setAttribute("x", "0");
|
|
@@ -923,6 +1265,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
923
1265
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
924
1266
|
textEl.setAttribute("text-anchor", "start");
|
|
925
1267
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1268
|
+
if (rtl) {
|
|
1269
|
+
textEl.setAttribute("direction", "rtl");
|
|
1270
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1271
|
+
}
|
|
1272
|
+
if (options.locale) {
|
|
1273
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1274
|
+
}
|
|
926
1275
|
const tspans = lines.map((line, index) => {
|
|
927
1276
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
928
1277
|
tspan.textContent = line;
|
|
@@ -951,15 +1300,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
951
1300
|
height,
|
|
952
1301
|
ascent,
|
|
953
1302
|
descent,
|
|
954
|
-
fontSize
|
|
1303
|
+
fontSize,
|
|
1304
|
+
rtl
|
|
955
1305
|
};
|
|
956
1306
|
}
|
|
957
1307
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
958
1308
|
const random = createRandom(seed + 55);
|
|
959
1309
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
960
|
-
note.
|
|
1310
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1311
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
961
1312
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
962
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1313
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
963
1314
|
const cx = box.x + box.width / 2;
|
|
964
1315
|
const cy = box.y + box.height / 2;
|
|
965
1316
|
note.group.setAttribute(
|
|
@@ -968,9 +1319,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
968
1319
|
);
|
|
969
1320
|
if (options.underline !== false) {
|
|
970
1321
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1322
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
971
1323
|
drawUnderline(
|
|
972
1324
|
note.group,
|
|
973
|
-
|
|
1325
|
+
underlineX,
|
|
974
1326
|
box.y + box.height + 2,
|
|
975
1327
|
lastWidth - 2,
|
|
976
1328
|
{
|
|
@@ -991,7 +1343,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
991
1343
|
svg.setAttribute("width", String(width));
|
|
992
1344
|
svg.setAttribute("height", String(height));
|
|
993
1345
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
994
|
-
svg.style.position = "
|
|
1346
|
+
svg.style.position = "absolute";
|
|
995
1347
|
svg.style.left = `${left}px`;
|
|
996
1348
|
svg.style.top = `${top}px`;
|
|
997
1349
|
svg.style.width = `${width}px`;
|
|
@@ -1044,12 +1396,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1044
1396
|
return { ...box, x: box.x + dx };
|
|
1045
1397
|
}
|
|
1046
1398
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
1047
|
-
|
|
1399
|
+
const { text, locale } = resolveLocalizedText(
|
|
1400
|
+
noteOptions?.text,
|
|
1401
|
+
noteOptions?.locale
|
|
1402
|
+
);
|
|
1403
|
+
if (!text) return null;
|
|
1048
1404
|
const band = layout.band ?? null;
|
|
1049
1405
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1050
1406
|
const random = createRandom(seed + 9);
|
|
1051
|
-
const note = createNoteText(svg,
|
|
1052
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1407
|
+
const note = createNoteText(svg, text, style, {
|
|
1408
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1409
|
+
locale
|
|
1053
1410
|
});
|
|
1054
1411
|
const tilt = (random() - 0.45) * 7;
|
|
1055
1412
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1091,6 +1448,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1091
1448
|
strokeWidth: style.strokeWidth * scale
|
|
1092
1449
|
});
|
|
1093
1450
|
}
|
|
1451
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1452
|
+
const count = 6;
|
|
1453
|
+
const points = [];
|
|
1454
|
+
for (let i = 0; i < count; i++) {
|
|
1455
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1456
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1457
|
+
points.push({
|
|
1458
|
+
x: x + Math.cos(angle) * r,
|
|
1459
|
+
y: y + Math.sin(angle) * r
|
|
1460
|
+
});
|
|
1461
|
+
}
|
|
1462
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1463
|
+
...style,
|
|
1464
|
+
fill: style.color,
|
|
1465
|
+
strokeWidth: 0
|
|
1466
|
+
});
|
|
1467
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1468
|
+
return blob;
|
|
1469
|
+
}
|
|
1094
1470
|
function drawHeart(parent, size, style, random) {
|
|
1095
1471
|
const s = size;
|
|
1096
1472
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1114,26 +1490,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1114
1490
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1115
1491
|
"Z"
|
|
1116
1492
|
].join(" ");
|
|
1117
|
-
appendPath(parent, path, style);
|
|
1493
|
+
return appendPath(parent, path, style);
|
|
1118
1494
|
}
|
|
1119
1495
|
function drawSparkle(parent, size, style, random) {
|
|
1120
1496
|
twinkle(
|
|
1121
1497
|
parent,
|
|
1122
|
-
size * (0.
|
|
1498
|
+
size * (0.62 + random() * 0.12),
|
|
1123
1499
|
size,
|
|
1124
|
-
size * (0.
|
|
1500
|
+
size * (0.14 + random() * 0.08),
|
|
1125
1501
|
style
|
|
1126
1502
|
);
|
|
1127
1503
|
}
|
|
1128
1504
|
function drawStar(parent, size, style, random) {
|
|
1129
1505
|
twinkle(
|
|
1130
1506
|
parent,
|
|
1131
|
-
size * (0.
|
|
1132
|
-
size,
|
|
1133
|
-
size * (0.
|
|
1507
|
+
size * (0.62 + random() * 0.12),
|
|
1508
|
+
size * (0.8 + random() * 0.12),
|
|
1509
|
+
size * (0.14 + random() * 0.08),
|
|
1134
1510
|
style
|
|
1135
1511
|
);
|
|
1136
1512
|
}
|
|
1513
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1514
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1515
|
+
color: style.color,
|
|
1516
|
+
opacity: style.opacity * 0.22
|
|
1517
|
+
});
|
|
1518
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1519
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1520
|
+
const bluntness = size * 0.19;
|
|
1521
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1522
|
+
...style,
|
|
1523
|
+
fill: style.color,
|
|
1524
|
+
strokeWidth: bluntness
|
|
1525
|
+
});
|
|
1526
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1527
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1528
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1529
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1530
|
+
const corners = [
|
|
1531
|
+
{ x: 0, y: -(armY + gap) },
|
|
1532
|
+
{ x: armX + gap, y: 0 },
|
|
1533
|
+
{ x: 0, y: armY + gap },
|
|
1534
|
+
{ x: -(armX + gap), y: 0 }
|
|
1535
|
+
];
|
|
1536
|
+
for (const { x, y } of corners) {
|
|
1537
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1538
|
+
drawFleck(
|
|
1539
|
+
parent,
|
|
1540
|
+
x * jitter,
|
|
1541
|
+
y * jitter,
|
|
1542
|
+
size * (0.1 + random() * 0.03),
|
|
1543
|
+
style,
|
|
1544
|
+
random
|
|
1545
|
+
);
|
|
1546
|
+
}
|
|
1547
|
+
}
|
|
1137
1548
|
function drawTwinkle(parent, size, style, random) {
|
|
1138
1549
|
const count = 5;
|
|
1139
1550
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1241,46 +1652,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1241
1652
|
);
|
|
1242
1653
|
}
|
|
1243
1654
|
}
|
|
1244
|
-
function drawDots(parent, size, style, random) {
|
|
1245
|
-
let x = -size * 0.7;
|
|
1246
|
-
for (let i = 0; i < 3; i++) {
|
|
1247
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1248
|
-
x += size * (0.6 + random() * 0.25);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
function drawStroke(parent, size, style, random) {
|
|
1252
|
-
appendPath(
|
|
1253
|
-
parent,
|
|
1254
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1255
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1256
|
-
steps: 4
|
|
1257
|
-
}),
|
|
1258
|
-
style
|
|
1259
|
-
);
|
|
1260
|
-
}
|
|
1261
|
-
function drawSteam(parent, size, style, random) {
|
|
1262
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1263
|
-
for (let i = 0; i < 2; i++) {
|
|
1264
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1265
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1266
|
-
appendPath(
|
|
1267
|
-
parent,
|
|
1268
|
-
`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)}`,
|
|
1269
|
-
style
|
|
1270
|
-
);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
1655
|
var DECORATION_DRAWERS = {
|
|
1274
1656
|
heart: drawHeart,
|
|
1275
1657
|
sparkle: drawSparkle,
|
|
1276
1658
|
star: drawStar,
|
|
1277
1659
|
twinkle: drawTwinkle,
|
|
1660
|
+
glimmer: drawGlimmer,
|
|
1278
1661
|
smiley: drawSmiley,
|
|
1279
1662
|
emphasis: drawEmphasis,
|
|
1280
|
-
arcs: drawArcs
|
|
1281
|
-
dots: drawDots,
|
|
1282
|
-
stroke: drawStroke,
|
|
1283
|
-
steam: drawSteam
|
|
1663
|
+
arcs: drawArcs
|
|
1284
1664
|
};
|
|
1285
1665
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1286
1666
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1291,12 +1671,10 @@ var TYPE_TRAITS = {
|
|
|
1291
1671
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1292
1672
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1293
1673
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1674
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1294
1675
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1295
1676
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1296
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1297
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1298
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1299
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1677
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1300
1678
|
};
|
|
1301
1679
|
var DEFAULT_TYPES = [
|
|
1302
1680
|
"twinkle",
|
|
@@ -1499,6 +1877,9 @@ function ensureHandwrittenFont() {
|
|
|
1499
1877
|
|
|
1500
1878
|
// src/doodle.js
|
|
1501
1879
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1880
|
+
function now() {
|
|
1881
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1882
|
+
}
|
|
1502
1883
|
function localBand(overlayRect, inset = 8) {
|
|
1503
1884
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1504
1885
|
return {
|
|
@@ -1515,19 +1896,21 @@ var DoodleOverlay = class {
|
|
|
1515
1896
|
this.element = element;
|
|
1516
1897
|
this.options = options;
|
|
1517
1898
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1899
|
+
this.startedAt = now();
|
|
1518
1900
|
this.svg = null;
|
|
1901
|
+
this.wrapper = null;
|
|
1519
1902
|
this.resizeObserver = null;
|
|
1520
1903
|
this.mutationObserver = null;
|
|
1521
1904
|
this.pendingFrame = null;
|
|
1522
|
-
this.
|
|
1523
|
-
this.onScroll = this.scheduleUpdate.bind(this);
|
|
1905
|
+
this.listeners = [];
|
|
1524
1906
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1907
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1525
1908
|
this.childElements = [];
|
|
1526
1909
|
this.mount();
|
|
1527
1910
|
}
|
|
1528
1911
|
mount() {
|
|
1529
1912
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1530
|
-
insertOverlaySvg(this.svg, this.element);
|
|
1913
|
+
this.wrapper = insertOverlaySvg(this.svg, this.element);
|
|
1531
1914
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1532
1915
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1533
1916
|
this.resizeObserver.observe(this.element);
|
|
@@ -1540,12 +1923,18 @@ var DoodleOverlay = class {
|
|
|
1540
1923
|
subtree: true,
|
|
1541
1924
|
attributes: true
|
|
1542
1925
|
});
|
|
1543
|
-
for (const target of getScrollableAncestors(this.element)) {
|
|
1544
|
-
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1545
|
-
this.scrollTargets.push({ target, type: "scroll" });
|
|
1546
|
-
}
|
|
1547
1926
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1548
|
-
this.
|
|
1927
|
+
this.listeners.push({
|
|
1928
|
+
target: window,
|
|
1929
|
+
type: "resize",
|
|
1930
|
+
handler: this.onResize
|
|
1931
|
+
});
|
|
1932
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1933
|
+
this.listeners.push({
|
|
1934
|
+
target: window,
|
|
1935
|
+
type: "languagechange",
|
|
1936
|
+
handler: this.onLanguageChange
|
|
1937
|
+
});
|
|
1549
1938
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1550
1939
|
ensureHandwrittenFont();
|
|
1551
1940
|
}
|
|
@@ -1581,17 +1970,19 @@ var DoodleOverlay = class {
|
|
|
1581
1970
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1582
1971
|
const targets = this.getTargets();
|
|
1583
1972
|
const margin = this.overlayMargin();
|
|
1973
|
+
const elapsed = now() - this.startedAt;
|
|
1584
1974
|
const absoluteRects = targets.map(
|
|
1585
1975
|
(target) => getElementBounds(target, padding, radius)
|
|
1586
1976
|
);
|
|
1587
1977
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1588
1978
|
clearSvg(this.svg);
|
|
1589
1979
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1980
|
+
const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1590
1981
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1591
1982
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1592
1983
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
1593
|
-
this.svg.style.left = `${overlayRect.x}px`;
|
|
1594
|
-
this.svg.style.top = `${overlayRect.y}px`;
|
|
1984
|
+
this.svg.style.left = `${overlayRect.x - origin.left}px`;
|
|
1985
|
+
this.svg.style.top = `${overlayRect.y - origin.top}px`;
|
|
1595
1986
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1596
1987
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
1597
1988
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
@@ -1633,7 +2024,8 @@ var DoodleOverlay = class {
|
|
|
1633
2024
|
this.options.arrow,
|
|
1634
2025
|
strokeStyle,
|
|
1635
2026
|
seed,
|
|
1636
|
-
note ? note.box : null
|
|
2027
|
+
note ? note.box : null,
|
|
2028
|
+
{ elapsed }
|
|
1637
2029
|
) : null;
|
|
1638
2030
|
if (this.options.decorations) {
|
|
1639
2031
|
renderDecorations(
|
|
@@ -1660,7 +2052,7 @@ var DoodleOverlay = class {
|
|
|
1660
2052
|
overlayMargin() {
|
|
1661
2053
|
const { note, arrow, decorations } = this.options;
|
|
1662
2054
|
if (!note && !arrow && !decorations) return 48;
|
|
1663
|
-
const text =
|
|
2055
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1664
2056
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1665
2057
|
}
|
|
1666
2058
|
destroy() {
|
|
@@ -1680,14 +2072,15 @@ var DoodleOverlay = class {
|
|
|
1680
2072
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1681
2073
|
this.onFontsLoaded = null;
|
|
1682
2074
|
}
|
|
1683
|
-
for (const { target, type } of this.
|
|
1684
|
-
target.removeEventListener(type,
|
|
2075
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2076
|
+
target.removeEventListener(type, handler);
|
|
1685
2077
|
}
|
|
1686
|
-
this.
|
|
1687
|
-
if (this.svg
|
|
1688
|
-
this.svg.
|
|
2078
|
+
this.listeners = [];
|
|
2079
|
+
if (this.svg) {
|
|
2080
|
+
removeOverlaySvg(this.svg, this.wrapper, this.element);
|
|
1689
2081
|
}
|
|
1690
2082
|
this.svg = null;
|
|
2083
|
+
this.wrapper = null;
|
|
1691
2084
|
instances.delete(this.element);
|
|
1692
2085
|
}
|
|
1693
2086
|
};
|