sacred-geometry-generator 1.0.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/js/sg.js ADDED
@@ -0,0 +1,318 @@
1
+ /*!
2
+ * Sacred Geometry Generator - rendering and UI
3
+ * https://github.com/evoluteur/sacred-geometry
4
+ * (c) 2026 Olivier Giulieri - MIT license
5
+ */
6
+
7
+ const SVG_NS = "http://www.w3.org/2000/svg";
8
+ const VIEW = 100; // viewBox is "-50 -50 100 100"
9
+ const PAD = 0.94; // margin left around the figure
10
+
11
+ const PALETTES = [
12
+ { id: "gold", name: "Gold", stroke: "#d4af37", guide: "#d4af37" },
13
+ { id: "moon", name: "Moonlight", stroke: "#eef1ff", guide: "#aab4e8" },
14
+ { id: "rose", name: "Rose Quartz", stroke: "#f3a7bd", guide: "#c98aa5" },
15
+ { id: "jade", name: "Jade", stroke: "#7fd6b5", guide: "#5fae95" },
16
+ { id: "spectrum", name: "Spectrum", stroke: null, guide: "#8d88b8" },
17
+ { id: "ink", name: "Ink", stroke: "#1b1b2f", guide: "#1b1b2f" },
18
+ ];
19
+
20
+ const BACKGROUNDS = [
21
+ { id: "midnight", name: "Midnight", fill: "#12122a" },
22
+ { id: "void", name: "Void", fill: "#000000" },
23
+ { id: "parchment", name: "Parchment", fill: "#f4ecd8" },
24
+ { id: "none", name: "Transparent", fill: null },
25
+ ];
26
+
27
+ const byId = (list, id) => list.find((x) => x.id === id) || list[0];
28
+
29
+ const svgEl = (tag, attrs) => {
30
+ const node = document.createElementNS(SVG_NS, tag);
31
+ for (const k in attrs) {
32
+ if (attrs[k] !== null && attrs[k] !== undefined) {
33
+ node.setAttribute(k, attrs[k]);
34
+ }
35
+ }
36
+ return node;
37
+ };
38
+
39
+ // Options: { steps, rot, stroke, palette, bg, guides, animate }
40
+ const buildSvg = (pattern, o) => {
41
+ const pal = byId(PALETTES, o.palette);
42
+ const bg = byId(BACKGROUNDS, o.bg);
43
+ const { shapes, extent } = pattern.draw({ steps: o.steps });
44
+ const scale = ((VIEW / 2) * PAD) / extent;
45
+ const svg = svgEl("svg", {
46
+ viewBox: `${-VIEW / 2} ${-VIEW / 2} ${VIEW} ${VIEW}`,
47
+ class: o.animate ? "anim" : null,
48
+ });
49
+ if (bg.fill) {
50
+ svg.appendChild(
51
+ svgEl("rect", {
52
+ x: -VIEW / 2,
53
+ y: -VIEW / 2,
54
+ width: VIEW,
55
+ height: VIEW,
56
+ fill: bg.fill,
57
+ }),
58
+ );
59
+ }
60
+ const layer = (isGuide) =>
61
+ svgEl("g", {
62
+ fill: "none",
63
+ stroke: isGuide ? pal.guide : pal.stroke,
64
+ "stroke-opacity": isGuide ? 0.3 : 1,
65
+ "stroke-width": (isGuide ? o.stroke * 0.6 : o.stroke) / scale,
66
+ "stroke-linecap": "round",
67
+ "stroke-linejoin": "round",
68
+ transform: `scale(${scale}) rotate(${o.rot})`,
69
+ });
70
+ const guides = layer(true);
71
+ const main = layer(false);
72
+ const drawn = shapes.filter((s) => o.guides || !s.guide);
73
+ const step = Math.min(0.04, 0.9 / Math.max(drawn.length, 1));
74
+ drawn.forEach((s, i) => {
75
+ const node = svgEl(s.tag, s.attrs);
76
+ node.setAttribute("data-len", "");
77
+ node.style.setProperty("--len", s.len || 100);
78
+ let stroke = s.guide ? pal.guide : pal.stroke;
79
+ if (pal.stroke === null && !s.guide) {
80
+ const hue = 190 + Math.round((280 * i) / Math.max(drawn.length - 1, 1));
81
+ stroke = `hsl(${hue}, 72%, 64%)`;
82
+ node.setAttribute("stroke", stroke);
83
+ }
84
+ if (s.fill) {
85
+ node.setAttribute("fill", stroke || pal.guide);
86
+ node.setAttribute("fill-opacity", 0.1);
87
+ }
88
+ if (o.animate) node.style.animationDelay = `${(i * step).toFixed(3)}s`;
89
+ (s.guide ? guides : main).appendChild(node);
90
+ });
91
+ svg.appendChild(guides);
92
+ svg.appendChild(main);
93
+ return svg;
94
+ };
95
+
96
+ const svgMarkup = (node, size = 1024) => {
97
+ const clone = node.cloneNode(true);
98
+ clone.removeAttribute("class");
99
+ clone.setAttribute("xmlns", SVG_NS);
100
+ clone.setAttribute("width", size);
101
+ clone.setAttribute("height", size);
102
+ clone.querySelectorAll("[style]").forEach((n) => n.removeAttribute("style"));
103
+ clone
104
+ .querySelectorAll("[data-len]")
105
+ .forEach((n) => n.removeAttribute("data-len"));
106
+ return `<?xml version="1.0" encoding="UTF-8"?>\n${new XMLSerializer().serializeToString(clone)}`;
107
+ };
108
+
109
+ const saveBlob = (blob, filename) => {
110
+ const url = URL.createObjectURL(blob);
111
+ const a = document.createElement("a");
112
+ a.href = url;
113
+ a.download = filename;
114
+ a.click();
115
+ setTimeout(() => URL.revokeObjectURL(url), 2000);
116
+ };
117
+
118
+ /* ------------------------------------------------------------------ app */
119
+
120
+ const $ = (id) => document.getElementById(id);
121
+
122
+ const state = {
123
+ pattern: PATTERNS[0],
124
+ steps: 2,
125
+ rot: 0,
126
+ stroke: 0.5,
127
+ palette: "gold",
128
+ bg: "midnight",
129
+ guides: false,
130
+ animate: true,
131
+ };
132
+
133
+ const hint = (msg) => {
134
+ const el = $("hint");
135
+ if (!el) return;
136
+ el.textContent = msg;
137
+ clearTimeout(hint.timer);
138
+ hint.timer = setTimeout(() => (el.textContent = ""), 2600);
139
+ };
140
+
141
+ const render = () => {
142
+ const svg = buildSvg(state.pattern, state);
143
+ const stage = $("stage");
144
+ stage.replaceChildren(svg);
145
+ };
146
+
147
+ const setPattern = (id, skipHash) => {
148
+ const p = patternById(id);
149
+ state.pattern = p;
150
+ state.steps = p.steps ? p.steps.def : 0;
151
+ $("pname").textContent = p.name;
152
+ $("tagline").textContent = p.tagline;
153
+ $("blurb").textContent = p.blurb;
154
+ document.title = `${p.name} - Sacred Geometry Generator`;
155
+ const row = $("stepsRow");
156
+ if (p.steps) {
157
+ row.style.display = "";
158
+ $("stepsLabel").textContent = p.steps.label;
159
+ const input = $("steps");
160
+ input.min = p.steps.min;
161
+ input.max = p.steps.max;
162
+ input.value = p.steps.def;
163
+ $("stepsVal").textContent = p.steps.def;
164
+ } else {
165
+ row.style.display = "none";
166
+ }
167
+ document
168
+ .querySelectorAll(".nav > a")
169
+ .forEach((a) => a.classList.toggle("on", a.dataset.id === p.id));
170
+ if (!skipHash) location.hash = p.id;
171
+ render();
172
+ };
173
+
174
+ const fillSelect = (id, list) => {
175
+ $(id).replaceChildren(
176
+ ...list.map((x) => new Option(x.name, x.id, false, x.id === state[id])),
177
+ );
178
+ };
179
+
180
+ const onSteps = (input) => {
181
+ state.steps = +input.value;
182
+ $("stepsVal").textContent = input.value;
183
+ render();
184
+ };
185
+
186
+ const onRot = (input) => {
187
+ state.rot = +input.value;
188
+ $("rotVal").textContent = `${input.value}°`;
189
+ render();
190
+ };
191
+
192
+ const onStroke = (input) => {
193
+ state.stroke = +input.value;
194
+ $("strokeVal").textContent = (+input.value).toFixed(1);
195
+ render();
196
+ };
197
+
198
+ const onSelect = (input, key) => {
199
+ state[key] = input.value;
200
+ render();
201
+ };
202
+
203
+ const onCheck = (input, key) => {
204
+ state[key] = input.checked;
205
+ render();
206
+ };
207
+
208
+ const replay = () => {
209
+ state.animate = $("animate").checked;
210
+ render();
211
+ };
212
+
213
+ const filename = (ext) => `sacred-geometry-${state.pattern.id}.${ext}`;
214
+
215
+ const downloadSvg = () => {
216
+ const markup = svgMarkup($("stage").firstElementChild);
217
+ saveBlob(new Blob([markup], { type: "image/svg+xml" }), filename("svg"));
218
+ hint("SVG saved.");
219
+ };
220
+
221
+ const downloadPng = (size = 2048) => {
222
+ const markup = svgMarkup($("stage").firstElementChild, size);
223
+ const url = URL.createObjectURL(
224
+ new Blob([markup], { type: "image/svg+xml;charset=utf-8" }),
225
+ );
226
+ const img = new Image();
227
+ img.onload = () => {
228
+ const canvas = document.createElement("canvas");
229
+ canvas.width = size;
230
+ canvas.height = size;
231
+ canvas.getContext("2d").drawImage(img, 0, 0, size, size);
232
+ URL.revokeObjectURL(url);
233
+ canvas.toBlob((blob) => {
234
+ saveBlob(blob, filename("png"));
235
+ hint(`PNG saved (${size}×${size}).`);
236
+ });
237
+ };
238
+ img.onerror = () => hint("Could not render the PNG.");
239
+ img.src = url;
240
+ };
241
+
242
+ const copySvg = async () => {
243
+ try {
244
+ await navigator.clipboard.writeText(
245
+ svgMarkup($("stage").firstElementChild),
246
+ );
247
+ hint("SVG copied to the clipboard.");
248
+ } catch (e) {
249
+ hint("Clipboard not available - use Save SVG.");
250
+ }
251
+ };
252
+
253
+ const surprise = () => {
254
+ const p = PATTERNS[Math.floor(Math.random() * PATTERNS.length)];
255
+ const pick = (list) => list[Math.floor(Math.random() * list.length)].id;
256
+ state.palette = pick(PALETTES);
257
+ state.bg = pick(BACKGROUNDS.slice(0, 3));
258
+ state.rot = Math.floor(Math.random() * 360);
259
+ state.stroke = Math.round((0.2 + Math.random() * 1.2) * 10) / 10;
260
+ $("palette").value = state.palette;
261
+ $("bg").value = state.bg;
262
+ $("rot").value = state.rot;
263
+ $("rotVal").textContent = `${state.rot}°`;
264
+ $("stroke").value = state.stroke;
265
+ $("strokeVal").textContent = state.stroke.toFixed(1);
266
+ setPattern(p.id);
267
+ if (p.steps) {
268
+ const s =
269
+ p.steps.min + Math.floor(Math.random() * (p.steps.max - p.steps.min + 1));
270
+ $("steps").value = s;
271
+ onSteps($("steps"));
272
+ }
273
+ };
274
+
275
+ const init = () => {
276
+ const nav = document.querySelector(".nav");
277
+ nav.replaceChildren(
278
+ ...PATTERNS.map((p) => {
279
+ const a = document.createElement("a");
280
+ a.href = `#${p.id}`;
281
+ a.dataset.id = p.id;
282
+ a.textContent = p.name;
283
+ return a;
284
+ }),
285
+ );
286
+ fillSelect("palette", PALETTES);
287
+ fillSelect("bg", BACKGROUNDS);
288
+ addEventListener("hashchange", () =>
289
+ setPattern(location.hash.slice(1), true),
290
+ );
291
+ setPattern(location.hash.slice(1) || PATTERNS[0].id, true);
292
+ };
293
+
294
+ const initGallery = () => {
295
+ const cards = PATTERNS.map((p) => {
296
+ const a = document.createElement("a");
297
+ a.className = "card";
298
+ a.href = `index.html#${p.id}`;
299
+ a.appendChild(
300
+ buildSvg(p, {
301
+ steps: p.steps ? p.steps.def : 0,
302
+ rot: 0,
303
+ stroke: 0.45,
304
+ palette: "gold",
305
+ bg: "none",
306
+ guides: false,
307
+ animate: true,
308
+ }),
309
+ );
310
+ const h2 = document.createElement("h2");
311
+ h2.textContent = p.name;
312
+ const tag = document.createElement("p");
313
+ tag.textContent = p.tagline;
314
+ a.append(h2, tag);
315
+ return a;
316
+ });
317
+ document.querySelector(".cards").replaceChildren(...cards);
318
+ };
package/manifest.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "Sacred Geometry Generator",
3
+ "short_name": "Sacred Geometry",
4
+ "description": "Draw, tune, and export sacred geometry patterns: Vesica Piscis, Seed of Life, Flower of Life, Metatron's Cube, and the Golden Spiral.",
5
+ "start_url": "./",
6
+ "scope": "./",
7
+ "display": "standalone",
8
+ "background_color": "#12122a",
9
+ "theme_color": "#12122a",
10
+ "icons": [
11
+ {
12
+ "src": "icon-192.png",
13
+ "sizes": "192x192",
14
+ "type": "image/png",
15
+ "purpose": "any"
16
+ },
17
+ {
18
+ "src": "icon-512.png",
19
+ "sizes": "512x512",
20
+ "type": "image/png",
21
+ "purpose": "any maskable"
22
+ }
23
+ ]
24
+ }
package/package.json ADDED
@@ -0,0 +1,35 @@
1
+ {
2
+ "name": "sacred-geometry-generator",
3
+ "version": "1.0.0",
4
+ "description": "Sacred Geometry Generator: draw, tune, and export Vesica Piscis, Seed of Life, Flower of Life, Metatron's Cube, and the Golden Spiral as SVG.",
5
+ "repository": "github:evoluteur/sacred-geometry",
6
+ "homepage": "https://evoluteur.github.io/sacred-geometry/",
7
+ "copyright": "(c) 2026 Olivier Giulieri",
8
+ "author": "Olivier Giulieri (https://evoluteur.github.io/)",
9
+ "license": "MIT",
10
+ "keywords": [
11
+ "sacred-geometry",
12
+ "geometry",
13
+ "svg",
14
+ "generator",
15
+ "flower-of-life",
16
+ "seed-of-life",
17
+ "metatron",
18
+ "metatrons-cube",
19
+ "vesica-piscis",
20
+ "fruit-of-life",
21
+ "golden-ratio",
22
+ "golden-spiral",
23
+ "fibonacci",
24
+ "phi",
25
+ "mandala",
26
+ "hexagram",
27
+ "spiral",
28
+ "pattern",
29
+ "javascript",
30
+ "pwa",
31
+ "meditation",
32
+ "mindfulness",
33
+ "wellness"
34
+ ]
35
+ }
package/patterns.js ADDED
@@ -0,0 +1,316 @@
1
+ /*!
2
+ * Sacred Geometry Generator - pattern definitions
3
+ * https://github.com/evoluteur/sacred-geometry
4
+ * (c) 2026 Olivier Giulieri - MIT license
5
+ *
6
+ * Every pattern is built in an abstract unit space (the construction circle
7
+ * has radius 1) and returns plain shape descriptors, so the same code can
8
+ * feed the DOM, an exported SVG file, or a test runner.
9
+ *
10
+ * draw(options) -> { shapes: [{ tag, attrs, guide }], extent }
11
+ *
12
+ * "extent" is the radius of the drawing around the origin: the renderer uses
13
+ * it to scale the pattern into the viewBox.
14
+ */
15
+
16
+ const PHI = (1 + Math.sqrt(5)) / 2;
17
+ const SQRT3 = Math.sqrt(3);
18
+ const DEG = Math.PI / 180;
19
+
20
+ // 4 decimals is plenty at any export size, and keeps the SVG small
21
+ const n4 = (n) => Math.round(n * 1e4) / 1e4;
22
+ const xy = (p) => `${n4(p[0])},${n4(p[1])}`;
23
+
24
+ const polar = (dist, deg) => [
25
+ dist * Math.cos(deg * DEG),
26
+ dist * Math.sin(deg * DEG),
27
+ ];
28
+
29
+ // 6 points on a circle, starting at "offset" degrees
30
+ const hex = (dist, offset = 0) =>
31
+ [0, 1, 2, 3, 4, 5].map((i) => polar(dist, i * 60 + offset));
32
+
33
+ // "len" is the exact outline length, which the renderer uses to animate the
34
+ // drawing (browsers measure a circle's length too crudely to do it for us)
35
+ const shape = (tag, attrs, len, guide) => ({
36
+ tag,
37
+ attrs,
38
+ len: n4(len),
39
+ guide: !!guide,
40
+ });
41
+
42
+ const circle = (c, r, guide) =>
43
+ shape(
44
+ "circle",
45
+ { cx: n4(c[0]), cy: n4(c[1]), r: n4(r) },
46
+ 2 * Math.PI * r,
47
+ guide,
48
+ );
49
+
50
+ const line = (a, b, guide) =>
51
+ shape(
52
+ "line",
53
+ { x1: n4(a[0]), y1: n4(a[1]), x2: n4(b[0]), y2: n4(b[1]) },
54
+ Math.hypot(b[0] - a[0], b[1] - a[1]),
55
+ guide,
56
+ );
57
+
58
+ const polygon = (pts, guide) =>
59
+ shape(
60
+ "polygon",
61
+ { points: pts.map(xy).join(" ") },
62
+ pts.reduce((sum, p, i) => {
63
+ const q = pts[(i + 1) % pts.length];
64
+ return sum + Math.hypot(q[0] - p[0], q[1] - p[1]);
65
+ }, 0),
66
+ guide,
67
+ );
68
+
69
+ const path = (d, len, guide) => shape("path", { d }, len, guide);
70
+
71
+ // every pair of points, once
72
+ const pairs = (pts) => {
73
+ const out = [];
74
+ for (let i = 0; i < pts.length; i++) {
75
+ for (let j = i + 1; j < pts.length; j++) out.push([pts[i], pts[j]]);
76
+ }
77
+ return out;
78
+ };
79
+
80
+ // The lens (mandorla) shared by two unit circles whose centers are "d" apart
81
+ const mandorla = (mid, d, r = 1) => {
82
+ const h = Math.sqrt(r * r - (d / 2) * (d / 2));
83
+ const lens = path(
84
+ `M ${n4(mid)},${n4(-h)} A ${n4(r)},${n4(r)} 0 0,1 ${n4(mid)},${n4(h)}` +
85
+ ` A ${n4(r)},${n4(r)} 0 0,1 ${n4(mid)},${n4(-h)} Z`,
86
+ // two arcs, each subtending 120 degrees
87
+ (4 * Math.PI * r) / 3,
88
+ );
89
+ lens.fill = true; // the renderer tints it with the palette color
90
+ return lens;
91
+ };
92
+
93
+ // Triangular lattice of circle centers, keeping everything within "rings"
94
+ const lattice = (rings) => {
95
+ const pts = [];
96
+ const span = rings + 1;
97
+ for (let i = -span; i <= span; i++) {
98
+ for (let j = -span; j <= span; j++) {
99
+ const x = i + j * 0.5;
100
+ const y = (j * SQRT3) / 2;
101
+ if (Math.hypot(x, y) <= rings + 1e-9) pts.push([x, y]);
102
+ }
103
+ }
104
+ return pts;
105
+ };
106
+
107
+ // Fibonacci squares, spiralling counter-clockwise out of a unit seed square.
108
+ // dir: 0 right, 1 up, 2 left, 3 down - it also picks the arc's pivot corner.
109
+ const fibSquares = (count) => {
110
+ const sq = [{ x0: 0, y0: 0, x1: 1, y1: 1, dir: 3 }];
111
+ let minx = 0;
112
+ let miny = 0;
113
+ let maxx = 1;
114
+ let maxy = 1;
115
+ for (let k = 1; k < count; k++) {
116
+ const dir = (k - 1) % 4;
117
+ if (dir === 0) {
118
+ const s = maxy - miny;
119
+ sq.push({ x0: maxx, y0: miny, x1: maxx + s, y1: miny + s, dir });
120
+ maxx += s;
121
+ } else if (dir === 1) {
122
+ const s = maxx - minx;
123
+ sq.push({ x0: minx, y0: maxy, x1: minx + s, y1: maxy + s, dir });
124
+ maxy += s;
125
+ } else if (dir === 2) {
126
+ const s = maxy - miny;
127
+ sq.push({ x0: minx - s, y0: maxy - s, x1: minx, y1: maxy, dir });
128
+ minx -= s;
129
+ } else {
130
+ const s = maxx - minx;
131
+ sq.push({ x0: maxx - s, y0: miny - s, x1: maxx, y1: miny, dir });
132
+ miny -= s;
133
+ }
134
+ }
135
+ return { sq, bbox: [minx, miny, maxx, maxy] };
136
+ };
137
+
138
+ const PATTERNS = [
139
+ {
140
+ id: "vesica",
141
+ name: "Vesica Piscis",
142
+ tagline: "Two become one",
143
+ blurb:
144
+ "Two circles, each passing through the other's center. The almond-shaped overlap - the mandorla - holds the square roots of 2, 3, and 5, and every pattern that follows is built from it.",
145
+ steps: { label: "Circles", min: 2, max: 8, def: 2 },
146
+ draw({ steps = 2 }) {
147
+ const n = Math.max(2, steps);
148
+ const shapes = [];
149
+ const xs = [];
150
+ for (let i = 0; i < n; i++) xs.push(i - (n - 1) / 2);
151
+ // guides first so they sit behind the figure
152
+ shapes.push(line([xs[0] - 1.15, 0], [xs[n - 1] + 1.15, 0], true));
153
+ const h = SQRT3 / 2;
154
+ for (let i = 0; i < n - 1; i++) {
155
+ const mid = (xs[i] + xs[i + 1]) / 2;
156
+ shapes.push(line([mid, -h], [mid, h], true));
157
+ shapes.push(
158
+ polygon(
159
+ [
160
+ [xs[i], 0],
161
+ [xs[i + 1], 0],
162
+ [mid, -h],
163
+ ],
164
+ true,
165
+ ),
166
+ );
167
+ shapes.push(
168
+ polygon(
169
+ [
170
+ [xs[i], 0],
171
+ [xs[i + 1], 0],
172
+ [mid, h],
173
+ ],
174
+ true,
175
+ ),
176
+ );
177
+ }
178
+ xs.forEach((x) => shapes.push(circle([x, 0], 1)));
179
+ for (let i = 0; i < n - 1; i++) {
180
+ shapes.push(mandorla((xs[i] + xs[i + 1]) / 2, 1));
181
+ }
182
+ return { shapes, extent: (n - 1) / 2 + 1 };
183
+ },
184
+ },
185
+ {
186
+ id: "seed",
187
+ name: "Seed of Life",
188
+ tagline: "Seven days, seven circles",
189
+ blurb:
190
+ "Six circles around a seventh, each one centered on the rim of the last. Six overlapping vesicas make the six-petalled rosette at the heart of the Flower of Life.",
191
+ steps: null,
192
+ draw() {
193
+ const shapes = [polygon(hex(1), true), circle([0, 0], 2, true)];
194
+ shapes.push(circle([0, 0], 1));
195
+ hex(1).forEach((c) => shapes.push(circle(c, 1)));
196
+ return { shapes, extent: 2 };
197
+ },
198
+ },
199
+ {
200
+ id: "flower",
201
+ name: "Flower of Life",
202
+ tagline: "The lattice everything grows on",
203
+ blurb:
204
+ "The Seed of Life continued outward on a triangular lattice. Two rings give the classic nineteen-circle flower carved on the Osirion at Abydos; keep going and the rosettes tile the plane forever.",
205
+ steps: { label: "Rings", min: 1, max: 6, def: 2 },
206
+ draw({ steps = 2 }) {
207
+ const rings = Math.max(1, steps);
208
+ const shapes = [polygon(hex(rings), true)];
209
+ lattice(rings).forEach((c) => shapes.push(circle(c, 1)));
210
+ shapes.push(circle([0, 0], rings + 1));
211
+ shapes.push(circle([0, 0], rings + 1.1));
212
+ return { shapes, extent: rings + 1.1 };
213
+ },
214
+ },
215
+ {
216
+ id: "metatron",
217
+ name: "Metatron's Cube",
218
+ tagline: "Thirteen circles, seventy-eight lines",
219
+ blurb:
220
+ "Take the thirteen complete circles of the Fruit of Life and join every center to every other one. The seventy-eight lines hold two hexagrams, a hexagon, and the flat shadows of all five Platonic solids.",
221
+ steps: null,
222
+ draw() {
223
+ const centers = [[0, 0]].concat(hex(2), hex(2 * SQRT3, 30));
224
+ const shapes = [
225
+ circle([0, 0], 2 * SQRT3 + 1, true),
226
+ polygon(hex(2), true),
227
+ ];
228
+ centers.forEach((c) => shapes.push(circle(c, 1)));
229
+ pairs(centers).forEach(([a, b]) => shapes.push(line(a, b)));
230
+ return { shapes, extent: 2 * SQRT3 + 1 };
231
+ },
232
+ },
233
+ {
234
+ id: "golden",
235
+ name: "Golden Spiral",
236
+ tagline: "1, 1, 2, 3, 5, 8, 13...",
237
+ blurb:
238
+ "Fibonacci squares laid corner to corner, each one the sum of the two before it. Quarter circles across them approximate the logarithmic spiral that grows by phi (1.618...) every quarter turn.",
239
+ steps: { label: "Squares", min: 2, max: 13, def: 8 },
240
+ draw({ steps = 8 }) {
241
+ const count = Math.max(2, steps);
242
+ const { sq, bbox } = fibSquares(count);
243
+ const cx = (bbox[0] + bbox[2]) / 2;
244
+ const cy = (bbox[1] + bbox[3]) / 2;
245
+ // center the figure and flip y so the spiral reads the usual way up
246
+ const p = (x, y) => [x - cx, -(y - cy)];
247
+ const shapes = [];
248
+ sq.forEach((s) => {
249
+ const c = p(s.x0, s.y1);
250
+ const side = s.x1 - s.x0;
251
+ shapes.push(
252
+ shape(
253
+ "rect",
254
+ {
255
+ x: n4(c[0]),
256
+ y: n4(c[1]),
257
+ width: n4(side),
258
+ height: n4(side),
259
+ },
260
+ 4 * side,
261
+ true,
262
+ ),
263
+ );
264
+ });
265
+ const bw = bbox[2] - bbox[0];
266
+ const bh = bbox[3] - bbox[1];
267
+ shapes.push(
268
+ shape(
269
+ "rect",
270
+ {
271
+ x: n4(bbox[0] - cx),
272
+ y: n4(-(bbox[3] - cy)),
273
+ width: n4(bw),
274
+ height: n4(bh),
275
+ },
276
+ 2 * (bw + bh),
277
+ true,
278
+ ),
279
+ );
280
+ sq.forEach((s) => {
281
+ const r = s.x1 - s.x0;
282
+ let from;
283
+ let to;
284
+ if (s.dir === 0) {
285
+ from = p(s.x0, s.y0);
286
+ to = p(s.x1, s.y1);
287
+ } else if (s.dir === 1) {
288
+ from = p(s.x1, s.y0);
289
+ to = p(s.x0, s.y1);
290
+ } else if (s.dir === 2) {
291
+ from = p(s.x1, s.y1);
292
+ to = p(s.x0, s.y0);
293
+ } else {
294
+ from = p(s.x0, s.y1);
295
+ to = p(s.x1, s.y0);
296
+ }
297
+ shapes.push(
298
+ path(
299
+ `M ${xy(from)} A ${n4(r)},${n4(r)} 0 0,0 ${xy(to)}`,
300
+ (Math.PI * r) / 2,
301
+ ),
302
+ );
303
+ });
304
+ return {
305
+ shapes,
306
+ extent: Math.max(bbox[2] - bbox[0], bbox[3] - bbox[1]) / 2,
307
+ };
308
+ },
309
+ },
310
+ ];
311
+
312
+ const patternById = (id) => PATTERNS.find((p) => p.id === id) || PATTERNS[0];
313
+
314
+ if (typeof module !== "undefined" && module.exports) {
315
+ module.exports = { PATTERNS, patternById, PHI };
316
+ }
package/sg-app.png ADDED
Binary file
package/sg-flower.png ADDED
Binary file
package/sg-gallery.png ADDED
Binary file