foster-ts-shapes 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 +21 -0
- package/README.md +242 -0
- package/dist/canonicalize.d.ts +2 -0
- package/dist/canonicalize.js +125 -0
- package/dist/fmt.d.ts +1 -0
- package/dist/fmt.js +3 -0
- package/dist/generate.d.ts +2 -0
- package/dist/generate.js +140 -0
- package/dist/id.d.ts +1 -0
- package/dist/id.js +4 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/mask.d.ts +2 -0
- package/dist/mask.js +142 -0
- package/dist/prng.d.ts +22 -0
- package/dist/prng.js +36 -0
- package/dist/shapes/blob.d.ts +5 -0
- package/dist/shapes/blob.js +35 -0
- package/dist/shapes/circle.d.ts +5 -0
- package/dist/shapes/circle.js +23 -0
- package/dist/shapes/octagon.d.ts +5 -0
- package/dist/shapes/octagon.js +24 -0
- package/dist/shapes/oval.d.ts +5 -0
- package/dist/shapes/oval.js +31 -0
- package/dist/shapes/polygon.d.ts +5 -0
- package/dist/shapes/polygon.js +23 -0
- package/dist/shapes/rectangle.d.ts +5 -0
- package/dist/shapes/rectangle.js +34 -0
- package/dist/shapes/square.d.ts +5 -0
- package/dist/shapes/square.js +34 -0
- package/dist/shapes/trapezoid.d.ts +5 -0
- package/dist/shapes/trapezoid.js +29 -0
- package/dist/shapes/triangle.d.ts +5 -0
- package/dist/shapes/triangle.js +28 -0
- package/dist/styling.d.ts +3 -0
- package/dist/styling.js +65 -0
- package/dist/svg.d.ts +2 -0
- package/dist/svg.js +22 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.js +1 -0
- package/dist/validate.d.ts +2 -0
- package/dist/validate.js +266 -0
- package/dist/variation.d.ts +11 -0
- package/dist/variation.js +244 -0
- package/package.json +41 -0
package/dist/mask.js
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { createPrng, maskSeed } from "./prng.js";
|
|
2
|
+
import { applySizeVariance, renderVaried, extractVertices, verticesToPath } from "./variation.js";
|
|
3
|
+
import { buildGradientDef, buildStylingAttrs } from "./styling.js";
|
|
4
|
+
import { shapeId } from "./id.js";
|
|
5
|
+
import { renderSquare } from "./shapes/square.js";
|
|
6
|
+
import { renderRectangle } from "./shapes/rectangle.js";
|
|
7
|
+
import { renderCircle } from "./shapes/circle.js";
|
|
8
|
+
import { renderTriangle } from "./shapes/triangle.js";
|
|
9
|
+
import { renderTrapezoid } from "./shapes/trapezoid.js";
|
|
10
|
+
import { renderOctagon } from "./shapes/octagon.js";
|
|
11
|
+
import { renderPolygon } from "./shapes/polygon.js";
|
|
12
|
+
import { renderOval } from "./shapes/oval.js";
|
|
13
|
+
import { renderBlob } from "./shapes/blob.js";
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
// buildMaskDef — renders mask shapes into a <mask> SVG element string.
|
|
16
|
+
//
|
|
17
|
+
// maskId — the id attribute for the <mask> element (e.g. "mask-s1-circle-0")
|
|
18
|
+
// maskShapes — the canonical MaskShape[] (mask/layer fields already dropped)
|
|
19
|
+
// mode — "semantic" | "path" (passed through to shape renderers)
|
|
20
|
+
// generatorSeed — top-level generator seed (for deterministic PRNG)
|
|
21
|
+
// parentIndex — original index of the masked shape (for maskSeed independence)
|
|
22
|
+
// gradientDefs — accumulator array; mask shape gradients are pushed here so they
|
|
23
|
+
// appear in the top-level <defs> block alongside main-shape gradients
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
export function buildMaskDef(maskId, maskShapes, mode, generatorSeed, parentIndex, gradientDefs) {
|
|
26
|
+
const elementStrings = [];
|
|
27
|
+
for (let mi = 0; mi < maskShapes.length; mi++) {
|
|
28
|
+
const ms = maskShapes[mi];
|
|
29
|
+
const sv = ms.sizeVariance ?? 0;
|
|
30
|
+
const dt = ms.distort ?? 0;
|
|
31
|
+
const bz = ms.bezier ?? 0;
|
|
32
|
+
// Isolated PRNG for this mask shape — independent from parent shape's varPrng
|
|
33
|
+
const varPrng = (sv > 0 || dt > 0) ? createPrng(maskSeed(generatorSeed, parentIndex, mi)) : null;
|
|
34
|
+
// Apply sizeVariance if present
|
|
35
|
+
const workShape = (sv > 0 && varPrng) ? applySizeVariance(ms, varPrng) : ms;
|
|
36
|
+
// Render geometry
|
|
37
|
+
let tag;
|
|
38
|
+
let attrs;
|
|
39
|
+
if (dt > 0 || bz > 0) {
|
|
40
|
+
if (dt > 0 && varPrng) {
|
|
41
|
+
const r = renderVaried(workShape, varPrng, generatorSeed, mi, bz, workShape.bezierDirection ?? "out");
|
|
42
|
+
tag = r.tag;
|
|
43
|
+
attrs = r.attrs;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const verts = extractVertices(workShape, generatorSeed, mi);
|
|
47
|
+
const d = verticesToPath(verts, bz, workShape.bezierDirection ?? "out");
|
|
48
|
+
tag = "path";
|
|
49
|
+
attrs = { d };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
switch (workShape.type) {
|
|
54
|
+
case "square": {
|
|
55
|
+
const r = renderSquare(workShape, mode);
|
|
56
|
+
tag = r.tag;
|
|
57
|
+
attrs = r.attrs;
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "rectangle": {
|
|
61
|
+
const r = renderRectangle(workShape, mode);
|
|
62
|
+
tag = r.tag;
|
|
63
|
+
attrs = r.attrs;
|
|
64
|
+
break;
|
|
65
|
+
}
|
|
66
|
+
case "circle": {
|
|
67
|
+
const r = renderCircle(workShape, mode);
|
|
68
|
+
tag = r.tag;
|
|
69
|
+
attrs = r.attrs;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
case "triangle": {
|
|
73
|
+
const r = renderTriangle(workShape, mode);
|
|
74
|
+
tag = r.tag;
|
|
75
|
+
attrs = r.attrs;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
case "trapezoid": {
|
|
79
|
+
const r = renderTrapezoid(workShape, mode);
|
|
80
|
+
tag = r.tag;
|
|
81
|
+
attrs = r.attrs;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
case "octagon": {
|
|
85
|
+
const r = renderOctagon(workShape, mode);
|
|
86
|
+
tag = r.tag;
|
|
87
|
+
attrs = r.attrs;
|
|
88
|
+
break;
|
|
89
|
+
}
|
|
90
|
+
case "polygon": {
|
|
91
|
+
const r = renderPolygon(workShape, mode);
|
|
92
|
+
tag = r.tag;
|
|
93
|
+
attrs = r.attrs;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
case "oval": {
|
|
97
|
+
const r = renderOval(workShape, mode);
|
|
98
|
+
tag = r.tag;
|
|
99
|
+
attrs = r.attrs;
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
case "blob": {
|
|
103
|
+
const r = renderBlob(workShape, mode, generatorSeed, mi);
|
|
104
|
+
tag = r.tag;
|
|
105
|
+
attrs = r.attrs;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
default: throw new Error(`Unsupported shape type in mask: ${workShape.type}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Accumulate gradient defs for this mask shape (if any)
|
|
112
|
+
const msId = shapeId(generatorSeed, ms.type, mi);
|
|
113
|
+
let fillGradId;
|
|
114
|
+
if (ms.fillGradient) {
|
|
115
|
+
fillGradId = `grad-${msId}-mask-fill`;
|
|
116
|
+
gradientDefs.push(buildGradientDef(fillGradId, ms.fillGradient));
|
|
117
|
+
}
|
|
118
|
+
let strokeGradId;
|
|
119
|
+
if (ms.strokeGradient) {
|
|
120
|
+
strokeGradId = `grad-${msId}-mask-stroke`;
|
|
121
|
+
gradientDefs.push(buildGradientDef(strokeGradId, ms.strokeGradient));
|
|
122
|
+
}
|
|
123
|
+
// Apply styling attrs; inject fill="white" default when no fill or fillGradient
|
|
124
|
+
const stylingAttrs = buildStylingAttrs(ms, fillGradId, strokeGradId);
|
|
125
|
+
if (stylingAttrs.fill === undefined && fillGradId === undefined) {
|
|
126
|
+
stylingAttrs.fill = "white";
|
|
127
|
+
}
|
|
128
|
+
Object.assign(attrs, stylingAttrs);
|
|
129
|
+
// Apply rotation as transform attr if present
|
|
130
|
+
if (ms.rotation !== undefined && ms.rotation !== 0) {
|
|
131
|
+
attrs["transform"] = `rotate(${ms.rotation}, ${ms.x}, ${ms.y})`;
|
|
132
|
+
}
|
|
133
|
+
// Build element string (no id attribute — mask shapes are anonymous)
|
|
134
|
+
const attrParts = Object.entries(attrs).map(([k, v]) => `${k}="${v}"`);
|
|
135
|
+
elementStrings.push(` <${tag} ${attrParts.join(" ")}/>`);
|
|
136
|
+
}
|
|
137
|
+
return [
|
|
138
|
+
` <mask id="${maskId}" maskUnits="userSpaceOnUse">`,
|
|
139
|
+
...elementStrings,
|
|
140
|
+
` </mask>`,
|
|
141
|
+
].join("\n");
|
|
142
|
+
}
|
package/dist/prng.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a deterministic seed for a blob shape from the generator seed and shape index.
|
|
3
|
+
* Combines two integer values into a new seed using prime multiplication.
|
|
4
|
+
*/
|
|
5
|
+
export declare function blobSeed(generatorSeed: number, index: number): number;
|
|
6
|
+
/**
|
|
7
|
+
* Derive a deterministic seed for a shape's variation PRNG from the generator seed and shape index.
|
|
8
|
+
* Uses different prime constants from blobSeed to guarantee independence.
|
|
9
|
+
*/
|
|
10
|
+
export declare function varSeed(generatorSeed: number, index: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Derive a deterministic seed for a mask shape's PRNG.
|
|
13
|
+
* Uses different prime constants from blobSeed and varSeed to guarantee independence.
|
|
14
|
+
* parentIdx is the original (pre-sort) index of the masked shape; maskIdx is the
|
|
15
|
+
* position of this mask shape within the mask array.
|
|
16
|
+
*/
|
|
17
|
+
export declare function maskSeed(genSeed: number, parentIdx: number, maskIdx: number): number;
|
|
18
|
+
/**
|
|
19
|
+
* Create a mulberry32 PRNG function seeded with the given value.
|
|
20
|
+
* Each call to the returned function advances state and returns a float in [0, 1).
|
|
21
|
+
*/
|
|
22
|
+
export declare function createPrng(seed: number): () => number;
|
package/dist/prng.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a deterministic seed for a blob shape from the generator seed and shape index.
|
|
3
|
+
* Combines two integer values into a new seed using prime multiplication.
|
|
4
|
+
*/
|
|
5
|
+
export function blobSeed(generatorSeed, index) {
|
|
6
|
+
return (Math.imul(generatorSeed | 0, 1000003) + Math.imul(index, 2654435761)) | 0;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Derive a deterministic seed for a shape's variation PRNG from the generator seed and shape index.
|
|
10
|
+
* Uses different prime constants from blobSeed to guarantee independence.
|
|
11
|
+
*/
|
|
12
|
+
export function varSeed(generatorSeed, index) {
|
|
13
|
+
return (Math.imul(generatorSeed | 0, 1000033) + Math.imul(index, 2246822519)) | 0;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Derive a deterministic seed for a mask shape's PRNG.
|
|
17
|
+
* Uses different prime constants from blobSeed and varSeed to guarantee independence.
|
|
18
|
+
* parentIdx is the original (pre-sort) index of the masked shape; maskIdx is the
|
|
19
|
+
* position of this mask shape within the mask array.
|
|
20
|
+
*/
|
|
21
|
+
export function maskSeed(genSeed, parentIdx, maskIdx) {
|
|
22
|
+
return (Math.imul(genSeed | 0, 1000039) + Math.imul(parentIdx, 65537) + maskIdx) | 0;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Create a mulberry32 PRNG function seeded with the given value.
|
|
26
|
+
* Each call to the returned function advances state and returns a float in [0, 1).
|
|
27
|
+
*/
|
|
28
|
+
export function createPrng(seed) {
|
|
29
|
+
let s = seed | 0;
|
|
30
|
+
return () => {
|
|
31
|
+
s = (s + 0x6D2B79F5) | 0;
|
|
32
|
+
let z = Math.imul(s ^ (s >>> 15), s | 1);
|
|
33
|
+
z ^= z + Math.imul(z ^ (z >>> 7), z | 61);
|
|
34
|
+
return ((z ^ (z >>> 14)) >>> 0) / 0x100000000;
|
|
35
|
+
};
|
|
36
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
import { createPrng, blobSeed } from "../prng.js";
|
|
3
|
+
export function renderBlob(shape, _outputMode, generatorSeed, shapeIndex) {
|
|
4
|
+
const { x, y, size } = shape;
|
|
5
|
+
const n = shape.points ?? 6;
|
|
6
|
+
const prng = createPrng(blobSeed(generatorSeed, shapeIndex));
|
|
7
|
+
// Place n control points at evenly-spaced angles with seed-based radial variation
|
|
8
|
+
const pts = [];
|
|
9
|
+
for (let k = 0; k < n; k++) {
|
|
10
|
+
const angle = (k * Math.PI * 2) / n;
|
|
11
|
+
const radius = size * (0.6 + 0.4 * prng());
|
|
12
|
+
pts.push([x + radius * Math.cos(angle), y + radius * Math.sin(angle)]);
|
|
13
|
+
}
|
|
14
|
+
// Convert closed Catmull-Rom spline to cubic Bézier segments
|
|
15
|
+
// For segment pts[i] → pts[i+1]:
|
|
16
|
+
// CP1 = pts[i] + (pts[i+1] − pts[i−1]) / 6
|
|
17
|
+
// CP2 = pts[i+1] − (pts[i+2] − pts[i]) / 6
|
|
18
|
+
// Indices are taken modulo n for the closed loop.
|
|
19
|
+
const p = (i) => pts[((i % n) + n) % n];
|
|
20
|
+
const segments = [];
|
|
21
|
+
for (let i = 0; i < n; i++) {
|
|
22
|
+
const [p0x, p0y] = p(i);
|
|
23
|
+
const [p1x, p1y] = p(i + 1);
|
|
24
|
+
const [pm1x, pm1y] = p(i - 1);
|
|
25
|
+
const [p2x, p2y] = p(i + 2);
|
|
26
|
+
const cp1x = p0x + (p1x - pm1x) / 6;
|
|
27
|
+
const cp1y = p0y + (p1y - pm1y) / 6;
|
|
28
|
+
const cp2x = p1x - (p2x - p0x) / 6;
|
|
29
|
+
const cp2y = p1y - (p2y - p0y) / 6;
|
|
30
|
+
segments.push(`C ${fmt(cp1x)} ${fmt(cp1y)} ${fmt(cp2x)} ${fmt(cp2y)} ${fmt(p1x)} ${fmt(p1y)}`);
|
|
31
|
+
}
|
|
32
|
+
const [startX, startY] = pts[0];
|
|
33
|
+
const d = `M ${fmt(startX)} ${fmt(startY)} ${segments.join(" ")} Z`;
|
|
34
|
+
return { tag: "path", attrs: { d } };
|
|
35
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderCircle(shape, outputMode) {
|
|
3
|
+
const { x, y, size } = shape;
|
|
4
|
+
const r = size / 2;
|
|
5
|
+
if (outputMode === "semantic") {
|
|
6
|
+
return {
|
|
7
|
+
tag: "circle",
|
|
8
|
+
attrs: {
|
|
9
|
+
cx: fmt(x),
|
|
10
|
+
cy: fmt(y),
|
|
11
|
+
r: fmt(r),
|
|
12
|
+
},
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
// Exact two-arc path (no Bézier)
|
|
17
|
+
const d = `M ${fmt(x + r)} ${fmt(y)} A ${fmt(r)} ${fmt(r)} 0 1 0 ${fmt(x - r)} ${fmt(y)} A ${fmt(r)} ${fmt(r)} 0 1 0 ${fmt(x + r)} ${fmt(y)} Z`;
|
|
18
|
+
return {
|
|
19
|
+
tag: "path",
|
|
20
|
+
attrs: { d },
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderOctagon(shape, outputMode) {
|
|
3
|
+
const { x, y, size } = shape;
|
|
4
|
+
const n = 8;
|
|
5
|
+
// Regular octagon: 8 vertices at k·π/4 − π/2, first vertex at top
|
|
6
|
+
const vertices = [];
|
|
7
|
+
for (let k = 0; k < n; k++) {
|
|
8
|
+
const angle = (k * Math.PI * 2) / n - Math.PI / 2;
|
|
9
|
+
vertices.push([fmt(x + size * Math.cos(angle)), fmt(y + size * Math.sin(angle))]);
|
|
10
|
+
}
|
|
11
|
+
if (outputMode === "semantic") {
|
|
12
|
+
return {
|
|
13
|
+
tag: "polygon",
|
|
14
|
+
attrs: {
|
|
15
|
+
points: vertices.map(([vx, vy]) => `${vx},${vy}`).join(" "),
|
|
16
|
+
},
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
const [first, ...rest] = vertices;
|
|
21
|
+
const d = `M ${first[0]} ${first[1]} ` + rest.map(([vx, vy]) => `L ${vx} ${vy}`).join(" ") + " Z";
|
|
22
|
+
return { tag: "path", attrs: { d } };
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderOval(shape, outputMode) {
|
|
3
|
+
const { x, y, width, height } = shape;
|
|
4
|
+
const rx = width / 2;
|
|
5
|
+
const ry = height / 2;
|
|
6
|
+
if (outputMode === "semantic") {
|
|
7
|
+
return {
|
|
8
|
+
tag: "ellipse",
|
|
9
|
+
attrs: {
|
|
10
|
+
cx: fmt(x),
|
|
11
|
+
cy: fmt(y),
|
|
12
|
+
rx: fmt(rx),
|
|
13
|
+
ry: fmt(ry),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// Two-arc path split at left and right extremes (exact ellipse representation)
|
|
19
|
+
const right = fmt(x + rx);
|
|
20
|
+
const left = fmt(x - rx);
|
|
21
|
+
const cy = fmt(y);
|
|
22
|
+
const rxF = fmt(rx);
|
|
23
|
+
const ryF = fmt(ry);
|
|
24
|
+
return {
|
|
25
|
+
tag: "path",
|
|
26
|
+
attrs: {
|
|
27
|
+
d: `M ${right} ${cy} A ${rxF} ${ryF} 0 1 0 ${left} ${cy} A ${rxF} ${ryF} 0 1 0 ${right} ${cy} Z`,
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderPolygon(shape, outputMode) {
|
|
3
|
+
const { x, y, size, sides } = shape;
|
|
4
|
+
// Regular n-gon: n vertices at 2πk/n − π/2, first vertex at top
|
|
5
|
+
const vertices = [];
|
|
6
|
+
for (let k = 0; k < sides; k++) {
|
|
7
|
+
const angle = (k * Math.PI * 2) / sides - Math.PI / 2;
|
|
8
|
+
vertices.push([fmt(x + size * Math.cos(angle)), fmt(y + size * Math.sin(angle))]);
|
|
9
|
+
}
|
|
10
|
+
if (outputMode === "semantic") {
|
|
11
|
+
return {
|
|
12
|
+
tag: "polygon",
|
|
13
|
+
attrs: {
|
|
14
|
+
points: vertices.map(([vx, vy]) => `${vx},${vy}`).join(" "),
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const [first, ...rest] = vertices;
|
|
20
|
+
const d = `M ${first[0]} ${first[1]} ` + rest.map(([vx, vy]) => `L ${vx} ${vy}`).join(" ") + " Z";
|
|
21
|
+
return { tag: "path", attrs: { d } };
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderRectangle(shape, outputMode) {
|
|
3
|
+
const { x, y, width, height } = shape;
|
|
4
|
+
const halfW = width / 2;
|
|
5
|
+
const halfH = height / 2;
|
|
6
|
+
if (outputMode === "semantic") {
|
|
7
|
+
return {
|
|
8
|
+
tag: "rect",
|
|
9
|
+
attrs: {
|
|
10
|
+
x: fmt(x - halfW),
|
|
11
|
+
y: fmt(y - halfH),
|
|
12
|
+
width: fmt(width),
|
|
13
|
+
height: fmt(height),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// TL → TR → BR → BL
|
|
19
|
+
const x1 = fmt(x - halfW);
|
|
20
|
+
const y1 = fmt(y - halfH);
|
|
21
|
+
const x2 = fmt(x + halfW);
|
|
22
|
+
const y2 = fmt(y - halfH);
|
|
23
|
+
const x3 = fmt(x + halfW);
|
|
24
|
+
const y3 = fmt(y + halfH);
|
|
25
|
+
const x4 = fmt(x - halfW);
|
|
26
|
+
const y4 = fmt(y + halfH);
|
|
27
|
+
return {
|
|
28
|
+
tag: "path",
|
|
29
|
+
attrs: {
|
|
30
|
+
d: `M ${x1} ${y1} L ${x2} ${y2} L ${x3} ${y3} L ${x4} ${y4} Z`,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderSquare(shape, outputMode) {
|
|
3
|
+
const { x, y, size } = shape;
|
|
4
|
+
const halfW = size / 2;
|
|
5
|
+
const halfH = size / 2;
|
|
6
|
+
if (outputMode === "semantic") {
|
|
7
|
+
return {
|
|
8
|
+
tag: "rect",
|
|
9
|
+
attrs: {
|
|
10
|
+
x: fmt(x - halfW),
|
|
11
|
+
y: fmt(y - halfH),
|
|
12
|
+
width: fmt(size),
|
|
13
|
+
height: fmt(size),
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
// TL → TR → BR → BL
|
|
19
|
+
const x1 = fmt(x - halfW);
|
|
20
|
+
const y1 = fmt(y - halfH);
|
|
21
|
+
const x2 = fmt(x + halfW);
|
|
22
|
+
const y2 = fmt(y - halfH);
|
|
23
|
+
const x3 = fmt(x + halfW);
|
|
24
|
+
const y3 = fmt(y + halfH);
|
|
25
|
+
const x4 = fmt(x - halfW);
|
|
26
|
+
const y4 = fmt(y + halfH);
|
|
27
|
+
return {
|
|
28
|
+
tag: "path",
|
|
29
|
+
attrs: {
|
|
30
|
+
d: `M ${x1} ${y1} L ${x2} ${y2} L ${x3} ${y3} L ${x4} ${y4} Z`,
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderTrapezoid(shape, outputMode) {
|
|
3
|
+
const { x, y, topWidth, bottomWidth, height } = shape;
|
|
4
|
+
// Bounding-box center: top edge at y - height/2, bottom at y + height/2
|
|
5
|
+
const tlX = fmt(x - topWidth / 2);
|
|
6
|
+
const tlY = fmt(y - height / 2);
|
|
7
|
+
const trX = fmt(x + topWidth / 2);
|
|
8
|
+
const trY = fmt(y - height / 2);
|
|
9
|
+
const brX = fmt(x + bottomWidth / 2);
|
|
10
|
+
const brY = fmt(y + height / 2);
|
|
11
|
+
const blX = fmt(x - bottomWidth / 2);
|
|
12
|
+
const blY = fmt(y + height / 2);
|
|
13
|
+
if (outputMode === "semantic") {
|
|
14
|
+
return {
|
|
15
|
+
tag: "polygon",
|
|
16
|
+
attrs: {
|
|
17
|
+
points: `${tlX},${tlY} ${trX},${trY} ${brX},${brY} ${blX},${blY}`,
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
return {
|
|
23
|
+
tag: "path",
|
|
24
|
+
attrs: {
|
|
25
|
+
d: `M ${tlX} ${tlY} L ${trX} ${trY} L ${brX} ${brY} L ${blX} ${blY} Z`,
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { fmt } from "../fmt.js";
|
|
2
|
+
export function renderTriangle(shape, outputMode) {
|
|
3
|
+
const { x, y, size } = shape;
|
|
4
|
+
const h = size * Math.sqrt(3) / 2;
|
|
5
|
+
// Equilateral triangle centroid vertices
|
|
6
|
+
const topX = fmt(x);
|
|
7
|
+
const topY = fmt(y - (2 * h) / 3);
|
|
8
|
+
const blX = fmt(x - size / 2);
|
|
9
|
+
const blY = fmt(y + h / 3);
|
|
10
|
+
const brX = fmt(x + size / 2);
|
|
11
|
+
const brY = fmt(y + h / 3);
|
|
12
|
+
if (outputMode === "semantic") {
|
|
13
|
+
return {
|
|
14
|
+
tag: "polygon",
|
|
15
|
+
attrs: {
|
|
16
|
+
points: `${topX},${topY} ${blX},${blY} ${brX},${brY}`,
|
|
17
|
+
},
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return {
|
|
22
|
+
tag: "path",
|
|
23
|
+
attrs: {
|
|
24
|
+
d: `M ${topX} ${topY} L ${blX} ${blY} L ${brX} ${brY} Z`,
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { Gradient, ShapeBase } from "./types.js";
|
|
2
|
+
export declare function buildGradientDef(gradientId: string, gradient: Gradient): string;
|
|
3
|
+
export declare function buildStylingAttrs(shape: ShapeBase, fillGradId?: string, strokeGradId?: string): Record<string, string | number>;
|
package/dist/styling.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { fmt } from "./fmt.js";
|
|
2
|
+
// ---------------------------------------------------------------------------
|
|
3
|
+
// buildGradientDef — build SVG <linearGradient> or <radialGradient> element string
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
export function buildGradientDef(gradientId, gradient) {
|
|
6
|
+
const stops = gradient.stops.map((stop) => {
|
|
7
|
+
let s = ` <stop offset="${fmt(stop.offset)}" stop-color="${stop.color}"`;
|
|
8
|
+
if (stop.opacity !== undefined)
|
|
9
|
+
s += ` stop-opacity="${fmt(stop.opacity)}"`;
|
|
10
|
+
s += "/>";
|
|
11
|
+
return s;
|
|
12
|
+
}).join("\n");
|
|
13
|
+
if (gradient.type === "linear") {
|
|
14
|
+
const x1 = gradient.x1 ?? 0;
|
|
15
|
+
const y1 = gradient.y1 ?? 0;
|
|
16
|
+
const x2 = gradient.x2 ?? 1;
|
|
17
|
+
const y2 = gradient.y2 ?? 0;
|
|
18
|
+
return [
|
|
19
|
+
` <linearGradient id="${gradientId}" x1="${fmt(x1)}" y1="${fmt(y1)}" x2="${fmt(x2)}" y2="${fmt(y2)}" gradientUnits="objectBoundingBox">`,
|
|
20
|
+
stops,
|
|
21
|
+
` </linearGradient>`,
|
|
22
|
+
].join("\n");
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const cx = gradient.cx ?? 0.5;
|
|
26
|
+
const cy = gradient.cy ?? 0.5;
|
|
27
|
+
const r = gradient.r ?? 0.5;
|
|
28
|
+
return [
|
|
29
|
+
` <radialGradient id="${gradientId}" cx="${fmt(cx)}" cy="${fmt(cy)}" r="${fmt(r)}" gradientUnits="objectBoundingBox">`,
|
|
30
|
+
stops,
|
|
31
|
+
` </radialGradient>`,
|
|
32
|
+
].join("\n");
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// buildStylingAttrs — build SVG presentation attributes from styling fields
|
|
37
|
+
// Handles fill/stroke precedence: gradient URL overrides string color.
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
export function buildStylingAttrs(shape, fillGradId, strokeGradId) {
|
|
40
|
+
const attrs = {};
|
|
41
|
+
// Fill: gradient URL takes precedence over string color
|
|
42
|
+
if (fillGradId !== undefined) {
|
|
43
|
+
attrs.fill = `url(#${fillGradId})`;
|
|
44
|
+
}
|
|
45
|
+
else if (shape.fill !== undefined) {
|
|
46
|
+
attrs.fill = shape.fill;
|
|
47
|
+
}
|
|
48
|
+
// Stroke: gradient URL takes precedence over string color
|
|
49
|
+
if (strokeGradId !== undefined) {
|
|
50
|
+
attrs.stroke = `url(#${strokeGradId})`;
|
|
51
|
+
}
|
|
52
|
+
else if (shape.stroke !== undefined) {
|
|
53
|
+
attrs.stroke = shape.stroke;
|
|
54
|
+
}
|
|
55
|
+
// stroke-width: only emitted when there is a stroke source
|
|
56
|
+
const hasStrokeSrc = strokeGradId !== undefined || shape.stroke !== undefined;
|
|
57
|
+
if (hasStrokeSrc && shape.strokeWidth !== undefined) {
|
|
58
|
+
attrs["stroke-width"] = fmt(shape.strokeWidth);
|
|
59
|
+
}
|
|
60
|
+
// Opacity
|
|
61
|
+
if (shape.opacity !== undefined) {
|
|
62
|
+
attrs.opacity = fmt(shape.opacity);
|
|
63
|
+
}
|
|
64
|
+
return attrs;
|
|
65
|
+
}
|
package/dist/svg.d.ts
ADDED
package/dist/svg.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export function assembleSvg(canvas, elements, defs) {
|
|
2
|
+
const children = elements.map((el) => {
|
|
3
|
+
const attrs = [];
|
|
4
|
+
attrs.push(`id="${el.id}"`);
|
|
5
|
+
for (const [k, v] of Object.entries(el.attrs)) {
|
|
6
|
+
attrs.push(`${k}="${v}"`);
|
|
7
|
+
}
|
|
8
|
+
if (el.rotation !== undefined && el.rotation !== 0) {
|
|
9
|
+
attrs.push(`transform="rotate(${el.rotation}, ${el.cx}, ${el.cy})"`);
|
|
10
|
+
}
|
|
11
|
+
return `<${el.tag} ${attrs.join(" ")}/>`;
|
|
12
|
+
});
|
|
13
|
+
const defsBlock = defs && defs.length > 0
|
|
14
|
+
? [` <defs>`, ...defs, ` </defs>`]
|
|
15
|
+
: [];
|
|
16
|
+
return [
|
|
17
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${canvas.width}" height="${canvas.height}" viewBox="0 0 ${canvas.width} ${canvas.height}">`,
|
|
18
|
+
...defsBlock,
|
|
19
|
+
...children.map((c) => ` ${c}`),
|
|
20
|
+
`</svg>`,
|
|
21
|
+
].join("\n");
|
|
22
|
+
}
|