@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,544 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
const canvas = document.getElementById("heroCanvas");
|
|
3
|
+
if (!canvas) return;
|
|
4
|
+
const W = window.innerWidth;
|
|
5
|
+
const 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
|
+
// Full document height for the tree
|
|
14
|
+
const docH = Math.max(document.body.scrollHeight, 5000);
|
|
15
|
+
// Tree trunk runs along the right third of the page
|
|
16
|
+
const trunkX = W * 0.7;
|
|
17
|
+
|
|
18
|
+
const stripH = Math.min(docH, 8000);
|
|
19
|
+
let staticCanvas = document.createElement("canvas");
|
|
20
|
+
staticCanvas.width = canvas.width;
|
|
21
|
+
staticCanvas.height = stripH * dpr;
|
|
22
|
+
|
|
23
|
+
let nodes = [];
|
|
24
|
+
let builtTheme = null;
|
|
25
|
+
// Captured at build time so the animation loop can send pulses along them
|
|
26
|
+
let trunkPath = [];
|
|
27
|
+
let branchPaths = []; // each entry: [x0,y0,c1x,c1y,c2x,c2y,x1,y1]
|
|
28
|
+
|
|
29
|
+
function buildStatic() {
|
|
30
|
+
const light = isLightMode();
|
|
31
|
+
if (builtTheme === light) return;
|
|
32
|
+
builtTheme = light;
|
|
33
|
+
nodes = [];
|
|
34
|
+
trunkPath = [];
|
|
35
|
+
branchPaths = [];
|
|
36
|
+
const rng = createRand(42);
|
|
37
|
+
// Completely replace the static canvas — no chance of state leak
|
|
38
|
+
staticCanvas = document.createElement("canvas");
|
|
39
|
+
staticCanvas.width = canvas.width;
|
|
40
|
+
staticCanvas.height = stripH * dpr;
|
|
41
|
+
const sctx = staticCanvas.getContext("2d");
|
|
42
|
+
sctx.scale(dpr, dpr);
|
|
43
|
+
|
|
44
|
+
// Ambient glows at different heights
|
|
45
|
+
const ambients = [
|
|
46
|
+
{ y: stripH * 0.08, r: W * 0.4 },
|
|
47
|
+
{ y: stripH * 0.35, r: W * 0.35 },
|
|
48
|
+
{ y: stripH * 0.6, r: W * 0.3 },
|
|
49
|
+
{ y: stripH * 0.85, r: W * 0.25 },
|
|
50
|
+
];
|
|
51
|
+
ambients.forEach((a) => {
|
|
52
|
+
const ag = sctx.createRadialGradient(trunkX, a.y, 0, trunkX, a.y, a.r);
|
|
53
|
+
if (!light) {
|
|
54
|
+
ag.addColorStop(0, rgba(C.AMBER, 0.04));
|
|
55
|
+
ag.addColorStop(0.5, rgba(C.PURPLE, 0.01));
|
|
56
|
+
ag.addColorStop(1, rgba(C.BG_DEEP, 0));
|
|
57
|
+
} else {
|
|
58
|
+
ag.addColorStop(0, rgba(C.AMBER_DK, 0.03));
|
|
59
|
+
ag.addColorStop(1, rgba(C.AMBER_DK, 0));
|
|
60
|
+
}
|
|
61
|
+
sctx.fillStyle = ag;
|
|
62
|
+
sctx.fillRect(0, a.y - a.r, W, a.r * 2);
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Branch helper
|
|
66
|
+
function branch(x, y, angle, len, w, depth, color, op) {
|
|
67
|
+
if (depth <= 0 || w < 0.15 || len < 4) return;
|
|
68
|
+
const j = rng.range(-0.15, 0.15);
|
|
69
|
+
const ex = x + Math.cos(angle + j) * len;
|
|
70
|
+
const ey = y + Math.sin(angle + j) * len;
|
|
71
|
+
const c1x = x + Math.cos(angle + rng.range(-0.4, 0.4)) * len * 0.35;
|
|
72
|
+
const c1y = y + Math.sin(angle + rng.range(-0.4, 0.4)) * len * 0.35;
|
|
73
|
+
const c2x = x + Math.cos(angle + rng.range(-0.25, 0.25)) * len * 0.7;
|
|
74
|
+
const c2y = y + Math.sin(angle + rng.range(-0.25, 0.25)) * len * 0.7;
|
|
75
|
+
drawGlowBezier(
|
|
76
|
+
sctx,
|
|
77
|
+
[x, y, c1x, c1y, c2x, c2y, ex, ey],
|
|
78
|
+
color,
|
|
79
|
+
w,
|
|
80
|
+
Math.max(2, w * 3),
|
|
81
|
+
op,
|
|
82
|
+
light,
|
|
83
|
+
);
|
|
84
|
+
// Capture substantial branches so the animation loop can run
|
|
85
|
+
// sap pulses along their bezier curves
|
|
86
|
+
if (w >= 1.4) {
|
|
87
|
+
branchPaths.push([x, y, c1x, c1y, c2x, c2y, ex, ey]);
|
|
88
|
+
}
|
|
89
|
+
if (depth <= 2) {
|
|
90
|
+
const nr = Math.max(0.5, w * 0.4);
|
|
91
|
+
drawGlowNode(sctx, ex, ey, nr, color, op * 0.4, light);
|
|
92
|
+
nodes.push({
|
|
93
|
+
x: ex,
|
|
94
|
+
y: ey,
|
|
95
|
+
r: nr,
|
|
96
|
+
color,
|
|
97
|
+
op: op * 0.4,
|
|
98
|
+
phase: rng.next() * Math.PI * 2,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
const nb =
|
|
102
|
+
depth > 3 ? Math.floor(rng.range(2, 3)) : Math.floor(rng.range(1, 3));
|
|
103
|
+
for (let i = 0; i < nb; i++) {
|
|
104
|
+
const sp = depth > 3 ? rng.range(0.2, 0.6) : rng.range(0.3, 0.9);
|
|
105
|
+
let nc = color;
|
|
106
|
+
if (depth <= 3 && rng.next() < 0.2) nc = C.PURPLE;
|
|
107
|
+
else if (rng.next() < 0.3) nc = nc === C.AMBER ? C.AMBER_LT : C.AMBER;
|
|
108
|
+
branch(
|
|
109
|
+
ex,
|
|
110
|
+
ey,
|
|
111
|
+
angle + rng.range(-sp, sp),
|
|
112
|
+
len * rng.range(0.45, 0.75),
|
|
113
|
+
w * rng.range(0.45, 0.7),
|
|
114
|
+
depth - 1,
|
|
115
|
+
nc,
|
|
116
|
+
op * rng.range(0.5, 0.8),
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// === MAIN TRUNK — continuous line from bottom to top ===
|
|
122
|
+
const segments = 22;
|
|
123
|
+
let prevX = trunkX + rng.range(-10, 10);
|
|
124
|
+
let prevY = stripH;
|
|
125
|
+
const trunkPoints = [{ x: prevX, y: prevY }];
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < segments; i++) {
|
|
128
|
+
const t = (i + 1) / segments;
|
|
129
|
+
const nextY = stripH * (1 - t);
|
|
130
|
+
const drift = rng.range(-40, 40);
|
|
131
|
+
const nextX = trunkX + drift + Math.sin(t * Math.PI * 2) * 30;
|
|
132
|
+
trunkPoints.push({ x: nextX, y: nextY });
|
|
133
|
+
|
|
134
|
+
// Draw trunk segment
|
|
135
|
+
const w = 5 * (1 - t * 0.15); // thicker trunk
|
|
136
|
+
const op = 0.55 * (1 - t * 0.1);
|
|
137
|
+
const c = i % 2 === 0 ? C.AMBER : C.AMBER_LT;
|
|
138
|
+
drawGlowBezier(
|
|
139
|
+
sctx,
|
|
140
|
+
[
|
|
141
|
+
prevX,
|
|
142
|
+
prevY,
|
|
143
|
+
prevX + rng.range(-20, 20),
|
|
144
|
+
prevY - (prevY - nextY) * 0.35,
|
|
145
|
+
nextX + rng.range(-15, 15),
|
|
146
|
+
nextY + (prevY - nextY) * 0.3,
|
|
147
|
+
nextX,
|
|
148
|
+
nextY,
|
|
149
|
+
],
|
|
150
|
+
c,
|
|
151
|
+
w,
|
|
152
|
+
Math.max(3, w * 3.5),
|
|
153
|
+
op,
|
|
154
|
+
light,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
// Hub node at each segment junction
|
|
158
|
+
const hubR = 3 + (1 - t) * 4;
|
|
159
|
+
drawGlowNode(sctx, nextX, nextY, hubR, C.AMBER_LT, op * 0.8, light);
|
|
160
|
+
nodes.push({
|
|
161
|
+
x: nextX,
|
|
162
|
+
y: nextY,
|
|
163
|
+
r: hubR,
|
|
164
|
+
color: C.AMBER_LT,
|
|
165
|
+
op: op * 0.8,
|
|
166
|
+
phase: i * 0.9,
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Branches at each junction — reaching left and right
|
|
170
|
+
const branchCount = Math.floor(rng.range(3, 5));
|
|
171
|
+
for (let b = 0; b < branchCount; b++) {
|
|
172
|
+
const side = rng.next() < 0.5 ? -1 : 1;
|
|
173
|
+
const bAngle =
|
|
174
|
+
side * rng.range(0.2, 1.4) +
|
|
175
|
+
(rng.next() < 0.4 ? Math.PI * 0.5 * side : 0);
|
|
176
|
+
const bLen = rng.range(80, 250);
|
|
177
|
+
const bW = rng.range(1.2, 3);
|
|
178
|
+
const bOp = rng.range(0.18, 0.42);
|
|
179
|
+
const bColor =
|
|
180
|
+
rng.next() < 0.7
|
|
181
|
+
? rng.next() < 0.5
|
|
182
|
+
? C.AMBER
|
|
183
|
+
: C.AMBER_LT
|
|
184
|
+
: C.PURPLE;
|
|
185
|
+
branch(
|
|
186
|
+
nextX,
|
|
187
|
+
nextY,
|
|
188
|
+
bAngle,
|
|
189
|
+
bLen,
|
|
190
|
+
bW,
|
|
191
|
+
Math.floor(rng.range(3, 5)),
|
|
192
|
+
bColor,
|
|
193
|
+
bOp,
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
prevX = nextX;
|
|
198
|
+
prevY = nextY;
|
|
199
|
+
}
|
|
200
|
+
// Snapshot the trunk polyline for the animation loop to traverse
|
|
201
|
+
trunkPath = trunkPoints.slice();
|
|
202
|
+
|
|
203
|
+
// Secondary strands alongside trunk
|
|
204
|
+
for (let strand = 0; strand < 3; strand++) {
|
|
205
|
+
const offset = (strand - 1) * 20 + rng.range(-5, 5);
|
|
206
|
+
let spx = trunkX + offset;
|
|
207
|
+
let spy = stripH;
|
|
208
|
+
const strandW = [2, 1.5, 1][strand];
|
|
209
|
+
const strandOp = [0.25, 0.18, 0.12][strand];
|
|
210
|
+
for (let i = 0; i < segments - 2; i++) {
|
|
211
|
+
const t = (i + 1) / segments;
|
|
212
|
+
const ref = trunkPoints[i + 1];
|
|
213
|
+
const nx = ref.x + rng.range(-25, 35);
|
|
214
|
+
const ny = ref.y + rng.range(-30, 30);
|
|
215
|
+
drawGlowBezier(
|
|
216
|
+
sctx,
|
|
217
|
+
[
|
|
218
|
+
spx,
|
|
219
|
+
spy,
|
|
220
|
+
spx + rng.range(-15, 15),
|
|
221
|
+
(spy + ny) * 0.5,
|
|
222
|
+
nx + rng.range(-15, 15),
|
|
223
|
+
(spy + ny) * 0.5 + rng.range(-25, 25),
|
|
224
|
+
nx,
|
|
225
|
+
ny,
|
|
226
|
+
],
|
|
227
|
+
strand < 2 ? C.AMBER_LT : C.AMBER,
|
|
228
|
+
strandW * (1 - t * 0.4),
|
|
229
|
+
6,
|
|
230
|
+
strandOp * (1 - t * 0.3),
|
|
231
|
+
light,
|
|
232
|
+
);
|
|
233
|
+
// Extra small branches off secondary strands
|
|
234
|
+
if (rng.next() < 0.4) {
|
|
235
|
+
const a = rng.range(-1.5, 1.5);
|
|
236
|
+
branch(
|
|
237
|
+
nx,
|
|
238
|
+
ny,
|
|
239
|
+
a,
|
|
240
|
+
rng.range(40, 100),
|
|
241
|
+
rng.range(0.5, 1.5),
|
|
242
|
+
3,
|
|
243
|
+
C.AMBER_LT,
|
|
244
|
+
strandOp * 0.5,
|
|
245
|
+
light,
|
|
246
|
+
);
|
|
247
|
+
}
|
|
248
|
+
spx = nx;
|
|
249
|
+
spy = ny;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Particles — spread across full viewport
|
|
255
|
+
const particles = [];
|
|
256
|
+
const rngP = createRand(99);
|
|
257
|
+
const dustC = [C.AMBER_LT, C.AMBER, C.PURPLE, C.PURPLE_LT, C.GLOW];
|
|
258
|
+
for (let i = 0; i < 200; i++) {
|
|
259
|
+
particles.push({
|
|
260
|
+
x: rngP.range(0, W),
|
|
261
|
+
y: rngP.range(0, H),
|
|
262
|
+
r: rngP.range(0.3, 1.5),
|
|
263
|
+
vx: rngP.range(-0.06, 0.06),
|
|
264
|
+
vy: rngP.range(-0.1, 0.03),
|
|
265
|
+
color: dustC[Math.floor(rngP.next() * dustC.length)],
|
|
266
|
+
alpha: rngP.range(0.03, 0.15),
|
|
267
|
+
phase: rngP.next() * Math.PI * 2,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
buildStatic();
|
|
272
|
+
window._heroRebuild = function () {
|
|
273
|
+
builtTheme = null;
|
|
274
|
+
buildStatic();
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
// Sap pulses traveling along trunk and branches
|
|
278
|
+
const pulses = [];
|
|
279
|
+
let lastPulseSpawn = 0;
|
|
280
|
+
let lastFlareSpawn = 0;
|
|
281
|
+
|
|
282
|
+
function spawnTrunkPulse() {
|
|
283
|
+
if (trunkPath.length < 2) return;
|
|
284
|
+
pulses.push({
|
|
285
|
+
type: "trunk",
|
|
286
|
+
seg: 0,
|
|
287
|
+
progress: 0,
|
|
288
|
+
speed: 0.8 + Math.random() * 0.55,
|
|
289
|
+
color: Math.random() < 0.6 ? C.AMBER_LT : C.GLOW,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function spawnBranchPulse() {
|
|
294
|
+
if (!branchPaths.length) return;
|
|
295
|
+
pulses.push({
|
|
296
|
+
type: "branch",
|
|
297
|
+
pathIdx: Math.floor(Math.random() * branchPaths.length),
|
|
298
|
+
progress: 0,
|
|
299
|
+
speed: 0.9 + Math.random() * 0.75,
|
|
300
|
+
color: Math.random() < 0.7 ? C.AMBER_LT : C.AMBER,
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function bezierPoint(p, u) {
|
|
305
|
+
const mu = 1 - u;
|
|
306
|
+
const mu2 = mu * mu;
|
|
307
|
+
const u2 = u * u;
|
|
308
|
+
return [
|
|
309
|
+
mu2 * mu * p[0] + 3 * mu2 * u * p[2] + 3 * mu * u2 * p[4] + u2 * u * p[6],
|
|
310
|
+
mu2 * mu * p[1] + 3 * mu2 * u * p[3] + 3 * mu * u2 * p[5] + u2 * u * p[7],
|
|
311
|
+
];
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Released spores — bright motes that drift outward from random visible
|
|
315
|
+
// tree nodes, biased toward the empty side of the canvas so the whole
|
|
316
|
+
// viewport gets ambient motion (not just the right side where the tree lives)
|
|
317
|
+
const spores = [];
|
|
318
|
+
let lastSporeRelease = 0;
|
|
319
|
+
|
|
320
|
+
function releaseSpore(srcY) {
|
|
321
|
+
// Only spawn from nodes near the trunk axis so spores genuinely
|
|
322
|
+
// radiate from the spine, not from arbitrary branch tips
|
|
323
|
+
const trunkRange = 90;
|
|
324
|
+
const candidates = [];
|
|
325
|
+
for (const n of nodes) {
|
|
326
|
+
const sy = n.y - srcY;
|
|
327
|
+
if (sy > -50 && sy < H + 50 && Math.abs(n.x - trunkX) < trunkRange) {
|
|
328
|
+
candidates.push(n);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
if (!candidates.length) return;
|
|
332
|
+
const n = candidates[Math.floor(Math.random() * candidates.length)];
|
|
333
|
+
// Mostly leftward drift (outward into empty space) with some up/down spread
|
|
334
|
+
const angle = Math.PI + (Math.random() - 0.5) * Math.PI * 0.75;
|
|
335
|
+
const speed = 14 + Math.random() * 18;
|
|
336
|
+
spores.push({
|
|
337
|
+
x: n.x,
|
|
338
|
+
y: n.y,
|
|
339
|
+
vx: Math.cos(angle) * speed,
|
|
340
|
+
vy: Math.sin(angle) * speed,
|
|
341
|
+
age: 0,
|
|
342
|
+
maxAge: 5 + Math.random() * 3,
|
|
343
|
+
color: Math.random() < 0.7 ? C.AMBER_LT : C.GLOW,
|
|
344
|
+
r: 0.9 + Math.random() * 0.8,
|
|
345
|
+
});
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
let t = 0,
|
|
349
|
+
lastTime = 0;
|
|
350
|
+
function animate(timestamp) {
|
|
351
|
+
if (document.hidden) {
|
|
352
|
+
requestAnimationFrame(animate);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
const dt = lastTime ? Math.min((timestamp - lastTime) / 1000, 0.05) : 0.016;
|
|
356
|
+
lastTime = timestamp;
|
|
357
|
+
t += dt;
|
|
358
|
+
|
|
359
|
+
const light = isLightMode();
|
|
360
|
+
const scrollY = window.scrollY || window.pageYOffset;
|
|
361
|
+
const scrollRatio = (scrollY * 0.7) / Math.max(1, docH - H);
|
|
362
|
+
|
|
363
|
+
ctx.clearRect(0, 0, W, H);
|
|
364
|
+
|
|
365
|
+
// Draw the visible slice of the static tree
|
|
366
|
+
// Map scroll position to the strip
|
|
367
|
+
const srcY = scrollRatio * (stripH - H);
|
|
368
|
+
// Source rect in the offscreen canvas (in device pixels)
|
|
369
|
+
ctx.drawImage(
|
|
370
|
+
staticCanvas,
|
|
371
|
+
0,
|
|
372
|
+
srcY * dpr,
|
|
373
|
+
W * dpr,
|
|
374
|
+
H * dpr, // source
|
|
375
|
+
0,
|
|
376
|
+
0,
|
|
377
|
+
W,
|
|
378
|
+
H, // destination
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
// Animated particles (viewport-local)
|
|
382
|
+
particles.forEach((p) => {
|
|
383
|
+
p.x += p.vx;
|
|
384
|
+
p.y += p.vy;
|
|
385
|
+
if (p.x < 0) p.x = W;
|
|
386
|
+
if (p.x > W) p.x = 0;
|
|
387
|
+
if (p.y < 0) p.y = H;
|
|
388
|
+
if (p.y > H) p.y = 0;
|
|
389
|
+
const fl = 0.5 + 0.5 * Math.sin(t * 1.5 + p.phase);
|
|
390
|
+
ctx.fillStyle = rgba(p.color, p.alpha * 2 * fl);
|
|
391
|
+
ctx.beginPath();
|
|
392
|
+
ctx.arc(
|
|
393
|
+
p.x,
|
|
394
|
+
p.y,
|
|
395
|
+
p.r * (0.8 + 0.3 * Math.sin(t + p.phase)),
|
|
396
|
+
0,
|
|
397
|
+
Math.PI * 2,
|
|
398
|
+
);
|
|
399
|
+
ctx.fill();
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
// Spawn sap pulses — trunk pulses bring life up the spine,
|
|
403
|
+
// branch pulses scatter through the limbs.
|
|
404
|
+
// Tuned for the ai site's kinetic energy: shorter interval, higher
|
|
405
|
+
// probability, occasional triple branch spawns.
|
|
406
|
+
if (timestamp - lastPulseSpawn > 200) {
|
|
407
|
+
lastPulseSpawn = timestamp;
|
|
408
|
+
if (Math.random() < 0.7) spawnTrunkPulse();
|
|
409
|
+
if (Math.random() < 0.95) spawnBranchPulse();
|
|
410
|
+
if (Math.random() < 0.55) spawnBranchPulse();
|
|
411
|
+
if (Math.random() < 0.3) spawnBranchPulse();
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// Animate + draw pulses (world-space coords, drawn relative to srcY)
|
|
415
|
+
for (let i = pulses.length - 1; i >= 0; i--) {
|
|
416
|
+
const p = pulses[i];
|
|
417
|
+
p.progress += dt * p.speed;
|
|
418
|
+
let px = 0,
|
|
419
|
+
py = 0,
|
|
420
|
+
alive = true;
|
|
421
|
+
if (p.type === "trunk") {
|
|
422
|
+
if (p.progress >= 1) {
|
|
423
|
+
// Often fork onto a branch when arriving at a junction
|
|
424
|
+
if (Math.random() < 0.55) spawnBranchPulse();
|
|
425
|
+
p.seg++;
|
|
426
|
+
p.progress = 0;
|
|
427
|
+
if (p.seg >= trunkPath.length - 1) {
|
|
428
|
+
alive = false;
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
if (alive) {
|
|
432
|
+
const a = trunkPath[p.seg];
|
|
433
|
+
const b = trunkPath[p.seg + 1];
|
|
434
|
+
const e = p.progress * p.progress * (3 - 2 * p.progress);
|
|
435
|
+
px = a.x + (b.x - a.x) * e;
|
|
436
|
+
py = a.y + (b.y - a.y) * e;
|
|
437
|
+
}
|
|
438
|
+
} else {
|
|
439
|
+
if (p.progress >= 1) {
|
|
440
|
+
alive = false;
|
|
441
|
+
}
|
|
442
|
+
if (alive) {
|
|
443
|
+
const path = branchPaths[p.pathIdx];
|
|
444
|
+
const pt = bezierPoint(path, p.progress);
|
|
445
|
+
px = pt[0];
|
|
446
|
+
py = pt[1];
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (!alive) {
|
|
450
|
+
pulses.splice(i, 1);
|
|
451
|
+
continue;
|
|
452
|
+
}
|
|
453
|
+
const screenY = py - srcY;
|
|
454
|
+
if (screenY < -20 || screenY > H + 20) continue;
|
|
455
|
+
const fade = Math.sin(p.progress * Math.PI); // fade in + out at endpoints
|
|
456
|
+
ctx.fillStyle = rgba(p.color, (light ? 0.32 : 0.42) * fade);
|
|
457
|
+
ctx.beginPath();
|
|
458
|
+
ctx.arc(px, screenY, 6, 0, Math.PI * 2);
|
|
459
|
+
ctx.fill();
|
|
460
|
+
ctx.fillStyle = rgba(p.color, (light ? 0.5 : 0.6) * fade);
|
|
461
|
+
ctx.beginPath();
|
|
462
|
+
ctx.arc(px, screenY, 2.4, 0, Math.PI * 2);
|
|
463
|
+
ctx.fill();
|
|
464
|
+
ctx.fillStyle = rgba(light ? C.AMBER_DK : C.CORE, 0.85 * fade);
|
|
465
|
+
ctx.beginPath();
|
|
466
|
+
ctx.arc(px, screenY, 1.2, 0, Math.PI * 2);
|
|
467
|
+
ctx.fill();
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Spawn + animate drifting spores released from the tree
|
|
471
|
+
if (timestamp - lastSporeRelease > 200) {
|
|
472
|
+
lastSporeRelease = timestamp;
|
|
473
|
+
releaseSpore(srcY);
|
|
474
|
+
if (Math.random() < 0.85) releaseSpore(srcY);
|
|
475
|
+
if (Math.random() < 0.35) releaseSpore(srcY);
|
|
476
|
+
}
|
|
477
|
+
for (let i = spores.length - 1; i >= 0; i--) {
|
|
478
|
+
const sp = spores[i];
|
|
479
|
+
sp.x += sp.vx * dt;
|
|
480
|
+
sp.y += sp.vy * dt;
|
|
481
|
+
sp.vy += 4 * dt; // gentle gravity
|
|
482
|
+
sp.age += dt;
|
|
483
|
+
if (sp.age > sp.maxAge) {
|
|
484
|
+
spores.splice(i, 1);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
const screenY = sp.y - srcY;
|
|
488
|
+
if (screenY < -20 || screenY > H + 20 || sp.x < -20 || sp.x > W + 20)
|
|
489
|
+
continue;
|
|
490
|
+
const lr = sp.age / sp.maxAge;
|
|
491
|
+
const alpha =
|
|
492
|
+
lr < 0.1 ? lr / 0.1 : lr > 0.7 ? Math.max(0, (1 - lr) / 0.3) : 1;
|
|
493
|
+
ctx.fillStyle = rgba(sp.color, alpha * 0.18);
|
|
494
|
+
ctx.beginPath();
|
|
495
|
+
ctx.arc(sp.x, screenY, sp.r * 4, 0, Math.PI * 2);
|
|
496
|
+
ctx.fill();
|
|
497
|
+
ctx.fillStyle = rgba(light ? C.AMBER_DK : sp.color, alpha * 0.85);
|
|
498
|
+
ctx.beginPath();
|
|
499
|
+
ctx.arc(sp.x, screenY, sp.r, 0, Math.PI * 2);
|
|
500
|
+
ctx.fill();
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// Trigger random flares on visible mid+large nodes
|
|
504
|
+
if (timestamp - lastFlareSpawn > 600 && Math.random() < 0.7) {
|
|
505
|
+
lastFlareSpawn = timestamp;
|
|
506
|
+
const candidates = [];
|
|
507
|
+
for (const n of nodes) {
|
|
508
|
+
const sy = n.y - srcY;
|
|
509
|
+
if (sy > -50 && sy < H + 50 && n.r > 1.5) candidates.push(n);
|
|
510
|
+
}
|
|
511
|
+
if (candidates.length) {
|
|
512
|
+
const n = candidates[Math.floor(Math.random() * candidates.length)];
|
|
513
|
+
n.flare = 1.0;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// Pulsing nodes (with optional flare boost) — only visible window
|
|
518
|
+
nodes.forEach((n) => {
|
|
519
|
+
if (n.flare > 0) n.flare = Math.max(0, n.flare - dt * 0.85);
|
|
520
|
+
const screenY = n.y - srcY;
|
|
521
|
+
if (screenY < -100 || screenY > H + 100) return;
|
|
522
|
+
const pulse = 0.6 + 0.4 * Math.sin(t * 1.2 + n.phase);
|
|
523
|
+
const flareBoost = 1 + (n.flare || 0) * 2.5;
|
|
524
|
+
const pr = n.r * (2 + Math.sin(t * 0.7 + n.phase)) * flareBoost;
|
|
525
|
+
ctx.fillStyle = rgba(n.color, n.op * 0.1 * pulse * flareBoost);
|
|
526
|
+
ctx.beginPath();
|
|
527
|
+
ctx.arc(n.x, screenY, pr * 5, 0, Math.PI * 2);
|
|
528
|
+
ctx.fill();
|
|
529
|
+
ctx.fillStyle = rgba(C.GLOW, n.op * 0.12 * pulse * flareBoost);
|
|
530
|
+
ctx.beginPath();
|
|
531
|
+
ctx.arc(n.x, screenY, pr * 2, 0, Math.PI * 2);
|
|
532
|
+
ctx.fill();
|
|
533
|
+
if (n.flare > 0.05) {
|
|
534
|
+
ctx.fillStyle = rgba(C.CORE, n.flare * 0.9);
|
|
535
|
+
ctx.beginPath();
|
|
536
|
+
ctx.arc(n.x, screenY, n.r * 0.6, 0, Math.PI * 2);
|
|
537
|
+
ctx.fill();
|
|
538
|
+
}
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
requestAnimationFrame(animate);
|
|
542
|
+
}
|
|
543
|
+
requestAnimationFrame(animate);
|
|
544
|
+
})();
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { DefaultRizomLayout } from "./default-layout";
|
|
2
|
+
export { rizomBaseSite } from "./base-site";
|
|
3
|
+
export {
|
|
4
|
+
RizomRuntimePlugin,
|
|
5
|
+
buildRizomHeadScript,
|
|
6
|
+
rizomRuntimeStaticAssets,
|
|
7
|
+
} from "./plugin";
|
|
8
|
+
export type { RizomRuntimeConfig, RizomThemeProfile } from "./plugin";
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { listCanonicalAtprotoLexicons } from "@brains/atproto-contracts";
|
|
2
|
+
import type {
|
|
3
|
+
RizomPluginCapabilities,
|
|
4
|
+
RizomRuntimeConfig,
|
|
5
|
+
RizomSiteShell,
|
|
6
|
+
RizomThemeProfile,
|
|
7
|
+
} from "../contracts";
|
|
8
|
+
import canvasPrelude from "./canvases/prelude.canvas.js" with { type: "text" };
|
|
9
|
+
import treeCanvas from "./canvases/tree.canvas.js" with { type: "text" };
|
|
10
|
+
import constellationCanvas from "./canvases/constellation.canvas.js" with { type: "text" };
|
|
11
|
+
import rootsCanvas from "./canvases/roots.canvas.js" with { type: "text" };
|
|
12
|
+
import bootScript from "./boot/boot.boot.js" with { type: "text" };
|
|
13
|
+
|
|
14
|
+
export type { RizomRuntimeConfig, RizomThemeProfile } from "../contracts";
|
|
15
|
+
|
|
16
|
+
const THEME_PROFILES = new Set<string>(["product", "editorial", "studio"]);
|
|
17
|
+
|
|
18
|
+
function isRizomThemeProfile(value: unknown): value is RizomThemeProfile {
|
|
19
|
+
return typeof value === "string" && THEME_PROFILES.has(value);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function parseRuntimeConfig(
|
|
23
|
+
config: Record<string, unknown>,
|
|
24
|
+
): RizomRuntimeConfig {
|
|
25
|
+
const themeProfile = config["themeProfile"];
|
|
26
|
+
const theme = config["theme"];
|
|
27
|
+
|
|
28
|
+
if (themeProfile !== undefined && !isRizomThemeProfile(themeProfile)) {
|
|
29
|
+
throw new Error(
|
|
30
|
+
`Invalid rizom site themeProfile ${JSON.stringify(themeProfile)}; expected one of: ${[...THEME_PROFILES].join(", ")}`,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
if (theme !== undefined && typeof theme !== "string") {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Invalid rizom site theme ${JSON.stringify(theme)}; expected a package name string`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...(themeProfile !== undefined ? { themeProfile } : {}),
|
|
41
|
+
...(theme !== undefined ? { theme } : {}),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const CANVAS_BY_THEME_PROFILE: Record<RizomThemeProfile, string> = {
|
|
46
|
+
product: "/canvases/tree.canvas.js",
|
|
47
|
+
editorial: "/canvases/roots.canvas.js",
|
|
48
|
+
studio: "/canvases/constellation.canvas.js",
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
function getRizomCanvasPath(
|
|
52
|
+
themeProfile?: RizomThemeProfile,
|
|
53
|
+
): string | undefined {
|
|
54
|
+
return themeProfile ? CANVAS_BY_THEME_PROFILE[themeProfile] : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function buildRizomHeadScript(themeProfile?: RizomThemeProfile): string {
|
|
58
|
+
const canvasPath = getRizomCanvasPath(themeProfile);
|
|
59
|
+
const scripts = [`<script src="/boot.js" defer></script>`];
|
|
60
|
+
|
|
61
|
+
if (themeProfile) {
|
|
62
|
+
const themeProfileJson = JSON.stringify(themeProfile);
|
|
63
|
+
scripts.unshift(
|
|
64
|
+
`<script>document.documentElement.setAttribute("data-theme-profile", ${themeProfileJson});</script>`,
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (canvasPath) {
|
|
69
|
+
scripts.push(`<script src="/canvases/prelude.canvas.js" defer></script>`);
|
|
70
|
+
scripts.push(`<script src="${canvasPath}" defer></script>`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return scripts.join("");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const RIZOM_ATPROTO_LEXICON_BASE_PATH = "/atproto/lexicons";
|
|
77
|
+
|
|
78
|
+
function formatLexiconJson(lexicon: unknown): string {
|
|
79
|
+
return `${JSON.stringify(lexicon, null, 2)}\n`;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const rizomAtprotoLexiconStaticAssets: Record<string, string> =
|
|
83
|
+
Object.fromEntries(
|
|
84
|
+
listCanonicalAtprotoLexicons().map((lexicon) => [
|
|
85
|
+
`${RIZOM_ATPROTO_LEXICON_BASE_PATH}/${lexicon.id}.json`,
|
|
86
|
+
formatLexiconJson(lexicon),
|
|
87
|
+
]),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export const rizomRuntimeStaticAssets: Record<string, string> = {
|
|
91
|
+
...rizomAtprotoLexiconStaticAssets,
|
|
92
|
+
"/canvases/prelude.canvas.js": canvasPrelude,
|
|
93
|
+
"/canvases/tree.canvas.js": treeCanvas,
|
|
94
|
+
"/canvases/constellation.canvas.js": constellationCanvas,
|
|
95
|
+
"/canvases/roots.canvas.js": rootsCanvas,
|
|
96
|
+
"/boot.js": bootScript,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export class RizomRuntimePlugin {
|
|
100
|
+
public readonly id = "rizom-site";
|
|
101
|
+
public readonly version = "0.1.0";
|
|
102
|
+
public readonly type = "service" as const;
|
|
103
|
+
public readonly packageName: string;
|
|
104
|
+
public readonly description: string;
|
|
105
|
+
public readonly config: RizomRuntimeConfig;
|
|
106
|
+
|
|
107
|
+
constructor(packageName: string, config: Record<string, unknown> = {}) {
|
|
108
|
+
this.packageName = packageName;
|
|
109
|
+
this.description = `${packageName} plugin`;
|
|
110
|
+
this.config = parseRuntimeConfig(config);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async register(
|
|
114
|
+
shell: RizomSiteShell,
|
|
115
|
+
_context?: unknown,
|
|
116
|
+
): Promise<RizomPluginCapabilities> {
|
|
117
|
+
await this.onRegister(shell);
|
|
118
|
+
return { tools: [], resources: [] };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
protected async onRegister(shell: RizomSiteShell): Promise<void> {
|
|
122
|
+
const themeProfile = this.getThemeProfile();
|
|
123
|
+
const messaging = shell.getMessageBus();
|
|
124
|
+
|
|
125
|
+
messaging.subscribe("system:plugins:ready", async () => {
|
|
126
|
+
await messaging.send({
|
|
127
|
+
type: "plugin:site-builder:head-script:register",
|
|
128
|
+
sender: this.id,
|
|
129
|
+
payload: {
|
|
130
|
+
pluginId: this.id,
|
|
131
|
+
script: buildRizomHeadScript(themeProfile),
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return { success: true };
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
shell
|
|
138
|
+
.getLogger()
|
|
139
|
+
.info(
|
|
140
|
+
`Rizom runtime plugin registered${themeProfile ? ` (theme profile: ${themeProfile})` : ""}`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
protected getThemeProfile(): RizomThemeProfile | undefined {
|
|
145
|
+
return this.config.themeProfile;
|
|
146
|
+
}
|
|
147
|
+
}
|