@arraypress/covers 1.1.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 (244) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +18 -809
  3. package/package.json +2 -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-rest-rhythm.json +125 -0
  18. package/recipes/flyers/groovy-honeycomb.json +816 -0
  19. package/recipes/flyers/groovy-marigold.json +479 -0
  20. package/recipes/flyers/groovy-peachfuzz.json +974 -0
  21. package/recipes/flyers/halden-orbit-motion.json +381 -0
  22. package/recipes/flyers/halden-orbit.json +356 -0
  23. package/recipes/flyers/halvard.json +120 -0
  24. package/recipes/flyers/kairo-weekender.json +696 -0
  25. package/recipes/flyers/linger-motion.json +518 -0
  26. package/recipes/flyers/linger.json +67 -0
  27. package/recipes/flyers/little-orchard.json +105 -0
  28. package/recipes/flyers/lofi-hexmoth.json +912 -0
  29. package/recipes/flyers/lofi-lowbit-motion.json +888 -0
  30. package/recipes/flyers/lofi-lowbit.json +880 -0
  31. package/recipes/flyers/lofi-nullpoint.json +3013 -0
  32. package/recipes/flyers/lumio-stack-motion.json +648 -0
  33. package/recipes/flyers/lumio-stack.json +438 -0
  34. package/recipes/flyers/magazine-cover.json +168 -0
  35. package/recipes/flyers/magazine-extended.json +269 -0
  36. package/recipes/flyers/magazine-knockout.json +40 -0
  37. package/recipes/flyers/northfold-varsity.json +618 -0
  38. package/recipes/flyers/open-studio.json +69 -0
  39. package/recipes/flyers/orrin-merch.json +715 -0
  40. package/recipes/flyers/oxyde.json +113 -0
  41. package/recipes/flyers/quiet-fields.json +610 -0
  42. package/recipes/flyers/scrapbook-halcyon.json +948 -0
  43. package/recipes/flyers/solenne-ticket.json +730 -0
  44. package/recipes/flyers/solvane-motion.json +576 -0
  45. package/recipes/flyers/solvane.json +98 -0
  46. package/recipes/flyers/subduct.json +99 -0
  47. package/recipes/flyers/sunvale-lineup.json +584 -0
  48. package/recipes/flyers/tidewell.json +91 -0
  49. package/recipes/flyers/tour-kairo.json +535 -0
  50. package/recipes/flyers/velora-stickers.json +885 -0
  51. package/recipes/flyers/wheatpaste-black.json +313 -0
  52. package/recipes/flyers/wheatpaste-mark.json +198 -0
  53. package/recipes/flyers/wheatpaste-torn.json +114 -0
  54. package/recipes/flyers/wheatpaste-wall.json +984 -0
  55. package/recipes/flyers/wheatpaste-words.json +233 -0
  56. package/recipes/release/aura.json +137 -0
  57. package/recipes/release/chrome.json +433 -0
  58. package/recipes/release/collage.json +372 -0
  59. package/recipes/release/duotone.json +113 -0
  60. package/recipes/release/film-still.json +102 -0
  61. package/recipes/release/headline.json +70 -0
  62. package/recipes/release/instant.json +170 -0
  63. package/recipes/release/label.json +364 -0
  64. package/recipes/release/liquid-glass.json +301 -0
  65. package/recipes/release/odd-tile-dark.json +688 -0
  66. package/recipes/release/odd-tile-dense.json +1056 -0
  67. package/recipes/release/odd-tile-shift.json +541 -0
  68. package/recipes/release/odd-tile-split.json +571 -0
  69. package/recipes/release/odd-tile.json +633 -0
  70. package/recipes/release/off-edge.json +96 -0
  71. package/recipes/release/plinth.json +794 -0
  72. package/recipes/release/riso.json +108 -0
  73. package/recipes/release/series/fluid-label.json +561 -0
  74. package/recipes/release/series/fluid-pixel.json +334 -0
  75. package/recipes/release/series/hairline.json +305 -0
  76. package/recipes/release/series/halo.json +282 -0
  77. package/recipes/release/series/mark-crop.json +135 -0
  78. package/recipes/release/series/mark-window.json +150 -0
  79. package/recipes/release/series/photo-label.json +475 -0
  80. package/recipes/release/series/smear-field.json +345 -0
  81. package/recipes/release/series/smear.json +202 -0
  82. package/recipes/release/series/spin.json +172 -0
  83. package/recipes/release/series/split-ghost.json +230 -0
  84. package/recipes/release/series/zoom.json +168 -0
  85. package/recipes/release/small-print.json +114 -0
  86. package/recipes/release/stack.json +156 -0
  87. package/recipes/release/swiss.json +117 -0
  88. package/schema/recipe.schema.json +332 -65
  89. package/src/animate/index.d.ts +9 -19
  90. package/src/animate/index.js +6 -111
  91. package/src/animate/keyframes.d.ts +21 -0
  92. package/src/animate/keyframes.js +119 -0
  93. package/src/animate/marquee.d.ts +30 -0
  94. package/src/animate/marquee.js +93 -0
  95. package/src/animate/morph.d.ts +8 -0
  96. package/src/animate/morph.js +99 -0
  97. package/src/animate/motion.d.ts +59 -0
  98. package/src/animate/motion.js +63 -0
  99. package/src/animate/sheet.d.ts +19 -0
  100. package/src/animate/sheet.js +61 -0
  101. package/src/animate/timing.d.ts +28 -0
  102. package/src/animate/timing.js +146 -0
  103. package/src/animate/tracks.d.ts +18 -0
  104. package/src/animate/tracks.js +139 -0
  105. package/src/diagram.js +4 -3
  106. package/src/extract.js +2 -2
  107. package/src/fonts.js +1 -2
  108. package/src/geometry.js +4 -34
  109. package/src/grid.js +1 -1
  110. package/src/handmade.js +9 -7
  111. package/src/icons.js +1 -2
  112. package/src/index.d.ts +39 -48
  113. package/src/index.js +5 -5
  114. package/src/inter-data.js +1 -1
  115. package/src/json-recipe.js +23 -7
  116. package/src/layer-options.js +21 -1
  117. package/src/layers/barcode.js +3 -3
  118. package/src/layers/geometry.js +5 -5
  119. package/src/layers/perforation.js +3 -3
  120. package/src/layers/repeat.js +2 -2
  121. package/src/layers/shape.d.ts +2 -2
  122. package/src/layers/shape.js +5 -5
  123. package/src/layers/soft.js +2 -2
  124. package/src/layers/svg.js +2 -3
  125. package/src/layers/table.js +3 -3
  126. package/src/layers/tear.d.ts +1 -1
  127. package/src/layers/tear.js +4 -4
  128. package/src/layers/texture.js +3 -3
  129. package/src/layers/util.js +15 -2
  130. package/src/layers.js +10 -26
  131. package/src/{effects/paint.js → paint/index.js} +5 -5
  132. package/src/{effects/paint-presets.d.ts → paint/presets.d.ts} +1 -1
  133. package/src/{effects/paint-presets.js → paint/presets.js} +1 -1
  134. package/src/{effects → paint}/tile.d.ts +1 -1
  135. package/src/{effects → paint}/tile.js +4 -4
  136. package/src/palette.js +1 -1
  137. package/src/platform/files.d.ts +6 -0
  138. package/src/platform/files.js +41 -0
  139. package/src/platform/harfbuzz.js +1 -1
  140. package/src/platform/index.d.ts +3 -3
  141. package/src/platform/index.js +6 -4
  142. package/src/platform/load.js +1 -2
  143. package/src/print.js +9 -7
  144. package/src/recipes.js +1 -1
  145. package/src/render/backdrop.js +2 -2
  146. package/src/render/grain.js +1 -1
  147. package/src/render/group.js +3 -3
  148. package/src/render/key.js +1 -2
  149. package/src/{effects → render}/transform.d.ts +2 -1
  150. package/src/{effects → render}/transform.js +17 -5
  151. package/src/render/use.d.ts +1 -1
  152. package/src/render/use.js +2 -2
  153. package/src/render/wrap.js +5 -60
  154. package/src/render.js +55 -18
  155. package/src/text.js +1 -1
  156. package/src/type/auto-color.js +1 -1
  157. package/src/type/glyphs.js +2 -2
  158. package/src/type/line.js +1 -1
  159. package/src/type/live.js +1 -1
  160. package/src/type/path.js +2 -2
  161. package/src/type/pixel.d.ts +0 -2
  162. package/src/type/pixel.js +3 -33
  163. package/src/type/spans.js +2 -2
  164. package/src/type/typeset.js +19 -8
  165. package/src/version.js +1 -1
  166. package/src/color.js +0 -246
  167. package/src/effects/ascii.d.ts +0 -7
  168. package/src/effects/ascii.js +0 -93
  169. package/src/effects/buffer.d.ts +0 -60
  170. package/src/effects/buffer.js +0 -472
  171. package/src/effects/detail.d.ts +0 -12
  172. package/src/effects/detail.js +0 -124
  173. package/src/effects/digital.d.ts +0 -14
  174. package/src/effects/digital.js +0 -216
  175. package/src/effects/distort.d.ts +0 -20
  176. package/src/effects/distort.js +0 -314
  177. package/src/effects/dither.d.ts +0 -8
  178. package/src/effects/dither.js +0 -147
  179. package/src/effects/drop-shadow.d.ts +0 -4
  180. package/src/effects/drop-shadow.js +0 -45
  181. package/src/effects/extrude.d.ts +0 -5
  182. package/src/effects/extrude.js +0 -55
  183. package/src/effects/film.d.ts +0 -11
  184. package/src/effects/film.js +0 -204
  185. package/src/effects/glow.d.ts +0 -8
  186. package/src/effects/glow.js +0 -56
  187. package/src/effects/grade.d.ts +0 -30
  188. package/src/effects/grade.js +0 -274
  189. package/src/effects/halftone.d.ts +0 -8
  190. package/src/effects/halftone.js +0 -95
  191. package/src/effects/index.d.ts +0 -125
  192. package/src/effects/index.js +0 -277
  193. package/src/effects/ink.d.ts +0 -12
  194. package/src/effects/ink.js +0 -70
  195. package/src/effects/lens.d.ts +0 -14
  196. package/src/effects/lens.js +0 -277
  197. package/src/effects/light.d.ts +0 -10
  198. package/src/effects/light.js +0 -59
  199. package/src/effects/looks.d.ts +0 -9
  200. package/src/effects/looks.js +0 -74
  201. package/src/effects/mask.d.ts +0 -52
  202. package/src/effects/mask.js +0 -314
  203. package/src/effects/motion.d.ts +0 -6
  204. package/src/effects/motion.js +0 -60
  205. package/src/effects/newsprint.d.ts +0 -6
  206. package/src/effects/newsprint.js +0 -89
  207. package/src/effects/optics.d.ts +0 -10
  208. package/src/effects/optics.js +0 -213
  209. package/src/effects/options.d.ts +0 -6
  210. package/src/effects/options.js +0 -106
  211. package/src/effects/pixels.d.ts +0 -24
  212. package/src/effects/pixels.js +0 -138
  213. package/src/effects/polar.d.ts +0 -10
  214. package/src/effects/polar.js +0 -265
  215. package/src/effects/region.d.ts +0 -8
  216. package/src/effects/region.js +0 -136
  217. package/src/effects/round-border.d.ts +0 -5
  218. package/src/effects/round-border.js +0 -62
  219. package/src/effects/screen.d.ts +0 -14
  220. package/src/effects/screen.js +0 -212
  221. package/src/effects/sort.d.ts +0 -8
  222. package/src/effects/sort.js +0 -125
  223. package/src/effects/symmetry.d.ts +0 -6
  224. package/src/effects/symmetry.js +0 -74
  225. package/src/effects/tone.d.ts +0 -16
  226. package/src/effects/tone.js +0 -117
  227. package/src/effects/util.d.ts +0 -16
  228. package/src/effects/util.js +0 -85
  229. package/src/platform/bytes.d.ts +0 -22
  230. package/src/platform/bytes.js +0 -103
  231. package/src/platform/compress.d.ts +0 -12
  232. package/src/platform/compress.js +0 -81
  233. package/src/platform/node.d.ts +0 -26
  234. package/src/platform/node.js +0 -86
  235. package/src/png.js +0 -126
  236. package/src/raster/index.d.ts +0 -24
  237. package/src/raster/index.js +0 -171
  238. package/src/render/content.d.ts +0 -5
  239. package/src/render/content.js +0 -58
  240. package/src/render/tiles.d.ts +0 -6
  241. package/src/render/tiles.js +0 -77
  242. package/src/rng.js +0 -95
  243. package/src/svg.js +0 -69
  244. /package/src/{effects/paint.d.ts → paint/index.d.ts} +0 -0
