@aznabee/freehand-ui 0.2.1 → 0.3.0
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 +51 -2
- package/dist/freehand-ui.cjs +147 -29
- package/dist/freehand-ui.js +147 -29
- package/dist/index.d.ts +47 -0
- package/dist/native.cjs +131 -14
- package/dist/native.d.ts +1 -0
- package/dist/native.js +131 -14
- package/dist/react.cjs +147 -29
- package/dist/react.d.ts +1 -0
- package/dist/react.js +147 -29
- package/package.json +11 -3
package/dist/react.cjs
CHANGED
|
@@ -356,6 +356,9 @@ function mergeOptions(options = {}) {
|
|
|
356
356
|
// `animate: false` opts out, an object tunes speed and gestures
|
|
357
357
|
animate: normalizeArrowAnimation(config.animate)
|
|
358
358
|
};
|
|
359
|
+
for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
|
|
360
|
+
if (config[key] !== void 0) merged.arrow[key] = config[key];
|
|
361
|
+
}
|
|
359
362
|
}
|
|
360
363
|
if (merged.decorations === true) {
|
|
361
364
|
merged.decorations = { count: null, types: null };
|
|
@@ -477,14 +480,23 @@ function detectBorderRadius(element) {
|
|
|
477
480
|
const fitsTightly = Math.abs(outer.width - inner.width) <= 2 && Math.abs(outer.height - inner.height) <= 2;
|
|
478
481
|
return fitsTightly ? averageCornerRadius(child) : 0;
|
|
479
482
|
}
|
|
480
|
-
function
|
|
483
|
+
function screenScaleOf(element) {
|
|
484
|
+
const layoutWidth = (
|
|
485
|
+
/** @type {HTMLElement} */
|
|
486
|
+
element.offsetWidth
|
|
487
|
+
);
|
|
488
|
+
if (!layoutWidth) return 1;
|
|
489
|
+
const scale = element.getBoundingClientRect().width / layoutWidth;
|
|
490
|
+
return Number.isFinite(scale) && scale > 0.01 ? scale : 1;
|
|
491
|
+
}
|
|
492
|
+
function getElementBounds(element, padding = 0, radiusOverride = null, scale = 1) {
|
|
481
493
|
const rect = element.getBoundingClientRect();
|
|
482
494
|
const radius = radiusOverride != null ? radiusOverride : detectBorderRadius(element);
|
|
483
|
-
const width = rect.width + padding * 2;
|
|
484
|
-
const height = rect.height + padding * 2;
|
|
495
|
+
const width = rect.width / scale + padding * 2;
|
|
496
|
+
const height = rect.height / scale + padding * 2;
|
|
485
497
|
return {
|
|
486
|
-
x: rect.left - padding,
|
|
487
|
-
y: rect.top - padding,
|
|
498
|
+
x: rect.left / scale - padding,
|
|
499
|
+
y: rect.top / scale - padding,
|
|
488
500
|
width,
|
|
489
501
|
height,
|
|
490
502
|
// Grow the radius with the padding so the doodle stays concentric with the
|
|
@@ -1080,7 +1092,24 @@ function boundsOf(points) {
|
|
|
1080
1092
|
height: Math.max(...ys) - Math.min(...ys)
|
|
1081
1093
|
};
|
|
1082
1094
|
}
|
|
1083
|
-
function
|
|
1095
|
+
function resolveArrowStyle(base, options = {}) {
|
|
1096
|
+
const style = { ...base };
|
|
1097
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1098
|
+
style.color = options.color;
|
|
1099
|
+
}
|
|
1100
|
+
if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
|
|
1101
|
+
style.strokeWidth = options.strokeWidth;
|
|
1102
|
+
}
|
|
1103
|
+
if (Number.isFinite(options.roughness) && options.roughness >= 0) {
|
|
1104
|
+
style.roughness = options.roughness;
|
|
1105
|
+
}
|
|
1106
|
+
if (Number.isFinite(options.opacity)) {
|
|
1107
|
+
style.opacity = Math.min(1, Math.max(0, options.opacity));
|
|
1108
|
+
}
|
|
1109
|
+
return style;
|
|
1110
|
+
}
|
|
1111
|
+
function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
|
|
1112
|
+
const style = resolveArrowStyle(baseStyle, arrowOptions);
|
|
1084
1113
|
const random = createRandom(seed + 17);
|
|
1085
1114
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
1086
1115
|
const dx = end.x - start.x;
|
|
@@ -1200,17 +1229,69 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
1200
1229
|
);
|
|
1201
1230
|
}
|
|
1202
1231
|
var DEFAULT_WORDS_PER_LINE = 2;
|
|
1203
|
-
function wrapWords(text, perLine) {
|
|
1232
|
+
function wrapWords(text, perLine, marks = null) {
|
|
1233
|
+
const source = String(text);
|
|
1204
1234
|
const lines = [];
|
|
1205
|
-
|
|
1206
|
-
|
|
1235
|
+
let cursor = 0;
|
|
1236
|
+
for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
|
|
1237
|
+
const start = cursor;
|
|
1238
|
+
cursor += paragraph.length;
|
|
1239
|
+
if (index % 2 === 1) continue;
|
|
1240
|
+
const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
|
|
1241
|
+
text: m[0],
|
|
1242
|
+
at: start + m.index
|
|
1243
|
+
}));
|
|
1207
1244
|
if (!words.length) continue;
|
|
1208
1245
|
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
1209
1246
|
for (let i = 0; i < words.length; i += size) {
|
|
1210
|
-
lines.push(words.slice(i, i + size)
|
|
1247
|
+
lines.push(lineOf(words.slice(i, i + size), marks));
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
|
|
1251
|
+
}
|
|
1252
|
+
function lineOf(words, marks) {
|
|
1253
|
+
const text = words.map((w) => w.text).join(" ");
|
|
1254
|
+
const result = [];
|
|
1255
|
+
let previous = -1;
|
|
1256
|
+
words.forEach((word, index) => {
|
|
1257
|
+
const first = marks ? marks[word.at] ?? -1 : -1;
|
|
1258
|
+
if (index > 0) result.push(previous === first ? first : -1);
|
|
1259
|
+
for (let k = 0; k < word.text.length; k++) {
|
|
1260
|
+
result.push(marks ? marks[word.at + k] ?? -1 : -1);
|
|
1261
|
+
}
|
|
1262
|
+
previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
|
|
1263
|
+
});
|
|
1264
|
+
return { text, marks: result };
|
|
1265
|
+
}
|
|
1266
|
+
function normalizeHighlights(highlight) {
|
|
1267
|
+
const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
|
|
1268
|
+
return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
|
|
1269
|
+
}
|
|
1270
|
+
function highlightMarks(text, highlights) {
|
|
1271
|
+
if (!highlights.length) return null;
|
|
1272
|
+
const marks = new Int16Array(text.length).fill(-1);
|
|
1273
|
+
let any = false;
|
|
1274
|
+
highlights.forEach((highlight, index) => {
|
|
1275
|
+
for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
|
|
1276
|
+
for (let k = at; k < at + highlight.text.length; k++) {
|
|
1277
|
+
if (marks[k] === -1) {
|
|
1278
|
+
marks[k] = index;
|
|
1279
|
+
any = true;
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1211
1282
|
}
|
|
1283
|
+
});
|
|
1284
|
+
return any ? marks : null;
|
|
1285
|
+
}
|
|
1286
|
+
function segmentsOf(line) {
|
|
1287
|
+
const segments = [];
|
|
1288
|
+
for (let i = 0; i < line.text.length; i++) {
|
|
1289
|
+
const highlight = line.marks[i] ?? -1;
|
|
1290
|
+
const last = segments[segments.length - 1];
|
|
1291
|
+
if (last && last.highlight === highlight) last.text += line.text[i];
|
|
1292
|
+
else segments.push({ text: line.text[i], highlight });
|
|
1212
1293
|
}
|
|
1213
|
-
return
|
|
1294
|
+
return segments;
|
|
1214
1295
|
}
|
|
1215
1296
|
function measureWidth(element, content, fontSize) {
|
|
1216
1297
|
try {
|
|
@@ -1227,7 +1308,13 @@ function measureWidth(element, content, fontSize) {
|
|
|
1227
1308
|
}
|
|
1228
1309
|
function createNoteText(svg, text, style, options = {}) {
|
|
1229
1310
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
1230
|
-
const
|
|
1311
|
+
const highlights = normalizeHighlights(options.highlight);
|
|
1312
|
+
const wrapped = wrapWords(
|
|
1313
|
+
text,
|
|
1314
|
+
options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
|
|
1315
|
+
highlightMarks(String(text), highlights)
|
|
1316
|
+
);
|
|
1317
|
+
const lines = wrapped.map((line) => line.text);
|
|
1231
1318
|
const lineHeight = fontSize * 1.05;
|
|
1232
1319
|
const rtl = options.rtl ?? isRtlText(text);
|
|
1233
1320
|
const group = createSvgElement("g");
|
|
@@ -1238,7 +1325,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1238
1325
|
textEl.setAttribute("opacity", String(style.opacity));
|
|
1239
1326
|
textEl.setAttribute("font-size", String(fontSize));
|
|
1240
1327
|
textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
|
|
1241
|
-
textEl.setAttribute("font-weight", "600");
|
|
1328
|
+
textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
|
|
1242
1329
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
1243
1330
|
textEl.setAttribute("text-anchor", "start");
|
|
1244
1331
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
@@ -1249,11 +1336,25 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1249
1336
|
if (options.locale) {
|
|
1250
1337
|
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1251
1338
|
}
|
|
1252
|
-
const tspans =
|
|
1339
|
+
const tspans = wrapped.map((line, index) => {
|
|
1253
1340
|
const tspan = createSvgElement("tspan");
|
|
1254
|
-
tspan.textContent = line;
|
|
1255
1341
|
tspan.setAttribute("x", "0");
|
|
1256
1342
|
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
1343
|
+
const segments = segmentsOf(line);
|
|
1344
|
+
if (segments.every((segment) => segment.highlight === -1)) {
|
|
1345
|
+
tspan.textContent = line.text;
|
|
1346
|
+
} else {
|
|
1347
|
+
for (const segment of segments) {
|
|
1348
|
+
const run = createSvgElement("tspan");
|
|
1349
|
+
run.textContent = segment.text;
|
|
1350
|
+
const highlight = highlights[segment.highlight];
|
|
1351
|
+
if (highlight?.color) run.setAttribute("fill", highlight.color);
|
|
1352
|
+
if (highlight?.fontWeight != null) {
|
|
1353
|
+
run.setAttribute("font-weight", String(highlight.fontWeight));
|
|
1354
|
+
}
|
|
1355
|
+
tspan.appendChild(run);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1257
1358
|
textEl.appendChild(tspan);
|
|
1258
1359
|
return tspan;
|
|
1259
1360
|
});
|
|
@@ -1372,7 +1473,21 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1372
1473
|
if (!dx) return box;
|
|
1373
1474
|
return { ...box, x: box.x + dx };
|
|
1374
1475
|
}
|
|
1375
|
-
function
|
|
1476
|
+
function resolveNoteStyle(base, options = {}) {
|
|
1477
|
+
const style = { ...base };
|
|
1478
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1479
|
+
style.color = options.color;
|
|
1480
|
+
}
|
|
1481
|
+
if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
|
|
1482
|
+
style.fontSize = options.fontSize;
|
|
1483
|
+
}
|
|
1484
|
+
if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
|
|
1485
|
+
style.fontWeight = options.fontWeight;
|
|
1486
|
+
}
|
|
1487
|
+
return style;
|
|
1488
|
+
}
|
|
1489
|
+
function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
|
|
1490
|
+
const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
|
|
1376
1491
|
const { text, locale } = resolveLocalizedText(
|
|
1377
1492
|
noteOptions?.text,
|
|
1378
1493
|
noteOptions?.locale
|
|
@@ -1383,9 +1498,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
|
|
|
1383
1498
|
const random = createRandom(seed + 9);
|
|
1384
1499
|
const note = createNoteText(svg, text, style, {
|
|
1385
1500
|
wordsPerLine: noteOptions.wordsPerLine,
|
|
1386
|
-
locale
|
|
1501
|
+
locale,
|
|
1502
|
+
highlight: noteOptions.highlight
|
|
1387
1503
|
});
|
|
1388
|
-
const
|
|
1504
|
+
const randomTilt = (random() - 0.45) * 7;
|
|
1505
|
+
const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
|
|
1389
1506
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
1390
1507
|
const size = {
|
|
1391
1508
|
width: note.width,
|
|
@@ -1857,14 +1974,14 @@ var instances = /* @__PURE__ */ new WeakMap();
|
|
|
1857
1974
|
function now() {
|
|
1858
1975
|
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1859
1976
|
}
|
|
1860
|
-
function localBand(originLeft, inset = 8) {
|
|
1977
|
+
function localBand(originLeft, inset = 8, scale = 1) {
|
|
1861
1978
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1862
1979
|
return {
|
|
1863
|
-
x: -originLeft + inset,
|
|
1864
|
-
width: Math.max(0, width - inset * 2)
|
|
1980
|
+
x: -originLeft + inset / scale,
|
|
1981
|
+
width: Math.max(0, width - inset * 2) / scale
|
|
1865
1982
|
};
|
|
1866
1983
|
}
|
|
1867
|
-
function paddingBox(element) {
|
|
1984
|
+
function paddingBox(element, scale = 1) {
|
|
1868
1985
|
const rect = element.getBoundingClientRect();
|
|
1869
1986
|
const style = getComputedStyle(element);
|
|
1870
1987
|
const left = parseFloat(style.borderLeftWidth) || 0;
|
|
@@ -1872,10 +1989,10 @@ function paddingBox(element) {
|
|
|
1872
1989
|
const right = parseFloat(style.borderRightWidth) || 0;
|
|
1873
1990
|
const bottom = parseFloat(style.borderBottomWidth) || 0;
|
|
1874
1991
|
return {
|
|
1875
|
-
left: rect.left + left,
|
|
1876
|
-
top: rect.top + top,
|
|
1877
|
-
width: Math.max(0, rect.width - left - right),
|
|
1878
|
-
height: Math.max(0, rect.height - top - bottom)
|
|
1992
|
+
left: rect.left / scale + left,
|
|
1993
|
+
top: rect.top / scale + top,
|
|
1994
|
+
width: Math.max(0, rect.width / scale - left - right),
|
|
1995
|
+
height: Math.max(0, rect.height / scale - top - bottom)
|
|
1879
1996
|
};
|
|
1880
1997
|
}
|
|
1881
1998
|
var DoodleOverlay = class {
|
|
@@ -1964,12 +2081,13 @@ var DoodleOverlay = class {
|
|
|
1964
2081
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1965
2082
|
const targets = this.getTargets();
|
|
1966
2083
|
const elapsed = now() - this.startedAt;
|
|
2084
|
+
const scale = screenScaleOf(this.element);
|
|
1967
2085
|
const absoluteRects = targets.map(
|
|
1968
|
-
(target) => getElementBounds(target, padding, radius)
|
|
2086
|
+
(target) => getElementBounds(target, padding, radius, scale)
|
|
1969
2087
|
);
|
|
1970
2088
|
clearSvg(this.svg);
|
|
1971
2089
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1972
|
-
const box = paddingBox(this.element);
|
|
2090
|
+
const box = paddingBox(this.element, scale);
|
|
1973
2091
|
const width = Math.max(1, box.width);
|
|
1974
2092
|
const height = Math.max(1, box.height);
|
|
1975
2093
|
const origin = { x: box.left, y: box.top };
|
|
@@ -1990,7 +2108,7 @@ var DoodleOverlay = class {
|
|
|
1990
2108
|
};
|
|
1991
2109
|
const noteGroups = [];
|
|
1992
2110
|
const noteLayout = {
|
|
1993
|
-
band: localBand(origin.x),
|
|
2111
|
+
band: localBand(origin.x, 8, scale),
|
|
1994
2112
|
gap: this.options.arrow ? 40 : 14
|
|
1995
2113
|
};
|
|
1996
2114
|
targets.forEach((target, index) => {
|
package/dist/react.d.ts
CHANGED
package/dist/react.js
CHANGED
|
@@ -339,6 +339,9 @@ function mergeOptions(options = {}) {
|
|
|
339
339
|
// `animate: false` opts out, an object tunes speed and gestures
|
|
340
340
|
animate: normalizeArrowAnimation(config.animate)
|
|
341
341
|
};
|
|
342
|
+
for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
|
|
343
|
+
if (config[key] !== void 0) merged.arrow[key] = config[key];
|
|
344
|
+
}
|
|
342
345
|
}
|
|
343
346
|
if (merged.decorations === true) {
|
|
344
347
|
merged.decorations = { count: null, types: null };
|
|
@@ -460,14 +463,23 @@ function detectBorderRadius(element) {
|
|
|
460
463
|
const fitsTightly = Math.abs(outer.width - inner.width) <= 2 && Math.abs(outer.height - inner.height) <= 2;
|
|
461
464
|
return fitsTightly ? averageCornerRadius(child) : 0;
|
|
462
465
|
}
|
|
463
|
-
function
|
|
466
|
+
function screenScaleOf(element) {
|
|
467
|
+
const layoutWidth = (
|
|
468
|
+
/** @type {HTMLElement} */
|
|
469
|
+
element.offsetWidth
|
|
470
|
+
);
|
|
471
|
+
if (!layoutWidth) return 1;
|
|
472
|
+
const scale = element.getBoundingClientRect().width / layoutWidth;
|
|
473
|
+
return Number.isFinite(scale) && scale > 0.01 ? scale : 1;
|
|
474
|
+
}
|
|
475
|
+
function getElementBounds(element, padding = 0, radiusOverride = null, scale = 1) {
|
|
464
476
|
const rect = element.getBoundingClientRect();
|
|
465
477
|
const radius = radiusOverride != null ? radiusOverride : detectBorderRadius(element);
|
|
466
|
-
const width = rect.width + padding * 2;
|
|
467
|
-
const height = rect.height + padding * 2;
|
|
478
|
+
const width = rect.width / scale + padding * 2;
|
|
479
|
+
const height = rect.height / scale + padding * 2;
|
|
468
480
|
return {
|
|
469
|
-
x: rect.left - padding,
|
|
470
|
-
y: rect.top - padding,
|
|
481
|
+
x: rect.left / scale - padding,
|
|
482
|
+
y: rect.top / scale - padding,
|
|
471
483
|
width,
|
|
472
484
|
height,
|
|
473
485
|
// Grow the radius with the padding so the doodle stays concentric with the
|
|
@@ -1063,7 +1075,24 @@ function boundsOf(points) {
|
|
|
1063
1075
|
height: Math.max(...ys) - Math.min(...ys)
|
|
1064
1076
|
};
|
|
1065
1077
|
}
|
|
1066
|
-
function
|
|
1078
|
+
function resolveArrowStyle(base, options = {}) {
|
|
1079
|
+
const style = { ...base };
|
|
1080
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1081
|
+
style.color = options.color;
|
|
1082
|
+
}
|
|
1083
|
+
if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
|
|
1084
|
+
style.strokeWidth = options.strokeWidth;
|
|
1085
|
+
}
|
|
1086
|
+
if (Number.isFinite(options.roughness) && options.roughness >= 0) {
|
|
1087
|
+
style.roughness = options.roughness;
|
|
1088
|
+
}
|
|
1089
|
+
if (Number.isFinite(options.opacity)) {
|
|
1090
|
+
style.opacity = Math.min(1, Math.max(0, options.opacity));
|
|
1091
|
+
}
|
|
1092
|
+
return style;
|
|
1093
|
+
}
|
|
1094
|
+
function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
|
|
1095
|
+
const style = resolveArrowStyle(baseStyle, arrowOptions);
|
|
1067
1096
|
const random = createRandom(seed + 17);
|
|
1068
1097
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
1069
1098
|
const dx = end.x - start.x;
|
|
@@ -1183,17 +1212,69 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
1183
1212
|
);
|
|
1184
1213
|
}
|
|
1185
1214
|
var DEFAULT_WORDS_PER_LINE = 2;
|
|
1186
|
-
function wrapWords(text, perLine) {
|
|
1215
|
+
function wrapWords(text, perLine, marks = null) {
|
|
1216
|
+
const source = String(text);
|
|
1187
1217
|
const lines = [];
|
|
1188
|
-
|
|
1189
|
-
|
|
1218
|
+
let cursor = 0;
|
|
1219
|
+
for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
|
|
1220
|
+
const start = cursor;
|
|
1221
|
+
cursor += paragraph.length;
|
|
1222
|
+
if (index % 2 === 1) continue;
|
|
1223
|
+
const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
|
|
1224
|
+
text: m[0],
|
|
1225
|
+
at: start + m.index
|
|
1226
|
+
}));
|
|
1190
1227
|
if (!words.length) continue;
|
|
1191
1228
|
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
1192
1229
|
for (let i = 0; i < words.length; i += size) {
|
|
1193
|
-
lines.push(words.slice(i, i + size)
|
|
1230
|
+
lines.push(lineOf(words.slice(i, i + size), marks));
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
|
|
1234
|
+
}
|
|
1235
|
+
function lineOf(words, marks) {
|
|
1236
|
+
const text = words.map((w) => w.text).join(" ");
|
|
1237
|
+
const result = [];
|
|
1238
|
+
let previous = -1;
|
|
1239
|
+
words.forEach((word, index) => {
|
|
1240
|
+
const first = marks ? marks[word.at] ?? -1 : -1;
|
|
1241
|
+
if (index > 0) result.push(previous === first ? first : -1);
|
|
1242
|
+
for (let k = 0; k < word.text.length; k++) {
|
|
1243
|
+
result.push(marks ? marks[word.at + k] ?? -1 : -1);
|
|
1244
|
+
}
|
|
1245
|
+
previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
|
|
1246
|
+
});
|
|
1247
|
+
return { text, marks: result };
|
|
1248
|
+
}
|
|
1249
|
+
function normalizeHighlights(highlight) {
|
|
1250
|
+
const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
|
|
1251
|
+
return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
|
|
1252
|
+
}
|
|
1253
|
+
function highlightMarks(text, highlights) {
|
|
1254
|
+
if (!highlights.length) return null;
|
|
1255
|
+
const marks = new Int16Array(text.length).fill(-1);
|
|
1256
|
+
let any = false;
|
|
1257
|
+
highlights.forEach((highlight, index) => {
|
|
1258
|
+
for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
|
|
1259
|
+
for (let k = at; k < at + highlight.text.length; k++) {
|
|
1260
|
+
if (marks[k] === -1) {
|
|
1261
|
+
marks[k] = index;
|
|
1262
|
+
any = true;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1194
1265
|
}
|
|
1266
|
+
});
|
|
1267
|
+
return any ? marks : null;
|
|
1268
|
+
}
|
|
1269
|
+
function segmentsOf(line) {
|
|
1270
|
+
const segments = [];
|
|
1271
|
+
for (let i = 0; i < line.text.length; i++) {
|
|
1272
|
+
const highlight = line.marks[i] ?? -1;
|
|
1273
|
+
const last = segments[segments.length - 1];
|
|
1274
|
+
if (last && last.highlight === highlight) last.text += line.text[i];
|
|
1275
|
+
else segments.push({ text: line.text[i], highlight });
|
|
1195
1276
|
}
|
|
1196
|
-
return
|
|
1277
|
+
return segments;
|
|
1197
1278
|
}
|
|
1198
1279
|
function measureWidth(element, content, fontSize) {
|
|
1199
1280
|
try {
|
|
@@ -1210,7 +1291,13 @@ function measureWidth(element, content, fontSize) {
|
|
|
1210
1291
|
}
|
|
1211
1292
|
function createNoteText(svg, text, style, options = {}) {
|
|
1212
1293
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
1213
|
-
const
|
|
1294
|
+
const highlights = normalizeHighlights(options.highlight);
|
|
1295
|
+
const wrapped = wrapWords(
|
|
1296
|
+
text,
|
|
1297
|
+
options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
|
|
1298
|
+
highlightMarks(String(text), highlights)
|
|
1299
|
+
);
|
|
1300
|
+
const lines = wrapped.map((line) => line.text);
|
|
1214
1301
|
const lineHeight = fontSize * 1.05;
|
|
1215
1302
|
const rtl = options.rtl ?? isRtlText(text);
|
|
1216
1303
|
const group = createSvgElement("g");
|
|
@@ -1221,7 +1308,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1221
1308
|
textEl.setAttribute("opacity", String(style.opacity));
|
|
1222
1309
|
textEl.setAttribute("font-size", String(fontSize));
|
|
1223
1310
|
textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
|
|
1224
|
-
textEl.setAttribute("font-weight", "600");
|
|
1311
|
+
textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
|
|
1225
1312
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
1226
1313
|
textEl.setAttribute("text-anchor", "start");
|
|
1227
1314
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
@@ -1232,11 +1319,25 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1232
1319
|
if (options.locale) {
|
|
1233
1320
|
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1234
1321
|
}
|
|
1235
|
-
const tspans =
|
|
1322
|
+
const tspans = wrapped.map((line, index) => {
|
|
1236
1323
|
const tspan = createSvgElement("tspan");
|
|
1237
|
-
tspan.textContent = line;
|
|
1238
1324
|
tspan.setAttribute("x", "0");
|
|
1239
1325
|
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
1326
|
+
const segments = segmentsOf(line);
|
|
1327
|
+
if (segments.every((segment) => segment.highlight === -1)) {
|
|
1328
|
+
tspan.textContent = line.text;
|
|
1329
|
+
} else {
|
|
1330
|
+
for (const segment of segments) {
|
|
1331
|
+
const run = createSvgElement("tspan");
|
|
1332
|
+
run.textContent = segment.text;
|
|
1333
|
+
const highlight = highlights[segment.highlight];
|
|
1334
|
+
if (highlight?.color) run.setAttribute("fill", highlight.color);
|
|
1335
|
+
if (highlight?.fontWeight != null) {
|
|
1336
|
+
run.setAttribute("font-weight", String(highlight.fontWeight));
|
|
1337
|
+
}
|
|
1338
|
+
tspan.appendChild(run);
|
|
1339
|
+
}
|
|
1340
|
+
}
|
|
1240
1341
|
textEl.appendChild(tspan);
|
|
1241
1342
|
return tspan;
|
|
1242
1343
|
});
|
|
@@ -1355,7 +1456,21 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1355
1456
|
if (!dx) return box;
|
|
1356
1457
|
return { ...box, x: box.x + dx };
|
|
1357
1458
|
}
|
|
1358
|
-
function
|
|
1459
|
+
function resolveNoteStyle(base, options = {}) {
|
|
1460
|
+
const style = { ...base };
|
|
1461
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1462
|
+
style.color = options.color;
|
|
1463
|
+
}
|
|
1464
|
+
if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
|
|
1465
|
+
style.fontSize = options.fontSize;
|
|
1466
|
+
}
|
|
1467
|
+
if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
|
|
1468
|
+
style.fontWeight = options.fontWeight;
|
|
1469
|
+
}
|
|
1470
|
+
return style;
|
|
1471
|
+
}
|
|
1472
|
+
function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
|
|
1473
|
+
const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
|
|
1359
1474
|
const { text, locale } = resolveLocalizedText(
|
|
1360
1475
|
noteOptions?.text,
|
|
1361
1476
|
noteOptions?.locale
|
|
@@ -1366,9 +1481,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
|
|
|
1366
1481
|
const random = createRandom(seed + 9);
|
|
1367
1482
|
const note = createNoteText(svg, text, style, {
|
|
1368
1483
|
wordsPerLine: noteOptions.wordsPerLine,
|
|
1369
|
-
locale
|
|
1484
|
+
locale,
|
|
1485
|
+
highlight: noteOptions.highlight
|
|
1370
1486
|
});
|
|
1371
|
-
const
|
|
1487
|
+
const randomTilt = (random() - 0.45) * 7;
|
|
1488
|
+
const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
|
|
1372
1489
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
1373
1490
|
const size = {
|
|
1374
1491
|
width: note.width,
|
|
@@ -1840,14 +1957,14 @@ var instances = /* @__PURE__ */ new WeakMap();
|
|
|
1840
1957
|
function now() {
|
|
1841
1958
|
return typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
|
|
1842
1959
|
}
|
|
1843
|
-
function localBand(originLeft, inset = 8) {
|
|
1960
|
+
function localBand(originLeft, inset = 8, scale = 1) {
|
|
1844
1961
|
const width = window.innerWidth || document.documentElement.clientWidth || 0;
|
|
1845
1962
|
return {
|
|
1846
|
-
x: -originLeft + inset,
|
|
1847
|
-
width: Math.max(0, width - inset * 2)
|
|
1963
|
+
x: -originLeft + inset / scale,
|
|
1964
|
+
width: Math.max(0, width - inset * 2) / scale
|
|
1848
1965
|
};
|
|
1849
1966
|
}
|
|
1850
|
-
function paddingBox(element) {
|
|
1967
|
+
function paddingBox(element, scale = 1) {
|
|
1851
1968
|
const rect = element.getBoundingClientRect();
|
|
1852
1969
|
const style = getComputedStyle(element);
|
|
1853
1970
|
const left = parseFloat(style.borderLeftWidth) || 0;
|
|
@@ -1855,10 +1972,10 @@ function paddingBox(element) {
|
|
|
1855
1972
|
const right = parseFloat(style.borderRightWidth) || 0;
|
|
1856
1973
|
const bottom = parseFloat(style.borderBottomWidth) || 0;
|
|
1857
1974
|
return {
|
|
1858
|
-
left: rect.left + left,
|
|
1859
|
-
top: rect.top + top,
|
|
1860
|
-
width: Math.max(0, rect.width - left - right),
|
|
1861
|
-
height: Math.max(0, rect.height - top - bottom)
|
|
1975
|
+
left: rect.left / scale + left,
|
|
1976
|
+
top: rect.top / scale + top,
|
|
1977
|
+
width: Math.max(0, rect.width / scale - left - right),
|
|
1978
|
+
height: Math.max(0, rect.height / scale - top - bottom)
|
|
1862
1979
|
};
|
|
1863
1980
|
}
|
|
1864
1981
|
var DoodleOverlay = class {
|
|
@@ -1947,12 +2064,13 @@ var DoodleOverlay = class {
|
|
|
1947
2064
|
const { padding, radius, color, strokeWidth, roughness, opacity } = this.options;
|
|
1948
2065
|
const targets = this.getTargets();
|
|
1949
2066
|
const elapsed = now() - this.startedAt;
|
|
2067
|
+
const scale = screenScaleOf(this.element);
|
|
1950
2068
|
const absoluteRects = targets.map(
|
|
1951
|
-
(target) => getElementBounds(target, padding, radius)
|
|
2069
|
+
(target) => getElementBounds(target, padding, radius, scale)
|
|
1952
2070
|
);
|
|
1953
2071
|
clearSvg(this.svg);
|
|
1954
2072
|
syncOverlayStacking(this.svg, this.element, this.options.zIndex);
|
|
1955
|
-
const box = paddingBox(this.element);
|
|
2073
|
+
const box = paddingBox(this.element, scale);
|
|
1956
2074
|
const width = Math.max(1, box.width);
|
|
1957
2075
|
const height = Math.max(1, box.height);
|
|
1958
2076
|
const origin = { x: box.left, y: box.top };
|
|
@@ -1973,7 +2091,7 @@ var DoodleOverlay = class {
|
|
|
1973
2091
|
};
|
|
1974
2092
|
const noteGroups = [];
|
|
1975
2093
|
const noteLayout = {
|
|
1976
|
-
band: localBand(origin.x),
|
|
2094
|
+
band: localBand(origin.x, 8, scale),
|
|
1977
2095
|
gap: this.options.arrow ? 40 : 14
|
|
1978
2096
|
};
|
|
1979
2097
|
targets.forEach((target, index) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aznabee/freehand-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Wrap any existing web element with a beautiful, responsive, hand-drawn doodle layer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/freehand-ui.cjs",
|
|
@@ -36,7 +36,9 @@
|
|
|
36
36
|
"build:example-react": "node scripts/build-example-react.js",
|
|
37
37
|
"dev": "node scripts/build.js && node scripts/build-example-react.js && node scripts/serve.js",
|
|
38
38
|
"test": "node --test tests/*.test.js",
|
|
39
|
-
"prepublishOnly": "npm run build && npm test"
|
|
39
|
+
"prepublishOnly": "npm run build && npm test",
|
|
40
|
+
"typecheck": "tsc --noEmit --strict --skipLibCheck --jsx react-jsx --moduleResolution bundler --module esnext src/index.d.ts src/react.d.ts",
|
|
41
|
+
"check": "npm run typecheck && npm test && npm run build"
|
|
40
42
|
},
|
|
41
43
|
"author": "Lakshay Gupta",
|
|
42
44
|
"repository": {
|
|
@@ -76,8 +78,14 @@
|
|
|
76
78
|
}
|
|
77
79
|
},
|
|
78
80
|
"devDependencies": {
|
|
81
|
+
"@types/react": "^19.3.0",
|
|
82
|
+
"@types/react-dom": "^19.3.0",
|
|
79
83
|
"esbuild": "^0.25.0",
|
|
80
84
|
"react": "^19.0.0",
|
|
81
|
-
"react-dom": "^19.0.0"
|
|
85
|
+
"react-dom": "^19.0.0",
|
|
86
|
+
"typescript": "^7.0.2"
|
|
87
|
+
},
|
|
88
|
+
"engines": {
|
|
89
|
+
"node": ">=18"
|
|
82
90
|
}
|
|
83
91
|
}
|