@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.
- package/CHANGELOG.md +60 -0
- package/README.md +18 -809
- package/package.json +3 -1
- package/recipes/flyers/afterglow-motion.json +540 -0
- package/recipes/flyers/afterglow.json +70 -0
- package/recipes/flyers/arcfield-mirror-motion.json +397 -0
- package/recipes/flyers/arcfield-mirror.json +364 -0
- package/recipes/flyers/bauhaus-oktav.json +319 -0
- package/recipes/flyers/bauhaus-ritmo.json +735 -0
- package/recipes/flyers/bauhaus-volta.json +394 -0
- package/recipes/flyers/bloomfall.json +99 -0
- package/recipes/flyers/drag-solenne-motion.json +444 -0
- package/recipes/flyers/drag-solenne.json +426 -0
- package/recipes/flyers/fennick-patches.json +874 -0
- package/recipes/flyers/ferro-pass.json +398 -0
- package/recipes/flyers/ferrova.json +812 -0
- package/recipes/flyers/grid-poster.json +91 -0
- package/recipes/flyers/grid-rest-rhythm.json +125 -0
- package/recipes/flyers/groovy-honeycomb.json +816 -0
- package/recipes/flyers/groovy-marigold.json +479 -0
- package/recipes/flyers/groovy-peachfuzz.json +974 -0
- package/recipes/flyers/halden-orbit-motion.json +381 -0
- package/recipes/flyers/halden-orbit.json +356 -0
- package/recipes/flyers/halvard.json +120 -0
- package/recipes/flyers/kairo-weekender.json +696 -0
- package/recipes/flyers/linger-motion.json +518 -0
- package/recipes/flyers/linger.json +67 -0
- package/recipes/flyers/little-orchard.json +105 -0
- package/recipes/flyers/lofi-hexmoth.json +912 -0
- package/recipes/flyers/lofi-lowbit-motion.json +888 -0
- package/recipes/flyers/lofi-lowbit.json +880 -0
- package/recipes/flyers/lofi-nullpoint.json +3013 -0
- package/recipes/flyers/logofolio.json +60 -0
- package/recipes/flyers/lumio-stack-motion.json +648 -0
- package/recipes/flyers/lumio-stack.json +438 -0
- package/recipes/flyers/magazine-cover.json +168 -0
- package/recipes/flyers/magazine-extended.json +269 -0
- package/recipes/flyers/magazine-knockout.json +40 -0
- package/recipes/flyers/northfold-varsity.json +618 -0
- package/recipes/flyers/open-studio.json +69 -0
- package/recipes/flyers/orrin-merch.json +715 -0
- package/recipes/flyers/oxyde.json +113 -0
- package/recipes/flyers/pixel-mosaic.json +58 -0
- package/recipes/flyers/quiet-fields.json +610 -0
- package/recipes/flyers/scrapbook-halcyon.json +948 -0
- package/recipes/flyers/solenne-ticket.json +730 -0
- package/recipes/flyers/solvane-motion.json +576 -0
- package/recipes/flyers/solvane.json +98 -0
- package/recipes/flyers/subduct.json +99 -0
- package/recipes/flyers/sunvale-lineup.json +584 -0
- package/recipes/flyers/tidewell.json +91 -0
- package/recipes/flyers/tour-kairo.json +535 -0
- package/recipes/flyers/velora-stickers.json +885 -0
- package/recipes/flyers/wheatpaste-black.json +313 -0
- package/recipes/flyers/wheatpaste-mark.json +198 -0
- package/recipes/flyers/wheatpaste-torn.json +114 -0
- package/recipes/flyers/wheatpaste-wall.json +984 -0
- package/recipes/flyers/wheatpaste-words.json +233 -0
- package/recipes/release/aura.json +137 -0
- package/recipes/release/chrome.json +433 -0
- package/recipes/release/collage.json +372 -0
- package/recipes/release/duotone.json +113 -0
- package/recipes/release/film-still.json +102 -0
- package/recipes/release/headline.json +70 -0
- package/recipes/release/instant.json +170 -0
- package/recipes/release/label.json +364 -0
- package/recipes/release/liquid-glass.json +301 -0
- package/recipes/release/odd-tile-dark.json +688 -0
- package/recipes/release/odd-tile-dense.json +1056 -0
- package/recipes/release/odd-tile-shift.json +541 -0
- package/recipes/release/odd-tile-split.json +571 -0
- package/recipes/release/odd-tile.json +633 -0
- package/recipes/release/off-edge.json +96 -0
- package/recipes/release/plinth.json +794 -0
- package/recipes/release/riso.json +108 -0
- package/recipes/release/series/fluid-label.json +561 -0
- package/recipes/release/series/fluid-pixel.json +334 -0
- package/recipes/release/series/hairline.json +305 -0
- package/recipes/release/series/halo.json +282 -0
- package/recipes/release/series/mark-crop.json +135 -0
- package/recipes/release/series/mark-window.json +150 -0
- package/recipes/release/series/photo-label.json +475 -0
- package/recipes/release/series/smear-field.json +345 -0
- package/recipes/release/series/smear.json +202 -0
- package/recipes/release/series/spin.json +172 -0
- package/recipes/release/series/split-ghost.json +230 -0
- package/recipes/release/series/zoom.json +168 -0
- package/recipes/release/small-print.json +114 -0
- package/recipes/release/stack.json +156 -0
- package/recipes/release/swiss.json +117 -0
- package/schema/recipe.schema.json +638 -69
- package/src/animate/index.d.ts +9 -19
- package/src/animate/index.js +6 -111
- package/src/animate/keyframes.d.ts +21 -0
- package/src/animate/keyframes.js +119 -0
- package/src/animate/marquee.d.ts +30 -0
- package/src/animate/marquee.js +93 -0
- package/src/animate/morph.d.ts +8 -0
- package/src/animate/morph.js +99 -0
- package/src/animate/motion.d.ts +59 -0
- package/src/animate/motion.js +63 -0
- package/src/animate/sheet.d.ts +19 -0
- package/src/animate/sheet.js +61 -0
- package/src/animate/timing.d.ts +28 -0
- package/src/animate/timing.js +146 -0
- package/src/animate/tracks.d.ts +18 -0
- package/src/animate/tracks.js +139 -0
- package/src/diagram.js +4 -3
- package/src/extract.js +2 -2
- package/src/fonts.js +1 -2
- package/src/geometry.js +4 -34
- package/src/grid.js +1 -1
- package/src/handmade.js +9 -7
- package/src/icons.js +1 -2
- package/src/index.d.ts +47 -54
- package/src/index.js +5 -5
- package/src/inter-data.js +1 -1
- package/src/json-recipe.js +28 -7
- package/src/layer-options.js +26 -4
- package/src/layers/barcode.js +3 -3
- package/src/layers/geometry.js +5 -5
- package/src/layers/index.d.ts +6 -0
- package/src/layers/index.js +3 -0
- package/src/layers/mark-geometry.d.ts +15 -0
- package/src/layers/mark-geometry.js +46 -0
- package/src/layers/marks.d.ts +25 -0
- package/src/layers/marks.js +102 -0
- package/src/layers/mosaic.d.ts +32 -0
- package/src/layers/mosaic.js +155 -0
- package/src/layers/perforation.js +3 -3
- package/src/layers/repeat.js +2 -2
- package/src/layers/shape.d.ts +2 -2
- package/src/layers/shape.js +5 -5
- package/src/layers/soft.js +2 -2
- package/src/layers/svg.js +2 -3
- package/src/layers/table.js +3 -3
- package/src/layers/tear.d.ts +1 -1
- package/src/layers/tear.js +4 -4
- package/src/layers/texture.js +3 -3
- package/src/layers/util.js +15 -2
- package/src/layers.js +17 -32
- package/src/{effects/paint.js → paint/index.js} +5 -5
- package/src/{effects/paint-presets.d.ts → paint/presets.d.ts} +1 -1
- package/src/{effects/paint-presets.js → paint/presets.js} +1 -1
- package/src/{effects → paint}/tile.d.ts +1 -1
- package/src/{effects → paint}/tile.js +4 -4
- package/src/palette.js +1 -1
- package/src/platform/files.d.ts +6 -0
- package/src/platform/files.js +41 -0
- package/src/platform/harfbuzz.js +1 -1
- package/src/platform/index.d.ts +3 -3
- package/src/platform/index.js +6 -4
- package/src/platform/load.js +1 -2
- package/src/print.js +9 -7
- package/src/recipes.js +1 -1
- package/src/render/backdrop.js +2 -2
- package/src/render/grain.js +1 -1
- package/src/render/group.js +3 -3
- package/src/render/key.js +1 -2
- package/src/{effects → render}/transform.d.ts +2 -1
- package/src/{effects → render}/transform.js +17 -5
- package/src/render/use.d.ts +1 -1
- package/src/render/use.js +2 -2
- package/src/render/wrap.js +5 -60
- package/src/render.js +55 -18
- package/src/text.js +1 -1
- package/src/type/auto-color.js +1 -1
- package/src/type/fallback.js +1 -1
- package/src/type/glyphs.js +2 -2
- package/src/type/line.js +1 -1
- package/src/type/live.js +1 -1
- package/src/type/path.js +2 -2
- package/src/type/pixel.d.ts +0 -2
- package/src/type/pixel.js +3 -33
- package/src/type/spans.d.ts +5 -1
- package/src/type/spans.js +28 -5
- package/src/type/symbols.d.ts +1 -1
- package/src/type/symbols.js +20 -2
- package/src/type/typeset.js +25 -10
- package/src/version.js +1 -1
- package/src/color.js +0 -246
- package/src/effects/ascii.d.ts +0 -7
- package/src/effects/ascii.js +0 -93
- package/src/effects/buffer.d.ts +0 -60
- package/src/effects/buffer.js +0 -472
- package/src/effects/detail.d.ts +0 -12
- package/src/effects/detail.js +0 -124
- package/src/effects/digital.d.ts +0 -14
- package/src/effects/digital.js +0 -216
- package/src/effects/distort.d.ts +0 -20
- package/src/effects/distort.js +0 -314
- package/src/effects/dither.d.ts +0 -8
- package/src/effects/dither.js +0 -147
- package/src/effects/drop-shadow.d.ts +0 -4
- package/src/effects/drop-shadow.js +0 -45
- package/src/effects/extrude.d.ts +0 -5
- package/src/effects/extrude.js +0 -55
- package/src/effects/film.d.ts +0 -11
- package/src/effects/film.js +0 -204
- package/src/effects/glow.d.ts +0 -8
- package/src/effects/glow.js +0 -56
- package/src/effects/grade.d.ts +0 -30
- package/src/effects/grade.js +0 -274
- package/src/effects/halftone.d.ts +0 -8
- package/src/effects/halftone.js +0 -95
- package/src/effects/index.d.ts +0 -125
- package/src/effects/index.js +0 -277
- package/src/effects/ink.d.ts +0 -12
- package/src/effects/ink.js +0 -70
- package/src/effects/lens.d.ts +0 -14
- package/src/effects/lens.js +0 -277
- package/src/effects/light.d.ts +0 -10
- package/src/effects/light.js +0 -59
- package/src/effects/looks.d.ts +0 -9
- package/src/effects/looks.js +0 -74
- package/src/effects/mask.d.ts +0 -52
- package/src/effects/mask.js +0 -314
- package/src/effects/motion.d.ts +0 -6
- package/src/effects/motion.js +0 -60
- package/src/effects/newsprint.d.ts +0 -6
- package/src/effects/newsprint.js +0 -89
- package/src/effects/optics.d.ts +0 -10
- package/src/effects/optics.js +0 -213
- package/src/effects/options.d.ts +0 -6
- package/src/effects/options.js +0 -106
- package/src/effects/pixels.d.ts +0 -24
- package/src/effects/pixels.js +0 -138
- package/src/effects/polar.d.ts +0 -10
- package/src/effects/polar.js +0 -265
- package/src/effects/region.d.ts +0 -8
- package/src/effects/region.js +0 -136
- package/src/effects/round-border.d.ts +0 -5
- package/src/effects/round-border.js +0 -62
- package/src/effects/screen.d.ts +0 -14
- package/src/effects/screen.js +0 -212
- package/src/effects/sort.d.ts +0 -8
- package/src/effects/sort.js +0 -125
- package/src/effects/symmetry.d.ts +0 -6
- package/src/effects/symmetry.js +0 -74
- package/src/effects/tone.d.ts +0 -16
- package/src/effects/tone.js +0 -117
- package/src/effects/util.d.ts +0 -16
- package/src/effects/util.js +0 -85
- package/src/platform/bytes.d.ts +0 -22
- package/src/platform/bytes.js +0 -103
- package/src/platform/compress.d.ts +0 -12
- package/src/platform/compress.js +0 -81
- package/src/platform/node.d.ts +0 -26
- package/src/platform/node.js +0 -86
- package/src/png.js +0 -126
- package/src/raster/index.d.ts +0 -24
- package/src/raster/index.js +0 -171
- package/src/render/content.d.ts +0 -5
- package/src/render/content.js +0 -58
- package/src/render/tiles.d.ts +0 -6
- package/src/render/tiles.js +0 -77
- package/src/rng.js +0 -95
- package/src/svg.js +0 -69
- /package/src/{effects/paint.d.ts → paint/index.d.ts} +0 -0
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
|
-
|
|
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.d.ts
CHANGED
|
@@ -8,6 +8,8 @@ import type { Length, Color } from '../index.js';
|
|
|
8
8
|
export interface TextSpan {
|
|
9
9
|
content: string;
|
|
10
10
|
font?: string; weight?: number; style?: 'normal' | 'italic'; axes?: Record<string, number>;
|
|
11
|
+
/** Set at the layer's `boldWeight` (default 700) when `weight` isn't given. */
|
|
12
|
+
bold?: boolean;
|
|
11
13
|
/** Size relative to the layer's (default 1). */
|
|
12
14
|
scale?: number;
|
|
13
15
|
/** Size outright. */
|
|
@@ -24,7 +26,9 @@ export interface TextSpan {
|
|
|
24
26
|
export interface Span { text: string; face: LoadedFont; scale: number; tracking: number; paint: string | undefined; dy: number }
|
|
25
27
|
|
|
26
28
|
/** Resolve a layer's spans (faces loaded, sizes relative to `size`). */
|
|
27
|
-
export function loadSpans(layer: { spans: (TextSpan | string)[]; font?: string; weight?: number; style?: string; axes?: Record<string, number>; letterSpacing?: number; transform?: string }, ctx: RenderContext, size: number): Promise<Span[]>;
|
|
29
|
+
export function loadSpans(layer: { spans: (TextSpan | string)[]; font?: string; weight?: number; boldWeight?: number; style?: string; axes?: Record<string, number>; letterSpacing?: number; transform?: string }, ctx: RenderContext, size: number): Promise<Span[]>;
|
|
30
|
+
/** A `content` array as spans, one a line, when any item is a span object; else `undefined`. */
|
|
31
|
+
export function lineSpans(content: unknown): TextSpan[] | undefined;
|
|
28
32
|
/** Spans as lines (split at `\n`, wrapped at `maxWidth`). */
|
|
29
33
|
export function spanLines(spans: Span[], size: number, maxWidth: number, fallbacks: LoadedFont[]): Span[][];
|
|
30
34
|
/** Shape a line of spans as one run. */
|
package/src/type/spans.js
CHANGED
|
@@ -3,15 +3,17 @@
|
|
|
3
3
|
* style, axes, size, colour, tracking and baseline shift, set one after
|
|
4
4
|
* another on the same lines (a `\n` in a span starts a new line).
|
|
5
5
|
*
|
|
6
|
-
* `spans: [{ content, font, weight, style, axes, scale, size, color, letterSpacing, dy }]`;
|
|
6
|
+
* `spans: [{ content, font, weight, bold, style, axes, scale, size, color, letterSpacing, dy }]`;
|
|
7
7
|
* anything a span leaves out comes from the layer. `scale` sizes a span
|
|
8
|
-
* relative to the layer (`size` gives it outright)
|
|
8
|
+
* relative to the layer (`size` gives it outright); `bold: true` sets it at
|
|
9
|
+
* the layer's `boldWeight` (700). A `content` array with span objects in it
|
|
10
|
+
* is one span a line (`lineSpans`): a headline whose lines change weight.
|
|
9
11
|
*
|
|
10
12
|
* @module type/spans
|
|
11
13
|
*/
|
|
12
14
|
|
|
13
|
-
import { len } from '
|
|
14
|
-
import { toHex } from '
|
|
15
|
+
import { len } from '@logomarks/image-effects/length';
|
|
16
|
+
import { toHex } from '@logomarks/image-effects/color';
|
|
15
17
|
import { faceFor } from './line.js';
|
|
16
18
|
import { shapeText } from './fallback.js';
|
|
17
19
|
|
|
@@ -37,7 +39,8 @@ export async function loadSpans(layer, ctx, size) {
|
|
|
37
39
|
const out = [];
|
|
38
40
|
for (const given of layer.spans) {
|
|
39
41
|
const s = typeof given === 'string' ? { content: given } : given;
|
|
40
|
-
const
|
|
42
|
+
const weight = s.weight ?? (s.bold ? layer.boldWeight ?? 700 : layer.weight);
|
|
43
|
+
const face = await faceFor(ctx, { font: s.font ?? layer.font, weight, style: s.style ?? layer.style, axes: { ...layer.axes, ...s.axes } });
|
|
41
44
|
const kase = s.transform ?? layer.transform;
|
|
42
45
|
const raw = String(s.content ?? '').replace(/[\t\r ]+/g, ' ');
|
|
43
46
|
out.push({
|
|
@@ -52,6 +55,26 @@ export async function loadSpans(layer, ctx, size) {
|
|
|
52
55
|
return out;
|
|
53
56
|
}
|
|
54
57
|
|
|
58
|
+
/**
|
|
59
|
+
* A `content` array as spans, one a line, when any item is a span object
|
|
60
|
+
* (`{ content, bold, weight, … }`); otherwise `undefined` (plain lines stay
|
|
61
|
+
* content).
|
|
62
|
+
*
|
|
63
|
+
* @param {unknown} content - A text layer's `content`.
|
|
64
|
+
* @returns {import('./spans.js').TextSpan[] | undefined}
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* lineSpans([{ content: 'Signal', bold: true }, 'over the']);
|
|
68
|
+
* // [{ content: 'Signal\n', bold: true }, { content: 'over the' }]
|
|
69
|
+
*/
|
|
70
|
+
export function lineSpans(content) {
|
|
71
|
+
if (!Array.isArray(content) || !content.some((x) => x && typeof x === 'object')) return undefined;
|
|
72
|
+
return content.map((item, i) => {
|
|
73
|
+
const s = item && typeof item === 'object' ? item : { content: item };
|
|
74
|
+
return { ...s, content: `${s.content ?? ''}${i < content.length - 1 ? '\n' : ''}` };
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
55
78
|
/**
|
|
56
79
|
* Spans as lines: split at `\n`, then (with a finite `maxWidth`) wrapped at
|
|
57
80
|
* spaces, greedily, at `size`.
|
package/src/type/symbols.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Built-in symbols, drawn as shapes (the last glyph fallback).
|
|
2
2
|
import type { LoadedFont } from '../fonts.js';
|
|
3
3
|
|
|
4
|
-
/** The symbol face: ✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■
|
|
4
|
+
/** The symbol face: ✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪ ≠. */
|
|
5
5
|
export function symbolFace(): LoadedFont;
|
|
6
6
|
/** The characters it draws. */
|
|
7
7
|
export const SYMBOLS: string;
|
package/src/type/symbols.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* A built-in face of a few symbols type often wants and fonts often lack
|
|
3
|
-
* (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪), drawn as shapes. The last fallback when a
|
|
3
|
+
* (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪ ≠), drawn as shapes. The last fallback when a
|
|
4
4
|
* text layer's font has no glyph for a character.
|
|
5
5
|
*
|
|
6
6
|
* @module type/symbols
|
|
@@ -38,6 +38,23 @@ function heart(k) {
|
|
|
38
38
|
/** @param {number} r @param {number} [n=48] @returns {[number, number][]} */
|
|
39
39
|
const circle = (r, n = 48) => Array.from({ length: n }, (_, i) => /** @type {[number, number]} */ ([CX + r * Math.sin((i / n) * Math.PI * 2), CY + r * Math.cos((i / n) * Math.PI * 2)]));
|
|
40
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Not-equal, on a narrower advance (`NARROW`): two bars and a slash, one
|
|
43
|
+
* contour each, all wound alike so they fill as one.
|
|
44
|
+
*
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function notEqual() {
|
|
48
|
+
const cx = NARROW / 2;
|
|
49
|
+
const [half, bar, gap, lean, stem] = [230, 62, 112, 150, 31];
|
|
50
|
+
const rect = (/** @type {number} */ y) => poly([[cx - half, y + bar / 2], [cx + half, y + bar / 2], [cx + half, y - bar / 2], [cx - half, y - bar / 2]]);
|
|
51
|
+
const slash = poly([[cx + lean - stem, CY + 300], [cx + lean + stem, CY + 300], [cx - lean + stem, CY - 300], [cx - lean - stem, CY - 300]]);
|
|
52
|
+
return rect(CY + gap) + rect(CY - gap) + slash;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Advance of the narrow symbols (≠), font units. */
|
|
56
|
+
const NARROW = 600;
|
|
57
|
+
|
|
41
58
|
/** Outlines by character. An inner contour runs the other way (a hole). */
|
|
42
59
|
const SHAPES = /** @type {Record<string, string>} */ ({
|
|
43
60
|
'✺': poly(star(16, 0.5)),
|
|
@@ -53,6 +70,7 @@ const SHAPES = /** @type {Record<string, string>} */ ({
|
|
|
53
70
|
'◆': poly([[CX, CY + R], [CX + R * 0.8, CY], [CX, CY - R], [CX - R * 0.8, CY]]),
|
|
54
71
|
'■': poly([[CX - R * 0.75, CY + R * 0.75], [CX + R * 0.75, CY + R * 0.75], [CX + R * 0.75, CY - R * 0.75], [CX - R * 0.75, CY - R * 0.75]]),
|
|
55
72
|
'♪': poly([[CX - 40, CY + R], [CX + 220, CY + R * 0.7], [CX + 220, CY + R * 0.45], [CX + 20, CY + R * 0.72], [CX + 20, CY - R * 0.55], [CX - 40, CY - R * 0.55]]) + poly(circle(110).map(([x, y]) => /** @type {[number, number]} */ ([x - 120, y - R * 0.6]))),
|
|
73
|
+
'≠': notEqual(),
|
|
56
74
|
});
|
|
57
75
|
|
|
58
76
|
const CHARS = Object.keys(SHAPES);
|
|
@@ -80,7 +98,7 @@ export function symbolFace() {
|
|
|
80
98
|
variations: {},
|
|
81
99
|
source: { name: 'symbols', src: 'covers:symbols', weight: 400 },
|
|
82
100
|
glyphToPath: (/** @type {number} */ gid) => SHAPES[CHARS[gid - 1]] ?? '',
|
|
83
|
-
shapeRun: (/** @type {string} */ text) => Array.from(text).map((ch, i) => ({ gid: CHARS.indexOf(ch) + 1, cluster: i, xAdvance: ADV, xOffset: 0, yOffset: 0 })),
|
|
101
|
+
shapeRun: (/** @type {string} */ text) => Array.from(text).map((ch, i) => ({ gid: CHARS.indexOf(ch) + 1, cluster: i, xAdvance: ch === '≠' ? NARROW : ADV, xOffset: 0, yOffset: 0 })),
|
|
84
102
|
};
|
|
85
103
|
return face;
|
|
86
104
|
}
|
package/src/type/typeset.js
CHANGED
|
@@ -24,7 +24,8 @@
|
|
|
24
24
|
* - **Live text**: `render: 'live'` writes `<text>` with the font embedded
|
|
25
25
|
* by `@font-face` (or linked by URL), laid out the same way, for in-browser
|
|
26
26
|
* speed and selectable text.
|
|
27
|
-
* - **Mixed runs**: `spans`, each with its own font, weight, size, colour
|
|
27
|
+
* - **Mixed runs**: `spans`, each with its own font, weight, size, colour;
|
|
28
|
+
* or `content` lines as span objects (`{ content, bold }`), one a line.
|
|
28
29
|
* - **Per-glyph control**: `glyphs: { scale, dx, dy, rotate, color }` arrays.
|
|
29
30
|
* - **Fallback**: characters the font lacks come from `fallback`, fonts
|
|
30
31
|
* registered with `fallback: true`, or the built-in symbols.
|
|
@@ -36,14 +37,15 @@
|
|
|
36
37
|
* @module type/typeset
|
|
37
38
|
*/
|
|
38
39
|
|
|
39
|
-
import { attrs, esc, num } from '
|
|
40
|
-
import { len
|
|
41
|
-
import {
|
|
40
|
+
import { attrs, esc, num } from '@logomarks/image-effects/svg';
|
|
41
|
+
import { len } from '@logomarks/image-effects/length';
|
|
42
|
+
import { place, anchorAlign, unit } from '../geometry.js';
|
|
43
|
+
import { toHex } from '@logomarks/image-effects/color';
|
|
42
44
|
import { layout, caseText } from '../text.js';
|
|
43
|
-
import { paint } from '../
|
|
45
|
+
import { paint } from '../paint/index.js';
|
|
44
46
|
import { pos, wrap } from '../layers/util.js';
|
|
45
|
-
import { presetStrokes } from '../
|
|
46
|
-
import { extrude } from '
|
|
47
|
+
import { presetStrokes } from '../paint/presets.js';
|
|
48
|
+
import { extrude } from '@logomarks/image-effects/effects/extrude';
|
|
47
49
|
import { dashArray } from '../layers/shape.js';
|
|
48
50
|
import { faceFor, mappedPath, setLine } from './line.js';
|
|
49
51
|
import { textPathD, pathSampler } from './path.js';
|
|
@@ -51,8 +53,9 @@ import { fontFace } from './live.js';
|
|
|
51
53
|
import { autoColor } from './auto-color.js';
|
|
52
54
|
import { fallbackFaces, shapeText } from './fallback.js';
|
|
53
55
|
import { controlGlyphs, glyphCount } from './glyphs.js';
|
|
54
|
-
import { loadSpans, spanLines, shapeSpans, spanText } from './spans.js';
|
|
56
|
+
import { loadSpans, spanLines, shapeSpans, spanText, lineSpans } from './spans.js';
|
|
55
57
|
import { recordLayout } from './layout-ref.js';
|
|
58
|
+
import { marquee } from '../animate/marquee.js';
|
|
56
59
|
|
|
57
60
|
/** Options that switch the text layer onto this typesetter. */
|
|
58
61
|
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'];
|
|
@@ -81,6 +84,9 @@ export function wantsType(layer) {
|
|
|
81
84
|
* @returns {Promise<import('../layers.js').Markup>}
|
|
82
85
|
*/
|
|
83
86
|
export async function typeset(layer, ctx) {
|
|
87
|
+
// Lines given as span objects (a headline whose lines change weight) are spans.
|
|
88
|
+
const perLine = lineSpans(layer.content);
|
|
89
|
+
if (perLine) layer = { ...layer, content: undefined, spans: perLine };
|
|
84
90
|
const rawContent = Array.isArray(layer.content) ? layer.content.join('\n') : layer.content && typeof layer.content === 'object' ? layer.content.content : layer.content;
|
|
85
91
|
const kase = layer.transform === 'uppercase' || layer.transform === 'lowercase' ? layer.transform : undefined;
|
|
86
92
|
let content = layer.spans ? '' : caseText(String(rawContent ?? ''), kase);
|
|
@@ -170,7 +176,7 @@ export async function typeset(layer, ctx) {
|
|
|
170
176
|
}
|
|
171
177
|
const so = layer.startOffset ?? (layer.fitPath ? 0 : 0.5);
|
|
172
178
|
const offset = typeof so === 'number' && so <= 1 && so >= 0 ? so * sampler.length : pos(so, 'x', ctx.canvas);
|
|
173
|
-
|
|
179
|
+
let start = layer.fitPath ? offset : align === 'center' || layer.align == null ? offset - run.width / 2 : align === 'right' ? offset - run.width : offset;
|
|
174
180
|
const dy = len(layer.dy, ctx.canvas, 0);
|
|
175
181
|
const adv = (/** @type {number} */ i) => (i + 1 < run.glyphs.length ? run.glyphs[i + 1].x : run.width) - run.glyphs[i].x;
|
|
176
182
|
const mids = run.glyphs.map((g, i) => g.x + adv(i) / 2);
|
|
@@ -185,8 +191,17 @@ export async function typeset(layer, ctx) {
|
|
|
185
191
|
const whole = { x: 0, y: 0, width: ctx.width, height: ctx.height };
|
|
186
192
|
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
193
|
defs += fill.defs;
|
|
194
|
+
// A marquee (motion): the still moves the start; playing, each glyph is drawn once in its own frame and placed by copies.
|
|
195
|
+
const running = /** @type {any} */ (ctx).marquee && marquee(/** @type {any} */ (ctx).marquee, {
|
|
196
|
+
run, mids, start, sampler, id,
|
|
197
|
+
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),
|
|
198
|
+
});
|
|
199
|
+
if (running) {
|
|
200
|
+
start += running.shift;
|
|
201
|
+
defs += running.defs;
|
|
202
|
+
}
|
|
188
203
|
report(ctx, layer, content, size, face, null, { capHeight: (face.capHeight / face.upem) * size });
|
|
189
|
-
const drawn = drawBuckets(run, along, fill.value, strokes, ctx);
|
|
204
|
+
const drawn = running?.body ?? drawBuckets(run, along, fill.value, strokes, ctx);
|
|
190
205
|
recordLayout(ctx, layer.id, whole, { x: 0, y: 0, baseline: 0, size, lines: 1 });
|
|
191
206
|
return { defs, body: wrap(layer, finish(drawn, layer, ctx)) };
|
|
192
207
|
}
|
package/src/version.js
CHANGED
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
|
-
}
|
package/src/effects/ascii.d.ts
DELETED
|
@@ -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;
|
package/src/effects/ascii.js
DELETED
|
@@ -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
|
-
}
|