@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/index.d.ts
CHANGED
|
@@ -28,7 +28,25 @@ export type DecorationType =
|
|
|
28
28
|
*/
|
|
29
29
|
export type LocalizedText = string | Record<string, string>;
|
|
30
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Words of a note drawn in a colour (and weight) of their own. Every
|
|
33
|
+
* occurrence of `text` in the note is painted, including one that wraps onto
|
|
34
|
+
* the next line. Where two highlights overlap, the earlier one wins.
|
|
35
|
+
*/
|
|
36
|
+
export interface NoteHighlight {
|
|
37
|
+
/** The words to find, exactly as written in the note. */
|
|
38
|
+
text: string;
|
|
39
|
+
/** Any CSS colour. Omit to keep the note's own colour. */
|
|
40
|
+
color?: string;
|
|
41
|
+
/** e.g. `700`. Omit to keep the note's own weight. */
|
|
42
|
+
fontWeight?: number | string;
|
|
43
|
+
}
|
|
44
|
+
|
|
31
45
|
export interface NoteOptions {
|
|
46
|
+
/**
|
|
47
|
+
* What the note says. A line break (`\n`) starts a new line; see
|
|
48
|
+
* `wordsPerLine` for how the rest is wrapped.
|
|
49
|
+
*/
|
|
32
50
|
text: LocalizedText;
|
|
33
51
|
/**
|
|
34
52
|
* Which side of the element the note sits on. Always honoured - a note that
|
|
@@ -49,6 +67,27 @@ export interface NoteOptions {
|
|
|
49
67
|
* visitor's own languages. Only meaningful when `text` is a map.
|
|
50
68
|
*/
|
|
51
69
|
locale?: string | string[] | null;
|
|
70
|
+
/**
|
|
71
|
+
* Colour of the handwriting and its underline. Defaults to the doodle's own
|
|
72
|
+
* `color`.
|
|
73
|
+
*/
|
|
74
|
+
color?: string;
|
|
75
|
+
/** Size of the handwriting, px. Default `19`. */
|
|
76
|
+
fontSize?: number;
|
|
77
|
+
/** Weight of the handwriting. Default `600`. */
|
|
78
|
+
fontWeight?: number | string;
|
|
79
|
+
/**
|
|
80
|
+
* Rotate the note by exactly this many degrees (negative leans it up to the
|
|
81
|
+
* right). Omit for a small random tilt, different for each note.
|
|
82
|
+
*/
|
|
83
|
+
tilt?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Words per line. Default `2`. Use `0` to break only where the text has a
|
|
86
|
+
* line break (`\n`).
|
|
87
|
+
*/
|
|
88
|
+
wordsPerLine?: number;
|
|
89
|
+
/** Paint some of the words in another colour, e.g. `{ text: "Game Mode!", color: "#ffc83d" }`. */
|
|
90
|
+
highlight?: NoteHighlight | NoteHighlight[];
|
|
52
91
|
}
|
|
53
92
|
|
|
54
93
|
export interface ArrowAnimationOptions {
|
|
@@ -85,6 +124,14 @@ export interface ArrowOptions {
|
|
|
85
124
|
/** `"edge"` lands just outside the frame; `"center"` points into it. */
|
|
86
125
|
to?: DoodlePosition | "edge";
|
|
87
126
|
style?: "curved" | "straight" | "dotted" | "looped";
|
|
127
|
+
/** Colour of the arrow. Defaults to the doodle's own `color`. */
|
|
128
|
+
color?: string;
|
|
129
|
+
/** Thickness of the arrow's line, px. Defaults to the doodle's `strokeWidth`. */
|
|
130
|
+
strokeWidth?: number;
|
|
131
|
+
/** How shaky the pen is. Defaults to the doodle's `roughness`. */
|
|
132
|
+
roughness?: number;
|
|
133
|
+
/** `0`-`1`. Defaults to the doodle's `opacity`. */
|
|
134
|
+
opacity?: number;
|
|
88
135
|
/**
|
|
89
136
|
* Motion for the arrow. On by default - the shaft draws itself on and the
|
|
90
137
|
* whole mark leans toward its target. `false` draws it static. Respects
|
package/dist/native.cjs
CHANGED
|
@@ -342,6 +342,9 @@ function mergeOptions(options = {}) {
|
|
|
342
342
|
// `animate: false` opts out, an object tunes speed and gestures
|
|
343
343
|
animate: normalizeArrowAnimation(config.animate)
|
|
344
344
|
};
|
|
345
|
+
for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
|
|
346
|
+
if (config[key] !== void 0) merged.arrow[key] = config[key];
|
|
347
|
+
}
|
|
345
348
|
}
|
|
346
349
|
if (merged.decorations === true) {
|
|
347
350
|
merged.decorations = { count: null, types: null };
|
|
@@ -1269,7 +1272,24 @@ function boundsOf(points) {
|
|
|
1269
1272
|
height: Math.max(...ys) - Math.min(...ys)
|
|
1270
1273
|
};
|
|
1271
1274
|
}
|
|
1272
|
-
function
|
|
1275
|
+
function resolveArrowStyle(base, options = {}) {
|
|
1276
|
+
const style = { ...base };
|
|
1277
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1278
|
+
style.color = options.color;
|
|
1279
|
+
}
|
|
1280
|
+
if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
|
|
1281
|
+
style.strokeWidth = options.strokeWidth;
|
|
1282
|
+
}
|
|
1283
|
+
if (Number.isFinite(options.roughness) && options.roughness >= 0) {
|
|
1284
|
+
style.roughness = options.roughness;
|
|
1285
|
+
}
|
|
1286
|
+
if (Number.isFinite(options.opacity)) {
|
|
1287
|
+
style.opacity = Math.min(1, Math.max(0, options.opacity));
|
|
1288
|
+
}
|
|
1289
|
+
return style;
|
|
1290
|
+
}
|
|
1291
|
+
function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
|
|
1292
|
+
const style = resolveArrowStyle(baseStyle, arrowOptions);
|
|
1273
1293
|
const random = createRandom(seed + 17);
|
|
1274
1294
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
1275
1295
|
const dx = end.x - start.x;
|
|
@@ -1389,17 +1409,69 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
1389
1409
|
);
|
|
1390
1410
|
}
|
|
1391
1411
|
var DEFAULT_WORDS_PER_LINE = 2;
|
|
1392
|
-
function wrapWords(text, perLine) {
|
|
1412
|
+
function wrapWords(text, perLine, marks = null) {
|
|
1413
|
+
const source = String(text);
|
|
1393
1414
|
const lines = [];
|
|
1394
|
-
|
|
1395
|
-
|
|
1415
|
+
let cursor = 0;
|
|
1416
|
+
for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
|
|
1417
|
+
const start = cursor;
|
|
1418
|
+
cursor += paragraph.length;
|
|
1419
|
+
if (index % 2 === 1) continue;
|
|
1420
|
+
const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
|
|
1421
|
+
text: m[0],
|
|
1422
|
+
at: start + m.index
|
|
1423
|
+
}));
|
|
1396
1424
|
if (!words.length) continue;
|
|
1397
1425
|
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
1398
1426
|
for (let i = 0; i < words.length; i += size) {
|
|
1399
|
-
lines.push(words.slice(i, i + size)
|
|
1427
|
+
lines.push(lineOf(words.slice(i, i + size), marks));
|
|
1400
1428
|
}
|
|
1401
1429
|
}
|
|
1402
|
-
return lines.length ? lines : [
|
|
1430
|
+
return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
|
|
1431
|
+
}
|
|
1432
|
+
function lineOf(words, marks) {
|
|
1433
|
+
const text = words.map((w) => w.text).join(" ");
|
|
1434
|
+
const result = [];
|
|
1435
|
+
let previous = -1;
|
|
1436
|
+
words.forEach((word, index) => {
|
|
1437
|
+
const first = marks ? marks[word.at] ?? -1 : -1;
|
|
1438
|
+
if (index > 0) result.push(previous === first ? first : -1);
|
|
1439
|
+
for (let k = 0; k < word.text.length; k++) {
|
|
1440
|
+
result.push(marks ? marks[word.at + k] ?? -1 : -1);
|
|
1441
|
+
}
|
|
1442
|
+
previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
|
|
1443
|
+
});
|
|
1444
|
+
return { text, marks: result };
|
|
1445
|
+
}
|
|
1446
|
+
function normalizeHighlights(highlight) {
|
|
1447
|
+
const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
|
|
1448
|
+
return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
|
|
1449
|
+
}
|
|
1450
|
+
function highlightMarks(text, highlights) {
|
|
1451
|
+
if (!highlights.length) return null;
|
|
1452
|
+
const marks = new Int16Array(text.length).fill(-1);
|
|
1453
|
+
let any = false;
|
|
1454
|
+
highlights.forEach((highlight, index) => {
|
|
1455
|
+
for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
|
|
1456
|
+
for (let k = at; k < at + highlight.text.length; k++) {
|
|
1457
|
+
if (marks[k] === -1) {
|
|
1458
|
+
marks[k] = index;
|
|
1459
|
+
any = true;
|
|
1460
|
+
}
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
});
|
|
1464
|
+
return any ? marks : null;
|
|
1465
|
+
}
|
|
1466
|
+
function segmentsOf(line) {
|
|
1467
|
+
const segments = [];
|
|
1468
|
+
for (let i = 0; i < line.text.length; i++) {
|
|
1469
|
+
const highlight = line.marks[i] ?? -1;
|
|
1470
|
+
const last = segments[segments.length - 1];
|
|
1471
|
+
if (last && last.highlight === highlight) last.text += line.text[i];
|
|
1472
|
+
else segments.push({ text: line.text[i], highlight });
|
|
1473
|
+
}
|
|
1474
|
+
return segments;
|
|
1403
1475
|
}
|
|
1404
1476
|
function measureWidth(element, content, fontSize) {
|
|
1405
1477
|
try {
|
|
@@ -1416,7 +1488,13 @@ function measureWidth(element, content, fontSize) {
|
|
|
1416
1488
|
}
|
|
1417
1489
|
function createNoteText(svg, text, style, options = {}) {
|
|
1418
1490
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
1419
|
-
const
|
|
1491
|
+
const highlights = normalizeHighlights(options.highlight);
|
|
1492
|
+
const wrapped = wrapWords(
|
|
1493
|
+
text,
|
|
1494
|
+
options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
|
|
1495
|
+
highlightMarks(String(text), highlights)
|
|
1496
|
+
);
|
|
1497
|
+
const lines = wrapped.map((line) => line.text);
|
|
1420
1498
|
const lineHeight = fontSize * 1.05;
|
|
1421
1499
|
const rtl = options.rtl ?? isRtlText(text);
|
|
1422
1500
|
const group = createSvgElement("g");
|
|
@@ -1427,7 +1505,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1427
1505
|
textEl.setAttribute("opacity", String(style.opacity));
|
|
1428
1506
|
textEl.setAttribute("font-size", String(fontSize));
|
|
1429
1507
|
textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
|
|
1430
|
-
textEl.setAttribute("font-weight", "600");
|
|
1508
|
+
textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
|
|
1431
1509
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
1432
1510
|
textEl.setAttribute("text-anchor", "start");
|
|
1433
1511
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
@@ -1438,11 +1516,25 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1438
1516
|
if (options.locale) {
|
|
1439
1517
|
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1440
1518
|
}
|
|
1441
|
-
const tspans =
|
|
1519
|
+
const tspans = wrapped.map((line, index) => {
|
|
1442
1520
|
const tspan = createSvgElement("tspan");
|
|
1443
|
-
tspan.textContent = line;
|
|
1444
1521
|
tspan.setAttribute("x", "0");
|
|
1445
1522
|
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
1523
|
+
const segments = segmentsOf(line);
|
|
1524
|
+
if (segments.every((segment) => segment.highlight === -1)) {
|
|
1525
|
+
tspan.textContent = line.text;
|
|
1526
|
+
} else {
|
|
1527
|
+
for (const segment of segments) {
|
|
1528
|
+
const run = createSvgElement("tspan");
|
|
1529
|
+
run.textContent = segment.text;
|
|
1530
|
+
const highlight = highlights[segment.highlight];
|
|
1531
|
+
if (highlight?.color) run.setAttribute("fill", highlight.color);
|
|
1532
|
+
if (highlight?.fontWeight != null) {
|
|
1533
|
+
run.setAttribute("font-weight", String(highlight.fontWeight));
|
|
1534
|
+
}
|
|
1535
|
+
tspan.appendChild(run);
|
|
1536
|
+
}
|
|
1537
|
+
}
|
|
1446
1538
|
textEl.appendChild(tspan);
|
|
1447
1539
|
return tspan;
|
|
1448
1540
|
});
|
|
@@ -1545,7 +1637,21 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1545
1637
|
if (!dx) return box;
|
|
1546
1638
|
return { ...box, x: box.x + dx };
|
|
1547
1639
|
}
|
|
1548
|
-
function
|
|
1640
|
+
function resolveNoteStyle(base, options = {}) {
|
|
1641
|
+
const style = { ...base };
|
|
1642
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1643
|
+
style.color = options.color;
|
|
1644
|
+
}
|
|
1645
|
+
if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
|
|
1646
|
+
style.fontSize = options.fontSize;
|
|
1647
|
+
}
|
|
1648
|
+
if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
|
|
1649
|
+
style.fontWeight = options.fontWeight;
|
|
1650
|
+
}
|
|
1651
|
+
return style;
|
|
1652
|
+
}
|
|
1653
|
+
function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
|
|
1654
|
+
const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
|
|
1549
1655
|
const { text, locale } = resolveLocalizedText(
|
|
1550
1656
|
noteOptions?.text,
|
|
1551
1657
|
noteOptions?.locale
|
|
@@ -1556,9 +1662,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
|
|
|
1556
1662
|
const random = createRandom(seed + 9);
|
|
1557
1663
|
const note = createNoteText(svg, text, style, {
|
|
1558
1664
|
wordsPerLine: noteOptions.wordsPerLine,
|
|
1559
|
-
locale
|
|
1665
|
+
locale,
|
|
1666
|
+
highlight: noteOptions.highlight
|
|
1560
1667
|
});
|
|
1561
|
-
const
|
|
1668
|
+
const randomTilt = (random() - 0.45) * 7;
|
|
1669
|
+
const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
|
|
1562
1670
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
1563
1671
|
const size = {
|
|
1564
1672
|
width: note.width,
|
|
@@ -2156,7 +2264,16 @@ function blurOf(node) {
|
|
|
2156
2264
|
}
|
|
2157
2265
|
function renderNode(node, key, context) {
|
|
2158
2266
|
const props = { key, ...propsOf(node) };
|
|
2159
|
-
if (node.tagName === "tspan")
|
|
2267
|
+
if (node.tagName === "tspan") {
|
|
2268
|
+
if (node.children.length) {
|
|
2269
|
+
return (0, import_react.createElement)(
|
|
2270
|
+
import_react_native_svg.TSpan,
|
|
2271
|
+
props,
|
|
2272
|
+
...node.children.map((child, index) => renderNode(child, index, context))
|
|
2273
|
+
);
|
|
2274
|
+
}
|
|
2275
|
+
return (0, import_react.createElement)(import_react_native_svg.TSpan, props, node.textContent);
|
|
2276
|
+
}
|
|
2160
2277
|
if (node.tagName === "circle" && blurOf(node) > 0) {
|
|
2161
2278
|
const id = `${context.prefix}-glow-${context.defs.length}`;
|
|
2162
2279
|
const color = props.fill;
|
package/dist/native.d.ts
CHANGED
package/dist/native.js
CHANGED
|
@@ -343,6 +343,9 @@ function mergeOptions(options = {}) {
|
|
|
343
343
|
// `animate: false` opts out, an object tunes speed and gestures
|
|
344
344
|
animate: normalizeArrowAnimation(config.animate)
|
|
345
345
|
};
|
|
346
|
+
for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
|
|
347
|
+
if (config[key] !== void 0) merged.arrow[key] = config[key];
|
|
348
|
+
}
|
|
346
349
|
}
|
|
347
350
|
if (merged.decorations === true) {
|
|
348
351
|
merged.decorations = { count: null, types: null };
|
|
@@ -1270,7 +1273,24 @@ function boundsOf(points) {
|
|
|
1270
1273
|
height: Math.max(...ys) - Math.min(...ys)
|
|
1271
1274
|
};
|
|
1272
1275
|
}
|
|
1273
|
-
function
|
|
1276
|
+
function resolveArrowStyle(base, options = {}) {
|
|
1277
|
+
const style = { ...base };
|
|
1278
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1279
|
+
style.color = options.color;
|
|
1280
|
+
}
|
|
1281
|
+
if (Number.isFinite(options.strokeWidth) && options.strokeWidth > 0) {
|
|
1282
|
+
style.strokeWidth = options.strokeWidth;
|
|
1283
|
+
}
|
|
1284
|
+
if (Number.isFinite(options.roughness) && options.roughness >= 0) {
|
|
1285
|
+
style.roughness = options.roughness;
|
|
1286
|
+
}
|
|
1287
|
+
if (Number.isFinite(options.opacity)) {
|
|
1288
|
+
style.opacity = Math.min(1, Math.max(0, options.opacity));
|
|
1289
|
+
}
|
|
1290
|
+
return style;
|
|
1291
|
+
}
|
|
1292
|
+
function drawArrow(svg, rect, arrowOptions, baseStyle, seed, origin = null, context = {}) {
|
|
1293
|
+
const style = resolveArrowStyle(baseStyle, arrowOptions);
|
|
1274
1294
|
const random = createRandom(seed + 17);
|
|
1275
1295
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
1276
1296
|
const dx = end.x - start.x;
|
|
@@ -1390,17 +1410,69 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
1390
1410
|
);
|
|
1391
1411
|
}
|
|
1392
1412
|
var DEFAULT_WORDS_PER_LINE = 2;
|
|
1393
|
-
function wrapWords(text, perLine) {
|
|
1413
|
+
function wrapWords(text, perLine, marks = null) {
|
|
1414
|
+
const source = String(text);
|
|
1394
1415
|
const lines = [];
|
|
1395
|
-
|
|
1396
|
-
|
|
1416
|
+
let cursor = 0;
|
|
1417
|
+
for (const [index, paragraph] of source.split(/(\r?\n)/).entries()) {
|
|
1418
|
+
const start = cursor;
|
|
1419
|
+
cursor += paragraph.length;
|
|
1420
|
+
if (index % 2 === 1) continue;
|
|
1421
|
+
const words = [...paragraph.matchAll(/\S+/g)].map((m) => ({
|
|
1422
|
+
text: m[0],
|
|
1423
|
+
at: start + m.index
|
|
1424
|
+
}));
|
|
1397
1425
|
if (!words.length) continue;
|
|
1398
1426
|
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
1399
1427
|
for (let i = 0; i < words.length; i += size) {
|
|
1400
|
-
lines.push(words.slice(i, i + size)
|
|
1428
|
+
lines.push(lineOf(words.slice(i, i + size), marks));
|
|
1401
1429
|
}
|
|
1402
1430
|
}
|
|
1403
|
-
return lines.length ? lines : [
|
|
1431
|
+
return lines.length ? lines : [lineOf([{ text: source, at: 0 }], marks)];
|
|
1432
|
+
}
|
|
1433
|
+
function lineOf(words, marks) {
|
|
1434
|
+
const text = words.map((w) => w.text).join(" ");
|
|
1435
|
+
const result = [];
|
|
1436
|
+
let previous = -1;
|
|
1437
|
+
words.forEach((word, index) => {
|
|
1438
|
+
const first = marks ? marks[word.at] ?? -1 : -1;
|
|
1439
|
+
if (index > 0) result.push(previous === first ? first : -1);
|
|
1440
|
+
for (let k = 0; k < word.text.length; k++) {
|
|
1441
|
+
result.push(marks ? marks[word.at + k] ?? -1 : -1);
|
|
1442
|
+
}
|
|
1443
|
+
previous = marks ? marks[word.at + word.text.length - 1] ?? -1 : -1;
|
|
1444
|
+
});
|
|
1445
|
+
return { text, marks: result };
|
|
1446
|
+
}
|
|
1447
|
+
function normalizeHighlights(highlight) {
|
|
1448
|
+
const list = Array.isArray(highlight) ? highlight : highlight ? [highlight] : [];
|
|
1449
|
+
return list.map((entry) => typeof entry === "string" ? { text: entry } : entry).filter((entry) => entry && typeof entry.text === "string" && entry.text);
|
|
1450
|
+
}
|
|
1451
|
+
function highlightMarks(text, highlights) {
|
|
1452
|
+
if (!highlights.length) return null;
|
|
1453
|
+
const marks = new Int16Array(text.length).fill(-1);
|
|
1454
|
+
let any = false;
|
|
1455
|
+
highlights.forEach((highlight, index) => {
|
|
1456
|
+
for (let at = text.indexOf(highlight.text); at !== -1; at = text.indexOf(highlight.text, at + highlight.text.length)) {
|
|
1457
|
+
for (let k = at; k < at + highlight.text.length; k++) {
|
|
1458
|
+
if (marks[k] === -1) {
|
|
1459
|
+
marks[k] = index;
|
|
1460
|
+
any = true;
|
|
1461
|
+
}
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
});
|
|
1465
|
+
return any ? marks : null;
|
|
1466
|
+
}
|
|
1467
|
+
function segmentsOf(line) {
|
|
1468
|
+
const segments = [];
|
|
1469
|
+
for (let i = 0; i < line.text.length; i++) {
|
|
1470
|
+
const highlight = line.marks[i] ?? -1;
|
|
1471
|
+
const last = segments[segments.length - 1];
|
|
1472
|
+
if (last && last.highlight === highlight) last.text += line.text[i];
|
|
1473
|
+
else segments.push({ text: line.text[i], highlight });
|
|
1474
|
+
}
|
|
1475
|
+
return segments;
|
|
1404
1476
|
}
|
|
1405
1477
|
function measureWidth(element, content, fontSize) {
|
|
1406
1478
|
try {
|
|
@@ -1417,7 +1489,13 @@ function measureWidth(element, content, fontSize) {
|
|
|
1417
1489
|
}
|
|
1418
1490
|
function createNoteText(svg, text, style, options = {}) {
|
|
1419
1491
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
1420
|
-
const
|
|
1492
|
+
const highlights = normalizeHighlights(options.highlight);
|
|
1493
|
+
const wrapped = wrapWords(
|
|
1494
|
+
text,
|
|
1495
|
+
options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE,
|
|
1496
|
+
highlightMarks(String(text), highlights)
|
|
1497
|
+
);
|
|
1498
|
+
const lines = wrapped.map((line) => line.text);
|
|
1421
1499
|
const lineHeight = fontSize * 1.05;
|
|
1422
1500
|
const rtl = options.rtl ?? isRtlText(text);
|
|
1423
1501
|
const group = createSvgElement("g");
|
|
@@ -1428,7 +1506,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1428
1506
|
textEl.setAttribute("opacity", String(style.opacity));
|
|
1429
1507
|
textEl.setAttribute("font-size", String(fontSize));
|
|
1430
1508
|
textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
|
|
1431
|
-
textEl.setAttribute("font-weight", "600");
|
|
1509
|
+
textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
|
|
1432
1510
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
1433
1511
|
textEl.setAttribute("text-anchor", "start");
|
|
1434
1512
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
@@ -1439,11 +1517,25 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1439
1517
|
if (options.locale) {
|
|
1440
1518
|
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1441
1519
|
}
|
|
1442
|
-
const tspans =
|
|
1520
|
+
const tspans = wrapped.map((line, index) => {
|
|
1443
1521
|
const tspan = createSvgElement("tspan");
|
|
1444
|
-
tspan.textContent = line;
|
|
1445
1522
|
tspan.setAttribute("x", "0");
|
|
1446
1523
|
tspan.setAttribute("dy", index === 0 ? "0" : String(lineHeight));
|
|
1524
|
+
const segments = segmentsOf(line);
|
|
1525
|
+
if (segments.every((segment) => segment.highlight === -1)) {
|
|
1526
|
+
tspan.textContent = line.text;
|
|
1527
|
+
} else {
|
|
1528
|
+
for (const segment of segments) {
|
|
1529
|
+
const run = createSvgElement("tspan");
|
|
1530
|
+
run.textContent = segment.text;
|
|
1531
|
+
const highlight = highlights[segment.highlight];
|
|
1532
|
+
if (highlight?.color) run.setAttribute("fill", highlight.color);
|
|
1533
|
+
if (highlight?.fontWeight != null) {
|
|
1534
|
+
run.setAttribute("font-weight", String(highlight.fontWeight));
|
|
1535
|
+
}
|
|
1536
|
+
tspan.appendChild(run);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1447
1539
|
textEl.appendChild(tspan);
|
|
1448
1540
|
return tspan;
|
|
1449
1541
|
});
|
|
@@ -1546,7 +1638,21 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1546
1638
|
if (!dx) return box;
|
|
1547
1639
|
return { ...box, x: box.x + dx };
|
|
1548
1640
|
}
|
|
1549
|
-
function
|
|
1641
|
+
function resolveNoteStyle(base, options = {}) {
|
|
1642
|
+
const style = { ...base };
|
|
1643
|
+
if (typeof options.color === "string" && options.color.trim()) {
|
|
1644
|
+
style.color = options.color;
|
|
1645
|
+
}
|
|
1646
|
+
if (Number.isFinite(options.fontSize) && options.fontSize > 0) {
|
|
1647
|
+
style.fontSize = options.fontSize;
|
|
1648
|
+
}
|
|
1649
|
+
if (typeof options.fontWeight === "number" && options.fontWeight > 0 || typeof options.fontWeight === "string" && options.fontWeight.trim()) {
|
|
1650
|
+
style.fontWeight = options.fontWeight;
|
|
1651
|
+
}
|
|
1652
|
+
return style;
|
|
1653
|
+
}
|
|
1654
|
+
function renderAnnotation(svg, rect, noteOptions, baseStyle, seed = 1, layout = {}) {
|
|
1655
|
+
const style = resolveNoteStyle(baseStyle, noteOptions ?? {});
|
|
1550
1656
|
const { text, locale } = resolveLocalizedText(
|
|
1551
1657
|
noteOptions?.text,
|
|
1552
1658
|
noteOptions?.locale
|
|
@@ -1557,9 +1663,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
|
|
|
1557
1663
|
const random = createRandom(seed + 9);
|
|
1558
1664
|
const note = createNoteText(svg, text, style, {
|
|
1559
1665
|
wordsPerLine: noteOptions.wordsPerLine,
|
|
1560
|
-
locale
|
|
1666
|
+
locale,
|
|
1667
|
+
highlight: noteOptions.highlight
|
|
1561
1668
|
});
|
|
1562
|
-
const
|
|
1669
|
+
const randomTilt = (random() - 0.45) * 7;
|
|
1670
|
+
const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
|
|
1563
1671
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
1564
1672
|
const size = {
|
|
1565
1673
|
width: note.width,
|
|
@@ -2157,7 +2265,16 @@ function blurOf(node) {
|
|
|
2157
2265
|
}
|
|
2158
2266
|
function renderNode(node, key, context) {
|
|
2159
2267
|
const props = { key, ...propsOf(node) };
|
|
2160
|
-
if (node.tagName === "tspan")
|
|
2268
|
+
if (node.tagName === "tspan") {
|
|
2269
|
+
if (node.children.length) {
|
|
2270
|
+
return h(
|
|
2271
|
+
TSpan,
|
|
2272
|
+
props,
|
|
2273
|
+
...node.children.map((child, index) => renderNode(child, index, context))
|
|
2274
|
+
);
|
|
2275
|
+
}
|
|
2276
|
+
return h(TSpan, props, node.textContent);
|
|
2277
|
+
}
|
|
2161
2278
|
if (node.tagName === "circle" && blurOf(node) > 0) {
|
|
2162
2279
|
const id = `${context.prefix}-glow-${context.defs.length}`;
|
|
2163
2280
|
const color = props.fill;
|