@arraypress/covers 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +126 -0
- package/LICENSE +29 -0
- package/README.md +846 -2
- package/fonts/OFL.txt +93 -0
- package/fonts/inter-latin-400.woff +0 -0
- package/fonts/inter-latin-500.woff +0 -0
- package/fonts/inter-latin-600.woff +0 -0
- package/fonts/inter-latin-700.woff +0 -0
- package/package.json +124 -4
- package/recipes/aurora.json +132 -0
- package/recipes/bauhaus.json +113 -0
- package/recipes/bigType.json +103 -0
- package/recipes/brand.json +1143 -0
- package/recipes/contour.json +109 -0
- package/recipes/cutout.json +191 -0
- package/recipes/diagram.json +92 -0
- package/recipes/dither.json +170 -0
- package/recipes/duotone.json +75 -0
- package/recipes/gridTitle.json +136 -0
- package/recipes/iconTile.json +214 -0
- package/recipes/issue.json +221 -0
- package/recipes/mark.json +170 -0
- package/recipes/photo.json +156 -0
- package/recipes/riso.json +387 -0
- package/recipes/stripes.json +206 -0
- package/recipes/waveform.json +154 -0
- package/schema/recipe.schema.json +8822 -0
- package/src/animate/index.d.ts +19 -0
- package/src/animate/index.js +116 -0
- package/src/astro.d.ts +31 -0
- package/src/astro.js +86 -0
- package/src/builtins.js +4 -0
- package/src/color.js +246 -0
- package/src/diagram.js +248 -0
- package/src/effects/ascii.d.ts +7 -0
- package/src/effects/ascii.js +93 -0
- package/src/effects/buffer.d.ts +60 -0
- package/src/effects/buffer.js +472 -0
- package/src/effects/detail.d.ts +12 -0
- package/src/effects/detail.js +124 -0
- package/src/effects/digital.d.ts +14 -0
- package/src/effects/digital.js +216 -0
- package/src/effects/distort.d.ts +20 -0
- package/src/effects/distort.js +314 -0
- package/src/effects/dither.d.ts +8 -0
- package/src/effects/dither.js +147 -0
- package/src/effects/drop-shadow.d.ts +4 -0
- package/src/effects/drop-shadow.js +45 -0
- package/src/effects/extrude.d.ts +5 -0
- package/src/effects/extrude.js +55 -0
- package/src/effects/film.d.ts +11 -0
- package/src/effects/film.js +204 -0
- package/src/effects/glow.d.ts +8 -0
- package/src/effects/glow.js +56 -0
- package/src/effects/grade.d.ts +30 -0
- package/src/effects/grade.js +274 -0
- package/src/effects/halftone.d.ts +8 -0
- package/src/effects/halftone.js +95 -0
- package/src/effects/index.d.ts +125 -0
- package/src/effects/index.js +277 -0
- package/src/effects/ink.d.ts +12 -0
- package/src/effects/ink.js +70 -0
- package/src/effects/lens.d.ts +14 -0
- package/src/effects/lens.js +277 -0
- package/src/effects/light.d.ts +10 -0
- package/src/effects/light.js +59 -0
- package/src/effects/looks.d.ts +9 -0
- package/src/effects/looks.js +74 -0
- package/src/effects/mask.d.ts +52 -0
- package/src/effects/mask.js +314 -0
- package/src/effects/motion.d.ts +6 -0
- package/src/effects/motion.js +60 -0
- package/src/effects/newsprint.d.ts +6 -0
- package/src/effects/newsprint.js +89 -0
- package/src/effects/optics.d.ts +10 -0
- package/src/effects/optics.js +213 -0
- package/src/effects/options.d.ts +6 -0
- package/src/effects/options.js +106 -0
- package/src/effects/paint-presets.d.ts +10 -0
- package/src/effects/paint-presets.js +47 -0
- package/src/effects/paint.d.ts +49 -0
- package/src/effects/paint.js +66 -0
- package/src/effects/pixels.d.ts +24 -0
- package/src/effects/pixels.js +138 -0
- package/src/effects/polar.d.ts +10 -0
- package/src/effects/polar.js +265 -0
- package/src/effects/region.d.ts +8 -0
- package/src/effects/region.js +136 -0
- package/src/effects/round-border.d.ts +5 -0
- package/src/effects/round-border.js +62 -0
- package/src/effects/screen.d.ts +14 -0
- package/src/effects/screen.js +212 -0
- package/src/effects/sort.d.ts +8 -0
- package/src/effects/sort.js +125 -0
- package/src/effects/symmetry.d.ts +6 -0
- package/src/effects/symmetry.js +74 -0
- package/src/effects/tile.d.ts +7 -0
- package/src/effects/tile.js +55 -0
- package/src/effects/tone.d.ts +16 -0
- package/src/effects/tone.js +117 -0
- package/src/effects/transform.d.ts +35 -0
- package/src/effects/transform.js +127 -0
- package/src/effects/util.d.ts +16 -0
- package/src/effects/util.js +85 -0
- package/src/extract.js +91 -0
- package/src/flatten.js +234 -0
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +393 -0
- package/src/formats/cut.d.ts +10 -0
- package/src/formats/cut.js +77 -0
- package/src/formats/index.d.ts +6 -0
- package/src/formats/index.js +9 -0
- package/src/formats/presets.d.ts +41 -0
- package/src/formats/presets.js +89 -0
- package/src/formats/print.d.ts +25 -0
- package/src/formats/print.js +117 -0
- package/src/formats/zones.d.ts +10 -0
- package/src/formats/zones.js +64 -0
- package/src/geometry.js +187 -0
- package/src/grid.js +41 -0
- package/src/handmade.js +253 -0
- package/src/icons.js +191 -0
- package/src/index.d.ts +1083 -0
- package/src/index.js +27 -0
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +616 -0
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +56 -0
- package/src/layers/barcode.d.ts +19 -0
- package/src/layers/barcode.js +63 -0
- package/src/layers/code128.d.ts +4 -0
- package/src/layers/code128.js +28 -0
- package/src/layers/expr.d.ts +6 -0
- package/src/layers/expr.js +121 -0
- package/src/layers/geometry.d.ts +6 -0
- package/src/layers/geometry.js +238 -0
- package/src/layers/index.d.ts +18 -0
- package/src/layers/index.js +15 -0
- package/src/layers/perforation.d.ts +14 -0
- package/src/layers/perforation.js +47 -0
- package/src/layers/repeat.d.ts +29 -0
- package/src/layers/repeat.js +90 -0
- package/src/layers/shape.d.ts +46 -0
- package/src/layers/shape.js +115 -0
- package/src/layers/soft.d.ts +5 -0
- package/src/layers/soft.js +43 -0
- package/src/layers/svg.d.ts +23 -0
- package/src/layers/svg.js +109 -0
- package/src/layers/table.d.ts +25 -0
- package/src/layers/table.js +80 -0
- package/src/layers/tear.d.ts +21 -0
- package/src/layers/tear.js +74 -0
- package/src/layers/texture.d.ts +17 -0
- package/src/layers/texture.js +106 -0
- package/src/layers/util.d.ts +8 -0
- package/src/layers/util.js +48 -0
- package/src/layers.js +1056 -0
- package/src/lint/image-size.d.ts +4 -0
- package/src/lint/image-size.js +34 -0
- package/src/lint/index.d.ts +16 -0
- package/src/lint/index.js +127 -0
- package/src/lint/rules.d.ts +5 -0
- package/src/lint/rules.js +19 -0
- package/src/node.d.ts +2 -0
- package/src/node.js +20 -0
- package/src/palette.js +185 -0
- package/src/platform/bytes.d.ts +22 -0
- package/src/platform/bytes.js +103 -0
- package/src/platform/compress.d.ts +12 -0
- package/src/platform/compress.js +81 -0
- package/src/platform/harfbuzz.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +13 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +72 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/node.d.ts +26 -0
- package/src/platform/node.js +86 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/png.js +126 -0
- package/src/print.js +659 -0
- package/src/raster/index.d.ts +24 -0
- package/src/raster/index.js +171 -0
- package/src/recipes.js +79 -0
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -0
- package/src/render/content.d.ts +5 -0
- package/src/render/content.js +58 -0
- package/src/render/context.d.ts +82 -0
- package/src/render/grain.d.ts +7 -0
- package/src/render/grain.js +113 -0
- package/src/render/group.d.ts +23 -0
- package/src/render/group.js +67 -0
- package/src/render/key.d.ts +5 -0
- package/src/render/key.js +91 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/tiles.d.ts +6 -0
- package/src/render/tiles.js +77 -0
- package/src/render/use.d.ts +27 -0
- package/src/render/use.js +73 -0
- package/src/render/wrap.d.ts +14 -0
- package/src/render/wrap.js +199 -0
- package/src/render.js +553 -0
- package/src/rng.js +95 -0
- package/src/svg.js +69 -0
- package/src/text.d.ts +29 -0
- package/src/text.js +237 -0
- package/src/type/auto-color.d.ts +9 -0
- package/src/type/auto-color.js +44 -0
- package/src/type/fallback.d.ts +11 -0
- package/src/type/fallback.js +92 -0
- package/src/type/glyphs.d.ts +24 -0
- package/src/type/glyphs.js +87 -0
- package/src/type/index.d.ts +5 -0
- package/src/type/index.js +10 -0
- package/src/type/kern.d.ts +4 -0
- package/src/type/kern.js +144 -0
- package/src/type/layout-ref.d.ts +10 -0
- package/src/type/layout-ref.js +74 -0
- package/src/type/line.d.ts +11 -0
- package/src/type/line.js +105 -0
- package/src/type/live.d.ts +6 -0
- package/src/type/live.js +39 -0
- package/src/type/name.d.ts +4 -0
- package/src/type/name.js +32 -0
- package/src/type/path.d.ts +18 -0
- package/src/type/path.js +89 -0
- package/src/type/pixel.d.ts +9 -0
- package/src/type/pixel.js +115 -0
- package/src/type/sfnt.d.ts +20 -0
- package/src/type/sfnt.js +258 -0
- package/src/type/spans.d.ts +33 -0
- package/src/type/spans.js +138 -0
- package/src/type/symbols.d.ts +7 -0
- package/src/type/symbols.js +89 -0
- package/src/type/typeset.d.ts +10 -0
- package/src/type/typeset.js +352 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
package/src/svg.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Small helpers for writing SVG markup deterministically.
|
|
3
|
+
*
|
|
4
|
+
* Numbers are always written through {@link num}, so the same input gives
|
|
5
|
+
* byte-identical output on every machine (no `0.30000000000000004`, no
|
|
6
|
+
* locale formatting, no `-0`).
|
|
7
|
+
*
|
|
8
|
+
* @module svg
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Format a number for SVG: at most `digits` decimals, no trailing zeros.
|
|
13
|
+
*
|
|
14
|
+
* @param {number} v - The number.
|
|
15
|
+
* @param {number} [digits=2] - Maximum decimals.
|
|
16
|
+
* @returns {string} The formatted number.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* num(1 / 3); // '0.33'
|
|
20
|
+
* num(12.5); // '12.5'
|
|
21
|
+
* num(-0.001); // '0'
|
|
22
|
+
*/
|
|
23
|
+
export function num(v, digits = 2) {
|
|
24
|
+
if (!Number.isFinite(v)) return '0';
|
|
25
|
+
const s = v.toFixed(digits).replace(/\.?0+$/, '');
|
|
26
|
+
return s === '-0' || s === '' ? '0' : s;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Escape text for an XML attribute or text node.
|
|
31
|
+
*
|
|
32
|
+
* @param {string} s - Raw text.
|
|
33
|
+
* @returns {string} Escaped text.
|
|
34
|
+
*/
|
|
35
|
+
export function esc(s) {
|
|
36
|
+
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' })[c]);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Serialise an attribute object. `undefined`, `null` and `false` values are
|
|
41
|
+
* skipped; numbers go through {@link num}. Keys keep their insertion order.
|
|
42
|
+
*
|
|
43
|
+
* @param {Record<string, string | number | boolean | null | undefined>} obj - Attributes.
|
|
44
|
+
* @returns {string} ` key="value"` pairs (with a leading space), or `''`.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* attrs({ x: 1.005, fill: '#fff', opacity: undefined }); // ' x="1" fill="#fff"'
|
|
48
|
+
*/
|
|
49
|
+
export function attrs(obj) {
|
|
50
|
+
let out = '';
|
|
51
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
52
|
+
if (v === undefined || v === null || v === false) continue;
|
|
53
|
+
out += ` ${k}="${typeof v === 'number' ? num(v) : esc(String(v))}"`;
|
|
54
|
+
}
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Opacity and blend-mode attributes shared by every layer.
|
|
60
|
+
*
|
|
61
|
+
* @param {{ opacity?: number, blend?: string }} layer - The layer.
|
|
62
|
+
* @returns {Record<string, string | number | undefined>} Attributes for the layer's group.
|
|
63
|
+
*/
|
|
64
|
+
export function layerAttrs(layer) {
|
|
65
|
+
return {
|
|
66
|
+
opacity: layer.opacity != null && layer.opacity < 1 ? Math.max(0, layer.opacity) : undefined,
|
|
67
|
+
style: layer.blend && layer.blend !== 'normal' ? `mix-blend-mode:${layer.blend}` : undefined,
|
|
68
|
+
};
|
|
69
|
+
}
|
package/src/text.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Text: shaping, line breaking and outline drawing.
|
|
2
|
+
import type { LoadedFont } from './fonts.js';
|
|
3
|
+
|
|
4
|
+
/** A shaped run: glyph ids and pen positions, px. */
|
|
5
|
+
export interface Shaped {
|
|
6
|
+
/** Pen positions, px. A glyph from another face (fallback, a span) carries it, its own size, a paint key and a transform. */
|
|
7
|
+
glyphs: { gid: number; x: number; y: number; cluster?: number; face?: LoadedFont; size?: number; paint?: string; t?: [number, number, number, number, number, number] }[];
|
|
8
|
+
/** Advance width, px. */
|
|
9
|
+
width: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Laid-out text. */
|
|
13
|
+
export interface Layout {
|
|
14
|
+
lines: { text: string; run: Shaped }[];
|
|
15
|
+
size: number;
|
|
16
|
+
truncated: boolean;
|
|
17
|
+
width: number;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Shape one line at a size with letter-spacing (px between clusters). */
|
|
21
|
+
export function shape(face: LoadedFont, text: string, size: number, tracking: number, hb?: unknown): Shaped;
|
|
22
|
+
/** A glyph's outline as parsed commands, font units (cached on the face). */
|
|
23
|
+
export function outline(face: LoadedFont, gid: number): (string | number)[];
|
|
24
|
+
/** Shaped glyphs as SVG path data, px. */
|
|
25
|
+
export function runPath(face: LoadedFont, run: Shaped, size: number, x: number, baseline: number): string;
|
|
26
|
+
/** Break text into lines: greedy, optionally balanced, shrinking toward `minSize`, truncated past `maxLines`. */
|
|
27
|
+
export function layout(o: { face: LoadedFont; hb?: unknown; text: string; size: number; minSize?: number; letterSpacing?: number; maxWidth?: number; maxLines?: number; ellipsis?: string; balance?: boolean }): Layout;
|
|
28
|
+
/** Collapse spaces, trim lines and apply a case. */
|
|
29
|
+
export function caseText(s: string, t: string | undefined): string;
|
package/src/text.js
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text: shaping, line breaking and outline drawing.
|
|
3
|
+
*
|
|
4
|
+
* A line is shaped by HarfBuzz as a whole (so kerning across spaces and
|
|
5
|
+
* ligatures are right), then every glyph's outline is transformed into
|
|
6
|
+
* canvas pixels and written as one `<path>` per line. The output never
|
|
7
|
+
* references a font, so it renders the same in librsvg, a browser or a
|
|
8
|
+
* design tool.
|
|
9
|
+
*
|
|
10
|
+
* Line breaking is greedy on spaces, then optionally balanced (like CSS
|
|
11
|
+
* `text-wrap: balance`: the narrowest width that keeps the same number of
|
|
12
|
+
* lines, so a headline doesn't end on a lonely word). A word wider than
|
|
13
|
+
* the line is broken between characters. Past `maxLines` the text is cut
|
|
14
|
+
* at a word and given an ellipsis; with `minSize`, the size first steps
|
|
15
|
+
* down until the text fits.
|
|
16
|
+
*
|
|
17
|
+
* Vertical metrics are trimmed to the cap height above the first line and
|
|
18
|
+
* the baseline below the last (CSS `text-box: trim-both cap alphabetic`),
|
|
19
|
+
* so a margin measured to a headline is the margin you see.
|
|
20
|
+
*
|
|
21
|
+
* @module text
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { num } from './svg.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* A shaped run of text.
|
|
28
|
+
*
|
|
29
|
+
* @typedef {object} Shaped
|
|
30
|
+
* @property {{ gid: number, x: number, y: number, cluster?: number }[]} glyphs - Glyph ids and pen positions in px (y is the baseline offset), and the character each starts at.
|
|
31
|
+
* @property {number} width - Advance width in px (no trailing letter-spacing).
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Shape a run of text.
|
|
36
|
+
*
|
|
37
|
+
* @param {import('./fonts.js').LoadedFont} face - The face.
|
|
38
|
+
* @param {string} text - One line of text.
|
|
39
|
+
* @param {number} size - Font size in px.
|
|
40
|
+
* @param {number} tracking - Letter-spacing in px, added between clusters (0 for none).
|
|
41
|
+
* @param {any} [hb] - Unused (kept for the 1.0 signature).
|
|
42
|
+
* @returns {Shaped} Positioned glyphs.
|
|
43
|
+
*/
|
|
44
|
+
export function shape(face, text, size, tracking, hb) {
|
|
45
|
+
const glyphsIn = face.shapeRun(text);
|
|
46
|
+
const scale = size / face.upem;
|
|
47
|
+
const glyphs = [];
|
|
48
|
+
let pen = 0;
|
|
49
|
+
for (let i = 0; i < glyphsIn.length; i++) {
|
|
50
|
+
const p = glyphsIn[i];
|
|
51
|
+
glyphs.push({ gid: p.gid, x: pen + p.xOffset * scale, y: -p.yOffset * scale, cluster: p.cluster });
|
|
52
|
+
pen += p.xAdvance * scale;
|
|
53
|
+
const nextCluster = glyphsIn[i + 1]?.cluster;
|
|
54
|
+
if (tracking && nextCluster !== undefined && nextCluster !== p.cluster) pen += tracking;
|
|
55
|
+
}
|
|
56
|
+
return { glyphs, width: pen };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A glyph's outline as parsed commands, in font units (y up). Cached on
|
|
61
|
+
* the face.
|
|
62
|
+
*
|
|
63
|
+
* @param {import('./fonts.js').LoadedFont} face
|
|
64
|
+
* @param {number} gid
|
|
65
|
+
* @returns {(string | number)[]} Flat list: command letter followed by its numbers.
|
|
66
|
+
*/
|
|
67
|
+
export function outline(face, gid) {
|
|
68
|
+
let cmds = face.paths.get(gid);
|
|
69
|
+
if (!cmds) {
|
|
70
|
+
cmds = [];
|
|
71
|
+
const d = face.glyphToPath(gid);
|
|
72
|
+
for (const m of d.matchAll(/([MLQCZ])([^MLQCZ]*)/gi)) {
|
|
73
|
+
cmds.push(m[1].toUpperCase());
|
|
74
|
+
for (const n of m[2].split(/[\s,]+/).filter(Boolean)) cmds.push(parseFloat(n));
|
|
75
|
+
}
|
|
76
|
+
face.paths.set(gid, cmds);
|
|
77
|
+
}
|
|
78
|
+
return cmds;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Write shaped glyphs as SVG path data in canvas pixels.
|
|
83
|
+
*
|
|
84
|
+
* @param {import('./fonts.js').LoadedFont} face
|
|
85
|
+
* @param {Shaped} run - Shaped glyphs.
|
|
86
|
+
* @param {number} size - Font size in px.
|
|
87
|
+
* @param {number} x - Left edge of the run.
|
|
88
|
+
* @param {number} baseline - Baseline y.
|
|
89
|
+
* @returns {string} Path data.
|
|
90
|
+
*/
|
|
91
|
+
export function runPath(face, run, size, x, baseline) {
|
|
92
|
+
const s = size / face.upem;
|
|
93
|
+
const parts = [];
|
|
94
|
+
for (const g of run.glyphs) {
|
|
95
|
+
const ox = x + g.x;
|
|
96
|
+
const oy = baseline + g.y;
|
|
97
|
+
let coords = [];
|
|
98
|
+
for (const c of outline(face, g.gid)) {
|
|
99
|
+
if (typeof c === 'string') {
|
|
100
|
+
if (coords.length) parts.push(coords.join(' '));
|
|
101
|
+
parts.push(c);
|
|
102
|
+
coords = [];
|
|
103
|
+
} else {
|
|
104
|
+
// Coordinates alternate x, y; y flips (font units point up).
|
|
105
|
+
coords.push(num(coords.length % 2 === 0 ? ox + c * s : oy - c * s));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (coords.length) parts.push(coords.join(' '));
|
|
109
|
+
}
|
|
110
|
+
return parts.join('');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Laid-out text.
|
|
115
|
+
*
|
|
116
|
+
* @typedef {object} Layout
|
|
117
|
+
* @property {{ text: string, run: Shaped }[]} lines - Each line and its shaping.
|
|
118
|
+
* @property {number} size - The size used (may be below the asked size with `minSize`).
|
|
119
|
+
* @property {boolean} truncated - Whether an ellipsis was added.
|
|
120
|
+
* @property {number} width - The widest line.
|
|
121
|
+
*/
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Break text into lines.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} o
|
|
127
|
+
* @param {import('./fonts.js').LoadedFont} o.face - The face.
|
|
128
|
+
* @param {typeof import('harfbuzzjs')} o.hb - harfbuzzjs.
|
|
129
|
+
* @param {string} o.text - The text (`\n` starts a new line).
|
|
130
|
+
* @param {number} o.size - Font size in px.
|
|
131
|
+
* @param {number} [o.minSize] - Smallest size to shrink to before truncating.
|
|
132
|
+
* @param {number} [o.letterSpacing=0] - Tracking in em.
|
|
133
|
+
* @param {number} [o.maxWidth=Infinity] - Line width in px.
|
|
134
|
+
* @param {number} [o.maxLines=Infinity] - Most lines before truncating.
|
|
135
|
+
* @param {string} [o.ellipsis='…'] - Appended to a cut line.
|
|
136
|
+
* @param {boolean} [o.balance=false] - Balance line lengths.
|
|
137
|
+
* @returns {Layout} The layout.
|
|
138
|
+
*/
|
|
139
|
+
export function layout(o) {
|
|
140
|
+
const maxLines = Math.max(1, o.maxLines ?? Infinity);
|
|
141
|
+
const maxWidth = o.maxWidth ?? Infinity;
|
|
142
|
+
let size = o.size;
|
|
143
|
+
const floor = Math.min(o.minSize ?? size, size);
|
|
144
|
+
|
|
145
|
+
for (;;) {
|
|
146
|
+
const attempt = breakLines({ ...o, size, maxWidth, maxLines });
|
|
147
|
+
if ((!attempt.truncated && !attempt.forced) || size <= floor) return attempt;
|
|
148
|
+
size = Math.max(floor, size * 0.94);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* One layout pass at a fixed size.
|
|
154
|
+
*
|
|
155
|
+
* @param {Parameters<typeof layout>[0] & { maxWidth: number, maxLines: number }} o
|
|
156
|
+
* @returns {Layout & { forced: boolean }} The layout; `forced` is true when a word had to be split.
|
|
157
|
+
*/
|
|
158
|
+
function breakLines(o) {
|
|
159
|
+
const tracking = (o.letterSpacing ?? 0) * o.size;
|
|
160
|
+
const measure = (/** @type {string} */ t) => shape(o.face, t, o.size, tracking, o.hb);
|
|
161
|
+
const ellipsis = o.ellipsis ?? '…';
|
|
162
|
+
|
|
163
|
+
/** @param {number} width */
|
|
164
|
+
const wrap = (width) => {
|
|
165
|
+
/** @type {string[]} */
|
|
166
|
+
const lines = [];
|
|
167
|
+
let forced = false;
|
|
168
|
+
for (const para of o.text.split('\n')) {
|
|
169
|
+
const words = para.split(/ +/).filter(Boolean);
|
|
170
|
+
if (!words.length) { lines.push(''); continue; }
|
|
171
|
+
let line = '';
|
|
172
|
+
for (const word of words) {
|
|
173
|
+
const candidate = line ? `${line} ${word}` : word;
|
|
174
|
+
if (!line || measure(candidate).width <= width) {
|
|
175
|
+
line = candidate;
|
|
176
|
+
} else {
|
|
177
|
+
lines.push(line);
|
|
178
|
+
line = word;
|
|
179
|
+
}
|
|
180
|
+
// A single word wider than the line: split it between characters.
|
|
181
|
+
while (measure(line).width > width && Array.from(line).length > 1) {
|
|
182
|
+
forced = true;
|
|
183
|
+
const chars = Array.from(line);
|
|
184
|
+
let cut = chars.length - 1;
|
|
185
|
+
while (cut > 1 && measure(chars.slice(0, cut).join('')).width > width) cut--;
|
|
186
|
+
lines.push(chars.slice(0, cut).join(''));
|
|
187
|
+
line = chars.slice(cut).join('');
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
lines.push(line);
|
|
191
|
+
}
|
|
192
|
+
return { lines, forced };
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
let { lines, forced } = wrap(o.maxWidth);
|
|
196
|
+
|
|
197
|
+
if (o.balance && lines.length > 1 && lines.length <= o.maxLines && Number.isFinite(o.maxWidth)) {
|
|
198
|
+
const target = lines.length;
|
|
199
|
+
let lo = o.maxWidth * 0.4;
|
|
200
|
+
let hi = o.maxWidth;
|
|
201
|
+
for (let i = 0; i < 12; i++) {
|
|
202
|
+
const mid = (lo + hi) / 2;
|
|
203
|
+
const w = wrap(mid);
|
|
204
|
+
if (w.lines.length <= target && !w.forced) hi = mid; else lo = mid;
|
|
205
|
+
}
|
|
206
|
+
lines = wrap(hi).lines;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
let truncated = false;
|
|
210
|
+
if (lines.length > o.maxLines) {
|
|
211
|
+
truncated = true;
|
|
212
|
+
const kept = lines.slice(0, o.maxLines);
|
|
213
|
+
let last = kept[o.maxLines - 1];
|
|
214
|
+
const fits = (/** @type {string} */ t) => measure(t + ellipsis).width <= o.maxWidth;
|
|
215
|
+
while (!fits(last) && last.length) {
|
|
216
|
+
const cut = last.lastIndexOf(' ');
|
|
217
|
+
last = cut > 0 ? last.slice(0, cut) : Array.from(last).slice(0, -1).join('');
|
|
218
|
+
}
|
|
219
|
+
kept[o.maxLines - 1] = last.replace(/[\s.,;:!?·—–-]+$/u, '') + ellipsis;
|
|
220
|
+
lines = kept;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const shaped = lines.map((text) => ({ text, run: measure(text) }));
|
|
224
|
+
return { lines: shaped, size: o.size, truncated, forced, width: Math.max(0, ...shaped.map((l) => l.run.width)) };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Collapse spaces, trim each line, and apply a case.
|
|
229
|
+
*
|
|
230
|
+
* @param {string} s - The text.
|
|
231
|
+
* @param {string | undefined} t - `'uppercase'`, `'lowercase'`, anything else for none.
|
|
232
|
+
* @returns {string}
|
|
233
|
+
*/
|
|
234
|
+
export function caseText(s, t) {
|
|
235
|
+
const clean = s.replace(/[\t\r ]+/g, ' ').replace(/ *\n */g, '\n').trim();
|
|
236
|
+
return t === 'uppercase' ? clean.toUpperCase() : t === 'lowercase' ? clean.toLowerCase() : clean;
|
|
237
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Auto contrast for `color: 'auto'`.
|
|
2
|
+
import type { RenderContext } from '../render/context.js';
|
|
3
|
+
|
|
4
|
+
/** Backdrop luma (0–1) above which `color: 'auto'` picks the dark colour (0.72). */
|
|
5
|
+
export const AUTO_THRESHOLD: number;
|
|
6
|
+
/** Rec. 601 luma of a colour, 0–1. */
|
|
7
|
+
export function luma(c: string): number;
|
|
8
|
+
/** White or near-black (or `autoColors`) by the luma of what's drawn under `box`. */
|
|
9
|
+
export function autoColor(layer: { autoColors?: [string, string]; autoThreshold?: number }, ctx: RenderContext, box: { x: number; y: number; width: number; height: number }): Promise<string>;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto contrast: white or near-black (or a layer's `autoColors`), by how
|
|
3
|
+
* light what's drawn underneath is.
|
|
4
|
+
*
|
|
5
|
+
* The rule is the one the site's designs use: the backdrop's luma (Rec. 601
|
|
6
|
+
* weights on the gamma-encoded channels, 0–1) above `AUTO_THRESHOLD`
|
|
7
|
+
* (0.72) takes the dark colour, anything else the light one. A pure
|
|
8
|
+
* contrast-ratio choice picks black on mid-tones (a mid pink, a sky blue)
|
|
9
|
+
* where designers set white; this matches what they set.
|
|
10
|
+
*
|
|
11
|
+
* @module type/auto-color
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { toHex } from '../color.js';
|
|
15
|
+
|
|
16
|
+
/** Backdrop luma (0–1) above which `color: 'auto'` picks the dark colour. */
|
|
17
|
+
export const AUTO_THRESHOLD = 0.72;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Rec. 601 luma of a colour, 0–1.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} c
|
|
23
|
+
* @returns {number}
|
|
24
|
+
*/
|
|
25
|
+
export function luma(c) {
|
|
26
|
+
const h = toHex(c);
|
|
27
|
+
const [r, g, b] = [1, 3, 5].map((i) => parseInt(h.slice(i, i + 2), 16) / 255);
|
|
28
|
+
return 0.299 * r + 0.587 * g + 0.114 * b;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pick the text colour against what's underneath (`autoThreshold` on the
|
|
33
|
+
* layer overrides the 0.72 cut).
|
|
34
|
+
*
|
|
35
|
+
* @param {any} layer
|
|
36
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
37
|
+
* @param {{ x: number, y: number, width: number, height: number }} box
|
|
38
|
+
* @returns {Promise<string>}
|
|
39
|
+
*/
|
|
40
|
+
export async function autoColor(layer, ctx, box) {
|
|
41
|
+
const [light, dark] = (layer.autoColors ?? ['#ffffff', '#0b0b0b']).map(toHex);
|
|
42
|
+
const under = await /** @type {any} */ (ctx).backdrop?.(box);
|
|
43
|
+
return luma(under ?? ctx.palette.bg) > (layer.autoThreshold ?? AUTO_THRESHOLD) ? dark : light;
|
|
44
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Glyph fallback.
|
|
2
|
+
import type { LoadedFont } from '../fonts.js';
|
|
3
|
+
import type { Shaped } from '../text.js';
|
|
4
|
+
import type { RenderContext } from '../render/context.js';
|
|
5
|
+
|
|
6
|
+
/** The faces to fall back to: the layer's `fallback`, fonts registered with `fallback: true`, then the built-in symbols. */
|
|
7
|
+
export function fallbackFaces(ctx: RenderContext, o: { fallback?: string | string[]; weight?: number; style?: string }): Promise<LoadedFont[]>;
|
|
8
|
+
/** Whether a face has a glyph for a character. */
|
|
9
|
+
export function covers(face: LoadedFont, ch: string): boolean;
|
|
10
|
+
/** Shape a line, falling back face by face where the face lacks a glyph. */
|
|
11
|
+
export function shapeText(face: LoadedFont, text: string, size: number, tracking: number, fallbacks?: LoadedFont[]): Shaped;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Glyph fallback: characters the chosen font lacks are shaped in a
|
|
3
|
+
* fallback font (the layer's `fallback`, then any font registered with
|
|
4
|
+
* `fallback: true`), and failing those drawn from the built-in symbols
|
|
5
|
+
* (✺ ✦ ✶ ★ ☆ ♥ ♡ ▶ ◀ ● ◆ ■ ♪). Text the font covers shapes exactly as
|
|
6
|
+
* before (one HarfBuzz run).
|
|
7
|
+
*
|
|
8
|
+
* @module type/fallback
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { shape } from '../text.js';
|
|
12
|
+
import { loadFont, pickFont } from '../fonts.js';
|
|
13
|
+
import { symbolFace } from './symbols.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The faces to fall back to, in order.
|
|
17
|
+
*
|
|
18
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
19
|
+
* @param {{ fallback?: string | string[], weight?: number, style?: string }} o
|
|
20
|
+
* @returns {Promise<import('../fonts.js').LoadedFont[]>}
|
|
21
|
+
*/
|
|
22
|
+
export async function fallbackFaces(ctx, o) {
|
|
23
|
+
const names = o.fallback == null ? [] : [].concat(/** @type {any} */ (o.fallback));
|
|
24
|
+
const sources = [
|
|
25
|
+
...names.map((n) => ctx.fonts.find((f) => f.name.toLowerCase() === String(n).toLowerCase()) ? pickFont(ctx.fonts, n, o.weight ?? 600, o.style) : undefined),
|
|
26
|
+
...ctx.fonts.filter((f) => /** @type {any} */ (f).fallback),
|
|
27
|
+
].filter(Boolean);
|
|
28
|
+
const faces = [];
|
|
29
|
+
for (const s of new Set(sources)) faces.push(await loadFont(/** @type {any} */ (s)));
|
|
30
|
+
faces.push(symbolFace());
|
|
31
|
+
return faces;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Characters each face lacks, by face. @type {WeakMap<object, Map<string, boolean>>} */
|
|
35
|
+
const missing = new WeakMap();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Whether a face has a glyph for a character.
|
|
39
|
+
*
|
|
40
|
+
* @param {import('../fonts.js').LoadedFont} face
|
|
41
|
+
* @param {string} ch
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
export function covers(face, ch) {
|
|
45
|
+
let m = missing.get(face.paths);
|
|
46
|
+
if (!m) missing.set(face.paths, (m = new Map()));
|
|
47
|
+
let has = m.get(ch);
|
|
48
|
+
if (has === undefined) {
|
|
49
|
+
has = /\s/.test(ch) || face.shapeRun(ch).every((g) => g.gid !== 0);
|
|
50
|
+
m.set(ch, has);
|
|
51
|
+
}
|
|
52
|
+
return has;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Shape a line, falling back face by face for characters the face lacks.
|
|
57
|
+
* Glyphs from another face carry it (`glyph.face`).
|
|
58
|
+
*
|
|
59
|
+
* @param {import('../fonts.js').LoadedFont} face
|
|
60
|
+
* @param {string} text
|
|
61
|
+
* @param {number} size
|
|
62
|
+
* @param {number} tracking - px between clusters.
|
|
63
|
+
* @param {import('../fonts.js').LoadedFont[]} [fallbacks]
|
|
64
|
+
* @returns {import('../text.js').Shaped}
|
|
65
|
+
*/
|
|
66
|
+
export function shapeText(face, text, size, tracking, fallbacks) {
|
|
67
|
+
const run = shape(face, text, size, tracking);
|
|
68
|
+
if (!fallbacks?.length || run.glyphs.every((g) => g.gid !== 0)) return run;
|
|
69
|
+
// Segments of one face each, in order.
|
|
70
|
+
/** @type {{ face: import('../fonts.js').LoadedFont, text: string }[]} */
|
|
71
|
+
const segs = [];
|
|
72
|
+
for (const ch of Array.from(text)) {
|
|
73
|
+
const f = covers(face, ch) ? face : fallbacks.find((x) => covers(x, ch)) ?? face;
|
|
74
|
+
const last = segs[segs.length - 1];
|
|
75
|
+
if (last && last.face === f) last.text += ch;
|
|
76
|
+
else segs.push({ face: f, text: ch });
|
|
77
|
+
}
|
|
78
|
+
/** @type {import('../text.js').Shaped['glyphs']} */
|
|
79
|
+
const glyphs = [];
|
|
80
|
+
let x = 0;
|
|
81
|
+
let at = 0;
|
|
82
|
+
segs.forEach((seg, i) => {
|
|
83
|
+
const part = shape(seg.face, seg.text, size, tracking);
|
|
84
|
+
for (const g of part.glyphs) {
|
|
85
|
+
const placed = { ...g, x: g.x + x, cluster: (g.cluster ?? 0) + at };
|
|
86
|
+
glyphs.push(seg.face === face ? placed : { ...placed, face: seg.face });
|
|
87
|
+
}
|
|
88
|
+
x += part.width + (i < segs.length - 1 ? tracking : 0);
|
|
89
|
+
at += seg.text.length;
|
|
90
|
+
});
|
|
91
|
+
return { glyphs, width: x };
|
|
92
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Per-glyph control.
|
|
2
|
+
import type { Shaped } from '../text.js';
|
|
3
|
+
import type { Length, Color } from '../index.js';
|
|
4
|
+
|
|
5
|
+
/** Per-glyph scale, shift, turn and colour (arrays cycle; counted across the block, spaces skipped). */
|
|
6
|
+
export interface GlyphControl {
|
|
7
|
+
scale?: number | number[];
|
|
8
|
+
scaleX?: number | number[];
|
|
9
|
+
scaleY?: number | number[];
|
|
10
|
+
dx?: Length | Length[];
|
|
11
|
+
dy?: Length | Length[];
|
|
12
|
+
/** Degrees. */
|
|
13
|
+
rotate?: number | number[];
|
|
14
|
+
color?: Color | Color[];
|
|
15
|
+
/** Transforms about the glyph's middle (default) or its baseline. */
|
|
16
|
+
origin?: 'center' | 'baseline';
|
|
17
|
+
/** A scaled glyph takes its scaled width (default true). */
|
|
18
|
+
advance?: boolean;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Apply per-glyph control to a shaped line (`start`: the line's first glyph's index in the block). */
|
|
22
|
+
export function controlGlyphs(run: Shaped, spec: GlyphControl | undefined, o: { start: number; size: number; capHeight: number; canvas: { width: number; height: number }; text: string }): { run: Shaped; count: number };
|
|
23
|
+
/** How many glyphs a line counts (non-space characters). */
|
|
24
|
+
export function glyphCount(text: string): number;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-glyph control: each letter its own scale, shift, turn and colour,
|
|
3
|
+
* from arrays (cycled), so bulging 70s type or a wave of letters is one
|
|
4
|
+
* text layer.
|
|
5
|
+
*
|
|
6
|
+
* `glyphs: { scale, scaleX, scaleY, dx, dy, rotate, color, origin, advance }`:
|
|
7
|
+
* numbers (or colours) per glyph, counted across the whole block and
|
|
8
|
+
* skipping spaces; `origin` is `'center'` (the default: the glyph's middle
|
|
9
|
+
* at half the cap height) or `'baseline'`; `advance` (default true) lets a
|
|
10
|
+
* scaled glyph take its scaled width, pushing its neighbours along.
|
|
11
|
+
*
|
|
12
|
+
* @module type/glyphs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { len } from '../geometry.js';
|
|
16
|
+
import { toHex } from '../color.js';
|
|
17
|
+
|
|
18
|
+
/** @param {any} list @param {number} i @param {any} fallback @returns {any} The i-th of a cycled list (or a single value). */
|
|
19
|
+
const at = (list, i, fallback) => (list == null ? fallback : Array.isArray(list) ? (list.length ? list[i % list.length] : fallback) : list);
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Apply per-glyph control to a shaped line: advances widened for scaled
|
|
23
|
+
* glyphs, and each glyph's transform (`t`, about its pen position) and
|
|
24
|
+
* paint key (`paint`, a colour).
|
|
25
|
+
*
|
|
26
|
+
* @param {import('../text.js').Shaped} run
|
|
27
|
+
* @param {any} spec - The layer's `glyphs`.
|
|
28
|
+
* @param {{ start: number, size: number, capHeight: number, canvas: { width: number, height: number }, text: string }} o - `start`: index of the line's first glyph in the block.
|
|
29
|
+
* @returns {{ run: import('../text.js').Shaped, count: number }} The run, and how many glyphs it counted.
|
|
30
|
+
*/
|
|
31
|
+
export function controlGlyphs(run, spec, o) {
|
|
32
|
+
if (!spec) return { run, count: 0 };
|
|
33
|
+
const glyphs = run.glyphs.map((g) => ({ ...g }));
|
|
34
|
+
const blanks = blankGlyphs(run, o.text);
|
|
35
|
+
let shift = 0;
|
|
36
|
+
let n = 0;
|
|
37
|
+
for (let i = 0; i < glyphs.length; i++) {
|
|
38
|
+
const g = glyphs[i];
|
|
39
|
+
const adv = (i + 1 < run.glyphs.length ? run.glyphs[i + 1].x : run.width) - run.glyphs[i].x;
|
|
40
|
+
g.x += shift;
|
|
41
|
+
if (blanks.has(i)) continue;
|
|
42
|
+
const k = o.start + n++;
|
|
43
|
+
const s = at(spec.scale, k, 1);
|
|
44
|
+
const sx = at(spec.scaleX, k, 1) * s;
|
|
45
|
+
const sy = at(spec.scaleY, k, 1) * s;
|
|
46
|
+
const rot = (at(spec.rotate, k, 0) * Math.PI) / 180;
|
|
47
|
+
const dx = len(at(spec.dx, k, 0), o.canvas);
|
|
48
|
+
const dy = len(at(spec.dy, k, 0), o.canvas);
|
|
49
|
+
const color = at(spec.color, k, undefined);
|
|
50
|
+
const grow = spec.advance === false ? 0 : adv * (sx - 1);
|
|
51
|
+
// About the glyph's centre (at half the cap height), or its baseline.
|
|
52
|
+
const ox = (adv * (spec.advance === false ? 1 : sx)) / 2;
|
|
53
|
+
const oy = spec.origin === 'baseline' ? 0 : -o.capHeight / 2;
|
|
54
|
+
const cos = Math.cos(rot);
|
|
55
|
+
const sin = Math.sin(rot);
|
|
56
|
+
const a = cos * sx;
|
|
57
|
+
const b = sin * sx;
|
|
58
|
+
const c = -sin * sy;
|
|
59
|
+
const d = cos * sy;
|
|
60
|
+
// Glyph-local point p maps to origin + R·S·(p − origin/scaled) + shift.
|
|
61
|
+
const px0 = adv / 2;
|
|
62
|
+
g.t = [a, b, c, d, ox - (a * px0 + c * oy) + dx, oy - (b * px0 + d * oy) + dy];
|
|
63
|
+
if (color != null) g.paint = toHex(color);
|
|
64
|
+
shift += grow;
|
|
65
|
+
}
|
|
66
|
+
return { run: { glyphs, width: run.width + shift }, count: n };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Glyph indices that are spaces (by cluster), so they don't take a slot.
|
|
71
|
+
*
|
|
72
|
+
* @param {import('../text.js').Shaped} run
|
|
73
|
+
* @param {string} text
|
|
74
|
+
* @returns {Set<number>}
|
|
75
|
+
*/
|
|
76
|
+
function blankGlyphs(run, text) {
|
|
77
|
+
const chars = Array.from(text);
|
|
78
|
+
const out = new Set();
|
|
79
|
+
run.glyphs.forEach((g, i) => {
|
|
80
|
+
const ch = /** @type {any} */ (g).cluster != null ? text[/** @type {any} */ (g).cluster] : chars[i];
|
|
81
|
+
if (ch != null && /\s/.test(ch)) out.add(i);
|
|
82
|
+
});
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** How many glyphs a line counts (non-space characters). @param {string} text @returns {number} */
|
|
87
|
+
export const glyphCount = (text) => Array.from(text).filter((ch) => !/\s/.test(ch)).length;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poster typesetting: what the `text` layer does with its 1.1 options, and
|
|
3
|
+
* the line-setting the poster layers (tables, barcodes) share.
|
|
4
|
+
*
|
|
5
|
+
* @module type
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
export { typeset, wantsType, TYPE_OPTIONS } from './typeset.js';
|
|
9
|
+
export { faceFor, mappedPath, setLine } from './line.js';
|
|
10
|
+
export { textPathD, pathSampler } from './path.js';
|