@arraypress/covers 1.0.0 → 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 (213) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/README.md +296 -11
  3. package/package.json +33 -5
  4. package/recipes/brand.json +1143 -0
  5. package/schema/recipe.schema.json +7300 -770
  6. package/src/animate/index.d.ts +19 -0
  7. package/src/animate/index.js +116 -0
  8. package/src/builtins.js +4 -0
  9. package/src/effects/ascii.d.ts +7 -0
  10. package/src/effects/ascii.js +93 -0
  11. package/src/effects/buffer.d.ts +60 -0
  12. package/src/effects/buffer.js +472 -0
  13. package/src/effects/detail.d.ts +12 -0
  14. package/src/effects/detail.js +124 -0
  15. package/src/effects/digital.d.ts +14 -0
  16. package/src/effects/digital.js +216 -0
  17. package/src/effects/distort.d.ts +20 -0
  18. package/src/effects/distort.js +314 -0
  19. package/src/effects/dither.d.ts +8 -0
  20. package/src/effects/dither.js +147 -0
  21. package/src/effects/drop-shadow.d.ts +4 -0
  22. package/src/effects/drop-shadow.js +45 -0
  23. package/src/effects/extrude.d.ts +5 -0
  24. package/src/effects/extrude.js +55 -0
  25. package/src/effects/film.d.ts +11 -0
  26. package/src/effects/film.js +204 -0
  27. package/src/effects/glow.d.ts +8 -0
  28. package/src/effects/glow.js +56 -0
  29. package/src/effects/grade.d.ts +30 -0
  30. package/src/effects/grade.js +274 -0
  31. package/src/effects/halftone.d.ts +8 -0
  32. package/src/effects/halftone.js +95 -0
  33. package/src/effects/index.d.ts +125 -0
  34. package/src/effects/index.js +277 -0
  35. package/src/effects/ink.d.ts +12 -0
  36. package/src/effects/ink.js +70 -0
  37. package/src/effects/lens.d.ts +14 -0
  38. package/src/effects/lens.js +277 -0
  39. package/src/effects/light.d.ts +10 -0
  40. package/src/effects/light.js +59 -0
  41. package/src/effects/looks.d.ts +9 -0
  42. package/src/effects/looks.js +74 -0
  43. package/src/effects/mask.d.ts +52 -0
  44. package/src/effects/mask.js +314 -0
  45. package/src/effects/motion.d.ts +6 -0
  46. package/src/effects/motion.js +60 -0
  47. package/src/effects/newsprint.d.ts +6 -0
  48. package/src/effects/newsprint.js +89 -0
  49. package/src/effects/optics.d.ts +10 -0
  50. package/src/effects/optics.js +213 -0
  51. package/src/effects/options.d.ts +6 -0
  52. package/src/effects/options.js +106 -0
  53. package/src/effects/paint-presets.d.ts +10 -0
  54. package/src/effects/paint-presets.js +47 -0
  55. package/src/effects/paint.d.ts +49 -0
  56. package/src/effects/paint.js +66 -0
  57. package/src/effects/pixels.d.ts +24 -0
  58. package/src/effects/pixels.js +138 -0
  59. package/src/effects/polar.d.ts +10 -0
  60. package/src/effects/polar.js +265 -0
  61. package/src/effects/region.d.ts +8 -0
  62. package/src/effects/region.js +136 -0
  63. package/src/effects/round-border.d.ts +5 -0
  64. package/src/effects/round-border.js +62 -0
  65. package/src/effects/screen.d.ts +14 -0
  66. package/src/effects/screen.js +212 -0
  67. package/src/effects/sort.d.ts +8 -0
  68. package/src/effects/sort.js +125 -0
  69. package/src/effects/symmetry.d.ts +6 -0
  70. package/src/effects/symmetry.js +74 -0
  71. package/src/effects/tile.d.ts +7 -0
  72. package/src/effects/tile.js +55 -0
  73. package/src/effects/tone.d.ts +16 -0
  74. package/src/effects/tone.js +117 -0
  75. package/src/effects/transform.d.ts +35 -0
  76. package/src/effects/transform.js +127 -0
  77. package/src/effects/util.d.ts +16 -0
  78. package/src/effects/util.js +85 -0
  79. package/src/extract.js +4 -3
  80. package/src/fonts.d.ts +55 -0
  81. package/src/fonts.js +246 -53
  82. package/src/formats/cut.d.ts +10 -0
  83. package/src/formats/cut.js +77 -0
  84. package/src/formats/index.d.ts +6 -0
  85. package/src/formats/index.js +9 -0
  86. package/src/formats/presets.d.ts +41 -0
  87. package/src/formats/presets.js +89 -0
  88. package/src/formats/print.d.ts +25 -0
  89. package/src/formats/print.js +117 -0
  90. package/src/formats/zones.d.ts +10 -0
  91. package/src/formats/zones.js +64 -0
  92. package/src/geometry.js +1 -1
  93. package/src/icons.js +43 -9
  94. package/src/index.d.ts +205 -19
  95. package/src/index.js +12 -4
  96. package/src/inter-data.js +27 -0
  97. package/src/json-recipe.js +181 -23
  98. package/src/kit/index.d.ts +20 -0
  99. package/src/kit/index.js +58 -0
  100. package/src/layer-options.js +23 -4
  101. package/src/layers/barcode.d.ts +19 -0
  102. package/src/layers/barcode.js +63 -0
  103. package/src/layers/code128.d.ts +4 -0
  104. package/src/layers/code128.js +28 -0
  105. package/src/layers/expr.d.ts +6 -0
  106. package/src/layers/expr.js +121 -0
  107. package/src/layers/geometry.d.ts +6 -0
  108. package/src/layers/geometry.js +238 -0
  109. package/src/layers/index.d.ts +18 -0
  110. package/src/layers/index.js +15 -0
  111. package/src/layers/perforation.d.ts +14 -0
  112. package/src/layers/perforation.js +47 -0
  113. package/src/layers/repeat.d.ts +29 -0
  114. package/src/layers/repeat.js +90 -0
  115. package/src/layers/shape.d.ts +46 -0
  116. package/src/layers/shape.js +115 -0
  117. package/src/layers/soft.d.ts +5 -0
  118. package/src/layers/soft.js +43 -0
  119. package/src/layers/svg.d.ts +23 -0
  120. package/src/layers/svg.js +109 -0
  121. package/src/layers/table.d.ts +25 -0
  122. package/src/layers/table.js +80 -0
  123. package/src/layers/tear.d.ts +21 -0
  124. package/src/layers/tear.js +74 -0
  125. package/src/layers/texture.d.ts +17 -0
  126. package/src/layers/texture.js +106 -0
  127. package/src/layers/util.d.ts +8 -0
  128. package/src/layers/util.js +48 -0
  129. package/src/layers.js +125 -41
  130. package/src/lint/image-size.d.ts +4 -0
  131. package/src/lint/image-size.js +34 -0
  132. package/src/lint/index.d.ts +16 -0
  133. package/src/lint/index.js +127 -0
  134. package/src/lint/rules.d.ts +5 -0
  135. package/src/lint/rules.js +19 -0
  136. package/src/node.d.ts +2 -0
  137. package/src/node.js +20 -0
  138. package/src/platform/bytes.d.ts +22 -0
  139. package/src/platform/bytes.js +103 -0
  140. package/src/platform/compress.d.ts +12 -0
  141. package/src/platform/compress.js +81 -0
  142. package/src/platform/harfbuzz.d.ts +9 -0
  143. package/src/platform/harfbuzz.js +80 -0
  144. package/src/platform/index.d.ts +8 -0
  145. package/src/platform/index.js +13 -0
  146. package/src/platform/load.d.ts +6 -0
  147. package/src/platform/load.js +72 -0
  148. package/src/platform/no-harfbuzz.d.ts +2 -0
  149. package/src/platform/no-harfbuzz.js +11 -0
  150. package/src/platform/node.d.ts +26 -0
  151. package/src/platform/node.js +86 -0
  152. package/src/platform/settings.d.ts +28 -0
  153. package/src/platform/settings.js +51 -0
  154. package/src/png.js +77 -28
  155. package/src/print.js +8 -7
  156. package/src/raster/index.d.ts +24 -0
  157. package/src/raster/index.js +171 -0
  158. package/src/recipes.js +4 -3
  159. package/src/render/backdrop.d.ts +6 -0
  160. package/src/render/backdrop.js +55 -0
  161. package/src/render/content.d.ts +5 -0
  162. package/src/render/content.js +58 -0
  163. package/src/render/context.d.ts +82 -0
  164. package/src/render/grain.d.ts +7 -0
  165. package/src/render/grain.js +113 -0
  166. package/src/render/group.d.ts +23 -0
  167. package/src/render/group.js +67 -0
  168. package/src/render/key.d.ts +5 -0
  169. package/src/render/key.js +91 -0
  170. package/src/render/report.d.ts +5 -0
  171. package/src/render/report.js +19 -0
  172. package/src/render/tiles.d.ts +6 -0
  173. package/src/render/tiles.js +77 -0
  174. package/src/render/use.d.ts +27 -0
  175. package/src/render/use.js +73 -0
  176. package/src/render/wrap.d.ts +14 -0
  177. package/src/render/wrap.js +199 -0
  178. package/src/render.js +303 -212
  179. package/src/text.d.ts +29 -0
  180. package/src/text.js +22 -15
  181. package/src/type/auto-color.d.ts +9 -0
  182. package/src/type/auto-color.js +44 -0
  183. package/src/type/fallback.d.ts +11 -0
  184. package/src/type/fallback.js +92 -0
  185. package/src/type/glyphs.d.ts +24 -0
  186. package/src/type/glyphs.js +87 -0
  187. package/src/type/index.d.ts +5 -0
  188. package/src/type/index.js +10 -0
  189. package/src/type/kern.d.ts +4 -0
  190. package/src/type/kern.js +144 -0
  191. package/src/type/layout-ref.d.ts +10 -0
  192. package/src/type/layout-ref.js +74 -0
  193. package/src/type/line.d.ts +11 -0
  194. package/src/type/line.js +105 -0
  195. package/src/type/live.d.ts +6 -0
  196. package/src/type/live.js +39 -0
  197. package/src/type/name.d.ts +4 -0
  198. package/src/type/name.js +32 -0
  199. package/src/type/path.d.ts +18 -0
  200. package/src/type/path.js +89 -0
  201. package/src/type/pixel.d.ts +9 -0
  202. package/src/type/pixel.js +115 -0
  203. package/src/type/sfnt.d.ts +20 -0
  204. package/src/type/sfnt.js +258 -0
  205. package/src/type/spans.d.ts +33 -0
  206. package/src/type/spans.js +138 -0
  207. package/src/type/symbols.d.ts +7 -0
  208. package/src/type/symbols.js +89 -0
  209. package/src/type/typeset.d.ts +10 -0
  210. package/src/type/typeset.js +352 -0
  211. package/src/type/woff2.d.ts +6 -0
  212. package/src/type/woff2.js +301 -0
  213. package/src/version.js +4 -0
