@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.js
CHANGED
|
@@ -1,3 +1,262 @@
|
|
|
1
|
+
// src/animation.js
|
|
2
|
+
var STYLE_MARKER = "data-freehand-animation";
|
|
3
|
+
var CSS = `
|
|
4
|
+
@keyframes freehand-arrow-dash {
|
|
5
|
+
from { stroke-dashoffset: var(--freehand-dash, 0px); }
|
|
6
|
+
to { stroke-dashoffset: 0; }
|
|
7
|
+
}
|
|
8
|
+
@keyframes freehand-arrow-drift {
|
|
9
|
+
0%, 100% { transform: translate(0px, 0px); }
|
|
10
|
+
50% { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
11
|
+
}
|
|
12
|
+
@keyframes freehand-arrow-settle {
|
|
13
|
+
from { transform: translate(0px, 0px); }
|
|
14
|
+
to { transform: translate(var(--freehand-drift-x, 0px), var(--freehand-drift-y, 0px)); }
|
|
15
|
+
}
|
|
16
|
+
@media (prefers-reduced-motion: reduce) {
|
|
17
|
+
.freehand-arrow, .freehand-arrow path { animation: none !important; }
|
|
18
|
+
.freehand-arrow path { stroke-dashoffset: 0 !important; }
|
|
19
|
+
}
|
|
20
|
+
`;
|
|
21
|
+
var ARROW_ANIMATION_DEFAULTS = {
|
|
22
|
+
/** Reveal the stroke with a dash sweep. */
|
|
23
|
+
draw: true,
|
|
24
|
+
/** Lean the arrow toward its tip. */
|
|
25
|
+
drift: true,
|
|
26
|
+
/** Multiplier over every duration - 2 plays twice as fast. */
|
|
27
|
+
speed: 1,
|
|
28
|
+
/** How long the shaft takes to draw, ms, before `speed`. */
|
|
29
|
+
duration: 900,
|
|
30
|
+
/** Wait before the first stroke appears, ms. */
|
|
31
|
+
delay: 150,
|
|
32
|
+
/** How far the arrow leans, px. */
|
|
33
|
+
distance: 5,
|
|
34
|
+
/** One full lean-and-return, ms. */
|
|
35
|
+
driftDuration: 2200,
|
|
36
|
+
/** Keep leaning. `false` settles once and stays put. */
|
|
37
|
+
repeat: true
|
|
38
|
+
};
|
|
39
|
+
function positive(value, fallback) {
|
|
40
|
+
const n = Number(value);
|
|
41
|
+
return Number.isFinite(n) && n > 0 ? n : fallback;
|
|
42
|
+
}
|
|
43
|
+
function atLeastZero(value, fallback) {
|
|
44
|
+
const n = Number(value);
|
|
45
|
+
return Number.isFinite(n) && n >= 0 ? n : fallback;
|
|
46
|
+
}
|
|
47
|
+
function normalizeArrowAnimation(config) {
|
|
48
|
+
if (config === false || config === null) return false;
|
|
49
|
+
const settings = config === true || config === void 0 || typeof config !== "object" ? {} : config;
|
|
50
|
+
const animation = {
|
|
51
|
+
draw: settings.draw !== false,
|
|
52
|
+
drift: settings.drift !== false,
|
|
53
|
+
speed: positive(settings.speed, ARROW_ANIMATION_DEFAULTS.speed),
|
|
54
|
+
duration: positive(settings.duration, ARROW_ANIMATION_DEFAULTS.duration),
|
|
55
|
+
delay: atLeastZero(settings.delay, ARROW_ANIMATION_DEFAULTS.delay),
|
|
56
|
+
distance: atLeastZero(settings.distance, ARROW_ANIMATION_DEFAULTS.distance),
|
|
57
|
+
driftDuration: positive(
|
|
58
|
+
settings.driftDuration,
|
|
59
|
+
ARROW_ANIMATION_DEFAULTS.driftDuration
|
|
60
|
+
),
|
|
61
|
+
repeat: settings.repeat !== false
|
|
62
|
+
};
|
|
63
|
+
if (!animation.draw && (!animation.drift || !animation.distance)) return false;
|
|
64
|
+
return animation;
|
|
65
|
+
}
|
|
66
|
+
var FLOW_PERIOD = 260;
|
|
67
|
+
function arrowTiming(animation) {
|
|
68
|
+
const duration = animation.duration / animation.speed;
|
|
69
|
+
const delay = animation.delay / animation.speed;
|
|
70
|
+
const drawn = animation.draw ? duration : 0;
|
|
71
|
+
return {
|
|
72
|
+
shaft: { duration, delay },
|
|
73
|
+
head: { duration: duration * 0.22, delay: delay + drawn * 0.82 },
|
|
74
|
+
flow: { duration: FLOW_PERIOD / animation.speed, delay },
|
|
75
|
+
drift: {
|
|
76
|
+
duration: animation.driftDuration / animation.speed,
|
|
77
|
+
delay: delay + drawn * 1.04
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
var injected = false;
|
|
82
|
+
function ensureArrowAnimationStyles() {
|
|
83
|
+
if (injected) return;
|
|
84
|
+
if (typeof document === "undefined" || !document.head) return;
|
|
85
|
+
injected = true;
|
|
86
|
+
if (document.querySelector(`style[${STYLE_MARKER}]`)) return;
|
|
87
|
+
const style = document.createElement("style");
|
|
88
|
+
style.setAttribute(STYLE_MARKER, "");
|
|
89
|
+
style.textContent = CSS;
|
|
90
|
+
document.head.appendChild(style);
|
|
91
|
+
}
|
|
92
|
+
function time(ms) {
|
|
93
|
+
return `${Math.round(ms)}ms`;
|
|
94
|
+
}
|
|
95
|
+
function setAnimation(element, spec) {
|
|
96
|
+
const parts = [
|
|
97
|
+
spec.name,
|
|
98
|
+
time(spec.duration),
|
|
99
|
+
spec.easing,
|
|
100
|
+
time(spec.delay),
|
|
101
|
+
spec.iterations ?? "1"
|
|
102
|
+
];
|
|
103
|
+
if (spec.fill) parts.push(spec.fill);
|
|
104
|
+
element.style.animation = parts.join(" ");
|
|
105
|
+
}
|
|
106
|
+
function pathLength(path) {
|
|
107
|
+
try {
|
|
108
|
+
const length = path.getTotalLength();
|
|
109
|
+
return Number.isFinite(length) && length > 0 ? length : 0;
|
|
110
|
+
} catch {
|
|
111
|
+
return 0;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function dashPeriod(path) {
|
|
115
|
+
const pattern = (path.getAttribute("stroke-dasharray") || "").split(/[\s,]+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
|
|
116
|
+
if (!pattern.length) return 0;
|
|
117
|
+
const sum = pattern.reduce((total, n) => total + n, 0);
|
|
118
|
+
return pattern.length % 2 === 0 ? sum : sum * 2;
|
|
119
|
+
}
|
|
120
|
+
function applyDash(path, timing, elapsed, flow) {
|
|
121
|
+
const length = pathLength(path);
|
|
122
|
+
if (!length) return;
|
|
123
|
+
const period = dashPeriod(path);
|
|
124
|
+
if (period) {
|
|
125
|
+
path.style.setProperty("--freehand-dash", `${period.toFixed(2)}px`);
|
|
126
|
+
setAnimation(path, {
|
|
127
|
+
name: "freehand-arrow-dash",
|
|
128
|
+
duration: flow.duration,
|
|
129
|
+
easing: "linear",
|
|
130
|
+
delay: flow.delay - elapsed,
|
|
131
|
+
iterations: "infinite"
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
path.style.setProperty("--freehand-dash", `${length.toFixed(2)}px`);
|
|
136
|
+
path.style.strokeDasharray = `${length.toFixed(2)}px`;
|
|
137
|
+
setAnimation(path, {
|
|
138
|
+
name: "freehand-arrow-dash",
|
|
139
|
+
duration: timing.duration,
|
|
140
|
+
easing: "ease-out",
|
|
141
|
+
delay: timing.delay - elapsed,
|
|
142
|
+
fill: "both"
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function applyDrift(group, parts, timing, elapsed, animation) {
|
|
146
|
+
const dx = parts.end.x - parts.start.x;
|
|
147
|
+
const dy = parts.end.y - parts.start.y;
|
|
148
|
+
const length = Math.hypot(dx, dy);
|
|
149
|
+
if (!length) return;
|
|
150
|
+
const scale = animation.distance / length;
|
|
151
|
+
group.style.setProperty("--freehand-drift-x", `${(dx * scale).toFixed(2)}px`);
|
|
152
|
+
group.style.setProperty("--freehand-drift-y", `${(dy * scale).toFixed(2)}px`);
|
|
153
|
+
setAnimation(
|
|
154
|
+
group,
|
|
155
|
+
animation.repeat ? {
|
|
156
|
+
name: "freehand-arrow-drift",
|
|
157
|
+
duration: timing.duration,
|
|
158
|
+
easing: "ease-in-out",
|
|
159
|
+
delay: timing.delay - elapsed,
|
|
160
|
+
iterations: "infinite"
|
|
161
|
+
} : {
|
|
162
|
+
// A single lean is a settle, not a round trip, so it takes half a cycle
|
|
163
|
+
name: "freehand-arrow-settle",
|
|
164
|
+
duration: timing.duration * 0.5,
|
|
165
|
+
easing: "ease-out",
|
|
166
|
+
delay: timing.delay - elapsed,
|
|
167
|
+
fill: "both"
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
function animateArrow(group, parts, animation, elapsed = 0) {
|
|
172
|
+
if (!animation || !group) return;
|
|
173
|
+
ensureArrowAnimationStyles();
|
|
174
|
+
group.setAttribute("class", "freehand-arrow");
|
|
175
|
+
const timing = arrowTiming(animation);
|
|
176
|
+
const age = Number.isFinite(elapsed) ? Math.max(0, elapsed) : 0;
|
|
177
|
+
if (animation.draw) {
|
|
178
|
+
for (const path of parts.shaft) {
|
|
179
|
+
applyDash(path, timing.shaft, age, timing.flow);
|
|
180
|
+
}
|
|
181
|
+
for (const path of parts.head) applyDash(path, timing.head, age, timing.flow);
|
|
182
|
+
}
|
|
183
|
+
if (animation.drift && animation.distance > 0) {
|
|
184
|
+
applyDrift(group, parts, timing.drift, age, animation);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// src/locale.js
|
|
189
|
+
var DEFAULT_KEY = "default";
|
|
190
|
+
var LANGUAGE_TAG = /^[a-z]{2,3}(-[a-z0-9]{2,8})*$/i;
|
|
191
|
+
var RTL_STRONG = /[֑-߿יִ-﷿ﹰ-ﻼ]/;
|
|
192
|
+
var LTR_STRONG = /[A-Za-zÀ-֏ऀ-Ⰰ-豈-ﬗA-Za-z]/;
|
|
193
|
+
function normalizeTag(tag) {
|
|
194
|
+
if (typeof tag !== "string") return "";
|
|
195
|
+
return tag.trim().replace(/_/g, "-").toLowerCase();
|
|
196
|
+
}
|
|
197
|
+
function isLocaleMap(value) {
|
|
198
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
199
|
+
const entries = Object.entries(value);
|
|
200
|
+
if (!entries.length) return false;
|
|
201
|
+
return entries.every(([key, text]) => {
|
|
202
|
+
if (typeof text !== "string") return false;
|
|
203
|
+
const tag = normalizeTag(key);
|
|
204
|
+
return tag === DEFAULT_KEY || LANGUAGE_TAG.test(tag);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function preferredLocales(override) {
|
|
208
|
+
const tags = [];
|
|
209
|
+
const add = (value) => {
|
|
210
|
+
const tag = normalizeTag(value);
|
|
211
|
+
if (tag && !tags.includes(tag)) tags.push(tag);
|
|
212
|
+
};
|
|
213
|
+
if (Array.isArray(override)) override.forEach(add);
|
|
214
|
+
else add(override);
|
|
215
|
+
const nav = typeof navigator === "undefined" ? null : navigator;
|
|
216
|
+
if (nav) {
|
|
217
|
+
if (Array.isArray(nav.languages)) nav.languages.forEach(add);
|
|
218
|
+
add(nav.language);
|
|
219
|
+
}
|
|
220
|
+
if (typeof document !== "undefined") add(document.documentElement?.lang);
|
|
221
|
+
return tags;
|
|
222
|
+
}
|
|
223
|
+
function matchLocale(tags, preferred) {
|
|
224
|
+
const available = tags.map(normalizeTag);
|
|
225
|
+
for (const want of preferred) {
|
|
226
|
+
let tag = want;
|
|
227
|
+
while (tag) {
|
|
228
|
+
const exact = available.indexOf(tag);
|
|
229
|
+
if (exact !== -1) return exact;
|
|
230
|
+
const cut = tag.lastIndexOf("-");
|
|
231
|
+
tag = cut > 0 ? tag.slice(0, cut) : "";
|
|
232
|
+
}
|
|
233
|
+
const base = want.split("-")[0];
|
|
234
|
+
const sibling = available.findIndex((key) => key.split("-")[0] === base);
|
|
235
|
+
if (sibling !== -1) return sibling;
|
|
236
|
+
}
|
|
237
|
+
return -1;
|
|
238
|
+
}
|
|
239
|
+
function resolveLocalizedText(text, locale = null) {
|
|
240
|
+
if (typeof text === "string") return { text, locale: null };
|
|
241
|
+
if (!isLocaleMap(text)) return { text: "", locale: null };
|
|
242
|
+
const keys = Object.keys(text);
|
|
243
|
+
const tags = keys.filter((key) => normalizeTag(key) !== DEFAULT_KEY);
|
|
244
|
+
const index = matchLocale(tags, preferredLocales(locale));
|
|
245
|
+
if (index !== -1) {
|
|
246
|
+
return { text: text[tags[index]], locale: normalizeTag(tags[index]) };
|
|
247
|
+
}
|
|
248
|
+
const fallback = keys.find((key) => normalizeTag(key) === DEFAULT_KEY);
|
|
249
|
+
if (fallback) return { text: text[fallback], locale: null };
|
|
250
|
+
return { text: text[keys[0]], locale: normalizeTag(keys[0]) };
|
|
251
|
+
}
|
|
252
|
+
function isRtlText(text) {
|
|
253
|
+
if (typeof text !== "string") return false;
|
|
254
|
+
const rtl = text.search(RTL_STRONG);
|
|
255
|
+
if (rtl === -1) return false;
|
|
256
|
+
const ltr = text.search(LTR_STRONG);
|
|
257
|
+
return ltr === -1 || rtl < ltr;
|
|
258
|
+
}
|
|
259
|
+
|
|
1
260
|
// src/utils.js
|
|
2
261
|
var DEFAULT_OPTIONS = {
|
|
3
262
|
border: true,
|
|
@@ -17,6 +276,11 @@ var DEFAULT_OPTIONS = {
|
|
|
17
276
|
fontFamily: null,
|
|
18
277
|
/** Fetch Caveat when the page has not provided it. */
|
|
19
278
|
autoLoadFont: true,
|
|
279
|
+
/**
|
|
280
|
+
* Language for a note written in more than one. Omit to follow the visitor's
|
|
281
|
+
* own languages, which is what their operating system is set to.
|
|
282
|
+
*/
|
|
283
|
+
locale: null,
|
|
20
284
|
/**
|
|
21
285
|
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
22
286
|
* doodles stay above the annotated element but below modals and other UI.
|
|
@@ -35,8 +299,15 @@ function resolveElement(target) {
|
|
|
35
299
|
}
|
|
36
300
|
function mergeOptions(options = {}) {
|
|
37
301
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
38
|
-
if (merged.note
|
|
39
|
-
merged.note
|
|
302
|
+
if (merged.note) {
|
|
303
|
+
const config = typeof merged.note === "string" || isLocaleMap(merged.note) ? { text: merged.note } : merged.note;
|
|
304
|
+
merged.note = {
|
|
305
|
+
...config,
|
|
306
|
+
position: config.position ?? "top-right",
|
|
307
|
+
// The note carries the chosen language, so `renderAnnotation` can resolve
|
|
308
|
+
// a multilingual note on its own.
|
|
309
|
+
locale: config.locale ?? merged.locale ?? null
|
|
310
|
+
};
|
|
40
311
|
}
|
|
41
312
|
if (merged.arrow) {
|
|
42
313
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -46,7 +317,10 @@ function mergeOptions(options = {}) {
|
|
|
46
317
|
// explicit `from` is honoured whether or not there is a note.
|
|
47
318
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
48
319
|
to: config.to ?? "edge",
|
|
49
|
-
style: config.style ?? "curved"
|
|
320
|
+
style: config.style ?? "curved",
|
|
321
|
+
// Arrows draw themselves on and lean toward their target by default;
|
|
322
|
+
// `animate: false` opts out, an object tunes speed and gestures
|
|
323
|
+
animate: normalizeArrowAnimation(config.animate)
|
|
50
324
|
};
|
|
51
325
|
}
|
|
52
326
|
if (merged.decorations === true) {
|
|
@@ -100,20 +374,6 @@ function isValidPosition(position) {
|
|
|
100
374
|
"center"
|
|
101
375
|
].includes(position);
|
|
102
376
|
}
|
|
103
|
-
function getScrollableAncestors(element) {
|
|
104
|
-
const ancestors = [];
|
|
105
|
-
let node = element.parentElement;
|
|
106
|
-
while (node && node !== document.documentElement) {
|
|
107
|
-
const style = getComputedStyle(node);
|
|
108
|
-
const overflow = style.overflow + style.overflowY + style.overflowX;
|
|
109
|
-
if (/(auto|scroll|overlay)/.test(overflow)) {
|
|
110
|
-
ancestors.push(node);
|
|
111
|
-
}
|
|
112
|
-
node = node.parentElement;
|
|
113
|
-
}
|
|
114
|
-
ancestors.push(window);
|
|
115
|
-
return ancestors;
|
|
116
|
-
}
|
|
117
377
|
function resolveOverlayZIndex(element, override) {
|
|
118
378
|
if (override != null) return override;
|
|
119
379
|
const { zIndex, position } = getComputedStyle(element);
|
|
@@ -132,13 +392,45 @@ function resolveOverlayZIndex(element, override) {
|
|
|
132
392
|
}
|
|
133
393
|
return null;
|
|
134
394
|
}
|
|
395
|
+
var ITEM_PLACEMENT_PROPERTIES = [
|
|
396
|
+
"order",
|
|
397
|
+
"flexGrow",
|
|
398
|
+
"flexShrink",
|
|
399
|
+
"flexBasis",
|
|
400
|
+
"alignSelf",
|
|
401
|
+
"justifySelf",
|
|
402
|
+
"gridColumn",
|
|
403
|
+
"gridRow",
|
|
404
|
+
"gridArea"
|
|
405
|
+
];
|
|
406
|
+
function isInlineDisplay(computed) {
|
|
407
|
+
return computed.display.startsWith("inline");
|
|
408
|
+
}
|
|
135
409
|
function insertOverlaySvg(svg, element) {
|
|
136
410
|
const parent = element.parentNode;
|
|
137
411
|
if (!parent) {
|
|
138
412
|
document.body.appendChild(svg);
|
|
139
|
-
return;
|
|
413
|
+
return null;
|
|
414
|
+
}
|
|
415
|
+
const wrapper = document.createElement("div");
|
|
416
|
+
wrapper.style.position = "relative";
|
|
417
|
+
const computed = getComputedStyle(element);
|
|
418
|
+
wrapper.style.display = isInlineDisplay(computed) ? "inline-block" : "block";
|
|
419
|
+
for (const property of ITEM_PLACEMENT_PROPERTIES) {
|
|
420
|
+
const value = computed[property];
|
|
421
|
+
if (value) wrapper.style[property] = value;
|
|
422
|
+
}
|
|
423
|
+
parent.replaceChild(wrapper, element);
|
|
424
|
+
wrapper.appendChild(element);
|
|
425
|
+
wrapper.appendChild(svg);
|
|
426
|
+
return wrapper;
|
|
427
|
+
}
|
|
428
|
+
function removeOverlaySvg(svg, wrapper, element) {
|
|
429
|
+
if (svg.parentNode) {
|
|
430
|
+
svg.parentNode.removeChild(svg);
|
|
140
431
|
}
|
|
141
|
-
|
|
432
|
+
if (!wrapper || !wrapper.parentNode) return;
|
|
433
|
+
wrapper.parentNode.replaceChild(element, wrapper);
|
|
142
434
|
}
|
|
143
435
|
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
144
436
|
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
@@ -295,6 +587,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
295
587
|
|
|
296
588
|
// src/renderer.js
|
|
297
589
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
590
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
298
591
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
299
592
|
var NOTE_FONT_SIZE = 19;
|
|
300
593
|
function clearSvg(svg) {
|
|
@@ -540,6 +833,15 @@ function appendPath(svg, d, style) {
|
|
|
540
833
|
svg.appendChild(path);
|
|
541
834
|
return path;
|
|
542
835
|
}
|
|
836
|
+
function appendGlow(svg, radius, style) {
|
|
837
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
838
|
+
circle.setAttribute("r", fmt(radius));
|
|
839
|
+
circle.setAttribute("fill", style.color);
|
|
840
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
841
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
842
|
+
svg.appendChild(circle);
|
|
843
|
+
return circle;
|
|
844
|
+
}
|
|
543
845
|
function rectPerimeter(width, height, radius) {
|
|
544
846
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
545
847
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -626,20 +928,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
626
928
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
627
929
|
};
|
|
628
930
|
}
|
|
629
|
-
|
|
931
|
+
var STRADDLE = 0.75;
|
|
932
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
630
933
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
631
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
632
934
|
const knot = 0.46 + random() * 0.12;
|
|
633
935
|
const points = [];
|
|
634
936
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
635
937
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
636
938
|
const pivot = at(knot);
|
|
637
|
-
const
|
|
939
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
940
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
941
|
+
const heading = normalize(derivative);
|
|
638
942
|
const normal = { x: -heading.y, y: heading.x };
|
|
639
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
640
943
|
const centre = {
|
|
641
|
-
x: pivot.x + normal.x * radius * side,
|
|
642
|
-
y: pivot.y + normal.y * radius * side
|
|
944
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
945
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
643
946
|
};
|
|
644
947
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
645
948
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -647,15 +950,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
647
950
|
for (let i = 1; i <= steps; i++) {
|
|
648
951
|
const progress = i / steps;
|
|
649
952
|
const angle = from + turns * progress * side;
|
|
953
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
650
954
|
const drift = radius * 0.55 * progress;
|
|
651
955
|
points.push({
|
|
652
|
-
x: centre.x + Math.cos(angle) *
|
|
653
|
-
y: centre.y + Math.sin(angle) *
|
|
956
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
957
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
654
958
|
});
|
|
655
959
|
}
|
|
960
|
+
const exit = points[points.length - 1];
|
|
961
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
962
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
963
|
+
const rejoin = at(resume);
|
|
964
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
965
|
+
const settle = 6;
|
|
656
966
|
const tail = 14;
|
|
657
967
|
for (let i = 1; i <= tail; i++) {
|
|
658
|
-
|
|
968
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
969
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
970
|
+
points.push({
|
|
971
|
+
x: point.x + normal.x * residual * fade,
|
|
972
|
+
y: point.y + normal.y * residual * fade
|
|
973
|
+
});
|
|
659
974
|
}
|
|
660
975
|
return points;
|
|
661
976
|
}
|
|
@@ -729,21 +1044,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
729
1044
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
730
1045
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
731
1046
|
const spread = 0.46 + random() * 0.1;
|
|
1047
|
+
const arms = [];
|
|
732
1048
|
for (const side of [-1, 1]) {
|
|
733
1049
|
const armAngle = angle + spread * side;
|
|
734
1050
|
const tail = {
|
|
735
1051
|
x: tip.x - length * Math.cos(armAngle),
|
|
736
1052
|
y: tip.y - length * Math.sin(armAngle)
|
|
737
1053
|
};
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
1054
|
+
arms.push(
|
|
1055
|
+
appendPath(
|
|
1056
|
+
svg,
|
|
1057
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1058
|
+
steps: 2,
|
|
1059
|
+
bow: 0.35 * side
|
|
1060
|
+
}),
|
|
1061
|
+
{ ...style, dashed: false }
|
|
1062
|
+
)
|
|
745
1063
|
);
|
|
746
1064
|
}
|
|
1065
|
+
return arms;
|
|
1066
|
+
}
|
|
1067
|
+
function bowOverTheTop(perpendicular) {
|
|
1068
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1069
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
747
1070
|
}
|
|
748
1071
|
function boundsOf(points) {
|
|
749
1072
|
const xs = points.map((p) => p.x);
|
|
@@ -755,7 +1078,7 @@ function boundsOf(points) {
|
|
|
755
1078
|
height: Math.max(...ys) - Math.min(...ys)
|
|
756
1079
|
};
|
|
757
1080
|
}
|
|
758
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1081
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
759
1082
|
const random = createRandom(seed + 17);
|
|
760
1083
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
761
1084
|
const dx = end.x - start.x;
|
|
@@ -764,6 +1087,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
764
1087
|
const dashed = arrowOptions.style === "dotted";
|
|
765
1088
|
let tipAngle = Math.atan2(dy, dx);
|
|
766
1089
|
let shaft;
|
|
1090
|
+
let ghost = null;
|
|
767
1091
|
const hull = [start, end];
|
|
768
1092
|
if (arrowOptions.style === "straight") {
|
|
769
1093
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -777,7 +1101,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
777
1101
|
y: rect.y + rect.height / 2 - mid.y
|
|
778
1102
|
};
|
|
779
1103
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
780
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1104
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1105
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1106
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1107
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1108
|
+
bowOverTheTop(perpendicular)
|
|
1109
|
+
);
|
|
781
1110
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
782
1111
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
783
1112
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -792,34 +1121,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
792
1121
|
{ x: c2x, y: c2y },
|
|
793
1122
|
end,
|
|
794
1123
|
radius,
|
|
795
|
-
random
|
|
1124
|
+
random,
|
|
1125
|
+
-sign
|
|
796
1126
|
);
|
|
797
1127
|
shaft = catmullRomPath(points, false);
|
|
798
1128
|
hull.push(...points);
|
|
799
1129
|
const last = points[points.length - 1];
|
|
800
1130
|
const prior = points[points.length - 2] ?? start;
|
|
801
1131
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1132
|
+
} else {
|
|
1133
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1134
|
+
if (style.roughness >= 1.1) {
|
|
1135
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1136
|
+
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)}`;
|
|
1137
|
+
}
|
|
1138
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
805
1139
|
}
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
1140
|
+
}
|
|
1141
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1142
|
+
svg.appendChild(group);
|
|
1143
|
+
const shaftPaths = [];
|
|
1144
|
+
if (ghost) {
|
|
1145
|
+
shaftPaths.push(
|
|
1146
|
+
appendPath(group, ghost, {
|
|
811
1147
|
...style,
|
|
812
1148
|
opacity: style.opacity * 0.3,
|
|
813
1149
|
strokeWidth: style.strokeWidth * 0.7,
|
|
814
1150
|
dashed
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1151
|
+
})
|
|
1152
|
+
);
|
|
818
1153
|
}
|
|
819
|
-
appendPath(
|
|
820
|
-
drawArrowHead(
|
|
1154
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1155
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1156
|
+
animateArrow(
|
|
1157
|
+
group,
|
|
1158
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1159
|
+
arrowOptions.animate,
|
|
1160
|
+
context.elapsed
|
|
1161
|
+
);
|
|
821
1162
|
const bounds = boundsOf(hull);
|
|
822
|
-
return { start, end, bounds };
|
|
1163
|
+
return { start, end, bounds, group };
|
|
823
1164
|
}
|
|
824
1165
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
825
1166
|
if (width <= 4) return;
|
|
@@ -886,6 +1227,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
886
1227
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
887
1228
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
888
1229
|
const lineHeight = fontSize * 1.05;
|
|
1230
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
889
1231
|
const group = document.createElementNS(SVG_NS, "g");
|
|
890
1232
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
891
1233
|
textEl.setAttribute("x", "0");
|
|
@@ -898,6 +1240,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
898
1240
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
899
1241
|
textEl.setAttribute("text-anchor", "start");
|
|
900
1242
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1243
|
+
if (rtl) {
|
|
1244
|
+
textEl.setAttribute("direction", "rtl");
|
|
1245
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1246
|
+
}
|
|
1247
|
+
if (options.locale) {
|
|
1248
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1249
|
+
}
|
|
901
1250
|
const tspans = lines.map((line, index) => {
|
|
902
1251
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
903
1252
|
tspan.textContent = line;
|
|
@@ -926,15 +1275,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
926
1275
|
height,
|
|
927
1276
|
ascent,
|
|
928
1277
|
descent,
|
|
929
|
-
fontSize
|
|
1278
|
+
fontSize,
|
|
1279
|
+
rtl
|
|
930
1280
|
};
|
|
931
1281
|
}
|
|
932
1282
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
933
1283
|
const random = createRandom(seed + 55);
|
|
934
1284
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
935
|
-
note.
|
|
1285
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1286
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
936
1287
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
937
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1288
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
938
1289
|
const cx = box.x + box.width / 2;
|
|
939
1290
|
const cy = box.y + box.height / 2;
|
|
940
1291
|
note.group.setAttribute(
|
|
@@ -943,9 +1294,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
943
1294
|
);
|
|
944
1295
|
if (options.underline !== false) {
|
|
945
1296
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1297
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
946
1298
|
drawUnderline(
|
|
947
1299
|
note.group,
|
|
948
|
-
|
|
1300
|
+
underlineX,
|
|
949
1301
|
box.y + box.height + 2,
|
|
950
1302
|
lastWidth - 2,
|
|
951
1303
|
{
|
|
@@ -966,7 +1318,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
966
1318
|
svg.setAttribute("width", String(width));
|
|
967
1319
|
svg.setAttribute("height", String(height));
|
|
968
1320
|
svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
|
|
969
|
-
svg.style.position = "
|
|
1321
|
+
svg.style.position = "absolute";
|
|
970
1322
|
svg.style.left = `${left}px`;
|
|
971
1323
|
svg.style.top = `${top}px`;
|
|
972
1324
|
svg.style.width = `${width}px`;
|
|
@@ -1019,12 +1371,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1019
1371
|
return { ...box, x: box.x + dx };
|
|
1020
1372
|
}
|
|
1021
1373
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
1022
|
-
|
|
1374
|
+
const { text, locale } = resolveLocalizedText(
|
|
1375
|
+
noteOptions?.text,
|
|
1376
|
+
noteOptions?.locale
|
|
1377
|
+
);
|
|
1378
|
+
if (!text) return null;
|
|
1023
1379
|
const band = layout.band ?? null;
|
|
1024
1380
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1025
1381
|
const random = createRandom(seed + 9);
|
|
1026
|
-
const note = createNoteText(svg,
|
|
1027
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1382
|
+
const note = createNoteText(svg, text, style, {
|
|
1383
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1384
|
+
locale
|
|
1028
1385
|
});
|
|
1029
1386
|
const tilt = (random() - 0.45) * 7;
|
|
1030
1387
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1066,6 +1423,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1066
1423
|
strokeWidth: style.strokeWidth * scale
|
|
1067
1424
|
});
|
|
1068
1425
|
}
|
|
1426
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1427
|
+
const count = 6;
|
|
1428
|
+
const points = [];
|
|
1429
|
+
for (let i = 0; i < count; i++) {
|
|
1430
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1431
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1432
|
+
points.push({
|
|
1433
|
+
x: x + Math.cos(angle) * r,
|
|
1434
|
+
y: y + Math.sin(angle) * r
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1438
|
+
...style,
|
|
1439
|
+
fill: style.color,
|
|
1440
|
+
strokeWidth: 0
|
|
1441
|
+
});
|
|
1442
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1443
|
+
return blob;
|
|
1444
|
+
}
|
|
1069
1445
|
function drawHeart(parent, size, style, random) {
|
|
1070
1446
|
const s = size;
|
|
1071
1447
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1089,26 +1465,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1089
1465
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1090
1466
|
"Z"
|
|
1091
1467
|
].join(" ");
|
|
1092
|
-
appendPath(parent, path, style);
|
|
1468
|
+
return appendPath(parent, path, style);
|
|
1093
1469
|
}
|
|
1094
1470
|
function drawSparkle(parent, size, style, random) {
|
|
1095
1471
|
twinkle(
|
|
1096
1472
|
parent,
|
|
1097
|
-
size * (0.
|
|
1473
|
+
size * (0.62 + random() * 0.12),
|
|
1098
1474
|
size,
|
|
1099
|
-
size * (0.
|
|
1475
|
+
size * (0.14 + random() * 0.08),
|
|
1100
1476
|
style
|
|
1101
1477
|
);
|
|
1102
1478
|
}
|
|
1103
1479
|
function drawStar(parent, size, style, random) {
|
|
1104
1480
|
twinkle(
|
|
1105
1481
|
parent,
|
|
1106
|
-
size * (0.
|
|
1107
|
-
size,
|
|
1108
|
-
size * (0.
|
|
1482
|
+
size * (0.62 + random() * 0.12),
|
|
1483
|
+
size * (0.8 + random() * 0.12),
|
|
1484
|
+
size * (0.14 + random() * 0.08),
|
|
1109
1485
|
style
|
|
1110
1486
|
);
|
|
1111
1487
|
}
|
|
1488
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1489
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1490
|
+
color: style.color,
|
|
1491
|
+
opacity: style.opacity * 0.22
|
|
1492
|
+
});
|
|
1493
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1494
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1495
|
+
const bluntness = size * 0.19;
|
|
1496
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1497
|
+
...style,
|
|
1498
|
+
fill: style.color,
|
|
1499
|
+
strokeWidth: bluntness
|
|
1500
|
+
});
|
|
1501
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1502
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1503
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1504
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1505
|
+
const corners = [
|
|
1506
|
+
{ x: 0, y: -(armY + gap) },
|
|
1507
|
+
{ x: armX + gap, y: 0 },
|
|
1508
|
+
{ x: 0, y: armY + gap },
|
|
1509
|
+
{ x: -(armX + gap), y: 0 }
|
|
1510
|
+
];
|
|
1511
|
+
for (const { x, y } of corners) {
|
|
1512
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1513
|
+
drawFleck(
|
|
1514
|
+
parent,
|
|
1515
|
+
x * jitter,
|
|
1516
|
+
y * jitter,
|
|
1517
|
+
size * (0.1 + random() * 0.03),
|
|
1518
|
+
style,
|
|
1519
|
+
random
|
|
1520
|
+
);
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1112
1523
|
function drawTwinkle(parent, size, style, random) {
|
|
1113
1524
|
const count = 5;
|
|
1114
1525
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1216,46 +1627,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1216
1627
|
);
|
|
1217
1628
|
}
|
|
1218
1629
|
}
|
|
1219
|
-
function drawDots(parent, size, style, random) {
|
|
1220
|
-
let x = -size * 0.7;
|
|
1221
|
-
for (let i = 0; i < 3; i++) {
|
|
1222
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1223
|
-
x += size * (0.6 + random() * 0.25);
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
function drawStroke(parent, size, style, random) {
|
|
1227
|
-
appendPath(
|
|
1228
|
-
parent,
|
|
1229
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1230
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1231
|
-
steps: 4
|
|
1232
|
-
}),
|
|
1233
|
-
style
|
|
1234
|
-
);
|
|
1235
|
-
}
|
|
1236
|
-
function drawSteam(parent, size, style, random) {
|
|
1237
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1238
|
-
for (let i = 0; i < 2; i++) {
|
|
1239
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1240
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1241
|
-
appendPath(
|
|
1242
|
-
parent,
|
|
1243
|
-
`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)}`,
|
|
1244
|
-
style
|
|
1245
|
-
);
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1248
1630
|
var DECORATION_DRAWERS = {
|
|
1249
1631
|
heart: drawHeart,
|
|
1250
1632
|
sparkle: drawSparkle,
|
|
1251
1633
|
star: drawStar,
|
|
1252
1634
|
twinkle: drawTwinkle,
|
|
1635
|
+
glimmer: drawGlimmer,
|
|
1253
1636
|
smiley: drawSmiley,
|
|
1254
1637
|
emphasis: drawEmphasis,
|
|
1255
|
-
arcs: drawArcs
|
|
1256
|
-
dots: drawDots,
|
|
1257
|
-
stroke: drawStroke,
|
|
1258
|
-
steam: drawSteam
|
|
1638
|
+
arcs: drawArcs
|
|
1259
1639
|
};
|
|
1260
1640
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1261
1641
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1266,12 +1646,10 @@ var TYPE_TRAITS = {
|
|
|
1266
1646
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1267
1647
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1268
1648
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1649
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1269
1650
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1270
1651
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1271
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1272
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1273
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1274
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1652
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1275
1653
|
};
|
|
1276
1654
|
var DEFAULT_TYPES = [
|
|
1277
1655
|
"twinkle",
|
|
@@ -1474,6 +1852,9 @@ function ensureHandwrittenFont() {
|
|
|
1474
1852
|
|
|
1475
1853
|
// src/doodle.js
|
|
1476
1854
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1855
|
+
function now() {
|
|
1856
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1857
|
+
}
|
|
1477
1858
|
function localBand(overlayRect, inset = 8) {
|
|
1478
1859
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1479
1860
|
return {
|
|
@@ -1490,19 +1871,21 @@ var DoodleOverlay = class {
|
|
|
1490
1871
|
this.element = element;
|
|
1491
1872
|
this.options = options;
|
|
1492
1873
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1874
|
+
this.startedAt = now();
|
|
1493
1875
|
this.svg = null;
|
|
1876
|
+
this.wrapper = null;
|
|
1494
1877
|
this.resizeObserver = null;
|
|
1495
1878
|
this.mutationObserver = null;
|
|
1496
1879
|
this.pendingFrame = null;
|
|
1497
|
-
this.
|
|
1498
|
-
this.onScroll = this.scheduleUpdate.bind(this);
|
|
1880
|
+
this.listeners = [];
|
|
1499
1881
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1882
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1500
1883
|
this.childElements = [];
|
|
1501
1884
|
this.mount();
|
|
1502
1885
|
}
|
|
1503
1886
|
mount() {
|
|
1504
1887
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1505
|
-
insertOverlaySvg(this.svg, this.element);
|
|
1888
|
+
this.wrapper = insertOverlaySvg(this.svg, this.element);
|
|
1506
1889
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1507
1890
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1508
1891
|
this.resizeObserver.observe(this.element);
|
|
@@ -1515,12 +1898,18 @@ var DoodleOverlay = class {
|
|
|
1515
1898
|
subtree: true,
|
|
1516
1899
|
attributes: true
|
|
1517
1900
|
});
|
|
1518
|
-
for (const target of getScrollableAncestors(this.element)) {
|
|
1519
|
-
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1520
|
-
this.scrollTargets.push({ target, type: "scroll" });
|
|
1521
|
-
}
|
|
1522
1901
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1523
|
-
this.
|
|
1902
|
+
this.listeners.push({
|
|
1903
|
+
target: window,
|
|
1904
|
+
type: "resize",
|
|
1905
|
+
handler: this.onResize
|
|
1906
|
+
});
|
|
1907
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1908
|
+
this.listeners.push({
|
|
1909
|
+
target: window,
|
|
1910
|
+
type: "languagechange",
|
|
1911
|
+
handler: this.onLanguageChange
|
|
1912
|
+
});
|
|
1524
1913
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1525
1914
|
ensureHandwrittenFont();
|
|
1526
1915
|
}
|
|
@@ -1556,17 +1945,19 @@ var DoodleOverlay = class {
|
|
|
1556
1945
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1557
1946
|
const targets = this.getTargets();
|
|
1558
1947
|
const margin = this.overlayMargin();
|
|
1948
|
+
const elapsed = now() - this.startedAt;
|
|
1559
1949
|
const absoluteRects = targets.map(
|
|
1560
1950
|
(target) => getElementBounds(target, padding, radius)
|
|
1561
1951
|
);
|
|
1562
1952
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1563
1953
|
clearSvg(this.svg);
|
|
1564
1954
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1955
|
+
const origin = this.wrapper ? this.wrapper.getBoundingClientRect() : { left: 0, top: 0 };
|
|
1565
1956
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1566
1957
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1567
1958
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
1568
|
-
this.svg.style.left = `${overlayRect.x}px`;
|
|
1569
|
-
this.svg.style.top = `${overlayRect.y}px`;
|
|
1959
|
+
this.svg.style.left = `${overlayRect.x - origin.left}px`;
|
|
1960
|
+
this.svg.style.top = `${overlayRect.y - origin.top}px`;
|
|
1570
1961
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1571
1962
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
1572
1963
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
@@ -1608,7 +1999,8 @@ var DoodleOverlay = class {
|
|
|
1608
1999
|
this.options.arrow,
|
|
1609
2000
|
strokeStyle,
|
|
1610
2001
|
seed,
|
|
1611
|
-
note ? note.box : null
|
|
2002
|
+
note ? note.box : null,
|
|
2003
|
+
{ elapsed }
|
|
1612
2004
|
) : null;
|
|
1613
2005
|
if (this.options.decorations) {
|
|
1614
2006
|
renderDecorations(
|
|
@@ -1635,7 +2027,7 @@ var DoodleOverlay = class {
|
|
|
1635
2027
|
overlayMargin() {
|
|
1636
2028
|
const { note, arrow, decorations } = this.options;
|
|
1637
2029
|
if (!note && !arrow && !decorations) return 48;
|
|
1638
|
-
const text =
|
|
2030
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1639
2031
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1640
2032
|
}
|
|
1641
2033
|
destroy() {
|
|
@@ -1655,14 +2047,15 @@ var DoodleOverlay = class {
|
|
|
1655
2047
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1656
2048
|
this.onFontsLoaded = null;
|
|
1657
2049
|
}
|
|
1658
|
-
for (const { target, type } of this.
|
|
1659
|
-
target.removeEventListener(type,
|
|
2050
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2051
|
+
target.removeEventListener(type, handler);
|
|
1660
2052
|
}
|
|
1661
|
-
this.
|
|
1662
|
-
if (this.svg
|
|
1663
|
-
this.svg.
|
|
2053
|
+
this.listeners = [];
|
|
2054
|
+
if (this.svg) {
|
|
2055
|
+
removeOverlaySvg(this.svg, this.wrapper, this.element);
|
|
1664
2056
|
}
|
|
1665
2057
|
this.svg = null;
|
|
2058
|
+
this.wrapper = null;
|
|
1666
2059
|
instances.delete(this.element);
|
|
1667
2060
|
}
|
|
1668
2061
|
};
|