@arraypress/covers 1.1.0 → 1.4.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 (259) hide show
  1. package/CHANGELOG.md +60 -0
  2. package/README.md +18 -809
  3. package/package.json +3 -1
  4. package/recipes/flyers/afterglow-motion.json +540 -0
  5. package/recipes/flyers/afterglow.json +70 -0
  6. package/recipes/flyers/arcfield-mirror-motion.json +397 -0
  7. package/recipes/flyers/arcfield-mirror.json +364 -0
  8. package/recipes/flyers/bauhaus-oktav.json +319 -0
  9. package/recipes/flyers/bauhaus-ritmo.json +735 -0
  10. package/recipes/flyers/bauhaus-volta.json +394 -0
  11. package/recipes/flyers/bloomfall.json +99 -0
  12. package/recipes/flyers/drag-solenne-motion.json +444 -0
  13. package/recipes/flyers/drag-solenne.json +426 -0
  14. package/recipes/flyers/fennick-patches.json +874 -0
  15. package/recipes/flyers/ferro-pass.json +398 -0
  16. package/recipes/flyers/ferrova.json +812 -0
  17. package/recipes/flyers/grid-poster.json +91 -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/logofolio.json +60 -0
  34. package/recipes/flyers/lumio-stack-motion.json +648 -0
  35. package/recipes/flyers/lumio-stack.json +438 -0
  36. package/recipes/flyers/magazine-cover.json +168 -0
  37. package/recipes/flyers/magazine-extended.json +269 -0
  38. package/recipes/flyers/magazine-knockout.json +40 -0
  39. package/recipes/flyers/northfold-varsity.json +618 -0
  40. package/recipes/flyers/open-studio.json +69 -0
  41. package/recipes/flyers/orrin-merch.json +715 -0
  42. package/recipes/flyers/oxyde.json +113 -0
  43. package/recipes/flyers/pixel-mosaic.json +58 -0
  44. package/recipes/flyers/quiet-fields.json +610 -0
  45. package/recipes/flyers/scrapbook-halcyon.json +948 -0
  46. package/recipes/flyers/solenne-ticket.json +730 -0
  47. package/recipes/flyers/solvane-motion.json +576 -0
  48. package/recipes/flyers/solvane.json +98 -0
  49. package/recipes/flyers/subduct.json +99 -0
  50. package/recipes/flyers/sunvale-lineup.json +584 -0
  51. package/recipes/flyers/tidewell.json +91 -0
  52. package/recipes/flyers/tour-kairo.json +535 -0
  53. package/recipes/flyers/velora-stickers.json +885 -0
  54. package/recipes/flyers/wheatpaste-black.json +313 -0
  55. package/recipes/flyers/wheatpaste-mark.json +198 -0
  56. package/recipes/flyers/wheatpaste-torn.json +114 -0
  57. package/recipes/flyers/wheatpaste-wall.json +984 -0
  58. package/recipes/flyers/wheatpaste-words.json +233 -0
  59. package/recipes/release/aura.json +137 -0
  60. package/recipes/release/chrome.json +433 -0
  61. package/recipes/release/collage.json +372 -0
  62. package/recipes/release/duotone.json +113 -0
  63. package/recipes/release/film-still.json +102 -0
  64. package/recipes/release/headline.json +70 -0
  65. package/recipes/release/instant.json +170 -0
  66. package/recipes/release/label.json +364 -0
  67. package/recipes/release/liquid-glass.json +301 -0
  68. package/recipes/release/odd-tile-dark.json +688 -0
  69. package/recipes/release/odd-tile-dense.json +1056 -0
  70. package/recipes/release/odd-tile-shift.json +541 -0
  71. package/recipes/release/odd-tile-split.json +571 -0
  72. package/recipes/release/odd-tile.json +633 -0
  73. package/recipes/release/off-edge.json +96 -0
  74. package/recipes/release/plinth.json +794 -0
  75. package/recipes/release/riso.json +108 -0
  76. package/recipes/release/series/fluid-label.json +561 -0
  77. package/recipes/release/series/fluid-pixel.json +334 -0
  78. package/recipes/release/series/hairline.json +305 -0
  79. package/recipes/release/series/halo.json +282 -0
  80. package/recipes/release/series/mark-crop.json +135 -0
  81. package/recipes/release/series/mark-window.json +150 -0
  82. package/recipes/release/series/photo-label.json +475 -0
  83. package/recipes/release/series/smear-field.json +345 -0
  84. package/recipes/release/series/smear.json +202 -0
  85. package/recipes/release/series/spin.json +172 -0
  86. package/recipes/release/series/split-ghost.json +230 -0
  87. package/recipes/release/series/zoom.json +168 -0
  88. package/recipes/release/small-print.json +114 -0
  89. package/recipes/release/stack.json +156 -0
  90. package/recipes/release/swiss.json +117 -0
  91. package/schema/recipe.schema.json +638 -69
  92. package/src/animate/index.d.ts +9 -19
  93. package/src/animate/index.js +6 -111
  94. package/src/animate/keyframes.d.ts +21 -0
  95. package/src/animate/keyframes.js +119 -0
  96. package/src/animate/marquee.d.ts +30 -0
  97. package/src/animate/marquee.js +93 -0
  98. package/src/animate/morph.d.ts +8 -0
  99. package/src/animate/morph.js +99 -0
  100. package/src/animate/motion.d.ts +59 -0
  101. package/src/animate/motion.js +63 -0
  102. package/src/animate/sheet.d.ts +19 -0
  103. package/src/animate/sheet.js +61 -0
  104. package/src/animate/timing.d.ts +28 -0
  105. package/src/animate/timing.js +146 -0
  106. package/src/animate/tracks.d.ts +18 -0
  107. package/src/animate/tracks.js +139 -0
  108. package/src/diagram.js +4 -3
  109. package/src/extract.js +2 -2
  110. package/src/fonts.js +1 -2
  111. package/src/geometry.js +4 -34
  112. package/src/grid.js +1 -1
  113. package/src/handmade.js +9 -7
  114. package/src/icons.js +1 -2
  115. package/src/index.d.ts +47 -54
  116. package/src/index.js +5 -5
  117. package/src/inter-data.js +1 -1
  118. package/src/json-recipe.js +28 -7
  119. package/src/layer-options.js +26 -4
  120. package/src/layers/barcode.js +3 -3
  121. package/src/layers/geometry.js +5 -5
  122. package/src/layers/index.d.ts +6 -0
  123. package/src/layers/index.js +3 -0
  124. package/src/layers/mark-geometry.d.ts +15 -0
  125. package/src/layers/mark-geometry.js +46 -0
  126. package/src/layers/marks.d.ts +25 -0
  127. package/src/layers/marks.js +102 -0
  128. package/src/layers/mosaic.d.ts +32 -0
  129. package/src/layers/mosaic.js +155 -0
  130. package/src/layers/perforation.js +3 -3
  131. package/src/layers/repeat.js +2 -2
  132. package/src/layers/shape.d.ts +2 -2
  133. package/src/layers/shape.js +5 -5
  134. package/src/layers/soft.js +2 -2
  135. package/src/layers/svg.js +2 -3
  136. package/src/layers/table.js +3 -3
  137. package/src/layers/tear.d.ts +1 -1
  138. package/src/layers/tear.js +4 -4
  139. package/src/layers/texture.js +3 -3
  140. package/src/layers/util.js +15 -2
  141. package/src/layers.js +17 -32
  142. package/src/{effects/paint.js → paint/index.js} +5 -5
  143. package/src/{effects/paint-presets.d.ts → paint/presets.d.ts} +1 -1
  144. package/src/{effects/paint-presets.js → paint/presets.js} +1 -1
  145. package/src/{effects → paint}/tile.d.ts +1 -1
  146. package/src/{effects → paint}/tile.js +4 -4
  147. package/src/palette.js +1 -1
  148. package/src/platform/files.d.ts +6 -0
  149. package/src/platform/files.js +41 -0
  150. package/src/platform/harfbuzz.js +1 -1
  151. package/src/platform/index.d.ts +3 -3
  152. package/src/platform/index.js +6 -4
  153. package/src/platform/load.js +1 -2
  154. package/src/print.js +9 -7
  155. package/src/recipes.js +1 -1
  156. package/src/render/backdrop.js +2 -2
  157. package/src/render/grain.js +1 -1
  158. package/src/render/group.js +3 -3
  159. package/src/render/key.js +1 -2
  160. package/src/{effects → render}/transform.d.ts +2 -1
  161. package/src/{effects → render}/transform.js +17 -5
  162. package/src/render/use.d.ts +1 -1
  163. package/src/render/use.js +2 -2
  164. package/src/render/wrap.js +5 -60
  165. package/src/render.js +55 -18
  166. package/src/text.js +1 -1
  167. package/src/type/auto-color.js +1 -1
  168. package/src/type/fallback.js +1 -1
  169. package/src/type/glyphs.js +2 -2
  170. package/src/type/line.js +1 -1
  171. package/src/type/live.js +1 -1
  172. package/src/type/path.js +2 -2
  173. package/src/type/pixel.d.ts +0 -2
  174. package/src/type/pixel.js +3 -33
  175. package/src/type/spans.d.ts +5 -1
  176. package/src/type/spans.js +28 -5
  177. package/src/type/symbols.d.ts +1 -1
  178. package/src/type/symbols.js +20 -2
  179. package/src/type/typeset.js +25 -10
  180. package/src/version.js +1 -1
  181. package/src/color.js +0 -246
  182. package/src/effects/ascii.d.ts +0 -7
  183. package/src/effects/ascii.js +0 -93
  184. package/src/effects/buffer.d.ts +0 -60
  185. package/src/effects/buffer.js +0 -472
  186. package/src/effects/detail.d.ts +0 -12
  187. package/src/effects/detail.js +0 -124
  188. package/src/effects/digital.d.ts +0 -14
  189. package/src/effects/digital.js +0 -216
  190. package/src/effects/distort.d.ts +0 -20
  191. package/src/effects/distort.js +0 -314
  192. package/src/effects/dither.d.ts +0 -8
  193. package/src/effects/dither.js +0 -147
  194. package/src/effects/drop-shadow.d.ts +0 -4
  195. package/src/effects/drop-shadow.js +0 -45
  196. package/src/effects/extrude.d.ts +0 -5
  197. package/src/effects/extrude.js +0 -55
  198. package/src/effects/film.d.ts +0 -11
  199. package/src/effects/film.js +0 -204
  200. package/src/effects/glow.d.ts +0 -8
  201. package/src/effects/glow.js +0 -56
  202. package/src/effects/grade.d.ts +0 -30
  203. package/src/effects/grade.js +0 -274
  204. package/src/effects/halftone.d.ts +0 -8
  205. package/src/effects/halftone.js +0 -95
  206. package/src/effects/index.d.ts +0 -125
  207. package/src/effects/index.js +0 -277
  208. package/src/effects/ink.d.ts +0 -12
  209. package/src/effects/ink.js +0 -70
  210. package/src/effects/lens.d.ts +0 -14
  211. package/src/effects/lens.js +0 -277
  212. package/src/effects/light.d.ts +0 -10
  213. package/src/effects/light.js +0 -59
  214. package/src/effects/looks.d.ts +0 -9
  215. package/src/effects/looks.js +0 -74
  216. package/src/effects/mask.d.ts +0 -52
  217. package/src/effects/mask.js +0 -314
  218. package/src/effects/motion.d.ts +0 -6
  219. package/src/effects/motion.js +0 -60
  220. package/src/effects/newsprint.d.ts +0 -6
  221. package/src/effects/newsprint.js +0 -89
  222. package/src/effects/optics.d.ts +0 -10
  223. package/src/effects/optics.js +0 -213
  224. package/src/effects/options.d.ts +0 -6
  225. package/src/effects/options.js +0 -106
  226. package/src/effects/pixels.d.ts +0 -24
  227. package/src/effects/pixels.js +0 -138
  228. package/src/effects/polar.d.ts +0 -10
  229. package/src/effects/polar.js +0 -265
  230. package/src/effects/region.d.ts +0 -8
  231. package/src/effects/region.js +0 -136
  232. package/src/effects/round-border.d.ts +0 -5
  233. package/src/effects/round-border.js +0 -62
  234. package/src/effects/screen.d.ts +0 -14
  235. package/src/effects/screen.js +0 -212
  236. package/src/effects/sort.d.ts +0 -8
  237. package/src/effects/sort.js +0 -125
  238. package/src/effects/symmetry.d.ts +0 -6
  239. package/src/effects/symmetry.js +0 -74
  240. package/src/effects/tone.d.ts +0 -16
  241. package/src/effects/tone.js +0 -117
  242. package/src/effects/util.d.ts +0 -16
  243. package/src/effects/util.js +0 -85
  244. package/src/platform/bytes.d.ts +0 -22
  245. package/src/platform/bytes.js +0 -103
  246. package/src/platform/compress.d.ts +0 -12
  247. package/src/platform/compress.js +0 -81
  248. package/src/platform/node.d.ts +0 -26
  249. package/src/platform/node.js +0 -86
  250. package/src/png.js +0 -126
  251. package/src/raster/index.d.ts +0 -24
  252. package/src/raster/index.js +0 -171
  253. package/src/render/content.d.ts +0 -5
  254. package/src/render/content.js +0 -58
  255. package/src/render/tiles.d.ts +0 -6
  256. package/src/render/tiles.js +0 -77
  257. package/src/rng.js +0 -95
  258. package/src/svg.js +0 -69
  259. /package/src/{effects/paint.d.ts → paint/index.d.ts} +0 -0
