@arraypress/covers 1.0.0 → 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 +76 -0
- package/README.md +296 -11
- package/package.json +33 -5
- package/recipes/brand.json +1143 -0
- package/schema/recipe.schema.json +7300 -770
- package/src/animate/index.d.ts +19 -0
- package/src/animate/index.js +116 -0
- package/src/builtins.js +4 -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 +4 -3
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +246 -53
- 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 +1 -1
- package/src/icons.js +43 -9
- package/src/index.d.ts +205 -19
- package/src/index.js +12 -4
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +181 -23
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +23 -4
- 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 +125 -41
- 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/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 +77 -28
- package/src/print.js +8 -7
- package/src/raster/index.d.ts +24 -0
- package/src/raster/index.js +171 -0
- package/src/recipes.js +4 -3
- 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 +303 -212
- package/src/text.d.ts +29 -0
- package/src/text.js +22 -15
- 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/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
CHANGED
|
@@ -27,7 +27,7 @@ import { num } from './svg.js';
|
|
|
27
27
|
* A shaped run of text.
|
|
28
28
|
*
|
|
29
29
|
* @typedef {object} Shaped
|
|
30
|
-
* @property {{ gid: number, x: number, y: number }[]} glyphs - Glyph ids and pen positions in px (y is the baseline offset).
|
|
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
31
|
* @property {number} width - Advance width in px (no trailing letter-spacing).
|
|
32
32
|
*/
|
|
33
33
|
|
|
@@ -38,25 +38,20 @@ import { num } from './svg.js';
|
|
|
38
38
|
* @param {string} text - One line of text.
|
|
39
39
|
* @param {number} size - Font size in px.
|
|
40
40
|
* @param {number} tracking - Letter-spacing in px, added between clusters (0 for none).
|
|
41
|
-
* @param {
|
|
41
|
+
* @param {any} [hb] - Unused (kept for the 1.0 signature).
|
|
42
42
|
* @returns {Shaped} Positioned glyphs.
|
|
43
43
|
*/
|
|
44
44
|
export function shape(face, text, size, tracking, hb) {
|
|
45
|
-
const
|
|
46
|
-
buffer.addText(text);
|
|
47
|
-
buffer.guessSegmentProperties();
|
|
48
|
-
hb.shape(face.font, buffer);
|
|
49
|
-
const infos = buffer.getGlyphInfos();
|
|
50
|
-
const positions = buffer.getGlyphPositions();
|
|
45
|
+
const glyphsIn = face.shapeRun(text);
|
|
51
46
|
const scale = size / face.upem;
|
|
52
47
|
const glyphs = [];
|
|
53
48
|
let pen = 0;
|
|
54
|
-
for (let i = 0; i <
|
|
55
|
-
const p =
|
|
56
|
-
glyphs.push({ gid:
|
|
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 });
|
|
57
52
|
pen += p.xAdvance * scale;
|
|
58
|
-
const nextCluster =
|
|
59
|
-
if (tracking && nextCluster !== undefined && nextCluster !==
|
|
53
|
+
const nextCluster = glyphsIn[i + 1]?.cluster;
|
|
54
|
+
if (tracking && nextCluster !== undefined && nextCluster !== p.cluster) pen += tracking;
|
|
60
55
|
}
|
|
61
56
|
return { glyphs, width: pen };
|
|
62
57
|
}
|
|
@@ -69,11 +64,11 @@ export function shape(face, text, size, tracking, hb) {
|
|
|
69
64
|
* @param {number} gid
|
|
70
65
|
* @returns {(string | number)[]} Flat list: command letter followed by its numbers.
|
|
71
66
|
*/
|
|
72
|
-
function outline(face, gid) {
|
|
67
|
+
export function outline(face, gid) {
|
|
73
68
|
let cmds = face.paths.get(gid);
|
|
74
69
|
if (!cmds) {
|
|
75
70
|
cmds = [];
|
|
76
|
-
const d = face.
|
|
71
|
+
const d = face.glyphToPath(gid);
|
|
77
72
|
for (const m of d.matchAll(/([MLQCZ])([^MLQCZ]*)/gi)) {
|
|
78
73
|
cmds.push(m[1].toUpperCase());
|
|
79
74
|
for (const n of m[2].split(/[\s,]+/).filter(Boolean)) cmds.push(parseFloat(n));
|
|
@@ -228,3 +223,15 @@ function breakLines(o) {
|
|
|
228
223
|
const shaped = lines.map((text) => ({ text, run: measure(text) }));
|
|
229
224
|
return { lines: shaped, size: o.size, truncated, forced, width: Math.max(0, ...shaped.map((l) => l.run.width)) };
|
|
230
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';
|
package/src/type/kern.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pair kerning from a TrueType font: GPOS `kern` feature pair adjustments
|
|
3
|
+
* (formats 1 and 2, through extension lookups; lookups add up), else the
|
|
4
|
+
* legacy `kern` table.
|
|
5
|
+
*
|
|
6
|
+
* @module type/kern
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Pair kerning: GPOS `kern` feature pair adjustments (formats 1 and 2,
|
|
11
|
+
* through extension lookups), else the legacy `kern` table.
|
|
12
|
+
*
|
|
13
|
+
* @param {DataView} v
|
|
14
|
+
* @param {Record<string, number>} tables
|
|
15
|
+
* @returns {(a: number, b: number) => number} X advance adjustment, font units.
|
|
16
|
+
*/
|
|
17
|
+
export function pairKerning(v, tables) {
|
|
18
|
+
const u16 = (/** @type {number} */ o) => v.getUint16(o);
|
|
19
|
+
const i16 = (/** @type {number} */ o) => v.getInt16(o);
|
|
20
|
+
/** Pair lookups, each a list of subtables (the first that covers a pair applies; lookups add up). @type {((a: number, b: number) => number | undefined)[][]} */
|
|
21
|
+
const lookupsList = [];
|
|
22
|
+
/** @type {((a: number, b: number) => number | undefined)[]} */
|
|
23
|
+
let subtables = [];
|
|
24
|
+
|
|
25
|
+
const coverage = (/** @type {number} */ o) => {
|
|
26
|
+
const fmt = u16(o);
|
|
27
|
+
/** @type {Map<number, number>} */
|
|
28
|
+
const m = new Map();
|
|
29
|
+
if (fmt === 1) for (let i = 0, n = u16(o + 2); i < n; i++) m.set(u16(o + 4 + i * 2), i);
|
|
30
|
+
else for (let i = 0, n = u16(o + 2); i < n; i++) {
|
|
31
|
+
const r = o + 4 + i * 6;
|
|
32
|
+
for (let g = u16(r), idx = u16(r + 4); g <= u16(r + 2); g++, idx++) m.set(g, idx);
|
|
33
|
+
}
|
|
34
|
+
return m;
|
|
35
|
+
};
|
|
36
|
+
const classDef = (/** @type {number} */ o) => {
|
|
37
|
+
const fmt = u16(o);
|
|
38
|
+
/** @type {Map<number, number>} */
|
|
39
|
+
const m = new Map();
|
|
40
|
+
if (fmt === 1) { const s = u16(o + 2); for (let i = 0, n = u16(o + 4); i < n; i++) m.set(s + i, u16(o + 6 + i * 2)); }
|
|
41
|
+
else for (let i = 0, n = u16(o + 2); i < n; i++) { const r = o + 4 + i * 6; for (let g = u16(r); g <= u16(r + 2); g++) m.set(g, u16(r + 4)); }
|
|
42
|
+
return m;
|
|
43
|
+
};
|
|
44
|
+
const bits = (/** @type {number} */ f) => { let c = 0; for (let x = f; x; x >>= 1) c += x & 1; return c * 2; };
|
|
45
|
+
// XAdvance of a value record (or XPlacement, the rare alternative).
|
|
46
|
+
const xadv = (/** @type {number} */ o, /** @type {number} */ fmt) => {
|
|
47
|
+
let p = o;
|
|
48
|
+
let x = 0;
|
|
49
|
+
if (fmt & 1) { x += i16(p); p += 2; }
|
|
50
|
+
if (fmt & 2) p += 2;
|
|
51
|
+
if (fmt & 4) x += i16(p);
|
|
52
|
+
return x;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const pairPos = (/** @type {number} */ st) => {
|
|
56
|
+
const fmt = u16(st);
|
|
57
|
+
const cov = coverage(st + u16(st + 2));
|
|
58
|
+
const vf1 = u16(st + 4);
|
|
59
|
+
const vf2 = u16(st + 6);
|
|
60
|
+
const s1 = bits(vf1);
|
|
61
|
+
const s2 = bits(vf2);
|
|
62
|
+
if (fmt === 1) {
|
|
63
|
+
const sets = st + 10;
|
|
64
|
+
subtables.push((a, b) => {
|
|
65
|
+
const idx = cov.get(a);
|
|
66
|
+
if (idx === undefined) return undefined;
|
|
67
|
+
const ps = st + u16(sets + idx * 2);
|
|
68
|
+
const n = u16(ps);
|
|
69
|
+
const rec = 2 + s1 + s2;
|
|
70
|
+
let lo = 0;
|
|
71
|
+
let hi = n - 1;
|
|
72
|
+
while (lo <= hi) {
|
|
73
|
+
const mid = (lo + hi) >> 1;
|
|
74
|
+
const r = ps + 2 + mid * rec;
|
|
75
|
+
const g = u16(r);
|
|
76
|
+
if (g === b) return xadv(r + 2, vf1);
|
|
77
|
+
if (g < b) lo = mid + 1; else hi = mid - 1;
|
|
78
|
+
}
|
|
79
|
+
return undefined;
|
|
80
|
+
});
|
|
81
|
+
} else if (fmt === 2) {
|
|
82
|
+
const c1 = classDef(st + u16(st + 8));
|
|
83
|
+
const c2 = classDef(st + u16(st + 10));
|
|
84
|
+
const n2 = u16(st + 14);
|
|
85
|
+
const rec = s1 + s2;
|
|
86
|
+
subtables.push((a, b) => {
|
|
87
|
+
if (!cov.has(a)) return undefined;
|
|
88
|
+
const r = st + 16 + ((c1.get(a) ?? 0) * n2 + (c2.get(b) ?? 0)) * rec;
|
|
89
|
+
return xadv(r, vf1);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const gpos = tables.GPOS;
|
|
95
|
+
if (gpos != null) {
|
|
96
|
+
const features = gpos + u16(gpos + 6);
|
|
97
|
+
const lookups = gpos + u16(gpos + 8);
|
|
98
|
+
/** @type {Set<number>} */
|
|
99
|
+
const wanted = new Set();
|
|
100
|
+
for (let i = 0, n = u16(features); i < n; i++) {
|
|
101
|
+
const r = features + 2 + i * 6;
|
|
102
|
+
if (String.fromCharCode(v.getUint8(r), v.getUint8(r + 1), v.getUint8(r + 2), v.getUint8(r + 3)) !== 'kern') continue;
|
|
103
|
+
const f = features + u16(r + 4);
|
|
104
|
+
for (let k = 0, m = u16(f + 2); k < m; k++) wanted.add(u16(f + 4 + k * 2));
|
|
105
|
+
}
|
|
106
|
+
for (const li of [...wanted].sort((a, b) => a - b)) {
|
|
107
|
+
subtables = [];
|
|
108
|
+
lookupsList.push(subtables);
|
|
109
|
+
const l = lookups + u16(lookups + 2 + li * 2);
|
|
110
|
+
const type = u16(l);
|
|
111
|
+
for (let k = 0, m = u16(l + 4); k < m; k++) {
|
|
112
|
+
let st = l + u16(l + 6 + k * 2);
|
|
113
|
+
let t = type;
|
|
114
|
+
if (t === 9) { t = u16(st + 2); st += v.getUint32(st + 4); }
|
|
115
|
+
if (t === 2) pairPos(st);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
if (!lookupsList.some((l) => l.length) && tables.kern != null && u16(tables.kern) === 0) {
|
|
120
|
+
subtables = [];
|
|
121
|
+
lookupsList.push(subtables);
|
|
122
|
+
const k = tables.kern;
|
|
123
|
+
const st = k + 4;
|
|
124
|
+
if (u16(st + 4) >> 8 === 0) {
|
|
125
|
+
/** @type {Map<number, number>} */
|
|
126
|
+
const pairs = new Map();
|
|
127
|
+
for (let i = 0, n = u16(st + 6); i < n; i++) {
|
|
128
|
+
const r = st + 14 + i * 6;
|
|
129
|
+
pairs.set(u16(r) * 65536 + u16(r + 2), i16(r + 4));
|
|
130
|
+
}
|
|
131
|
+
subtables.push((a, b) => pairs.get(a * 65536 + b));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return (a, b) => {
|
|
135
|
+
let total = 0;
|
|
136
|
+
for (const list of lookupsList) {
|
|
137
|
+
for (const s of list) {
|
|
138
|
+
const x = s(a, b);
|
|
139
|
+
if (x !== undefined) { total += x; break; }
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return total;
|
|
143
|
+
};
|
|
144
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Text layout later layers can read: {layout.<id>.<field>}.
|
|
2
|
+
|
|
3
|
+
/** Fields a reference can read. */
|
|
4
|
+
export const LAYOUT_FIELDS: readonly ('x' | 'y' | 'top' | 'bottom' | 'left' | 'right' | 'width' | 'height' | 'baseline' | 'size' | 'lines')[];
|
|
5
|
+
/** Whether a value holds a layout reference (outside nested layers). */
|
|
6
|
+
export function hasLayoutRef(v: unknown): boolean;
|
|
7
|
+
/** A layer with its layout references replaced by numbers. */
|
|
8
|
+
export function resolveLayoutRefs<T>(v: T, layout: Map<string, Record<string, number>>): T;
|
|
9
|
+
/** Record a text block's layout for later layers. */
|
|
10
|
+
export function recordLayout(ctx: { layout?: Map<string, Record<string, number>> }, id: string | undefined, box: { x: number; y: number; width: number; height: number }, more: { x: number; y: number; baseline: number; size: number; lines: number }): void;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text layout other layers can read: a text layer with an `id` records
|
|
3
|
+
* where its block landed, and any later layer can use `{layout.<id>.<field>}`
|
|
4
|
+
* (optionally `+ n` or `- n`) as a number: `bottom`, `top`, `left`,
|
|
5
|
+
* `right`, `width`, `height`, `x`, `y` (the first baseline), `baseline`
|
|
6
|
+
* (the last one), `size` (as fitted) and `lines`. Units are the text
|
|
7
|
+
* layer's own (a group's grid inside a group).
|
|
8
|
+
*
|
|
9
|
+
* @module type/layout-ref
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/** A layout reference, whole or inside a string. */
|
|
13
|
+
const TOKEN = /\{layout\.([\w-]+)\.(\w+)\s*(?:([+-])\s*([\d.]+))?\}/g;
|
|
14
|
+
|
|
15
|
+
/** Fields a reference can read. */
|
|
16
|
+
export const LAYOUT_FIELDS = Object.freeze(['x', 'y', 'top', 'bottom', 'left', 'right', 'width', 'height', 'baseline', 'size', 'lines']);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Whether a value holds a layout reference (layers nested in it aside: they
|
|
20
|
+
* resolve when they're drawn).
|
|
21
|
+
*
|
|
22
|
+
* @param {unknown} v
|
|
23
|
+
* @returns {boolean}
|
|
24
|
+
*/
|
|
25
|
+
export function hasLayoutRef(v) {
|
|
26
|
+
if (typeof v === 'string') return v.includes('{layout.');
|
|
27
|
+
if (Array.isArray(v)) return v.some(hasLayoutRef);
|
|
28
|
+
if (v && typeof v === 'object' && !(v instanceof Uint8Array)) return Object.entries(v).some(([k, x]) => !NESTED.has(k) && hasLayoutRef(x));
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Options holding nested layers. */
|
|
33
|
+
const NESTED = new Set(['layers', 'mask', 'knockout', 'clip']);
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* A layer with its layout references replaced by numbers.
|
|
37
|
+
*
|
|
38
|
+
* @param {any} v
|
|
39
|
+
* @param {Map<string, Record<string, number>>} layout
|
|
40
|
+
* @returns {any}
|
|
41
|
+
* @throws {TypeError} On a reference to a text layer not drawn yet, or an unknown field.
|
|
42
|
+
*/
|
|
43
|
+
export function resolveLayoutRefs(v, layout) {
|
|
44
|
+
if (typeof v === 'string') {
|
|
45
|
+
const one = new RegExp(`^${TOKEN.source}$`).exec(v.trim());
|
|
46
|
+
if (one) return value(one, layout);
|
|
47
|
+
return v.replace(TOKEN, (...m) => String(value(/** @type {any} */ (m), layout)));
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(v)) return v.map((x) => resolveLayoutRefs(x, layout));
|
|
50
|
+
if (v && typeof v === 'object' && !(v instanceof Uint8Array)) return Object.fromEntries(Object.entries(v).map(([k, x]) => [k, NESTED.has(k) ? x : resolveLayoutRefs(x, layout)]));
|
|
51
|
+
return v;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** @param {string[]} m @param {Map<string, Record<string, number>>} layout @returns {number} */
|
|
55
|
+
function value(m, layout) {
|
|
56
|
+
const [, id, field, sign, n] = m;
|
|
57
|
+
const box = layout.get(id);
|
|
58
|
+
if (!box) throw new TypeError(`{layout.${id}.${field}}: no text layer "${id}" drawn before this one`);
|
|
59
|
+
if (!LAYOUT_FIELDS.includes(field)) throw new TypeError(`{layout.${id}.${field}}: unknown field (${LAYOUT_FIELDS.join(', ')})`);
|
|
60
|
+
return box[field] + (sign ? (sign === '-' ? -1 : 1) * Number(n) : 0);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Record a text block's layout for later layers.
|
|
65
|
+
*
|
|
66
|
+
* @param {any} ctx
|
|
67
|
+
* @param {string | undefined} id
|
|
68
|
+
* @param {{ x: number, y: number, width: number, height: number }} box - The block, from cap height to last baseline.
|
|
69
|
+
* @param {{ x: number, y: number, baseline: number, size: number, lines: number }} more
|
|
70
|
+
*/
|
|
71
|
+
export function recordLayout(ctx, id, box, more) {
|
|
72
|
+
if (!id || !ctx.layout) return;
|
|
73
|
+
ctx.layout.set(id, { ...more, top: box.y, bottom: box.y + box.height, left: box.x, right: box.x + box.width, width: box.width, height: box.height });
|
|
74
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Setting one line of type.
|
|
2
|
+
import type { LoadedFont } from '../fonts.js';
|
|
3
|
+
import type { Shaped } from '../text.js';
|
|
4
|
+
import type { RenderContext } from '../render/context.js';
|
|
5
|
+
|
|
6
|
+
/** A face for a family, weight, style and variable axes (`'pixel'`: the built-in 5×7 face). */
|
|
7
|
+
export function faceFor(ctx: RenderContext, o: { font?: string; weight?: number; style?: string; axes?: Record<string, number> }): Promise<LoadedFont>;
|
|
8
|
+
/** Glyph outlines of a run through a map from glyph px (y down, baseline 0) to canvas px. */
|
|
9
|
+
export function mappedPath(face: LoadedFont, run: Shaped, size: number, map: (gx: number, gy: number, glyph: Shaped['glyphs'][number]) => [number, number], only?: (glyph: Shaped['glyphs'][number]) => boolean): string;
|
|
10
|
+
/** Shape, justify (`width`) and draw one line anchored at `x` by `align`. */
|
|
11
|
+
export function setLine(o: { face: LoadedFont; text: string; size: number; tracking: number; x: number; y: number; align: 'left' | 'center' | 'right'; width?: number; adjust?: 'spacingAndGlyphs' | 'spacing'; shape?: (tracking: number) => Shaped; only?: (glyph: Shaped['glyphs'][number]) => boolean }): { d: string; width: number; left: number; run: Shaped; scaleX: number };
|