@arraypress/covers 0.0.0-stage → 1.1.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.
Files changed (245) hide show
  1. package/CHANGELOG.md +126 -0
  2. package/LICENSE +29 -0
  3. package/README.md +846 -2
  4. package/fonts/OFL.txt +93 -0
  5. package/fonts/inter-latin-400.woff +0 -0
  6. package/fonts/inter-latin-500.woff +0 -0
  7. package/fonts/inter-latin-600.woff +0 -0
  8. package/fonts/inter-latin-700.woff +0 -0
  9. package/package.json +124 -4
  10. package/recipes/aurora.json +132 -0
  11. package/recipes/bauhaus.json +113 -0
  12. package/recipes/bigType.json +103 -0
  13. package/recipes/brand.json +1143 -0
  14. package/recipes/contour.json +109 -0
  15. package/recipes/cutout.json +191 -0
  16. package/recipes/diagram.json +92 -0
  17. package/recipes/dither.json +170 -0
  18. package/recipes/duotone.json +75 -0
  19. package/recipes/gridTitle.json +136 -0
  20. package/recipes/iconTile.json +214 -0
  21. package/recipes/issue.json +221 -0
  22. package/recipes/mark.json +170 -0
  23. package/recipes/photo.json +156 -0
  24. package/recipes/riso.json +387 -0
  25. package/recipes/stripes.json +206 -0
  26. package/recipes/waveform.json +154 -0
  27. package/schema/recipe.schema.json +8822 -0
  28. package/src/animate/index.d.ts +19 -0
  29. package/src/animate/index.js +116 -0
  30. package/src/astro.d.ts +31 -0
  31. package/src/astro.js +86 -0
  32. package/src/builtins.js +4 -0
  33. package/src/color.js +246 -0
  34. package/src/diagram.js +248 -0
  35. package/src/effects/ascii.d.ts +7 -0
  36. package/src/effects/ascii.js +93 -0
  37. package/src/effects/buffer.d.ts +60 -0
  38. package/src/effects/buffer.js +472 -0
  39. package/src/effects/detail.d.ts +12 -0
  40. package/src/effects/detail.js +124 -0
  41. package/src/effects/digital.d.ts +14 -0
  42. package/src/effects/digital.js +216 -0
  43. package/src/effects/distort.d.ts +20 -0
  44. package/src/effects/distort.js +314 -0
  45. package/src/effects/dither.d.ts +8 -0
  46. package/src/effects/dither.js +147 -0
  47. package/src/effects/drop-shadow.d.ts +4 -0
  48. package/src/effects/drop-shadow.js +45 -0
  49. package/src/effects/extrude.d.ts +5 -0
  50. package/src/effects/extrude.js +55 -0
  51. package/src/effects/film.d.ts +11 -0
  52. package/src/effects/film.js +204 -0
  53. package/src/effects/glow.d.ts +8 -0
  54. package/src/effects/glow.js +56 -0
  55. package/src/effects/grade.d.ts +30 -0
  56. package/src/effects/grade.js +274 -0
  57. package/src/effects/halftone.d.ts +8 -0
  58. package/src/effects/halftone.js +95 -0
  59. package/src/effects/index.d.ts +125 -0
  60. package/src/effects/index.js +277 -0
  61. package/src/effects/ink.d.ts +12 -0
  62. package/src/effects/ink.js +70 -0
  63. package/src/effects/lens.d.ts +14 -0
  64. package/src/effects/lens.js +277 -0
  65. package/src/effects/light.d.ts +10 -0
  66. package/src/effects/light.js +59 -0
  67. package/src/effects/looks.d.ts +9 -0
  68. package/src/effects/looks.js +74 -0
  69. package/src/effects/mask.d.ts +52 -0
  70. package/src/effects/mask.js +314 -0
  71. package/src/effects/motion.d.ts +6 -0
  72. package/src/effects/motion.js +60 -0
  73. package/src/effects/newsprint.d.ts +6 -0
  74. package/src/effects/newsprint.js +89 -0
  75. package/src/effects/optics.d.ts +10 -0
  76. package/src/effects/optics.js +213 -0
  77. package/src/effects/options.d.ts +6 -0
  78. package/src/effects/options.js +106 -0
  79. package/src/effects/paint-presets.d.ts +10 -0
  80. package/src/effects/paint-presets.js +47 -0
  81. package/src/effects/paint.d.ts +49 -0
  82. package/src/effects/paint.js +66 -0
  83. package/src/effects/pixels.d.ts +24 -0
  84. package/src/effects/pixels.js +138 -0
  85. package/src/effects/polar.d.ts +10 -0
  86. package/src/effects/polar.js +265 -0
  87. package/src/effects/region.d.ts +8 -0
  88. package/src/effects/region.js +136 -0
  89. package/src/effects/round-border.d.ts +5 -0
  90. package/src/effects/round-border.js +62 -0
  91. package/src/effects/screen.d.ts +14 -0
  92. package/src/effects/screen.js +212 -0
  93. package/src/effects/sort.d.ts +8 -0
  94. package/src/effects/sort.js +125 -0
  95. package/src/effects/symmetry.d.ts +6 -0
  96. package/src/effects/symmetry.js +74 -0
  97. package/src/effects/tile.d.ts +7 -0
  98. package/src/effects/tile.js +55 -0
  99. package/src/effects/tone.d.ts +16 -0
  100. package/src/effects/tone.js +117 -0
  101. package/src/effects/transform.d.ts +35 -0
  102. package/src/effects/transform.js +127 -0
  103. package/src/effects/util.d.ts +16 -0
  104. package/src/effects/util.js +85 -0
  105. package/src/extract.js +91 -0
  106. package/src/flatten.js +234 -0
  107. package/src/fonts.d.ts +55 -0
  108. package/src/fonts.js +393 -0
  109. package/src/formats/cut.d.ts +10 -0
  110. package/src/formats/cut.js +77 -0
  111. package/src/formats/index.d.ts +6 -0
  112. package/src/formats/index.js +9 -0
  113. package/src/formats/presets.d.ts +41 -0
  114. package/src/formats/presets.js +89 -0
  115. package/src/formats/print.d.ts +25 -0
  116. package/src/formats/print.js +117 -0
  117. package/src/formats/zones.d.ts +10 -0
  118. package/src/formats/zones.js +64 -0
  119. package/src/geometry.js +187 -0
  120. package/src/grid.js +41 -0
  121. package/src/handmade.js +253 -0
  122. package/src/icons.js +191 -0
  123. package/src/index.d.ts +1083 -0
  124. package/src/index.js +27 -0
  125. package/src/inter-data.js +27 -0
  126. package/src/json-recipe.js +616 -0
  127. package/src/kit/index.d.ts +20 -0
  128. package/src/kit/index.js +58 -0
  129. package/src/layer-options.js +56 -0
  130. package/src/layers/barcode.d.ts +19 -0
  131. package/src/layers/barcode.js +63 -0
  132. package/src/layers/code128.d.ts +4 -0
  133. package/src/layers/code128.js +28 -0
  134. package/src/layers/expr.d.ts +6 -0
  135. package/src/layers/expr.js +121 -0
  136. package/src/layers/geometry.d.ts +6 -0
  137. package/src/layers/geometry.js +238 -0
  138. package/src/layers/index.d.ts +18 -0
  139. package/src/layers/index.js +15 -0
  140. package/src/layers/perforation.d.ts +14 -0
  141. package/src/layers/perforation.js +47 -0
  142. package/src/layers/repeat.d.ts +29 -0
  143. package/src/layers/repeat.js +90 -0
  144. package/src/layers/shape.d.ts +46 -0
  145. package/src/layers/shape.js +115 -0
  146. package/src/layers/soft.d.ts +5 -0
  147. package/src/layers/soft.js +43 -0
  148. package/src/layers/svg.d.ts +23 -0
  149. package/src/layers/svg.js +109 -0
  150. package/src/layers/table.d.ts +25 -0
  151. package/src/layers/table.js +80 -0
  152. package/src/layers/tear.d.ts +21 -0
  153. package/src/layers/tear.js +74 -0
  154. package/src/layers/texture.d.ts +17 -0
  155. package/src/layers/texture.js +106 -0
  156. package/src/layers/util.d.ts +8 -0
  157. package/src/layers/util.js +48 -0
  158. package/src/layers.js +1056 -0
  159. package/src/lint/image-size.d.ts +4 -0
  160. package/src/lint/image-size.js +34 -0
  161. package/src/lint/index.d.ts +16 -0
  162. package/src/lint/index.js +127 -0
  163. package/src/lint/rules.d.ts +5 -0
  164. package/src/lint/rules.js +19 -0
  165. package/src/node.d.ts +2 -0
  166. package/src/node.js +20 -0
  167. package/src/palette.js +185 -0
  168. package/src/platform/bytes.d.ts +22 -0
  169. package/src/platform/bytes.js +103 -0
  170. package/src/platform/compress.d.ts +12 -0
  171. package/src/platform/compress.js +81 -0
  172. package/src/platform/harfbuzz.d.ts +9 -0
  173. package/src/platform/harfbuzz.js +80 -0
  174. package/src/platform/index.d.ts +8 -0
  175. package/src/platform/index.js +13 -0
  176. package/src/platform/load.d.ts +6 -0
  177. package/src/platform/load.js +72 -0
  178. package/src/platform/no-harfbuzz.d.ts +2 -0
  179. package/src/platform/no-harfbuzz.js +11 -0
  180. package/src/platform/node.d.ts +26 -0
  181. package/src/platform/node.js +86 -0
  182. package/src/platform/settings.d.ts +28 -0
  183. package/src/platform/settings.js +51 -0
  184. package/src/png.js +126 -0
  185. package/src/print.js +659 -0
  186. package/src/raster/index.d.ts +24 -0
  187. package/src/raster/index.js +171 -0
  188. package/src/recipes.js +79 -0
  189. package/src/render/backdrop.d.ts +6 -0
  190. package/src/render/backdrop.js +55 -0
  191. package/src/render/content.d.ts +5 -0
  192. package/src/render/content.js +58 -0
  193. package/src/render/context.d.ts +82 -0
  194. package/src/render/grain.d.ts +7 -0
  195. package/src/render/grain.js +113 -0
  196. package/src/render/group.d.ts +23 -0
  197. package/src/render/group.js +67 -0
  198. package/src/render/key.d.ts +5 -0
  199. package/src/render/key.js +91 -0
  200. package/src/render/report.d.ts +5 -0
  201. package/src/render/report.js +19 -0
  202. package/src/render/tiles.d.ts +6 -0
  203. package/src/render/tiles.js +77 -0
  204. package/src/render/use.d.ts +27 -0
  205. package/src/render/use.js +73 -0
  206. package/src/render/wrap.d.ts +14 -0
  207. package/src/render/wrap.js +199 -0
  208. package/src/render.js +553 -0
  209. package/src/rng.js +95 -0
  210. package/src/svg.js +69 -0
  211. package/src/text.d.ts +29 -0
  212. package/src/text.js +237 -0
  213. package/src/type/auto-color.d.ts +9 -0
  214. package/src/type/auto-color.js +44 -0
  215. package/src/type/fallback.d.ts +11 -0
  216. package/src/type/fallback.js +92 -0
  217. package/src/type/glyphs.d.ts +24 -0
  218. package/src/type/glyphs.js +87 -0
  219. package/src/type/index.d.ts +5 -0
  220. package/src/type/index.js +10 -0
  221. package/src/type/kern.d.ts +4 -0
  222. package/src/type/kern.js +144 -0
  223. package/src/type/layout-ref.d.ts +10 -0
  224. package/src/type/layout-ref.js +74 -0
  225. package/src/type/line.d.ts +11 -0
  226. package/src/type/line.js +105 -0
  227. package/src/type/live.d.ts +6 -0
  228. package/src/type/live.js +39 -0
  229. package/src/type/name.d.ts +4 -0
  230. package/src/type/name.js +32 -0
  231. package/src/type/path.d.ts +18 -0
  232. package/src/type/path.js +89 -0
  233. package/src/type/pixel.d.ts +9 -0
  234. package/src/type/pixel.js +115 -0
  235. package/src/type/sfnt.d.ts +20 -0
  236. package/src/type/sfnt.js +258 -0
  237. package/src/type/spans.d.ts +33 -0
  238. package/src/type/spans.js +138 -0
  239. package/src/type/symbols.d.ts +7 -0
  240. package/src/type/symbols.js +89 -0
  241. package/src/type/typeset.d.ts +10 -0
  242. package/src/type/typeset.js +352 -0
  243. package/src/type/woff2.d.ts +6 -0
  244. package/src/type/woff2.js +301 -0
  245. package/src/version.js +4 -0
