@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.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,
|
|
@@ -16,7 +275,17 @@ var DEFAULT_OPTIONS = {
|
|
|
16
275
|
/** Override the handwriting stack, e.g. a next/font CSS variable. */
|
|
17
276
|
fontFamily: null,
|
|
18
277
|
/** Fetch Caveat when the page has not provided it. */
|
|
19
|
-
autoLoadFont: true
|
|
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,
|
|
284
|
+
/**
|
|
285
|
+
* Overlay stacking order. Omit to mirror the target element's z-index so
|
|
286
|
+
* doodles stay above the annotated element but below modals and other UI.
|
|
287
|
+
*/
|
|
288
|
+
zIndex: null
|
|
20
289
|
};
|
|
21
290
|
function resolveElement(target) {
|
|
22
291
|
if (!target) return null;
|
|
@@ -30,8 +299,15 @@ function resolveElement(target) {
|
|
|
30
299
|
}
|
|
31
300
|
function mergeOptions(options = {}) {
|
|
32
301
|
const merged = { ...DEFAULT_OPTIONS, ...options };
|
|
33
|
-
if (merged.note
|
|
34
|
-
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
|
+
};
|
|
35
311
|
}
|
|
36
312
|
if (merged.arrow) {
|
|
37
313
|
const config = merged.arrow === true ? {} : merged.arrow;
|
|
@@ -41,7 +317,10 @@ function mergeOptions(options = {}) {
|
|
|
41
317
|
// explicit `from` is honoured whether or not there is a note.
|
|
42
318
|
from: config.from ?? (merged.note ? "note" : "top-right"),
|
|
43
319
|
to: config.to ?? "edge",
|
|
44
|
-
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)
|
|
45
324
|
};
|
|
46
325
|
}
|
|
47
326
|
if (merged.decorations === true) {
|
|
@@ -109,6 +388,40 @@ function getScrollableAncestors(element) {
|
|
|
109
388
|
ancestors.push(window);
|
|
110
389
|
return ancestors;
|
|
111
390
|
}
|
|
391
|
+
function resolveOverlayZIndex(element, override) {
|
|
392
|
+
if (override != null) return override;
|
|
393
|
+
const { zIndex, position } = getComputedStyle(element);
|
|
394
|
+
if (zIndex !== "auto") {
|
|
395
|
+
const parsed = Number.parseInt(zIndex, 10);
|
|
396
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
397
|
+
}
|
|
398
|
+
let node = element.parentElement;
|
|
399
|
+
while (node && node !== document.documentElement) {
|
|
400
|
+
const style = getComputedStyle(node);
|
|
401
|
+
if (style.zIndex !== "auto" && style.position !== "static") {
|
|
402
|
+
const parsed = Number.parseInt(style.zIndex, 10);
|
|
403
|
+
if (Number.isFinite(parsed)) return parsed + 1;
|
|
404
|
+
}
|
|
405
|
+
node = node.parentElement;
|
|
406
|
+
}
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
function insertOverlaySvg(svg, element) {
|
|
410
|
+
const parent = element.parentNode;
|
|
411
|
+
if (!parent) {
|
|
412
|
+
document.body.appendChild(svg);
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
parent.insertBefore(svg, element.nextSibling);
|
|
416
|
+
}
|
|
417
|
+
function syncOverlayStacking(svg, element, zIndexOverride) {
|
|
418
|
+
const zIndex = resolveOverlayZIndex(element, zIndexOverride);
|
|
419
|
+
if (zIndex == null) {
|
|
420
|
+
svg.style.zIndex = "";
|
|
421
|
+
} else {
|
|
422
|
+
svg.style.zIndex = String(zIndex);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
112
425
|
|
|
113
426
|
// src/geometry.js
|
|
114
427
|
function averageCornerRadius(element) {
|
|
@@ -256,6 +569,7 @@ function annotationAnchor(position, rect, offset = 12) {
|
|
|
256
569
|
|
|
257
570
|
// src/renderer.js
|
|
258
571
|
var SVG_NS = "http://www.w3.org/2000/svg";
|
|
572
|
+
var XML_NS = "http://www.w3.org/XML/1998/namespace";
|
|
259
573
|
var HANDWRITTEN_FONT = '"Caveat", "Kalam", "Patrick Hand", "Bradley Hand", "Segoe Script", "Comic Sans MS", cursive';
|
|
260
574
|
var NOTE_FONT_SIZE = 19;
|
|
261
575
|
function clearSvg(svg) {
|
|
@@ -501,6 +815,15 @@ function appendPath(svg, d, style) {
|
|
|
501
815
|
svg.appendChild(path);
|
|
502
816
|
return path;
|
|
503
817
|
}
|
|
818
|
+
function appendGlow(svg, radius, style) {
|
|
819
|
+
const circle = document.createElementNS(SVG_NS, "circle");
|
|
820
|
+
circle.setAttribute("r", fmt(radius));
|
|
821
|
+
circle.setAttribute("fill", style.color);
|
|
822
|
+
circle.setAttribute("opacity", String(style.opacity));
|
|
823
|
+
circle.style.filter = `blur(${fmt(radius * 0.4)}px)`;
|
|
824
|
+
svg.appendChild(circle);
|
|
825
|
+
return circle;
|
|
826
|
+
}
|
|
504
827
|
function rectPerimeter(width, height, radius) {
|
|
505
828
|
const r = clamp(radius, 0, Math.min(width, height) / 2);
|
|
506
829
|
return 2 * (width - 2 * r) + 2 * (height - 2 * r) + 2 * Math.PI * r;
|
|
@@ -587,20 +910,21 @@ function cubicTangentAt(p0, c1, c2, p3, t) {
|
|
|
587
910
|
y: a * (c1.y - p0.y) + b * (c2.y - c1.y) + c * (p3.y - c2.y)
|
|
588
911
|
};
|
|
589
912
|
}
|
|
590
|
-
|
|
913
|
+
var STRADDLE = 0.75;
|
|
914
|
+
function loopedCurvePoints(start, c1, c2, end, radius, random, side = 1) {
|
|
591
915
|
const at = (t) => cubicAt(start, c1, c2, end, t);
|
|
592
|
-
const tangentAt = (t) => normalize(cubicTangentAt(start, c1, c2, end, t));
|
|
593
916
|
const knot = 0.46 + random() * 0.12;
|
|
594
917
|
const points = [];
|
|
595
918
|
const lead = Math.max(2, Math.round(knot * 16));
|
|
596
919
|
for (let i = 0; i <= lead; i++) points.push(at(knot * i / lead));
|
|
597
920
|
const pivot = at(knot);
|
|
598
|
-
const
|
|
921
|
+
const derivative = cubicTangentAt(start, c1, c2, end, knot);
|
|
922
|
+
const speed = Math.hypot(derivative.x, derivative.y) || 1;
|
|
923
|
+
const heading = normalize(derivative);
|
|
599
924
|
const normal = { x: -heading.y, y: heading.x };
|
|
600
|
-
const side = random() < 0.5 ? 1 : -1;
|
|
601
925
|
const centre = {
|
|
602
|
-
x: pivot.x + normal.x * radius * side,
|
|
603
|
-
y: pivot.y + normal.y * radius * side
|
|
926
|
+
x: pivot.x + normal.x * radius * STRADDLE * side,
|
|
927
|
+
y: pivot.y + normal.y * radius * STRADDLE * side
|
|
604
928
|
};
|
|
605
929
|
const from = Math.atan2(pivot.y - centre.y, pivot.x - centre.x);
|
|
606
930
|
const turns = Math.PI * 2 * (0.97 + random() * 0.08);
|
|
@@ -608,15 +932,27 @@ function loopedCurvePoints(start, c1, c2, end, radius, random) {
|
|
|
608
932
|
for (let i = 1; i <= steps; i++) {
|
|
609
933
|
const progress = i / steps;
|
|
610
934
|
const angle = from + turns * progress * side;
|
|
935
|
+
const spread = radius * (STRADDLE + (1 - STRADDLE) * progress);
|
|
611
936
|
const drift = radius * 0.55 * progress;
|
|
612
937
|
points.push({
|
|
613
|
-
x: centre.x + Math.cos(angle) *
|
|
614
|
-
y: centre.y + Math.sin(angle) *
|
|
938
|
+
x: centre.x + Math.cos(angle) * spread + heading.x * drift,
|
|
939
|
+
y: centre.y + Math.sin(angle) * spread + heading.y * drift
|
|
615
940
|
});
|
|
616
941
|
}
|
|
942
|
+
const exit = points[points.length - 1];
|
|
943
|
+
const gained = (exit.x - pivot.x) * heading.x + (exit.y - pivot.y) * heading.y;
|
|
944
|
+
const resume = Math.min(knot + Math.max(0, gained) / speed, 0.985);
|
|
945
|
+
const rejoin = at(resume);
|
|
946
|
+
const residual = (exit.x - rejoin.x) * normal.x + (exit.y - rejoin.y) * normal.y;
|
|
947
|
+
const settle = 6;
|
|
617
948
|
const tail = 14;
|
|
618
949
|
for (let i = 1; i <= tail; i++) {
|
|
619
|
-
|
|
950
|
+
const point = at(resume + (1 - resume) * (i / tail));
|
|
951
|
+
const fade = Math.max(0, 1 - i / settle);
|
|
952
|
+
points.push({
|
|
953
|
+
x: point.x + normal.x * residual * fade,
|
|
954
|
+
y: point.y + normal.y * residual * fade
|
|
955
|
+
});
|
|
620
956
|
}
|
|
621
957
|
return points;
|
|
622
958
|
}
|
|
@@ -690,21 +1026,29 @@ function arrowEndpoints(rect, arrowOptions, random, origin) {
|
|
|
690
1026
|
function drawArrowHead(svg, tip, angle, style, random, shaftLength = Infinity) {
|
|
691
1027
|
const length = Math.min(9 + style.strokeWidth * 1.4, shaftLength * 0.3);
|
|
692
1028
|
const spread = 0.46 + random() * 0.1;
|
|
1029
|
+
const arms = [];
|
|
693
1030
|
for (const side of [-1, 1]) {
|
|
694
1031
|
const armAngle = angle + spread * side;
|
|
695
1032
|
const tail = {
|
|
696
1033
|
x: tip.x - length * Math.cos(armAngle),
|
|
697
1034
|
y: tip.y - length * Math.sin(armAngle)
|
|
698
1035
|
};
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
1036
|
+
arms.push(
|
|
1037
|
+
appendPath(
|
|
1038
|
+
svg,
|
|
1039
|
+
roughLine(tail.x, tail.y, tip.x, tip.y, style.roughness * 0.5, random, {
|
|
1040
|
+
steps: 2,
|
|
1041
|
+
bow: 0.35 * side
|
|
1042
|
+
}),
|
|
1043
|
+
{ ...style, dashed: false }
|
|
1044
|
+
)
|
|
706
1045
|
);
|
|
707
1046
|
}
|
|
1047
|
+
return arms;
|
|
1048
|
+
}
|
|
1049
|
+
function bowOverTheTop(perpendicular) {
|
|
1050
|
+
if (Math.abs(perpendicular.y) < 0.02) return perpendicular.x > 0 ? 1 : -1;
|
|
1051
|
+
return perpendicular.y > 0 ? -1 : 1;
|
|
708
1052
|
}
|
|
709
1053
|
function boundsOf(points) {
|
|
710
1054
|
const xs = points.map((p) => p.x);
|
|
@@ -716,7 +1060,7 @@ function boundsOf(points) {
|
|
|
716
1060
|
height: Math.max(...ys) - Math.min(...ys)
|
|
717
1061
|
};
|
|
718
1062
|
}
|
|
719
|
-
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
1063
|
+
function drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
|
|
720
1064
|
const random = createRandom(seed + 17);
|
|
721
1065
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
722
1066
|
const dx = end.x - start.x;
|
|
@@ -725,6 +1069,7 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
725
1069
|
const dashed = arrowOptions.style === "dotted";
|
|
726
1070
|
let tipAngle = Math.atan2(dy, dx);
|
|
727
1071
|
let shaft;
|
|
1072
|
+
let ghost = null;
|
|
728
1073
|
const hull = [start, end];
|
|
729
1074
|
if (arrowOptions.style === "straight") {
|
|
730
1075
|
shaft = roughLine(start.x, start.y, end.x, end.y, style.roughness, random, {
|
|
@@ -738,7 +1083,12 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
738
1083
|
y: rect.y + rect.height / 2 - mid.y
|
|
739
1084
|
};
|
|
740
1085
|
const lateral = perpendicular.x * towardCenter.x + perpendicular.y * towardCenter.y;
|
|
741
|
-
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 :
|
|
1086
|
+
const sign = Math.abs(lateral) > distance * 0.12 ? lateral > 0 ? -1 : 1 : (
|
|
1087
|
+
// Nothing to steer around, so sweep over the top: a hand arcs above
|
|
1088
|
+
// the straight line and drops onto the target, and a coin flip here
|
|
1089
|
+
// used to mirror the whole gesture from one redraw to the next.
|
|
1090
|
+
bowOverTheTop(perpendicular)
|
|
1091
|
+
);
|
|
742
1092
|
const bow = Math.min(distance * (0.16 + random() * 0.08), 30) * sign;
|
|
743
1093
|
const c1x = start.x + dx * 0.28 + perpendicular.x * bow;
|
|
744
1094
|
const c1y = start.y + dy * 0.28 + perpendicular.y * bow;
|
|
@@ -753,34 +1103,46 @@ function drawArrow(svg, rect, arrowOptions, style, seed, origin = null) {
|
|
|
753
1103
|
{ x: c2x, y: c2y },
|
|
754
1104
|
end,
|
|
755
1105
|
radius,
|
|
756
|
-
random
|
|
1106
|
+
random,
|
|
1107
|
+
-sign
|
|
757
1108
|
);
|
|
758
1109
|
shaft = catmullRomPath(points, false);
|
|
759
1110
|
hull.push(...points);
|
|
760
1111
|
const last = points[points.length - 1];
|
|
761
1112
|
const prior = points[points.length - 2] ?? start;
|
|
762
1113
|
tipAngle = Math.atan2(last.y - prior.y, last.x - prior.x);
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
1114
|
+
} else {
|
|
1115
|
+
shaft = `M ${fmt(start.x)} ${fmt(start.y)} C ${fmt(c1x)} ${fmt(c1y)} ${fmt(c2x)} ${fmt(c2y)} ${fmt(end.x)} ${fmt(end.y)}`;
|
|
1116
|
+
if (style.roughness >= 1.1) {
|
|
1117
|
+
const drift = () => (random() - 0.5) * 1.4;
|
|
1118
|
+
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)}`;
|
|
1119
|
+
}
|
|
1120
|
+
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
766
1121
|
}
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
1122
|
+
}
|
|
1123
|
+
const group = document.createElementNS(SVG_NS, "g");
|
|
1124
|
+
svg.appendChild(group);
|
|
1125
|
+
const shaftPaths = [];
|
|
1126
|
+
if (ghost) {
|
|
1127
|
+
shaftPaths.push(
|
|
1128
|
+
appendPath(group, ghost, {
|
|
772
1129
|
...style,
|
|
773
1130
|
opacity: style.opacity * 0.3,
|
|
774
1131
|
strokeWidth: style.strokeWidth * 0.7,
|
|
775
1132
|
dashed
|
|
776
|
-
})
|
|
777
|
-
|
|
778
|
-
tipAngle = Math.atan2(end.y - c2y, end.x - c2x);
|
|
1133
|
+
})
|
|
1134
|
+
);
|
|
779
1135
|
}
|
|
780
|
-
appendPath(
|
|
781
|
-
drawArrowHead(
|
|
1136
|
+
shaftPaths.push(appendPath(group, shaft, { ...style, dashed }));
|
|
1137
|
+
const head = drawArrowHead(group, end, tipAngle, style, random, distance);
|
|
1138
|
+
animateArrow(
|
|
1139
|
+
group,
|
|
1140
|
+
{ shaft: shaftPaths, head, start, end },
|
|
1141
|
+
arrowOptions.animate,
|
|
1142
|
+
context.elapsed
|
|
1143
|
+
);
|
|
782
1144
|
const bounds = boundsOf(hull);
|
|
783
|
-
return { start, end, bounds };
|
|
1145
|
+
return { start, end, bounds, group };
|
|
784
1146
|
}
|
|
785
1147
|
function drawUnderline(svg, x, y, width, style, random) {
|
|
786
1148
|
if (width <= 4) return;
|
|
@@ -847,6 +1209,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
847
1209
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
848
1210
|
const lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
|
|
849
1211
|
const lineHeight = fontSize * 1.05;
|
|
1212
|
+
const rtl = options.rtl ?? isRtlText(text);
|
|
850
1213
|
const group = document.createElementNS(SVG_NS, "g");
|
|
851
1214
|
const textEl = document.createElementNS(SVG_NS, "text");
|
|
852
1215
|
textEl.setAttribute("x", "0");
|
|
@@ -859,6 +1222,13 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
859
1222
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
860
1223
|
textEl.setAttribute("text-anchor", "start");
|
|
861
1224
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
1225
|
+
if (rtl) {
|
|
1226
|
+
textEl.setAttribute("direction", "rtl");
|
|
1227
|
+
textEl.setAttribute("unicode-bidi", "embed");
|
|
1228
|
+
}
|
|
1229
|
+
if (options.locale) {
|
|
1230
|
+
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1231
|
+
}
|
|
862
1232
|
const tspans = lines.map((line, index) => {
|
|
863
1233
|
const tspan = document.createElementNS(SVG_NS, "tspan");
|
|
864
1234
|
tspan.textContent = line;
|
|
@@ -887,15 +1257,17 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
887
1257
|
height,
|
|
888
1258
|
ascent,
|
|
889
1259
|
descent,
|
|
890
|
-
fontSize
|
|
1260
|
+
fontSize,
|
|
1261
|
+
rtl
|
|
891
1262
|
};
|
|
892
1263
|
}
|
|
893
1264
|
function placeNoteText(note, box, style, seed, options = {}) {
|
|
894
1265
|
const random = createRandom(seed + 55);
|
|
895
1266
|
const tilt = options.tilt ?? (random() - 0.5) * 7;
|
|
896
|
-
note.
|
|
1267
|
+
const anchorX = note.rtl ? box.x + box.width : box.x;
|
|
1268
|
+
note.textEl.setAttribute("x", fmt(anchorX));
|
|
897
1269
|
note.textEl.setAttribute("y", fmt(box.y + note.ascent));
|
|
898
|
-
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(
|
|
1270
|
+
for (const tspan of note.tspans) tspan.setAttribute("x", fmt(anchorX));
|
|
899
1271
|
const cx = box.x + box.width / 2;
|
|
900
1272
|
const cy = box.y + box.height / 2;
|
|
901
1273
|
note.group.setAttribute(
|
|
@@ -904,9 +1276,10 @@ function placeNoteText(note, box, style, seed, options = {}) {
|
|
|
904
1276
|
);
|
|
905
1277
|
if (options.underline !== false) {
|
|
906
1278
|
const lastWidth = note.lineWidths[note.lineWidths.length - 1] ?? box.width;
|
|
1279
|
+
const underlineX = note.rtl ? box.x + box.width - lastWidth + 1 : box.x + 1;
|
|
907
1280
|
drawUnderline(
|
|
908
1281
|
note.group,
|
|
909
|
-
|
|
1282
|
+
underlineX,
|
|
910
1283
|
box.y + box.height + 2,
|
|
911
1284
|
lastWidth - 2,
|
|
912
1285
|
{
|
|
@@ -934,7 +1307,7 @@ function createOverlaySvg(left, top, width, height) {
|
|
|
934
1307
|
svg.style.height = `${height}px`;
|
|
935
1308
|
svg.style.pointerEvents = "none";
|
|
936
1309
|
svg.style.overflow = "visible";
|
|
937
|
-
svg.style.
|
|
1310
|
+
svg.style.willChange = "transform";
|
|
938
1311
|
return svg;
|
|
939
1312
|
}
|
|
940
1313
|
|
|
@@ -981,12 +1354,17 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
981
1354
|
return { ...box, x: box.x + dx };
|
|
982
1355
|
}
|
|
983
1356
|
function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
|
|
984
|
-
|
|
1357
|
+
const { text, locale } = resolveLocalizedText(
|
|
1358
|
+
noteOptions?.text,
|
|
1359
|
+
noteOptions?.locale
|
|
1360
|
+
);
|
|
1361
|
+
if (!text) return null;
|
|
985
1362
|
const band = layout.band ?? null;
|
|
986
1363
|
const position = isValidPosition(noteOptions.position) ? noteOptions.position : "top-right";
|
|
987
1364
|
const random = createRandom(seed + 9);
|
|
988
|
-
const note = createNoteText(svg,
|
|
989
|
-
wordsPerLine: noteOptions.wordsPerLine
|
|
1365
|
+
const note = createNoteText(svg, text, style, {
|
|
1366
|
+
wordsPerLine: noteOptions.wordsPerLine,
|
|
1367
|
+
locale
|
|
990
1368
|
});
|
|
991
1369
|
const tilt = (random() - 0.45) * 7;
|
|
992
1370
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
@@ -1028,6 +1406,25 @@ function drawDot(parent, x, y, style, scale = 1.7) {
|
|
|
1028
1406
|
strokeWidth: style.strokeWidth * scale
|
|
1029
1407
|
});
|
|
1030
1408
|
}
|
|
1409
|
+
function drawFleck(parent, x, y, radius, style, random) {
|
|
1410
|
+
const count = 6;
|
|
1411
|
+
const points = [];
|
|
1412
|
+
for (let i = 0; i < count; i++) {
|
|
1413
|
+
const angle = Math.PI * 2 * i / count + (random() - 0.5) * 0.5;
|
|
1414
|
+
const r = radius * (0.78 + random() * 0.34);
|
|
1415
|
+
points.push({
|
|
1416
|
+
x: x + Math.cos(angle) * r,
|
|
1417
|
+
y: y + Math.sin(angle) * r
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
const blob = appendPath(parent, catmullRomPath(points, true), {
|
|
1421
|
+
...style,
|
|
1422
|
+
fill: style.color,
|
|
1423
|
+
strokeWidth: 0
|
|
1424
|
+
});
|
|
1425
|
+
blob.style.filter = `blur(${(radius * 0.35).toFixed(2)}px)`;
|
|
1426
|
+
return blob;
|
|
1427
|
+
}
|
|
1031
1428
|
function drawHeart(parent, size, style, random) {
|
|
1032
1429
|
const s = size;
|
|
1033
1430
|
const j = () => (random() - 0.5) * s * 0.12;
|
|
@@ -1051,26 +1448,61 @@ function twinkle(parent, radiusX, radiusY, pinch, style) {
|
|
|
1051
1448
|
`Q ${(-c).toFixed(2)} ${(-c).toFixed(2)} 0 ${(-ry).toFixed(2)}`,
|
|
1052
1449
|
"Z"
|
|
1053
1450
|
].join(" ");
|
|
1054
|
-
appendPath(parent, path, style);
|
|
1451
|
+
return appendPath(parent, path, style);
|
|
1055
1452
|
}
|
|
1056
1453
|
function drawSparkle(parent, size, style, random) {
|
|
1057
1454
|
twinkle(
|
|
1058
1455
|
parent,
|
|
1059
|
-
size * (0.
|
|
1456
|
+
size * (0.62 + random() * 0.12),
|
|
1060
1457
|
size,
|
|
1061
|
-
size * (0.
|
|
1458
|
+
size * (0.14 + random() * 0.08),
|
|
1062
1459
|
style
|
|
1063
1460
|
);
|
|
1064
1461
|
}
|
|
1065
1462
|
function drawStar(parent, size, style, random) {
|
|
1066
1463
|
twinkle(
|
|
1067
1464
|
parent,
|
|
1068
|
-
size * (0.
|
|
1069
|
-
size,
|
|
1070
|
-
size * (0.
|
|
1465
|
+
size * (0.62 + random() * 0.12),
|
|
1466
|
+
size * (0.8 + random() * 0.12),
|
|
1467
|
+
size * (0.14 + random() * 0.08),
|
|
1071
1468
|
style
|
|
1072
1469
|
);
|
|
1073
1470
|
}
|
|
1471
|
+
function drawGlimmer(parent, size, style, random) {
|
|
1472
|
+
appendGlow(parent, size * (1.2 + random() * 0.2), {
|
|
1473
|
+
color: style.color,
|
|
1474
|
+
opacity: style.opacity * 0.22
|
|
1475
|
+
});
|
|
1476
|
+
const armX = size * (0.5 + random() * 0.08);
|
|
1477
|
+
const armY = size * (0.45 + random() * 0.08);
|
|
1478
|
+
const bluntness = size * 0.19;
|
|
1479
|
+
const main = twinkle(parent, armX, armY, size * (0.08 + random() * 0.03), {
|
|
1480
|
+
...style,
|
|
1481
|
+
fill: style.color,
|
|
1482
|
+
strokeWidth: bluntness
|
|
1483
|
+
});
|
|
1484
|
+
main.setAttribute("stroke-linejoin", "round");
|
|
1485
|
+
main.setAttribute("stroke-linecap", "round");
|
|
1486
|
+
main.style.filter = `blur(${(size * 0.05).toFixed(2)}px)`;
|
|
1487
|
+
const gap = bluntness / 2 + size * 0.2;
|
|
1488
|
+
const corners = [
|
|
1489
|
+
{ x: 0, y: -(armY + gap) },
|
|
1490
|
+
{ x: armX + gap, y: 0 },
|
|
1491
|
+
{ x: 0, y: armY + gap },
|
|
1492
|
+
{ x: -(armX + gap), y: 0 }
|
|
1493
|
+
];
|
|
1494
|
+
for (const { x, y } of corners) {
|
|
1495
|
+
const jitter = 0.95 + random() * 0.1;
|
|
1496
|
+
drawFleck(
|
|
1497
|
+
parent,
|
|
1498
|
+
x * jitter,
|
|
1499
|
+
y * jitter,
|
|
1500
|
+
size * (0.1 + random() * 0.03),
|
|
1501
|
+
style,
|
|
1502
|
+
random
|
|
1503
|
+
);
|
|
1504
|
+
}
|
|
1505
|
+
}
|
|
1074
1506
|
function drawTwinkle(parent, size, style, random) {
|
|
1075
1507
|
const count = 5;
|
|
1076
1508
|
const first = -Math.PI / 2 + (random() - 0.5) * 0.5;
|
|
@@ -1178,46 +1610,15 @@ function drawEmphasis(parent, size, style, random, opts = {}) {
|
|
|
1178
1610
|
);
|
|
1179
1611
|
}
|
|
1180
1612
|
}
|
|
1181
|
-
function drawDots(parent, size, style, random) {
|
|
1182
|
-
let x = -size * 0.7;
|
|
1183
|
-
for (let i = 0; i < 3; i++) {
|
|
1184
|
-
drawDot(parent, x, (random() - 0.5) * size * 0.16, style);
|
|
1185
|
-
x += size * (0.6 + random() * 0.25);
|
|
1186
|
-
}
|
|
1187
|
-
}
|
|
1188
|
-
function drawStroke(parent, size, style, random) {
|
|
1189
|
-
appendPath(
|
|
1190
|
-
parent,
|
|
1191
|
-
roughLine(-size, 0, size, 0, 1.2, random, {
|
|
1192
|
-
bow: (random() - 0.5) * size * 0.35,
|
|
1193
|
-
steps: 4
|
|
1194
|
-
}),
|
|
1195
|
-
style
|
|
1196
|
-
);
|
|
1197
|
-
}
|
|
1198
|
-
function drawSteam(parent, size, style, random) {
|
|
1199
|
-
const lean = random() < 0.5 ? -1 : 1;
|
|
1200
|
-
for (let i = 0; i < 2; i++) {
|
|
1201
|
-
const x = (i - 0.5) * size * 0.72;
|
|
1202
|
-
const base = size * 0.62 - i * size * 0.14;
|
|
1203
|
-
appendPath(
|
|
1204
|
-
parent,
|
|
1205
|
-
`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)}`,
|
|
1206
|
-
style
|
|
1207
|
-
);
|
|
1208
|
-
}
|
|
1209
|
-
}
|
|
1210
1613
|
var DECORATION_DRAWERS = {
|
|
1211
1614
|
heart: drawHeart,
|
|
1212
1615
|
sparkle: drawSparkle,
|
|
1213
1616
|
star: drawStar,
|
|
1214
1617
|
twinkle: drawTwinkle,
|
|
1618
|
+
glimmer: drawGlimmer,
|
|
1215
1619
|
smiley: drawSmiley,
|
|
1216
1620
|
emphasis: drawEmphasis,
|
|
1217
|
-
arcs: drawArcs
|
|
1218
|
-
dots: drawDots,
|
|
1219
|
-
stroke: drawStroke,
|
|
1220
|
-
steam: drawSteam
|
|
1621
|
+
arcs: drawArcs
|
|
1221
1622
|
};
|
|
1222
1623
|
var TEXT_TYPES = ["heart", "sparkle"];
|
|
1223
1624
|
var CORNER_ACCENTS = ["arcs", "emphasis"];
|
|
@@ -1228,12 +1629,10 @@ var TYPE_TRAITS = {
|
|
|
1228
1629
|
sparkle: { size: [6, 9], tilt: 20, standoff: 22 },
|
|
1229
1630
|
star: { size: [8, 11], tilt: 25, standoff: 26 },
|
|
1230
1631
|
twinkle: { size: [5, 7.5], tilt: 30, standoff: 26, brightness: 1.4 },
|
|
1632
|
+
glimmer: { size: [7, 10], tilt: 20, standoff: 28, brightness: 1.3 },
|
|
1231
1633
|
smiley: { size: [8, 11], tilt: 10, standoff: 22 },
|
|
1232
1634
|
emphasis: { size: [12, 17], tilt: 10, standoff: 13, outward: true },
|
|
1233
|
-
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1234
|
-
dots: { size: [6, 9], tilt: 8, standoff: 20 },
|
|
1235
|
-
stroke: { size: [8, 12], tilt: 40, standoff: 20 },
|
|
1236
|
-
steam: { size: [8, 11], tilt: 10, standoff: 20 }
|
|
1635
|
+
arcs: { size: [11, 16], tilt: 12, standoff: 12, outward: true }
|
|
1237
1636
|
};
|
|
1238
1637
|
var DEFAULT_TYPES = [
|
|
1239
1638
|
"twinkle",
|
|
@@ -1436,6 +1835,9 @@ function ensureHandwrittenFont() {
|
|
|
1436
1835
|
|
|
1437
1836
|
// src/doodle.js
|
|
1438
1837
|
var instances = /* @__PURE__ */ new WeakMap();
|
|
1838
|
+
function now() {
|
|
1839
|
+
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1840
|
+
}
|
|
1439
1841
|
function localBand(overlayRect, inset = 8) {
|
|
1440
1842
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1441
1843
|
return {
|
|
@@ -1452,19 +1854,24 @@ var DoodleOverlay = class {
|
|
|
1452
1854
|
this.element = element;
|
|
1453
1855
|
this.options = options;
|
|
1454
1856
|
this.seed = Math.floor(Math.random() * 1e9);
|
|
1857
|
+
this.startedAt = now();
|
|
1455
1858
|
this.svg = null;
|
|
1456
1859
|
this.resizeObserver = null;
|
|
1457
1860
|
this.mutationObserver = null;
|
|
1458
1861
|
this.pendingFrame = null;
|
|
1459
|
-
this.
|
|
1460
|
-
this.
|
|
1862
|
+
this.pendingRepositionFrame = null;
|
|
1863
|
+
this.basePosition = { x: 0, y: 0 };
|
|
1864
|
+
this.listeners = [];
|
|
1865
|
+
this.onScroll = this.scheduleReposition.bind(this);
|
|
1461
1866
|
this.onResize = this.scheduleUpdate.bind(this);
|
|
1867
|
+
this.onLanguageChange = this.scheduleUpdate.bind(this);
|
|
1462
1868
|
this.childElements = [];
|
|
1463
1869
|
this.mount();
|
|
1464
1870
|
}
|
|
1465
1871
|
mount() {
|
|
1466
1872
|
this.svg = createOverlaySvg(0, 0, 0, 0);
|
|
1467
|
-
|
|
1873
|
+
insertOverlaySvg(this.svg, this.element);
|
|
1874
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1468
1875
|
this.resizeObserver = new ResizeObserver(this.onResize);
|
|
1469
1876
|
this.resizeObserver.observe(this.element);
|
|
1470
1877
|
if (this.options.children) {
|
|
@@ -1478,10 +1885,20 @@ var DoodleOverlay = class {
|
|
|
1478
1885
|
});
|
|
1479
1886
|
for (const target of getScrollableAncestors(this.element)) {
|
|
1480
1887
|
target.addEventListener("scroll", this.onScroll, { passive: true });
|
|
1481
|
-
this.
|
|
1888
|
+
this.listeners.push({ target, type: "scroll", handler: this.onScroll });
|
|
1482
1889
|
}
|
|
1483
1890
|
window.addEventListener("resize", this.onResize, { passive: true });
|
|
1484
|
-
this.
|
|
1891
|
+
this.listeners.push({
|
|
1892
|
+
target: window,
|
|
1893
|
+
type: "resize",
|
|
1894
|
+
handler: this.onResize
|
|
1895
|
+
});
|
|
1896
|
+
window.addEventListener("languagechange", this.onLanguageChange);
|
|
1897
|
+
this.listeners.push({
|
|
1898
|
+
target: window,
|
|
1899
|
+
type: "languagechange",
|
|
1900
|
+
handler: this.onLanguageChange
|
|
1901
|
+
});
|
|
1485
1902
|
if (this.options.note && this.options.autoLoadFont && !this.options.fontFamily) {
|
|
1486
1903
|
ensureHandwrittenFont();
|
|
1487
1904
|
}
|
|
@@ -1505,6 +1922,46 @@ var DoodleOverlay = class {
|
|
|
1505
1922
|
this.update();
|
|
1506
1923
|
});
|
|
1507
1924
|
}
|
|
1925
|
+
scheduleReposition() {
|
|
1926
|
+
if (this.pendingFrame != null || this.pendingRepositionFrame != null) {
|
|
1927
|
+
return;
|
|
1928
|
+
}
|
|
1929
|
+
this.pendingRepositionFrame = scheduleFrame(() => {
|
|
1930
|
+
this.pendingRepositionFrame = null;
|
|
1931
|
+
this.reposition();
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
/**
|
|
1935
|
+
* Cheap scroll-driven path: slide the overlay to the element's new
|
|
1936
|
+
* viewport position without touching its contents. Scrolling can't change
|
|
1937
|
+
* an element's size, so the rects, viewBox and every hand-drawn stroke
|
|
1938
|
+
* inside them are still valid - only the overlay's position needs to move.
|
|
1939
|
+
*
|
|
1940
|
+
* That move is a `transform`, not `left`/`top`. `left`/`top` sit in the
|
|
1941
|
+
* same box-geometry pass as layout, so the browser re-checks layout on
|
|
1942
|
+
* every write; `transform` is composited, so the scroll path never
|
|
1943
|
+
* touches layout at all.
|
|
1944
|
+
*/
|
|
1945
|
+
reposition() {
|
|
1946
|
+
if (!this.svg || !this.element.isConnected) return;
|
|
1947
|
+
const { padding, radius } = this.options;
|
|
1948
|
+
const targets = this.getTargets();
|
|
1949
|
+
const margin = this.overlayMargin();
|
|
1950
|
+
const absoluteRects = targets.map(
|
|
1951
|
+
(target) => getElementBounds(target, padding, radius)
|
|
1952
|
+
);
|
|
1953
|
+
const overlayRect = unionRects(absoluteRects, margin);
|
|
1954
|
+
const currentWidth = parseFloat(this.svg.style.width) || 0;
|
|
1955
|
+
const currentHeight = parseFloat(this.svg.style.height) || 0;
|
|
1956
|
+
const resized = Math.abs(overlayRect.width - currentWidth) > 0.5 || Math.abs(overlayRect.height - currentHeight) > 0.5;
|
|
1957
|
+
if (resized) {
|
|
1958
|
+
this.update();
|
|
1959
|
+
return;
|
|
1960
|
+
}
|
|
1961
|
+
const dx = overlayRect.x - this.basePosition.x;
|
|
1962
|
+
const dy = overlayRect.y - this.basePosition.y;
|
|
1963
|
+
this.svg.style.transform = `translate3d(${dx}px, ${dy}px, 0)`;
|
|
1964
|
+
}
|
|
1508
1965
|
getTargets() {
|
|
1509
1966
|
if (this.options.children) {
|
|
1510
1967
|
const nodes = this.element.querySelectorAll(this.options.children);
|
|
@@ -1517,11 +1974,13 @@ var DoodleOverlay = class {
|
|
|
1517
1974
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1518
1975
|
const targets = this.getTargets();
|
|
1519
1976
|
const margin = this.overlayMargin();
|
|
1977
|
+
const elapsed = now() - this.startedAt;
|
|
1520
1978
|
const absoluteRects = targets.map(
|
|
1521
1979
|
(target) => getElementBounds(target, padding, radius)
|
|
1522
1980
|
);
|
|
1523
1981
|
const overlayRect = unionRects(absoluteRects, margin);
|
|
1524
1982
|
clearSvg(this.svg);
|
|
1983
|
+
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1525
1984
|
this.svg.setAttribute("width", String(overlayRect.width));
|
|
1526
1985
|
this.svg.setAttribute("height", String(overlayRect.height));
|
|
1527
1986
|
this.svg.setAttribute("viewBox", `0 0 ${overlayRect.width} ${overlayRect.height}`);
|
|
@@ -1529,6 +1988,8 @@ var DoodleOverlay = class {
|
|
|
1529
1988
|
this.svg.style.top = `${overlayRect.y}px`;
|
|
1530
1989
|
this.svg.style.width = `${overlayRect.width}px`;
|
|
1531
1990
|
this.svg.style.height = `${overlayRect.height}px`;
|
|
1991
|
+
this.svg.style.transform = "";
|
|
1992
|
+
this.basePosition = { x: overlayRect.x, y: overlayRect.y };
|
|
1532
1993
|
const strokeStyle = { color, strokeWidth, roughness, opacity };
|
|
1533
1994
|
const noteStyle = {
|
|
1534
1995
|
color,
|
|
@@ -1568,7 +2029,8 @@ var DoodleOverlay = class {
|
|
|
1568
2029
|
this.options.arrow,
|
|
1569
2030
|
strokeStyle,
|
|
1570
2031
|
seed,
|
|
1571
|
-
note ? note.box : null
|
|
2032
|
+
note ? note.box : null,
|
|
2033
|
+
{ elapsed }
|
|
1572
2034
|
) : null;
|
|
1573
2035
|
if (this.options.decorations) {
|
|
1574
2036
|
renderDecorations(
|
|
@@ -1595,7 +2057,7 @@ var DoodleOverlay = class {
|
|
|
1595
2057
|
overlayMargin() {
|
|
1596
2058
|
const { note, arrow, decorations } = this.options;
|
|
1597
2059
|
if (!note && !arrow && !decorations) return 48;
|
|
1598
|
-
const text =
|
|
2060
|
+
const { text } = resolveLocalizedText(note?.text, note?.locale);
|
|
1599
2061
|
return Math.max(96, Math.min(240, 80 + text.length * 8));
|
|
1600
2062
|
}
|
|
1601
2063
|
destroy() {
|
|
@@ -1603,6 +2065,10 @@ var DoodleOverlay = class {
|
|
|
1603
2065
|
cancelFrame(this.pendingFrame);
|
|
1604
2066
|
this.pendingFrame = null;
|
|
1605
2067
|
}
|
|
2068
|
+
if (this.pendingRepositionFrame != null) {
|
|
2069
|
+
cancelFrame(this.pendingRepositionFrame);
|
|
2070
|
+
this.pendingRepositionFrame = null;
|
|
2071
|
+
}
|
|
1606
2072
|
if (this.resizeObserver) {
|
|
1607
2073
|
this.resizeObserver.disconnect();
|
|
1608
2074
|
this.resizeObserver = null;
|
|
@@ -1615,10 +2081,10 @@ var DoodleOverlay = class {
|
|
|
1615
2081
|
document.fonts?.removeEventListener?.("loadingdone", this.onFontsLoaded);
|
|
1616
2082
|
this.onFontsLoaded = null;
|
|
1617
2083
|
}
|
|
1618
|
-
for (const { target, type } of this.
|
|
1619
|
-
target.removeEventListener(type,
|
|
2084
|
+
for (const { target, type, handler } of this.listeners) {
|
|
2085
|
+
target.removeEventListener(type, handler);
|
|
1620
2086
|
}
|
|
1621
|
-
this.
|
|
2087
|
+
this.listeners = [];
|
|
1622
2088
|
if (this.svg?.parentNode) {
|
|
1623
2089
|
this.svg.parentNode.removeChild(this.svg);
|
|
1624
2090
|
}
|