@arraypress/covers 1.0.0 → 1.3.1

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 (246) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README.md +18 -524
  3. package/package.json +34 -5
  4. package/recipes/brand.json +1143 -0
  5. package/recipes/flyers/afterglow-motion.json +540 -0
  6. package/recipes/flyers/afterglow.json +70 -0
  7. package/recipes/flyers/arcfield-mirror-motion.json +397 -0
  8. package/recipes/flyers/arcfield-mirror.json +364 -0
  9. package/recipes/flyers/bauhaus-oktav.json +319 -0
  10. package/recipes/flyers/bauhaus-ritmo.json +735 -0
  11. package/recipes/flyers/bauhaus-volta.json +394 -0
  12. package/recipes/flyers/bloomfall.json +99 -0
  13. package/recipes/flyers/drag-solenne-motion.json +444 -0
  14. package/recipes/flyers/drag-solenne.json +426 -0
  15. package/recipes/flyers/fennick-patches.json +874 -0
  16. package/recipes/flyers/ferro-pass.json +398 -0
  17. package/recipes/flyers/ferrova.json +812 -0
  18. package/recipes/flyers/grid-rest-rhythm.json +125 -0
  19. package/recipes/flyers/groovy-honeycomb.json +816 -0
  20. package/recipes/flyers/groovy-marigold.json +479 -0
  21. package/recipes/flyers/groovy-peachfuzz.json +974 -0
  22. package/recipes/flyers/halden-orbit-motion.json +381 -0
  23. package/recipes/flyers/halden-orbit.json +356 -0
  24. package/recipes/flyers/halvard.json +120 -0
  25. package/recipes/flyers/kairo-weekender.json +696 -0
  26. package/recipes/flyers/linger-motion.json +518 -0
  27. package/recipes/flyers/linger.json +67 -0
  28. package/recipes/flyers/little-orchard.json +105 -0
  29. package/recipes/flyers/lofi-hexmoth.json +912 -0
  30. package/recipes/flyers/lofi-lowbit-motion.json +888 -0
  31. package/recipes/flyers/lofi-lowbit.json +880 -0
  32. package/recipes/flyers/lofi-nullpoint.json +3013 -0
  33. package/recipes/flyers/lumio-stack-motion.json +648 -0
  34. package/recipes/flyers/lumio-stack.json +438 -0
  35. package/recipes/flyers/magazine-cover.json +168 -0
  36. package/recipes/flyers/magazine-extended.json +269 -0
  37. package/recipes/flyers/magazine-knockout.json +40 -0
  38. package/recipes/flyers/northfold-varsity.json +618 -0
  39. package/recipes/flyers/open-studio.json +69 -0
  40. package/recipes/flyers/orrin-merch.json +715 -0
  41. package/recipes/flyers/oxyde.json +113 -0
  42. package/recipes/flyers/quiet-fields.json +610 -0
  43. package/recipes/flyers/scrapbook-halcyon.json +948 -0
  44. package/recipes/flyers/solenne-ticket.json +730 -0
  45. package/recipes/flyers/solvane-motion.json +576 -0
  46. package/recipes/flyers/solvane.json +98 -0
  47. package/recipes/flyers/subduct.json +99 -0
  48. package/recipes/flyers/sunvale-lineup.json +584 -0
  49. package/recipes/flyers/tidewell.json +91 -0
  50. package/recipes/flyers/tour-kairo.json +535 -0
  51. package/recipes/flyers/velora-stickers.json +885 -0
  52. package/recipes/flyers/wheatpaste-black.json +313 -0
  53. package/recipes/flyers/wheatpaste-mark.json +198 -0
  54. package/recipes/flyers/wheatpaste-torn.json +114 -0
  55. package/recipes/flyers/wheatpaste-wall.json +984 -0
  56. package/recipes/flyers/wheatpaste-words.json +233 -0
  57. package/recipes/release/aura.json +137 -0
  58. package/recipes/release/chrome.json +433 -0
  59. package/recipes/release/collage.json +372 -0
  60. package/recipes/release/duotone.json +113 -0
  61. package/recipes/release/film-still.json +102 -0
  62. package/recipes/release/headline.json +70 -0
  63. package/recipes/release/instant.json +170 -0
  64. package/recipes/release/label.json +364 -0
  65. package/recipes/release/liquid-glass.json +301 -0
  66. package/recipes/release/odd-tile-dark.json +688 -0
  67. package/recipes/release/odd-tile-dense.json +1056 -0
  68. package/recipes/release/odd-tile-shift.json +541 -0
  69. package/recipes/release/odd-tile-split.json +571 -0
  70. package/recipes/release/odd-tile.json +633 -0
  71. package/recipes/release/off-edge.json +96 -0
  72. package/recipes/release/plinth.json +794 -0
  73. package/recipes/release/riso.json +108 -0
  74. package/recipes/release/series/fluid-label.json +561 -0
  75. package/recipes/release/series/fluid-pixel.json +334 -0
  76. package/recipes/release/series/hairline.json +305 -0
  77. package/recipes/release/series/halo.json +282 -0
  78. package/recipes/release/series/mark-crop.json +135 -0
  79. package/recipes/release/series/mark-window.json +150 -0
  80. package/recipes/release/series/photo-label.json +475 -0
  81. package/recipes/release/series/smear-field.json +345 -0
  82. package/recipes/release/series/smear.json +202 -0
  83. package/recipes/release/series/spin.json +172 -0
  84. package/recipes/release/series/split-ghost.json +230 -0
  85. package/recipes/release/series/zoom.json +168 -0
  86. package/recipes/release/small-print.json +114 -0
  87. package/recipes/release/stack.json +156 -0
  88. package/recipes/release/swiss.json +117 -0
  89. package/schema/recipe.schema.json +7624 -827
  90. package/src/animate/index.d.ts +9 -0
  91. package/src/animate/index.js +11 -0
  92. package/src/animate/keyframes.d.ts +21 -0
  93. package/src/animate/keyframes.js +119 -0
  94. package/src/animate/marquee.d.ts +30 -0
  95. package/src/animate/marquee.js +93 -0
  96. package/src/animate/morph.d.ts +8 -0
  97. package/src/animate/morph.js +99 -0
  98. package/src/animate/motion.d.ts +59 -0
  99. package/src/animate/motion.js +63 -0
  100. package/src/animate/sheet.d.ts +19 -0
  101. package/src/animate/sheet.js +61 -0
  102. package/src/animate/timing.d.ts +28 -0
  103. package/src/animate/timing.js +146 -0
  104. package/src/animate/tracks.d.ts +18 -0
  105. package/src/animate/tracks.js +139 -0
  106. package/src/builtins.js +4 -0
  107. package/src/diagram.js +4 -3
  108. package/src/extract.js +6 -5
  109. package/src/fonts.d.ts +55 -0
  110. package/src/fonts.js +245 -53
  111. package/src/formats/cut.d.ts +10 -0
  112. package/src/formats/cut.js +77 -0
  113. package/src/formats/index.d.ts +6 -0
  114. package/src/formats/index.js +9 -0
  115. package/src/formats/presets.d.ts +41 -0
  116. package/src/formats/presets.js +89 -0
  117. package/src/formats/print.d.ts +25 -0
  118. package/src/formats/print.js +117 -0
  119. package/src/formats/zones.d.ts +10 -0
  120. package/src/formats/zones.js +64 -0
  121. package/src/geometry.js +5 -35
  122. package/src/grid.js +1 -1
  123. package/src/handmade.js +9 -7
  124. package/src/icons.js +42 -9
  125. package/src/index.d.ts +232 -55
  126. package/src/index.js +14 -6
  127. package/src/inter-data.js +27 -0
  128. package/src/json-recipe.js +201 -27
  129. package/src/kit/index.d.ts +20 -0
  130. package/src/kit/index.js +58 -0
  131. package/src/layer-options.js +43 -4
  132. package/src/layers/barcode.d.ts +19 -0
  133. package/src/layers/barcode.js +63 -0
  134. package/src/layers/code128.d.ts +4 -0
  135. package/src/layers/code128.js +28 -0
  136. package/src/layers/expr.d.ts +6 -0
  137. package/src/layers/expr.js +121 -0
  138. package/src/layers/geometry.d.ts +6 -0
  139. package/src/layers/geometry.js +238 -0
  140. package/src/layers/index.d.ts +18 -0
  141. package/src/layers/index.js +15 -0
  142. package/src/layers/perforation.d.ts +14 -0
  143. package/src/layers/perforation.js +47 -0
  144. package/src/layers/repeat.d.ts +29 -0
  145. package/src/layers/repeat.js +90 -0
  146. package/src/layers/shape.d.ts +46 -0
  147. package/src/layers/shape.js +115 -0
  148. package/src/layers/soft.d.ts +5 -0
  149. package/src/layers/soft.js +43 -0
  150. package/src/layers/svg.d.ts +23 -0
  151. package/src/layers/svg.js +108 -0
  152. package/src/layers/table.d.ts +25 -0
  153. package/src/layers/table.js +80 -0
  154. package/src/layers/tear.d.ts +21 -0
  155. package/src/layers/tear.js +74 -0
  156. package/src/layers/texture.d.ts +17 -0
  157. package/src/layers/texture.js +106 -0
  158. package/src/layers/util.d.ts +8 -0
  159. package/src/layers/util.js +61 -0
  160. package/src/layers.js +129 -61
  161. package/src/lint/image-size.d.ts +4 -0
  162. package/src/lint/image-size.js +34 -0
  163. package/src/lint/index.d.ts +16 -0
  164. package/src/lint/index.js +127 -0
  165. package/src/lint/rules.d.ts +5 -0
  166. package/src/lint/rules.js +19 -0
  167. package/src/node.d.ts +2 -0
  168. package/src/node.js +20 -0
  169. package/src/paint/index.d.ts +49 -0
  170. package/src/paint/index.js +66 -0
  171. package/src/paint/presets.d.ts +10 -0
  172. package/src/paint/presets.js +47 -0
  173. package/src/paint/tile.d.ts +7 -0
  174. package/src/paint/tile.js +55 -0
  175. package/src/palette.js +1 -1
  176. package/src/platform/files.d.ts +6 -0
  177. package/src/platform/files.js +41 -0
  178. package/src/platform/harfbuzz.d.ts +9 -0
  179. package/src/platform/harfbuzz.js +80 -0
  180. package/src/platform/index.d.ts +8 -0
  181. package/src/platform/index.js +15 -0
  182. package/src/platform/load.d.ts +6 -0
  183. package/src/platform/load.js +71 -0
  184. package/src/platform/no-harfbuzz.d.ts +2 -0
  185. package/src/platform/no-harfbuzz.js +11 -0
  186. package/src/platform/settings.d.ts +28 -0
  187. package/src/platform/settings.js +51 -0
  188. package/src/print.js +16 -13
  189. package/src/recipes.js +5 -4
  190. package/src/render/backdrop.d.ts +6 -0
  191. package/src/render/backdrop.js +55 -0
  192. package/src/render/context.d.ts +82 -0
  193. package/src/render/grain.d.ts +7 -0
  194. package/src/render/grain.js +113 -0
  195. package/src/render/group.d.ts +23 -0
  196. package/src/render/group.js +67 -0
  197. package/src/render/key.d.ts +5 -0
  198. package/src/render/key.js +90 -0
  199. package/src/render/report.d.ts +5 -0
  200. package/src/render/report.js +19 -0
  201. package/src/render/transform.d.ts +36 -0
  202. package/src/render/transform.js +139 -0
  203. package/src/render/use.d.ts +27 -0
  204. package/src/render/use.js +73 -0
  205. package/src/render/wrap.d.ts +14 -0
  206. package/src/render/wrap.js +144 -0
  207. package/src/render.js +343 -215
  208. package/src/text.d.ts +29 -0
  209. package/src/text.js +23 -16
  210. package/src/type/auto-color.d.ts +9 -0
  211. package/src/type/auto-color.js +44 -0
  212. package/src/type/fallback.d.ts +11 -0
  213. package/src/type/fallback.js +92 -0
  214. package/src/type/glyphs.d.ts +24 -0
  215. package/src/type/glyphs.js +87 -0
  216. package/src/type/index.d.ts +5 -0
  217. package/src/type/index.js +10 -0
  218. package/src/type/kern.d.ts +4 -0
  219. package/src/type/kern.js +144 -0
  220. package/src/type/layout-ref.d.ts +10 -0
  221. package/src/type/layout-ref.js +74 -0
  222. package/src/type/line.d.ts +11 -0
  223. package/src/type/line.js +105 -0
  224. package/src/type/live.d.ts +6 -0
  225. package/src/type/live.js +39 -0
  226. package/src/type/name.d.ts +4 -0
  227. package/src/type/name.js +32 -0
  228. package/src/type/path.d.ts +18 -0
  229. package/src/type/path.js +89 -0
  230. package/src/type/pixel.d.ts +7 -0
  231. package/src/type/pixel.js +85 -0
  232. package/src/type/sfnt.d.ts +20 -0
  233. package/src/type/sfnt.js +258 -0
  234. package/src/type/spans.d.ts +33 -0
  235. package/src/type/spans.js +138 -0
  236. package/src/type/symbols.d.ts +7 -0
  237. package/src/type/symbols.js +89 -0
  238. package/src/type/typeset.d.ts +10 -0
  239. package/src/type/typeset.js +363 -0
  240. package/src/type/woff2.d.ts +6 -0
  241. package/src/type/woff2.js +301 -0
  242. package/src/version.js +4 -0
  243. package/src/color.js +0 -246
  244. package/src/png.js +0 -77
  245. package/src/rng.js +0 -95
  246. package/src/svg.js +0 -69
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Setting one line of type: a face for a layer, glyph outlines through any
3
+ * affine map, and a line justified to a width (`textLength`).
4
+ *
5
+ * @module type/line
6
+ */
7
+
8
+ import { num } from '@logomarks/image-effects/svg';
9
+ import { loadFont, pickFont, variationsFor } from '../fonts.js';
10
+ import { shape, outline } from '../text.js';
11
+ import { pixelFace } from './pixel.js';
12
+
13
+ /**
14
+ * A face for a layer's family, weight, style and axes (`'pixel'` is the
15
+ * built-in 5×7 face, unless a font of that name is registered).
16
+ *
17
+ * @param {import('../layers.js').RenderContext} ctx
18
+ * @param {{ font?: string, weight?: number, style?: string, axes?: Record<string, number> }} o
19
+ * @returns {Promise<import('../fonts.js').LoadedFont>}
20
+ */
21
+ export async function faceFor(ctx, o) {
22
+ if (o.font === 'pixel' && !ctx.fonts.some((f) => f.name === 'pixel')) return pixelFace();
23
+ const weight = o.weight ?? 600;
24
+ const source = pickFont(ctx.fonts, o.font ?? 'Inter', weight, o.style);
25
+ return loadFont(source, variationsFor(source, weight, o.axes));
26
+ }
27
+
28
+ /**
29
+ * Glyph outlines of a shaped run, through an affine map: glyph point
30
+ * (gx, gy) in px (y down, baseline 0) goes to `map(gx, gy)`.
31
+ *
32
+ * @param {import('../fonts.js').LoadedFont} face
33
+ * @param {import('../text.js').Shaped} run
34
+ * @param {number} size
35
+ * @param {(gx: number, gy: number, glyph: { x: number, y: number }) => [number, number]} map
36
+ * @param {(glyph: any) => boolean} [only] - Draw only these glyphs (one paint at a time).
37
+ * @returns {string}
38
+ */
39
+ export function mappedPath(face, run, size, map, only) {
40
+ const parts = [];
41
+ for (const g of run.glyphs) {
42
+ if (only && !only(g)) continue;
43
+ const f = g.face ?? face;
44
+ const s = (g.size ?? size) / f.upem;
45
+ const t = g.t;
46
+ let coords = [];
47
+ let even = true;
48
+ let px = 0;
49
+ for (const c of outline(f, g.gid)) {
50
+ if (typeof c === 'string') {
51
+ if (coords.length) parts.push(coords.join(' '));
52
+ parts.push(c);
53
+ coords = [];
54
+ even = true;
55
+ } else if (even) {
56
+ px = c;
57
+ even = false;
58
+ } else {
59
+ // A glyph's own transform (per-glyph scale, turn, shift) is about its pen position.
60
+ const gx = px * s;
61
+ const gy = -c * s;
62
+ const [X, Y] = t ? map(g.x + t[0] * gx + t[2] * gy + t[4], g.y + t[1] * gx + t[3] * gy + t[5], g) : map(g.x + gx, g.y + gy, g);
63
+ coords.push(num(X), num(Y));
64
+ even = true;
65
+ }
66
+ }
67
+ if (coords.length) parts.push(coords.join(' '));
68
+ }
69
+ return parts.join('');
70
+ }
71
+
72
+ /**
73
+ * Set one line: shape, justify and draw.
74
+ *
75
+ * @param {object} o
76
+ * @param {import('../fonts.js').LoadedFont} o.face
77
+ * @param {string} o.text
78
+ * @param {number} o.size
79
+ * @param {number} o.tracking - Letter-spacing, px.
80
+ * @param {number} o.x - Anchor x (start, middle or end by `align`).
81
+ * @param {number} o.y - Baseline.
82
+ * @param {'left' | 'center' | 'right'} o.align
83
+ * @param {number} [o.width] - Justify to this width.
84
+ * @param {'spacingAndGlyphs' | 'spacing'} [o.adjust]
85
+ * @param {(tracking: number) => import('../text.js').Shaped} [o.shape] - Shapes the line at a tracking (fallback faces, spans, per-glyph control); default the face alone.
86
+ * @param {(glyph: any) => boolean} [o.only] - Draw only these glyphs.
87
+ * @returns {{ d: string, width: number, left: number, run: import('../text.js').Shaped, scaleX: number }}
88
+ */
89
+ export function setLine(o) {
90
+ const shapeAt = o.shape ?? ((/** @type {number} */ tracking) => shape(o.face, o.text, o.size, tracking));
91
+ let run = shapeAt(o.tracking);
92
+ let scaleX = 1;
93
+ if (o.width && run.width > 0) {
94
+ if (o.adjust === 'spacing') {
95
+ const gaps = Math.max(1, new Set(o.face.shapeRun(o.text).map((g) => g.cluster)).size - 1);
96
+ run = shapeAt(o.tracking + (o.width - run.width) / gaps);
97
+ } else {
98
+ scaleX = o.width / run.width;
99
+ }
100
+ }
101
+ const w = run.width * scaleX;
102
+ const left = o.align === 'center' ? o.x - w / 2 : o.align === 'right' ? o.x - w : o.x;
103
+ const d = mappedPath(o.face, run, o.size, (gx, gy) => [left + gx * scaleX, o.y + gy], o.only);
104
+ return { d, width: w, left, run, scaleX };
105
+ }
@@ -0,0 +1,6 @@
1
+ // Live text: @font-face rules.
2
+ import type { LoadedFont } from '../fonts.js';
3
+ import type { RenderContext } from '../render/context.js';
4
+
5
+ /** The face's `@font-face` rule (empty when this document has it already) and its family name. */
6
+ export function fontFace(face: LoadedFont, ctx: RenderContext): Promise<{ family: string; css: string }>;
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Live text: the `@font-face` rule for a face, embedded (or linked by URL)
3
+ * once per document.
4
+ *
5
+ * @module type/live
6
+ */
7
+
8
+ import { fontBytes } from '../fonts.js';
9
+ import { toBase64 } from '../platform/index.js';
10
+
11
+ /** Faces embedded in this document already (live text). @type {WeakMap<object, Set<string>>} */
12
+ const embedded = new WeakMap();
13
+
14
+ /**
15
+ * `@font-face` for a face, once per document.
16
+ *
17
+ * @param {import('../fonts.js').LoadedFont} face
18
+ * @param {import('../layers.js').RenderContext} ctx
19
+ * @returns {Promise<{ family: string, css: string }>}
20
+ */
21
+ export async function fontFace(face, ctx) {
22
+ const src = face.source;
23
+ const family = `covers-${src.name}-${Array.isArray(src.weight) ? 'var' : src.weight ?? 400}-${src.style ?? 'normal'}`.replace(/[^\w-]/g, '_');
24
+ const doc = /** @type {any} */ (ctx).doc ?? ctx;
25
+ let seen = embedded.get(doc);
26
+ if (!seen) embedded.set(doc, (seen = new Set()));
27
+ if (seen.has(family)) return { family, css: '' };
28
+ seen.add(family);
29
+ let url;
30
+ if (src.data == null && typeof src.src === 'string' && !src.src.startsWith('covers:')) url = src.src;
31
+ else {
32
+ const bytes = await fontBytes(src);
33
+ const tag = new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0);
34
+ const mime = tag === 0x774f4646 ? 'font/woff' : tag === 0x774f4632 ? 'font/woff2' : tag === 0x4f54544f ? 'font/otf' : 'font/ttf';
35
+ url = `data:${mime};base64,${toBase64(bytes)}`;
36
+ }
37
+ const weight = Array.isArray(src.weight) ? `${src.weight[0]} ${src.weight[1]}` : src.weight ?? 400;
38
+ return { family, css: `@font-face{font-family:"${family}";src:url("${url}");font-weight:${weight};font-style:${src.style ?? 'normal'}}` };
39
+ }
@@ -0,0 +1,4 @@
1
+ // A font's family name.
2
+
3
+ /** The typographic family (name ID 16), else the family (1), else ''. */
4
+ export function familyName(v: DataView, bytes: Uint8Array, base: number | undefined): string;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * A font's family name from its `name` table.
3
+ *
4
+ * @module type/name
5
+ */
6
+
7
+ /**
8
+ * The family name from the `name` table (typographic family, else family).
9
+ *
10
+ * @param {DataView} v @param {Uint8Array} bytes @param {number | undefined} base
11
+ * @returns {string}
12
+ */
13
+ export function familyName(v, bytes, base) {
14
+ if (base == null) return '';
15
+ const count = v.getUint16(base + 2);
16
+ const strings = base + v.getUint16(base + 4);
17
+ /** @type {Record<number, string>} */
18
+ const found = {};
19
+ for (let i = 0; i < count; i++) {
20
+ const r = base + 6 + i * 12;
21
+ const pid = v.getUint16(r);
22
+ const id = v.getUint16(r + 6);
23
+ if ((id !== 1 && id !== 16) || found[id]) continue;
24
+ const len = v.getUint16(r + 8);
25
+ const off = strings + v.getUint16(r + 10);
26
+ let s = '';
27
+ if (pid === 3 || pid === 0) for (let k = 0; k < len; k += 2) s += String.fromCharCode(v.getUint16(off + k));
28
+ else for (let k = 0; k < len; k++) s += String.fromCharCode(bytes[off + k]);
29
+ found[id] = s;
30
+ }
31
+ return found[16] ?? found[1] ?? '';
32
+ }
@@ -0,0 +1,18 @@
1
+ // Paths for text.
2
+ import type { Length } from '../index.js';
3
+
4
+ /** A path for text: an arc, circle, ellipse or wave, or any path data. */
5
+ export type TextPath = string | { d: string } | {
6
+ kind?: 'circle' | 'arc' | 'ellipse' | 'wave';
7
+ cx?: Length | `${number}%`; cy?: Length | `${number}%`; r?: Length; rx?: Length; ry?: Length;
8
+ /** Degrees, 0 at 12 o'clock, clockwise. */
9
+ start?: number; end?: number;
10
+ direction?: 'cw' | 'ccw'; side?: 'outside' | 'inside';
11
+ x1?: Length; x2?: Length; y?: Length; amplitude?: Length; wavelength?: Length; phase?: number;
12
+ };
13
+
14
+
15
+ /** A text path as path data, px. */
16
+ export function textPathD(p: TextPath, canvas: { width: number; height: number }): string;
17
+ /** A sampler along path data: point and unit tangent at a distance (continuing straight past the ends). */
18
+ export function pathSampler(d: string): { length: number; at: (s: number) => { x: number; y: number; tx: number; ty: number } };
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Paths for text: circles, arcs, ellipses and waves as path data, and a
3
+ * sampler that gives a point and tangent at any distance along a path.
4
+ *
5
+ * @module type/path
6
+ */
7
+
8
+ import { num } from '@logomarks/image-effects/svg';
9
+ import { len } from '@logomarks/image-effects/length';
10
+ import { flattenPath } from '../flatten.js';
11
+ import { pos } from '../layers/util.js';
12
+
13
+ /**
14
+ * A text path as path data, in px.
15
+ *
16
+ * @param {any} p - `{ kind: 'circle' | 'arc' | 'ellipse' | 'wave', … }` or `{ d }`.
17
+ * @param {{ width: number, height: number }} canvas
18
+ * @returns {string}
19
+ */
20
+ export function textPathD(p, canvas) {
21
+ const X = (/** @type {any} */ v, /** @type {any} */ f) => pos(v ?? f, 'x', canvas);
22
+ const Y = (/** @type {any} */ v, /** @type {any} */ f) => pos(v ?? f, 'y', canvas);
23
+ if (typeof p === 'string') return p;
24
+ if (p.d) return p.d;
25
+ const kind = p.kind ?? 'circle';
26
+ if (kind === 'circle' || kind === 'ellipse' || kind === 'arc') {
27
+ const cx = X(p.cx, '50%');
28
+ const cy = Y(p.cy, '50%');
29
+ const rx = len(p.rx ?? p.r, canvas, '30vmin');
30
+ const ry = len(p.ry ?? p.r, canvas, '30vmin');
31
+ // Angles: 0 is 12 o'clock, clockwise. A circle starts at its `start` and goes all the way round.
32
+ const ccw = p.direction === 'ccw' || p.side === 'inside';
33
+ const a0 = p.start ?? (kind === 'arc' ? -90 : 180);
34
+ const sweep = kind === 'arc' ? (p.end ?? 90) - a0 : 360;
35
+ const n = Math.max(24, Math.ceil(Math.abs(sweep) / 2));
36
+ const pts = [];
37
+ for (let i = 0; i <= n; i++) {
38
+ const a = ((a0 + (ccw ? -1 : 1) * Math.abs(sweep) * (i / n)) * Math.PI) / 180;
39
+ pts.push(`${i ? 'L' : 'M'}${num(cx + rx * Math.sin(a), 3)} ${num(cy - ry * Math.cos(a), 3)}`);
40
+ }
41
+ return pts.join('');
42
+ }
43
+ if (kind === 'wave') {
44
+ const x1 = X(p.x1, 0);
45
+ const x2 = X(p.x2, '100%');
46
+ const y = Y(p.y, '50%');
47
+ const amp = len(p.amplitude, canvas, '2vmin');
48
+ const wl = len(p.wavelength, canvas, '40vmin');
49
+ const ph = p.phase ?? 0;
50
+ const pts = [];
51
+ for (let x = x1, i = 0; x <= x2 + 0.01; x += 2, i++) pts.push(`${i ? 'L' : 'M'}${num(x, 3)} ${num(y + Math.sin(((x - x1) / wl) * Math.PI * 2 + ph) * amp, 3)}`);
52
+ return pts.join('');
53
+ }
54
+ throw new TypeError(`Unknown text path kind "${kind}" (circle, arc, ellipse, wave, or { d })`);
55
+ }
56
+
57
+ /**
58
+ * A sampler along path data: point and tangent at a distance.
59
+ *
60
+ * @param {string} d
61
+ * @returns {{ length: number, at: (s: number) => { x: number, y: number, tx: number, ty: number } }}
62
+ */
63
+ export function pathSampler(d) {
64
+ const subs = flattenPath(d, 1);
65
+ const pts = subs.flatMap((s) => s.points);
66
+ if (pts.length < 2) throw new TypeError('A text path needs at least two points');
67
+ const cum = [0];
68
+ for (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));
69
+ const length = cum[cum.length - 1];
70
+ return {
71
+ length,
72
+ at(s) {
73
+ const t = Math.max(0, Math.min(length, s));
74
+ let lo = 0;
75
+ let hi = cum.length - 1;
76
+ while (hi - lo > 1) { const mid = (lo + hi) >> 1; if (cum[mid] <= t) lo = mid; else hi = mid; }
77
+ const seg = cum[hi] - cum[lo] || 1;
78
+ const f = (t - cum[lo]) / seg;
79
+ const [ax, ay] = pts[lo];
80
+ const [bx, by] = pts[hi];
81
+ const dl = Math.hypot(bx - ax, by - ay) || 1;
82
+ // Beyond the ends, keep going along the end tangent.
83
+ const over = s - t;
84
+ const tx = (bx - ax) / dl;
85
+ const ty = (by - ay) / dl;
86
+ return { x: ax + (bx - ax) * f + tx * over, y: ay + (by - ay) * f + ty * over, tx, ty };
87
+ },
88
+ };
89
+ }
@@ -0,0 +1,7 @@
1
+ // The built-in 5×7 pixel face.
2
+ import type { LoadedFont } from '../fonts.js';
3
+
4
+ /** The pixel face (`font: 'pixel'`): A–Z, 0–9 and basic punctuation, as merged cell paths. */
5
+ export function pixelFace(): LoadedFont;
6
+ /** The characters it draws (lowercase is drawn as capitals). */
7
+ export const PIXEL_CHARS: string;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * A built-in 5×7 pixel face (`font: 'pixel'`): A–Z, 0–9 and basic
3
+ * punctuation, each glyph a set of square cells merged into row runs. It
4
+ * is a face like any other (layout, fitting, justification, text on a
5
+ * path, per-glyph control all work), drawn as paths. The bitmaps are
6
+ * image-effects' pixel font (its `ascii` effect draws with them).
7
+ *
8
+ * Metrics, in a 1000-unit em: a cell is 100 units, capitals are 7 cells
9
+ * (700), the advance is 6 cells (a cell's gap after each glyph), a space
10
+ * is 4 cells. Lowercase is drawn as capitals.
11
+ *
12
+ * @module type/pixel
13
+ */
14
+
15
+ import { PIXEL_GLYPHS as BITMAPS } from '@logomarks/image-effects/pixel-font';
16
+
17
+ /** Cell size, font units. */
18
+ const CELL = 100;
19
+
20
+ /** Glyph id per character (1-based; 0 is missing), and back. */
21
+ const CHARS = Object.keys(BITMAPS);
22
+ const GID = new Map(CHARS.map((c, i) => [c, i + 1]));
23
+ const SPACE = CHARS.length + 1;
24
+
25
+ /**
26
+ * A bitmap's cells as path data, row runs merged: font units, y up,
27
+ * baseline 0.
28
+ *
29
+ * @param {string} bits - 35 cells, row by row from the top.
30
+ * @returns {string}
31
+ */
32
+ function cellsPath(bits) {
33
+ let d = '';
34
+ for (let row = 0; row < 7; row++) {
35
+ const top = (7 - row) * CELL;
36
+ for (let col = 0; col < 5; col++) {
37
+ if (bits[row * 5 + col] !== '#') continue;
38
+ let end = col;
39
+ while (end + 1 < 5 && bits[row * 5 + end + 1] === '#') end++;
40
+ const x0 = col * CELL;
41
+ const x1 = (end + 1) * CELL;
42
+ d += `M${x0},${top}L${x1},${top}L${x1},${top - CELL}L${x0},${top - CELL}Z`;
43
+ col = end;
44
+ }
45
+ }
46
+ return d;
47
+ }
48
+
49
+ /** @type {Map<number, string>} */
50
+ const PATHS = new Map(CHARS.map((c, i) => [i + 1, cellsPath(BITMAPS[c])]));
51
+
52
+ /** @type {any} */
53
+ let face;
54
+
55
+ /**
56
+ * The pixel face, shaped and outlined in JavaScript (no font file).
57
+ *
58
+ * @returns {import('../fonts.js').LoadedFont}
59
+ */
60
+ export function pixelFace() {
61
+ face ??= {
62
+ name: 'pixel',
63
+ weight: 400,
64
+ style: 'normal',
65
+ font: null,
66
+ upem: 1000,
67
+ ascender: 800,
68
+ descender: -200,
69
+ capHeight: 7 * CELL,
70
+ paths: new Map(),
71
+ axes: {},
72
+ variations: {},
73
+ source: { name: 'pixel', src: 'covers:pixel', weight: 400 },
74
+ glyphToPath: (/** @type {number} */ gid) => PATHS.get(gid) ?? '',
75
+ shapeRun: (/** @type {string} */ text) => Array.from(text).map((ch, i) => {
76
+ const c = BITMAPS[ch] ? ch : ch.toUpperCase();
77
+ const gid = ch === ' ' ? SPACE : GID.get(c) ?? 0;
78
+ return { gid, cluster: i, xAdvance: ch === ' ' ? 4 * CELL : 6 * CELL, xOffset: 0, yOffset: 0 };
79
+ }),
80
+ };
81
+ return face;
82
+ }
83
+
84
+ /** Characters the pixel face draws. */
85
+ export const PIXEL_CHARS = Object.freeze(CHARS.join(''));
@@ -0,0 +1,20 @@
1
+ // A small TrueType reader.
2
+
3
+ /** A parsed TrueType face. */
4
+ export interface JsFace {
5
+ kind: 'js';
6
+ upem: number;
7
+ ascender: number;
8
+ descender: number;
9
+ capHeight: number;
10
+ /** A glyph's outline, font units, y up. */
11
+ glyphToPath: (gid: number) => string;
12
+ /** Glyphs with kerned advances, font units. */
13
+ shape: (text: string) => { gid: number; cluster: number; advance: number }[];
14
+ /** Variation axes (fvar). */
15
+ axes: Record<string, { min: number; default: number; max: number }>;
16
+ family: string;
17
+ }
18
+
19
+ /** Parse a TrueType font (sfnt bytes; not CFF). */
20
+ export function parseSfnt(bytes: Uint8Array): JsFace;
@@ -0,0 +1,258 @@
1
+ /**
2
+ * A small TrueType reader in plain JavaScript: character map, advances,
3
+ * pair kerning (GPOS pair adjustment, incl. extension lookups, and the old
4
+ * `kern` table) and glyph outlines (`glyf`, simple and composite).
5
+ *
6
+ * It's the outliner where HarfBuzz can't run (a Cloudflare Worker forbids
7
+ * compiling WebAssembly at runtime, which harfbuzzjs needs), and what
8
+ * live-text mode measures with. It doesn't do ligatures, complex scripts,
9
+ * CFF (`OTTO`) outlines or variation deltas (a variable font draws its
10
+ * default instance); HarfBuzz does all of those where it's available.
11
+ *
12
+ * @module type/sfnt
13
+ */
14
+
15
+ import { pairKerning } from './kern.js';
16
+ import { familyName } from './name.js';
17
+
18
+ /**
19
+ * @typedef {object} JsFace
20
+ * @property {'js'} kind
21
+ * @property {number} upem
22
+ * @property {number} ascender
23
+ * @property {number} descender
24
+ * @property {number} capHeight
25
+ * @property {(gid: number) => string} glyphToPath - Outline in font units, y up.
26
+ * @property {(text: string) => { gid: number, cluster: number, advance: number }[]} shape - Glyphs with kerned advances, font units.
27
+ * @property {Record<string, { min: number, default: number, max: number }>} axes - Variation axes (fvar), if any.
28
+ * @property {string} family - The font's family name (name ID 1/16), if found.
29
+ */
30
+
31
+ /**
32
+ * Parse a TrueType font.
33
+ *
34
+ * @param {Uint8Array} bytes - sfnt bytes (decoded from WOFF already).
35
+ * @returns {JsFace}
36
+ * @throws {TypeError} On a CFF font or one without the tables it needs.
37
+ */
38
+ export function parseSfnt(bytes) {
39
+ const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
40
+ const tag = v.getUint32(0);
41
+ if (tag === 0x4f54544f) throw new TypeError('This font has CFF outlines (OTTO), which the JavaScript outliner can\'t draw: use a TrueType (glyf) font here, or HarfBuzz');
42
+ /** @type {Record<string, number>} */
43
+ const tables = {};
44
+ const n = v.getUint16(4);
45
+ for (let i = 0; i < n; i++) {
46
+ const o = 12 + i * 16;
47
+ tables[String.fromCharCode(bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3])] = v.getUint32(o + 8);
48
+ }
49
+ for (const t of ['head', 'hhea', 'hmtx', 'maxp', 'cmap', 'loca', 'glyf']) if (tables[t] == null) throw new TypeError(`Font has no ${t} table`);
50
+ const u16 = (/** @type {number} */ o) => v.getUint16(o);
51
+ const i16 = (/** @type {number} */ o) => v.getInt16(o);
52
+ const u32 = (/** @type {number} */ o) => v.getUint32(o);
53
+
54
+ const upem = u16(tables.head + 18);
55
+ const longLoca = i16(tables.head + 50) === 1;
56
+ const ascender = i16(tables.hhea + 4);
57
+ const descender = i16(tables.hhea + 6);
58
+ const numH = u16(tables.hhea + 34);
59
+ const numGlyphs = u16(tables.maxp + 4);
60
+ let capHeight = 0;
61
+ if (tables['OS/2'] != null && u16(tables['OS/2']) >= 2) capHeight = i16(tables['OS/2'] + 88);
62
+
63
+ const advance = (/** @type {number} */ g) => u16(tables.hmtx + 4 * Math.min(g, numH - 1));
64
+
65
+ // cmap: prefer a full-repertoire (format 12) subtable, else BMP (format 4).
66
+ /** @type {(cp: number) => number} */
67
+ let cmap = () => 0;
68
+ {
69
+ const base = tables.cmap;
70
+ const count = u16(base + 2);
71
+ let best = -1;
72
+ let bestRank = -1;
73
+ for (let i = 0; i < count; i++) {
74
+ const pid = u16(base + 4 + i * 8);
75
+ const eid = u16(base + 6 + i * 8);
76
+ const off = base + u32(base + 8 + i * 8);
77
+ const fmt = u16(off);
78
+ const rank = fmt === 12 && (pid === 3 || pid === 0) ? 3 : fmt === 4 && ((pid === 3 && (eid === 1 || eid === 10)) || pid === 0) ? 2 : -1;
79
+ if (rank > bestRank) { bestRank = rank; best = off; }
80
+ }
81
+ if (best >= 0 && u16(best) === 4) {
82
+ const segX2 = u16(best + 6);
83
+ const ends = best + 14;
84
+ const starts = ends + segX2 + 2;
85
+ const deltas = starts + segX2;
86
+ const ranges = deltas + segX2;
87
+ cmap = (cp) => {
88
+ if (cp > 0xffff) return 0;
89
+ for (let s = 0; s < segX2; s += 2) {
90
+ if (cp > u16(ends + s)) continue;
91
+ const start = u16(starts + s);
92
+ if (cp < start) return 0;
93
+ const ro = u16(ranges + s);
94
+ if (!ro) return (cp + i16(deltas + s)) & 0xffff;
95
+ const g = u16(ranges + s + ro + (cp - start) * 2);
96
+ return g ? (g + i16(deltas + s)) & 0xffff : 0;
97
+ }
98
+ return 0;
99
+ };
100
+ } else if (best >= 0) {
101
+ const groups = u32(best + 12);
102
+ cmap = (cp) => {
103
+ let lo = 0;
104
+ let hi = groups - 1;
105
+ while (lo <= hi) {
106
+ const mid = (lo + hi) >> 1;
107
+ const g = best + 16 + mid * 12;
108
+ if (cp < u32(g)) hi = mid - 1;
109
+ else if (cp > u32(g + 4)) lo = mid + 1;
110
+ else return u32(g + 8) + (cp - u32(g));
111
+ }
112
+ return 0;
113
+ };
114
+ }
115
+ }
116
+
117
+ const kern = pairKerning(v, tables);
118
+
119
+ /** @param {number} g @returns {[number, number]} */
120
+ const locaRange = (g) => (longLoca
121
+ ? [u32(tables.loca + g * 4), u32(tables.loca + g * 4 + 4)]
122
+ : [u16(tables.loca + g * 2) * 2, u16(tables.loca + g * 2 + 2) * 2]);
123
+
124
+ /**
125
+ * A glyph's contours as points `[x, y, onCurve]`, composites resolved.
126
+ *
127
+ * @param {number} g
128
+ * @param {number} depth
129
+ * @returns {[number, number, boolean][][]}
130
+ */
131
+ const contours = (g, depth = 0) => {
132
+ if (g >= numGlyphs || depth > 8) return [];
133
+ const [start, end] = locaRange(g);
134
+ if (end <= start) return [];
135
+ const o = tables.glyf + start;
136
+ const nc = i16(o);
137
+ if (nc >= 0) {
138
+ const endPts = [];
139
+ for (let i = 0; i < nc; i++) endPts.push(u16(o + 10 + i * 2));
140
+ const count = nc ? endPts[nc - 1] + 1 : 0;
141
+ let p = o + 10 + nc * 2;
142
+ p += 2 + u16(p);
143
+ const flags = [];
144
+ while (flags.length < count) {
145
+ const f = bytes[p++];
146
+ flags.push(f);
147
+ if (f & 8) { let r = bytes[p++]; while (r--) flags.push(f); }
148
+ }
149
+ const xs = [];
150
+ let x = 0;
151
+ for (const f of flags) {
152
+ if (f & 2) { const d = bytes[p++]; x += f & 16 ? d : -d; } else if (!(f & 16)) { x += i16(p); p += 2; }
153
+ xs.push(x);
154
+ }
155
+ const ys = [];
156
+ let y = 0;
157
+ for (const f of flags) {
158
+ if (f & 4) { const d = bytes[p++]; y += f & 32 ? d : -d; } else if (!(f & 32)) { y += i16(p); p += 2; }
159
+ ys.push(y);
160
+ }
161
+ const out = [];
162
+ let s = 0;
163
+ for (const e of endPts) {
164
+ const c = [];
165
+ for (let i = s; i <= e; i++) c.push(/** @type {[number, number, boolean]} */ ([xs[i], ys[i], Boolean(flags[i] & 1)]));
166
+ out.push(c);
167
+ s = e + 1;
168
+ }
169
+ return out;
170
+ }
171
+ // Composite.
172
+ const out = [];
173
+ let p = o + 10;
174
+ for (;;) {
175
+ const f = u16(p);
176
+ const gi = u16(p + 2);
177
+ p += 4;
178
+ let dx;
179
+ let dy;
180
+ if (f & 1) { dx = f & 2 ? i16(p) : 0; dy = f & 2 ? i16(p + 2) : 0; p += 4; } else { dx = f & 2 ? v.getInt8(p) : 0; dy = f & 2 ? v.getInt8(p + 1) : 0; p += 2; }
181
+ let a = 1; let b = 0; let c = 0; let d = 1;
182
+ const f2 = (/** @type {number} */ q) => i16(q) / 16384;
183
+ if (f & 8) { a = d = f2(p); p += 2; } else if (f & 0x40) { a = f2(p); d = f2(p + 2); p += 4; } else if (f & 0x80) { a = f2(p); b = f2(p + 2); c = f2(p + 4); d = f2(p + 6); p += 8; }
184
+ for (const contour of contours(gi, depth + 1)) {
185
+ out.push(contour.map(([px, py, on]) => /** @type {[number, number, boolean]} */ ([px * a + py * c + dx, px * b + py * d + dy, on])));
186
+ }
187
+ if (!(f & 0x20)) break;
188
+ }
189
+ return out;
190
+ };
191
+
192
+ /** @type {Map<number, string>} */
193
+ const paths = new Map();
194
+ /** @param {number} g @returns {string} */
195
+ const glyphToPath = (g) => {
196
+ let d = paths.get(g);
197
+ if (d !== undefined) return d;
198
+ d = '';
199
+ for (const pts of contours(g)) {
200
+ if (!pts.length) continue;
201
+ // Start on an on-curve point (or the midpoint of two off-curve ones).
202
+ let first = pts.findIndex((q) => q[2]);
203
+ /** @type {[number, number]} */
204
+ let start;
205
+ if (first < 0) { start = [(pts[0][0] + pts[1 % pts.length][0]) / 2, (pts[0][1] + pts[1 % pts.length][1]) / 2]; first = 0; } else start = [pts[first][0], pts[first][1]];
206
+ d += `M${start[0]},${start[1]}`;
207
+ const n = pts.length;
208
+ /** @type {[number, number] | null} */
209
+ let ctrl = null;
210
+ for (let k = 1; k <= n; k++) {
211
+ const q = pts[(first + k) % n];
212
+ if (q[2]) {
213
+ d += ctrl ? `Q${ctrl[0]},${ctrl[1]} ${q[0]},${q[1]}` : `L${q[0]},${q[1]}`;
214
+ ctrl = null;
215
+ } else {
216
+ if (ctrl) {
217
+ const mx = (ctrl[0] + q[0]) / 2;
218
+ const my = (ctrl[1] + q[1]) / 2;
219
+ d += `Q${ctrl[0]},${ctrl[1]} ${mx},${my}`;
220
+ }
221
+ ctrl = [q[0], q[1]];
222
+ }
223
+ }
224
+ if (ctrl) d += `Q${ctrl[0]},${ctrl[1]} ${start[0]},${start[1]}`;
225
+ d += 'Z';
226
+ }
227
+ paths.set(g, d);
228
+ return d;
229
+ };
230
+
231
+ /** @param {string} text */
232
+ const shape = (text) => {
233
+ const out = [];
234
+ let i = 0;
235
+ for (const ch of text) {
236
+ out.push({ gid: cmap(/** @type {number} */ (ch.codePointAt(0))), cluster: i, advance: 0 });
237
+ i += ch.length;
238
+ }
239
+ for (let k = 0; k < out.length; k++) out[k].advance = advance(out[k].gid) + (k + 1 < out.length ? kern(out[k].gid, out[k + 1].gid) : 0);
240
+ return out;
241
+ };
242
+
243
+ /** @type {Record<string, { min: number, default: number, max: number }>} */
244
+ const axes = {};
245
+ if (tables.fvar != null) {
246
+ const f = tables.fvar;
247
+ const off = u16(f + 4);
248
+ const count = u16(f + 8);
249
+ const size = u16(f + 10);
250
+ const fixed = (/** @type {number} */ q) => v.getInt32(q) / 65536;
251
+ for (let k = 0; k < count; k++) {
252
+ const a = f + off + k * size;
253
+ axes[String.fromCharCode(bytes[a], bytes[a + 1], bytes[a + 2], bytes[a + 3])] = { min: fixed(a + 4), default: fixed(a + 8), max: fixed(a + 12) };
254
+ }
255
+ }
256
+
257
+ return { kind: 'js', upem, ascender, descender, capHeight, glyphToPath, shape, axes, family: familyName(v, bytes, tables.name) };
258
+ }