strokes-js 0.1.3 → 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`
@@ -82,15 +101,14 @@ export function renderText(text, options = {}) {
82
101
  }
83
102
  const word = part;
84
103
  const { svg, letterCount } = renderWord(word, delayOffset, animate);
85
- const space = pendingWhitespace
86
- ? `<span class="hw-space" aria-hidden="true" style="white-space: pre-wrap">${escapeHtml(pendingWhitespace)}</span>`
87
- : "";
88
- markup.push(`<span class="hw-token">${space}${svg}</span>`);
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>`);
89
106
  pendingWhitespace = "";
90
- delayOffset += letterCount;
107
+ delayOffset += spaceCount + letterCount;
91
108
  }
92
109
  if (pendingWhitespace) {
93
- markup.push(`<span class="hw-token"><span class="hw-space" aria-hidden="true" style="white-space: pre-wrap">${escapeHtml(pendingWhitespace)}</span></span>`);
110
+ const { markup: space } = renderWhitespace(pendingWhitespace, delayOffset, animate);
111
+ markup.push(`<span class="hw-token" style="display:inline-flex;align-items:baseline">${space}</span>`);
94
112
  }
95
113
  return `<span class="hw-sentence" aria-label="${escapeAttr(text)}">${markup.join("")}</span>`;
96
114
  }
@@ -101,12 +119,6 @@ function escapeAttr(value) {
101
119
  .replace(/</g, "&lt;")
102
120
  .replace(/>/g, "&gt;");
103
121
  }
104
- function escapeHtml(value) {
105
- return value
106
- .replace(/&/g, "&amp;")
107
- .replace(/</g, "&lt;")
108
- .replace(/>/g, "&gt;");
109
- }
110
122
  /** Toggles the class that starts the draw-in transition for a rendered word's strokes. */
111
123
  export function animateWriting(el) {
112
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.3",
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",