strokes-js 0.1.2 → 0.1.4

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.
@@ -66,59 +66,61 @@ import { punctExclamation } from "./punctuation/punct-exclamation.js";
66
66
  import { punctQuestion } from "./punctuation/punct-question.js";
67
67
  import { punctApostrophe } from "./punctuation/punct-apostrophe.js";
68
68
  import { punctHyphen } from "./punctuation/punct-hyphen.js";
69
+ import { space } from "./space.js";
69
70
  export const glyphs = {
70
- "A": letterAUpper,
71
- "a": letterA,
72
- "B": letterBUpper,
73
- "b": letterB,
74
- "C": letterCUpper,
75
- "c": letterC,
76
- "D": letterDUpper,
77
- "d": letterD,
78
- "E": letterEUpper,
79
- "e": letterE,
80
- "F": letterFUpper,
81
- "f": letterF,
82
- "G": letterGUpper,
83
- "g": letterG,
84
- "H": letterHUpper,
85
- "h": letterH,
86
- "I": letterIUpper,
87
- "i": letterI,
88
- "J": letterJUpper,
89
- "j": letterJ,
90
- "K": letterKUpper,
91
- "k": letterK,
92
- "L": letterLUpper,
93
- "l": letterL,
94
- "M": letterMUpper,
95
- "m": letterM,
96
- "N": letterNUpper,
97
- "n": letterN,
98
- "O": letterOUpper,
99
- "o": letterO,
100
- "P": letterPUpper,
101
- "p": letterP,
102
- "Q": letterQUpper,
103
- "q": letterQ,
104
- "R": letterRUpper,
105
- "r": letterR,
106
- "S": letterSUpper,
107
- "s": letterS,
108
- "T": letterTUpper,
109
- "t": letterT,
110
- "U": letterUUpper,
111
- "u": letterU,
112
- "V": letterVUpper,
113
- "v": letterV,
114
- "W": letterWUpper,
115
- "w": letterW,
116
- "X": letterXUpper,
117
- "x": letterX,
118
- "Y": letterYUpper,
119
- "y": letterY,
120
- "Z": letterZUpper,
121
- "z": letterZ,
71
+ " ": space,
72
+ A: letterAUpper,
73
+ a: letterA,
74
+ B: letterBUpper,
75
+ b: letterB,
76
+ C: letterCUpper,
77
+ c: letterC,
78
+ D: letterDUpper,
79
+ d: letterD,
80
+ E: letterEUpper,
81
+ e: letterE,
82
+ F: letterFUpper,
83
+ f: letterF,
84
+ G: letterGUpper,
85
+ g: letterG,
86
+ H: letterHUpper,
87
+ h: letterH,
88
+ I: letterIUpper,
89
+ i: letterI,
90
+ J: letterJUpper,
91
+ j: letterJ,
92
+ K: letterKUpper,
93
+ k: letterK,
94
+ L: letterLUpper,
95
+ l: letterL,
96
+ M: letterMUpper,
97
+ m: letterM,
98
+ N: letterNUpper,
99
+ n: letterN,
100
+ O: letterOUpper,
101
+ o: letterO,
102
+ P: letterPUpper,
103
+ p: letterP,
104
+ Q: letterQUpper,
105
+ q: letterQ,
106
+ R: letterRUpper,
107
+ r: letterR,
108
+ S: letterSUpper,
109
+ s: letterS,
110
+ T: letterTUpper,
111
+ t: letterT,
112
+ U: letterUUpper,
113
+ u: letterU,
114
+ V: letterVUpper,
115
+ v: letterV,
116
+ W: letterWUpper,
117
+ w: letterW,
118
+ X: letterXUpper,
119
+ x: letterX,
120
+ Y: letterYUpper,
121
+ y: letterY,
122
+ Z: letterZUpper,
123
+ z: letterZ,
122
124
  "0": digit0,
123
125
  "1": digit1,
124
126
  "2": digit2,
@@ -0,0 +1,2 @@
1
+ import type { GlyphEntry } from "./types.js";
2
+ export declare const space: GlyphEntry;
@@ -0,0 +1,5 @@
1
+ export const space = {
2
+ width: 8,
3
+ variantWidths: [7, 8, 9],
4
+ variants: ["", "", ""],
5
+ };
@@ -1,5 +1,6 @@
1
1
  export interface GlyphEntry {
2
2
  width: number;
3
+ variantWidths?: number[];
3
4
  variants: string[];
4
5
  }
5
6
  export type GlyphDatabase = Record<string, GlyphEntry>;
package/dist/renderer.js CHANGED
@@ -45,25 +45,44 @@ function renderWord(word, delayOffset, animate) {
45
45
  continue;
46
46
  const variantIndex = pickVariant(char, entry);
47
47
  const d = entry.variants[variantIndex];
48
+ const width = entry.variantWidths?.[variantIndex] ?? entry.width;
48
49
  const style = animate
49
50
  ? `stroke-dasharray:1;stroke-dashoffset:1;transition:stroke-dashoffset ${STROKE_MS}ms ease ${(delayOffset + letterIndex) * STAGGER_MS}ms;`
50
51
  : "";
51
- const { rotate, dy, scale } = randomJitter();
52
- const cx = entry.width / 2;
53
- const cy = LETTER_HEIGHT / 2;
54
- // Position first, then rotate/scale around the letter's own center (not the SVG origin)
55
- // so the jitter can't nudge later letters in long words out of position.
56
- const transform = `translate(${x} ${dy.toFixed(2)}) translate(${cx} ${cy}) ` +
57
- `rotate(${rotate.toFixed(1)}) scale(${scale.toFixed(3)}) translate(${-cx} ${-cy})`;
58
- paths.push(`<path d="${d}" transform="${transform}" stroke="var(--hw-color, #000)" fill="none" ` +
59
- `stroke-width="2" stroke-linecap="round" pathLength="1" style="${style}" class="hw-letter" />`);
60
- x += entry.width;
52
+ if (d) {
53
+ const { rotate, dy, scale } = randomJitter();
54
+ const cx = width / 2;
55
+ const cy = LETTER_HEIGHT / 2;
56
+ // Position first, then rotate/scale around the letter's own center (not the SVG origin)
57
+ // so the jitter can't nudge later letters in long words out of position.
58
+ const transform = `translate(${x} ${dy.toFixed(2)}) translate(${cx} ${cy}) ` +
59
+ `rotate(${rotate.toFixed(1)}) scale(${scale.toFixed(3)}) translate(${-cx} ${-cy})`;
60
+ paths.push(`<path d="${d}" transform="${transform}" stroke="var(--hw-color, #000)" fill="none" ` +
61
+ `stroke-width="2" stroke-linecap="round" pathLength="1" style="${style}" class="hw-letter" />`);
62
+ }
63
+ x += width;
61
64
  letterIndex++;
62
65
  }
63
66
  const svg = `<svg class="hw-word" xmlns="http://www.w3.org/2000/svg" width="${x}" height="${LETTER_HEIGHT}" ` +
64
67
  `viewBox="0 0 ${x} ${LETTER_HEIGHT}" aria-hidden="true">${paths.join("")}</svg>`;
65
68
  return { svg, letterCount: letterIndex };
66
69
  }