@@ -0,0 +1,102 @@
1
+ /**
2
+ * The `marks` layer.
3
+ *
4
+ * @module layers/marks
5
+ */
6
+
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { toHex } from '@logomarks/image-effects/color';
9
+ import { pos, wrap } from './util.js';
10
+ import { shapesOnly } from './svg.js';
11
+ import { markGeometry } from './mark-geometry.js';
12
+
13
+ /** The smallest and largest sheet: 3 × 3 to 10 × 7. */
14
+ const MIN = 3;
15
+ const MAX_COLS = 10;
16
+ const MAX_ROWS = 7;
17
+
18
+ /**
19
+ * `marks` — a set of marks on an even grid of square cells, centred in its
20
+ * box so the margins match, each mark optically sized the same: scaled by
21
+ * its real bounds (not its 512 grid), to the same area, nudged up a little
22
+ * where it inks less of that area (an outline looks smaller than a solid
23
+ * of the same size), and never past its cell. The logofolio sheet.
24
+ *
25
+ * Options: `marks` (a list of markup strings, or `{ markup }`), `cols` and
26
+ * `rows` (default from the count and the box: 3 × 3 up to 10 × 7), `x`,
27
+ * `y`, `width`, `height` (the box; default the canvas), `size` (the share
28
+ * of a cell a mark's area takes, 0.5), `optical` (`false` fits each
29
+ * mark's bounds to the same box instead), `color` (default `text`).
30
+ *
31
+ * @param {any} layer
32
+ * @param {import('../layers.js').RenderContext} ctx
33
+ * @returns {import('../layers.js').Markup}
34
+ *
35
+ * @example
36
+ * { type: 'marks', marks: ['<path d="…"/>', '<path d="…"/>'], x: 24, y: 120, width: 352, height: 340, color: '#f2efe8' }
37
+ */
38
+ export function marks(layer, ctx) {
39
+ const list = (layer.marks ?? []).map((m) => (m && typeof m === 'object' ? m.markup : m)).filter(Boolean);
40
+ if (!list.length) return { defs: '', body: '' };
41
+ const bx = pos(layer.x ?? 0, 'x', ctx.canvas);
42
+ const by = pos(layer.y ?? 0, 'y', ctx.canvas);
43
+ const bw = pos(layer.width ?? '100%', 'x', ctx.canvas);
44
+ const bh = pos(layer.height ?? '100%', 'y', ctx.canvas);
45
+ const { cols, rows } = sheetGrid(list.length, bw / bh, layer.cols, layer.rows);
46
+ const cell = Math.min(bw / cols, bh / rows);
47
+ const ox = bx + (bw - cols * cell) / 2;
48
+ const oy = by + (bh - rows * cell) / 2;
49
+ const share = layer.size ?? 0.5;
50
+ const color = layer.color ? toHex(layer.color) : ctx.palette.text;
51
+
52
+ let body = '';
53
+ list.slice(0, cols * rows).forEach((markup, i) => {
54
+ const geo = markGeometry(markup);
55
+ if (!geo) return;
56
+ const { x, y, width, height } = geo.box;
57
+ // Optical: the same area (density-weighted, gently), capped at the cell; else the same longest side.
58
+ const fit = (cell * 0.86) / Math.max(width, height);
59
+ const k = layer.optical === false ? (cell * Math.sqrt(share)) / Math.max(width, height)
60
+ : Math.min(fit, (cell * Math.sqrt(share)) / (Math.sqrt(width * height) * (geo.density / 0.5) ** 0.15));
61
+ const cx = ox + (i % cols + 0.5) * cell;
62
+ const cy = oy + (Math.floor(i / cols) + 0.5) * cell;
63
+ body += `<svg${attrs({ x: cx - (width * k) / 2, y: cy - (height * k) / 2, width: width * k, height: height * k, viewBox: `${num(x)} ${num(y)} ${num(width)} ${num(height)}`, overflow: 'visible' })}>${shapesOnly(markup)}</svg>`;
64
+ });
65
+ return { defs: '', body: wrap(layer, `<g fill="${color}">${body}</g>`) };
66
+ }
67
+
68
+ /**
69
+ * Columns and rows for `n` marks in a box of `aspect` (width / height):
70
+ * the biggest cells the box allows, less a cost for empty cells (so 25
71
+ * is 5 × 5, not 6 × 5 with a ragged row), 3 × 3 at the least, 10 × 7 at
72
+ * the most (marks past 70 are left out). Given ones are kept.
73
+ *
74
+ * @param {number} n
75
+ * @param {number} aspect
76
+ * @param {number} [cols]
77
+ * @param {number} [rows]
78
+ * @returns {{ cols: number, rows: number }}
79
+ *
80
+ * @example
81
+ * sheetGrid(70, 0.8); // { cols: 10, rows: 7 }
82
+ * sheetGrid(25, 1.6); // { cols: 5, rows: 5 }
83
+ */
84
+ export function sheetGrid(n, aspect, cols, rows) {
85
+ if (cols && rows) return { cols: Math.round(cols), rows: Math.round(rows) };
86
+ if (cols) return { cols: Math.round(cols), rows: Math.max(1, Math.ceil(n / cols)) };
87
+ if (rows) return { cols: Math.max(1, Math.ceil(n / rows)), rows: Math.round(rows) };
88
+ // The biggest cells, less a cost for empty ones (a ragged last row).
89
+ const count = Math.min(n, MAX_COLS * MAX_ROWS);
90
+ let best = { cols: MAX_COLS, rows: MAX_ROWS };
91
+ let score = -Infinity;
92
+ for (let c = MIN; c <= MAX_COLS; c++) {
93
+ const r = clamp(Math.ceil(count / c), MIN, MAX_ROWS);
94
+ if (c * r < count) continue;
95
+ const s = Math.min(aspect / c, 1 / r) * (1 - (1.5 * (c * r - count)) / (c * r));
96
+ if (s > score + 1e-9) [best, score] = [{ cols: c, rows: r }, s];
97
+ }
98
+ return best;
99
+ }
100
+
101
+ /** @param {number} v @param {number} lo @param {number} hi @returns {number} */
102
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
@@ -0,0 +1,32 @@
1
+ // The `mosaic` layer.
2
+ import type { Color, Length, BaseLayer } from '../index.js';
3
+ import type { RenderContext, Markup } from '../render/context.js';
4
+ import type { PixelGrid } from '@logomarks/core/edit';
5
+ import type { MarkGeometry } from './mark-geometry.js';
6
+
7
+ export interface MosaicLayer extends BaseLayer {
8
+ type: 'mosaic';
9
+ /** The mark's paths, on a 512 grid. */
10
+ markup: string;
11
+ /** The box the mosaic is fitted and centred in (default the canvas). */
12
+ x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
13
+ /** Cells across the mark's longest side, 4–48. Default 10. */
14
+ cells?: number;
15
+ /** The gap between cells, a share of a cell. Default 0.12. */
16
+ gap?: number;
17
+ /** Cell corner radius, a share of a cell. Default 0. */
18
+ radius?: number;
19
+ /** The share of filled cells left out, by the layer's seed (0–0.9). Default 0.12. */
20
+ drop?: number;
21
+ /** Default the palette's accent. */
22
+ color?: Color;
23
+ /** A small asterisk or dot in one empty cell beside the mark. */
24
+ accent?: 'asterisk' | 'dot' | false;
25
+ /** Default `color`. */
26
+ accentColor?: Color;
27
+ }
28
+
29
+ /** Draw a mark as a mosaic of square cells. */
30
+ export function mosaic(layer: MosaicLayer, ctx: RenderContext): Markup;
31
+ /** The mosaic's grid over a mark: `cells` across its longest side, centred on its bounds. */
32
+ export function mosaicGrid(geo: MarkGeometry, cells?: number): PixelGrid;
@@ -0,0 +1,155 @@
1
+ /**
2
+ * The `mosaic` layer.
3
+ *
4
+ * @module layers/mosaic
5
+ */
6
+
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { toHex } from '@logomarks/image-effects/color';
9
+ import { pixelGrid, pixelCoverage, pixelCells } from '@logomarks/core/edit';
10
+ import { pos, wrap } from './util.js';
11
+ import { markGeometry } from './mark-geometry.js';
12
+
13
+ /**
14
+ * `mosaic` — a mark rastered into big square cells, each drawn on its own
15
+ * with a gap between, so whatever is under it shows through. The cells
16
+ * follow the mark's own shape (`@logomarks/core`'s pixelate: a cell is
17
+ * filled when the mark covers half of it, or a fifth where it would
18
+ * otherwise lose a thin line or a dot). `drop` leaves a share of the
19
+ * filled cells out, chosen by the layer's seed; `accent` puts a small
20
+ * asterisk or dot in one empty cell beside the mark.
21
+ *
22
+ * Options: `markup` (the mark's paths, 512 grid), `x`, `y`, `width`,
23
+ * `height` (the box it's fitted and centred in; default the canvas),
24
+ * `cells` (across the mark's longest side, 10), `gap` (a share of a cell,
25
+ * 0.12), `radius` (a share of a cell, 0), `drop` (0–1, 0.12), `color`
26
+ * (default the accent), `accent` (`'asterisk'` or `'dot'`), `accentColor`.
27
+ *
28
+ * @param {any} layer
29
+ * @param {import('../layers.js').RenderContext} ctx
30
+ * @returns {import('../layers.js').Markup}
31
+ *
32
+ * @example
33
+ * { type: 'mosaic', markup: mark, x: 40, y: 60, width: 320, height: 320, cells: 11, color: '#c8ff2e', drop: 0.15, accent: 'asterisk' }
34
+ */
35
+ export function mosaic(layer, ctx) {
36
+ const geo = markGeometry(layer.markup);
37
+ if (!geo) return { defs: '', body: '' };
38
+ const g = mosaicGrid(geo, layer.cells);
39
+ const on = pixelCells(pixelCoverage(geo.paths, g), g);
40
+
41
+ // Fit the grid into the box, centred.
42
+ const bx = pos(layer.x ?? 0, 'x', ctx.canvas);
43
+ const by = pos(layer.y ?? 0, 'y', ctx.canvas);
44
+ const bw = pos(layer.width ?? '100%', 'x', ctx.canvas);
45
+ const bh = pos(layer.height ?? '100%', 'y', ctx.canvas);
46
+ const k = Math.min(bw / (g.cols * g.cell), bh / (g.rows * g.cell));
47
+ const cell = g.cell * k;
48
+ const ox = bx + (bw - g.cols * cell) / 2;
49
+ const oy = by + (bh - g.rows * cell) / 2;
50
+
51
+ const kept = dropCells(on, layer.drop ?? 0.12, ctx.rng);
52
+ const gap = cell * clamp(layer.gap ?? 0.12, 0, 0.9);
53
+ const r = cell * clamp(layer.radius ?? 0, 0, 0.5);
54
+ const side = cell - gap;
55
+ let rects = '';
56
+ kept.forEach((v, i) => {
57
+ if (!v) return;
58
+ const x = ox + (i % g.cols) * cell + gap / 2;
59
+ const y = oy + Math.floor(i / g.cols) * cell + gap / 2;
60
+ rects += r ? `<rect${attrs({ x, y, width: side, height: side, rx: r })}/>` : `M${num(x)} ${num(y)}h${num(side)}v${num(side)}h${num(-side)}Z`;
61
+ });
62
+ const color = layer.color ? toHex(layer.color) : ctx.palette.accent;
63
+ let body = r ? `<g fill="${color}">${rects}</g>` : `<path${attrs({ d: rects, fill: color })}/>`;
64
+
65
+ if (layer.accent) {
66
+ const at = accentCell(on, g, ctx.rng);
67
+ if (at >= 0) {
68
+ const cx = ox + (at % g.cols + 0.5) * cell;
69
+ const cy = oy + (Math.floor(at / g.cols) + 0.5) * cell;
70
+ body += accentGlyph(layer.accent, cx, cy, side, layer.accentColor ? toHex(layer.accentColor) : color);
71
+ }
72
+ }
73
+ return { defs: '', body: wrap(layer, body) };
74
+ }
75
+
76
+ /**
77
+ * The mosaic's grid over a mark: `cells` across its longest side (4–48),
78
+ * centred on its bounds (pixelate's grid, at that coarseness).
79
+ *
80
+ * @param {import('./mark-geometry.js').MarkGeometry} geo
81
+ * @param {number} [cells=10]
82
+ * @returns {import('@logomarks/core/edit').PixelGrid}
83
+ */
84
+ export function mosaicGrid(geo, cells = 10) {
85
+ const n = clamp(Math.round(cells), 4, 48);
86
+ // pixelGrid sets 48 − 34 × amount cells across.
87
+ return pixelGrid((48 - n) / 34, geo.box);
88
+ }
89
+
90
+ /**
91
+ * The filled cells less a seeded share left out (at least one is kept).
92
+ *
93
+ * @param {Uint8Array} on
94
+ * @param {number} drop - 0–1.
95
+ * @param {import('../index.js').Rng} rng
96
+ * @returns {Uint8Array}
97
+ */
98
+ function dropCells(on, drop, rng) {
99
+ const out = on.slice();
100
+ const share = clamp(drop, 0, 0.9);
101
+ let left = 0;
102
+ out.forEach((v, i) => {
103
+ if (!v) return;
104
+ // One draw a filled cell, always, so the choice is stable whatever `drop` is.
105
+ if (rng.next() < share) out[i] = 0;
106
+ else left++;
107
+ });
108
+ if (!left) return on.slice();
109
+ return out;
110
+ }
111
+
112
+ /**
113
+ * A seeded empty cell beside the mark (one with a filled neighbour), or -1.
114
+ *
115
+ * @param {Uint8Array} on
116
+ * @param {{ cols: number, rows: number }} g
117
+ * @param {import('../index.js').Rng} rng
118
+ * @returns {number}
119
+ */
120
+ function accentCell(on, { cols, rows }, rng) {
121
+ const near = [];
122
+ for (let i = 0; i < on.length; i++) {
123
+ if (on[i]) continue;
124
+ const q = i % cols;
125
+ const r = Math.floor(i / cols);
126
+ const n = [[q - 1, r], [q + 1, r], [q, r - 1], [q, r + 1]].some(([a, b]) => a >= 0 && b >= 0 && a < cols && b < rows && on[b * cols + a]);
127
+ if (n) near.push(i);
128
+ }
129
+ if (!near.length) return -1;
130
+ return near[Math.floor(rng.next() * near.length)];
131
+ }
132
+
133
+ /**
134
+ * An asterisk (three bars) or a dot, centred in a cell of `side`.
135
+ *
136
+ * @param {'asterisk' | 'dot' | string} kind
137
+ * @param {number} cx @param {number} cy @param {number} side
138
+ * @param {string} color
139
+ * @returns {string}
140
+ */
141
+ function accentGlyph(kind, cx, cy, side, color) {
142
+ if (kind === 'dot') return `<circle${attrs({ cx, cy, r: side * 0.24, fill: color })}/>`;
143
+ const L = side * 0.36;
144
+ const t = side * 0.085;
145
+ const d = [0, 60, 120].map((deg) => {
146
+ const a = (deg * Math.PI) / 180;
147
+ const [ux, uy, vx, vy] = [Math.cos(a), Math.sin(a), -Math.sin(a), Math.cos(a)];
148
+ const pts = [[-L, -t], [L, -t], [L, t], [-L, t]].map(([u, v]) => `${num(cx + u * ux + v * vx)} ${num(cy + u * uy + v * vy)}`);
149
+ return `M${pts.join('L')}Z`;
150
+ }).join('');
151
+ return `<path${attrs({ d, fill: color })}/>`;
152
+ }
153
+
154
+ /** @param {number} v @param {number} lo @param {number} hi @returns {number} */
155
+ const clamp = (v, lo, hi) => Math.min(hi, Math.max(lo, v));
@@ -4,9 +4,9 @@
4
4
  * @module layers/perforation
