freshcoat 0.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/LICENSE +202 -0
- package/NOTICE +17 -0
- package/README.md +110 -0
- package/package.json +53 -0
- package/src/adjust.d.ts +18 -0
- package/src/adjust.js +153 -0
- package/src/approx-layout.d.ts +2 -0
- package/src/approx-layout.js +91 -0
- package/src/bake-text.d.ts +12 -0
- package/src/bake-text.js +321 -0
- package/src/browser.d.ts +7 -0
- package/src/browser.js +79 -0
- package/src/canvaskit.d.ts +4 -0
- package/src/canvaskit.js +1674 -0
- package/src/compile-scene.d.ts +24 -0
- package/src/compile-scene.js +325 -0
- package/src/css.d.ts +5 -0
- package/src/css.js +153 -0
- package/src/decode.d.ts +22 -0
- package/src/decode.js +112 -0
- package/src/export-scale.d.ts +25 -0
- package/src/export-scale.js +74 -0
- package/src/font-bytes.d.ts +4 -0
- package/src/font-bytes.js +165 -0
- package/src/font-metrics.d.ts +5 -0
- package/src/font-metrics.js +62 -0
- package/src/headless.d.ts +24 -0
- package/src/headless.js +58 -0
- package/src/index.d.ts +25 -0
- package/src/index.js +29 -0
- package/src/jpeg.d.ts +5 -0
- package/src/jpeg.js +20 -0
- package/src/line-height.d.ts +7 -0
- package/src/line-height.js +80 -0
- package/src/node.d.ts +139 -0
- package/src/node.js +47 -0
- package/src/paint-cache.d.ts +49 -0
- package/src/paint-cache.js +109 -0
- package/src/paint-helpers.d.ts +21 -0
- package/src/paint-helpers.js +71 -0
- package/src/paragraph-layout.d.ts +6 -0
- package/src/paragraph-layout.js +283 -0
- package/src/path-data.d.ts +5 -0
- package/src/path-data.js +101 -0
- package/src/png.d.ts +23 -0
- package/src/png.js +189 -0
- package/src/resolve-layout.d.ts +9 -0
- package/src/resolve-layout.js +657 -0
- package/src/runtime.d.ts +6 -0
- package/src/runtime.js +44 -0
- package/src/squircle.d.ts +1 -0
- package/src/squircle.js +60 -0
- package/src/text-cache.d.ts +14 -0
- package/src/text-cache.js +46 -0
- package/src/text-engine.d.ts +26 -0
- package/src/text-engine.js +1 -0
- package/src/text-types.d.ts +30 -0
- package/src/text-types.js +1 -0
- package/src/types.d.ts +406 -0
- package/src/types.js +13 -0
- package/src/validate-commands.d.ts +8 -0
- package/src/validate-commands.js +206 -0
package/src/runtime.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { paintScene } from "./canvaskit.js";
|
|
2
|
+
// The "env owns disposal" seam: a server env encodes+disposes the output (PNG
|
|
3
|
+
// bytes), a browser env hands back the live canvas untouched.
|
|
4
|
+
export async function applyDisposePolicy(output, policy, encode) {
|
|
5
|
+
if (policy === "encode") {
|
|
6
|
+
// The format comes back from the encoder rather than being echoed from the
|
|
7
|
+
// request: a build without the WebP encoder answers in PNG, and the caller
|
|
8
|
+
// has to know which it got before it labels it.
|
|
9
|
+
const { bytes, format } = await output.encode(encode);
|
|
10
|
+
output.dispose();
|
|
11
|
+
return { bytes, format, warnings: output.warnings };
|
|
12
|
+
}
|
|
13
|
+
// "keep" hands back the live surface, which only a raster backend has. A
|
|
14
|
+
// document backend under this policy is a misconfigured runtime rather than a
|
|
15
|
+
// render that can degrade, so it says so here instead of returning a result
|
|
16
|
+
// whose `canvas` is undefined.
|
|
17
|
+
if (!output.canvas)
|
|
18
|
+
throw new Error('paint runtime policy "keep" requires a backend with a canvas');
|
|
19
|
+
return {
|
|
20
|
+
canvas: output.canvas,
|
|
21
|
+
warnings: output.warnings,
|
|
22
|
+
dispose: output.dispose,
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
// The in-tree Painter: CanvasKit-WASM with its instance bound. A second backend
|
|
26
|
+
// supplies its own and is otherwise indistinguishable to the runtime.
|
|
27
|
+
export const paintCanvasKit = (ck) => (commands, rt) => paintScene(ck, commands, rt);
|
|
28
|
+
// Assemble a runtime from its I/O + canvas parts, a disposal policy and a
|
|
29
|
+
// backend. paint() runs the Painter for one scene and applies the policy;
|
|
30
|
+
// omitting the Painter selects CanvasKit, which is what every caller in the tree
|
|
31
|
+
// does.
|
|
32
|
+
export function makeRuntime(base, policy, encode, painter) {
|
|
33
|
+
const rt = {
|
|
34
|
+
...base,
|
|
35
|
+
async paint(commands, ck) {
|
|
36
|
+
// Without a painter the backend is CanvasKit and `ck` is its instance,
|
|
37
|
+
// which is every caller in the tree. With one, the backend was chosen at
|
|
38
|
+
// construction and `ck` is not consulted.
|
|
39
|
+
const paint = painter ?? paintCanvasKit(ck);
|
|
40
|
+
return applyDisposePolicy(await paint(commands, rt), policy, encode);
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
return rt;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function squircleSvg(x: number, y: number, w: number, h: number, radius: number, smoothing: number): string;
|
package/src/squircle.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
// Figma corner smoothing ("squircle") — an SVG path for a rounded rect whose
|
|
2
|
+
// corners blend from a plain circular arc (smoothing 0) to a
|
|
3
|
+
// continuous-curvature superellipse (smoothing → 1), matching Figma's
|
|
4
|
+
// corner-smoothing control. Ported from figma-squircle (tienphaw/figma-squircle,
|
|
5
|
+
// MIT), specialized to a uniform radius + smoothing.
|
|
6
|
+
function toRadians(deg) {
|
|
7
|
+
return (deg * Math.PI) / 180;
|
|
8
|
+
}
|
|
9
|
+
function n(v) {
|
|
10
|
+
return v.toFixed(4);
|
|
11
|
+
}
|
|
12
|
+
function getPathParamsForCorner(cornerRadius, cornerSmoothing, budget) {
|
|
13
|
+
let p = (1 + cornerSmoothing) * cornerRadius;
|
|
14
|
+
const maxCornerSmoothing = budget / cornerRadius - 1;
|
|
15
|
+
const smoothing = Math.min(cornerSmoothing, maxCornerSmoothing);
|
|
16
|
+
p = Math.min(p, budget);
|
|
17
|
+
const arcMeasure = 90 * (1 - smoothing);
|
|
18
|
+
const arcSectionLength = Math.sin(toRadians(arcMeasure / 2)) * cornerRadius * Math.SQRT2;
|
|
19
|
+
const angleAlpha = (90 - arcMeasure) / 2;
|
|
20
|
+
const p3ToP4Distance = cornerRadius * Math.tan(toRadians(angleAlpha / 2));
|
|
21
|
+
const angleBeta = 45 * smoothing;
|
|
22
|
+
const c = p3ToP4Distance * Math.cos(toRadians(angleBeta));
|
|
23
|
+
const d = c * Math.tan(toRadians(angleBeta));
|
|
24
|
+
const b = (p - arcSectionLength - c - d) / 3;
|
|
25
|
+
const a = 2 * b;
|
|
26
|
+
return { a, b, c, d, p, arcSectionLength, cornerRadius };
|
|
27
|
+
}
|
|
28
|
+
function topRight({ cornerRadius: r, a, b, c, d, arcSectionLength: arc, }) {
|
|
29
|
+
return `c ${n(a)} 0 ${n(a + b)} 0 ${n(a + b + c)} ${n(d)} a ${n(r)} ${n(r)} 0 0 1 ${n(arc)} ${n(arc)} c ${n(d)} ${n(c)} ${n(d)} ${n(b + c)} ${n(d)} ${n(a + b + c)}`;
|
|
30
|
+
}
|
|
31
|
+
function bottomRight({ cornerRadius: r, a, b, c, d, arcSectionLength: arc, }) {
|
|
32
|
+
return `c 0 ${n(a)} 0 ${n(a + b)} ${n(-d)} ${n(a + b + c)} a ${n(r)} ${n(r)} 0 0 1 ${n(-arc)} ${n(arc)} c ${n(-c)} ${n(d)} ${n(-(b + c))} ${n(d)} ${n(-(a + b + c))} ${n(d)}`;
|
|
33
|
+
}
|
|
34
|
+
function bottomLeft({ cornerRadius: r, a, b, c, d, arcSectionLength: arc, }) {
|
|
35
|
+
return `c ${n(-a)} 0 ${n(-(a + b))} 0 ${n(-(a + b + c))} ${n(-d)} a ${n(r)} ${n(r)} 0 0 1 ${n(-arc)} ${n(-arc)} c ${n(-d)} ${n(-c)} ${n(-d)} ${n(-(b + c))} ${n(-d)} ${n(-(a + b + c))}`;
|
|
36
|
+
}
|
|
37
|
+
function topLeft({ cornerRadius: r, a, b, c, d, arcSectionLength: arc, }) {
|
|
38
|
+
return `c 0 ${n(-a)} 0 ${n(-(a + b))} ${n(d)} ${n(-(a + b + c))} a ${n(r)} ${n(r)} 0 0 1 ${n(arc)} ${n(-arc)} c ${n(c)} ${n(-d)} ${n(b + c)} ${n(-d)} ${n(a + b + c)} ${n(-d)}`;
|
|
39
|
+
}
|
|
40
|
+
// SVG path for a smoothed rounded rect at (x, y) sized w×h with a uniform radius
|
|
41
|
+
// and smoothing in [0, 1]. Corners are identical, so one param set drives all four.
|
|
42
|
+
export function squircleSvg(x, y, w, h, radius, smoothing) {
|
|
43
|
+
const budget = Math.min(w, h) / 2;
|
|
44
|
+
const r = Math.min(radius, budget);
|
|
45
|
+
if (r <= 0)
|
|
46
|
+
return `M ${n(x)} ${n(y)} H ${n(x + w)} V ${n(y + h)} H ${n(x)} Z`;
|
|
47
|
+
const cp = getPathParamsForCorner(r, smoothing, budget);
|
|
48
|
+
const p = cp.p;
|
|
49
|
+
return [
|
|
50
|
+
`M ${n(x + w - p)} ${n(y)}`,
|
|
51
|
+
topRight(cp),
|
|
52
|
+
`L ${n(x + w)} ${n(y + h - p)}`,
|
|
53
|
+
bottomRight(cp),
|
|
54
|
+
`L ${n(x + p)} ${n(y + h)}`,
|
|
55
|
+
bottomLeft(cp),
|
|
56
|
+
`L ${n(x)} ${n(y + p)}`,
|
|
57
|
+
topLeft(cp),
|
|
58
|
+
"Z",
|
|
59
|
+
].join(" ");
|
|
60
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { TextEngine } from "./text-engine.js";
|
|
2
|
+
export type TextEngineCacheStats = {
|
|
3
|
+
hits: number;
|
|
4
|
+
misses: number;
|
|
5
|
+
size: number;
|
|
6
|
+
};
|
|
7
|
+
export type CachedTextEngine<E extends TextEngine> = E & {
|
|
8
|
+
cacheStats(): TextEngineCacheStats;
|
|
9
|
+
};
|
|
10
|
+
/** Wraps `engine` with a least-recently-used cache of `maxEntries` results per
|
|
11
|
+
* method. The wrapped engine must not change the faces it shapes with. */
|
|
12
|
+
export declare function memoizeTextEngine<E extends TextEngine>(engine: E, opts?: {
|
|
13
|
+
maxEntries?: number;
|
|
14
|
+
}): CachedTextEngine<E>;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Wraps `engine` with a least-recently-used cache of `maxEntries` results per
|
|
2
|
+
* method. The wrapped engine must not change the faces it shapes with. */
|
|
3
|
+
export function memoizeTextEngine(engine, opts = {}) {
|
|
4
|
+
const max = opts.maxEntries ?? 4000;
|
|
5
|
+
const stats = { hits: 0, misses: 0 };
|
|
6
|
+
const caches = [];
|
|
7
|
+
const memo = (fn) => {
|
|
8
|
+
const cache = new Map();
|
|
9
|
+
caches.push(cache);
|
|
10
|
+
return (...args) => {
|
|
11
|
+
const key = JSON.stringify(args);
|
|
12
|
+
const hit = cache.get(key);
|
|
13
|
+
if (hit !== undefined) {
|
|
14
|
+
stats.hits++;
|
|
15
|
+
cache.delete(key);
|
|
16
|
+
cache.set(key, hit);
|
|
17
|
+
return hit;
|
|
18
|
+
}
|
|
19
|
+
stats.misses++;
|
|
20
|
+
const value = deepFreeze(fn(...args));
|
|
21
|
+
cache.set(key, value);
|
|
22
|
+
if (cache.size > max)
|
|
23
|
+
cache.delete(cache.keys().next().value);
|
|
24
|
+
return value;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
...engine,
|
|
29
|
+
measureText: memo(engine.measureText),
|
|
30
|
+
measureSpanWidth: memo(engine.measureSpanWidth),
|
|
31
|
+
layoutText: memo(engine.layoutText),
|
|
32
|
+
...(engine.layoutInline ? { layoutInline: memo(engine.layoutInline) } : {}),
|
|
33
|
+
cacheStats: () => ({
|
|
34
|
+
...stats,
|
|
35
|
+
size: caches.reduce((n, c) => n + c.size, 0),
|
|
36
|
+
}),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
function deepFreeze(value) {
|
|
40
|
+
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
|
41
|
+
Object.freeze(value);
|
|
42
|
+
for (const v of Object.values(value))
|
|
43
|
+
deepFreeze(v);
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { MeasureText, TextLayout, TextLayoutInput } from "./text-types.js";
|
|
2
|
+
import type { FontVMetrics } from "./types.js";
|
|
3
|
+
export type SpanFont = TextLayoutInput["font"];
|
|
4
|
+
export type InlineSpan = {
|
|
5
|
+
text: string;
|
|
6
|
+
font: SpanFont;
|
|
7
|
+
};
|
|
8
|
+
export type InlineFragment = {
|
|
9
|
+
spanIndex: number;
|
|
10
|
+
text: string;
|
|
11
|
+
x: number;
|
|
12
|
+
width: number;
|
|
13
|
+
};
|
|
14
|
+
export type InlineShapedLine = {
|
|
15
|
+
fragments: InlineFragment[];
|
|
16
|
+
width: number;
|
|
17
|
+
};
|
|
18
|
+
export type TextEngine = {
|
|
19
|
+
measureText: MeasureText;
|
|
20
|
+
measureSpanWidth: (text: string, font: SpanFont) => number;
|
|
21
|
+
layoutText: (input: TextLayoutInput) => TextLayout;
|
|
22
|
+
layoutInline?: (spans: InlineSpan[], maxWidth: number) => {
|
|
23
|
+
lines: InlineShapedLine[];
|
|
24
|
+
};
|
|
25
|
+
metricsFor?: (family: string) => FontVMetrics | undefined;
|
|
26
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { FontVariations, ResolvedFont } from "./types.js";
|
|
2
|
+
export type MeasureText = (text: string, font: ResolvedFont, maxWidth: number | null) => {
|
|
3
|
+
width: number;
|
|
4
|
+
height: number;
|
|
5
|
+
};
|
|
6
|
+
export type TextLayoutInput = {
|
|
7
|
+
value: string;
|
|
8
|
+
font: {
|
|
9
|
+
family: string;
|
|
10
|
+
size: number;
|
|
11
|
+
weight?: number;
|
|
12
|
+
style?: string;
|
|
13
|
+
letterSpacing?: number;
|
|
14
|
+
variations?: FontVariations;
|
|
15
|
+
};
|
|
16
|
+
maxWidth: number;
|
|
17
|
+
maxHeight: number;
|
|
18
|
+
lineHeight: number;
|
|
19
|
+
fit: "shrink" | "clip" | undefined;
|
|
20
|
+
};
|
|
21
|
+
export type MeasuredLine = {
|
|
22
|
+
text: string;
|
|
23
|
+
width: number;
|
|
24
|
+
};
|
|
25
|
+
export type TextLayout = {
|
|
26
|
+
lines: MeasuredLine[];
|
|
27
|
+
totalHeight: number;
|
|
28
|
+
effectiveFontSize: number;
|
|
29
|
+
shrinkApplied: boolean;
|
|
30
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/src/types.d.ts
ADDED
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import type { DecodedPixels } from "./decode.js";
|
|
2
|
+
import type { PaintCache } from "./paint-cache.js";
|
|
3
|
+
import type { EncodeFormat, EncodeOptions } from "./png.js";
|
|
4
|
+
export type Vec2 = {
|
|
5
|
+
x: number;
|
|
6
|
+
y: number;
|
|
7
|
+
};
|
|
8
|
+
export type Size = {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
export type BlendMode = "normal" | "multiply" | "screen" | "overlay" | "darken" | "lighten" | "color-dodge" | "color-burn" | "hard-light" | "soft-light" | "difference" | "exclusion" | "hue" | "saturation" | "color" | "luminosity" | "plus";
|
|
13
|
+
export type FillRule = "nonzero" | "evenodd";
|
|
14
|
+
export type FontWeight = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
|
|
15
|
+
export type FontVariations = Record<string, number>;
|
|
16
|
+
export type FontFile = {
|
|
17
|
+
weight: FontWeight;
|
|
18
|
+
style?: "normal" | "italic";
|
|
19
|
+
src: string;
|
|
20
|
+
};
|
|
21
|
+
export type FontDescriptor = {
|
|
22
|
+
kind: "google";
|
|
23
|
+
family: string;
|
|
24
|
+
url: string;
|
|
25
|
+
} | {
|
|
26
|
+
kind: "fontsource";
|
|
27
|
+
family: string;
|
|
28
|
+
url: string;
|
|
29
|
+
} | {
|
|
30
|
+
kind: "local";
|
|
31
|
+
family: string;
|
|
32
|
+
files: FontFile[];
|
|
33
|
+
};
|
|
34
|
+
export type GradientStop = {
|
|
35
|
+
offset: number;
|
|
36
|
+
color: string;
|
|
37
|
+
};
|
|
38
|
+
export type FontRequest = {
|
|
39
|
+
family: string;
|
|
40
|
+
} | {
|
|
41
|
+
family: string;
|
|
42
|
+
descriptor: FontDescriptor;
|
|
43
|
+
};
|
|
44
|
+
export type Shadow = {
|
|
45
|
+
color: string;
|
|
46
|
+
dx: number;
|
|
47
|
+
dy: number;
|
|
48
|
+
blur: number;
|
|
49
|
+
/** Grows the shadow's silhouette before it is blurred (negative shrinks it),
|
|
50
|
+
* matching Figma's "Spread" and CSS's third box-shadow length. */
|
|
51
|
+
spread?: number;
|
|
52
|
+
/** Draw the shadow INSIDE the drawable instead of behind it — Figma's inner
|
|
53
|
+
* shadow, CSS's `inset`. The silhouette is inverted, so the shadow falls
|
|
54
|
+
* from the shape's own edges inward. */
|
|
55
|
+
inset?: boolean;
|
|
56
|
+
};
|
|
57
|
+
/** One shadow, or a stack of them painted bottom-up: the LAST entry is nearest
|
|
58
|
+
* the viewer, mirroring how a fills array stacks. Figma lists effects
|
|
59
|
+
* top-first, so a producer reverses them on the way in. */
|
|
60
|
+
export type Shadows = Shadow | Shadow[];
|
|
61
|
+
export type ClipOutset = {
|
|
62
|
+
top: number;
|
|
63
|
+
bottom: number;
|
|
64
|
+
};
|
|
65
|
+
export type AdjustLut = {
|
|
66
|
+
r: Uint8Array;
|
|
67
|
+
g: Uint8Array;
|
|
68
|
+
b: Uint8Array;
|
|
69
|
+
};
|
|
70
|
+
export type AdjustLut3d = {
|
|
71
|
+
size: number;
|
|
72
|
+
data: Uint8Array;
|
|
73
|
+
};
|
|
74
|
+
export type Adjust = {
|
|
75
|
+
colorMatrix?: number[];
|
|
76
|
+
lut?: AdjustLut;
|
|
77
|
+
lut3d?: AdjustLut3d;
|
|
78
|
+
sharpen?: number;
|
|
79
|
+
gamut?: "clip" | "preserve-hue";
|
|
80
|
+
};
|
|
81
|
+
export type ShapeMask = {
|
|
82
|
+
kind: "rect";
|
|
83
|
+
outset?: ClipOutset;
|
|
84
|
+
} | {
|
|
85
|
+
kind: "rounded-rect";
|
|
86
|
+
radius: number;
|
|
87
|
+
} | {
|
|
88
|
+
kind: "circle";
|
|
89
|
+
} | {
|
|
90
|
+
kind: "ellipse";
|
|
91
|
+
} | {
|
|
92
|
+
kind: "polygon";
|
|
93
|
+
sides: number;
|
|
94
|
+
rotation?: number;
|
|
95
|
+
} | {
|
|
96
|
+
kind: "squircle";
|
|
97
|
+
radius: number;
|
|
98
|
+
};
|
|
99
|
+
export type CornerRadius = number | [number, number, number, number];
|
|
100
|
+
export type ViewBox = {
|
|
101
|
+
x?: number;
|
|
102
|
+
y?: number;
|
|
103
|
+
width: number;
|
|
104
|
+
height: number;
|
|
105
|
+
};
|
|
106
|
+
export type Stroke = {
|
|
107
|
+
color: string;
|
|
108
|
+
width: number;
|
|
109
|
+
dash?: number[];
|
|
110
|
+
cap?: "butt" | "round" | "square";
|
|
111
|
+
join?: "round" | "bevel" | "miter";
|
|
112
|
+
align?: "inside" | "outside" | "center";
|
|
113
|
+
};
|
|
114
|
+
export type ResolvedFill = {
|
|
115
|
+
kind: "solid";
|
|
116
|
+
color: string;
|
|
117
|
+
} | {
|
|
118
|
+
kind: "linear";
|
|
119
|
+
stops: GradientStop[];
|
|
120
|
+
from: Vec2;
|
|
121
|
+
to: Vec2;
|
|
122
|
+
} | {
|
|
123
|
+
kind: "radial";
|
|
124
|
+
stops: GradientStop[];
|
|
125
|
+
center: Vec2;
|
|
126
|
+
radius: number;
|
|
127
|
+
radiusY?: number;
|
|
128
|
+
rotation?: number;
|
|
129
|
+
} | {
|
|
130
|
+
kind: "angular";
|
|
131
|
+
stops: GradientStop[];
|
|
132
|
+
center: Vec2;
|
|
133
|
+
rotation: number;
|
|
134
|
+
};
|
|
135
|
+
export type ResolvedFont = {
|
|
136
|
+
family: string;
|
|
137
|
+
weight: number;
|
|
138
|
+
style: "normal" | "italic";
|
|
139
|
+
size: number;
|
|
140
|
+
letterSpacing?: number;
|
|
141
|
+
lineHeight: number;
|
|
142
|
+
autoLineHeight?: boolean;
|
|
143
|
+
decoration?: "underline" | "line-through";
|
|
144
|
+
variations?: FontVariations;
|
|
145
|
+
};
|
|
146
|
+
export type BakedTextLayout = {
|
|
147
|
+
font: ResolvedFont;
|
|
148
|
+
lines: TextLine[];
|
|
149
|
+
totalHeight: number;
|
|
150
|
+
shrinkApplied: boolean;
|
|
151
|
+
};
|
|
152
|
+
export type TextLine = {
|
|
153
|
+
text: string;
|
|
154
|
+
y: number;
|
|
155
|
+
baseline?: number;
|
|
156
|
+
spans: TextLineSpan[];
|
|
157
|
+
};
|
|
158
|
+
export type TextLineSpan = {
|
|
159
|
+
text: string;
|
|
160
|
+
x: number;
|
|
161
|
+
width: number;
|
|
162
|
+
font: ResolvedFont;
|
|
163
|
+
color: string;
|
|
164
|
+
};
|
|
165
|
+
export type SetupCommand = {
|
|
166
|
+
op: "createCanvas";
|
|
167
|
+
width: number;
|
|
168
|
+
height: number;
|
|
169
|
+
scale?: number;
|
|
170
|
+
supersample?: number;
|
|
171
|
+
} | {
|
|
172
|
+
op: "loadFonts";
|
|
173
|
+
requests: FontRequest[];
|
|
174
|
+
} | {
|
|
175
|
+
op: "loadImages";
|
|
176
|
+
srcs: string[];
|
|
177
|
+
};
|
|
178
|
+
export type FrameDither = number | {
|
|
179
|
+
amount: number;
|
|
180
|
+
seed?: number;
|
|
181
|
+
mode?: "per-channel" | "monochrome";
|
|
182
|
+
};
|
|
183
|
+
export type FrameFinish = {
|
|
184
|
+
whiteClamp?: number;
|
|
185
|
+
blackExtract?: number;
|
|
186
|
+
dither?: FrameDither;
|
|
187
|
+
};
|
|
188
|
+
export type FinishCommand = {
|
|
189
|
+
op: "finishFrame";
|
|
190
|
+
finish: FrameFinish;
|
|
191
|
+
};
|
|
192
|
+
export type DrawCommandBase = {
|
|
193
|
+
id?: string;
|
|
194
|
+
pos: Vec2;
|
|
195
|
+
size: Size;
|
|
196
|
+
rotation?: number;
|
|
197
|
+
opacity?: number;
|
|
198
|
+
blendMode?: BlendMode;
|
|
199
|
+
clip?: ShapeMask;
|
|
200
|
+
shadow?: Shadows;
|
|
201
|
+
blur?: number;
|
|
202
|
+
adjust?: Adjust;
|
|
203
|
+
};
|
|
204
|
+
export type DrawTextCommand = DrawCommandBase & {
|
|
205
|
+
op: "drawText";
|
|
206
|
+
layout: BakedTextLayout;
|
|
207
|
+
color: string;
|
|
208
|
+
fill?: ResolvedFill;
|
|
209
|
+
};
|
|
210
|
+
export type DrawImageCommand = DrawCommandBase & {
|
|
211
|
+
op: "drawImage";
|
|
212
|
+
src: string;
|
|
213
|
+
fit: "cover" | "contain" | "fill" | "tile";
|
|
214
|
+
stroke?: Stroke;
|
|
215
|
+
};
|
|
216
|
+
export type DrawBitmapCommand = DrawCommandBase & {
|
|
217
|
+
op: "drawBitmap";
|
|
218
|
+
pixels: Uint8Array;
|
|
219
|
+
pixelWidth: number;
|
|
220
|
+
pixelHeight: number;
|
|
221
|
+
role?: BitmapRole;
|
|
222
|
+
};
|
|
223
|
+
export type BitmapRole = "barcode";
|
|
224
|
+
export type DrawQrCommand = DrawCommandBase & {
|
|
225
|
+
op: "drawQr";
|
|
226
|
+
modules: boolean[][];
|
|
227
|
+
foreground: string;
|
|
228
|
+
background?: string;
|
|
229
|
+
margin?: number;
|
|
230
|
+
};
|
|
231
|
+
export type DrawRectCommand = DrawCommandBase & {
|
|
232
|
+
op: "drawRect";
|
|
233
|
+
fills?: ResolvedFill[];
|
|
234
|
+
stroke?: Stroke;
|
|
235
|
+
cornerRadius?: CornerRadius;
|
|
236
|
+
cornerSmoothing?: number;
|
|
237
|
+
};
|
|
238
|
+
export type DrawPathCommand = DrawCommandBase & {
|
|
239
|
+
op: "drawPath";
|
|
240
|
+
d: string;
|
|
241
|
+
fills?: ResolvedFill[];
|
|
242
|
+
stroke?: Stroke;
|
|
243
|
+
viewBox?: ViewBox;
|
|
244
|
+
fillRule?: FillRule;
|
|
245
|
+
};
|
|
246
|
+
export type DrawGroupCommand = DrawCommandBase & {
|
|
247
|
+
op: "drawGroup";
|
|
248
|
+
children: DrawCommand[];
|
|
249
|
+
};
|
|
250
|
+
export type DrawMaskedCommand = DrawCommandBase & {
|
|
251
|
+
op: "drawMasked";
|
|
252
|
+
mask: DrawCommand;
|
|
253
|
+
children: DrawCommand[];
|
|
254
|
+
channel?: "alpha" | "luminance";
|
|
255
|
+
invert?: boolean;
|
|
256
|
+
};
|
|
257
|
+
export type DrawCommand = DrawTextCommand | DrawImageCommand | DrawBitmapCommand | DrawQrCommand | DrawRectCommand | DrawPathCommand | DrawGroupCommand | DrawMaskedCommand;
|
|
258
|
+
export type Command = SetupCommand | DrawCommand | FinishCommand;
|
|
259
|
+
export type FontVMetrics = {
|
|
260
|
+
ascent: number;
|
|
261
|
+
descent: number;
|
|
262
|
+
lineGap: number;
|
|
263
|
+
capHeight: number;
|
|
264
|
+
};
|
|
265
|
+
export type CanvasLike = {
|
|
266
|
+
width: number;
|
|
267
|
+
height: number;
|
|
268
|
+
getContext(type: "2d"): CanvasRenderingContext2DLike | null;
|
|
269
|
+
};
|
|
270
|
+
export type CanvasGradientLike = {
|
|
271
|
+
addColorStop(offset: number, color: string): void;
|
|
272
|
+
};
|
|
273
|
+
export type CanvasPatternLike = {
|
|
274
|
+
readonly __canvasPattern?: never;
|
|
275
|
+
};
|
|
276
|
+
export type LineCap = "butt" | "round" | "square";
|
|
277
|
+
export type LineJoin = "round" | "bevel" | "miter";
|
|
278
|
+
export type TextAlign = "start" | "end" | "left" | "right" | "center";
|
|
279
|
+
export type TextBaseline = "top" | "hanging" | "middle" | "alphabetic" | "ideographic" | "bottom";
|
|
280
|
+
export type CanvasRenderingContext2DLike = {
|
|
281
|
+
fillStyle: string | CanvasGradientLike | CanvasPatternLike;
|
|
282
|
+
strokeStyle: string;
|
|
283
|
+
lineWidth: number;
|
|
284
|
+
lineCap: LineCap;
|
|
285
|
+
lineJoin: LineJoin;
|
|
286
|
+
globalAlpha: number;
|
|
287
|
+
globalCompositeOperation: string;
|
|
288
|
+
font: string;
|
|
289
|
+
textAlign: TextAlign;
|
|
290
|
+
textBaseline: TextBaseline;
|
|
291
|
+
shadowColor: string;
|
|
292
|
+
shadowBlur: number;
|
|
293
|
+
shadowOffsetX: number;
|
|
294
|
+
shadowOffsetY: number;
|
|
295
|
+
filter: string;
|
|
296
|
+
imageSmoothingEnabled?: boolean;
|
|
297
|
+
imageSmoothingQuality?: "low" | "medium" | "high";
|
|
298
|
+
save(): void;
|
|
299
|
+
restore(): void;
|
|
300
|
+
translate(x: number, y: number): void;
|
|
301
|
+
rotate(angle: number): void;
|
|
302
|
+
fillRect(x: number, y: number, w: number, h: number): void;
|
|
303
|
+
strokeRect(x: number, y: number, w: number, h: number): void;
|
|
304
|
+
beginPath(): void;
|
|
305
|
+
closePath(): void;
|
|
306
|
+
rect(x: number, y: number, w: number, h: number): void;
|
|
307
|
+
fill(path?: unknown): void;
|
|
308
|
+
stroke(path?: unknown): void;
|
|
309
|
+
clip(): void;
|
|
310
|
+
measureText(text: string): {
|
|
311
|
+
width: number;
|
|
312
|
+
};
|
|
313
|
+
fillText(text: string, x: number, y: number): void;
|
|
314
|
+
drawImage(image: unknown, dx: number, dy: number, dw: number, dh: number): void;
|
|
315
|
+
drawImage(image: unknown, sx: number, sy: number, sw: number, sh: number, dx: number, dy: number, dw: number, dh: number): void;
|
|
316
|
+
roundRect(x: number, y: number, w: number, h: number, radius: number | number[]): void;
|
|
317
|
+
setLineDash(segments: number[]): void;
|
|
318
|
+
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradientLike;
|
|
319
|
+
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradientLike;
|
|
320
|
+
createConicGradient(startAngle: number, x: number, y: number): CanvasGradientLike;
|
|
321
|
+
createPattern(image: unknown, repetition: "repeat" | "repeat-x" | "repeat-y" | "no-repeat"): CanvasPatternLike | null;
|
|
322
|
+
};
|
|
323
|
+
export type ImageLike = {
|
|
324
|
+
width: number;
|
|
325
|
+
height: number;
|
|
326
|
+
};
|
|
327
|
+
export type PaintWarning = {
|
|
328
|
+
kind: "image_load_failed";
|
|
329
|
+
src: string;
|
|
330
|
+
error: string;
|
|
331
|
+
} | {
|
|
332
|
+
kind: "font_load_failed";
|
|
333
|
+
family: string;
|
|
334
|
+
error: string;
|
|
335
|
+
} | {
|
|
336
|
+
kind: "qr_generate_failed";
|
|
337
|
+
value: string;
|
|
338
|
+
error: string;
|
|
339
|
+
} | {
|
|
340
|
+
kind: "barcode_unavailable";
|
|
341
|
+
symbology: string;
|
|
342
|
+
layer?: string;
|
|
343
|
+
} | {
|
|
344
|
+
kind: "barcode_invalid";
|
|
345
|
+
symbology: string;
|
|
346
|
+
value: string;
|
|
347
|
+
message: string;
|
|
348
|
+
layer?: string;
|
|
349
|
+
} | {
|
|
350
|
+
kind: "unhandled_op";
|
|
351
|
+
op: string;
|
|
352
|
+
} | {
|
|
353
|
+
kind: "adjust_unsupported";
|
|
354
|
+
component: "lut" | "lut3d" | "sharpen" | "gamut";
|
|
355
|
+
layer?: string;
|
|
356
|
+
} | {
|
|
357
|
+
kind: "gamut_compressed";
|
|
358
|
+
layer?: string;
|
|
359
|
+
clipped: number;
|
|
360
|
+
pullback: number;
|
|
361
|
+
};
|
|
362
|
+
export type PaintOutput = {
|
|
363
|
+
canvas?: CanvasLike;
|
|
364
|
+
warnings: PaintWarning[];
|
|
365
|
+
readPixels?(): DecodedPixels | null;
|
|
366
|
+
encode(opts?: EncodeOptions): Promise<{
|
|
367
|
+
bytes: Uint8Array;
|
|
368
|
+
format: EncodeFormat;
|
|
369
|
+
}>;
|
|
370
|
+
dispose(): void;
|
|
371
|
+
};
|
|
372
|
+
export type CanvasHost = {
|
|
373
|
+
createCanvas(width: number, height: number): CanvasLike;
|
|
374
|
+
decodeImage(bytes: Uint8Array): Promise<ImageLike>;
|
|
375
|
+
encode(canvas: CanvasLike): Uint8Array;
|
|
376
|
+
};
|
|
377
|
+
export type FontResolution = {
|
|
378
|
+
kind: "bytes";
|
|
379
|
+
bytes: Uint8Array[];
|
|
380
|
+
} | {
|
|
381
|
+
kind: "descriptor";
|
|
382
|
+
descriptor: FontDescriptor;
|
|
383
|
+
} | {
|
|
384
|
+
kind: "none";
|
|
385
|
+
};
|
|
386
|
+
export type PaintRuntime = {
|
|
387
|
+
resolveFont(req: FontRequest | string): FontResolution;
|
|
388
|
+
loadImageBytes(src: string): Promise<Uint8Array>;
|
|
389
|
+
loadImage?(src: string, ck: unknown): Promise<unknown | null>;
|
|
390
|
+
canvas?: CanvasHost;
|
|
391
|
+
registerFont?(family: string, res: FontResolution): void | Promise<void>;
|
|
392
|
+
cache?: PaintCache;
|
|
393
|
+
paint(commands: Command[], ck?: unknown): Promise<PaintResult>;
|
|
394
|
+
};
|
|
395
|
+
export type Painter = (commands: Command[], rt: PaintRuntime) => Promise<PaintOutput>;
|
|
396
|
+
export type KeptPaintResult = {
|
|
397
|
+
canvas: CanvasLike;
|
|
398
|
+
warnings: PaintWarning[];
|
|
399
|
+
dispose(): void;
|
|
400
|
+
};
|
|
401
|
+
export type EncodedPaintResult = {
|
|
402
|
+
bytes: Uint8Array;
|
|
403
|
+
format: EncodeFormat;
|
|
404
|
+
warnings: PaintWarning[];
|
|
405
|
+
};
|
|
406
|
+
export type PaintResult = KeptPaintResult | EncodedPaintResult;
|
package/src/types.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// freshcoat's type vocabulary: the geometry primitives, the backend-agnostic
|
|
2
|
+
// IR (Command / DrawCommand) the painters consume, the resolved style vocabulary
|
|
3
|
+
// compile bakes and paint reads, and the paint runtime contract. Kept free of
|
|
4
|
+
// zod / authoring (Template, Element) so a consumer can build a Command[] scene
|
|
5
|
+
// and render it without pulling in the template-authoring layer. freshcoat paints
|
|
6
|
+
// ONE scene per paint() — the notion of a card's multiple sides (named frames)
|
|
7
|
+
// lives in coatfile, which drives paint() once per side.
|
|
8
|
+
//
|
|
9
|
+
// The geometry primitives (Vec2, Size, BlendMode, GradientStop, FontDescriptor,
|
|
10
|
+
// FontFile) are hand-declared here as the canonical runtime shapes. coatfile
|
|
11
|
+
// re-derives them via zod (schemas.ts) for authoring/validation; the two are kept
|
|
12
|
+
// structurally identical.
|
|
13
|
+
export {};
|