@rizom/site-rizom 0.2.0-alpha.142
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/dist/index.d.ts +135 -0
- package/dist/index.js +31089 -0
- package/dist/index.js.map +259 -0
- package/package.json +54 -0
- package/src/content.ts +9 -0
- package/src/contracts.ts +157 -0
- package/src/create-site.ts +108 -0
- package/src/index.ts +63 -0
- package/src/runtime/base-site.ts +13 -0
- package/src/runtime/boot/boot.boot.d.ts +2 -0
- package/src/runtime/boot/boot.boot.js +83 -0
- package/src/runtime/canvases/constellation.canvas.d.ts +2 -0
- package/src/runtime/canvases/constellation.canvas.js +307 -0
- package/src/runtime/canvases/prelude.canvas.d.ts +2 -0
- package/src/runtime/canvases/prelude.canvas.js +193 -0
- package/src/runtime/canvases/products.canvas.d.ts +2 -0
- package/src/runtime/canvases/roots.canvas.d.ts +2 -0
- package/src/runtime/canvases/roots.canvas.js +358 -0
- package/src/runtime/canvases/tree.canvas.d.ts +2 -0
- package/src/runtime/canvases/tree.canvas.js +544 -0
- package/src/runtime/default-layout.tsx +10 -0
- package/src/runtime/index.ts +8 -0
- package/src/runtime/plugin.ts +147 -0
- package/src/ui/Badge.tsx +14 -0
- package/src/ui/Button.tsx +56 -0
- package/src/ui/Divider.tsx +12 -0
- package/src/ui/Footer.tsx +76 -0
- package/src/ui/Header.tsx +45 -0
- package/src/ui/Section.tsx +22 -0
- package/src/ui/SideNav.tsx +22 -0
- package/src/ui/cn.ts +1 -0
- package/src/ui/external-link.ts +8 -0
- package/src/ui/frame.tsx +28 -0
- package/src/ui/highlighted-text.tsx +45 -0
- package/src/ui/index.ts +21 -0
- package/src/ui/site-info-links.ts +24 -0
- package/src/ui/types.ts +6 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const canvas = document.getElementById("heroCanvas");
|
|
3
|
+
if (!canvas) return;
|
|
4
|
+
let W = window.innerWidth,
|
|
5
|
+
H = window.innerHeight;
|
|
6
|
+
canvas.width = W * dpr;
|
|
7
|
+
canvas.height = H * dpr;
|
|
8
|
+
canvas.style.width = "100%";
|
|
9
|
+
canvas.style.height = "100%";
|
|
10
|
+
const ctx = canvas.getContext("2d");
|
|
11
|
+
ctx.scale(dpr, dpr);
|
|
12
|
+
|
|
13
|
+
let stars = [];
|
|
14
|
+
let packets = []; // energy traveling between connected stars
|
|
15
|
+
let shooters = []; // shooting stars
|
|
16
|
+
let theme = null;
|
|
17
|
+
let lastPacket = 0;
|
|
18
|
+
let lastShooter = 0;
|
|
19
|
+
let lastFlare = 0;
|
|
20
|
+
|
|
21
|
+
function build() {
|
|
22
|
+
theme = isLightMode();
|
|
23
|
+
stars = [];
|
|
24
|
+
packets = [];
|
|
25
|
+
shooters = [];
|
|
26
|
+
const rng = createRand(42);
|
|
27
|
+
// Higher density on small viewports — same px-per-star formula gives
|
|
28
|
+
// too few stars on phones to form a visible network
|
|
29
|
+
const density = W < 768 ? 4500 : 8500;
|
|
30
|
+
const count = Math.floor((W * H) / density);
|
|
31
|
+
for (let i = 0; i < count; i++) {
|
|
32
|
+
const tier = rng.next();
|
|
33
|
+
stars.push({
|
|
34
|
+
x: rng.range(0, W),
|
|
35
|
+
y: rng.range(0, H),
|
|
36
|
+
r:
|
|
37
|
+
tier < 0.7
|
|
38
|
+
? rng.range(0.4, 1.0)
|
|
39
|
+
: tier < 0.95
|
|
40
|
+
? rng.range(1.0, 1.8)
|
|
41
|
+
: rng.range(1.8, 2.6),
|
|
42
|
+
vx: rng.range(-0.35, 0.35),
|
|
43
|
+
vy: rng.range(-0.3, 0.2),
|
|
44
|
+
phase: rng.next() * Math.PI * 2,
|
|
45
|
+
speed: rng.range(0.5, 1.5),
|
|
46
|
+
color:
|
|
47
|
+
rng.next() < 0.6
|
|
48
|
+
? C.GLOW
|
|
49
|
+
: rng.next() < 0.55
|
|
50
|
+
? C.AMBER_LT
|
|
51
|
+
: C.PURPLE_LT,
|
|
52
|
+
baseAlpha: rng.range(0.3, 0.78),
|
|
53
|
+
flare: 0, // 0..1, decays over time when set
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function spawnPacket() {
|
|
59
|
+
if (stars.length < 2) return;
|
|
60
|
+
// Pick a random star, then a neighbor within connection range
|
|
61
|
+
// Match the connection radius logic in frame() so packets spawn on every viewport
|
|
62
|
+
const avgSpacing = Math.sqrt((W * H) / Math.max(1, stars.length));
|
|
63
|
+
const maxD = avgSpacing * 1.7;
|
|
64
|
+
const maxD2 = maxD * maxD;
|
|
65
|
+
let tries = 12;
|
|
66
|
+
while (tries-- > 0) {
|
|
67
|
+
const i = Math.floor(Math.random() * stars.length);
|
|
68
|
+
const a = stars[i];
|
|
69
|
+
// Find a connected neighbor
|
|
70
|
+
const neighbors = [];
|
|
71
|
+
for (let j = 0; j < stars.length; j++) {
|
|
72
|
+
if (j === i) continue;
|
|
73
|
+
const b = stars[j];
|
|
74
|
+
const dx = a.x - b.x;
|
|
75
|
+
const dy = a.y - b.y;
|
|
76
|
+
if (dx * dx + dy * dy < maxD2) neighbors.push(j);
|
|
77
|
+
}
|
|
78
|
+
if (!neighbors.length) continue;
|
|
79
|
+
const to = neighbors[Math.floor(Math.random() * neighbors.length)];
|
|
80
|
+
packets.push({
|
|
81
|
+
from: i,
|
|
82
|
+
to,
|
|
83
|
+
progress: 0,
|
|
84
|
+
speed: 0.7 + Math.random() * 0.6,
|
|
85
|
+
color: Math.random() < 0.7 ? C.GLOW : C.PURPLE_LT,
|
|
86
|
+
});
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function spawnShooter() {
|
|
92
|
+
// Origin somewhere along top/left edge, direction toward bottom-right-ish
|
|
93
|
+
const fromTop = Math.random() < 0.5;
|
|
94
|
+
const startX = fromTop ? Math.random() * W : -40;
|
|
95
|
+
const startY = fromTop ? -40 : Math.random() * H * 0.6;
|
|
96
|
+
const angle = Math.PI / 4 + (Math.random() - 0.5) * 0.5; // ~45° + jitter
|
|
97
|
+
const speed = 380 + Math.random() * 220; // px/sec
|
|
98
|
+
shooters.push({
|
|
99
|
+
x: startX,
|
|
100
|
+
y: startY,
|
|
101
|
+
vx: Math.cos(angle) * speed,
|
|
102
|
+
vy: Math.sin(angle) * speed,
|
|
103
|
+
life: 0,
|
|
104
|
+
maxLife: 1.0 + Math.random() * 0.6,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function triggerFlare() {
|
|
109
|
+
if (!stars.length) return;
|
|
110
|
+
// Prefer larger stars for flares so it reads more clearly
|
|
111
|
+
const candidates = stars.filter((s) => s.r > 1.0);
|
|
112
|
+
const pool = candidates.length ? candidates : stars;
|
|
113
|
+
const s = pool[Math.floor(Math.random() * pool.length)];
|
|
114
|
+
s.flare = 1.0;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
build();
|
|
118
|
+
window._heroRebuild = function () {
|
|
119
|
+
build();
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
let t = 0,
|
|
123
|
+
last = 0;
|
|
124
|
+
function frame(ts) {
|
|
125
|
+
if (document.hidden) {
|
|
126
|
+
requestAnimationFrame(frame);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
const dt = last ? Math.min((ts - last) / 1000, 0.05) : 0.016;
|
|
130
|
+
last = ts;
|
|
131
|
+
t += dt;
|
|
132
|
+
|
|
133
|
+
const light = isLightMode();
|
|
134
|
+
if (light !== theme) build();
|
|
135
|
+
|
|
136
|
+
ctx.clearRect(0, 0, W, H);
|
|
137
|
+
|
|
138
|
+
// Drift with gentle random walk on velocity
|
|
139
|
+
const maxV = 0.55;
|
|
140
|
+
for (const s of stars) {
|
|
141
|
+
// Tiny random nudge each frame so stars don't move in perfect straight lines
|
|
142
|
+
s.vx += (Math.random() - 0.5) * 0.012;
|
|
143
|
+
s.vy += (Math.random() - 0.5) * 0.012;
|
|
144
|
+
// Soft clamp so velocities stay bounded
|
|
145
|
+
if (s.vx > maxV) s.vx = maxV;
|
|
146
|
+
else if (s.vx < -maxV) s.vx = -maxV;
|
|
147
|
+
if (s.vy > maxV) s.vy = maxV;
|
|
148
|
+
else if (s.vy < -maxV) s.vy = -maxV;
|
|
149
|
+
s.x += s.vx;
|
|
150
|
+
s.y += s.vy;
|
|
151
|
+
if (s.x < -10) s.x = W + 10;
|
|
152
|
+
else if (s.x > W + 10) s.x = -10;
|
|
153
|
+
if (s.y < -10) s.y = H + 10;
|
|
154
|
+
else if (s.y > H + 10) s.y = -10;
|
|
155
|
+
if (s.flare > 0) s.flare = Math.max(0, s.flare - dt * 0.9);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Connection lines (nearest-neighbor mesh)
|
|
159
|
+
// Tie connection radius to actual star spacing so the mesh works on
|
|
160
|
+
// any viewport size, not just wide ones
|
|
161
|
+
const avgSpacing = Math.sqrt((W * H) / Math.max(1, stars.length));
|
|
162
|
+
const maxD = avgSpacing * 1.7;
|
|
163
|
+
const maxD2 = maxD * maxD;
|
|
164
|
+
ctx.lineWidth = W < 768 ? 0.85 : 0.6;
|
|
165
|
+
for (let i = 0; i < stars.length; i++) {
|
|
166
|
+
const a = stars[i];
|
|
167
|
+
for (let j = i + 1; j < stars.length; j++) {
|
|
168
|
+
const b = stars[j];
|
|
169
|
+
const dx = a.x - b.x;
|
|
170
|
+
const dy = a.y - b.y;
|
|
171
|
+
const d2 = dx * dx + dy * dy;
|
|
172
|
+
if (d2 < maxD2) {
|
|
173
|
+
const f = 1 - d2 / maxD2;
|
|
174
|
+
const op = f * f * (light ? 0.32 : 0.22);
|
|
175
|
+
ctx.strokeStyle = rgba(light ? C.AMBER_DK : C.GLOW, op);
|
|
176
|
+
ctx.beginPath();
|
|
177
|
+
ctx.moveTo(a.x, a.y);
|
|
178
|
+
ctx.lineTo(b.x, b.y);
|
|
179
|
+
ctx.stroke();
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Spawn + animate packets along connection lines
|
|
185
|
+
if (ts - lastPacket > 180 && packets.length < 28) {
|
|
186
|
+
lastPacket = ts;
|
|
187
|
+
spawnPacket();
|
|
188
|
+
if (Math.random() < 0.4) spawnPacket();
|
|
189
|
+
}
|
|
190
|
+
for (let i = packets.length - 1; i >= 0; i--) {
|
|
191
|
+
const p = packets[i];
|
|
192
|
+
const a = stars[p.from],
|
|
193
|
+
b = stars[p.to];
|
|
194
|
+
if (!a || !b) {
|
|
195
|
+
packets.splice(i, 1);
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
p.progress += dt * p.speed;
|
|
199
|
+
if (p.progress >= 1) {
|
|
200
|
+
// On arrival, occasionally flare the destination star
|
|
201
|
+
if (Math.random() < 0.18) b.flare = Math.max(b.flare, 0.7);
|
|
202
|
+
packets.splice(i, 1);
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
// Smooth easing for a more "energy traveling" feel
|
|
206
|
+
const e = p.progress * p.progress * (3 - 2 * p.progress);
|
|
207
|
+
const px = a.x + (b.x - a.x) * e;
|
|
208
|
+
const py = a.y + (b.y - a.y) * e;
|
|
209
|
+
// Fade in/out at ends
|
|
210
|
+
const fade = Math.sin(p.progress * Math.PI);
|
|
211
|
+
ctx.fillStyle = rgba(p.color, (light ? 0.55 : 0.42) * fade);
|
|
212
|
+
ctx.beginPath();
|
|
213
|
+
ctx.arc(px, py, 6, 0, Math.PI * 2);
|
|
214
|
+
ctx.fill();
|
|
215
|
+
ctx.fillStyle = rgba(light ? C.AMBER_DK : C.CORE, 0.85 * fade);
|
|
216
|
+
ctx.beginPath();
|
|
217
|
+
ctx.arc(px, py, 1.4, 0, Math.PI * 2);
|
|
218
|
+
ctx.fill();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Trigger random flares (separate from packet-induced ones)
|
|
222
|
+
if (ts - lastFlare > 1400 && Math.random() < 0.35) {
|
|
223
|
+
lastFlare = ts;
|
|
224
|
+
triggerFlare();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Spawn + animate shooting stars
|
|
228
|
+
if (ts - lastShooter > 4500 && Math.random() < 0.4) {
|
|
229
|
+
lastShooter = ts;
|
|
230
|
+
spawnShooter();
|
|
231
|
+
}
|
|
232
|
+
for (let i = shooters.length - 1; i >= 0; i--) {
|
|
233
|
+
const sh = shooters[i];
|
|
234
|
+
sh.x += sh.vx * dt;
|
|
235
|
+
sh.y += sh.vy * dt;
|
|
236
|
+
sh.life += dt;
|
|
237
|
+
if (sh.life > sh.maxLife || sh.x > W + 80 || sh.y > H + 80) {
|
|
238
|
+
shooters.splice(i, 1);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const lifeRatio = sh.life / sh.maxLife;
|
|
242
|
+
// Tail
|
|
243
|
+
const tailLen = 90;
|
|
244
|
+
const tx = sh.x - Math.cos(Math.atan2(sh.vy, sh.vx)) * tailLen;
|
|
245
|
+
const ty = sh.y - Math.sin(Math.atan2(sh.vy, sh.vx)) * tailLen;
|
|
246
|
+
const grad = ctx.createLinearGradient(tx, ty, sh.x, sh.y);
|
|
247
|
+
const alpha = (1 - lifeRatio) * (light ? 0.85 : 0.85);
|
|
248
|
+
grad.addColorStop(0, rgba(light ? C.AMBER_DK : C.GLOW, 0));
|
|
249
|
+
grad.addColorStop(1, rgba(light ? C.AMBER_DK : C.CORE, alpha));
|
|
250
|
+
ctx.strokeStyle = grad;
|
|
251
|
+
ctx.lineWidth = 1.6;
|
|
252
|
+
ctx.lineCap = "round";
|
|
253
|
+
ctx.beginPath();
|
|
254
|
+
ctx.moveTo(tx, ty);
|
|
255
|
+
ctx.lineTo(sh.x, sh.y);
|
|
256
|
+
ctx.stroke();
|
|
257
|
+
// Bright head
|
|
258
|
+
ctx.fillStyle = rgba(light ? C.AMBER_DK : C.CORE, alpha);
|
|
259
|
+
ctx.beginPath();
|
|
260
|
+
ctx.arc(sh.x, sh.y, 1.8, 0, Math.PI * 2);
|
|
261
|
+
ctx.fill();
|
|
262
|
+
// Soft halo
|
|
263
|
+
ctx.fillStyle = rgba(light ? C.AMBER : C.GLOW, alpha * 0.25);
|
|
264
|
+
ctx.beginPath();
|
|
265
|
+
ctx.arc(sh.x, sh.y, 6, 0, Math.PI * 2);
|
|
266
|
+
ctx.fill();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// Stars on top with twinkle (+ flare boost)
|
|
270
|
+
for (const s of stars) {
|
|
271
|
+
const tw = 0.55 + 0.45 * Math.sin(t * s.speed + s.phase);
|
|
272
|
+
const flareBoost = 1 + s.flare * 2.2;
|
|
273
|
+
const a = s.baseAlpha * tw * flareBoost;
|
|
274
|
+
const haloR = s.r * (4 + s.flare * 6);
|
|
275
|
+
// halo
|
|
276
|
+
ctx.fillStyle = rgba(s.color, Math.min(1, a) * (light ? 0.22 : 0.18));
|
|
277
|
+
ctx.beginPath();
|
|
278
|
+
ctx.arc(s.x, s.y, haloR, 0, Math.PI * 2);
|
|
279
|
+
ctx.fill();
|
|
280
|
+
// core
|
|
281
|
+
ctx.fillStyle = rgba(light ? C.AMBER_DK : s.color, Math.min(1, a));
|
|
282
|
+
ctx.beginPath();
|
|
283
|
+
ctx.arc(s.x, s.y, s.r * (1 + s.flare * 0.6), 0, Math.PI * 2);
|
|
284
|
+
ctx.fill();
|
|
285
|
+
// hot center for flaring stars
|
|
286
|
+
if (s.flare > 0.05) {
|
|
287
|
+
ctx.fillStyle = rgba(C.CORE, s.flare * 0.9);
|
|
288
|
+
ctx.beginPath();
|
|
289
|
+
ctx.arc(s.x, s.y, s.r * 0.5, 0, Math.PI * 2);
|
|
290
|
+
ctx.fill();
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
requestAnimationFrame(frame);
|
|
295
|
+
}
|
|
296
|
+
requestAnimationFrame(frame);
|
|
297
|
+
|
|
298
|
+
window.addEventListener("resize", () => {
|
|
299
|
+
W = window.innerWidth;
|
|
300
|
+
H = window.innerHeight;
|
|
301
|
+
canvas.width = W * dpr;
|
|
302
|
+
canvas.height = H * dpr;
|
|
303
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
304
|
+
ctx.scale(dpr, dpr);
|
|
305
|
+
build();
|
|
306
|
+
});
|
|
307
|
+
})();
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Shared globals + helpers used by tree / constellation / roots canvas
|
|
3
|
+
* scripts. Loaded as a shared static asset by the Rizom runtime package
|
|
4
|
+
* so the variant canvases
|
|
5
|
+
* stay byte-equivalent to docs/design/canvases/ and can be re-synced
|
|
6
|
+
* from the design source without merge conflicts.
|
|
7
|
+
*
|
|
8
|
+
* One difference from the design mock: `isLightMode()` reads the
|
|
9
|
+
* brain's `[data-theme="light"]` attribute on documentElement instead
|
|
10
|
+
* of the mock's `body.classList.contains('light')` — the brain's
|
|
11
|
+
* theme toggle uses the data-attribute convention.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function createRand(seed) {
|
|
15
|
+
let s = seed;
|
|
16
|
+
return {
|
|
17
|
+
next() {
|
|
18
|
+
s = (s * 16807) % 2147483647;
|
|
19
|
+
return s / 2147483647;
|
|
20
|
+
},
|
|
21
|
+
range(a, b) {
|
|
22
|
+
return a + this.next() * (b - a);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Brand palette — resolved from CSS custom properties on :root at
|
|
28
|
+
// load time so theme-rizom/src/theme.css stays the single source of
|
|
29
|
+
// truth. Each slot falls back to a hardcoded default if the CSS var
|
|
30
|
+
// isn't available yet (first-paint edge case, test sandbox, etc.).
|
|
31
|
+
//
|
|
32
|
+
// WARM + CORE are NOT brand tokens — they're canvas-only particle
|
|
33
|
+
// colors (warm halo, bright core highlight) that live here because
|
|
34
|
+
// they only have meaning inside the glow rendering, not in the rest
|
|
35
|
+
// of the site UI.
|
|
36
|
+
function readPaletteFromCSS() {
|
|
37
|
+
const defaults = {
|
|
38
|
+
AMBER: "#E87722",
|
|
39
|
+
AMBER_LT: "#FFA366",
|
|
40
|
+
AMBER_DK: "#C45A08",
|
|
41
|
+
GLOW: "#FFD4A8",
|
|
42
|
+
PURPLE: "#6B2FA0",
|
|
43
|
+
PURPLE_LT: "#8C82C8",
|
|
44
|
+
PURPLE_MU: "#818CF8",
|
|
45
|
+
WHITE: "#FFFFFF",
|
|
46
|
+
BG_DEEP: "#0D0A1A",
|
|
47
|
+
};
|
|
48
|
+
const varMap = {
|
|
49
|
+
AMBER: "--palette-amber",
|
|
50
|
+
AMBER_LT: "--palette-amber-light",
|
|
51
|
+
AMBER_DK: "--palette-amber-dark",
|
|
52
|
+
GLOW: "--palette-amber-glow",
|
|
53
|
+
PURPLE: "--palette-purple",
|
|
54
|
+
PURPLE_LT: "--palette-purple-light",
|
|
55
|
+
PURPLE_MU: "--palette-purple-muted",
|
|
56
|
+
WHITE: "--palette-white",
|
|
57
|
+
BG_DEEP: "--palette-bg-deep",
|
|
58
|
+
};
|
|
59
|
+
const out = {};
|
|
60
|
+
let cs;
|
|
61
|
+
try {
|
|
62
|
+
cs = window.getComputedStyle(document.documentElement);
|
|
63
|
+
} catch (_e) {
|
|
64
|
+
cs = null;
|
|
65
|
+
}
|
|
66
|
+
for (const key in varMap) {
|
|
67
|
+
const cssName = varMap[key];
|
|
68
|
+
let value = "";
|
|
69
|
+
if (cs) {
|
|
70
|
+
try {
|
|
71
|
+
value = (cs.getPropertyValue(cssName) || "").trim();
|
|
72
|
+
} catch (_e) {
|
|
73
|
+
value = "";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
out[key] = value || defaults[key];
|
|
77
|
+
}
|
|
78
|
+
// Canvas-only non-brand colors — always hardcoded.
|
|
79
|
+
out.WARM = "#FFB366";
|
|
80
|
+
out.CORE = "#FFF8EE";
|
|
81
|
+
return out;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const C = readPaletteFromCSS();
|
|
85
|
+
|
|
86
|
+
const _rgbCache = {};
|
|
87
|
+
function parseHex(hex) {
|
|
88
|
+
if (_rgbCache[hex]) return _rgbCache[hex];
|
|
89
|
+
const v = [
|
|
90
|
+
parseInt(hex.slice(1, 3), 16),
|
|
91
|
+
parseInt(hex.slice(3, 5), 16),
|
|
92
|
+
parseInt(hex.slice(5, 7), 16),
|
|
93
|
+
];
|
|
94
|
+
_rgbCache[hex] = v;
|
|
95
|
+
return v;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function rgba(hex, a) {
|
|
99
|
+
const [r, g, b] = parseHex(hex);
|
|
100
|
+
return `rgba(${r},${g},${b},${Math.max(0, Math.min(1, a))})`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Brain convention: dark mode is the default; `[data-theme="light"]`
|
|
104
|
+
// on <html> opts into light mode. Mock used `body.classList.contains('light')`.
|
|
105
|
+
function isLightMode() {
|
|
106
|
+
return document.documentElement.getAttribute("data-theme") === "light";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const dpr = window.devicePixelRatio || 1;
|
|
110
|
+
|
|
111
|
+
function drawGlowBezier(ctx, pts, color, w, glowR, op, light) {
|
|
112
|
+
if (light) {
|
|
113
|
+
// Light mode: ink-like strokes with denser halo
|
|
114
|
+
for (let r = glowR * 0.7; r > 0; r -= Math.max(0.5, glowR / 12)) {
|
|
115
|
+
ctx.strokeStyle = rgba(color, op * (1 - r / (glowR * 0.7)) * 0.07);
|
|
116
|
+
ctx.lineWidth = w + r * 1.8;
|
|
117
|
+
ctx.lineCap = "round";
|
|
118
|
+
ctx.beginPath();
|
|
119
|
+
ctx.moveTo(pts[0], pts[1]);
|
|
120
|
+
ctx.bezierCurveTo(pts[2], pts[3], pts[4], pts[5], pts[6], pts[7]);
|
|
121
|
+
ctx.stroke();
|
|
122
|
+
}
|
|
123
|
+
ctx.strokeStyle = rgba(color, op * 0.65);
|
|
124
|
+
ctx.lineWidth = w;
|
|
125
|
+
ctx.lineCap = "round";
|
|
126
|
+
ctx.beginPath();
|
|
127
|
+
ctx.moveTo(pts[0], pts[1]);
|
|
128
|
+
ctx.bezierCurveTo(pts[2], pts[3], pts[4], pts[5], pts[6], pts[7]);
|
|
129
|
+
ctx.stroke();
|
|
130
|
+
} else {
|
|
131
|
+
// Dark mode: full glow
|
|
132
|
+
for (let r = glowR; r > 0; r -= Math.max(0.5, glowR / 20)) {
|
|
133
|
+
ctx.strokeStyle = rgba(color, op * (1 - r / glowR) * 0.08);
|
|
134
|
+
ctx.lineWidth = w + r * 2;
|
|
135
|
+
ctx.lineCap = "round";
|
|
136
|
+
ctx.beginPath();
|
|
137
|
+
ctx.moveTo(pts[0], pts[1]);
|
|
138
|
+
ctx.bezierCurveTo(pts[2], pts[3], pts[4], pts[5], pts[6], pts[7]);
|
|
139
|
+
ctx.stroke();
|
|
140
|
+
}
|
|
141
|
+
ctx.strokeStyle = rgba(color, op * 0.8);
|
|
142
|
+
ctx.lineWidth = w;
|
|
143
|
+
ctx.lineCap = "round";
|
|
144
|
+
ctx.beginPath();
|
|
145
|
+
ctx.moveTo(pts[0], pts[1]);
|
|
146
|
+
ctx.bezierCurveTo(pts[2], pts[3], pts[4], pts[5], pts[6], pts[7]);
|
|
147
|
+
ctx.stroke();
|
|
148
|
+
ctx.strokeStyle = rgba(C.GLOW, op * 0.3);
|
|
149
|
+
ctx.lineWidth = w * 0.35;
|
|
150
|
+
ctx.beginPath();
|
|
151
|
+
ctx.moveTo(pts[0], pts[1]);
|
|
152
|
+
ctx.bezierCurveTo(pts[2], pts[3], pts[4], pts[5], pts[6], pts[7]);
|
|
153
|
+
ctx.stroke();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function drawGlowNode(ctx, x, y, r, color, op, light) {
|
|
158
|
+
if (light) {
|
|
159
|
+
// Light mode: solid dots with richer spread
|
|
160
|
+
for (let i = r * 4; i > r; i -= 0.8) {
|
|
161
|
+
ctx.fillStyle = rgba(color, op * 0.05 * (1 - (i - r) / (r * 3)));
|
|
162
|
+
ctx.beginPath();
|
|
163
|
+
ctx.arc(x, y, i, 0, Math.PI * 2);
|
|
164
|
+
ctx.fill();
|
|
165
|
+
}
|
|
166
|
+
ctx.fillStyle = rgba(color, op * 0.7);
|
|
167
|
+
ctx.beginPath();
|
|
168
|
+
ctx.arc(x, y, r * 0.8, 0, Math.PI * 2);
|
|
169
|
+
ctx.fill();
|
|
170
|
+
ctx.fillStyle = rgba(color, op * 0.95);
|
|
171
|
+
ctx.beginPath();
|
|
172
|
+
ctx.arc(x, y, r * 0.4, 0, Math.PI * 2);
|
|
173
|
+
ctx.fill();
|
|
174
|
+
} else {
|
|
175
|
+
// Dark mode: full glow halos
|
|
176
|
+
for (let i = r * 8; i > r; i -= 1.5) {
|
|
177
|
+
ctx.fillStyle = rgba(color, op * 0.01 * (1 - (i - r) / (r * 7)));
|
|
178
|
+
ctx.beginPath();
|
|
179
|
+
ctx.arc(x, y, i, 0, Math.PI * 2);
|
|
180
|
+
ctx.fill();
|
|
181
|
+
}
|
|
182
|
+
for (let i = r * 2; i > 0; i -= 0.4) {
|
|
183
|
+
ctx.fillStyle = rgba(C.WARM, op * 0.08 * (1 - i / (r * 2)));
|
|
184
|
+
ctx.beginPath();
|
|
185
|
+
ctx.arc(x, y, i, 0, Math.PI * 2);
|
|
186
|
+
ctx.fill();
|
|
187
|
+
}
|
|
188
|
+
ctx.fillStyle = rgba(C.CORE, op * 0.85);
|
|
189
|
+
ctx.beginPath();
|
|
190
|
+
ctx.arc(x, y, r * 0.35, 0, Math.PI * 2);
|
|
191
|
+
ctx.fill();
|
|
192
|
+
}
|
|
193
|
+
}
|