package/src/type/live.js CHANGED
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import { fontBytes } from '../fonts.js';
9
- import { toBase64 } from '../platform/bytes.js';
9
+ import { toBase64 } from '../platform/index.js';
10
10
 
11
11
  /** Faces embedded in this document already (live text). @type {WeakMap<object, Set<string>>} */
12
12
  const embedded = new WeakMap();
package/src/type/path.js CHANGED
@@ -5,8 +5,8 @@
5
5
  * @module type/path
6
6
  */
7
7
 
8
- import { num } from '../svg.js';
9
- import { len } from '../geometry.js';
8
+ import { num } from '@logomarks/image-effects/svg';
9
+ import { len } from '@logomarks/image-effects/length';
10
10
  import { flattenPath } from '../flatten.js';
11
11
  import { pos } from '../layers/util.js';
12
12
 
@@ -5,5 +5,3 @@ import type { LoadedFont } from '../fonts.js';
5
5
  export function pixelFace(): LoadedFont;
6
6
  /** The characters it draws (lowercase is drawn as capitals). */
7
7
  export const PIXEL_CHARS: string;
8
- /** A glyph's 5×7 bitmap (35 cells row by row, `#` inked), or undefined. */
9
- export function pixelBitmap(ch: string): string | undefined;
package/src/type/pixel.js CHANGED
@@ -2,7 +2,8 @@
2
2
  * A built-in 5×7 pixel face (`font: 'pixel'`): A–Z, 0–9 and basic
