@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/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 drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
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
- for (const paragraph of String(text).split(/\r?\n/)) {
1395
- const words = paragraph.trim().split(/\s+/).filter(Boolean);
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).join(" "));
1427
+ lines.push(lineOf(words.slice(i, i + size), marks));
1400
1428
  }
1401
1429
  }
1402
- return lines.length ? lines : [String(text)];
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 lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
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 = lines.map((line, index) => {
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 renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
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 tilt = (random() - 0.45) * 7;
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") return (0, import_react.createElement)(import_react_native_svg.TSpan, props, node.textContent);
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
@@ -8,6 +8,7 @@ export type {
8
8
  DecorationType,
9
9
  LocalizedText,
10
10
  NoteOptions,
11
+ NoteHighlight,
11
12
  ArrowOptions,
12
13
  ArrowAnimationOptions,
13
14
  DecorationOptions,
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 drawArrow(svg, rect, arrowOptions, style, seed, origin = null, context = {}) {
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
- for (const paragraph of String(text).split(/\r?\n/)) {
1396
- const words = paragraph.trim().split(/\s+/).filter(Boolean);
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).join(" "));
1428
+ lines.push(lineOf(words.slice(i, i + size), marks));
1401
1429
  }
1402
1430
  }
1403
- return lines.length ? lines : [String(text)];
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 lines = wrapWords(text, options.wordsPerLine ?? DEFAULT_WORDS_PER_LINE);
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 = lines.map((line, index) => {
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 renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {}) {
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 tilt = (random() - 0.45) * 7;
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") return h(TSpan, props, node.textContent);
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;