sri-yantra 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/index.html ADDED
@@ -0,0 +1,254 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Sri Yantra</title>
7
+ <meta
8
+ name="description"
9
+ content="Sri Yantra generator: nine interlocking triangles with lotus petals, circles and gates, drawn to scale. Pick colors, build it up triangle by triangle, and download it as SVG or PNG. Free, works offline."
10
+ />
11
+ <meta name="author" content="Olivier Giulieri" />
12
+ <link rel="canonical" href="https://evoluteur.github.io/sri-yantra/" />
13
+ <link rel="icon" type="image/png" href="favicon.png" />
14
+ <link rel="manifest" href="manifest.json" />
15
+ <link rel="apple-touch-icon" href="icon-192.png" />
16
+ <meta name="theme-color" content="#1a212d" />
17
+
18
+ <link
19
+ rel="stylesheet"
20
+ href="https://fonts.googleapis.com/css?family=Overpass"
21
+ />
22
+ <link id="omg-core-css" rel="stylesheet" href="css/core.css" />
23
+ <link id="omg-theme-css" rel="stylesheet" href="css/themes/dark/dark.css" />
24
+ <link id="omg-density-css" rel="stylesheet" href="css/densities.css" />
25
+ <link rel="stylesheet" href="css/common.css" />
26
+ <link rel="stylesheet" href="css/yantra.css" />
27
+
28
+ <script src="js/omg.js"></script>
29
+ <script src="js/yantra-data.js"></script>
30
+ <script src="js/yantra.js"></script>
31
+ <script>
32
+ if ("serviceWorker" in navigator) {
33
+ addEventListener("load", () =>
34
+ navigator.serviceWorker.register("sw.js"),
35
+ );
36
+ }
37
+ </script>
38
+ </head>
39
+
40
+ <body
41
+ onload="
42
+ setupPage('home');
43
+ initYantra();
44
+ "
45
+ id="omg-body"
46
+ >
47
+ <div id="omg-header">
48
+ <h1>Sri Yantra</h1>
49
+ <div id="omg-header-tools">
50
+ <div id="omg-theme-picker"></div>
51
+ <a
52
+ id="omg-github"
53
+ href="https://github.com/evoluteur/sri-yantra"
54
+ target="_blank"
55
+ rel="noopener"
56
+ title="Sri Yantra on GitHub"
57
+ aria-label="Sri Yantra on GitHub"
58
+ ><svg viewBox="0 0 16 16" aria-hidden="true">
59
+ <path
60
+ d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"
61
+ /></svg
62
+ ></a>
63
+ </div>
64
+ </div>
65
+ <noscript
66
+ ><p class="tagline">
67
+ Sri Yantra draws as SVG, so it needs JavaScript. Enable it to draw one,
68
+ or read <a href="about.html">about the Sri Yantra</a>.
69
+ </p></noscript
70
+ >
71
+ <h2>Nine triangles around a point</h2>
72
+ <div class="content">
73
+ <p class="tagline">
74
+ The Sri Yantra is the best known of the sacred diagrams of the Indian
75
+ tantric traditions: four upward triangles and five downward ones,
76
+ interlaced around a central point and ringed by lotus petals, circles
77
+ and a gated square. This one is drawn to scale in your browser. Change
78
+ the colors, build it up one triangle at a time, and save it. New to the
79
+ Sri Yantra? Read <a href="about.html">about it</a>.
80
+ </p>
81
+
82
+ <div class="layout">
83
+ <div class="stage">
84
+ <div id="yantra" class="canvas-box"></div>
85
+ <p class="note" id="yantra-note"></p>
86
+ </div>
87
+
88
+ <div class="side">
89
+ <div class="panel">
90
+ <h3>Build it up</h3>
91
+ <div class="ctl">
92
+ <label for="tri-n">Triangles drawn, smallest first</label
93
+ ><span class="val" id="tri-n-val"></span>
94
+ <input type="range" id="tri-n" min="0" max="9" step="1" />
95
+ </div>
96
+ <div
97
+ class="chips"
98
+ id="which-chips"
99
+ role="group"
100
+ aria-label="Which triangles"
101
+ ></div>
102
+ <div class="btns">
103
+ <button type="button" id="play" class="ghost">Build it up</button>
104
+ </div>
105
+ </div>
106
+
107
+ <div class="panel">
108
+ <h3>Layers</h3>
109
+ <label class="opt"
110
+ ><input type="checkbox" id="l-bindu" /> Bindu (the central
111
+ point)</label
112
+ >
113
+ <label class="opt"
114
+ ><input type="checkbox" id="l-tri" /> The nine triangles</label
115
+ >
116
+ <label class="opt"
117
+ ><input type="checkbox" id="l-lotus8" /> Eight-petal lotus</label
118
+ >
119
+ <label class="opt"
120
+ ><input type="checkbox" id="l-lotus16" /> Sixteen-petal
121
+ lotus</label
122
+ >
123
+ <label class="opt"
124
+ ><input type="checkbox" id="l-circles" /> Circles</label
125
+ >
126
+ <label class="opt"
127
+ ><input type="checkbox" id="l-gates" /> Square with four
128
+ gates</label
129
+ >
130
+ </div>
131
+
132
+ <div class="panel">
133
+ <h3>Look</h3>
134
+ <div
135
+ class="chips"
136
+ id="pal-chips"
137
+ role="group"
138
+ aria-label="Colors"
139
+ ></div>
140
+ <div
141
+ class="chips"
142
+ id="fill-chips"
143
+ role="group"
144
+ aria-label="Fill"
145
+ ></div>
146
+ <div class="ctl">
147
+ <label for="weight">Line weight</label
148
+ ><span class="val" id="weight-val"></span>
149
+ <input type="range" id="weight" min="4" max="30" step="1" />
150
+ </div>
151
+ </div>
152
+
153
+ <div class="panel">
154
+ <h3>Save</h3>
155
+ <div class="btns">
156
+ <button type="button" id="export-png">Download PNG</button>
157
+ <button type="button" id="export-svg" class="ghost">
158
+ Download SVG
159
+ </button>
160
+ </div>
161
+ </div>
162
+ </div>
163
+ </div>
164
+
165
+ <div class="layout info">
166
+ <div class="side wide">
167
+ <div class="panel">
168
+ <h3>The nine enclosures</h3>
169
+ <p class="note">
170
+ Tradition reads the yantra as nine enclosures, called
171
+ <em>avaranas</em>, from the outside in.
172
+ </p>
173
+ <ol class="enclosures" id="enclosures"></ol>
174
+ </div>
175
+ </div>
176
+ <div class="side wide">
177
+ <div class="panel">
178
+ <h3>Shiva and Shakti</h3>
179
+ <p class="note">
180
+ The four upward triangles are usually read as Shiva, the masculine
181
+ principle, and the five downward ones as Shakti, the feminine.
182
+ They meet at the bindu, where the two are said to be one. Meanings
183
+ vary between lineages, and there are several schools of drawing.
184
+ </p>
185
+ <p class="note">
186
+ This drawing takes its triangle coordinates from a published
187
+ construction. The two largest triangles have their corners on the
188
+ circle, and the figure is symmetric left to right. Practitioners
189
+ may prefer other proportions, and a hand-drawn yantra is a matter
190
+ of practice and not just geometry.
191
+ </p>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <div class="footer">
197
+ <p><a href="about.html">About the Sri Yantra</a></p>
198
+ <p>
199
+ Triangle coordinates from
200
+ <a href="https://texample.net/tikz/examples/sri-yantra/">TeXample</a>
201
+ (CC BY-SA 4.0).
202
+ </p>
203
+ <p>
204
+ The code of this page is
205
+ <a href="https://github.com/evoluteur/sri-yantra">Open source</a> with
206
+ MIT license.
207
+ </p>
208
+ <p>
209
+ Had fun browsing the app?
210
+ <a href="https://github.com/sponsors/evoluteur"
211
+ >Buy me a coffee by becoming a sponsor</a
212
+ >.
213
+ </p>
214
+ <p>
215
+ You may also like
216
+ <a
217
+ href="https://evoluteur.github.io/mandala-maker/"
218
+ target="_blank"
219
+ rel="noopener"
220
+ >Mandala Maker</a
221
+ >,
222
+ <a
223
+ href="https://evoluteur.github.io/sacred-geometry/"
224
+ target="_blank"
225
+ rel="noopener"
226
+ >Sacred Geometry</a
227
+ >,
228
+ <a
229
+ href="https://evoluteur.github.io/platonic-solids/"
230
+ target="_blank"
231
+ rel="noopener"
232
+ >Platonic Solids</a
233
+ >, and
234
+ <a
235
+ href="https://evoluteur.github.io/archimedean-solids/"
236
+ target="_blank"
237
+ rel="noopener"
238
+ >Archimedean Solids</a
239
+ >. See them all on
240
+ <a
241
+ href="https://evoluteur.github.io/esoterica.html"
242
+ target="_blank"
243
+ rel="noopener"
244
+ >Esoterica</a
245
+ >.
246
+ </p>
247
+ <p class="copyright">
248
+ &#169; 2026
249
+ <a href="https://evoluteur.github.io/">Olivier Giulieri</a>
250
+ </p>
251
+ </div>
252
+ </div>
253
+ </body>
254
+ </html>
package/js/omg.js ADDED
@@ -0,0 +1,88 @@
1
+ const themes = ["dark", "light", "evol-blue"];
2
+ const themesColors = {
3
+ "evol-blue": "#0288d1",
4
+ dark: "#1a212d",
5
+ light: "#fdfdff",
6
+ };
7
+
8
+ // Same icon set as react-morph-charts' ThemeToggle (sun / moon / the
9
+ // squiggle used for its third "omg" option, reused here for evol-blue).
10
+ const themeIcons = {
11
+ light:
12
+ '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M3.36,17L5.12,13.23C5.26,14 5.53,14.78 5.95,15.5C6.37,16.24 6.91,16.86 7.5,17.37L3.36,17M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M20.64,17L16.5,17.36C17.09,16.85 17.62,16.22 18.04,15.5C18.46,14.77 18.73,14 18.87,13.21L20.64,17M12,22L9.59,18.56C10.33,18.83 11.14,19 12,19C12.82,19 13.63,18.83 14.37,18.56L12,22Z" /></svg>',
13
+ dark:
14
+ '<svg viewBox="0 0 24 24" width="18" height="18" fill="currentColor" aria-hidden="true"><path d="M18.97,15.95C19.8,15.87 20.69,17.05 20.16,17.8C19.84,18.25 19.5,18.67 19.08,19.07C15.17,23 8.84,23 4.94,19.07C1.03,15.17 1.03,8.83 4.94,4.93C5.34,4.53 5.76,4.17 6.21,3.85C6.96,3.32 8.14,4.21 8.06,5.04C7.79,7.9 8.75,10.87 10.95,13.06C13.14,15.26 16.1,16.22 18.97,15.95M17.33,17.97C14.5,17.81 11.7,16.64 9.53,14.5C7.36,12.31 6.2,9.5 6.04,6.68C3.23,9.82 3.34,14.64 6.35,17.66C9.37,20.67 14.19,20.78 17.33,17.97Z" /></svg>',
15
+ "evol-blue":
16
+ '<svg viewBox="0 0 20 20" width="18" height="18" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linecap="round" aria-hidden="true"><path d="M10 10 10.18 10.07 10.28 10.25 10.26 10.5 10.09 10.74 9.78 10.91 9.37 10.93 8.93 10.75 8.55 10.37 8.32 9.81 8.33 9.15 8.61 8.47 9.17 7.91 9.96 7.56 10.89 7.53 11.83 7.86 12.63 8.55 13.16 9.55 13.29 10.74 12.98 11.96 12.2 13.03 11.04 13.8 9.61 14.11 8.1 13.87 6.72 13.08 5.67 11.79 5.13 10.15 5.21 8.36 5.95 6.65 7.31 5.28 9.12 4.44 11.18 4.31 13.21 4.93 14.95 6.28 16.12 8.22 16.54 10.51 16.11 12.87 14.83 14.98 12.83 16.54 10.34 17.3 7.68 17.13" /></svg>',
17
+ };
18
+ // const densities = ["huge", "big", "medium", "small", "tiny"];
19
+ const densities = ["big", "medium", "small", "tiny"];
20
+
21
+ const setupPage = (id) => {
22
+ // if (id === "home") {
23
+ // //
24
+ // alert("home");
25
+ // }
26
+ let elem;
27
+ if (typeof routes !== "undefined" && routes?.length) {
28
+ elem = document.getElementById("omg-nav");
29
+ if (elem) {
30
+ const h = routes.map((r) => `<a href="${r.url}">${r.name}</a>`).join("");
31
+ elem.innerHTML = h;
32
+ }
33
+ }
34
+ const theme = localStorage.getItem("omg-theme") || "dark";
35
+ const density = localStorage.getItem("omg-density") || "medium";
36
+ setTheme(theme);
37
+ renderThemePicker(theme);
38
+ setDensity(density);
39
+ renderDensityPicker(density);
40
+ };
41
+
42
+
43
+ const renderThemePicker = (id) => {
44
+ const elem = document.getElementById("omg-theme-picker");
45
+ if (elem) {
46
+ elem.innerHTML = themes
47
+ .map(
48
+ (t) => `
49
+ <a class="${id === t ? "selected" : ""}" href="javascript:setTheme('${t}')" alt="${t}" title="${t}">${themeIcons[t]}</a>`
50
+ )
51
+ .join("");
52
+ }
53
+ };
54
+
55
+ const renderDensityPicker = (id) => {
56
+ const elem = document.getElementById("omg-density-picker");
57
+ if (elem) {
58
+ elem.innerHTML =
59
+ "Density: " +
60
+ densities
61
+ .map(
62
+ (d) => `<a href="javascript:setDensity('${d}')" alt="${d}">${d}</a>`
63
+ )
64
+ .join("");
65
+ }
66
+ };
67
+
68
+ const setTheme = (id) => {
69
+ if (!id) {
70
+ return;
71
+ }
72
+ let elem = document.getElementById("omg-theme-css");
73
+ if (elem) {
74
+ elem.setAttribute("href", `css/themes/${id}/${id}.css`);
75
+ localStorage.setItem("omg-theme", id);
76
+ }
77
+ };
78
+
79
+ const setDensity = (id) => {
80
+ if (!id) {
81
+ return;
82
+ }
83
+ let elem = document.getElementById("omg-body");
84
+ if (elem) {
85
+ elem.className = id;
86
+ localStorage.setItem("omg-density", id);
87
+ }
88
+ };
@@ -0,0 +1,20 @@
1
+ /* The nine interlocking triangles of the Sri Yantra, in a 300 x 300 space
2
+ with the center at (150, 150) and the circle of radius 100 that passes
3
+ through every outer vertex. Each entry is [left x, base y, apex y, right x,
4
+ points up]. The base runs from (left, base y) to (right, base y) and the
5
+ apex is at (150, apex y). y grows downward, as on screen.
6
+
7
+ The numbers are from the Sri Yantra example on TeXample.net (CC BY-SA
8
+ 4.0). The two largest triangles have all their vertices on the circle, and
9
+ every triangle is symmetric left to right. */
10
+ const YANTRA_TRIANGLES = [
11
+ { id: "D1", data: [53.65669559977147, 123.20508075688774, 250, 246.34330440022853, 0] },
12
+ { id: "U1", data: [52.984011026495736, 174.24660560764943, 50, 247.01598897350425, 1] },
13
+ { id: "U3", data: [98.71823312733801, 220.03828947357886, 123.20508075688774, 201.281766872662, 1] },
14
+ { id: "U2", data: [78.26467997914015, 197.92315674002487, 78.10499177949904, 221.73532002085986, 1] },
15
+ { id: "D3", data: [90.4856922951427, 78.10499177949904, 160.66014976539617, 209.51430770485734, 0] },
16
+ { id: "D2", data: [80.98384838952128, 103.12199145016105, 220.03828947357886, 219.0161516104787, 0] },
17
+ { id: "U4", data: [114.9488500600036, 160.66014976539617, 103.12199145016105, 185.0511499399964, 1] },
18
+ { id: "D4", data: [116.35142605010424, 134.30757626706648, 197.92315674002487, 183.64857394989576, 0] },
19
+ { id: "D5", data: [124.61190803072795, 144.79777263138968, 174.24660560764943, 175.38809196927207, 0] },
20
+ ];
package/js/yantra.js ADDED
@@ -0,0 +1,305 @@
1
+ /* Sri Yantra generator: draws the nine interlocking triangles with their
2
+ lotus petals, circles and gated square as SVG, with color options and
3
+ PNG/SVG export. Plain JS, no dependencies. Triangle data: yantra-data.js. */
4
+
5
+ const $ = (id) => document.getElementById(id);
6
+
7
+ const PALETTES = [
8
+ { id: "classic", label: "Gold", bg: "#1a212d", line: "#f6e3bf", shiva: "#f0a830", shakti: "#e0533d", lotus8: "#c47a3a", lotus16: "#e0a458", gate: "#b8893c", bindu: "#ff5a3c" },
9
+ { id: "temple", label: "Temple", bg: "#fbf1dc", line: "#4a1f0c", shiva: "#e59a1a", shakti: "#b3261e", lotus8: "#e8892f", lotus16: "#f3c45a", gate: "#8c3b1e", bindu: "#b3261e" },
10
+ { id: "night", label: "Night", bg: "#0f1220", line: "#d6dcff", shiva: "#6f8dff", shakti: "#c076ff", lotus8: "#4d68d8", lotus16: "#8b6ee8", gate: "#5b6fbf", bindu: "#ffd76a" },
11
+ { id: "ink", label: "Ink", bg: "#ffffff", line: "#141414", shiva: "#9a9a9a", shakti: "#4a4a4a", lotus8: "#7a7a7a", lotus16: "#b5b5b5", gate: "#666666", bindu: "#141414" },
12
+ { id: "saffron", label: "Saffron", bg: "#2b0f0f", line: "#f7dfb0", shiva: "#f0973a", shakti: "#c81d3f", lotus8: "#d9822b", lotus16: "#f2b545", gate: "#8c2f1f", bindu: "#ffce54" },
13
+ { id: "emerald", label: "Emerald", bg: "#0b1f18", line: "#dff5e6", shiva: "#38b28a", shakti: "#1f7a4c", lotus8: "#3f9d6b", lotus16: "#7fd2a0", gate: "#2f6b52", bindu: "#ffe27a" },
14
+ { id: "slate", label: "Slate", bg: "#14171c", line: "#eef0f3", shiva: "#9aa4b2", shakti: "#5f6b7a", lotus8: "#7c8794", lotus16: "#b4bcc6", gate: "#3f4750", bindu: "#ffffff" },
15
+ { id: "parchment", label: "Parchment", bg: "#f2e6c9", line: "#4a3419", shiva: "#9a6a30", shakti: "#7a4420", lotus8: "#b5872f", lotus16: "#d4b26a", gate: "#5c3d1e", bindu: "#8a1f1f" },
16
+ { id: "rose", label: "Rose", bg: "#22131f", line: "#ffe6ef", shiva: "#e8829e", shakti: "#b84f80", lotus8: "#d97398", lotus16: "#f0aac2", gate: "#8a3f66", bindu: "#fff2df" },
17
+ ];
18
+ const FILLS = [
19
+ { id: "filled", label: "Filled" },
20
+ { id: "lines", label: "Outline" },
21
+ ];
22
+ const WHICH = [
23
+ { id: "both", label: "Both" },
24
+ { id: "up", label: "Shiva (upward)" },
25
+ { id: "down", label: "Shakti (downward)" },
26
+ ];
27
+ const ENCLOSURES = [
28
+ ["Trailokya Mohana", "the square with four gates: enchanter of the three worlds"],
29
+ ["Sarva Asha Paripuraka", "the sixteen petals: fulfiller of all hopes"],
30
+ ["Sarva Sankshobhana", "the eight petals: agitator of all"],
31
+ ["Sarva Saubhagyadayaka", "the fourteen triangles: giver of all good fortune"],
32
+ ["Sarvartha Sadhaka", "the outer ten triangles: accomplisher of all aims"],
33
+ ["Sarva Rakshakara", "the inner ten triangles: protector of all"],
34
+ ["Sarva Rogahara", "the eight triangles: remover of all disease"],
35
+ ["Sarva Siddhiprada", "the central triangle: bestower of all powers"],
36
+ ["Sarva Anandamaya", "the bindu: full of all bliss"],
37
+ ];
38
+
39
+ const DEFAULTS = {
40
+ n: 9,
41
+ which: "both",
42
+ bindu: true,
43
+ tri: true,
44
+ lotus8: true,
45
+ lotus16: true,
46
+ circles: true,
47
+ gates: true,
48
+ palette: "classic",
49
+ fill: "filled",
50
+ weight: 10,
51
+ };
52
+ let S = { ...DEFAULTS };
53
+ try {
54
+ Object.assign(S, JSON.parse(localStorage.getItem("yantra-settings") || "{}"));
55
+ } catch (e) {}
56
+ const save = () => {
57
+ try {
58
+ localStorage.setItem("yantra-settings", JSON.stringify(S));
59
+ } catch (e) {}
60
+ };
61
+
62
+ // Triangles as drawn, in a space centered on (0, 0)
63
+ const TRIS = YANTRA_TRIANGLES.map(({ id, data: [l, b, a, r, up] }) => ({
64
+ id,
65
+ up: !!up,
66
+ width: r - l,
67
+ pts: [
68
+ [l - 150, b - 150],
69
+ [r - 150, b - 150],
70
+ [0, a - 150],
71
+ ],
72
+ }));
73
+ const BY_SIZE = [...TRIS].sort((p, q) => p.width - q.width);
74
+
75
+ const pol = (deg, r) => {
76
+ const a = (deg * Math.PI) / 180;
77
+ return [r * Math.cos(a), -r * Math.sin(a)]; // y-up angles, y-down screen
78
+ };
79
+ const P = (p) => p[0].toFixed(2) + " " + p[1].toFixed(2);
80
+
81
+ // One lotus ring. petals: 8 or 16; r0 is the inner radius, r1 the petal tip.
82
+ const lotusPath = (count, r0, r1, controls) => {
83
+ const step = 360 / count;
84
+ let d = "";
85
+ for (let i = 0; i < count; i++) {
86
+ const start = step / 2 + step * i;
87
+ const [[a1, k1], [a2, k2]] = controls;
88
+ const at = (delta, r) => pol(start - delta, r);
89
+ d +=
90
+ `M${P(at(0, r0))}` +
91
+ `C${P(at(a1, k1))} ${P(at(a2, k2))} ${P(at(step / 2, r1))}` +
92
+ `C${P(at(step - a2, k2))} ${P(at(step - a1, k1))} ${P(at(step, r0))}` +
93
+ `A${r0} ${r0} 0 0 0 ${P(at(0, r0))}Z`;
94
+ }
95
+ return d;
96
+ };
97
+
98
+ // One outline of the gated square: half side h, gate half width g, gate depth d.
99
+ const gatePolygon = (h, g, d) => {
100
+ const pts = [];
101
+ for (let k = 0; k < 4; k++) {
102
+ // corner, gate in, gate out, ... for the top side, then rotate by 90 degrees each time
103
+ const side = [
104
+ [-h, -h], [-g, -h], [-g, -h - d], [g, -h - d], [g, -h],
105
+ ];
106
+ side.forEach(([x, y]) => {
107
+ for (let i = 0; i < k; i++) [x, y] = [-y, x];
108
+ pts.push([x, y]);
109
+ });
110
+ }
111
+ return "M" + pts.map(P).join("L") + "Z";
112
+ };
113
+
114
+ const paletteOf = () => PALETTES.find((p) => p.id === S.palette) || PALETTES[0];
115
+
116
+ const triCount = () => Math.max(0, Math.min(9, S.n));
117
+ const shownTriangles = () => {
118
+ const list = BY_SIZE.slice(0, triCount());
119
+ if (S.which === "up") return list.filter((t) => t.up);
120
+ if (S.which === "down") return list.filter((t) => !t.up);
121
+ return list;
122
+ };
123
+
124
+ const yantraSVG = () => {
125
+ const c = paletteOf();
126
+ const w = S.weight / 10;
127
+ const filled = S.fill === "filled";
128
+ const V = 215;
129
+ const stroke = (sw = 1, op = 1) => `stroke="${c.line}" stroke-width="${(w * sw).toFixed(2)}" stroke-opacity="${op}" stroke-linejoin="round"`;
130
+ let out = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${-V} ${-V} ${2 * V} ${2 * V}" class="plain" role="img" aria-label="Sri Yantra">`;
131
+ out += `<rect x="${-V}" y="${-V}" width="${2 * V}" height="${2 * V}" fill="${c.bg}"/>`;
132
+
133
+ if (S.gates) {
134
+ // three nested outlines; the band between the outer two is filled
135
+ const h = 168, g = 28, d = 26, gap = 7;
136
+ if (filled) {
137
+ out += `<path d="${gatePolygon(h + 2 * gap, g + 2 * gap, d)} ${gatePolygon(h, g, d)}" fill-rule="evenodd" fill="${c.gate}" fill-opacity="0.55"/>`;
138
+ }
139
+ for (let i = 0; i < 3; i++) {
140
+ out += `<path d="${gatePolygon(h + i * gap, g + i * gap, d)}" fill="none" ${stroke(i === 1 ? 0.6 : 1)}/>`;
141
+ }
142
+ }
143
+ if (S.circles) {
144
+ [140, 147, 154].forEach((r, i) => {
145
+ out += `<circle r="${r}" fill="none" ${stroke(i === 0 ? 1 : 0.6)}/>`;
146
+ });
147
+ }
148
+ if (S.lotus16) {
149
+ out += `<path d="${lotusPath(16, 120, 140, [[2.5, 134], [8.75, 130]])}" fill="${filled ? c.lotus16 : "none"}" fill-opacity="0.6" ${stroke()}/>`;
150
+ }
151
+ if (S.lotus8) {
152
+ out += `<path d="${lotusPath(8, 100, 120, [[5, 114], [17.5, 110]])}" fill="${filled ? c.lotus8 : "none"}" fill-opacity="0.65" ${stroke()}/>`;
153
+ }
154
+ if (S.lotus8 || S.lotus16 || S.tri) {
155
+ out += `<circle r="100" fill="none" ${stroke()}/>`;
156
+ }
157
+ if (S.tri) {
158
+ // largest first so the small ones read on top
159
+ [...shownTriangles()].reverse().forEach((t) => {
160
+ const [a, b, top] = t.pts;
161
+ const col = t.up ? c.shiva : c.shakti;
162
+ out += `<path d="M${P(a)}L${P(b)}L${P(top)}Z" fill="${filled ? col : "none"}" fill-opacity="0.5" ${stroke(0.9)}/>`;
163
+ });
164
+ }
165
+ if (S.bindu) {
166
+ out += `<circle r="4.5" fill="${c.bindu}" ${stroke(0.5)}/>`;
167
+ }
168
+ return out + "</svg>";
169
+ };
170
+
171
+ const render = () => {
172
+ $("yantra").innerHTML = yantraSVG();
173
+ $("tri-n-val").textContent = triCount() + " of 9";
174
+ const shown = S.tri ? shownTriangles() : [];
175
+ const up = shown.filter((t) => t.up).length;
176
+ $("yantra-note").textContent = S.tri
177
+ ? `${shown.length} triangle${shown.length === 1 ? "" : "s"} drawn: ${up} upward, ${shown.length - up} downward.`
178
+ : "Triangles hidden.";
179
+ document.querySelectorAll("#which-chips button").forEach((b, i) => {
180
+ const on = WHICH[i].id === S.which;
181
+ b.className = on ? "selected" : "";
182
+ b.setAttribute("aria-pressed", on);
183
+ });
184
+ document.querySelectorAll("#pal-chips button").forEach((b, i) => {
185
+ const on = PALETTES[i].id === S.palette;
186
+ b.className = on ? "selected" : "";
187
+ b.setAttribute("aria-pressed", on);
188
+ });
189
+ document.querySelectorAll("#fill-chips button").forEach((b, i) => {
190
+ const on = FILLS[i].id === S.fill;
191
+ b.className = on ? "selected" : "";
192
+ b.setAttribute("aria-pressed", on);
193
+ });
194
+ $("weight-val").textContent = (S.weight / 10).toFixed(1);
195
+ $("tri-n").value = S.n;
196
+ };
197
+
198
+ // ---------------------------------------------------------------- play
199
+
200
+ let playTimer = null;
201
+ const stopPlay = () => {
202
+ clearInterval(playTimer);
203
+ playTimer = null;
204
+ $("play").textContent = "Build it up";
205
+ };
206
+ const play = () => {
207
+ if (playTimer) return stopPlay();
208
+ S.n = 0;
209
+ S.tri = true;
210
+ $("l-tri").checked = true;
211
+ render();
212
+ $("play").textContent = "Stop";
213
+ playTimer = setInterval(() => {
214
+ S.n += 1;
215
+ render();
216
+ if (S.n >= 9) {
217
+ stopPlay();
218
+ save();
219
+ }
220
+ }, 700);
221
+ };
222
+
223
+ // ---------------------------------------------------------------- export
224
+
225
+ const download = (blob, name) => {
226
+ const a = document.createElement("a");
227
+ a.href = URL.createObjectURL(blob);
228
+ a.download = name;
229
+ document.body.appendChild(a);
230
+ a.click();
231
+ a.remove();
232
+ setTimeout(() => URL.revokeObjectURL(a.href), 2000);
233
+ };
234
+ const exportSVG = () => {
235
+ const svg = yantraSVG().replace(' class="plain"', "");
236
+ download(new Blob([svg], { type: "image/svg+xml" }), "sri-yantra.svg");
237
+ };
238
+ const exportPNG = () => {
239
+ const px = 2000;
240
+ const svg = yantraSVG()
241
+ .replace(' class="plain"', "")
242
+ .replace("<svg ", `<svg width="${px}" height="${px}" `);
243
+ const img = new Image();
244
+ img.onload = () => {
245
+ const c = document.createElement("canvas");
246
+ c.width = c.height = px;
247
+ c.getContext("2d").drawImage(img, 0, 0, px, px);
248
+ c.toBlob((b) => download(b, "sri-yantra.png"));
249
+ };
250
+ img.src = "data:image/svg+xml;charset=utf-8," + encodeURIComponent(svg);
251
+ };
252
+
253
+ // ---------------------------------------------------------------- UI
254
+
255
+ const chips = (id, items, pick) => {
256
+ const box = $(id);
257
+ box.innerHTML = "";
258
+ items.forEach((it) => {
259
+ const b = document.createElement("button");
260
+ b.type = "button";
261
+ b.textContent = it.label;
262
+ b.onclick = () => pick(it);
263
+ box.appendChild(b);
264
+ });
265
+ };
266
+
267
+ const initYantra = () => {
268
+ S.n = Math.max(0, Math.min(9, Math.round(S.n)));
269
+ if (!PALETTES.some((p) => p.id === S.palette)) S.palette = "classic";
270
+
271
+ $("tri-n").oninput = () => {
272
+ stopPlay();
273
+ S.n = +$("tri-n").value;
274
+ save();
275
+ render();
276
+ };
277
+ chips("which-chips", WHICH, (w) => { S.which = w.id; save(); render(); });
278
+ chips("pal-chips", PALETTES, (p) => { S.palette = p.id; save(); render(); });
279
+ chips("fill-chips", FILLS, (f) => { S.fill = f.id; save(); render(); });
280
+
281
+ ["bindu", "tri", "lotus8", "lotus16", "circles", "gates"].forEach((k) => {
282
+ const el = $("l-" + k);
283
+ el.checked = !!S[k];
284
+ el.onchange = () => {
285
+ S[k] = el.checked;
286
+ save();
287
+ render();
288
+ };
289
+ });
290
+ const wt = $("weight");
291
+ wt.value = S.weight;
292
+ wt.oninput = () => {
293
+ S.weight = +wt.value;
294
+ save();
295
+ render();
296
+ };
297
+ $("play").onclick = play;
298
+ $("export-png").onclick = exportPNG;
299
+ $("export-svg").onclick = exportSVG;
300
+
301
+ $("enclosures").innerHTML = ENCLOSURES.map(
302
+ ([sa, en]) => `<li><span class="sa">${sa}</span> <span class="en">&ndash; ${en}</span></li>`
303
+ ).join("");
304
+ render();
305
+ };