@aznabee/freehand-ui 0.2.0 → 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 +133 -26
- package/dist/native.d.ts +1 -0
- package/dist/native.js +133 -15
- 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
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
var __create = Object.create;
|
|
2
1
|
var __defProp = Object.defineProperty;
|
|
3
2
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
3
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
4
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
5
|
var __export = (target, all) => {
|
|
8
6
|
for (var name in all)
|
|
@@ -16,14 +14,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
16
14
|
}
|
|
17
15
|
return to;
|
|
18
16
|
};
|
|
19
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
-
mod
|
|
26
|
-
));
|
|
27
17
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
18
|
|
|
29
19
|
// src/native/react-native.js
|
|
@@ -36,7 +26,7 @@ __export(react_native_exports, {
|
|
|
36
26
|
module.exports = __toCommonJS(react_native_exports);
|
|
37
27
|
var import_react = require("react");
|
|
38
28
|
var import_react_native = require("react-native");
|
|
39
|
-
var import_react_native_svg =
|
|
29
|
+
var import_react_native_svg = require("react-native-svg");
|
|
40
30
|
|
|
41
31
|
// src/animation.js
|
|
42
32
|
var STYLE_MARKER = "data-freehand-animation";
|
|
@@ -352,6 +342,9 @@ function mergeOptions(options = {}) {
|
|
|
352
342
|
// `animate: false` opts out, an object tunes speed and gestures
|
|
353
343
|
animate: normalizeArrowAnimation(config.animate)
|
|
354
344
|
};
|
|
345
|
+
for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
|
|
346
|
+
if (config[key] !== void 0) merged.arrow[key] = config[key];
|
|
347
|
+
}
|
|
355
348
|
}
|
|
356
349
|
if (merged.decorations === true) {
|
|
357
350
|
merged.decorations = { count: null, types: null };
|
|
@@ -1279,7 +1272,24 @@ function boundsOf(points) {
|
|
|
1279
1272
|
height: Math.max(...ys) - Math.min(...ys)
|
|
1280
1273
|
};
|
|
1281
1274
|
}
|
|
1282
|
-
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);
|
|
1283
1293
|
const random = createRandom(seed + 17);
|
|
1284
1294
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
1285
1295
|
const dx = end.x - start.x;
|
|
@@ -1399,17 +1409,69 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
1399
1409
|
);
|
|
1400
1410
|
}
|
|
1401
1411
|
var DEFAULT_WORDS_PER_LINE = 2;
|
|
1402
|
-
function wrapWords(text, perLine) {
|
|
1412
|
+
function wrapWords(text, perLine, marks = null) {
|
|
1413
|
+
const source = String(text);
|
|
1403
1414
|
const lines = [];
|
|
1404
|
-
|
|
1405
|
-
|
|
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
|
+
}));
|
|
1406
1424
|
if (!words.length) continue;
|
|
1407
1425
|
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
1408
1426
|
for (let i = 0; i < words.length; i += size) {
|
|
1409
|
-
lines.push(words.slice(i, i + size)
|
|
1427
|
+
lines.push(lineOf(words.slice(i, i + size), marks));
|
|
1410
1428
|
}
|
|
1411
1429
|
}
|
|
1412
|
-
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;
|
|
1413
1475
|
}
|
|
1414
1476
|
function measureWidth(element, content, fontSize) {
|
|
1415
1477
|
try {
|
|
@@ -1426,7 +1488,13 @@ function measureWidth(element, content, fontSize) {
|
|
|
1426
1488
|
}
|
|
1427
1489
|
function createNoteText(svg, text, style, options = {}) {
|
|
1428
1490
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
1429
|
-
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);
|
|
1430
1498
|
const lineHeight = fontSize * 1.05;
|
|
1431
1499
|
const rtl = options.rtl ?? isRtlText(text);
|
|
1432
1500
|
const group = createSvgElement("g");
|
|
@@ -1437,7 +1505,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1437
1505
|
textEl.setAttribute("opacity", String(style.opacity));
|
|
1438
1506
|
textEl.setAttribute("font-size", String(fontSize));
|
|
1439
1507
|
textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
|
|
1440
|
-
textEl.setAttribute("font-weight", "600");
|
|
1508
|
+
textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
|
|
1441
1509
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
1442
1510
|
textEl.setAttribute("text-anchor", "start");
|
|
1443
1511
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
@@ -1448,11 +1516,25 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1448
1516
|
if (options.locale) {
|
|
1449
1517
|
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1450
1518
|
}
|
|
1451
|
-
const tspans =
|
|
1519
|
+
const tspans = wrapped.map((line, index) => {
|
|
1452
1520
|
const tspan = createSvgElement("tspan");
|
|
1453
|
-
tspan.textContent = line;
|
|
1454
1521
|
tspan.setAttribute("x", "0");
|
|
1455
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
|
+
}
|
|
1456
1538
|
textEl.appendChild(tspan);
|
|
1457
1539
|
return tspan;
|
|
1458
1540
|
});
|
|
@@ -1555,7 +1637,21 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1555
1637
|
if (!dx) return box;
|
|
1556
1638
|
return { ...box, x: box.x + dx };
|
|
1557
1639
|
}
|
|
1558
|
-
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 ?? {});
|
|
1559
1655
|
const { text, locale } = resolveLocalizedText(
|
|
1560
1656
|
noteOptions?.text,
|
|
1561
1657
|
noteOptions?.locale
|
|
@@ -1566,9 +1662,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
|
|
|
1566
1662
|
const random = createRandom(seed + 9);
|
|
1567
1663
|
const note = createNoteText(svg, text, style, {
|
|
1568
1664
|
wordsPerLine: noteOptions.wordsPerLine,
|
|
1569
|
-
locale
|
|
1665
|
+
locale,
|
|
1666
|
+
highlight: noteOptions.highlight
|
|
1570
1667
|
});
|
|
1571
|
-
const
|
|
1668
|
+
const randomTilt = (random() - 0.45) * 7;
|
|
1669
|
+
const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
|
|
1572
1670
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
1573
1671
|
const size = {
|
|
1574
1672
|
width: note.width,
|
|
@@ -2166,7 +2264,16 @@ function blurOf(node) {
|
|
|
2166
2264
|
}
|
|
2167
2265
|
function renderNode(node, key, context) {
|
|
2168
2266
|
const props = { key, ...propsOf(node) };
|
|
2169
|
-
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
|
+
}
|
|
2170
2277
|
if (node.tagName === "circle" && blurOf(node) > 0) {
|
|
2171
2278
|
const id = `${context.prefix}-glow-${context.defs.length}`;
|
|
2172
2279
|
const color = props.fill;
|
|
@@ -2229,7 +2336,7 @@ function LayerSvg({ nodes, bounds, clock, prefix }) {
|
|
|
2229
2336
|
const context = { defs: [], clock, prefix };
|
|
2230
2337
|
const content = nodes.map((node, index) => renderNode(node, index, context));
|
|
2231
2338
|
return (0, import_react.createElement)(
|
|
2232
|
-
import_react_native_svg.
|
|
2339
|
+
import_react_native_svg.Svg,
|
|
2233
2340
|
{
|
|
2234
2341
|
style: import_react_native.StyleSheet.absoluteFill,
|
|
2235
2342
|
width: bounds.width,
|
package/dist/native.d.ts
CHANGED
package/dist/native.js
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
View,
|
|
18
18
|
useWindowDimensions
|
|
19
19
|
} from "react-native";
|
|
20
|
-
import
|
|
20
|
+
import {
|
|
21
|
+
Svg,
|
|
21
22
|
Circle,
|
|
22
23
|
Defs,
|
|
23
24
|
G,
|
|
@@ -342,6 +343,9 @@ function mergeOptions(options = {}) {
|
|
|
342
343
|
// `animate: false` opts out, an object tunes speed and gestures
|
|
343
344
|
animate: normalizeArrowAnimation(config.animate)
|
|
344
345
|
};
|
|
346
|
+
for (const key of ["color", "strokeWidth", "roughness", "opacity"]) {
|
|
347
|
+
if (config[key] !== void 0) merged.arrow[key] = config[key];
|
|
348
|
+
}
|
|
345
349
|
}
|
|
346
350
|
if (merged.decorations === true) {
|
|
347
351
|
merged.decorations = { count: null, types: null };
|
|
@@ -1269,7 +1273,24 @@ function boundsOf(points) {
|
|
|
1269
1273
|
height: Math.max(...ys) - Math.min(...ys)
|
|
1270
1274
|
};
|
|
1271
1275
|
}
|
|
1272
|
-
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);
|
|
1273
1294
|
const random = createRandom(seed + 17);
|
|
1274
1295
|
const { start, end } = arrowEndpoints(rect, arrowOptions, random, origin);
|
|
1275
1296
|
const dx = end.x - start.x;
|
|
@@ -1389,17 +1410,69 @@ function drawUnderline(svg, x, y, width, style, random) {
|
|
|
1389
1410
|
);
|
|
1390
1411
|
}
|
|
1391
1412
|
var DEFAULT_WORDS_PER_LINE = 2;
|
|
1392
|
-
function wrapWords(text, perLine) {
|
|
1413
|
+
function wrapWords(text, perLine, marks = null) {
|
|
1414
|
+
const source = String(text);
|
|
1393
1415
|
const lines = [];
|
|
1394
|
-
|
|
1395
|
-
|
|
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
|
+
}));
|
|
1396
1425
|
if (!words.length) continue;
|
|
1397
1426
|
const size = Number.isFinite(perLine) && perLine >= 1 ? Math.round(perLine) : words.length;
|
|
1398
1427
|
for (let i = 0; i < words.length; i += size) {
|
|
1399
|
-
lines.push(words.slice(i, i + size)
|
|
1428
|
+
lines.push(lineOf(words.slice(i, i + size), marks));
|
|
1400
1429
|
}
|
|
1401
1430
|
}
|
|
1402
|
-
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;
|
|
1403
1476
|
}
|
|
1404
1477
|
function measureWidth(element, content, fontSize) {
|
|
1405
1478
|
try {
|
|
@@ -1416,7 +1489,13 @@ function measureWidth(element, content, fontSize) {
|
|
|
1416
1489
|
}
|
|
1417
1490
|
function createNoteText(svg, text, style, options = {}) {
|
|
1418
1491
|
const fontSize = style.fontSize ?? NOTE_FONT_SIZE;
|
|
1419
|
-
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);
|
|
1420
1499
|
const lineHeight = fontSize * 1.05;
|
|
1421
1500
|
const rtl = options.rtl ?? isRtlText(text);
|
|
1422
1501
|
const group = createSvgElement("g");
|
|
@@ -1427,7 +1506,7 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1427
1506
|
textEl.setAttribute("opacity", String(style.opacity));
|
|
1428
1507
|
textEl.setAttribute("font-size", String(fontSize));
|
|
1429
1508
|
textEl.setAttribute("font-family", style.fontFamily || HANDWRITTEN_FONT);
|
|
1430
|
-
textEl.setAttribute("font-weight", "600");
|
|
1509
|
+
textEl.setAttribute("font-weight", String(style.fontWeight ?? "600"));
|
|
1431
1510
|
textEl.setAttribute("letter-spacing", "0.4");
|
|
1432
1511
|
textEl.setAttribute("text-anchor", "start");
|
|
1433
1512
|
textEl.setAttribute("dominant-baseline", "auto");
|
|
@@ -1438,11 +1517,25 @@ function createNoteText(svg, text, style, options = {}) {
|
|
|
1438
1517
|
if (options.locale) {
|
|
1439
1518
|
textEl.setAttributeNS(XML_NS, "xml:lang", options.locale);
|
|
1440
1519
|
}
|
|
1441
|
-
const tspans =
|
|
1520
|
+
const tspans = wrapped.map((line, index) => {
|
|
1442
1521
|
const tspan = createSvgElement("tspan");
|
|
1443
|
-
tspan.textContent = line;
|
|
1444
1522
|
tspan.setAttribute("x", "0");
|
|
1445
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
|
+
}
|
|
1446
1539
|
textEl.appendChild(tspan);
|
|
1447
1540
|
return tspan;
|
|
1448
1541
|
});
|
|
@@ -1545,7 +1638,21 @@ function nudgeIntoBand(box, guard, band) {
|
|
|
1545
1638
|
if (!dx) return box;
|
|
1546
1639
|
return { ...box, x: box.x + dx };
|
|
1547
1640
|
}
|
|
1548
|
-
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 ?? {});
|
|
1549
1656
|
const { text, locale } = resolveLocalizedText(
|
|
1550
1657
|
noteOptions?.text,
|
|
1551
1658
|
noteOptions?.locale
|
|
@@ -1556,9 +1663,11 @@ function renderAnnotation(svg, rect, noteOptions, style, seed = 1, layout = {})
|
|
|
1556
1663
|
const random = createRandom(seed + 9);
|
|
1557
1664
|
const note = createNoteText(svg, text, style, {
|
|
1558
1665
|
wordsPerLine: noteOptions.wordsPerLine,
|
|
1559
|
-
locale
|
|
1666
|
+
locale,
|
|
1667
|
+
highlight: noteOptions.highlight
|
|
1560
1668
|
});
|
|
1561
|
-
const
|
|
1669
|
+
const randomTilt = (random() - 0.45) * 7;
|
|
1670
|
+
const tilt = Number.isFinite(noteOptions.tilt) ? noteOptions.tilt : randomTilt;
|
|
1562
1671
|
const tiltPad = Math.abs(Math.sin(tilt * Math.PI / 180)) * note.width * 0.5;
|
|
1563
1672
|
const size = {
|
|
1564
1673
|
width: note.width,
|
|
@@ -2156,7 +2265,16 @@ function blurOf(node) {
|
|
|
2156
2265
|
}
|
|
2157
2266
|
function renderNode(node, key, context) {
|
|
2158
2267
|
const props = { key, ...propsOf(node) };
|
|
2159
|
-
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
|
+
}
|
|
2160
2278
|
if (node.tagName === "circle" && blurOf(node) > 0) {
|
|
2161
2279
|
const id = `${context.prefix}-glow-${context.defs.length}`;
|
|
2162
2280
|
const color = props.fill;
|