5
5
  */
6
6
 
7
- import { attrs, num } from '../svg.js';
8
- import { len } from '../geometry.js';
9
- import { toHex } from '../color.js';
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { len } from '@logomarks/image-effects/length';
9
+ import { toHex } from '@logomarks/image-effects/color';
10
10
  import { pos, wrap } from './util.js';
11
11
 
12
12
 
@@ -19,8 +19,8 @@
19
19
  * @module layers/repeat
20
20
  */
21
21
 
22
- import { num } from '../svg.js';
23
- import { len } from '../geometry.js';
22
+ import { num } from '@logomarks/image-effects/svg';
23
+ import { len } from '@logomarks/image-effects/length';
24
24
  import { pos } from './util.js';
25
25
  import { fill, evaluate } from './expr.js';
26
26
 
@@ -1,9 +1,9 @@
1
1
  // The `shape` layer.
2
2
  import type { Color, Length, BaseLayer, Anchor } from '../index.js';
3
3
  import type { RenderContext, Markup } from '../render/context.js';
4
- import type { Paint } from '../effects/paint.js';
4
+ import type { Paint } from '../paint/index.js';
5
5
 
6
- /** One exact shape (see the README's `shape` table). */
6
+ /** One exact shape (see the `shape` row in docs/layers.md). */
7
7
  export interface ShapeSpec {
8
8
  kind?: 'rect' | 'circle' | 'ellipse' | 'line' | 'polygon' | 'polyline' | 'star' | 'burst' | 'scallop' | 'rays' | 'ring' | 'sector' | 'arch' | 'wave' | 'path' | 'cut';
9
9
  x?: Length | `${number}%`; y?: Length | `${number}%`; width?: Length | `${number}%`; height?: Length | `${number}%`;
@@ -4,11 +4,11 @@
4
4
  * @module layers/shape
5
5
  */
6
6
 
7
- import { attrs, num } from '../svg.js';
8
- import { len } from '../geometry.js';
9
- import { toHex } from '../color.js';
10
- import { paint } from '../effects/paint.js';
11
- import { presetStrokes } from '../effects/paint-presets.js';
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { len } from '@logomarks/image-effects/length';
9
+ import { toHex } from '@logomarks/image-effects/color';
10
+ import { paint } from '../paint/index.js';
11
+ import { presetStrokes } from '../paint/presets.js';
12
12
  import { wrap, pos } from './util.js';
13
13
  import { geometry } from './geometry.js';
14
14
  import { soften } from './soft.js';
@@ -6,8 +6,8 @@
6
6
  * @module layers/soft
7
7
  */
8
8
 
9
- import { attrs, num } from '../svg.js';
10
- import { toBase64 } from '../platform/bytes.js';
9
+ import { attrs, num } from '@logomarks/image-effects/svg';
10
+ import { toBase64 } from '../platform/index.js';
11
11
 
12
12
  /**
13
13
  * Blur markup by `sigma` (px in the layer's units). In `renderImage`, the markup
package/src/layers/svg.js CHANGED
@@ -4,11 +4,10 @@
4
4
  * @module layers/svg
5
5
  */
6
6
 
7
- import { attrs, esc, num } from '../svg.js';
7
+ import { attrs, esc, num } from '@logomarks/image-effects/svg';
8
8
  import { unit } from '../geometry.js';
9
9
  import { pos, wrap } from './util.js';
10
- import { expandDropShadows } from '../effects/drop-shadow.js';
11
- import { filterReach } from '../effects/region.js';
10
+ import { expandDropShadows, filterReach } from '@logomarks/image-effects';
12
11
 
13
12
 
14
13
  /**
@@ -4,9 +4,9 @@
4
4
  * @module layers/table
5
5
  */
6
6
 
7
- import { attrs, num } from '../svg.js';
8
- import { len } from '../geometry.js';
9
- import { toHex } from '../color.js';
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { len } from '@logomarks/image-effects/length';
9
+ import { toHex } from '@logomarks/image-effects/color';
10
10
  import { faceFor, setLine } from '../type/line.js';
11
11
  import { pos, wrap, contentOf } from './util.js';
12
12
 
@@ -1,7 +1,7 @@
1
1
  // The `tear` layer.
2
2
  import type { Color, Length, BaseLayer, Anchor } from '../index.js';
3
3
  import type { RenderContext, Markup } from '../render/context.js';
4
- import type { Paint } from '../effects/paint.js';
4
+ import type { Paint } from '../paint/index.js';
5
5
 
6
6
  export interface TearLayer extends BaseLayer {
7
7
  type: 'tear';
@@ -4,10 +4,10 @@
4
4
  * @module layers/tear
5
5
  */
6
6
 
7
- import { attrs, num } from '../svg.js';
8
- import { len } from '../geometry.js';
9
- import { toHex } from '../color.js';
10
- import { paint } from '../effects/paint.js';
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { len } from '@logomarks/image-effects/length';
9
+ import { toHex } from '@logomarks/image-effects/color';
10
+ import { paint } from '../paint/index.js';
11
11
  import { pos, wrap } from './util.js';
12
12
 
13
13
 
@@ -4,9 +4,9 @@
4
4
  * @module layers/texture
5
5
  */
6
6
 
7
- import { attrs, num } from '../svg.js';
8
- import { len } from '../geometry.js';
9
- import { toHex } from '../color.js';
7
+ import { attrs, num } from '@logomarks/image-effects/svg';
8
+ import { len } from '@logomarks/image-effects/length';
9
+ import { toHex } from '@logomarks/image-effects/color';
10
10
  import { pos, wrap } from './util.js';
11
11
 
12
12
 
@@ -5,8 +5,8 @@
5
5
  * @module layers/util
6
6
  */
7
7
 
8
- import { attrs, esc } from '../svg.js';
9
- import { len } from '../geometry.js';
8
+ import { attrs, esc } from '@logomarks/image-effects/svg';
9
+ import { len } from '@logomarks/image-effects/length';
10
10
 
11
11
  /**
12
12
  * A coordinate: px as a number, `'50%'` of the canvas along `axis`, or a
@@ -22,6 +22,19 @@ export function pos(v, axis, canvas) {
22
22
  return len(/** @type {any} */ (v), canvas, 0);
23
23
  }
