@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,25 @@
1
+ // Print set-up: sizes at a dpi, bleed, safe margin, crop marks, die lines.
2
+ import type { Zones, FormatName, Format } from './presets.js';
3
+
4
+ /** Print set-up, px. */
5
+ export interface PrintInfo {
6
+ dpi: number;
7
+ unit: 'mm' | 'in';
8
+ trimWidth: number;
9
+ trimHeight: number;
10
+ bleed: number;
11
+ safe: number;
12
+ slug: number;
13
+ cropMarks: boolean;
14
+ cut?: import('./cut.js').Cut;
15
+ cutLine: boolean;
16
+ mm: number;
17
+ }
18
+
19
+
20
+ /** A length in a unit as px at a dpi. */
21
+ export function toPx(v: number, unit: string, dpi: number): number;
22
+ /** A cover's `format` as its canvas, print set-up and zones (undefined without one). */
23
+ export function resolveFormat(o: { format?: FormatName | (Partial<Format> & { width: number; height: number }); width?: number; height?: number; dpi?: number; bleed?: number | false; safe?: number; cropMarks?: boolean; cutLine?: boolean; cut?: import('./cut.js').Cut }): { width: number; height: number; format?: string; zones?: Zones; print?: PrintInfo } | undefined;
24
+ /** Crop marks (slug coordinates) and the die line (bleed-box coordinates). */
25
+ export function printMarks(p: PrintInfo, w: number, h: number): { marks: string; cut: string };
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Named formats: release covers, social posts, flyers and print stationery,
3
+ * alongside `aspect`.
4
+ *
5
+ * Screen formats are pixel sizes. Print formats are a trim size in
6
+ * millimetres or inches, rendered at `dpi` (300 by default) with a bleed
7
+ * (3 mm by default) and a safe area inside the trim; `cropMarks: true`
8
+ * adds a slug with crop marks round the bleed. The layers are drawn on the
9
+ * bleed box (trim plus bleed on every side), so backgrounds run off the
10
+ * edge, and text and icons keep their default margins inside the safe area.
11
+ *
12
+ * @module formats/print
13
+ */
14
+
15
+ import { FORMATS } from './presets.js';
16
+ import { scaleZones } from './zones.js';
17
+ import { cutPath } from './cut.js';
18
+
19
+ /** @param {number} v @param {string} unit @param {number} dpi @returns {number} px */
20
+ export function toPx(v, unit, dpi) {
21
+ return unit === 'mm' ? (v / 25.4) * dpi : unit === 'in' ? v * dpi : v;
22
+ }
23
+
24
+ /**
25
+ * The print set-up of a cover, in px.
26
+ *
27
+ * @typedef {object} PrintInfo
28
+ * @property {number} dpi
29
+ * @property {'mm' | 'in'} unit - The format's unit.
30
+ * @property {number} trimWidth - px.
31
+ * @property {number} trimHeight - px.
32
+ * @property {number} bleed - px on each side.
33
+ * @property {number} safe - px inside the trim on each side.
34
+ * @property {number} slug - px of slug outside the bleed (crop marks), 0 without.
35
+ * @property {boolean} cropMarks
36
+ * @property {import('./cut.js').Cut} [cut] - Die-cut shape.
37
+ * @property {boolean} cutLine - Draw the die line.
38
+ * @property {number} mm - px per millimetre.
39
+ */
40
+
41
+ /**
42
+ * Resolve a cover's `format` (a name or a custom `{ width, height, unit }`)
43
+ * to its canvas and print set-up.
44
+ *
45
+ * @param {any} o - Cover options: `format`, `dpi`, `bleed` (mm, or `false`), `safe` (mm), `cropMarks`, `cutLine`.
46
+ * @returns {{ width: number, height: number, print?: PrintInfo, format?: string, zones?: import('./index.js').Zones } | undefined}
47
+ */
48
+ export function resolveFormat(o) {
49
+ if (o.format == null) return undefined;
50
+ const f = typeof o.format === 'string' ? FORMATS[o.format] : { unit: 'px', ...o.format };
51
+ if (!f) throw new RangeError(`Unknown format "${o.format}". Formats: ${Object.keys(FORMATS).join(', ')}`);
52
+ const name = typeof o.format === 'string' ? o.format : undefined;
53
+ if (f.unit === 'px') {
54
+ // A screen format: one side given scales the zones and the other side.
55
+ const k = o.width && !o.height ? o.width / f.width : 1;
56
+ const zones = f.zones ? scaleZones(f.zones, k) : undefined;
57
+ return { width: Math.round(f.width * k), height: Math.round(f.height * k), format: name, zones };
58
+ }
59
+ const dpi = o.dpi ?? 300;
60
+ const mm = dpi / 25.4;
61
+ const trimWidth = Math.round(toPx(f.width, f.unit, dpi));
62
+ const trimHeight = Math.round(toPx(f.height, f.unit, dpi));
63
+ const bleed = o.bleed === false ? 0 : Math.round((o.bleed ?? f.bleed ?? 3) * mm);
64
+ const safe = Math.round((o.safe ?? f.safe ?? 3) * mm);
65
+ const cropMarks = Boolean(o.cropMarks);
66
+ const slug = cropMarks ? Math.round(Math.max(6, (o.bleed === false ? 0 : o.bleed ?? f.bleed ?? 3) + 3) * mm) : 0;
67
+ return {
68
+ width: trimWidth + 2 * bleed,
69
+ height: trimHeight + 2 * bleed,
70
+ format: name,
71
+ print: { dpi, unit: /** @type {'mm' | 'in'} */ (f.unit), trimWidth, trimHeight, bleed, safe, slug, cropMarks, cut: o.cut ?? f.cut, cutLine: Boolean(o.cutLine), mm },
72
+ };
73
+ }
74
+
75
+ /**
76
+ * Crop marks in the slug, a die line, and the document's outer size.
77
+ *
78
+ * @param {PrintInfo} p
79
+ * @param {number} w - Bleed-box width, px.
80
+ * @param {number} h - Bleed-box height, px.
81
+ * @returns {{ marks: string, cut: string }} Markup: marks in slug coordinates (the bleed box starts at `slug`), and the die line in bleed-box coordinates.
82
+ */
83
+ export function printMarks(p, w, h) {
84
+ const n = (/** @type {number} */ v) => Math.round(v * 100) / 100;
85
+ let marks = '';
86
+ if (p.cropMarks) {
87
+ const s = p.slug;
88
+ const b = p.bleed;
89
+ const len = s - b - p.mm;
90
+ const gap = b + p.mm;
91
+ const stroke = Math.max(0.5, 0.25 * p.mm);
92
+ const xs = [s + b, s + w - b];
93
+ const ys = [s + b, s + h - b];
94
+ let d = '';
95
+ for (const x of xs) for (const y of ys) {
96
+ const dx = x < s + w / 2 ? -1 : 1;
97
+ const dy = y < s + h / 2 ? -1 : 1;
98
+ d += `M${n(x + dx * gap)} ${n(y)}h${n(dx * len)}M${n(x)} ${n(y + dy * gap)}v${n(dy * len)}`;
99
+ }
100
+ marks = `<path d="${d}" fill="none" stroke="#000" stroke-width="${n(stroke)}"/>`;
101
+ }
102
+ let cut = '';
103
+ if (p.cutLine) {
104
+ const x = p.bleed;
105
+ const y = p.bleed;
106
+ const cw = p.trimWidth;
107
+ const ch = p.trimHeight;
108
+ const custom = p.cut && typeof p.cut === 'object';
109
+ const shape = custom
110
+ ? `<path d="${cutPath(p.cut, { x, y, width: cw, height: ch }, p.mm).map((g) => g.cmd + g.pts.map(n).join(' ')).join('')}"`
111
+ : p.cut === 'circle'
112
+ ? `<ellipse cx="${n(x + cw / 2)}" cy="${n(y + ch / 2)}" rx="${n(cw / 2)}" ry="${n(ch / 2)}"`
113
+ : `<rect x="${n(x)}" y="${n(y)}" width="${n(cw)}" height="${n(ch)}"${p.cut === 'rounded' ? ` rx="${n(3 * p.mm)}"` : ''}`;
114
+ cut = `${shape} fill="none" stroke="#ec008c" stroke-width="${n(Math.max(0.5, 0.25 * p.mm))}" id="CutContour"/>`;
115
+ }
116
+ return { marks, cut };
117
+ }
@@ -0,0 +1,10 @@
1
+ // Safe zones.
2
+ import type { Zones } from './presets.js';
3
+ import type { PrintInfo } from './print.js';
4
+
5
+ /** A preview overlay of the zones: safe area dashed green, covered areas red, crops blue. */
6
+ export function zonesOverlay(z: Zones, w: number, h: number): string;
7
+ /** Zones scaled by `k`. */
8
+ export function scaleZones(z: Zones, k: number): Zones;
9
+ /** A cover's safe box, px. */
10
+ export function safeBox(c: { width: number; height: number; print?: PrintInfo; zones?: Zones }): { x: number; y: number; width: number; height: number } | undefined;
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Safe zones: the box layouts fit into, scaling, and a preview overlay.
3
+ *
4
+ * @module formats/zones
5
+ */
6
+
7
+ /**
8
+ * A preview overlay of a format's zones: the safe area dashed, covered
9
+ * areas tinted red, device crops outlined.
10
+ *
11
+ * @param {import('./index.js').Zones} z
12
+ * @param {number} w @param {number} h
13
+ * @returns {string} Markup.
14
+ */
15
+ export function zonesOverlay(z, w, h) {
16
+ const n = (/** @type {number} */ v) => Math.round(v * 100) / 100;
17
+ const sw = n(Math.max(1, Math.min(w, h) / 360));
18
+ let out = '';
19
+ for (const c of z.crops ?? []) {
20
+ const [x, y, cw, ch] = c.rect;
21
+ if (c.name === 'circle') {
22
+ out += `<path d="M0 0H${n(w)}V${n(h)}H0ZM${n(x)} ${n(y + ch / 2)}a${n(cw / 2)} ${n(ch / 2)} 0 1 0 ${n(cw)} 0a${n(cw / 2)} ${n(ch / 2)} 0 1 0 ${n(-cw)} 0Z" fill="#000" fill-opacity="0.45" fill-rule="evenodd"/>`;
23
+ } else {
24
+ out += `<rect x="${n(x)}" y="${n(y)}" width="${n(cw)}" height="${n(ch)}" fill="none" stroke="#00b3ff" stroke-width="${sw}" stroke-dasharray="${n(sw * 2)} ${n(sw * 3)}"/>`;
25
+ }
26
+ }
27
+ for (const a of z.avoid ?? []) {
28
+ if (a.circle) out += `<circle cx="${n(a.circle[0])}" cy="${n(a.circle[1])}" r="${n(a.circle[2])}" fill="#ff2d55" fill-opacity="0.35" stroke="#ff2d55" stroke-width="${sw}"/>`;
29
+ if (a.rect) out += `<rect x="${n(a.rect[0])}" y="${n(a.rect[1])}" width="${n(a.rect[2])}" height="${n(a.rect[3])}" fill="#ff2d55" fill-opacity="0.3"/>`;
30
+ }
31
+ const [x, y, sw2, sh] = z.safe;
32
+ out += `<rect x="${n(x)}" y="${n(y)}" width="${n(sw2)}" height="${n(sh)}" fill="none" stroke="#18e05a" stroke-width="${n(sw * 2)}" stroke-dasharray="${n(sw * 8)} ${n(sw * 5)}"/>`;
33
+ return `<g pointer-events="none">${out}</g>`;
34
+ }
35
+
36
+ /**
37
+ * @param {import('./index.js').Zones} z @param {number} k
38
+ * @returns {import('./index.js').Zones}
39
+ */
40
+ export function scaleZones(z, k) {
41
+ if (k === 1) return z;
42
+ const r = (/** @type {number[]} */ a) => /** @type {any} */ (a.map((v) => Math.round(v * k)));
43
+ return { ...z, safe: r(z.safe), avoid: z.avoid?.map((a) => ({ ...a, rect: a.rect && r(a.rect), circle: a.circle && r(a.circle) })), crops: z.crops?.map((c) => ({ ...c, rect: r(c.rect) })) };
44
+ }
45
+
46
+
47
+ /**
48
+ * The safe box of a cover, px: a format's safe zone, or a print piece's
49
+ * trim less its safe margin. Undefined for a plain canvas.
50
+ *
51
+ * @param {{ width: number, height: number, print?: import('./index.js').PrintInfo, zones?: import('./index.js').Zones }} c
52
+ * @returns {{ x: number, y: number, width: number, height: number } | undefined}
53
+ */
54
+ export function safeBox(c) {
55
+ if (c.zones) {
56
+ const [x, y, width, height] = c.zones.safe;
57
+ return { x, y, width, height };
58
+ }
59
+ if (c.print) {
60
+ const i = c.print.bleed + c.print.safe;
61
+ return { x: i, y: i, width: c.width - 2 * i, height: c.height - 2 * i };
62
+ }
63
+ return undefined;
64
+ }
package/src/geometry.js CHANGED
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Canvas size, lengths and anchoring.
3
- *
4
- * Lengths are pixels when given as numbers, or CSS-style viewport units as
5
- * strings: `'6vmin'` (6% of the shorter side), `'4vw'`, `'4vh'`, `'5vmax'`.
6
- * Defaults are written in these units, so a cover keeps its proportions at
7
- * any output size.
2
+ * Canvas size and anchoring. Lengths (`'6vmin'`, `'4vw'`…) resolve with
3
+ * image-effects' `len`, so layers and effects read them the same way.
8
4
  *