package/src/svg.js ADDED
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Small helpers for writing SVG markup deterministically.
3
+ *
4
+ * Numbers are always written through {@link num}, so the same input gives
5
+ * byte-identical output on every machine (no `0.30000000000000004`, no
6
+ * locale formatting, no `-0`).
7
+ *
8
+ * @module svg
9
+ */
10
+
11
+ /**
12
+ * Format a number for SVG: at most `digits` decimals, no trailing zeros.
13
+ *
14
+ * @param {number} v - The number.
15
+ * @param {number} [digits=2] - Maximum decimals.
16
+ * @returns {string} The formatted number.
17
+ *
18
+ * @example
19
+ * num(1 / 3); // '0.33'
20
+ * num(12.5); // '12.5'
21
+ * num(-0.001); // '0'
22
+ */
23
+ export function num(v, digits = 2) {
24
+ if (!Number.isFinite(v)) return '0';
25
+ const s = v.toFixed(digits).replace(/\.?0+$/, '');
26
+ return s === '-0' || s === '' ? '0' : s;
27
+ }
28
+
29
+ /**
30
+ * Escape text for an XML attribute or text node.
31
+ *
32
+ * @param {string} s - Raw text.
33
+ * @returns {string} Escaped text.
34
+ */
35
+ export function esc(s) {
36
+ return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' })[c]);
37
+ }
38
+
39
+ /**
40
+ * Serialise an attribute object. `undefined`, `null` and `false` values are
41
+ * skipped; numbers go through {@link num}. Keys keep their insertion order.
42
+ *
43
+ * @param {Record<string, string | number | boolean | null | undefined>} obj - Attributes.
44
+ * @returns {string} ` key="value"` pairs (with a leading space), or `''`.
45
+ *
46
+ * @example
47
+ * attrs({ x: 1.005, fill: '#fff', opacity: undefined }); // ' x="1" fill="#fff"'
48
+ */
49
+ export function attrs(obj) {
50
+ let out = '';
51
+ for (const [k, v] of Object.entries(obj)) {
52
+ if (v === undefined || v === null || v === false) continue;
53
+ out += ` ${k}="${typeof v === 'number' ? num(v) : esc(String(v))}"`;
54
+ }
55
+ return out;
56
+ }
57
+
58
+ /**
59
+ * Opacity and blend-mode attributes shared by every layer.
60
+ *
61
+ * @param {{ opacity?: number, blend?: string }} layer - The layer.
62
+ * @returns {Record<string, string | number | undefined>} Attributes for the layer's group.
63
+ */
64
+ export function layerAttrs(layer) {
65
+ return {
66
+ opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined,
67
+ style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${layer.blend}` : undefined,
68
+ };
69
+ }
package/src/text.d.ts ADDED
@@ -0,0 +1,29 @@
1
+ // Text: shaping, line breaking and outline drawing.
2
+ import type { LoadedFont } from './fonts.js';
3
+
4
+ /** A shaped run: glyph ids and pen positions, px. */
5
+ export interface Shaped {
6
+ /** Pen positions, px. A glyph from another face (fallback, a span) carries it, its own size, a paint key and a transform. */
7
+ glyphs: { gid: number; x: number; y: number; cluster?: number; face?: LoadedFont; size?: number; paint?: string; t?: [number, number, number, number, number, number] }[];
8
+ /** Advance width, px. */
9
+ width: number;
10
+ }
11
+
12
+ /** Laid-out text. */
13
+ export interface Layout {
14
+ lines: { text: string; run: Shaped }[];
15
+ size: number;
16
+ truncated: boolean;
17
+ width: number;
18
+ }
19
+
20
+ /** Shape one line at a size with letter-spacing (px between clusters). */
21
+ export function shape(face: LoadedFont, text: string, size: number, tracking: number, hb?: unknown): Shaped;
22
+ /** A glyph's outline as parsed commands, font units (cached on the face). */
23
+ export function outline(face: LoadedFont, gid: number): (string | number)[];
24
+ /** Shaped glyphs as SVG path data, px. */
25
+ export function runPath(face: LoadedFont, run: Shaped, size: number, x: number, baseline: number): string;
26
+ /** Break text into lines: greedy, optionally balanced, shrinking toward `minSize`, truncated past `maxLines`. */
27
+ export function layout(o: { face: LoadedFont; hb?: unknown; text: string; size: number; minSize?: number; letterSpacing?: number; maxWidth?: number; maxLines?: number; ellipsis?: string; balance?: boolean }): Layout;
28
+ /** Collapse spaces, trim lines and apply a case. */
29
+ export function caseText(s: string, t: string | undefined): string;
package/src/text.js ADDED
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Text: shaping, line breaking and outline drawing.
3
+ *
4
+ * A line is shaped by HarfBuzz as a whole (so kerning across spaces and
5
+ * ligatures are right), then every glyph's outline is transformed into
6
+ * canvas pixels and written as one `<path>` per line. The output never
7
+ * references a font, so it renders the same in librsvg, a browser or a
8
+ * design tool.
9
+ *
10
+ * Line breaking is greedy on spaces, then optionally balanced (like CSS
11
+ * `text-wrap: balance`: the narrowest width that keeps the same number of
12
+ * lines, so a headline doesn't end on a lonely word). A word wider than
13
+ * the line is broken between characters. Past `maxLines` the text is cut
14
+ * at a word and given an ellipsis; with `minSize`, the size first steps
15
+ * down until the text fits.
16
+ *
17
+ * Vertical metrics are trimmed to the cap height above the first line and
18
+ * the baseline below the last (CSS `text-box: trim-both cap alphabetic`),
19
+ * so a margin measured to a headline is the margin you see.
20
+ *
21
+ * @module text
22
+ */
23
+
24
+ import { num } from './svg.js';
25
+
26
+ /**
27
+ * A shaped run of text.
28
+ *
29
+ * @typedef {object} Shaped
30
+ * @property {{ gid: number, x: number, y: number, cluster?: number }[]} glyphs - Glyph ids and pen positions in px (y is the baseline offset), and the character each starts at.
31
+ * @property {number} width - Advance width in px (no trailing letter-spacing).
32
+ */
33
+
34
+ /**
35
+ * Shape a run of text.
36
+ *
37
+ * @param {import('./fonts.js').LoadedFont} face - The face.
38
+ * @param {string} text - One line of text.
39
+ * @param {number} size - Font size in px.
40
+ * @param {number} tracking - Letter-spacing in px, added between clusters (0 for none).
41
+ * @param {any} [hb] - Unused (kept for the 1.0 signature).
42
+ * @returns {Shaped} Positioned glyphs.
43
+ */
44
+ export function shape(face, text, size, tracking, hb) {
45
+ const glyphsIn = face.shapeRun(text);
46
+ const scale = size / face.upem;
47
+ const glyphs = [];
48
+ let pen = 0;
49
+ for (let i = 0; i < glyphsIn.length; i++) {
50
+ const p = glyphsIn[i];
51
+ glyphs.push({ gid: p.gid, x: pen + p.xOffset * scale, y: -p.yOffset * scale, cluster: p.cluster });
52
+ pen += p.xAdvance * scale;
53
+ const nextCluster = glyphsIn[i + 1]?.cluster;
54
+ if (tracking && nextCluster !== undefined && nextCluster !== p.cluster) pen += tracking;
55
+ }
56
+ return { glyphs, width: pen };
57
+ }
58
+
59
+ /**
60
+ * A glyph's outline as parsed commands, in font units (y up). Cached on
61
+ * the face.
62
+ *
63
+ * @param {import('./fonts.js').LoadedFont} face
64
+ * @param {number} gid
65
+ * @returns {(string | number)[]} Flat list: command letter followed by its numbers.
66
+ */
67
+ export function outline(face, gid) {
68
+ let cmds = face.paths.get(gid);
69
+ if (!cmds) {
70
+ cmds = [];
71
+ const d = face.glyphToPath(gid);
72
+ for (const m of d.matchAll(/([MLQCZ])([^MLQCZ]*)/gi)) {
73
+ cmds.push(m[1].toUpperCase());
74
+ for (const n of m[2].split(/[\s,]+/).filter(Boolean)) cmds.push(parseFloat(n));
75
+ }
76
+ face.paths.set(gid, cmds);
77
+ }
78
+ return cmds;
79
+ }
80
+
81
+ /**
82
+ * Write shaped glyphs as SVG path data in canvas pixels.
83
+ *
84
+ * @param {import('./fonts.js').LoadedFont} face
85
+ * @param {Shaped} run - Shaped glyphs.
86
+ * @param {number} size - Font size in px.
87
+ * @param {number} x - Left edge of the run.
88
+ * @param {number} baseline - Baseline y.
89
+ * @returns {string} Path data.
90
+ */
91
+ export function runPath(face, run, size, x, baseline) {
92
+ const s = size / face.upem;
93
+ const parts = [];
94
+ for (const g of run.glyphs) {
95
+ const ox = x + g.x;
96
+ const oy = baseline + g.y;
97
+ let coords = [];
98
+ for (const c of outline(face, g.gid)) {
99
+ if (typeof c === 'string') {
100
+ if (coords.length) parts.push(coords.join(' '));
101
+ parts.push(c);
102
+ coords = [];
103
+ } else {
104
+ // Coordinates alternate x, y; y flips (font units point up).
105
+ coords.push(num(coords.length % 2 === 0 ? ox + c * s : oy - c * s));
106
+ }
107
+ }
108
+ if (coords.length) parts.push(coords.join(' '));
109
+ }
110
+ return parts.join('');
111
+ }
112
+
113
+ /**
114
+ * Laid-out text.
115
+ *
116
+ * @typedef {object} Layout
117
+ * @property {{ text: string, run: Shaped }[]} lines - Each line and its shaping.
118
+ * @property {number} size - The size used (may be below the asked size with `minSize`).
119
+ * @property {boolean} truncated - Whether an ellipsis was added.
120
+ * @property {number} width - The widest line.
121
+ */
122
+
123
+ /**
124
+ * Break text into lines.
125
+ *
126
+ * @param {object} o
127
+ * @param {import('./fonts.js').LoadedFont} o.face - The face.
128
+ * @param {typeof import('harfbuzzjs')} o.hb - harfbuzzjs.
129
+ * @param {string} o.text - The text (`\n` starts a new line).
130
+ * @param {number} o.size - Font size in px.
131
+ * @param {number} [o.minSize] - Smallest size to shrink to before truncating.
132
+ * @param {number} [o.letterSpacing=0] - Tracking in em.
133
+ * @param {number} [o.maxWidth=Infinity] - Line width in px.
134
+ * @param {number} [o.maxLines=Infinity] - Most lines before truncating.
135
+ * @param {string} [o.ellipsis='…'] - Appended to a cut line.
136
+ * @param {boolean} [o.balance=false] - Balance line lengths.
137
+ * @returns {Layout} The layout.
138
+ */
139
+ export function layout(o) {
140
+ const maxLines = Math.max(1, o.maxLines ?? Infinity);
141
+ const maxWidth = o.maxWidth ?? Infinity;
142
+ let size = o.size;
143
+ const floor = Math.min(o.minSize ?? size, size);
144
+
145
+ for (;;) {
146
+ const attempt = breakLines({ ...o, size, maxWidth, maxLines });
147
+ if ((!attempt.truncated && !attempt.forced) || size <= floor) return attempt;
148
+ size = Math.max(floor, size * 0.94);
149
+ }
150
+ }
151
+
152
+ /**
153
+ * One layout pass at a fixed size.
154
+ *
155
+ * @param {Parameters<typeof layout>[0] & { maxWidth: number, maxLines: number }} o
156
+ * @returns {Layout & { forced: boolean }} The layout; `forced` is true when a word had to be split.
157
+ */
158
+ function breakLines(o) {
159
+ const tracking = (o.letterSpacing ?? 0) * o.size;
160
+ const measure = (/** @type {string} */ t) => shape(o.face, t, o.size, tracking, o.hb);
161
+ const ellipsis = o.ellipsis ?? '…';
162
+
163
+ /** @param {number} width */
164
+ const wrap = (width) => {
165
+ /** @type {string[]} */
166
+ const lines = [];
167
+ let forced = false;
168
+ for (const para of o.text.split('\n')) {
169
+ const words = para.split(/ +/).filter(Boolean);
170
+ if (!words.length) { lines.push(''); continue; }
171
+ let line = '';
172
+ for (const word of words) {
173
+ const candidate = line ? `${line} ${word}` : word;
174
+ if (!line || measure(candidate).width <= width) {
175
+ line = candidate;
176
+ } else {
177
+ lines.push(line);
178
+ line = word;
179
+ }
180
+ // A single word wider than the line: split it between characters.
181
+ while (measure(line).width > width && Array.from(line).length > 1) {
182
+ forced = true;
183
+ const chars = Array.from(line);
184
+ let cut = chars.length - 1;
185
+ while (cut > 1 && measure(chars.slice(0, cut).join('')).width > width) cut--;
186
+ lines.push(chars.slice(0, cut).join(''));
187
+ line = chars.slice(cut).join('');
188
+ }
189
+ }
190
+ lines.push(line);
191
+ }
192
+ return { lines, forced };
193
+ };
194
+
195
+ let { lines, forced } = wrap(o.maxWidth);
196
+
197
+ if (o.balance && lines.length > 1 && lines.length <= o.maxLines && Number.isFinite(o.maxWidth)) {
198
+ const target = lines.length;
199
+ let lo = o.maxWidth * 0.4;
200
+ let hi = o.maxWidth;
201
+ for (let i = 0; i < 12; i++) {
202
+ const mid = (lo + hi) / 2;
203
+ const w = wrap(mid);
204
+ if (w.lines.length <= target && !w.forced) hi = mid; else lo = mid;
205
+ }
206
+ lines = wrap(hi).lines;
207
+ }
208
+
209
+ let truncated = false;
210
+ if (lines.length > o.maxLines) {
211
+ truncated = true;
212
+ const kept = lines.slice(0, o.maxLines);
213
+ let last = kept[o.maxLines - 1];
214
+ const fits = (/** @type {string} */ t) => measure(t + ellipsis).width <= o.maxWidth;
215
+ while (!fits(last) && last.length) {
216
+ const cut = last.lastIndexOf(' ');
217
+ last = cut > 0 ? last.slice(0, cut) : Array.from(last).slice(0, -1).join('');
218
+ }
219
+ kept[o.maxLines - 1] = last.replace(/[\s.,;:!?·—–-]+$/u, '') + ellipsis;
220
+ lines = kept;
221
+ }
222
+
223
+ const shaped = lines.map((text) => ({ text, run: measure(text) }));
224
+ return { lines: shaped, size: o.size, truncated, forced, width: Math.max(0, ...shaped.map((l) => l.run.width)) };
225
+ }
226
+
227
+ /**
228
+ * Collapse spaces, trim each line, and apply a case.
229
+ *
230
+ * @param {string} s - The text.
231
+ * @param {string | undefined} t - `'uppercase'`, `'lowercase'`, anything else for none.
232
+ * @returns {string}
233
+ */
234
+ export function caseText(s, t) {
235
+ const clean = s.replace(/[\t\r ]+/g, ' ').replace(/ *\n */g, '\n').trim();
236
+ return t === 'uppercase' ? clean.toUpperCase() : t === 'lowercase' ? clean.toLowerCase() : clean;
237
+ }
@@ -0,0 +1,9 @@
1
+ // Auto contrast for `color: 'auto'`.
2
+ import type { RenderContext } from '../render/context.js';
3
+
4
+ /** Backdrop luma (0–1) above which `color: 'auto'` picks the dark colour (0.72). */
5
+ export const AUTO_THRESHOLD: number;
6
+ /** Rec. 601 luma of a colour, 0–1. */
7
+ export function luma(c: string): number;
8
+ /** White or near-black (or `autoColors`) by the luma of what's drawn under `box`. */
9
+ export function autoColor(layer: { autoColors?: [string, string]; autoThreshold?: number }, ctx: RenderContext, box: { x: number; y: number; width: number; height: number }): Promise<string>;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * Auto contrast: white or near-black (or a layer's `autoColors`), by how
3
+ * light what's drawn underneath is.
4
+ *
5
+ * The rule is the one the site's designs use: the backdrop's luma (Rec. 601
6
+ * weights on the gamma-encoded channels, 0–1) above `AUTO_THRESHOLD`
7
+ * (0.72) takes the dark colour, anything else the light one. A pure
8
+ * contrast-ratio choice picks black on mid-tones (a mid pink, a sky blue)
9
+ * where designers set white; this matches what they set.
10
+ *
11
+ * @module type/auto-color
12
+ */
13
+
14
+ import { toHex } from '../color.js';
15
+
16
+ /** Backdrop luma (0–1) above which `color: 'auto'` picks the dark colour. */
17
+ export const AUTO_THRESHOLD = 0.72;
18
+
19
+ /**
20
+ * Rec. 601 luma of a colour, 0–1.
21
+ *
22
+ * @param {string} c
23
+ * @returns {number}
24
+ */
25
+ export function luma(c) {
26
+ const h = toHex(c);
27
+ const [r, g, b] = [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
28
+ return 0.299 * r + 0.587 * g + 0.114 * b;
29
+ }
30
+
31
+ /**
32
+ * Pick the text colour against what's underneath (`autoThreshold` on the
33
+ * layer overrides the 0.72 cut).
34
+ *
35
+ * @param {any} layer
36
+ * @param {import('../layers.js').RenderContext} ctx
37
+ * @param {{ x: number, y: number, width: number, height: number }} box
38
+ * @returns {Promise<string>}
39
+ */
40
+ export async function autoColor(layer, ctx, box) {
41
+ const [light, dark] = (layer.autoColors ?? ['#ffffff', '#0b0b0b']).map(toHex);
42
+ const under = await /** @type {any} */ (ctx).backdrop?.(box);
43
+ return luma(under ?? ctx.palette.bg) > (layer.autoThreshold ?? AUTO_THRESHOLD) ? dark : light;
44
+ }
@@ -0,0 +1,11 @@
1
+ // Glyph fallback.
2
+ import type { LoadedFont } from '../fonts.js';
3
+ import type { Shaped } from '../text.js';
4
+ import type { RenderContext } from '../render/context.js';
5
+
6
+ /** The faces to fall back to: the layer's `fallback`, fonts registered with `fallback: true`, then the built-in symbols. */
7
+ export function fallbackFaces(ctx: RenderContext, o: { fallback?: string | string[]; weight?: number; style?: string }): Promise<LoadedFont[]>;
8
+ /** Whether a face has a glyph for a character. */
9
+ export function covers(face: LoadedFont, ch: string): boolean;
10
+ /** Shape a line, falling back face by face where the face lacks a glyph. */
11
+ export function shapeText(face: LoadedFont, text: string, size: number, tracking: number, fallbacks?: LoadedFont[]): Shaped;
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Glyph fallback: characters the chosen font lacks are shaped in a
3
+ * fallback font (the layer's `fallback`, then any font registered with
4
+ * `fallback: true`), and failing those drawn from the built-in symbols
5
+ * (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪). Text the font covers shapes exactly as
6
+ * before (one HarfBuzz run).
7
+ *
8
+ * @module type/fallback
9
+ */
10
+
11
+ import { shape } from '../text.js';
12
+ import { loadFont, pickFont } from '../fonts.js';
13
+ import { symbolFace } from './symbols.js';
14
+
15
+ /**
16
+ * The faces to fall back to, in order.
17
+ *
18
+ * @param {import('../layers.js').RenderContext} ctx
19
+ * @param {{ fallback?: string | string[], weight?: number, style?: string }} o
20
+ * @returns {Promise<import('../fonts.js').LoadedFont[]>}
21
+ */
22
+ export async function fallbackFaces(ctx, o) {
23
+ const names = o.fallback == null ? [] : [].concat(/** @type {any} */ (o.fallback));
24
+ const sources = [
25
+ ...names.map((n) => ctx.fonts.find((f) => f.name.toLowerCase() === String(n).toLowerCase()) ? pickFont(ctx.fonts, n, o.weight ?? 600, o.style) : undefined),
26
+ ...ctx.fonts.filter((f) => /** @type {any} */ (f).fallback),
27
+ ].filter(Boolean);
28
+ const faces = [];
29
+ for (const s of new Set(sources)) faces.push(await loadFont(/** @type {any} */ (s)));
30
+ faces.push(symbolFace());
31
+ return faces;
32
+ }
33
+
34
+ /** Characters each face lacks, by face. @type {WeakMap<object, Map<string, boolean>>} */
35
+ const missing = new WeakMap();
36
+
37
+ /**
38
+ * Whether a face has a glyph for a character.
39
+ *
40
+ * @param {import('../fonts.js').LoadedFont} face
41
+ * @param {string} ch
42
+ * @returns {boolean}
43
+ */
44
+ export function covers(face, ch) {
45
+ let m = missing.get(face.paths);
46
+ if (!m) missing.set(face.paths, (m = new Map()));
47
+ let has = m.get(ch);
48
+ if (has === undefined) {
49
+ has = /\s/.test(ch) || face.shapeRun(ch).every((g) => g.gid !== 0);
50
+ m.set(ch, has);
51
+ }
52
+ return has;
53
+ }
54
+
55
+ /**
56
+ * Shape a line, falling back face by face for characters the face lacks.
57
+ * Glyphs from another face carry it (`glyph.face`).
58
+ *
59
+ * @param {import('../fonts.js').LoadedFont} face
60
+ * @param {string} text
61
+ * @param {number} size
62
+ * @param {number} tracking - px between clusters.
63
+ * @param {import('../fonts.js').LoadedFont[]} [fallbacks]
64
+ * @returns {import('../text.js').Shaped}
65
+ */
66
+ export function shapeText(face, text, size, tracking, fallbacks) {
67
+ const run = shape(face, text, size, tracking);
68
+ if (!fallbacks?.length || run.glyphs.every((g) => g.gid !== 0)) return run;
69
+ // Segments of one face each, in order.
70
+ /** @type {{ face: import('../fonts.js').LoadedFont, text: string }[]} */
71
+ const segs = [];
72
+ for (const ch of Array.from(text)) {
73
+ const f = covers(face, ch) ? face : fallbacks.find((x) => covers(x, ch)) ?? face;
74
+ const last = segs[segs.length - 1];
75
+ if (last && last.face === f) last.text += ch;
76
+ else segs.push({ face: f, text: ch });
77
+ }
78
+ /** @type {import('../text.js').Shaped['glyphs']} */
79
+ const glyphs = [];
80
+ let x = 0;
81
+ let at = 0;
82
+ segs.forEach((seg, i) => {
83
+ const part = shape(seg.face, seg.text, size, tracking);
84
+ for (const g of part.glyphs) {
85
+ const placed = { ...g, x: g.x + x, cluster: (g.cluster ?? 0) + at };
86
+ glyphs.push(seg.face === face ? placed : { ...placed, face: seg.face });
87
+ }
88
+ x += part.width + (i < segs.length - 1 ? tracking : 0);
89
+ at += seg.text.length;
90
+ });
91
+ return { glyphs, width: x };
92
+ }
@@ -0,0 +1,24 @@
1
+ // Per-glyph control.
2
+ import type { Shaped } from '../text.js';
3
+ import type { Length, Color } from '../index.js';
4
+
5
+ /** Per-glyph scale, shift, turn and colour (arrays cycle; counted across the block, spaces skipped). */
6
+ export interface GlyphControl {
7
+ scale?: number | number[];
8
+ scaleX?: number | number[];
9
+ scaleY?: number | number[];
10
+ dx?: Length | Length[];
11
+ dy?: Length | Length[];
12
+ /** Degrees. */
13
+ rotate?: number | number[];
14
+ color?: Color | Color[];
15
+ /** Transforms about the glyph's middle (default) or its baseline. */
16
+ origin?: 'center' | 'baseline';
17
+ /** A scaled glyph takes its scaled width (default true). */
18
+ advance?: boolean;
19
+ }
20
+
21
+ /** Apply per-glyph control to a shaped line (`start`: the line's first glyph's index in the block). */
22
+ export function controlGlyphs(run: Shaped, spec: GlyphControl | undefined, o: { start: number; size: number; capHeight: number; canvas: { width: number; height: number }; text: string }): { run: Shaped; count: number };
23
+ /** How many glyphs a line counts (non-space characters). */
24
+ export function glyphCount(text: string): number;
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Per-glyph control: each letter its own scale, shift, turn and colour,
3
+ * from arrays (cycled), so bulging 70s type or a wave of letters is one
4
+ * text layer.
5
+ *
6
+ * `glyphs: { scale, scaleX, scaleY, dx, dy, rotate, color, origin, advance }`:
7
+ * numbers (or colours) per glyph, counted across the whole block and
8
+ * skipping spaces; `origin` is `'center'` (the default: the glyph's middle
9
+ * at half the cap height) or `'baseline'`; `advance` (default true) lets a
10
+ * scaled glyph take its scaled width, pushing its neighbours along.
11
+ *
12
+ * @module type/glyphs
13
+ */
14
+
15
+ import { len } from '../geometry.js';
16
+ import { toHex } from '../color.js';
17
+
18
+ /** @param {any} list @param {number} i @param {any} fallback @returns {any} The i-th of a cycled list (or a single value). */
19
+ const at = (list, i, fallback) => (list == null ? fallback : Array.isArray(list) ? (list.length ? list[i % list.length] : fallback) : list);
20
+
21
+ /**
22
+ * Apply per-glyph control to a shaped line: advances widened for scaled
23
+ * glyphs, and each glyph's transform (`t`, about its pen position) and
24
+ * paint key (`paint`, a colour).
25
+ *
26
+ * @param {import('../text.js').Shaped} run
27
+ * @param {any} spec - The layer's `glyphs`.
28
+ * @param {{ start: number, size: number, capHeight: number, canvas: { width: number, height: number }, text: string }} o - `start`: index of the line's first glyph in the block.
29
+ * @returns {{ run: import('../text.js').Shaped, count: number }} The run, and how many glyphs it counted.
30
+ */
31
+ export function controlGlyphs(run, spec, o) {
32
+ if (!spec) return { run, count: 0 };
33
+ const glyphs = run.glyphs.map((g) => ({ ...g }));
34
+ const blanks = blankGlyphs(run, o.text);
35
+ let shift = 0;
36
+ let n = 0;
37
+ for (let i = 0; i < glyphs.length; i++) {
38
+ const g = glyphs[i];
39
+ const adv = (i + 1 < run.glyphs.length ? run.glyphs[i + 1].x : run.width) - run.glyphs[i].x;
40
+ g.x += shift;
41
+ if (blanks.has(i)) continue;
42
+ const k = o.start + n++;
43
+ const s = at(spec.scale, k, 1);
44
+ const sx = at(spec.scaleX, k, 1) * s;
45
+ const sy = at(spec.scaleY, k, 1) * s;
46
+ const rot = (at(spec.rotate, k, 0) * Math.PI) / 180;
47
+ const dx = len(at(spec.dx, k, 0), o.canvas);
48
+ const dy = len(at(spec.dy, k, 0), o.canvas);
49
+ const color = at(spec.color, k, undefined);
50
+ const grow = spec.advance === false ? 0 : adv * (sx - 1);
51
+ // About the glyph's centre (at half the cap height), or its baseline.
52
+ const ox = (adv * (spec.advance === false ? 1 : sx)) / 2;
53
+ const oy = spec.origin === 'baseline' ? 0 : -o.capHeight / 2;
54
+ const cos = Math.cos(rot);
55
+ const sin = Math.sin(rot);
56
+ const a = cos * sx;
57
+ const b = sin * sx;
58
+ const c = -sin * sy;
59
+ const d = cos * sy;
60
+ // Glyph-local point p maps to origin + R·S·(p − origin/scaled) + shift.
61
+ const px0 = adv / 2;
62
+ g.t = [a, b, c, d, ox - (a * px0 + c * oy) + dx, oy - (b * px0 + d * oy) + dy];
63
+ if (color != null) g.paint = toHex(color);
64
+ shift += grow;
65
+ }
66
+ return { run: { glyphs, width: run.width + shift }, count: n };
67
+ }
68
+
69
+ /**
70
+ * Glyph indices that are spaces (by cluster), so they don't take a slot.
71
+ *
72
+ * @param {import('../text.js').Shaped} run
73
+ * @param {string} text
74
+ * @returns {Set<number>}
75
+ */
76
+ function blankGlyphs(run, text) {
77
+ const chars = Array.from(text);
78
+ const out = new Set();
79
+ run.glyphs.forEach((g, i) => {
80
+ const ch = /** @type {any} */ (g).cluster != null ? text[/** @type {any} */ (g).cluster] : chars[i];
81
+ if (ch != null && /\s/.test(ch)) out.add(i);
82
+ });
83
+ return out;
84
+ }
85
+
86
+ /** How many glyphs a line counts (non-space characters). @param {string} text @returns {number} */
87
+ export const glyphCount = (text) => Array.from(text).filter((ch) => !/\s/.test(ch)).length;
@@ -0,0 +1,5 @@
1
+ // Poster typesetting.
2
+ export { typeset, wantsType, TYPE_OPTIONS } from './typeset.js';
3
+ export { faceFor, mappedPath, setLine } from './line.js';
4
+ export { textPathD, pathSampler } from './path.js';
5
+ export type { TextPath } from './path.js';
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Poster typesetting: what the `text` layer does with its 1.1 options, and
3
+ * the line-setting the poster layers (tables, barcodes) share.
4
+ *
5
+ * @module type
6
+ */
7
+
8
+ export { typeset, wantsType, TYPE_OPTIONS } from './typeset.js';
9
+ export { faceFor, mappedPath, setLine } from './line.js';
10
+ export { textPathD, pathSampler } from './path.js';
@@ -0,0 +1,4 @@
1
+ // Pair kerning from a TrueType font.
2
+
3
+ /** A pair's X advance adjustment, font units (GPOS pair adjustment, else `kern`). */
4
+ export function pairKerning(v: DataView, tables: Record<string, number>): (a: number, b: number) => number;