3
3
  * punctuation, each glyph a set of square cells merged into row runs. It
4
4
  * is a face like any other (layout, fitting, justification, text on a
5
- * path, per-glyph control all work), drawn as paths.
5
+ * path, per-glyph control all work), drawn as paths. The bitmaps are
6
+ * image-effects' pixel font (its `ascii` effect draws with them).
6
7
  *
7
8
  * Metrics, in a 1000-unit em: a cell is 100 units, capitals are 7 cells
8
9
  * (700), the advance is 6 cells (a cell's gap after each glyph), a space
@@ -11,30 +12,7 @@
11
12
  * @module type/pixel
12
13
  */
13
14
 
14
- /** Glyph bitmaps, 5 wide × 7 tall, `#` inked. */
15
- const BITMAPS = /** @type {Record<string, string>} */ ({
16
- A: '.###.#...##...#######...##...##...#', B: '####.#...##...#####.#...##...#####.', C: '.###.#...##....#....#....#...#.###.',
17
- D: '####.#...##...##...##...##...#####.', E: '######....#....####.#....#....#####', F: '######....#....####.#....#....#....',
18
- G: '.###.#...##....#.####...##...#.####', H: '#...##...##...#######...##...##...#', I: '.###...#....#....#....#....#...###.',
19
- J: '..###...#....#....#....#.#..#..##..', K: '#...##..#.#.#..##...#.#..#..#.#...#', L: '#....#....#....#....#....#....#####',
20
- M: '#...###.###.#.##.#.##...##...##...#', N: '#...##...###..##.#.##..###...##...#', O: '.###.#...##...##...##...##...#.###.',
21
- P: '####.#...##...#####.#....#....#....', Q: '.###.#...##...##...##.#.##..#..##.#', R: '####.#...##...#####.#.#..#..#.#...#',
22
- S: '.#####....#.....###.....#....#####.', T: '#####..#....#....#....#....#....#..', U: '#...##...##...##...##...##...#.###.',
23
- V: '#...##...##...##...##...#.#.#...#..', W: '#...##...##...##.#.##.#.##.#.#.#.#.', X: '#...##...#.#.#...#...#.#.#...##...#',
24
- Y: '#...##...#.#.#...#....#....#....#..', Z: '#####....#...#...#...#...#....#####', '0': '.###.#...##..###.#.###..##...#.###.',
25
- '1': '..#...##....#....#....#....#...###.', '2': '.###.#...#....#...#...#...#...#####', '3': '#####...#...#.....#.....##...#.###.',
26
- '4': '...#...##..#.#.#..#.#####...#....#.', '5': '######....####.....#....##...#.###.', '6': '..##..#...#....####.#...##...#.###.',
27
- '7': '#####....#...#...#...#....#....#...', '8': '.###.#...##...#.###.#...##...#.###.', '9': '.###.#...##...#.####....#...#..##..',
28
- '.': '..........................##...##..', ',': '.....................##....#...#...', '!': '..#....#....#....#....#.........#..',
29
- '?': '.###.#...#....#...#...#.........#..', '-': '................###................', ':': '......##...##........##...##.......',
30
- ';': '......##...##........##....#...#...', "'": '..#....#...#.......................', '"': '.#.#..#.#..#.#.....................',
31
- '/': '.........#...#...#...#...#.........', '&': '.##..#..#.#.#...#...#.#.##..#..##.#', '+': '.......#....#..#####..#....#.......',
32
- '=': '..........#####.....#####..........', '(': '...#...#...#....#....#.....#.....#.', ')': '.#.....#.....#....#....#...#...#...',
33
- '#': '.#.#..#.#.#####.#.#.#####.#.#..#.#.', '@': '.###.#...##.####.#.##.####.....###.', '%': '##...##..#...#...#...#...#..##...##',
34
- '*': '.......#..#.#.#.###.#.#.#..#.......', _: '..............................#####', '<': '...#...#...#...#.....#.....#.....#.',
35
- '>': '.#.....#.....#.....#...#...#...#...', '[': '.###..#....#....#....#....#....###.', ']': '.###....#....#....#....#....#..###.',
36
- '|': '..#....#....#....#....#....#....#..', '$': '..#...#####.#...###...#.#####...#..', '·': '.................#.................',
37
- });
15
+ import { PIXEL_GLYPHS as BITMAPS } from '@logomarks/image-effects/pixel-font';
38
16
 
39
17
  /** Cell size, font units. */
40
18
  const CELL = 100;
@@ -105,11 +83,3 @@ export function pixelFace() {
105
83
 
106
84
  /** Characters the pixel face draws. */
107
85
  export const PIXEL_CHARS = Object.freeze(CHARS.join(''));
108
-
109
- /**
110
- * A glyph's 5×7 bitmap (35 cells row by row, `#` inked), or undefined.
111
- *
112
- * @param {string} ch
113
- * @returns {string | undefined}
114
- */
115
- export const pixelBitmap = (ch) => (Object.hasOwn(BITMAPS, ch.toUpperCase()) ? BITMAPS[ch.toUpperCase()] : undefined);
package/src/type/spans.js CHANGED
@@ -10,8 +10,8 @@
10
10
  * @module type/spans
11
11
  */
12
12
 
13
- import { len } from '../geometry.js';
14
- import { toHex } from '../color.js';
13
+ import { len } from '@logomarks/image-effects/length';
14
+ import { toHex } from '@logomarks/image-effects/color';
15
15
  import { faceFor } from './line.js';
16
16
  import { shapeText } from './fallback.js';
17
17
 
@@ -36,14 +36,15 @@
36
36
  * @module type/typeset
37
37
  */
38
38
 
39
- import { attrs, esc, num } from '../svg.js';
40
- import { len, place, anchorAlign, unit } from '../geometry.js';
41
- import { toHex } from '../color.js';
39
+ import { attrs, esc, num } from '@logomarks/image-effects/svg';
40
+ import { len } from '@logomarks/image-effects/length';
41
+ import { place, anchorAlign, unit } from '../geometry.js';
42
+ import { toHex } from '@logomarks/image-effects/color';
42
43
  import { layout, caseText } from '../text.js';
43
- import { paint } from '../effects/paint.js';
44
+ import { paint } from '../paint/index.js';
44
45
  import { pos, wrap } from '../layers/util.js';
45
- import { presetStrokes } from '../effects/paint-presets.js';
46
- import { extrude } from '../effects/extrude.js';
46
+ import { presetStrokes } from '../paint/presets.js';
47
+ import { extrude } from '@logomarks/image-effects/effects/extrude';
47
48
  import { dashArray } from '../layers/shape.js';
48
49
  import { faceFor, mappedPath, setLine } from './line.js';
49
50
  import { textPathD, pathSampler } from './path.js';
@@ -53,6 +54,7 @@ import { fallbackFaces, shapeText } from './fallback.js';
53
54
  import { controlGlyphs, glyphCount } from './glyphs.js';
54
55
  import { loadSpans, spanLines, shapeSpans, spanText } from './spans.js';
55
56
  import { recordLayout } from './layout-ref.js';
57
+ import { marquee } from '../animate/marquee.js';
56
58
 
57
59
  /** Options that switch the text layer onto this typesetter. */
58
60
  export const TYPE_OPTIONS = ['x', 'y', 'width', 'justify', 'lengthAdjust', 'fit', 'stretch', 'axes', 'stroke', 'strokeWidth', 'strokes', 'fill', 'path', 'startOffset', 'fitPath', 'dy', 'render', 'autoColors', 'lines', 'spans', 'glyphs', 'fallback', 'split', 'dash', 'extrude', 'maxHeight', 'autoThreshold'];
@@ -170,7 +172,7 @@ export async function typeset(layer, ctx) {
170
172
  }
171
173
  const so = layer.startOffset ?? (layer.fitPath ? 0 : 0.5);
172
174
  const offset = typeof so === 'number' && so <= 1 && so >= 0 ? so * sampler.length : pos(so, 'x', ctx.canvas);
173
- const start = layer.fitPath ? offset : align === 'center' || layer.align == null ? offset - run.width / 2 : align === 'right' ? offset - run.width : offset;
175
+ let start = layer.fitPath ? offset : align === 'center' || layer.align == null ? offset - run.width / 2 : align === 'right' ? offset - run.width : offset;
174
176
  const dy = len(layer.dy, ctx.canvas, 0);
175
177
  const adv = (/** @type {number} */ i) => (i + 1 < run.glyphs.length ? run.glyphs[i + 1].x : run.width) - run.glyphs[i].x;
176
178
  const mids = run.glyphs.map((g, i) => g.x + adv(i) / 2);
@@ -185,8 +187,17 @@ export async function typeset(layer, ctx) {
185
187
  const whole = { x: 0, y: 0, width: ctx.width, height: ctx.height };
186
188
  const fill = layer.fill === 'none' ? { defs: '', value: 'none' } : layer.color === 'auto' ? { defs: '', value: await autoColor(layer, ctx, whole) } : paint(layer.fill ?? layer.color, p.text, { id: `${id}f`, box: whole, canvas: ctx.canvas });
187
189
  defs += fill.defs;
190
+ // A marquee (motion): the still moves the start; playing, each glyph is drawn once in its own frame and placed by copies.
191
+ const running = /** @type {any} */ (ctx).marquee && marquee(/** @type {any} */ (ctx).marquee, {
192
+ run, mids, start, sampler, id,
193
+ glyph: (i) => drawBuckets({ glyphs: [run.glyphs[i]] }, (only) => mappedPath(face, run, size, (gx, gy) => [gx - mids[i], gy + dy], (g) => g === run.glyphs[i] && only(g)), fill.value, strokes, ctx),
194
+ });
195
+ if (running) {
196
+ start += running.shift;
197
+ defs += running.defs;
198
+ }
188
199
  report(ctx, layer, content, size, face, null, { capHeight: (face.capHeight / face.upem) * size });
189
- const drawn = drawBuckets(run, along, fill.value, strokes, ctx);
200
+ const drawn = running?.body ?? drawBuckets(run, along, fill.value, strokes, ctx);
190
201
  recordLayout(ctx, layer.id, whole, { x: 0, y: 0, baseline: 0, size, lines: 1 });
191
202
  return { defs, body: wrap(layer, finish(drawn, layer, ctx)) };
192
203
  }
package/src/version.js CHANGED
@@ -1,4 +1,4 @@
1
1
  // Generated by scripts/generate.mjs from package.json. Do not edit.
2
2
 
3
3
  /** The package version, part of every cache key. */
4
- export const VERSION = '1.1.0';
4
+ export const VERSION = '1.3.1';
package/src/color.js DELETED
@@ -1,246 +0,0 @@
1
- /**
2
- * Colour parsing and the few colour-space moves palettes need.
3
- *
4
- * Everything inside the library is a lowercase `#rrggbb` string. Input can
5
- * be hex (3, 4, 6 or 8 digits), `rgb()`/`rgba()` or `hsl()`/`hsla()`; alpha
6
- * is dropped, since a cover's layers carry their own `opacity`.
7
- *
8
- * Mixing and contrast come from `@arraypress/color-utils`; this module adds
9
- * OKLCH, so derived palettes can rotate hue and set lightness perceptually
10
- * instead of in sRGB (where "the same lightness" means very different
11
- * things for yellow and blue).
12
- *
13
- * @module color
14
- */
15
-
16
- import { hexToRgb, rgbToHex, contrastRatio } from '@arraypress/color-utils';
17
-
18
- /**
19
- * Parse a CSS colour to `#rrggbb`.
20
- *
21
- * @param {string} input - `#abc`, `#aabbcc`, `#aabbccdd`, `rgb(…)`, `rgba(…)`, `hsl(…)` or `hsla(…)`.
22
- * @returns {string} Lowercase `#rrggbb`.
23
- * @throws {TypeError} When the string isn't a colour this function understands.
24
- *
25
- * @example
26
- * toHex('rgb(37 99 235)'); // '#2563eb'
27
- * toHex('#FA0'); // '#ffaa00'
28
- * toHex('hsl(220 80% 50%)'); // '#1a5ee6'
29
- */
30
- export function toHex(input) {
31
- if (typeof input !== 'string') throw new TypeError(`Expected a colour string, got ${typeof input}`);
32
- const s = input.trim().toLowerCase();
33
-
34
- const hex = s.match(/^#?([0-9a-f]{3,8})$/);
35
- if (hex && [3, 4, 6, 8].includes(hex[1].length)) {
36
- let h = hex[1];
37
- if (h.length <= 4) h = h.split('').map((c) => c + c).join('');
38
- return '#' + h.slice(0, 6);
39
- }
40
-
41
- const fn = s.match(/^(rgba?|hsla?)\(([^)]*)\)$/);
42
- if (fn) {
43
- const parts = fn[2].split(/[\s,/]+/).filter(Boolean);
44
- if (parts.length >= 3) {
45
- if (fn[1].startsWith('rgb')) {
46
- const [r, g, b] = parts.slice(0, 3).map((p) => (p.endsWith('%') ? parseFloat(p) * 2.55 : parseFloat(p)));
47
- if ([r, g, b].every(Number.isFinite)) return rgbToHex(r, g, b);
48
- } else {
49
- const h = parseFloat(parts[0]);
50
- const sat = parseFloat(parts[1]) / 100;
51
- const l = parseFloat(parts[2]) / 100;
52
- if ([h, sat, l].every(Number.isFinite)) return hslToHex(h, sat, l);
53
- }
54
- }
55
- }
56
- throw new TypeError(`Unrecognised colour: "${input}"`);
57
- }
58
-
59
- /**
60
- * HSL to `#rrggbb`.
61
- *
62
- * @param {number} h - Hue in degrees.
63
- * @param {number} s - Saturation, 0–1.
64
- * @param {number} l - Lightness, 0–1.
65
- * @returns {string} `#rrggbb`.
66
- */
67
- function hslToHex(h, s, l) {
68
- const k = (n) => (n + h / 30) % 12;
69
- const a = s * Math.min(l, 1 - l);
70
- const f = (n) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1)));
71
- return rgbToHex(f(0) * 255, f(8) * 255, f(4) * 255);
72
- }
73
-
74
- /** @param {number} c - sRGB channel 0–1. @returns {number} Linear channel. */
75
- const toLinear = (c) => (c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4);
76
- /** @param {number} c - Linear channel. @returns {number} sRGB channel 0–1. */
77
- const toGamma = (c) => (c <= 0.0031308 ? 12.92 * c : 1.055 * c ** (1 / 2.4) - 0.055);
78
-
79
- /**
80
- * `#rrggbb` to OKLCH.
81
- *
82
- * @param {string} color - Any colour {@link toHex} accepts.
83
- * @returns {{ l: number, c: number, h: number }} Lightness 0–1, chroma (~0–0.37), hue in degrees.
84
- *
85
- * @example
86
- * toOklch('#2563eb'); // { l: 0.546, c: 0.215, h: 262.9 }
87
- */
88
- export function toOklch(color) {
89
- const { r, g, b } = hexToRgb(toHex(color));
90
- const [lr, lg, lb] = [r, g, b].map((v) => toLinear(v / 255));
91
- const l = Math.cbrt(0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb);
92
- const m = Math.cbrt(0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb);
93
- const s = Math.cbrt(0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb);
94
- const L = 0.2104542553 * l + 0.793617785 * m - 0.0040720468 * s;
95
- const A = 1.9779984951 * l - 2.428592205 * m + 0.4505937099 * s;
96
- const B = 0.0259040371 * l + 0.7827717662 * m - 0.808675766 * s;
97
- const h = (Math.atan2(B, A) * 180) / Math.PI;
98
- return { l: L, c: Math.hypot(A, B), h: h < 0 ? h + 360 : h };
99
- }
100
-
101
- /**
102
- * OKLCH to `#rrggbb`. Out-of-gamut colours are brought in by reducing
103
- * chroma (keeping lightness and hue), not by clipping channels, so a
104
- * derived colour keeps the hue it was asked for.
105
- *
106
- * @param {{ l: number, c: number, h: number }} lch - Lightness 0–1, chroma, hue in degrees.
107
- * @returns {string} `#rrggbb`.
108
- *
109
- * @example
110
- * fromOklch({ l: 0.7, c: 0.12, h: 30 }); // a soft coral
111
- */
112
- export function fromOklch({ l, c, h }) {
113
- const L = Math.max(0, Math.min(1, l));
114
- let chroma = Math.max(0, c);
115
- for (let i = 0; i < 24; i++) {
116
- const rgb = oklabToRgb(L, chroma * Math.cos((h * Math.PI) / 180), chroma * Math.sin((h * Math.PI) / 180));
117
- if (rgb.every((v) => v >= -0.0005 && v <= 1.0005) || chroma < 0.001) {
118
- return toRgbHex(rgb);
119
- }
120
- chroma *= 0.9;
121
- }
122
- return toRgbHex(oklabToRgb(L, 0, 0));
123
- }
124
-
125
- /**
126
- * Clamp 0–1 sRGB channels and write them as `#rrggbb`.
127
- *
128
- * @param {[number, number, number]} rgb
129
- * @returns {string}
130
- */
131
- function toRgbHex([r, g, b]) {
132
- const c = (/** @type {number} */ v) => Math.max(0, Math.min(1, v)) * 255;
133
- return rgbToHex(c(r), c(g), c(b));
134
- }
135
-
136
- /**
137
- * OKLab to gamma-encoded sRGB channels (0–1, possibly out of range).
138
- *
139
- * @param {number} L @param {number} A @param {number} B
140
- * @returns {[number, number, number]} sRGB channels.
141
- */
142
- function oklabToRgb(L, A, B) {
143
- const l = (L + 0.3963377774 * A + 0.2158037573 * B) ** 3;
144
- const m = (L - 0.1055613458 * A - 0.0638541728 * B) ** 3;
145
- const s = (L - 0.0894841775 * A - 1.291485548 * B) ** 3;
146
- return [
147
- toGamma(4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s),
148
- toGamma(-1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s),
149
- toGamma(-0.0041960863 * l - 0.7034186147 * m + 1.707614701 * s),
150
- ];
151
- }
152
-
153
- /**
154
- * Mix two colours in OKLab (perceptually even, no muddy midpoints).
155
- *
156
- * @param {string} a - First colour.
157
- * @param {string} b - Second colour.
158
- * @param {number} t - 0 gives `a`, 1 gives `b`.
159
- * @returns {string} `#rrggbb`.
160
- *
161
- * @example
162
- * mixOklab('#ffffff', '#2563eb', 0.1); // a pale blue tint
163
- */
164
- export function mixOklab(a, b, t) {
165
- const p = toLab(a);
166
- const q = toLab(b);
167
- const rgb = oklabToRgb(p[0] + (q[0] - p[0]) * t, p[1] + (q[1] - p[1]) * t, p[2] + (q[2] - p[2]) * t);
168
- return toRgbHex(rgb);
169
- }
170
-
171
- /** @param {string} color @returns {[number, number, number]} OKLab. */
172
- function toLab(color) {
173
- const { l, c, h } = toOklch(color);
174
- return [l, c * Math.cos((h * Math.PI) / 180), c * Math.sin((h * Math.PI) / 180)];
175
- }
176
-
177
- /**
178
- * Mix two colours in OKLCH: lightness and chroma linearly, hue the short
179
- * way round. Unlike OKLab, a mix of two far-apart hues stays vivid instead
180
- * of passing through grey (orange → blue goes by way of pink, not mud).
181
- *
182
- * @param {string} a - First colour.
183
- * @param {string} b - Second colour.
184
- * @param {number} t - 0 gives `a`, 1 gives `b`.
185
- * @returns {string} `#rrggbb`.
186
- *
187
- * @example
188
- * mixOklch('#c9502a', '#a9bdf2', 0.5); // a clear rose, not a muddy mauve
189
- */
190
- export function mixOklch(a, b, t) {
191
- const p = toOklch(a);
192
- const q = toOklch(b);
193
- // A near-grey has no meaningful hue: take the other colour's.
194
- const ph = p.c < 0.02 ? q.h : p.h;
195
- const qh = q.c < 0.02 ? p.h : q.h;
196
- const dh = ((qh - ph + 540) % 360) - 180;
197
- return fromOklch({ l: p.l + (q.l - p.l) * t, c: p.c + (q.c - p.c) * t, h: (ph + dh * t + 360) % 360 });
198
- }
199
-
200
- /** Near-black used wherever white doesn't read. */
201
- export const NEAR_BLACK = '#111111';
202
-
203
- /**
204
- * A colour for a mark on one or more backgrounds: `prefer` when it clears
205
- * `ratio` against every one of them, else whichever of white and
206
- * near-black has the better worst case.
207
- *
208
- * For icons and other non-text marks WCAG 1.4.11 asks 3:1 (text needs
209
- * 4.5:1), so `onBackground(bg, 3, 'white')` keeps white on mid-tone fills
210
- * where a text rule would flip to black.
211
- *
212
- * @param {string | string[]} backgrounds - The colour(s) under the mark.
213
- * @param {number} [ratio=3] - Contrast to clear.
214
- * @param {'white' | 'black' | string} [prefer='white'] - The colour to use when it clears `ratio`.
215
- * @returns {string} `#rrggbb`.
216
- *
217
- * @example
218
- * onBackground('#e0533d', 3, 'white'); // '#ffffff' (3.6:1)
219
- * onBackground('#ffe14d', 3, 'white'); // '#111111'
220
- */
221
- export function onBackground(backgrounds, ratio = 3, prefer = 'white') {
222
- const bgs = [].concat(/** @type {any} */ (backgrounds)).map(toHex);
223
- const want = prefer === 'white' ? '#ffffff' : prefer === 'black' ? NEAR_BLACK : toHex(prefer);
224
- const worst = (/** @type {string} */ c) => Math.min(...bgs.map((b) => contrastRatio(c, b)));
225
- if (worst(want) >= ratio) return want;
226
- return worst('#ffffff') >= worst(NEAR_BLACK) ? '#ffffff' : NEAR_BLACK;
227
- }
228
-
229
- /**
230
- * The colour of an eased multi-stop gradient at `t` (0–1), the way the
231
- * `linear` and `radial` layers draw it (smoothstep between stops, in OKLab).
232
- *
233
- * @param {string[]} colors - Two or more stops, evenly spaced.
234
- * @param {number} t - Position along the gradient.
235
- * @param {boolean} [ease=true] - Ease between stops.
236
- * @returns {string} `#rrggbb`.
237
- */
238
- export function gradientAt(colors, t, ease = true) {
239
- const list = colors.map(toHex);
240
- if (list.length === 1) return list[0];
241
- const n = list.length - 1;
242
- const x = Math.max(0, Math.min(1, t)) * n;
243
- const i = Math.min(n - 1, Math.floor(x));
244
- const f = x - i;
245
- return mixOklab(list[i], list[i + 1], ease ? f * f * (3 - 2 * f) : f);
246
- }
@@ -1,7 +0,0 @@
1
- // The layer as text art.
2
- import type { Effect } from './index.js';
3
- import type { Pixels } from './buffer.js';
4
- import type { PixelResult } from './pixels.js';
5
-
6
- /** `ascii`, on pixels (one pixel per font pixel, scaled up crisp). */
7
- export function ascii(img: Pixels, fx: Extract<Effect, { kind: 'ascii' }>, ctx: object): PixelResult;
@@ -1,93 +0,0 @@
1
- /**
2
- * `ascii`: the layer as text art. Each cell's brightness picks a
3
- * character from a ramp (ordered by how much ink each glyph of the
4
- * built-in 5×7 pixel face carries), drawn in the cell's own colour or one
5
- * ink, on a background. Rendered at one pixel per font pixel and embedded
6
- * scaled up crisp, so it's tiny and sharp at any size.
7
- *
8
- * @module effects/ascii
9
- */
10
-
11
- import { px, premultiplied } from './buffer.js';
12
- import { rgb } from './util.js';
13
- import { pixelBitmap } from '../type/pixel.js';
14
-
15
- /** The default ramp, light to dense. */
16
- const RAMP = ' .:-=+*%#@';
17
-
18
- /**
19
- * `ascii`: `cell` (a length: a character's width; height is 8/6 of it),
20
- * `ramp` (characters, sparse to dense, re-ordered by their ink), `color`
21
- * (`'source'`, each character the colour under it, or one colour),
22
- * `background` (a colour, or `'none'`), `invert` (dense where it's dark,
23
- * for dark ink on paper), `contrast` (after the tones are stretched to the
24
- * layer's own range).
25
- *
26
- * @param {import('./buffer.js').Pixels} img
27
- * @param {import('./index.js').Effect & { kind: 'ascii' }} fx
28
- * @param {any} ctx
29
- * @returns {import('./pixels.js').PixelResult}
30
- */
31
- export function ascii(img, fx, ctx) {
32
- const { width: w, height: h } = img;
33
- const cw = Math.max(3, px(fx.cell, ctx, img, '1.4vmin'));
34
- const ch = (cw * 8) / 6;
35
- const cols = Math.max(1, Math.round(w / cw));
36
- const rows = Math.max(1, Math.round(h / ch));
37
- // Glyphs that the face draws, by ink.
38
- const glyphs = [...new Set([...(fx.ramp ?? RAMP)])].map((c) => ({ c, bits: c === ' ' ? '.'.repeat(35) : pixelBitmap(c) })).filter((g) => g.bits);
39
- if (!glyphs.length) throw new TypeError('ascii: the ramp has no characters the pixel face draws (A–Z, 0–9, punctuation)');
40
- glyphs.sort((a, b) => (a.bits?.split('#').length ?? 0) - (b.bits?.split('#').length ?? 0));
41
- const src = premultiplied(img);
42
- const acc = new Float64Array(cols * rows * 5);
43
- for (let y = 0; y < h; y++) {
44
- const cy = Math.min(rows - 1, ((y * rows) / h) | 0);
45
- for (let x = 0; x < w; x++) {
46
- const c = (cy * cols + Math.min(cols - 1, ((x * cols) / w) | 0)) * 5;
47
- const i = (y * w + x) * 4;
48
- acc[c] += src[i]; acc[c + 1] += src[i + 1]; acc[c + 2] += src[i + 2]; acc[c + 3] += src[i + 3]; acc[c + 4]++;
49
- }
50
- }
51
- const ink = fx.color && fx.color !== 'source' ? rgb(fx.color).map((v) => v * 255) : null;
52
- const bg = fx.background === 'none' ? null : rgb(fx.background ?? '#0b0b0b').map((v) => v * 255);
53
- const contrast = fx.contrast ?? 1.2;
54
- // Tones stretched to the cells' own range, so a dim layer still reads.
55
- const lums = new Float32Array(cols * rows);
56
- let lo = 1;
57
- let hi = 0;
58
- for (let c = 0; c < lums.length; c++) {
59
- const a = acc[c * 5 + 3];
60
- if (!a) continue;
61
- lums[c] = (0.2126 * acc[c * 5] + 0.7152 * acc[c * 5 + 1] + 0.0722 * acc[c * 5 + 2]) / a;
62
- lo = Math.min(lo, lums[c]);
63
- hi = Math.max(hi, lums[c]);
64
- }
65
- const span = hi - lo > 0.05 ? hi - lo : 1;
66
- const W = cols * 6;
67
- const H = rows * 8;
68
- const out = new Uint8ClampedArray(W * H * 4);
69
- for (let cy = 0; cy < rows; cy++) {
70
- for (let cx = 0; cx < cols; cx++) {
71
- const c = (cy * cols + cx) * 5;
72
- const n = acc[c + 4] || 1;
73
- const a = acc[c + 3] / n;
74
- const k = a ? 255 / a : 0;
75
- const r = (acc[c] / n) * k;
76
- const g = (acc[c + 1] / n) * k;
77
- const b = (acc[c + 2] / n) * k;
78
- let l = (lums[cy * cols + cx] - (span === 1 ? 0 : lo)) / span;
79
- l = Math.min(1, Math.max(0, (l - 0.5) * contrast + 0.5));
80
- if (fx.invert) l = 1 - l;
81
- const glyph = a < 8 ? glyphs[0] : glyphs[Math.min(glyphs.length - 1, Math.floor(l * glyphs.length))];
82
- const col = ink ?? [r, g, b];
83
- for (let gy = 0; gy < 8; gy++) {
84
- for (let gx = 0; gx < 6; gx++) {
85
- const on = gy < 7 && gx < 5 && glyph.bits?.[gy * 5 + gx] === '#';
86
- const o = ((cy * 8 + gy) * W + cx * 6 + gx) * 4;
87
- if (on) { out[o] = col[0]; out[o + 1] = col[1]; out[o + 2] = col[2]; out[o + 3] = 255; } else if (bg) { out[o] = bg[0]; out[o + 1] = bg[1]; out[o + 2] = bg[2]; out[o + 3] = 255; }
88
- }
89
- }
90
- }
91
- }
92
- return { data: out, width: W, height: H, pixelated: true };
93
- }
@@ -1,60 +0,0 @@
1
- // Pixel helpers shared by the pixel effects.
2
- import type { Length } from '../index.js';
3
-
4
- /** A layer drawn as pixels: RGBA, 8-bit, not premultiplied. */
5
- export interface Pixels {
6
- data: Uint8ClampedArray;
7
- width: number;
8
- height: number;
9
- /** Pixels per layer unit. */
10
- scale: number;
11
- /** Every pixel has alpha 255. */
12
- opaque: boolean;
13
- }
14
-
15
- /** sRGB byte → linear light, 0–1. */
16
- export const TO_LINEAR: Float32Array;
17
- /** Linear light (clipped) to an sRGB byte. */
18
- export function toSrgb(v: number): number;
19
- /** A length in pixels. */
20
- export function px(v: Length | undefined, ctx: { canvas: { width: number; height: number } }, img: Pixels, fallback: Length): number;
21
- /** A point given as fractions of the canvas, in pixels. */
22
- export function pointPx(p: [number, number] | { x: number; y: number } | undefined, img: { width: number; height: number }, fallback?: [number, number]): [number, number];
23
- /** A point as fractions. */
24
- export function fraction(p: [number, number] | { x: number; y: number } | undefined, fallback: [number, number]): [number, number];
25
- /** 0–1. */
26
- export function clamp01(v: number): number;
27
- /** Hermite step from a to b. */
28
- export function smoothstep(a: number, b: number, v: number): number;
29
- /** A premultiplied copy (the same buffer when opaque). */
30
- export function premultiplied(img: Pixels): Uint8ClampedArray;
31
- /** Undo premultiplying, in place. */
32
- export function unpremultiply(d: Uint8ClampedArray): Uint8ClampedArray;
33
- /** Bilinear RGBA sample, the edge held, written to `out` at `o`. */
34
- export function bilinear(src: Uint8ClampedArray, w: number, h: number, x: number, y: number, out: Uint8ClampedArray | Float32Array, o: number): void;
35
- /** Resample every pixel from where `map` says it came from. */
36
- export function remap(img: Pixels, map: (x: number, y: number, at: Float64Array) => void): Uint8ClampedArray;
37
- /** Box radii for three passes approximating a Gaussian. */
38
- export function boxesFor(sigma: number): number[];
39
- /** A box blur along rows (0) or columns (1), the edge held. */
40
- export function boxPass(src: Uint8ClampedArray | Float32Array, dst: Uint8ClampedArray | Float32Array, w: number, h: number, ch: number, r: number, dir: 0 | 1): void;
41
- /** A Gaussian blur in place (three box passes each way). */
42
- export function gaussian<T extends Uint8ClampedArray | Float32Array>(data: T, w: number, h: number, ch: number, sigma: number, sigmaY?: number): T;
43
- /** Downsample by a whole factor, averaging blocks. */
44
- export function shrink(src: Uint8ClampedArray, w: number, h: number, f: number): { data: Uint8ClampedArray; width: number; height: number };
45
- /** Upscale, bilinear. */
46
- export function enlarge(src: Uint8ClampedArray, sw: number, sh: number, w: number, h: number): Uint8ClampedArray;
47
- /** Upscale, nearest neighbour. */
48
- export function nearest(src: Uint8ClampedArray, sw: number, sh: number, w: number, h: number): Uint8ClampedArray;
49
- /** Rec. 709 luma of a pixel, 0–255. */
50
- export function luma(d: ArrayLike<number>, i: number): number;
51
- /** The effect's own seed (hashed), else one from the layer's stream. */
52
- export function seedFor(fx: { seed?: number | string }, ctx: { rng: { int: (a: number, b: number) => number } }): number;
53
- /** An integer hash of a lattice point and a seed, 0–1. */
54
- export function hash2(x: number, y: number, seed: number): number;
55
- /** Smooth value noise, -1–1. */
56
- export function valueNoise(x: number, y: number, seed: number): number;
57
- /** Fractal value noise, about -1–1. */
58
- export function fractalNoise(x: number, y: number, seed: number, octaves?: number): number;
59
- /** A fractal noise field for every pixel, -1–1. */
60
- export function noiseField(w: number, h: number, period: number, seed: number, octaves?: number): Float32Array;