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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +242 -0
  3. package/dist/canonicalize.d.ts +2 -0
  4. package/dist/canonicalize.js +125 -0
  5. package/dist/fmt.d.ts +1 -0
  6. package/dist/fmt.js +3 -0
  7. package/dist/generate.d.ts +2 -0
  8. package/dist/generate.js +140 -0
  9. package/dist/id.d.ts +1 -0
  10. package/dist/id.js +4 -0
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.js +1 -0
  13. package/dist/mask.d.ts +2 -0
  14. package/dist/mask.js +142 -0
  15. package/dist/prng.d.ts +22 -0
  16. package/dist/prng.js +36 -0
  17. package/dist/shapes/blob.d.ts +5 -0
  18. package/dist/shapes/blob.js +35 -0
  19. package/dist/shapes/circle.d.ts +5 -0
  20. package/dist/shapes/circle.js +23 -0
  21. package/dist/shapes/octagon.d.ts +5 -0
  22. package/dist/shapes/octagon.js +24 -0
  23. package/dist/shapes/oval.d.ts +5 -0
  24. package/dist/shapes/oval.js +31 -0
  25. package/dist/shapes/polygon.d.ts +5 -0
  26. package/dist/shapes/polygon.js +23 -0
  27. package/dist/shapes/rectangle.d.ts +5 -0
  28. package/dist/shapes/rectangle.js +34 -0
  29. package/dist/shapes/square.d.ts +5 -0
  30. package/dist/shapes/square.js +34 -0
  31. package/dist/shapes/trapezoid.d.ts +5 -0
  32. package/dist/shapes/trapezoid.js +29 -0
  33. package/dist/shapes/triangle.d.ts +5 -0
  34. package/dist/shapes/triangle.js +28 -0
  35. package/dist/styling.d.ts +3 -0
  36. package/dist/styling.js +65 -0
  37. package/dist/svg.d.ts +2 -0
  38. package/dist/svg.js +22 -0
  39. package/dist/types.d.ts +121 -0
  40. package/dist/types.js +1 -0
  41. package/dist/validate.d.ts +2 -0
  42. package/dist/validate.js +266 -0
  43. package/dist/variation.d.ts +11 -0
  44. package/dist/variation.js +244 -0
  45. 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,5 @@
1
+ import type { BlobShape } from "../types.js";
2
+ export declare function renderBlob(shape: BlobShape, _outputMode: "semantic" | "path", generatorSeed: number, shapeIndex: number): {
3
+ tag: "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { CircleShape } from "../types.js";
2
+ export declare function renderCircle(shape: CircleShape, outputMode: "semantic" | "path"): {
3
+ tag: "circle" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { OctagonShape } from "../types.js";
2
+ export declare function renderOctagon(shape: OctagonShape, outputMode: "semantic" | "path"): {
3
+ tag: "polygon" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { OvalShape } from "../types.js";
2
+ export declare function renderOval(shape: OvalShape, outputMode: "semantic" | "path"): {
3
+ tag: "ellipse" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { PolygonShape } from "../types.js";
2
+ export declare function renderPolygon(shape: PolygonShape, outputMode: "semantic" | "path"): {
3
+ tag: "polygon" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { RectangleShape } from "../types.js";
2
+ export declare function renderRectangle(shape: RectangleShape, outputMode: "semantic" | "path"): {
3
+ tag: "rect" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { SquareShape } from "../types.js";
2
+ export declare function renderSquare(shape: SquareShape, outputMode: "semantic" | "path"): {
3
+ tag: "rect" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { TrapezoidShape } from "../types.js";
2
+ export declare function renderTrapezoid(shape: TrapezoidShape, outputMode: "semantic" | "path"): {
3
+ tag: "polygon" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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,5 @@
1
+ import type { TriangleShape } from "../types.js";
2
+ export declare function renderTriangle(shape: TriangleShape, outputMode: "semantic" | "path"): {
3
+ tag: "polygon" | "path";
4
+ attrs: Record<string, string | number>;
5
+ };
@@ -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>;
@@ -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
@@ -0,0 +1,2 @@
1
+ import type { Canvas, ShapeElement } from "./types.js";
2
+ export declare function assembleSvg(canvas: Canvas, elements: ShapeElement[], defs?: string[]): string;
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
+ }