70
+ function renderWhitespace(whitespace, delayOffset, animate) {
71
+ let letterCount = 0;
72
+ const markup = [];
73
+ for (const chunk of whitespace.split(/(\r\n|\r|\n)/)) {
74
+ if (!chunk)
75
+ continue;
76
+ if (/^(\r\n|\r|\n)$/.test(chunk)) {
77
+ markup.push("<br />");
78
+ continue;
79
+ }
80
+ const { svg, letterCount: count } = renderWord(chunk.replace(/\s/g, " "), delayOffset + letterCount, animate);
81
+ markup.push(svg);
82
+ letterCount += count;
83
+ }
84
+ return { markup: markup.join(""), letterCount };
85
+ }
67
86
  /**
68
87
  * Renders text as a sentence wrapper containing one <svg> per word.
69
88
  * When animate (default), each word's letters draw in via stroke-dashoffset once `.hw-visible`
@@ -74,15 +93,22 @@ export function renderText(text, options = {}) {
74
93
  const parts = splitText(text);
75
94
  let delayOffset = 0;
76
95
  const markup = [];
96
+ let pendingWhitespace = "";
77
97
  for (const part of parts) {
78
98
  if (/^\s+$/.test(part)) {
79
- markup.push(`<span class="hw-space" aria-hidden="true" style="white-space: pre-wrap">${escapeHtml(part)}</span>`);
99
+ pendingWhitespace += part;
80
100
  continue;
81
101
  }
82
102
  const word = part;
83
103
  const { svg, letterCount } = renderWord(word, delayOffset, animate);
84
- markup.push(svg);
85
- delayOffset += letterCount;
104
+ const { markup: space, letterCount: spaceCount } = renderWhitespace(pendingWhitespace, delayOffset, animate);
105
+ markup.push(`<span class="hw-token" style="display:inline-flex;align-items:baseline">${space}${svg}</span>`);
106
+ pendingWhitespace = "";
107
+ delayOffset += spaceCount + letterCount;
108
+ }
109
+ if (pendingWhitespace) {
110
+ const { markup: space } = renderWhitespace(pendingWhitespace, delayOffset, animate);
111
+ markup.push(`<span class="hw-token" style="display:inline-flex;align-items:baseline">${space}</span>`);
86
112
  }
87
113
  return `<span class="hw-sentence" aria-label="${escapeAttr(text)}">${markup.join("")}</span>`;
88
114
  }
@@ -93,12 +119,6 @@ function escapeAttr(value) {
93
119
  .replace(/</g, "&lt;")
94
120
  .replace(/>/g, "&gt;");
95
121
  }
96
- function escapeHtml(value) {
97
- return value
98
- .replace(/&/g, "&amp;")
99
- .replace(/</g, "&lt;")
100
- .replace(/>/g, "&gt;");
101
- }
102
122
  /** Toggles the class that starts the draw-in transition for a rendered word's strokes. */
103
123
  export function animateWriting(el) {
104
124
  el.classList.add("hw-visible");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "strokes-js",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Renders text as animated hand-drawn SVG",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",