@aznabee/freehand-ui 0.1.4 → 0.1.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -9
- package/dist/freehand-ui.cjs +564 -98
- package/dist/freehand-ui.js +564 -98
- package/dist/index.d.ts +61 -6
- package/dist/react.cjs +569 -99
- package/dist/react.d.ts +2 -0
- package/dist/react.js +569 -99
- package/package.json +1 -1
package/dist/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,
|
|
@@ -41,7 +300,17 @@ var DEFAULT_OPTIONS = {
|
|
|
41
300
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
42
301
|
fontFamily: null,
|
|
43
302
|
/** Fetch Caveat when the page has not provided it. */
|
|
44
|
-
autoLoadFont: true
|
|
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,
|
|
309
|
+
/**
|
|
310
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
311
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
312
|
+
*/
|
|
313
|
+
zIndex: null
|
|
45
314
|
};
|
|
46
315
|
function resolveElement(target) {
|
|
47
316
|
if (!target) return null;
|
|
@@ -55,8 +324,15 @@ function resolveElement(target) {
|
|
|
55
324
|
}
|
|
56
325
|
function mergeOptions(options = {}) {
|
|
57
326
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
58
|
-
if (merged.note
|
|
59
|
-
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
|
+
};
|
|
60
336
|
}
|
|
61
337
|
if (merged.arrow) {
|
|
62
338
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -66,7 +342,10 @@ function mergeOptions(options = {}) {
|
|
|
66
342
|
// explicit `from` is honoured whether or not there is a note.
|
|
67
343
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
68
344
|
to: config.to ?? "edge",
|
|
69
|
-
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)
|
|
70
349
|
};
|
|
71
350
|
}
|
|
72
351
|
if (merged.decorations === true) {
|
|
@@ -134,6 +413,40 @@ function getScrollableAncestors(element) {
|
|
|
134
413
|
ancestors.push(window);
|
|
135
414
|
return ancestors;
|
|
136
415
|
}
|
|
416
|
+
function resolveOverlayZIndex(element, override) {
|
|
417
|
+
if (override != null) return override;
|
|
418
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
419
|
+
if (zIndex !== "auto") {
|
|
420
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
421
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
422
|
+
}
|
|
423
|
+
let node = element.parentElement;
|
|
424
|
+
while (node && node !== document.documentElement) {
|
|
425
|
+
const style = getComputedStyle(node);
|
|
426
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
427
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
428
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
429
|
+
}
|
|
430
|
+
node = node.parentElement;
|
|
431
|
+
}
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
function insertOverlaySvg(svg, element) {
|
|
435
|
+
const parent = element.parentNode;
|
|
436
|
+
if (!parent) {
|
|
437
|
+
document.body.appendChild(svg);
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
441
|
+
}
|
|
442
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
443
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
444
|
+
if (zIndex == null) {
|
|
445
|
+
svg.style.zIndex = "";
|
|
446
|
+
} else {
|
|
447
|
+
svg.style.zIndex = String(zIndex);
|
|
448
|
+
}
|
|
449
|
+
}
|
|
137
450
|
|
|
138
451
|
// src/geometry.js
|
|
139
452
|
function averageCornerRadius(element) {
|
|
@@ -281,6 +594,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
281
594
|
|
|
282
595
|
// src/renderer.js
|
|
283
596
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
597
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
284
598
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
285
599
|
var NOTE_FONT_SIZE = 19;
|
|
286
600
|
function clearSvg(svg) {
|
|
@@ -526,6 +840,15 @@ function appendPath(svg, d, style) {
|
|
|
526
840
|
svg.appendChild(path);
|
|
527
841
|
return path;
|
|
528
842
|
}
|
|
843
|
+
function appendGlow(svg, radius, style) {
|
|
844
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
845
|
+
circle.setAttribute("r", fmt(radius));
|
|
846
|
+
circle.setAttribute("fill", style.color);
|
|
847
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
848
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
849
|
+
svg.appendChild(circle);
|
|
850
|
+
return circle;
|
|
851
|
+
}
|
|
529
852
|
function rectPerimeter(width, height, radius) {
|
|
530
853
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
531
854
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -612,20 +935,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
612
935
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
613
936
|
};
|
|
614
937
|
}
|
|
615
|
-
|
|
938
|
+
var STRADDLE = 0.75;
|
|
939
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
616
940
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
617
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
618
941
|
const knot = 0.46 + random() * 0.12;
|
|
619
942
|
const points = [];
|
|
620
943
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
621
944
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
622
945
|
const pivot = at(knot);
|
|
623
|
-
const
|
|
946
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
947
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
948
|
+
const heading = normalize(derivative);
|
|
624
949
|
const normal = { x: -heading.y, y: heading.x };
|
|
625
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
626
950
|
const centre = {
|
|
627
|
-
x: pivot.x + normal.x * radius * side,
|
|
628
|
-
y: pivot.y + normal.y * radius * side
|
|
951
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
952
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
629
953
|
};
|
|
630
954
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
631
955
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -633,15 +957,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
633
957
|
for (let i = 1; i <= steps; i++) {
|
|
634
958
|
const progress = i / steps;
|
|
635
959
|
const angle = from + turns * progress * side;
|
|
960
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
636
961
|
const drift = radius * 0.55 * progress;
|
|
637
962
|
points.push({
|
|
638
|
-
x: centre.x + Math.cos(angle) *
|
|
639
|
-
y: centre.y + Math.sin(angle) *
|
|
963
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
964
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
640
965
|
});
|
|
641
966
|
}
|
|
967
|
+
const exit = points[points.length - 1];
|
|
968
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
969
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
970
|
+
const rejoin = at(resume);
|
|
971
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
972
|
+
const settle = 6;
|
|
642
973
|
const tail = 14;
|
|
643
974
|
for (let i = 1; i <= tail; i++) {
|
|
644
|
-
|
|
975
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
976
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
977
|
+
points.push({
|
|
978
|
+
x: point.x + normal.x * residual * fade,
|
|
979
|
+
y: point.y + normal.y * residual * fade
|
|
980
|
+
});
|
|
645
981
|
}
|
|
646
982
|
return points;
|
|
647
983
|
}
|
|
@@ -715,21 +1051,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
715
1051
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
716
1052
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
717
1053
|
const spread = 0.46 + random() * 0.1;
|
|
1054
|
+
const arms = [];
|
|
718
1055
|
for (const side of [-1, 1]) {
|
|
719
1056
|
const armAngle = angle + spread * side;
|
|
720
1057
|
const tail = {
|
|
721
1058
|
x: tip.x - length * Math.cos(armAngle),
|
|
722
1059
|
y: tip.y - length * Math.sin(armAngle)
|
|
723
1060
|
};
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
1061
|
+
arms.push(
|
|
1062
|
+
appendPath(
|
|
1063
|
+
svg,
|
|
1064
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1065
|
+
steps: 2,
|
|
1066
|
+
bow: 0.35 * side
|
|
1067
|
+
}),
|
|
1068
|
+
{ ...style, dashed: false }
|
|
1069
|
+
)
|
|
731
1070
|
);
|
|
732
1071
|
}
|
|
1072
|
+
return arms;
|
|
1073
|
+
}
|
|
1074
|
+
function bowOverTheTop(perpendicular) {
|
|
1075
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1076
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
733
1077
|
}
|
|
734
1078
|
function boundsOf(points) {
|
|
735
1079
|
const xs = points.map((p) => p.x);
|
|
@@ -741,7 +1085,7 @@ function boundsOf(points) {
|
|
|
741
1085
|
height: Math.max(...ys) - Math.min(...ys)
|
|
742
1086
|
};
|
|
743
1087
|
}
|
|
744
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1088
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
745
1089
|
const random = createRandom(seed + 17);
|
|
746
1090
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
747
1091
|
const dx = end.x - start.x;
|
|
@@ -750,6 +1094,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
750
1094
|
const dashed = arrowOptions.style === "dotted";
|
|
751
1095
|
let tipAngle = Math.atan2(dy, dx);
|
|
752
1096
|
let shaft;
|
|
1097
|
+
let ghost = null;
|
|
753
1098
|
const hull = [start, end];
|
|
754
1099
|
if (arrowOptions.style === "straight") {
|
|
755
1100
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -763,7 +1108,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
763
1108
|
y: rect.y + rect.height / 2 - mid.y
|
|
764
1109
|
};
|
|
765
1110
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
766
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1111
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1112
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1113
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1114
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1115
|
+
bowOverTheTop(perpendicular)
|
|
1116
|
+
);
|
|
767
1117
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
768
1118
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
769
1119
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -778,34 +1128,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
778
1128
|
{ x: c2x, y: c2y },
|
|
779
1129
|
end,
|
|
780
1130
|
radius,
|
|
781
|
-
random
|
|
1131
|
+
random,
|
|
1132
|
+
-sign
|
|
782
1133
|
);
|
|
783
1134
|
shaft = catmullRomPath(points, false);
|
|
784
1135
|
hull.push(...points);
|
|
785
1136
|
const last = points[points.length - 1];
|
|
786
1137
|
const prior = points[points.length - 2] ?? start;
|
|
787
1138
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
1139
|
+
} else {
|
|
1140
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1141
|
+
if (style.roughness >= 1.1) {
|
|
1142
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1143
|
+
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)}`;
|
|
1144
|
+
}
|
|
1145
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
791
1146
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
1147
|
+
}
|
|
1148
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1149
|
+
svg.appendChild(group);
|
|
1150
|
+
const shaftPaths = [];
|
|
1151
|
+
if (ghost) {
|
|
1152
|
+
shaftPaths.push(
|
|
1153
|
+
appendPath(group, ghost, {
|
|
797
1154
|
...style,
|
|
798
1155
|
opacity: style.opacity * 0.3,
|
|
799
1156
|
strokeWidth: style.strokeWidth * 0.7,
|
|
800
1157
|
dashed
|
|
801
|
-
})
|
|
802
|
-
|
|
803
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1158
|
+
})
|
|
1159
|
+
);
|
|
804
1160
|
}
|
|
805
|
-
appendPath(
|
|
806
|
-
drawArrowHead(
|
|
1161
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1162
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1163
|
+
animateArrow(
|
|
1164
|
+
group,
|
|
1165
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1166
|
+
arrowOptions.animate,
|
|
1167
|
+
context.elapsed
|
|
1168
|
+
);
|
|
807
1169
|
const bounds = boundsOf(hull);
|
|
808
|
-
return { start, end, bounds };
|
|
1170
|
+
return { start, end, bounds, group };
|
|
809
1171
|
}
|
|
810
1172
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
811
1173
|
if (width <= 4) return;
|
|
@@ -872,6 +1234,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
872
1234
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
873
1235
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
874
1236
|
const lineHeight = fontSize * 1.05;
|
|
1237
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
875
1238
|
const group = document.createElementNS(SVG_NS, "g");
|
|
876
1239
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
877
1240
|
textEl.setAttribute("x", "0");
|
|
@@ -884,6 +1247,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
884
1247
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
885
1248
|
textEl.setAttribute("text-anchor", "start");
|
|
886
1249
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1250
|
+
if (rtl) {
|
|
1251
|
+
textEl.setAttribute("direction", "rtl");
|
|
1252
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1253
|
+
}
|
|
1254
|
+
if (options.locale) {
|
|
1255
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1256
|
+
}
|
|
887
1257
|
const tspans = lines.map((line, index) => {
|
|
888
1258
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
889
1259
|
tspan.textContent = line;
|
|
@@ -912,15 +1282,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
912
1282
|
height,
|
|
913
1283
|
ascent,
|
|
914
1284
|
descent,
|
|
915
|
-
fontSize
|
|
1285
|
+
fontSize,
|
|
1286
|
+
rtl
|
|
916
1287
|
};
|
|
917
1288
|
}
|
|
918
1289
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
919
1290
|
const random = createRandom(seed + 55);
|
|
920
1291
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
921
|
-
note.
|
|
1292
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1293
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
922
1294
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
923
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1295
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
924
1296
|
const cx = box.x + box.width / 2;
|
|
925
1297
|
const cy = box.y + box.height / 2;
|
|
926
1298
|
note.group.setAttribute(
|
|
@@ -929,9 +1301,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
929
1301
|
);
|
|
930
1302
|
if (options.underline !== false) {
|
|
931
1303
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1304
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
932
1305
|
drawUnderline(
|
|
933
1306
|
note.group,
|
|
934
|
-
|
|
1307
|
+
underlineX,
|
|
935
1308
|
box.y + box.height + 2,
|
|
936
1309
|
lastWidth - 2,
|
|
937
1310
|
{
|
|
@@ -959,7 +1332,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
959
1332
|
svg.style.height = `${height}px`;
|
|
960
1333
|
svg.style.pointerEvents = "none";
|
|
961
1334
|
svg.style.overflow = "visible";
|
|
962
|
-
svg.style.
|
|
1335
|
+
svg.style.willChange = "transform";
|
|
963
1336
|
return svg;
|
|
964
1337
|
}
|
|
965
1338
|
|
|
@@ -1006,12 +1379,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1006
1379
|
return { ...box, x: box.x + dx };
|
|
1007
1380
|
}
|
|
1008
1381
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
1009
|
-
|
|
1382
|
+
const { text, locale } = resolveLocalizedText(
|
|
1383
|
+
noteOptions?.text,
|
|
1384
|
+
noteOptions?.locale
|
|
1385
|
+
);
|
|
1386
|
+
if (!text) return null;
|
|
1010
1387
|
const band = layout.band ?? null;
|
|
1011
1388
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
1012
1389
|
const random = createRandom(seed + 9);
|
|
1013
|
-
const note = createNoteText(svg,
|
|
1014
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1390
|
+
const note = createNoteText(svg, text, style, {
|
|
1391
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1392
|
+
locale
|
|
1015
1393
|
});
|
|
1016
1394
|
const tilt = (random() - 0.45) * 7;
|
|
1017
1395
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1053,6 +1431,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1053
1431
|
strokeWidth: style.strokeWidth * scale
|
|
1054
1432
|
});
|
|
1055
1433
|
}
|
|
1434
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1435
|
+
const count = 6;
|
|
1436
|
+
const points = [];
|
|
1437
|
+
for (let i = 0; i < count; i++) {
|
|
1438
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1439
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1440
|
+
points.push({
|
|
1441
|
+
x: x + Math.cos(angle) * r,
|
|
1442
|
+
y: y + Math.sin(angle) * r
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1446
|
+
...style,
|
|
1447
|
+
fill: style.color,
|
|
1448
|
+
strokeWidth: 0
|
|
1449
|
+
});
|
|
1450
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1451
|
+
return blob;
|
|
1452
|
+
}
|
|
1056
1453
|
function drawHeart(parent, size, style, random) {
|
|
1057
1454
|
const s = size;
|
|
1058
1455
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1076,26 +1473,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1076
1473
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1077
1474
|
"Z"
|
|
1078
1475
|
].join(" ");
|
|
1079
|
-
appendPath(parent, path, style);
|
|
1476
|
+
return appendPath(parent, path, style);
|
|
1080
1477
|
}
|
|
1081
1478
|
function drawSparkle(parent, size, style, random) {
|
|
1082
1479
|
twinkle(
|
|
1083
1480
|
parent,
|
|
1084
|
-
size * (0.
|
|
1481
|
+
size * (0.62 + random() * 0.12),
|
|
1085
1482
|
size,
|
|
1086
|
-
size * (0.
|
|
1483
|
+
size * (0.14 + random() * 0.08),
|
|
1087
1484
|
style
|
|
1088
1485
|
);
|
|
1089
1486
|
}
|
|
1090
1487
|
function drawStar(parent, size, style, random) {
|
|
1091
1488
|
twinkle(
|
|
1092
1489
|
parent,
|
|
1093
|
-
size * (0.
|
|
1094
|
-
size,
|
|
1095
|
-
size * (0.
|
|
1490
|
+
size * (0.62 + random() * 0.12),
|
|
1491
|
+
size * (0.8 + random() * 0.12),
|
|
1492
|
+
size * (0.14 + random() * 0.08),
|
|
1096
1493
|
style
|
|
1097
1494
|
);
|
|
1098
1495
|
}
|
|
1496
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1497
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1498
|
+
color: style.color,
|
|
1499
|
+
opacity: style.opacity * 0.22
|
|
1500
|
+
});
|
|
1501
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1502
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1503
|
+
const bluntness = size * 0.19;
|
|
1504
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1505
|
+
...style,
|
|
1506
|
+
fill: style.color,
|
|
1507
|
+
strokeWidth: bluntness
|
|
1508
|
+
});
|
|
1509
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1510
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1511
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1512
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1513
|
+
const corners = [
|
|
1514
|
+
{ x: 0, y: -(armY + gap) },
|
|
1515
|
+
{ x: armX + gap, y: 0 },
|
|
1516
|
+
{ x: 0, y: armY + gap },
|
|
1517
|
+
{ x: -(armX + gap), y: 0 }
|
|
1518
|
+
];
|
|
1519
|
+
for (const { x, y } of corners) {
|
|
1520
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1521
|
+
drawFleck(
|
|
1522
|
+
parent,
|
|
1523
|
+
x * jitter,
|
|
1524
|
+
y * jitter,
|
|
1525
|
+
size * (0.1 + random() * 0.03),
|
|
1526
|
+
style,
|
|
1527
|
+
random
|
|
1528
|
+
);
|
|
1529
|
+
}
|
|
1530
|
+
}
|
|
1099
1531
|
function drawTwinkle(parent, size, style, random) {
|
|
1100
1532
|
const count = 5;
|
|
1101
1533
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1203,46 +1635,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1203
1635
|
);
|
|
1204
1636
|
}
|
|
1205
1637
|
}
|
|
1206
|
-
function drawDots(parent, size, style, random) {
|
|
1207
|
-
let x = -size * 0.7;
|
|
1208
|
-
for (let i = 0; i < 3; i++) {
|
|
1209
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1210
|
-
x += size * (0.6 + random() * 0.25);
|
|
1211
|
-
}
|
|
1212
|
-
}
|
|
1213
|
-
function drawStroke(parent, size, style, random) {
|
|
1214
|
-
appendPath(
|
|
1215
|
-
parent,
|
|
1216
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1217
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1218
|
-
steps: 4
|
|
1219
|
-
}),
|
|
1220
|
-
style
|
|
1221
|
-
);
|
|
1222
|
-
}
|
|
1223
|
-
function drawSteam(parent, size, style, random) {
|
|
1224
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1225
|
-
for (let i = 0; i < 2; i++) {
|
|
1226
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1227
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1228
|
-
appendPath(
|
|
1229
|
-
parent,
|
|
1230
|
-
`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)}`,
|
|
1231
|
-
style
|
|
1232
|
-
);
|
|
1233
|
-
}
|
|
1234
|
-
}
|
|
1235
1638
|
var DECORATION_DRAWERS = {
|
|
1236
1639
|
heart: drawHeart,
|
|
1237
1640
|
sparkle: drawSparkle,
|
|
1238
1641
|
star: drawStar,
|
|
1239
1642
|
twinkle: drawTwinkle,
|
|
1643
|
+
glimmer: drawGlimmer,
|
|
1240
1644
|
smiley: drawSmiley,
|
|
1241
1645
|
emphasis: drawEmphasis,
|
|
1242
|
-
arcs: drawArcs
|
|
1243
|
-
dots: drawDots,
|
|
1244
|
-
stroke: drawStroke,
|
|
1245
|
-
steam: drawSteam
|
|
1646
|
+
arcs: drawArcs
|
|
1246
1647
|
};
|
|
1247
1648
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1248
1649
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1253,12 +1654,10 @@ var TYPE_TRAITS = {
|
|
|
1253
1654
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1254
1655
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1255
1656
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1657
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1256
1658
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1257
1659
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1258
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1259
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1260
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1261
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1660
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1262
1661
|
};
|
|
1263
1662
|
var DEFAULT_TYPES = [
|
|
1264
1663
|
"twinkle",
|
|
@@ -1461,6 +1860,9 @@ function ensureHandwrittenFont() {
|
|
|
1461
1860
|
|
|
1462
1861
|
// src/doodle.js
|
|
1463
1862
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1863
|
+
function now() {
|
|
1864
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1865
|
+
}
|
|
1464
1866
|
function localBand(overlayRect, inset = 8) {
|
|
1465
1867
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1466
1868
|
return {
|
|
@@ -1477,19 +1879,24 @@ var DoodleOverlay = class {
|
|
|
1477
1879
|
this.element = element;
|
|
1478
1880
|
this.options = options;
|
|
1479
1881
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1882
|
+
this.startedAt = now();
|
|
1480
1883
|
this.svg = null;
|
|
1481
1884
|
this.resizeObserver = null;
|
|
1482
1885
|
this.mutationObserver = null;
|
|
1483
1886
|
this.pendingFrame = null;
|
|
1484
|
-
this.
|
|
1485
|
-
this.
|
|
1887
|
+
this.pendingRepositionFrame = null;
|
|
1888
|
+
this.basePosition = { x: 0, y: 0 };
|
|
1889
|
+
this.listeners = [];
|
|
1890
|
+
this.onScroll = this.scheduleReposition.bind(this);
|
|
1486
1891
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1892
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1487
1893
|
this.childElements = [];
|
|
1488
1894
|
this.mount();
|
|
1489
1895
|
}
|
|
1490
1896
|
mount() {
|
|
1491
1897
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1492
|
-
|
|
1898
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1899
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1493
1900
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1494
1901
|
this.resizeObserver.observe(this.element);
|
|
1495
1902
|
if (this.options.children) {
|
|
@@ -1503,10 +1910,20 @@ var DoodleOverlay = class {
|
|
|
1503
1910
|
});
|
|
1504
1911
|
for (const target of getScrollableAncestors(this.element)) {
|
|
1505
1912
|
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1506
|
-
this.
|
|
1913
|
+
this.listeners.push({ target, type: "scroll", handler: this.onScroll });
|
|
1507
1914
|
}
|
|
1508
1915
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1509
|
-
this.
|
|
1916
|
+
this.listeners.push({
|
|
1917
|
+
target: window,
|
|
1918
|
+
type: "resize",
|
|
1919
|
+
handler: this.onResize
|
|
1920
|
+
});
|
|
1921
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1922
|
+
this.listeners.push({
|
|
1923
|
+
target: window,
|
|
1924
|
+
type: "languagechange",
|
|
1925
|
+
handler: this.onLanguageChange
|
|
1926
|
+
});
|
|
1510
1927
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1511
1928
|
ensureHandwrittenFont();
|
|
1512
1929
|
}
|
|
@@ -1530,6 +1947,46 @@ var DoodleOverlay = class {
|
|
|
1530
1947
|
this.update();
|
|
1531
1948
|
});
|
|
1532
1949
|
}
|
|
1950
|
+
scheduleReposition() {
|
|
1951
|
+
if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
|
|
1952
|
+
return;
|
|
1953
|
+
}
|
|
1954
|
+
this.pendingRepositionFrame = scheduleFrame(() => {
|
|
1955
|
+
this.pendingRepositionFrame = null;
|
|
1956
|
+
this.reposition();
|
|
1957
|
+
});
|
|
1958
|
+
}
|
|
1959
|
+
/**
|
|
1960
|
+
* Cheap scroll-driven path: slide the overlay to the element's new
|
|
1961
|
+
* viewport position without touching its contents. Scrolling can't change
|
|
1962
|
+
* an element's size, so the rects, viewBox and every hand-drawn stroke
|
|
1963
|
+
* inside them are still valid - only the overlay's position needs to move.
|
|
1964
|
+
*
|
|
1965
|
+
* That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
|
|
1966
|
+
* same box-geometry pass as layout, so the browser re-checks layout on
|
|
1967
|
+
* every write; `transform` is composited, so the scroll path never
|
|
1968
|
+
* touches layout at all.
|
|
1969
|
+
*/
|
|
1970
|
+
reposition() {
|
|
1971
|
+
if (!this.svg || !this.element.isConnected) return;
|
|
1972
|
+
const { padding, radius } = this.options;
|
|
1973
|
+
const targets = this.getTargets();
|
|
1974
|
+
const margin = this.overlayMargin();
|
|
1975
|
+
const absoluteRects = targets.map(
|
|
1976
|
+
(target) => getElementBounds(target, padding, radius)
|
|
1977
|
+
);
|
|
1978
|
+
const overlayRect = unionRects(absoluteRects, margin);
|
|
1979
|
+
const currentWidth = parseFloat(this.svg.style.width) || 0;
|
|
1980
|
+
const currentHeight = parseFloat(this.svg.style.height) || 0;
|
|
1981
|
+
const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
|
|
1982
|
+
if (resized) {
|
|
1983
|
+
this.update();
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const dx = overlayRect.x - this.basePosition.x;
|
|
1987
|
+
const dy = overlayRect.y - this.basePosition.y;
|
|
1988
|
+
this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
1989
|
+
}
|
|
1533
1990
|
getTargets() {
|
|
1534
1991
|
if (this.options.children) {
|
|
1535
1992
|
const nodes = this.element.querySelectorAll(this.options.children);
|
|
@@ -1542,11 +1999,13 @@ var DoodleOverlay = class {
|
|
|
1542
1999
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1543
2000
|
const targets = this.getTargets();
|
|
1544
2001
|
const margin = this.overlayMargin();
|
|
2002
|
+
const elapsed = now() - this.startedAt;
|
|
1545
2003
|
const absoluteRects = targets.map(
|
|
1546
2004
|
(target) => getElementBounds(target, padding, radius)
|
|
1547
2005
|
);
|
|
1548
2006
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1549
2007
|
clearSvg(this.svg);
|
|
2008
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1550
2009
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1551
2010
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1552
2011
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
@@ -1554,6 +2013,8 @@ var DoodleOverlay = class {
|
|
|
1554
2013
|
this.svg.style.top = `${overlayRect.y}px`;
|
|
1555
2014
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1556
2015
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
2016
|
+
this.svg.style.transform = "";
|
|
2017
|
+
this.basePosition = { x: overlayRect.x, y: overlayRect.y };
|
|
1557
2018
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
1558
2019
|
const noteStyle = {
|
|
1559
2020
|
color,
|
|
@@ -1593,7 +2054,8 @@ var DoodleOverlay = class {
|
|
|
1593
2054
|
this.options.arrow,
|
|
1594
2055
|
strokeStyle,
|
|
1595
2056
|
seed,
|
|
1596
|
-
note ? note.box : null
|
|
2057
|
+
note ? note.box : null,
|
|
2058
|
+
{ elapsed }
|
|
1597
2059
|
) : null;
|
|
1598
2060
|
if (this.options.decorations) {
|
|
1599
2061
|
renderDecorations(
|
|
@@ -1620,7 +2082,7 @@ var DoodleOverlay = class {
|
|
|
1620
2082
|
overlayMargin() {
|
|
1621
2083
|
const { note, arrow, decorations } = this.options;
|
|
1622
2084
|
if (!note && !arrow && !decorations) return 48;
|
|
1623
|
-
const text =
|
|
2085
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1624
2086
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1625
2087
|
}
|
|
1626
2088
|
destroy() {
|
|
@@ -1628,6 +2090,10 @@ var DoodleOverlay = class {
|
|
|
1628
2090
|
cancelFrame(this.pendingFrame);
|
|
1629
2091
|
this.pendingFrame = null;
|
|
1630
2092
|
}
|
|
2093
|
+
if (this.pendingRepositionFrame != null) {
|
|
2094
|
+
cancelFrame(this.pendingRepositionFrame);
|
|
2095
|
+
this.pendingRepositionFrame = null;
|
|
2096
|
+
}
|
|
1631
2097
|
if (this.resizeObserver) {
|
|
1632
2098
|
this.resizeObserver.disconnect();
|
|
1633
2099
|
this.resizeObserver = null;
|
|
@@ -1640,10 +2106,10 @@ var DoodleOverlay = class {
|
|
|
1640
2106
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1641
2107
|
this.onFontsLoaded = null;
|
|
1642
2108
|
}
|
|
1643
|
-
for (const { target, type } of this.
|
|
1644
|
-
target.removeEventListener(type,
|
|
2109
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2110
|
+
target.removeEventListener(type, handler);
|
|
1645
2111
|
}
|
|
1646
|
-
this.
|
|
2112
|
+
this.listeners = [];
|
|
1647
2113
|
if (this.svg?.parentNode) {
|
|
1648
2114
|
this.svg.parentNode.removeChild(this.svg);
|
|
1649
2115
|
}
|