@@ -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
+ }
@@ -0,0 +1,33 @@
1
+ // Mixed runs in one text layer.
2
+ import type { LoadedFont } from '../fonts.js';
3
+ import type { Shaped } from '../text.js';
4
+ import type { RenderContext } from '../render/context.js';
5
+ import type { Length, Color } from '../index.js';
6
+
7
+ /** A span as written in a text layer's `spans`. */
8
+ export interface TextSpan {
9
+ content: string;
10
+ font?: string; weight?: number; style?: 'normal' | 'italic'; axes?: Record<string, number>;
11
+ /** Size relative to the layer's (default 1). */
12
+ scale?: number;
13
+ /** Size outright. */
14
+ size?: Length;
15
+ color?: Color; fill?: Color;
16
+ /** Em. */
17
+ letterSpacing?: number;
18
+ /** Baseline shift (down is positive). */
19
+ dy?: Length;
20
+ transform?: 'uppercase' | 'lowercase' | 'none';
21
+ }
22
+
23
+ /** A resolved span. */
24
+ export interface Span { text: string; face: LoadedFont; scale: number; tracking: number; paint: string | undefined; dy: number }
25
+
26
+ /** Resolve a layer's spans (faces loaded, sizes relative to `size`). */
27
+ export function loadSpans(layer: { spans: (TextSpan | string)[]; font?: string; weight?: number; style?: string; axes?: Record<string, number>; letterSpacing?: number; transform?: string }, ctx: RenderContext, size: number): Promise<Span[]>;
28
+ /** Spans as lines (split at `\n`, wrapped at `maxWidth`). */
29
+ export function spanLines(spans: Span[], size: number, maxWidth: number, fallbacks: LoadedFont[]): Span[][];
30
+ /** Shape a line of spans as one run. */
31
+ export function shapeSpans(line: Span[], size: number, extra: number, base: LoadedFont | undefined, fallbacks: LoadedFont[]): Shaped;
32
+ /** A line's text. */
33
+ export function spanText(line: Span[]): string;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Mixed runs in one text layer: `spans`, each with its own font, weight,
3
+ * style, axes, size, colour, tracking and baseline shift, set one after
4
+ * another on the same lines (a `\n` in a span starts a new line).
5
+ *
6
+ * `spans: [{ content, font, weight, style, axes, scale, size, color, letterSpacing, dy }]`;
7
+ * anything a span leaves out comes from the layer. `scale` sizes a span
8
+ * relative to the layer (`size` gives it outright).
9
+ *
10
+ * @module type/spans
11
+ */
12
+
13
+ import { len } from '../geometry.js';
14
+ import { toHex } from '../color.js';
15
+ import { faceFor } from './line.js';
16
+ import { shapeText } from './fallback.js';
17
+
18
+ /**
19
+ * @typedef {object} Span
20
+ * @property {string} text
21
+ * @property {import('../fonts.js').LoadedFont} face
22
+ * @property {number} scale - Size relative to the layer's.
23
+ * @property {number} tracking - Em.
24
+ * @property {string | undefined} paint - A colour, or the layer's fill.
25
+ * @property {number} dy - Baseline shift, px at the layer's size (scales with it).
26
+ */
27
+
28
+ /**
29
+ * Resolve a layer's spans: faces loaded, sizes as multiples of the layer's.
30
+ *
31
+ * @param {any} layer
32
+ * @param {import('../layers.js').RenderContext} ctx
33
+ * @param {number} size - The layer's size, px.
34
+ * @returns {Promise<Span[]>}
35
+ */
36
+ export async function loadSpans(layer, ctx, size) {
37
+ const out = [];
38
+ for (const given of layer.spans) {
39
+ const s = typeof given === 'string' ? { content: given } : given;
40
+ const face = await faceFor(ctx, { font: s.font ?? layer.font, weight: s.weight ?? layer.weight, style: s.style ?? layer.style, axes: { ...layer.axes, ...s.axes } });
41
+ const kase = s.transform ?? layer.transform;
42
+ const raw = String(s.content ?? '').replace(/[\t\r ]+/g, ' ');
43
+ out.push({
44
+ text: kase === 'uppercase' ? raw.toUpperCase() : kase === 'lowercase' ? raw.toLowerCase() : raw,
45
+ face,
46
+ scale: s.size != null ? len(s.size, ctx.canvas) / size : s.scale ?? 1,
47
+ tracking: s.letterSpacing ?? layer.letterSpacing ?? -0.02,
48
+ paint: s.color ?? (typeof s.fill === 'string' ? s.fill : undefined) ? toHex(s.color ?? s.fill) : undefined,
49
+ dy: len(s.dy, ctx.canvas, 0) / size,
50
+ });
51
+ }
52
+ return out;
53
+ }
54
+
55
+ /**
56
+ * Spans as lines: split at `\n`, then (with a finite `maxWidth`) wrapped at
57
+ * spaces, greedily, at `size`.
58
+ *
59
+ * @param {Span[]} spans
60
+ * @param {number} size
61
+ * @param {number} maxWidth
62
+ * @param {import('../fonts.js').LoadedFont[]} fallbacks
63
+ * @returns {Span[][]}
64
+ */
65
+ export function spanLines(spans, size, maxWidth, fallbacks) {
66
+ /** @type {Span[][]} */
67
+ const lines = [[]];
68
+ for (const s of spans) {
69
+ s.text.split('\n').forEach((part, i) => {
70
+ if (i) lines.push([]);
71
+ if (part) lines[lines.length - 1].push({ ...s, text: part });
72
+ });
73
+ }
74
+ if (!Number.isFinite(maxWidth)) return lines;
75
+ /** @type {Span[][]} */
76
+ const out = [];
77
+ for (const line of lines) {
78
+ /** @type {Span[]} */
79
+ let cur = [];
80
+ let width = 0;
81
+ for (const s of line) {
82
+ for (const word of s.text.split(/(?<= )/)) {
83
+ const piece = { ...s, text: word };
84
+ const w = shapeSpans([piece], size, 0, undefined, fallbacks).width;
85
+ if (cur.length && width + w - trailing(piece, size) > maxWidth) {
86
+ out.push(trim(cur));
87
+ cur = [];
88
+ width = 0;
89
+ }
90
+ cur.push(piece);
91
+ width += w;
92
+ }
93
+ }
94
+ out.push(trim(cur));
95
+ }
96
+ return out;
97
+ }
98
+
99
+ /** @param {Span} s @param {number} size @returns {number} The width of a trailing space, px (it may hang past the line). */
100
+ const trailing = (s, size) => (s.text.endsWith(' ') ? size * s.scale * 0.25 : 0);
101
+
102
+ /** @param {Span[]} line @returns {Span[]} The line with its ends' spaces trimmed. */
103
+ function trim(line) {
104
+ if (!line.length) return line;
105
+ const out = line.map((s) => ({ ...s }));
106
+ out[0].text = out[0].text.replace(/^ +/, '');
107
+ out[out.length - 1].text = out[out.length - 1].text.replace(/ +$/, '');
108
+ return out.filter((s) => s.text);
109
+ }
110
+
111
+ /**
112
+ * Shape a line of spans as one run: each span in its face and size, its
113
+ * glyphs carrying them (and their colour as a paint key).
114
+ *
115
+ * @param {Span[]} line
116
+ * @param {number} size - The layer's size, px.
117
+ * @param {number} extra - Tracking added everywhere, px (justification).
118
+ * @param {import('../fonts.js').LoadedFont | undefined} base - The layer's face (its glyphs needn't carry it).
119
+ * @param {import('../fonts.js').LoadedFont[]} fallbacks
120
+ * @returns {import('../text.js').Shaped}
121
+ */
122
+ export function shapeSpans(line, size, extra, base, fallbacks) {
123
+ /** @type {import('../text.js').Shaped['glyphs']} */
124
+ const glyphs = [];
125
+ let x = 0;
126
+ let at = 0;
127
+ line.forEach((s, i) => {
128
+ const sz = size * s.scale;
129
+ const run = shapeText(s.face, s.text, sz, s.tracking * sz + extra, fallbacks);
130
+ for (const g of run.glyphs) glyphs.push({ ...g, x: g.x + x, y: g.y - s.dy * size, face: g.face ?? s.face, size: sz, paint: s.paint, cluster: (g.cluster ?? 0) + at });
131
+ x += run.width + (i < line.length - 1 ? s.tracking * sz + extra : 0);
132
+ at += s.text.length;
133
+ });
134
+ return { glyphs, width: x };
135
+ }
136
+
137
+ /** @param {Span[]} line @returns {string} The line's text. */
138
+ export const spanText = (line) => line.map((s) => s.text).join('');
@@ -0,0 +1,7 @@
1
+ // Built-in symbols, drawn as shapes (the last glyph fallback).
2
+ import type { LoadedFont } from '../fonts.js';
3
+
4
+ /** The symbol face: ✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪. */
5
+ export function symbolFace(): LoadedFont;
6
+ /** The characters it draws. */
7
+ export const SYMBOLS: string;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * A built-in face of a few symbols type often wants and fonts often lack
3
+ * (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪), drawn as shapes. The last fallback when a
4
+ * text layer's font has no glyph for a character.
5
+ *
6
+ * @module type/symbols
7
+ */
8
+
9
+ /** Advance, font units (1000 per em). */
10
+ const ADV = 820;
11
+ /** Centre of a symbol: half the advance across, about the middle of a capital up. */
12
+ const CX = ADV / 2;
13
+ const CY = 340;
14
+ const R = 360;
15
+
16
+ /** @param {[number, number][]} pts @returns {string} A closed polygon, font units (y up). */
17
+ const poly = (pts) => `M${pts.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join('L')}Z`;
18
+
19
+ /** @param {number} n @param {number} inner @param {number} [turn=0] @returns {[number, number][]} A star's points (clockwise from the top). */
20
+ function star(n, inner, turn = 0) {
21
+ return Array.from({ length: n * 2 }, (_, i) => {
22
+ const a = (i / (n * 2)) * Math.PI * 2 + turn;
23
+ const r = i % 2 ? R * inner : R;
24
+ return /** @type {[number, number]} */ ([CX + r * Math.sin(a), CY + r * Math.cos(a)]);
25
+ });
26
+ }
27
+
28
+ /** @param {number} k - Size, 0–1. @returns {[number, number][]} A heart, clockwise. */
29
+ function heart(k) {
30
+ return Array.from({ length: 72 }, (_, i) => {
31
+ const t = (i / 72) * Math.PI * 2;
32
+ const x = 16 * Math.sin(t) ** 3;
33
+ const y = 13 * Math.cos(t) - 5 * Math.cos(2 * t) - 2 * Math.cos(3 * t) - Math.cos(4 * t);
34
+ return /** @type {[number, number]} */ ([CX + x * 22 * k, CY + 30 + y * 22 * k]);
35
+ });
36
+ }
37
+
38
+ /** @param {number} r @param {number} [n=48] @returns {[number, number][]} */
39
+ const circle = (r, n = 48) => Array.from({ length: n }, (_, i) => /** @type {[number, number]} */ ([CX + r * Math.sin((i / n) * Math.PI * 2), CY + r * Math.cos((i / n) * Math.PI * 2)]));
40
+
41
+ /** Outlines by character. An inner contour runs the other way (a hole). */
42
+ const SHAPES = /** @type {Record<string, string>} */ ({
43
+ '✺': poly(star(16, 0.5)),
44
+ '✦': poly(star(4, 0.32)),
45
+ '✶': poly(star(6, 0.45)),
46
+ '★': poly(star(5, 0.4)),
47
+ '☆': poly(star(5, 0.4)) + poly(star(5, 0.4).map(([x, y]) => /** @type {[number, number]} */ ([CX + (x - CX) * 0.7, CY + (y - CY) * 0.7])).reverse()),
48
+ '♥': poly(heart(1)),
49
+ '♡': poly(heart(1)) + poly(heart(0.72).map(([x, y]) => /** @type {[number, number]} */ ([x, y + 28])).reverse()),
50
+ '▶': poly([[CX - R * 0.8, CY + R], [CX + R, CY], [CX - R * 0.8, CY - R]]),
51
+ '◀': poly([[CX + R * 0.8, CY + R], [CX + R * 0.8, CY - R], [CX - R, CY]]),
52
+ '●': poly(circle(R * 0.8)),
53
+ '◆': poly([[CX, CY + R], [CX + R * 0.8, CY], [CX, CY - R], [CX - R * 0.8, CY]]),
54
+ '■': poly([[CX - R * 0.75, CY + R * 0.75], [CX + R * 0.75, CY + R * 0.75], [CX + R * 0.75, CY - R * 0.75], [CX - R * 0.75, CY - R * 0.75]]),
55
+ '♪': poly([[CX - 40, CY + R], [CX + 220, CY + R * 0.7], [CX + 220, CY + R * 0.45], [CX + 20, CY + R * 0.72], [CX + 20, CY - R * 0.55], [CX - 40, CY - R * 0.55]]) + poly(circle(110).map(([x, y]) => /** @type {[number, number]} */ ([x - 120, y - R * 0.6]))),
56
+ });
57
+
58
+ const CHARS = Object.keys(SHAPES);
59
+
60
+ /** @type {any} */
61
+ let face;
62
+
63
+ /**
64
+ * The symbol face (shaped and outlined in JavaScript).
65
+ *
66
+ * @returns {import('../fonts.js').LoadedFont}
67
+ */
68
+ export function symbolFace() {
69
+ face ??= {
70
+ name: 'symbols',
71
+ weight: 400,
72
+ style: 'normal',
73
+ font: null,
74
+ upem: 1000,
75
+ ascender: 800,
76
+ descender: -200,
77
+ capHeight: 700,
78
+ paths: new Map(),
79
+ axes: {},
80
+ variations: {},
81
+ source: { name: 'symbols', src: 'covers:symbols', weight: 400 },
82
+ glyphToPath: (/** @type {number} */ gid) => SHAPES[CHARS[gid - 1]] ?? '',
83
+ shapeRun: (/** @type {string} */ text) => Array.from(text).map((ch, i) => ({ gid: CHARS.indexOf(ch) + 1, cluster: i, xAdvance: ADV, xOffset: 0, yOffset: 0 })),
84
+ };
85
+ return face;
86
+ }
87
+
88
+ /** The characters the symbol face draws. */
89
+ export const SYMBOLS = Object.freeze(CHARS.join(''));
@@ -0,0 +1,10 @@
1
+ // The text layer with its 1.1 options.
2
+ import type { TextLayer } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+
5
+ /** The options that switch a text layer onto the poster typesetter. */
6
+ export const TYPE_OPTIONS: readonly (keyof TextLayer)[];
7
+ /** Whether a text layer uses any of them. */
8
+ export function wantsType(layer: TextLayer): boolean;
9
+ /** Draw a text layer with its 1.1 options. */
10
+ export function typeset(layer: TextLayer, ctx: RenderContext): Promise<Markup>;