9
5
  * @module geometry
10
6
  */
11
7
 
8
+ import { len } from '@logomarks/image-effects/length';
9
+
12
10
  /** Named aspect ratios, as `width / height`. */
13
11
  export const ASPECTS = Object.freeze({
14
12
  '1:1': 1,
@@ -77,36 +75,8 @@ function checkSize(w, h) {
77
75
  return { width, height };
78
76
  }
79
77
 
80
- /**
81
- * Resolve a length to pixels.
82
- *
83
- * @param {number | string | undefined | null} value - Pixels, or `'Nvmin'` / `'Nvw'` / `'Nvh'` / `'Nvmax'`.
84
- * @param {{ width: number, height: number }} canvas - The canvas size.
85
- * @param {number | string} [fallback=0] - Used when `value` is undefined or null.
86
- * @returns {number} Pixels.
87
- * @throws {TypeError} On a string with an unknown unit.
88
- *
89
- * @example
90
- * len('5vmin', { width: 1200, height: 630 }); // 31.5
91
- * len(undefined, canvas, '6vmin'); // the default
92
- */
93
- export function len(value, canvas, fallback = 0) {
94
- const v = value ?? fallback;
95
- if (typeof v === 'number') return v;
96
- const m = String(v).match(/^\s*(-?\d*\.?\d+)\s*(vmin|vmax|vw|vh|px)?\s*$/);
97
- if (!m) throw new TypeError(`Invalid length "${v}" (use px numbers or vmin/vmax/vw/vh)`);
98
- const n = parseFloat(m[1]);
99
- switch (m[2]) {
100
- case 'vmin': return (n / 100) * Math.min(canvas.width, canvas.height);
101
- case 'vmax': return (n / 100) * Math.max(canvas.width, canvas.height);
102
- case 'vw': return (n / 100) * canvas.width;
103
- case 'vh': return (n / 100) * canvas.height;
104
- default: return n;
105
- }
106
- }
107
-
108
78
  /** The nine anchors, as fractions of the free space along x and y. */
109
- const ANCHORS = Object.freeze({
79
+ export const ANCHORS = Object.freeze({
110
80
  'top-left': [0, 0], top: [0.5, 0], 'top-right': [1, 0],
111
81
  left: [0, 0.5], center: [0.5, 0.5], right: [1, 0.5],
112
82
  'bottom-left': [0, 1], bottom: [0.5, 1], 'bottom-right': [1, 1],
package/src/grid.js CHANGED
@@ -4,7 +4,7 @@
4
4
  * @module grid
5
5
  */
6
6
 
7
- import { num } from './svg.js';
7
+ import { num } from '@logomarks/image-effects/svg';
8
8
 
9
9
  /**
10
10
  * Path data for full-length grid lines at `xs` (columns) and `ys` (rows),
package/src/handmade.js CHANGED
@@ -11,9 +11,11 @@
11
11
  * @module handmade
12
12
  */
13
13
 
14
- import { attrs, num, layerAttrs } from './svg.js';
15
- import { len, regionPx } from './geometry.js';
16
- import { toHex } from './color.js';
14
+ import { attrs, num } from '@logomarks/image-effects/svg';
15
+ import { layerAttrs } from './layers/util.js';
16
+ import { len } from '@logomarks/image-effects/length';
17
+ import { regionPx } from './geometry.js';
18
+ import { toHex } from '@logomarks/image-effects/color';
17
19
  import { resolveIcon, parseSvgIcon } from './icons.js';
18
20
  import { flattenBody, densify } from './flatten.js';
19
21
 
@@ -25,7 +27,7 @@ export const INK = '#141413';
25
27
  /**
26
28
  * Smooth 1D value noise from a seeded stream: `noise(t)` in [-1, 1].
27
29
  *
28
- * @param {import('./rng.js').Rng} rng
30
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
29
31
  * @returns {(t: number) => number}
30
32
  */
31
33
  function noise1(rng) {
@@ -47,7 +49,7 @@ function noise1(rng) {
47
49
  * @param {[number, number][]} outline - A closed polyline (first point not repeated).
48
50
  * @param {number} cut - Cut length in px.
49
51
  * @param {number} wobble - Max nudge in px.
50
- * @param {import('./rng.js').Rng} rng
52
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
51
53
  * @returns {string} Path data.
52
54
  */
53
55
  function scissor(outline, cut, wobble, rng) {
@@ -74,7 +76,7 @@ function scissor(outline, cut, wobble, rng) {
74
76
  *
75
77
  * @param {string} kind - rect | square | circle | blob | arch | pill
76
78
  * @param {number} w @param {number} h
77
- * @param {import('./rng.js').Rng} rng
79
+ * @param {import('@logomarks/image-effects/rng').Rng} rng
78
80
  * @returns {[number, number][]}
79
81
  */
80
82
  function outlineOf(kind, w, h, rng) {
@@ -176,7 +178,7 @@ export function paper(layer, ctx) {
176
178
  * @param {number} o.amp - Wobble amplitude in px.
177
179
  * @param {number} o.period - Wobble period in px.
178
180
  * @param {string} o.color
179
- * @param {import('./rng.js').Rng} o.rng
181
+ * @param {import('@logomarks/image-effects/rng').Rng} o.rng
180
182
  * @returns {string} Markup.
181
183
  */
182
184
  function handLine(pts, { width, amp, period, color, rng }) {
package/src/icons.js CHANGED
@@ -14,12 +14,7 @@
14
14
  * @module icons
15
15
  */
16
16
 
17
- import { readFileSync } from 'node:fs';
18
- import { createRequire } from 'node:module';
19
- import { join } from 'node:path';
20
- import { fileURLToPath } from 'node:url';
21
-
22
- const require = createRequire(import.meta.url);
17
+ import { node, settings, loadBytes, utf8 } from './platform/index.js';
23
18
 
24
19
  /** @type {Map<string, IconSet>} */
25
20
  const sets = new Map();
@@ -72,17 +67,53 @@ function loadSet(prefix) {
72
67
  let set = sets.get(prefix);
73
68
  if (set) return set;
74
69
  const pkg = `@iconify-json/${prefix}`;
70
+ const mod = node('module');
71
+ const fs = node('fs');
72
+ const path = node('path');
73
+ const url = node('url');
75
74
  let file;
76
75
  try {
77
- file = join(require.resolve(`${pkg}/package.json`, { paths: [process.cwd(), fileURLToPath(new URL('.', import.meta.url))] }), '..', 'icons.json');
76
+ if (!mod || !fs) throw new Error('no node');
77
+ const require = mod.createRequire(import.meta.url);
78
+ const here = String(import.meta.url).startsWith('file:') ? [url.fileURLToPath(new URL('.', import.meta.url))] : [];
79
+ file = path.join(require.resolve(`${pkg}/package.json`, { paths: [/** @type {any} */ (globalThis).process.cwd(), ...here] }), '..', 'icons.json');
78
80
  } catch {
79
- throw new Error(`Icon set "${prefix}" is not available. Install ${pkg}, or pass the set to registerIcons().`);
81
+ throw new Error(`Icon set "${prefix}" is not available. Install ${pkg}, or pass the set to registerIcons()${mod ? '' : ' (or configure({ icons }) with a URL to load sets from)'}.`);
80
82
  }
81
- set = /** @type {IconSet} */ (JSON.parse(readFileSync(file, 'utf8')));
83
+ set = /** @type {IconSet} */ (JSON.parse(fs.readFileSync(file, 'utf8')));
82
84
  sets.set(prefix, set);
83
85
  return set;
84
86
  }
85
87
 
88
+ /**
89
+ * Make sure the icon sets these names need are loaded: from Node's
90
+ * `node_modules`, or (in a browser or Worker) from `configure({ icons })`,
91
+ * a URL template like `'https://cdn.jsdelivr.net/npm/@iconify-json/{prefix}/icons.json'`
92
+ * or a function of the prefix. Names whose set can't be found are left for
93
+ * {@link resolveIcon} to report.
94
+ *
95
+ * @param {Iterable<string>} names - Candidate icon names (`prefix:name`).
96
+ * @returns {Promise<void>}
97
+ */
98
+ export async function preloadIcons(names) {
99
+ const prefixes = new Set();
100
+ for (const n of names) {
101
+ const m = /^([a-z0-9-]+):([a-z0-9-]+)$/i.exec(n);
102
+ if (m && !sets.has(m[1])) prefixes.add(m[1]);
103
+ }
104
+ for (const prefix of prefixes) {
105
+ if (node('module')) {
106
+ try { loadSet(prefix); } catch { /* reported when drawn */ }
107
+ continue;
108
+ }
109
+ const where = typeof settings.icons === 'function' ? settings.icons(prefix) : settings.icons?.replace('{prefix}', prefix);
110
+ if (!where) continue;
111
+ try {
112
+ registerIcons(JSON.parse(utf8(await loadBytes(where, 'icons'))));
113
+ } catch { /* reported when drawn */ }
114
+ }
115
+ }
116
+
86
117
  /**
87
118
  * Resolve an Iconify name (`'prefix:name'`) to its markup and viewBox.
88
119
  *
@@ -128,6 +159,8 @@ export function resolveIcon(name) {
128
159
  */
129
160
  export function parseSvgIcon(svg) {
130
161
  const open = svg.match(/<svg\b[^>]*>/i);
162
+ // Bare markup (a mark's paths on a 512 grid): wrap it.
163
+ if (!open && /^\s*<(path|g|circle|rect|polygon|ellipse|line|polyline)\b/i.test(svg)) return parseSvgIcon(`<svg viewBox="0 0 512 512">${svg}</svg>`);
131
164
  if (!open) throw new Error('Icon SVG must contain an <svg> element');
132
165
  const tag = open[0];
133
166
  const vb = tag.match(/viewBox\s*=\s*["']([^"']+)["']/i);