24
24
 
25
+ /**
26
+ * Opacity and blend-mode attributes shared by every layer.
27
+ *
28
+ * @param {{ opacity?: number, blend?: string }} layer - The layer.
29
+ * @returns {Record<string, string | number | undefined>} Attributes for the layer's group.
30
+ */
31
+ export function layerAttrs(layer) {
32
+ return {
33
+ opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined,
34
+ style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${layer.blend}` : undefined,
35
+ };
36
+ }
37
+
25
38
  /**
26
39
  * Wrap markup in a group with the layer's `opacity` and `blend` (only when
27
40
  * it has them).
package/src/layers.js CHANGED
@@ -14,11 +14,12 @@
14
14
  * @module layers
15
15
  */
16
16
 
17
- import { loadBytes } from './platform/index.js';
18
- import { toBase64, latin1, utf8 } from './platform/bytes.js';
19
- import { attrs, esc, layerAttrs, num } from './svg.js';
20
- import { len, place, anchorAlign, unit, regionPx } from './geometry.js';
21
- import { toHex, mixOklab, toOklch, fromOklch } from './color.js';
17
+ import { loadBytes, toBase64 } from './platform/index.js';
18
+ import { attrs, esc, num } from '@logomarks/image-effects/svg';
19
+ import { imageMime } from '@logomarks/image-effects';
20
+ import { len } from '@logomarks/image-effects/length';
21
+ import { place, anchorAlign, unit, regionPx } from './geometry.js';
22
+ import { toHex, mixOklab, toOklch, fromOklch } from '@logomarks/image-effects/color';
22
23
  import { harfbuzz, loadFont, pickFont } from './fonts.js';
23
24
  import { layout, runPath, caseText } from './text.js';
24
25
  import { resolveIcon, parseSvgIcon } from './icons.js';
@@ -28,9 +29,9 @@ import { paper, scribble } from './handmade.js';
28
29
  import { ink, dither, contour, waveform, tiles, rule } from './print.js';
29
30
  import { typeset, wantsType } from './type/index.js';
30
31
  import { recordLayout } from './type/layout-ref.js';
31
- import { shape, svg, table, barcode, perforation, tear, texture } from './layers/index.js';
32
- import { pos } from './layers/util.js';
33
- import { paint } from './effects/paint.js';
32
+ import { shape, svg, table, barcode, perforation, tear, texture, mosaic, marks } from './layers/index.js';
33
+ import { pos, layerAttrs } from './layers/util.js';
34
+ import { paint } from './paint/index.js';
34
35
  import { imageSize } from './lint/image-size.js';
35
36
 
36
37
  /**
@@ -41,7 +42,7 @@ import { imageSize } from './lint/image-size.js';
41
42
  * @property {number} height - Canvas height in px.
42
43
  * @property {{ width: number, height: number }} canvas - The same, as an object.
43
44
  * @property {import('./index.js').Palette} palette - The resolved palette.
44
- * @property {import('./rng.js').Rng} rng - This layer's own random stream.
45
+ * @property {import('@logomarks/image-effects/rng').Rng} rng - This layer's own random stream.
45
46
  * @property {string} id - A unique id prefix for this layer's defs.
46
47
  * @property {import('./index.js').FontSource[]} fonts - Available fonts (Inter last).
47
48
  * @property {any} [sharp] - The `sharp` module, when available.
@@ -421,7 +422,7 @@ export async function image(layer, ctx) {
421
422
  const fit = layer.fit ?? 'cover';
422
423
  const focus = layer.focus ?? 'center';
423
424
  let bytes = typeof layer.src === 'string' || layer.src instanceof URL ? await readSource(layer.src) : await loadBytes(layer.src, 'image');
424
- let mime = sniff(bytes);
425
+ let mime = imageMime(bytes);
425
426
  let preserve = fit === 'stretch' ? 'none' : `${alignFor(focus)} ${fit === 'contain' ? 'meet' : 'slice'}`;
426
427
  let sharpBlurred = false;
427
428
 
@@ -548,23 +549,6 @@ async function readSource(src) {
548
549
  return loadBytes(src, 'image');
549
550
  }
550
551
 
551
- /**
552
- * Sniff an image's MIME type from its first bytes.
553
- *
554
- * @param {Uint8Array} b - Image bytes.
555
- * @returns {string | undefined} The MIME type.
556
- */
557
- export function sniff(b) {
558
- if (b[0] === 0x89 && b[1] === 0x50) return 'image/png';
559
- if (b[0] === 0xff && b[1] === 0xd8) return 'image/jpeg';
560
- if (b[0] === 0x47 && b[1] === 0x49) return 'image/gif';
561
- const head = latin1(b.subarray(0, 32));
562
- if (head.startsWith('RIFF') && head.slice(8, 12) === 'WEBP') return 'image/webp';
563
- if (head.slice(4, 12) === 'ftypavif' || head.slice(4, 12) === 'ftypavis') return 'image/avif';
564
- if (/^\s*(<\?xml|<svg)/i.test(utf8(b.subarray(0, 256)))) return 'image/svg+xml';
565
- return undefined;
566
- }
567
-
568
552
  // ---------------------------------------------------------------------------
569
553
  // Texture
570
554
  // ---------------------------------------------------------------------------
@@ -636,10 +620,11 @@ export function pattern(layer, ctx) {
636
620
  const d = [];
637
621
  if (angle === 0) {
638
622
  // Rows and columns: every `size` from the centre, or `divisions` equal
639
- // cells (inner lines only); broken wherever they cross a `gaps` region.
640
- const n = layer.divisions;
641
- const ys = n ? Array.from({ length: n - 1 }, (_, k) => ((k + 1) * h) / n) : range(cy % size, h, size);
642
- const xs = kind !== 'grid' ? [] : n ? Array.from({ length: n - 1 }, (_, k) => ((k + 1) * w) / n) : range(cx % size, w, size);
623
+ // cells (inner lines only; `[cols, rows]` for a design grid); broken
624
+ // wherever they cross a `gaps` region.
625
+ const [nx, ny] = Array.isArray(layer.divisions) ? layer.divisions.map((d) => Math.max(1, Math.round(d))) : [layer.divisions, layer.divisions];
626
+ const ys = ny ? Array.from({ length: ny - 1 }, (_, k) => ((k + 1) * h) / ny) : range(cy % size, h, size);
627
+ const xs = kind !== 'grid' ? [] : nx ? Array.from({ length: nx - 1 }, (_, k) => ((k + 1) * w) / nx) : range(cx % size, w, size);
643
628
  const gaps = (layer.gaps ?? []).map((g) => regionPx(g, ctx.canvas));
644
629
  d.push(gridPath(xs, ys, w, h, gaps));
645
630
  shapes = `<path${attrs({ d: d.join(''), fill: 'none', stroke: col, 'stroke-width': stroke, ...dashAttrs })}/>`;
@@ -1053,4 +1038,4 @@ function group() {
1053
1038
  throw new TypeError('A group layer is drawn by renderSvg()/renderImage(), not on its own');
1054
1039
  }
1055
1040
 
1056
- export const LAYERS = Object.freeze({ solid, linear, radial, mesh, stripes, image, grain, pattern, shapes, icon, text, frame, diagram, paper, scribble, ink, dither, contour, waveform, tiles, rule, group, shape, svg, table, barcode, perforation, tear, texture, repeat: group, defs: group, use: group });
1041
+ export const LAYERS = Object.freeze({ solid, linear, radial, mesh, stripes, image, grain, pattern, shapes, icon, text, frame, diagram, paper, scribble, ink, dither, contour, waveform, tiles, rule, group, shape, svg, table, barcode, perforation, tear, texture, mosaic, marks, repeat: group, defs: group, use: group });
@@ -4,13 +4,13 @@
4
4
  * radial fields with `rx`/`ry`), a `tile` pattern (checker, stripes, dots,
5
5
  * grid or a custom tile), or a named preset (`'chrome'`).
6
6
  *
7
- * @module effects/paint
7
+ * @module paint
8
8
  */
9
9
 
10
- import { attrs, num } from '../svg.js';
11
- import { len } from '../geometry.js';
12
- import { toHex } from '../color.js';
13
- import { PRESETS } from './paint-presets.js';
10
+ import { attrs, num } from '@logomarks/image-effects/svg';
11
+ import { len } from '@logomarks/image-effects/length';
12
+ import { toHex } from '@logomarks/image-effects/color';
13
+ import { PRESETS } from './presets.js';
14
14
  import { tile } from './tile.js';
15
15
 
16
16
  /**
@@ -1,5 +1,5 @@
1
1
  // Named paints: chrome, steel and gold, painted as a reflected horizon.
2
- import type { GradientPaint } from './paint.js';
2
+ import type { GradientPaint } from './index.js';
3
3
  import type { Color, Length } from '../index.js';
4
4
 
5
5
  /** Paint presets, by name. */
@@ -4,7 +4,7 @@
4
4
  * horizon a little past the middle, warm ground below it, and a light
5
5
  * bounce at the very bottom. Pair it with a light keyline (`strokes`).
6
6
  *
7
- * @module effects/paint-presets
7
+ * @module paint/presets
8
8
  */
9
9
 
10
10
  /** Paint presets, by name. A paint `{ preset, …fields }` overrides any of them. */
@@ -1,5 +1,5 @@
1
1
  // Tile paints.
2
- import type { TilePaint } from './paint.js';
2
+ import type { TilePaint } from './index.js';
3
3
 
4
4
  /** Tile kinds. */
5
5
  export const TILES: readonly ('checker' | 'stripes' | 'dots' | 'grid' | 'custom')[];
@@ -2,12 +2,12 @@
2
2
  * Tile paints: a pattern repeated over a shape (checker, stripes, dots,
3
3
  * grid), or a custom tile of markup.
4
4
  *
5
- * @module effects/tile
5
+ * @module paint/tile
6
6
  */
7
7
 
8
- import { attrs, num } from '../svg.js';
9
- import { len } from '../geometry.js';
10
- import { toHex } from '../color.js';
8
+ import { attrs, num } from '@logomarks/image-effects/svg';
9
+ import { len } from '@logomarks/image-effects/length';
10
+ import { toHex } from '@logomarks/image-effects/color';
11
11
  import { sanitize } from '../layers/svg.js';
12
12
 
13
13
  /** Tile kinds. */
package/src/palette.js CHANGED
@@ -20,7 +20,7 @@
20
20
  */
21
21
 
22
22
  import { relativeLuminance, contrastRatio, readableOn, readableText } from '@arraypress/color-utils';
23
- import { toHex, toOklch, fromOklch, mixOklab } from './color.js';
23
+ import { toHex, toOklch, fromOklch, mixOklab } from '@logomarks/image-effects/color';
24
24
 
25
25
  /**
26
26
  * Curated presets. Each is a full palette; the light ones pair with the
@@ -0,0 +1,6 @@
1
+ // Files read from disk in Node.
2
+
3
+ /** Read a text file synchronously (Node). */
4
+ export function readTextSync(path: string | URL): string;
5
+ /** A file shipped with the package, from disk (Node), or undefined. */
6
+ export function packageFileSync(rel: string): Uint8Array | undefined;