@arraypress/covers 1.0.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +118 -0
- package/README.md +18 -524
- package/package.json +34 -5
- package/recipes/brand.json +1143 -0
- 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-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/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/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 +7624 -827
- package/src/animate/index.d.ts +9 -0
- package/src/animate/index.js +11 -0
- 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/builtins.js +4 -0
- package/src/diagram.js +4 -3
- package/src/extract.js +6 -5
- package/src/fonts.d.ts +55 -0
- package/src/fonts.js +245 -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 +5 -35
- package/src/grid.js +1 -1
- package/src/handmade.js +9 -7
- package/src/icons.js +42 -9
- package/src/index.d.ts +232 -55
- package/src/index.js +14 -6
- package/src/inter-data.js +27 -0
- package/src/json-recipe.js +201 -27
- package/src/kit/index.d.ts +20 -0
- package/src/kit/index.js +58 -0
- package/src/layer-options.js +43 -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 +108 -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 +61 -0
- package/src/layers.js +129 -61
- 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/paint/index.d.ts +49 -0
- package/src/paint/index.js +66 -0
- package/src/paint/presets.d.ts +10 -0
- package/src/paint/presets.js +47 -0
- package/src/paint/tile.d.ts +7 -0
- package/src/paint/tile.js +55 -0
- 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.d.ts +9 -0
- package/src/platform/harfbuzz.js +80 -0
- package/src/platform/index.d.ts +8 -0
- package/src/platform/index.js +15 -0
- package/src/platform/load.d.ts +6 -0
- package/src/platform/load.js +71 -0
- package/src/platform/no-harfbuzz.d.ts +2 -0
- package/src/platform/no-harfbuzz.js +11 -0
- package/src/platform/settings.d.ts +28 -0
- package/src/platform/settings.js +51 -0
- package/src/print.js +16 -13
- package/src/recipes.js +5 -4
- package/src/render/backdrop.d.ts +6 -0
- package/src/render/backdrop.js +55 -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 +90 -0
- package/src/render/report.d.ts +5 -0
- package/src/render/report.js +19 -0
- package/src/render/transform.d.ts +36 -0
- package/src/render/transform.js +139 -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 +144 -0
- package/src/render.js +343 -215
- package/src/text.d.ts +29 -0
- package/src/text.js +23 -16
- 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 +7 -0
- package/src/type/pixel.js +85 -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 +363 -0
- package/src/type/woff2.d.ts +6 -0
- package/src/type/woff2.js +301 -0
- package/src/version.js +4 -0
- package/src/color.js +0 -246
- package/src/png.js +0 -77
- package/src/rng.js +0 -95
- package/src/svg.js +0 -69
package/src/type/line.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Setting one line of type: a face for a layer, glyph outlines through any
|
|
3
|
+
* affine map, and a line justified to a width (`textLength`).
|
|
4
|
+
*
|
|
5
|
+
* @module type/line
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { num } from '@logomarks/image-effects/svg';
|
|
9
|
+
import { loadFont, pickFont, variationsFor } from '../fonts.js';
|
|
10
|
+
import { shape, outline } from '../text.js';
|
|
11
|
+
import { pixelFace } from './pixel.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A face for a layer's family, weight, style and axes (`'pixel'` is the
|
|
15
|
+
* built-in 5×7 face, unless a font of that name is registered).
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
18
|
+
* @param {{ font?: string, weight?: number, style?: string, axes?: Record<string, number> }} o
|
|
19
|
+
* @returns {Promise<import('../fonts.js').LoadedFont>}
|
|
20
|
+
*/
|
|
21
|
+
export async function faceFor(ctx, o) {
|
|
22
|
+
if (o.font === 'pixel' && !ctx.fonts.some((f) => f.name === 'pixel')) return pixelFace();
|
|
23
|
+
const weight = o.weight ?? 600;
|
|
24
|
+
const source = pickFont(ctx.fonts, o.font ?? 'Inter', weight, o.style);
|
|
25
|
+
return loadFont(source, variationsFor(source, weight, o.axes));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Glyph outlines of a shaped run, through an affine map: glyph point
|
|
30
|
+
* (gx, gy) in px (y down, baseline 0) goes to `map(gx, gy)`.
|
|
31
|
+
*
|
|
32
|
+
* @param {import('../fonts.js').LoadedFont} face
|
|
33
|
+
* @param {import('../text.js').Shaped} run
|
|
34
|
+
* @param {number} size
|
|
35
|
+
* @param {(gx: number, gy: number, glyph: { x: number, y: number }) => [number, number]} map
|
|
36
|
+
* @param {(glyph: any) => boolean} [only] - Draw only these glyphs (one paint at a time).
|
|
37
|
+
* @returns {string}
|
|
38
|
+
*/
|
|
39
|
+
export function mappedPath(face, run, size, map, only) {
|
|
40
|
+
const parts = [];
|
|
41
|
+
for (const g of run.glyphs) {
|
|
42
|
+
if (only && !only(g)) continue;
|
|
43
|
+
const f = g.face ?? face;
|
|
44
|
+
const s = (g.size ?? size) / f.upem;
|
|
45
|
+
const t = g.t;
|
|
46
|
+
let coords = [];
|
|
47
|
+
let even = true;
|
|
48
|
+
let px = 0;
|
|
49
|
+
for (const c of outline(f, g.gid)) {
|
|
50
|
+
if (typeof c === 'string') {
|
|
51
|
+
if (coords.length) parts.push(coords.join(' '));
|
|
52
|
+
parts.push(c);
|
|
53
|
+
coords = [];
|
|
54
|
+
even = true;
|
|
55
|
+
} else if (even) {
|
|
56
|
+
px = c;
|
|
57
|
+
even = false;
|
|
58
|
+
} else {
|
|
59
|
+
// A glyph's own transform (per-glyph scale, turn, shift) is about its pen position.
|
|
60
|
+
const gx = px * s;
|
|
61
|
+
const gy = -c * s;
|
|
62
|
+
const [X, Y] = t ? map(g.x + t[0] * gx + t[2] * gy + t[4], g.y + t[1] * gx + t[3] * gy + t[5], g) : map(g.x + gx, g.y + gy, g);
|
|
63
|
+
coords.push(num(X), num(Y));
|
|
64
|
+
even = true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
if (coords.length) parts.push(coords.join(' '));
|
|
68
|
+
}
|
|
69
|
+
return parts.join('');
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Set one line: shape, justify and draw.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} o
|
|
76
|
+
* @param {import('../fonts.js').LoadedFont} o.face
|
|
77
|
+
* @param {string} o.text
|
|
78
|
+
* @param {number} o.size
|
|
79
|
+
* @param {number} o.tracking - Letter-spacing, px.
|
|
80
|
+
* @param {number} o.x - Anchor x (start, middle or end by `align`).
|
|
81
|
+
* @param {number} o.y - Baseline.
|
|
82
|
+
* @param {'left' | 'center' | 'right'} o.align
|
|
83
|
+
* @param {number} [o.width] - Justify to this width.
|
|
84
|
+
* @param {'spacingAndGlyphs' | 'spacing'} [o.adjust]
|
|
85
|
+
* @param {(tracking: number) => import('../text.js').Shaped} [o.shape] - Shapes the line at a tracking (fallback faces, spans, per-glyph control); default the face alone.
|
|
86
|
+
* @param {(glyph: any) => boolean} [o.only] - Draw only these glyphs.
|
|
87
|
+
* @returns {{ d: string, width: number, left: number, run: import('../text.js').Shaped, scaleX: number }}
|
|
88
|
+
*/
|
|
89
|
+
export function setLine(o) {
|
|
90
|
+
const shapeAt = o.shape ?? ((/** @type {number} */ tracking) => shape(o.face, o.text, o.size, tracking));
|
|
91
|
+
let run = shapeAt(o.tracking);
|
|
92
|
+
let scaleX = 1;
|
|
93
|
+
if (o.width && run.width > 0) {
|
|
94
|
+
if (o.adjust === 'spacing') {
|
|
95
|
+
const gaps = Math.max(1, new Set(o.face.shapeRun(o.text).map((g) => g.cluster)).size - 1);
|
|
96
|
+
run = shapeAt(o.tracking + (o.width - run.width) / gaps);
|
|
97
|
+
} else {
|
|
98
|
+
scaleX = o.width / run.width;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const w = run.width * scaleX;
|
|
102
|
+
const left = o.align === 'center' ? o.x - w / 2 : o.align === 'right' ? o.x - w : o.x;
|
|
103
|
+
const d = mappedPath(o.face, run, o.size, (gx, gy) => [left + gx * scaleX, o.y + gy], o.only);
|
|
104
|
+
return { d, width: w, left, run, scaleX };
|
|
105
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Live text: @font-face rules.
|
|
2
|
+
import type { LoadedFont } from '../fonts.js';
|
|
3
|
+
import type { RenderContext } from '../render/context.js';
|
|
4
|
+
|
|
5
|
+
/** The face's `@font-face` rule (empty when this document has it already) and its family name. */
|
|
6
|
+
export function fontFace(face: LoadedFont, ctx: RenderContext): Promise<{ family: string; css: string }>;
|
package/src/type/live.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live text: the `@font-face` rule for a face, embedded (or linked by URL)
|
|
3
|
+
* once per document.
|
|
4
|
+
*
|
|
5
|
+
* @module type/live
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { fontBytes } from '../fonts.js';
|
|
9
|
+
import { toBase64 } from '../platform/index.js';
|
|
10
|
+
|
|
11
|
+
/** Faces embedded in this document already (live text). @type {WeakMap<object, Set<string>>} */
|
|
12
|
+
const embedded = new WeakMap();
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* `@font-face` for a face, once per document.
|
|
16
|
+
*
|
|
17
|
+
* @param {import('../fonts.js').LoadedFont} face
|
|
18
|
+
* @param {import('../layers.js').RenderContext} ctx
|
|
19
|
+
* @returns {Promise<{ family: string, css: string }>}
|
|
20
|
+
*/
|
|
21
|
+
export async function fontFace(face, ctx) {
|
|
22
|
+
const src = face.source;
|
|
23
|
+
const family = `covers-${src.name}-${Array.isArray(src.weight) ? 'var' : src.weight ?? 400}-${src.style ?? 'normal'}`.replace(/[^\w-]/g, '_');
|
|
24
|
+
const doc = /** @type {any} */ (ctx).doc ?? ctx;
|
|
25
|
+
let seen = embedded.get(doc);
|
|
26
|
+
if (!seen) embedded.set(doc, (seen = new Set()));
|
|
27
|
+
if (seen.has(family)) return { family, css: '' };
|
|
28
|
+
seen.add(family);
|
|
29
|
+
let url;
|
|
30
|
+
if (src.data == null && typeof src.src === 'string' && !src.src.startsWith('covers:')) url = src.src;
|
|
31
|
+
else {
|
|
32
|
+
const bytes = await fontBytes(src);
|
|
33
|
+
const tag = new DataView(bytes.buffer, bytes.byteOffset, 4).getUint32(0);
|
|
34
|
+
const mime = tag === 0x774f4646 ? 'font/woff' : tag === 0x774f4632 ? 'font/woff2' : tag === 0x4f54544f ? 'font/otf' : 'font/ttf';
|
|
35
|
+
url = `data:${mime};base64,${toBase64(bytes)}`;
|
|
36
|
+
}
|
|
37
|
+
const weight = Array.isArray(src.weight) ? `${src.weight[0]} ${src.weight[1]}` : src.weight ?? 400;
|
|
38
|
+
return { family, css: `@font-face{font-family:"${family}";src:url("${url}");font-weight:${weight};font-style:${src.style ?? 'normal'}}` };
|
|
39
|
+
}
|
package/src/type/name.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A font's family name from its `name` table.
|
|
3
|
+
*
|
|
4
|
+
* @module type/name
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The family name from the `name` table (typographic family, else family).
|
|
9
|
+
*
|
|
10
|
+
* @param {DataView} v @param {Uint8Array} bytes @param {number | undefined} base
|
|
11
|
+
* @returns {string}
|
|
12
|
+
*/
|
|
13
|
+
export function familyName(v, bytes, base) {
|
|
14
|
+
if (base == null) return '';
|
|
15
|
+
const count = v.getUint16(base + 2);
|
|
16
|
+
const strings = base + v.getUint16(base + 4);
|
|
17
|
+
/** @type {Record<number, string>} */
|
|
18
|
+
const found = {};
|
|
19
|
+
for (let i = 0; i < count; i++) {
|
|
20
|
+
const r = base + 6 + i * 12;
|
|
21
|
+
const pid = v.getUint16(r);
|
|
22
|
+
const id = v.getUint16(r + 6);
|
|
23
|
+
if ((id !== 1 && id !== 16) || found[id]) continue;
|
|
24
|
+
const len = v.getUint16(r + 8);
|
|
25
|
+
const off = strings + v.getUint16(r + 10);
|
|
26
|
+
let s = '';
|
|
27
|
+
if (pid === 3 || pid === 0) for (let k = 0; k < len; k += 2) s += String.fromCharCode(v.getUint16(off + k));
|
|
28
|
+
else for (let k = 0; k < len; k++) s += String.fromCharCode(bytes[off + k]);
|
|
29
|
+
found[id] = s;
|
|
30
|
+
}
|
|
31
|
+
return found[16] ?? found[1] ?? '';
|
|
32
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Paths for text.
|
|
2
|
+
import type { Length } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/** A path for text: an arc, circle, ellipse or wave, or any path data. */
|
|
5
|
+
export type TextPath = string | { d: string } | {
|
|
6
|
+
kind?: 'circle' | 'arc' | 'ellipse' | 'wave';
|
|
7
|
+
cx?: Length | `${number}%`; cy?: Length | `${number}%`; r?: Length; rx?: Length; ry?: Length;
|
|
8
|
+
/** Degrees, 0 at 12 o'clock, clockwise. */
|
|
9
|
+
start?: number; end?: number;
|
|
10
|
+
direction?: 'cw' | 'ccw'; side?: 'outside' | 'inside';
|
|
11
|
+
x1?: Length; x2?: Length; y?: Length; amplitude?: Length; wavelength?: Length; phase?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
/** A text path as path data, px. */
|
|
16
|
+
export function textPathD(p: TextPath, canvas: { width: number; height: number }): string;
|
|
17
|
+
/** A sampler along path data: point and unit tangent at a distance (continuing straight past the ends). */
|
|
18
|
+
export function pathSampler(d: string): { length: number; at: (s: number) => { x: number; y: number; tx: number; ty: number } };
|
package/src/type/path.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Paths for text: circles, arcs, ellipses and waves as path data, and a
|
|
3
|
+
* sampler that gives a point and tangent at any distance along a path.
|
|
4
|
+
*
|
|
5
|
+
* @module type/path
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { num } from '@logomarks/image-effects/svg';
|
|
9
|
+
import { len } from '@logomarks/image-effects/length';
|
|
10
|
+
import { flattenPath } from '../flatten.js';
|
|
11
|
+
import { pos } from '../layers/util.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* A text path as path data, in px.
|
|
15
|
+
*
|
|
16
|
+
* @param {any} p - `{ kind: 'circle' | 'arc' | 'ellipse' | 'wave', … }` or `{ d }`.
|
|
17
|
+
* @param {{ width: number, height: number }} canvas
|
|
18
|
+
* @returns {string}
|
|
19
|
+
*/
|
|
20
|
+
export function textPathD(p, canvas) {
|
|
21
|
+
const X = (/** @type {any} */ v, /** @type {any} */ f) => pos(v ?? f, 'x', canvas);
|
|
22
|
+
const Y = (/** @type {any} */ v, /** @type {any} */ f) => pos(v ?? f, 'y', canvas);
|
|
23
|
+
if (typeof p === 'string') return p;
|
|
24
|
+
if (p.d) return p.d;
|
|
25
|
+
const kind = p.kind ?? 'circle';
|
|
26
|
+
if (kind === 'circle' || kind === 'ellipse' || kind === 'arc') {
|
|
27
|
+
const cx = X(p.cx, '50%');
|
|
28
|
+
const cy = Y(p.cy, '50%');
|
|
29
|
+
const rx = len(p.rx ?? p.r, canvas, '30vmin');
|
|
30
|
+
const ry = len(p.ry ?? p.r, canvas, '30vmin');
|
|
31
|
+
// Angles: 0 is 12 o'clock, clockwise. A circle starts at its `start` and goes all the way round.
|
|
32
|
+
const ccw = p.direction === 'ccw' || p.side === 'inside';
|
|
33
|
+
const a0 = p.start ?? (kind === 'arc' ? -90 : 180);
|
|
34
|
+
const sweep = kind === 'arc' ? (p.end ?? 90) - a0 : 360;
|
|
35
|
+
const n = Math.max(24, Math.ceil(Math.abs(sweep) / 2));
|
|
36
|
+
const pts = [];
|
|
37
|
+
for (let i = 0; i <= n; i++) {
|
|
38
|
+
const a = ((a0 + (ccw ? -1 : 1) * Math.abs(sweep) * (i / n)) * Math.PI) / 180;
|
|
39
|
+
pts.push(`${i ? 'L' : 'M'}${num(cx + rx * Math.sin(a), 3)} ${num(cy - ry * Math.cos(a), 3)}`);
|
|
40
|
+
}
|
|
41
|
+
return pts.join('');
|
|
42
|
+
}
|
|
43
|
+
if (kind === 'wave') {
|
|
44
|
+
const x1 = X(p.x1, 0);
|
|
45
|
+
const x2 = X(p.x2, '100%');
|
|
46
|
+
const y = Y(p.y, '50%');
|
|
47
|
+
const amp = len(p.amplitude, canvas, '2vmin');
|
|
48
|
+
const wl = len(p.wavelength, canvas, '40vmin');
|
|
49
|
+
const ph = p.phase ?? 0;
|
|
50
|
+
const pts = [];
|
|
51
|
+
for (let x = x1, i = 0; x <= x2 + 0.01; x += 2, i++) pts.push(`${i ? 'L' : 'M'}${num(x, 3)} ${num(y + Math.sin(((x - x1) / wl) * Math.PI * 2 + ph) * amp, 3)}`);
|
|
52
|
+
return pts.join('');
|
|
53
|
+
}
|
|
54
|
+
throw new TypeError(`Unknown text path kind "${kind}" (circle, arc, ellipse, wave, or { d })`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A sampler along path data: point and tangent at a distance.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} d
|
|
61
|
+
* @returns {{ length: number, at: (s: number) => { x: number, y: number, tx: number, ty: number } }}
|
|
62
|
+
*/
|
|
63
|
+
export function pathSampler(d) {
|
|
64
|
+
const subs = flattenPath(d, 1);
|
|
65
|
+
const pts = subs.flatMap((s) => s.points);
|
|
66
|
+
if (pts.length < 2) throw new TypeError('A text path needs at least two points');
|
|
67
|
+
const cum = [0];
|
|
68
|
+
for (let i = 1; i < pts.length; i++) cum.push(cum[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1]));
|
|
69
|
+
const length = cum[cum.length - 1];
|
|
70
|
+
return {
|
|
71
|
+
length,
|
|
72
|
+
at(s) {
|
|
73
|
+
const t = Math.max(0, Math.min(length, s));
|
|
74
|
+
let lo = 0;
|
|
75
|
+
let hi = cum.length - 1;
|
|
76
|
+
while (hi - lo > 1) { const mid = (lo + hi) >> 1; if (cum[mid] <= t) lo = mid; else hi = mid; }
|
|
77
|
+
const seg = cum[hi] - cum[lo] || 1;
|
|
78
|
+
const f = (t - cum[lo]) / seg;
|
|
79
|
+
const [ax, ay] = pts[lo];
|
|
80
|
+
const [bx, by] = pts[hi];
|
|
81
|
+
const dl = Math.hypot(bx - ax, by - ay) || 1;
|
|
82
|
+
// Beyond the ends, keep going along the end tangent.
|
|
83
|
+
const over = s - t;
|
|
84
|
+
const tx = (bx - ax) / dl;
|
|
85
|
+
const ty = (by - ay) / dl;
|
|
86
|
+
return { x: ax + (bx - ax) * f + tx * over, y: ay + (by - ay) * f + ty * over, tx, ty };
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
// The built-in 5×7 pixel face.
|
|
2
|
+
import type { LoadedFont } from '../fonts.js';
|
|
3
|
+
|
|
4
|
+
/** The pixel face (`font: 'pixel'`): A–Z, 0–9 and basic punctuation, as merged cell paths. */
|
|
5
|
+
export function pixelFace(): LoadedFont;
|
|
6
|
+
/** The characters it draws (lowercase is drawn as capitals). */
|
|
7
|
+
export const PIXEL_CHARS: string;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A built-in 5×7 pixel face (`font: 'pixel'`): A–Z, 0–9 and basic
|
|
3
|
+
* punctuation, each glyph a set of square cells merged into row runs. It
|
|
4
|
+
* is a face like any other (layout, fitting, justification, text on a
|
|
5
|
+
* path, per-glyph control all work), drawn as paths. The bitmaps are
|
|
6
|
+
* image-effects' pixel font (its `ascii` effect draws with them).
|
|
7
|
+
*
|
|
8
|
+
* Metrics, in a 1000-unit em: a cell is 100 units, capitals are 7 cells
|
|
9
|
+
* (700), the advance is 6 cells (a cell's gap after each glyph), a space
|
|
10
|
+
* is 4 cells. Lowercase is drawn as capitals.
|
|
11
|
+
*
|
|
12
|
+
* @module type/pixel
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { PIXEL_GLYPHS as BITMAPS } from '@logomarks/image-effects/pixel-font';
|
|
16
|
+
|
|
17
|
+
/** Cell size, font units. */
|
|
18
|
+
const CELL = 100;
|
|
19
|
+
|
|
20
|
+
/** Glyph id per character (1-based; 0 is missing), and back. */
|
|
21
|
+
const CHARS = Object.keys(BITMAPS);
|
|
22
|
+
const GID = new Map(CHARS.map((c, i) => [c, i + 1]));
|
|
23
|
+
const SPACE = CHARS.length + 1;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* A bitmap's cells as path data, row runs merged: font units, y up,
|
|
27
|
+
* baseline 0.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} bits - 35 cells, row by row from the top.
|
|
30
|
+
* @returns {string}
|
|
31
|
+
*/
|
|
32
|
+
function cellsPath(bits) {
|
|
33
|
+
let d = '';
|
|
34
|
+
for (let row = 0; row < 7; row++) {
|
|
35
|
+
const top = (7 - row) * CELL;
|
|
36
|
+
for (let col = 0; col < 5; col++) {
|
|
37
|
+
if (bits[row * 5 + col] !== '#') continue;
|
|
38
|
+
let end = col;
|
|
39
|
+
while (end + 1 < 5 && bits[row * 5 + end + 1] === '#') end++;
|
|
40
|
+
const x0 = col * CELL;
|
|
41
|
+
const x1 = (end + 1) * CELL;
|
|
42
|
+
d += `M${x0},${top}L${x1},${top}L${x1},${top - CELL}L${x0},${top - CELL}Z`;
|
|
43
|
+
col = end;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return d;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** @type {Map<number, string>} */
|
|
50
|
+
const PATHS = new Map(CHARS.map((c, i) => [i + 1, cellsPath(BITMAPS[c])]));
|
|
51
|
+
|
|
52
|
+
/** @type {any} */
|
|
53
|
+
let face;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* The pixel face, shaped and outlined in JavaScript (no font file).
|
|
57
|
+
*
|
|
58
|
+
* @returns {import('../fonts.js').LoadedFont}
|
|
59
|
+
*/
|
|
60
|
+
export function pixelFace() {
|
|
61
|
+
face ??= {
|
|
62
|
+
name: 'pixel',
|
|
63
|
+
weight: 400,
|
|
64
|
+
style: 'normal',
|
|
65
|
+
font: null,
|
|
66
|
+
upem: 1000,
|
|
67
|
+
ascender: 800,
|
|
68
|
+
descender: -200,
|
|
69
|
+
capHeight: 7 * CELL,
|
|
70
|
+
paths: new Map(),
|
|
71
|
+
axes: {},
|
|
72
|
+
variations: {},
|
|
73
|
+
source: { name: 'pixel', src: 'covers:pixel', weight: 400 },
|
|
74
|
+
glyphToPath: (/** @type {number} */ gid) => PATHS.get(gid) ?? '',
|
|
75
|
+
shapeRun: (/** @type {string} */ text) => Array.from(text).map((ch, i) => {
|
|
76
|
+
const c = BITMAPS[ch] ? ch : ch.toUpperCase();
|
|
77
|
+
const gid = ch === ' ' ? SPACE : GID.get(c) ?? 0;
|
|
78
|
+
return { gid, cluster: i, xAdvance: ch === ' ' ? 4 * CELL : 6 * CELL, xOffset: 0, yOffset: 0 };
|
|
79
|
+
}),
|
|
80
|
+
};
|
|
81
|
+
return face;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Characters the pixel face draws. */
|
|
85
|
+
export const PIXEL_CHARS = Object.freeze(CHARS.join(''));
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// A small TrueType reader.
|
|
2
|
+
|
|
3
|
+
/** A parsed TrueType face. */
|
|
4
|
+
export interface JsFace {
|
|
5
|
+
kind: 'js';
|
|
6
|
+
upem: number;
|
|
7
|
+
ascender: number;
|
|
8
|
+
descender: number;
|
|
9
|
+
capHeight: number;
|
|
10
|
+
/** A glyph's outline, font units, y up. */
|
|
11
|
+
glyphToPath: (gid: number) => string;
|
|
12
|
+
/** Glyphs with kerned advances, font units. */
|
|
13
|
+
shape: (text: string) => { gid: number; cluster: number; advance: number }[];
|
|
14
|
+
/** Variation axes (fvar). */
|
|
15
|
+
axes: Record<string, { min: number; default: number; max: number }>;
|
|
16
|
+
family: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Parse a TrueType font (sfnt bytes; not CFF). */
|
|
20
|
+
export function parseSfnt(bytes: Uint8Array): JsFace;
|
package/src/type/sfnt.js
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A small TrueType reader in plain JavaScript: character map, advances,
|
|
3
|
+
* pair kerning (GPOS pair adjustment, incl. extension lookups, and the old
|
|
4
|
+
* `kern` table) and glyph outlines (`glyf`, simple and composite).
|
|
5
|
+
*
|
|
6
|
+
* It's the outliner where HarfBuzz can't run (a Cloudflare Worker forbids
|
|
7
|
+
* compiling WebAssembly at runtime, which harfbuzzjs needs), and what
|
|
8
|
+
* live-text mode measures with. It doesn't do ligatures, complex scripts,
|
|
9
|
+
* CFF (`OTTO`) outlines or variation deltas (a variable font draws its
|
|
10
|
+
* default instance); HarfBuzz does all of those where it's available.
|
|
11
|
+
*
|
|
12
|
+
* @module type/sfnt
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { pairKerning } from './kern.js';
|
|
16
|
+
import { familyName } from './name.js';
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @typedef {object} JsFace
|
|
20
|
+
* @property {'js'} kind
|
|
21
|
+
* @property {number} upem
|
|
22
|
+
* @property {number} ascender
|
|
23
|
+
* @property {number} descender
|
|
24
|
+
* @property {number} capHeight
|
|
25
|
+
* @property {(gid: number) => string} glyphToPath - Outline in font units, y up.
|
|
26
|
+
* @property {(text: string) => { gid: number, cluster: number, advance: number }[]} shape - Glyphs with kerned advances, font units.
|
|
27
|
+
* @property {Record<string, { min: number, default: number, max: number }>} axes - Variation axes (fvar), if any.
|
|
28
|
+
* @property {string} family - The font's family name (name ID 1/16), if found.
|
|
29
|
+
*/
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Parse a TrueType font.
|
|
33
|
+
*
|
|
34
|
+
* @param {Uint8Array} bytes - sfnt bytes (decoded from WOFF already).
|
|
35
|
+
* @returns {JsFace}
|
|
36
|
+
* @throws {TypeError} On a CFF font or one without the tables it needs.
|
|
37
|
+
*/
|
|
38
|
+
export function parseSfnt(bytes) {
|
|
39
|
+
const v = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
40
|
+
const tag = v.getUint32(0);
|
|
41
|
+
if (tag === 0x4f54544f) throw new TypeError('This font has CFF outlines (OTTO), which the JavaScript outliner can\'t draw: use a TrueType (glyf) font here, or HarfBuzz');
|
|
42
|
+
/** @type {Record<string, number>} */
|
|
43
|
+
const tables = {};
|
|
44
|
+
const n = v.getUint16(4);
|
|
45
|
+
for (let i = 0; i < n; i++) {
|
|
46
|
+
const o = 12 + i * 16;
|
|
47
|
+
tables[String.fromCharCode(bytes[o], bytes[o + 1], bytes[o + 2], bytes[o + 3])] = v.getUint32(o + 8);
|
|
48
|
+
}
|
|
49
|
+
for (const t of ['head', 'hhea', 'hmtx', 'maxp', 'cmap', 'loca', 'glyf']) if (tables[t] == null) throw new TypeError(`Font has no ${t} table`);
|
|
50
|
+
const u16 = (/** @type {number} */ o) => v.getUint16(o);
|
|
51
|
+
const i16 = (/** @type {number} */ o) => v.getInt16(o);
|
|
52
|
+
const u32 = (/** @type {number} */ o) => v.getUint32(o);
|
|
53
|
+
|
|
54
|
+
const upem = u16(tables.head + 18);
|
|
55
|
+
const longLoca = i16(tables.head + 50) === 1;
|
|
56
|
+
const ascender = i16(tables.hhea + 4);
|
|
57
|
+
const descender = i16(tables.hhea + 6);
|
|
58
|
+
const numH = u16(tables.hhea + 34);
|
|
59
|
+
const numGlyphs = u16(tables.maxp + 4);
|
|
60
|
+
let capHeight = 0;
|
|
61
|
+
if (tables['OS/2'] != null && u16(tables['OS/2']) >= 2) capHeight = i16(tables['OS/2'] + 88);
|
|
62
|
+
|
|
63
|
+
const advance = (/** @type {number} */ g) => u16(tables.hmtx + 4 * Math.min(g, numH - 1));
|
|
64
|
+
|
|
65
|
+
// cmap: prefer a full-repertoire (format 12) subtable, else BMP (format 4).
|
|
66
|
+
/** @type {(cp: number) => number} */
|
|
67
|
+
let cmap = () => 0;
|
|
68
|
+
{
|
|
69
|
+
const base = tables.cmap;
|
|
70
|
+
const count = u16(base + 2);
|
|
71
|
+
let best = -1;
|
|
72
|
+
let bestRank = -1;
|
|
73
|
+
for (let i = 0; i < count; i++) {
|
|
74
|
+
const pid = u16(base + 4 + i * 8);
|
|
75
|
+
const eid = u16(base + 6 + i * 8);
|
|
76
|
+
const off = base + u32(base + 8 + i * 8);
|
|
77
|
+
const fmt = u16(off);
|
|
78
|
+
const rank = fmt === 12 && (pid === 3 || pid === 0) ? 3 : fmt === 4 && ((pid === 3 && (eid === 1 || eid === 10)) || pid === 0) ? 2 : -1;
|
|
79
|
+
if (rank > bestRank) { bestRank = rank; best = off; }
|
|
80
|
+
}
|
|
81
|
+
if (best >= 0 && u16(best) === 4) {
|
|
82
|
+
const segX2 = u16(best + 6);
|
|
83
|
+
const ends = best + 14;
|
|
84
|
+
const starts = ends + segX2 + 2;
|
|
85
|
+
const deltas = starts + segX2;
|
|
86
|
+
const ranges = deltas + segX2;
|
|
87
|
+
cmap = (cp) => {
|
|
88
|
+
if (cp > 0xffff) return 0;
|
|
89
|
+
for (let s = 0; s < segX2; s += 2) {
|
|
90
|
+
if (cp > u16(ends + s)) continue;
|
|
91
|
+
const start = u16(starts + s);
|
|
92
|
+
if (cp < start) return 0;
|
|
93
|
+
const ro = u16(ranges + s);
|
|
94
|
+
if (!ro) return (cp + i16(deltas + s)) & 0xffff;
|
|
95
|
+
const g = u16(ranges + s + ro + (cp - start) * 2);
|
|
96
|
+
return g ? (g + i16(deltas + s)) & 0xffff : 0;
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
};
|
|
100
|
+
} else if (best >= 0) {
|
|
101
|
+
const groups = u32(best + 12);
|
|
102
|
+
cmap = (cp) => {
|
|
103
|
+
let lo = 0;
|
|
104
|
+
let hi = groups - 1;
|
|
105
|
+
while (lo <= hi) {
|
|
106
|
+
const mid = (lo + hi) >> 1;
|
|
107
|
+
const g = best + 16 + mid * 12;
|
|
108
|
+
if (cp < u32(g)) hi = mid - 1;
|
|
109
|
+
else if (cp > u32(g + 4)) lo = mid + 1;
|
|
110
|
+
else return u32(g + 8) + (cp - u32(g));
|
|
111
|
+
}
|
|
112
|
+
return 0;
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const kern = pairKerning(v, tables);
|
|
118
|
+
|
|
119
|
+
/** @param {number} g @returns {[number, number]} */
|
|
120
|
+
const locaRange = (g) => (longLoca
|
|
121
|
+
? [u32(tables.loca + g * 4), u32(tables.loca + g * 4 + 4)]
|
|
122
|
+
: [u16(tables.loca + g * 2) * 2, u16(tables.loca + g * 2 + 2) * 2]);
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* A glyph's contours as points `[x, y, onCurve]`, composites resolved.
|
|
126
|
+
*
|
|
127
|
+
* @param {number} g
|
|
128
|
+
* @param {number} depth
|
|
129
|
+
* @returns {[number, number, boolean][][]}
|
|
130
|
+
*/
|
|
131
|
+
const contours = (g, depth = 0) => {
|
|
132
|
+
if (g >= numGlyphs || depth > 8) return [];
|
|
133
|
+
const [start, end] = locaRange(g);
|
|
134
|
+
if (end <= start) return [];
|
|
135
|
+
const o = tables.glyf + start;
|
|
136
|
+
const nc = i16(o);
|
|
137
|
+
if (nc >= 0) {
|
|
138
|
+
const endPts = [];
|
|
139
|
+
for (let i = 0; i < nc; i++) endPts.push(u16(o + 10 + i * 2));
|
|
140
|
+
const count = nc ? endPts[nc - 1] + 1 : 0;
|
|
141
|
+
let p = o + 10 + nc * 2;
|
|
142
|
+
p += 2 + u16(p);
|
|
143
|
+
const flags = [];
|
|
144
|
+
while (flags.length < count) {
|
|
145
|
+
const f = bytes[p++];
|
|
146
|
+
flags.push(f);
|
|
147
|
+
if (f & 8) { let r = bytes[p++]; while (r--) flags.push(f); }
|
|
148
|
+
}
|
|
149
|
+
const xs = [];
|
|
150
|
+
let x = 0;
|
|
151
|
+
for (const f of flags) {
|
|
152
|
+
if (f & 2) { const d = bytes[p++]; x += f & 16 ? d : -d; } else if (!(f & 16)) { x += i16(p); p += 2; }
|
|
153
|
+
xs.push(x);
|
|
154
|
+
}
|
|
155
|
+
const ys = [];
|
|
156
|
+
let y = 0;
|
|
157
|
+
for (const f of flags) {
|
|
158
|
+
if (f & 4) { const d = bytes[p++]; y += f & 32 ? d : -d; } else if (!(f & 32)) { y += i16(p); p += 2; }
|
|
159
|
+
ys.push(y);
|
|
160
|
+
}
|
|
161
|
+
const out = [];
|
|
162
|
+
let s = 0;
|
|
163
|
+
for (const e of endPts) {
|
|
164
|
+
const c = [];
|
|
165
|
+
for (let i = s; i <= e; i++) c.push(/** @type {[number, number, boolean]} */ ([xs[i], ys[i], Boolean(flags[i] & 1)]));
|
|
166
|
+
out.push(c);
|
|
167
|
+
s = e + 1;
|
|
168
|
+
}
|
|
169
|
+
return out;
|
|
170
|
+
}
|
|
171
|
+
// Composite.
|
|
172
|
+
const out = [];
|
|
173
|
+
let p = o + 10;
|
|
174
|
+
for (;;) {
|
|
175
|
+
const f = u16(p);
|
|
176
|
+
const gi = u16(p + 2);
|
|
177
|
+
p += 4;
|
|
178
|
+
let dx;
|
|
179
|
+
let dy;
|
|
180
|
+
if (f & 1) { dx = f & 2 ? i16(p) : 0; dy = f & 2 ? i16(p + 2) : 0; p += 4; } else { dx = f & 2 ? v.getInt8(p) : 0; dy = f & 2 ? v.getInt8(p + 1) : 0; p += 2; }
|
|
181
|
+
let a = 1; let b = 0; let c = 0; let d = 1;
|
|
182
|
+
const f2 = (/** @type {number} */ q) => i16(q) / 16384;
|
|
183
|
+
if (f & 8) { a = d = f2(p); p += 2; } else if (f & 0x40) { a = f2(p); d = f2(p + 2); p += 4; } else if (f & 0x80) { a = f2(p); b = f2(p + 2); c = f2(p + 4); d = f2(p + 6); p += 8; }
|
|
184
|
+
for (const contour of contours(gi, depth + 1)) {
|
|
185
|
+
out.push(contour.map(([px, py, on]) => /** @type {[number, number, boolean]} */ ([px * a + py * c + dx, px * b + py * d + dy, on])));
|
|
186
|
+
}
|
|
187
|
+
if (!(f & 0x20)) break;
|
|
188
|
+
}
|
|
189
|
+
return out;
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
/** @type {Map<number, string>} */
|
|
193
|
+
const paths = new Map();
|
|
194
|
+
/** @param {number} g @returns {string} */
|
|
195
|
+
const glyphToPath = (g) => {
|
|
196
|
+
let d = paths.get(g);
|
|
197
|
+
if (d !== undefined) return d;
|
|
198
|
+
d = '';
|
|
199
|
+
for (const pts of contours(g)) {
|
|
200
|
+
if (!pts.length) continue;
|
|
201
|
+
// Start on an on-curve point (or the midpoint of two off-curve ones).
|
|
202
|
+
let first = pts.findIndex((q) => q[2]);
|
|
203
|
+
/** @type {[number, number]} */
|
|
204
|
+
let start;
|
|
205
|
+
if (first < 0) { start = [(pts[0][0] + pts[1 % pts.length][0]) / 2, (pts[0][1] + pts[1 % pts.length][1]) / 2]; first = 0; } else start = [pts[first][0], pts[first][1]];
|
|
206
|
+
d += `M${start[0]},${start[1]}`;
|
|
207
|
+
const n = pts.length;
|
|
208
|
+
/** @type {[number, number] | null} */
|
|
209
|
+
let ctrl = null;
|
|
210
|
+
for (let k = 1; k <= n; k++) {
|
|
211
|
+
const q = pts[(first + k) % n];
|
|
212
|
+
if (q[2]) {
|
|
213
|
+
d += ctrl ? `Q${ctrl[0]},${ctrl[1]} ${q[0]},${q[1]}` : `L${q[0]},${q[1]}`;
|
|
214
|
+
ctrl = null;
|
|
215
|
+
} else {
|
|
216
|
+
if (ctrl) {
|
|
217
|
+
const mx = (ctrl[0] + q[0]) / 2;
|
|
218
|
+
const my = (ctrl[1] + q[1]) / 2;
|
|
219
|
+
d += `Q${ctrl[0]},${ctrl[1]} ${mx},${my}`;
|
|
220
|
+
}
|
|
221
|
+
ctrl = [q[0], q[1]];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (ctrl) d += `Q${ctrl[0]},${ctrl[1]} ${start[0]},${start[1]}`;
|
|
225
|
+
d += 'Z';
|
|
226
|
+
}
|
|
227
|
+
paths.set(g, d);
|
|
228
|
+
return d;
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
/** @param {string} text */
|
|
232
|
+
const shape = (text) => {
|
|
233
|
+
const out = [];
|
|
234
|
+
let i = 0;
|
|
235
|
+
for (const ch of text) {
|
|
236
|
+
out.push({ gid: cmap(/** @type {number} */ (ch.codePointAt(0))), cluster: i, advance: 0 });
|
|
237
|
+
i += ch.length;
|
|
238
|
+
}
|
|
239
|
+
for (let k = 0; k < out.length; k++) out[k].advance = advance(out[k].gid) + (k + 1 < out.length ? kern(out[k].gid, out[k + 1].gid) : 0);
|
|
240
|
+
return out;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
/** @type {Record<string, { min: number, default: number, max: number }>} */
|
|
244
|
+
const axes = {};
|
|
245
|
+
if (tables.fvar != null) {
|
|
246
|
+
const f = tables.fvar;
|
|
247
|
+
const off = u16(f + 4);
|
|
248
|
+
const count = u16(f + 8);
|
|
249
|
+
const size = u16(f + 10);
|
|
250
|
+
const fixed = (/** @type {number} */ q) => v.getInt32(q) / 65536;
|
|
251
|
+
for (let k = 0; k < count; k++) {
|
|
252
|
+
const a = f + off + k * size;
|
|
253
|
+
axes[String.fromCharCode(bytes[a], bytes[a + 1], bytes[a + 2], bytes[a + 3])] = { min: fixed(a + 4), default: fixed(a + 8), max: fixed(a + 12) };
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return { kind: 'js', upem, ascender, descender, capHeight, glyphToPath, shape, axes, family: familyName(v, bytes, tables.name) };
|
|
258
|
+
}
|