@fluritheme/moon-explorer 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/render.js ADDED
@@ -0,0 +1,467 @@
1
+ import { clamp, ease } from "./model.js";
2
+ function path(c, points, color, width) {
3
+ c.beginPath();
4
+ c.moveTo(points[0], points[1]);
5
+ for (let i = 2; i < points.length; i += 2)
6
+ c.lineTo(points[i], points[i + 1]);
7
+ c.strokeStyle = color;
8
+ c.lineWidth = width;
9
+ c.lineCap = "round";
10
+ c.lineJoin = "round";
11
+ c.stroke();
12
+ }
13
+ function ellipse(c, x, y, rx, ry, fill, rotation = 0) {
14
+ c.beginPath();
15
+ c.ellipse(x, y, Math.max(0.01, rx), Math.max(0.01, ry), rotation, 0, Math.PI * 2);
16
+ c.fillStyle = fill;
17
+ c.fill();
18
+ }
19
+ function round(c, x, y, w, h, r, fill, stroke) {
20
+ c.beginPath();
21
+ c.roundRect(x, y, w, h, r);
22
+ c.fillStyle = fill;
23
+ c.fill();
24
+ if (stroke) {
25
+ c.lineWidth = 0.8;
26
+ c.strokeStyle = stroke;
27
+ c.stroke();
28
+ }
29
+ }
30
+ function star(c, x, y, size, alpha) {
31
+ c.save();
32
+ c.globalAlpha = alpha;
33
+ c.beginPath();
34
+ c.moveTo(x, y - size);
35
+ c.quadraticCurveTo(x + size * 0.15, y - size * 0.15, x + size, y);
36
+ c.quadraticCurveTo(x + size * 0.15, y + size * 0.15, x, y + size);
37
+ c.quadraticCurveTo(x - size * 0.15, y + size * 0.15, x - size, y);
38
+ c.quadraticCurveTo(x - size * 0.15, y - size * 0.15, x, y - size);
39
+ c.fillStyle = "#fff4c7";
40
+ c.fill();
41
+ c.restore();
42
+ }
43
+ function shadow(c, x, y, width, alpha = 0.15) {
44
+ c.save();
45
+ c.globalAlpha = alpha;
46
+ ellipse(c, x, y, width, width * 0.13, "#10142a");
47
+ c.restore();
48
+ }
49
+ function flag(c, x, y, t, accent) {
50
+ c.save();
51
+ c.translate(x, y);
52
+ path(c, [0, 0, 0, -64], "#727d8b", 2);
53
+ path(c, [-1, -64, -1, -1], "#e9e5d6", 0.7);
54
+ ellipse(c, 0, -64, 2, 2, "#eee6c8");
55
+ c.beginPath();
56
+ c.moveTo(1, -63);
57
+ c.bezierCurveTo(10, -66 + Math.sin(t) * 1.2, 20, -59, 31, -63);
58
+ c.lineTo(31, -43);
59
+ c.bezierCurveTo(20, -39 + Math.sin(t) * 0.8, 10, -46, 1, -42);
60
+ c.closePath();
61
+ c.fillStyle = accent;
62
+ c.fill();
63
+ path(c, [2, -61, 29, -61], "#ffffff65", 0.8);
64
+ ellipse(c, 15, -52, 5, 5, "#fff0d0");
65
+ ellipse(c, 17, -53, 4, 4, accent);
66
+ star(c, 23, -54, 1.7, 0.85);
67
+ c.restore();
68
+ }
69
+ function rover(c, x, y, t, accent, facing) {
70
+ c.save();
71
+ c.translate(x, y);
72
+ c.scale(facing, 1);
73
+ shadow(c, 0, 1, 48, 0.17);
74
+ for (const wheelX of [-30, 30]) {
75
+ path(c, [0, -15, wheelX, -9], "#8a8a84", 3);
76
+ ellipse(c, wheelX, -8, 11, 11, "#242e39");
77
+ ellipse(c, wheelX, -8, 7.5, 7.5, "#56606c");
78
+ ellipse(c, wheelX, -8, 3, 3, "#c5c5b3");
79
+ for (let i = 0; i < 8; i++) {
80
+ const a = (i * Math.PI) / 4 + t * 2;
81
+ path(c, [
82
+ wheelX + Math.cos(a) * 8,
83
+ -8 + Math.sin(a) * 8,
84
+ wheelX + Math.cos(a) * 10,
85
+ -8 + Math.sin(a) * 10,
86
+ ], "#bcc1ba", 1.3);
87
+ }
88
+ }
89
+ round(c, -38, -27, 73, 17, 5, "#d5d6c7", "#62717b");
90
+ round(c, -37, -27, 73, 5, 2, "#f5ebd5");
91
+ round(c, -35, -24, 18, 10, 2, accent);
92
+ round(c, 7, -25, 22, 13, 2, "#8e9e9f");
93
+ for (let i = 0; i < 3; i++)
94
+ path(c, [11 + i * 5, -24, 11 + i * 5, -13], "#b7d2cd", 0.6);
95
+ path(c, [0, -29, -2, -48], "#7f8c91", 2);
96
+ c.save();
97
+ c.translate(-2, -47);
98
+ c.rotate(-0.35);
99
+ c.beginPath();
100
+ c.arc(0, 0, 10, 0, Math.PI);
101
+ c.fillStyle = "#d4ded5";
102
+ c.fill();
103
+ path(c, [-10, 0, 10, 0], "#75868b", 1);
104
+ path(c, [0, 0, 0, -6], "#a1aaa7", 1);
105
+ c.restore();
106
+ path(c, [29, -28, 34, -48], "#a6b0a8", 1);
107
+ ellipse(c, 34, -49, 2, 2, accent);
108
+ round(c, -22, -31, 21, 5, 1, "#74828a");
109
+ c.restore();
110
+ }
111
+ function lander(c, x, y, phase, t, accent, landingStart, ground) {
112
+ c.save();
113
+ c.translate(x, y);
114
+ const descending = phase === "landing";
115
+ const float = descending ? Math.sin(t * 5) * 0.02 : 0;
116
+ c.rotate(float);
117
+ const altitude = Math.max(0, y - ground);
118
+ void altitude;
119
+ shadow(c, 0, ground - y + 3, 52, 0.12);
120
+ if (descending && t < 3.6) {
121
+ const flame = (13 + Math.sin(t * 38) * 5) * (1 - ease((t - 2.7) / 0.9));
122
+ c.beginPath();
123
+ c.moveTo(-9, 17);
124
+ c.quadraticCurveTo(-5, 27 + flame, 0, 39 + flame);
125
+ c.quadraticCurveTo(5, 28 + flame, 9, 17);
126
+ const g = c.createLinearGradient(0, 17, 0, 45 + flame);
127
+ g.addColorStop(0, "#d8efff");
128
+ g.addColorStop(0.3, "#ffdc8e");
129
+ g.addColorStop(1, "#ff7a3800");
130
+ c.fillStyle = g;
131
+ c.fill();
132
+ ellipse(c, 0, 20, 5, 9, "#ffefc9");
133
+ }
134
+ for (const f of [-1, 1]) {
135
+ path(c, [f * 21, 1, f * 44, 31], "#8c989e", 3);
136
+ path(c, [f * 20, 6, f * 39, 26], "#d7d5bd", 1);
137
+ ellipse(c, f * 45, 32, 12, 3, "#78868a");
138
+ round(c, f < 0 ? -52 : 30, 29, 23, 3, 1, "#bbc6bd");
139
+ }
140
+ round(c, -26, -30, 52, 41, 6, "#d9c89b", "#687f87");
141
+ round(c, -24, -27, 48, 15, 4, "#fff1cf");
142
+ round(c, -22, -10, 44, 19, 2, "#b7a46e");
143
+ path(c, [-22, -8, 0, 7, 22, -8], "#eedbb1", 0.7);
144
+ path(c, [-21, 6, 0, -6, 22, 6], "#8b896d", 0.7);
145
+ c.beginPath();
146
+ c.moveTo(-25, -25);
147
+ c.lineTo(-18, -43);
148
+ c.lineTo(17, -43);
149
+ c.lineTo(27, -25);
150
+ c.closePath();
151
+ c.fillStyle = "#d8dfd6";
152
+ c.fill();
153
+ round(c, -14, -38, 28, 15, 5, "#284353", "#7c9b9d");
154
+ path(c, [-9, -36, -1, -36], "#bce7da", 1.3);
155
+ round(c, -10, -19, 20, 9, 2, accent);
156
+ round(c, -18, 10, 36, 9, 2, "#6f7f87");
157
+ path(c, [-5, 12, -5, 18], "#b8c1b1", 0.8);
158
+ path(c, [5, 12, 5, 18], "#b8c1b1", 0.8);
159
+ path(c, [17, -42, 25, -57], "#b4c2b7", 1.3);
160
+ ellipse(c, 26, -58, 2, 2, "#e5debf");
161
+ path(c, [-18, -43, -22, -50, -17, -57], "#d5d7bf", 1.5);
162
+ ellipse(c, -17, -58, 4, 2, accent);
163
+ if (!descending) {
164
+ path(c, [-4, 6, -4, 31], "#78888b", 1.5);
165
+ for (let i = 0; i < 5; i++)
166
+ path(c, [-7, 12 + i * 4, 0, 12 + i * 4], "#c5ccb8", 1.2);
167
+ }
168
+ void landingStart;
169
+ c.restore();
170
+ }
171
+ function astronaut(c, m, o) {
172
+ const t = m.phaseTime, phase = m.phase, s = m.size;
173
+ const moving = phase === "walking";
174
+ const driving = phase === "driving";
175
+ const crouch = phase === "sampling"
176
+ ? Math.sin(clamp(t / 4.2, 0, 1) * Math.PI) * 10
177
+ : phase === "planting"
178
+ ? Math.sin(clamp(t / 3.6, 0, 1) * Math.PI) * 6
179
+ : 0;
180
+ const bounce = moving
181
+ ? Math.sin(t * 5) * 1.3
182
+ : Math.sin(m.elapsed * 2) * 0.55;
183
+ const jump = phase === "jumping";
184
+ const swing = moving ? Math.sin(t * 5) * 0.48 : 0;
185
+ const seated = driving || (phase === "boarding" && t > 1);
186
+ const feetLift = seated ? 16 : 0;
187
+ c.save();
188
+ c.translate(m.x, m.y);
189
+ c.scale(s, s);
190
+ c.scale(m.facing, 1);
191
+ if (phase === "celebrating")
192
+ c.rotate(Math.sin(t * 6) * 0.07);
193
+ if (phase === "egress")
194
+ c.rotate(Math.sin(t * 5) * 0.035);
195
+ shadow(c, 0, (m.surface.y - m.y) / s + 1, 20 * (1 - clamp((m.surface.y - m.y) / 130, 0, 0.6)), 0.18);
196
+ c.translate(0, crouch + bounce - feetLift);
197
+ // Soft backpack silhouette, antenna and curved oxygen hose.
198
+ round(c, -23, -51, 16, 34, 5, "#adb6b1", "#344b55");
199
+ round(c, -25, -46, 8, 23, 3, "#d1d7cb");
200
+ round(c, -25, -43, 8, 6, 1, o.accent);
201
+ path(c, [-18, -51, -20, -63], "#6d8791", 1.6);
202
+ ellipse(c, -20, -64, 1.8, 1.8, o.accent);
203
+ path(c, [-22, -35, -27, -24, -18, -17, -12, -29], "#e6d6b5", 3.2);
204
+ path(c, [-22, -35, -27, -24, -18, -17, -12, -29], "#7f8b86", 0.7);
205
+ // Far arm and articulated legs.
206
+ const armFar = moving ? -swing * 0.8 : jump ? -1.1 : seated ? -0.7 : 0.15;
207
+ limb(c, -8, -38, -12, -25, -11, -13, armFar, "#b5bcb7", 6);
208
+ ellipse(c, -11, -12, 4, 4.5, "#81979a");
209
+ const legA = jump ? -0.65 : seated ? -1.2 : swing, legB = jump ? 0.55 : seated ? -0.8 : -swing;
210
+ leg(c, -6, -18, legA, seated, "#b8bfb7", crouch);
211
+ leg(c, 6, -18, legB, seated, "#e8e7d6", crouch);
212
+ // Suit with illuminated panel, stitching and little utility belt.
213
+ const suit = c.createLinearGradient(-13, -48, 14, -17);
214
+ suit.addColorStop(0, "#f8f1d9");
215
+ suit.addColorStop(0.55, "#e0e2d3");
216
+ suit.addColorStop(1, "#a8b9b1");
217
+ round(c, -13, -47, 28, 31, 9, suit, "#405963");
218
+ path(c, [-9, -42, -9, -26], "#fff8df", 1.2);
219
+ round(c, -10, -36, 21, 13, 3, "#a4b8b5", "#6b8d92");
220
+ round(c, -8, -34, 17, 7, 1, "#263f4a");
221
+ ellipse(c, -4, -30, 1.1, 1.1, "#b4f8e0");
222
+ ellipse(c, 1, -30, 1.1, 1.1, o.accent);
223
+ path(c, [5, -32, 7, -32], "#d3f3e1", 0.6);
224
+ round(c, -12, -22, 25, 5, 1, "#778d8e");
225
+ round(c, -1, -23, 6, 7, 1, "#cbd3bb");
226
+ round(c, 8, -43, 6, 4, 1, o.accent);
227
+ // Helmet: dark rim, pearl shell, gold metallic visor and layered reflections.
228
+ const headTilt = phase === "observing"
229
+ ? -0.18
230
+ : phase === "sleeping"
231
+ ? 0.19
232
+ : phase === "sampling"
233
+ ? 0.2
234
+ : Math.sin(m.elapsed * 0.8) * 0.025;
235
+ c.save();
236
+ c.translate(0, -52);
237
+ c.rotate(headTilt);
238
+ ellipse(c, 0, 0, 22, 21, "#546a70");
239
+ const shell = c.createRadialGradient(-8, -9, 3, 0, 0, 24);
240
+ shell.addColorStop(0, "#fff8df");
241
+ shell.addColorStop(0.65, "#e7e9d9");
242
+ shell.addColorStop(1, "#879e9b");
243
+ ellipse(c, 0, -1, 21, 21, shell);
244
+ ellipse(c, 2, 0, 16.5, 15.5, "#38505b");
245
+ const visor = c.createLinearGradient(-8, -13, 12, 16);
246
+ visor.addColorStop(0, "#4d747d");
247
+ visor.addColorStop(0.35, "#183948");
248
+ visor.addColorStop(0.7, "#11252e");
249
+ visor.addColorStop(1, "#998263");
250
+ ellipse(c, 3, 0, 14.5, 13.5, visor);
251
+ c.save();
252
+ c.beginPath();
253
+ c.ellipse(3, 0, 14.5, 13.5, 0, 0, Math.PI * 2);
254
+ c.clip();
255
+ const ref = c.createLinearGradient(-12, -13, 16, 9);
256
+ ref.addColorStop(0, "#f2e4b9c0");
257
+ ref.addColorStop(0.4, "#a9e6d542");
258
+ ref.addColorStop(1, "#d7e9ff00");
259
+ c.fillStyle = ref;
260
+ c.beginPath();
261
+ c.moveTo(-13, -9);
262
+ c.lineTo(14, -17);
263
+ c.lineTo(6, -6);
264
+ c.lineTo(-14, 4);
265
+ c.fill();
266
+ path(c, [-8, -8, 0, -11, 8, -10], "#f5f5dca0", 1.2);
267
+ ellipse(c, 8, -4, 2.5, 4, "#d2f5e525", -0.45);
268
+ c.restore();
269
+ ellipse(c, -19, 0, 4, 7, "#889f9d");
270
+ ellipse(c, -20, 0, 2, 4, "#c7d2ba");
271
+ round(c, -7, 17, 16, 4, 2, "#7d9696");
272
+ // Eyes are tiny emissive shapes, watching nearby pointers through the visor.
273
+ let eyeX = 4, eyeY = 4;
274
+ const dx = o.pointer.x - m.x, dy = o.pointer.y - (m.y - 55 * s);
275
+ if (o.pointer.active && Math.hypot(dx, dy) < 180 * s) {
276
+ eyeX += clamp(dx / (80 * s), -2, 2) * m.facing;
277
+ eyeY += clamp(dy / (80 * s), -1.5, 1.5);
278
+ }
279
+ const blink = Math.sin(m.elapsed * 1.7) > 0.988;
280
+ const asleep = phase === "sleeping";
281
+ if (asleep || blink) {
282
+ path(c, [eyeX - 6, eyeY, eyeX - 3, eyeY], "#deedcc", 1);
283
+ path(c, [eyeX + 1, eyeY, eyeX + 4, eyeY], "#deedcc", 1);
284
+ }
285
+ else {
286
+ ellipse(c, eyeX - 4.5, eyeY, 1.1, 1.6, "#e2f6d8");
287
+ ellipse(c, eyeX + 2.5, eyeY, 1.1, 1.6, "#e2f6d8");
288
+ }
289
+ c.restore();
290
+ let armAngle = moving ? swing * 0.65 : 0.15;
291
+ let handX = 20, handY = -24;
292
+ if (phase === "waving" || phase === "celebrating") {
293
+ armAngle = -1.4 + Math.sin(t * 8) * 0.22;
294
+ handX = 22 + Math.sin(t * 8) * 3;
295
+ handY = -62;
296
+ }
297
+ else if (phase === "sampling") {
298
+ armAngle = 0.5;
299
+ handX = 25;
300
+ handY = -12 - Math.sin(t * 5) * 3;
301
+ }
302
+ else if (phase === "planting") {
303
+ armAngle = -0.4;
304
+ handX = 24;
305
+ handY = -34;
306
+ }
307
+ else if (phase === "observing") {
308
+ armAngle = -1;
309
+ handX = 20;
310
+ handY = -48;
311
+ }
312
+ else if (jump) {
313
+ armAngle = -1.1;
314
+ handX = 28;
315
+ handY = -48;
316
+ }
317
+ else if (seated) {
318
+ armAngle = -0.7;
319
+ handX = 27;
320
+ handY = -24;
321
+ }
322
+ void armAngle;
323
+ path(c, [12, -39, 19, -34, handX, handY], "#47616b", 8);
324
+ path(c, [12, -39, 19, -34, handX, handY], "#e6e4d1", 6);
325
+ ellipse(c, handX, handY, 4.3, 4.7, "#a1b7b3");
326
+ path(c, [14, -37, 17, -36], o.accent, 3);
327
+ if (phase === "sampling") {
328
+ path(c, [handX + 2, handY - 2, handX + 10, handY - 12], "#a6bbb2", 2);
329
+ path(c, [handX + 5, handY - 14, handX + 15, handY - 10], "#91a7a6", 4);
330
+ if (t > 2.2) {
331
+ round(c, 17, -13, 9, 7, 1, "#afccbe");
332
+ ellipse(c, 21, -16, 5, 5, "#a19e88");
333
+ }
334
+ }
335
+ if (phase === "planting" && t < 1.8) {
336
+ path(c, [25, 0, 25, -60 * ease(t / 1.8)], "#aebdb2", 1.5);
337
+ }
338
+ if (phase === "observing") {
339
+ c.save();
340
+ c.translate(20, -49);
341
+ c.rotate(-0.3);
342
+ round(c, -4, -4, 24, 8, 2, "#6a858d");
343
+ round(c, 15, -5, 7, 10, 2, o.accent);
344
+ ellipse(c, 22, 0, 2, 5, "#c3eddf");
345
+ c.restore();
346
+ }
347
+ if (phase === "sleeping") {
348
+ for (let i = 0; i < 3; i++) {
349
+ c.globalAlpha = (1 - ((m.elapsed * 0.25 + i * 0.3) % 1)) * 0.8;
350
+ c.fillStyle = "#e9d4a9";
351
+ c.font = `${8 + i * 2}px monospace`;
352
+ c.fillText("z", 24 + i * 9, -66 - ((m.elapsed * 8 + i * 12) % 35));
353
+ }
354
+ c.globalAlpha = 1;
355
+ }
356
+ if (phase === "celebrating") {
357
+ for (let i = 0; i < 4; i++) {
358
+ const a = i * 1.8 + t;
359
+ star(c, Math.cos(a) * 31, -66 + Math.sin(a) * 14, 2.5, Math.sin((t * Math.PI) / 1.4));
360
+ }
361
+ }
362
+ if (phase === "waving" && t > 0.3 && t < 2.7) {
363
+ c.save();
364
+ c.scale(m.facing, 1);
365
+ round(c, 28, -83, 32, 20, 6, "#f5f0d8", "#9aa8a5");
366
+ c.beginPath();
367
+ c.moveTo(30, -64);
368
+ c.lineTo(26, -57);
369
+ c.lineTo(38, -64);
370
+ c.fillStyle = "#f5f0d8";
371
+ c.fill();
372
+ c.fillStyle = "#35515b";
373
+ c.font = "bold 9px monospace";
374
+ c.fillText("hi!", 35, -70);
375
+ c.restore();
376
+ }
377
+ c.restore();
378
+ }
379
+ function leg(c, x, y, a, seated, color, crouch = 0) {
380
+ const kneeX = x + Math.sin(a) * 10, kneeY = y + Math.cos(a) * 9, footX = x + Math.sin(a) * 17 + (seated ? 13 : 0), footY = seated ? -4 : -2 - crouch;
381
+ path(c, [x, y, kneeX, kneeY, footX, footY - 3], "#405963", 8);
382
+ path(c, [x, y, kneeX, kneeY, footX, footY - 3], color, 6);
383
+ round(c, footX - 4, footY - 5, 12, 7, 3, "#8aa1a0", "#4b6570");
384
+ path(c, [footX - 2, footY + 1, footX + 7, footY + 1], "#d9dccb", 1.2);
385
+ }
386
+ function limb(c, x, y, mx, my, ex, ey, a, color, width) {
387
+ c.save();
388
+ c.translate(x, y);
389
+ c.rotate(a);
390
+ path(c, [0, 0, mx - x, my - y, ex - x, ey - y], color, width);
391
+ c.restore();
392
+ }
393
+ export function paint(c, m, o) {
394
+ for (const f of m.footprints) {
395
+ c.save();
396
+ c.globalAlpha = clamp(f.life / 65, 0, 0.2);
397
+ c.translate(f.x, f.y);
398
+ c.rotate(f.facing * 0.3);
399
+ ellipse(c, f.side * 2, -0.4, 2.4, 1, "#233d4a");
400
+ c.restore();
401
+ }
402
+ for (const rock of m.rocks) {
403
+ if (rock.collected)
404
+ continue;
405
+ c.save();
406
+ c.translate(rock.x, rock.y);
407
+ c.rotate(0.07);
408
+ shadow(c, 0, 0, rock.size * 1.2, 0.12);
409
+ c.beginPath();
410
+ c.moveTo(-rock.size, 0);
411
+ c.lineTo(-rock.size * 0.6, -rock.size * 0.9);
412
+ c.lineTo(rock.size * 0.3, -rock.size * 1.2);
413
+ c.lineTo(rock.size, -rock.size * 0.4);
414
+ c.lineTo(rock.size * 0.7, 0);
415
+ c.closePath();
416
+ c.fillStyle = "#a0a49b";
417
+ c.fill();
418
+ path(c, [
419
+ -rock.size * 0.5,
420
+ -rock.size * 0.8,
421
+ rock.size * 0.2,
422
+ -rock.size * 0.9,
423
+ rock.size * 0.6,
424
+ -rock.size * 0.3,
425
+ ], "#d7d6c3", 1);
426
+ c.restore();
427
+ }
428
+ for (const f of m.flags) {
429
+ c.save();
430
+ c.translate(f.x, f.y);
431
+ c.scale(m.size, m.size);
432
+ flag(c, 0, 0, m.elapsed * 0.7 + f.phase, o.accent);
433
+ c.restore();
434
+ }
435
+ if (o.lander) {
436
+ c.save();
437
+ c.translate(m.landerX, m.landerY - 32 * m.size);
438
+ c.scale(m.size, m.size);
439
+ lander(c, 0, 0, m.phase, m.phaseTime, o.accent, m.landingStartY, (m.surface.y - m.landerY) / m.size + 32);
440
+ c.restore();
441
+ }
442
+ if (o.rover && m.phase !== "landing") {
443
+ c.save();
444
+ c.translate(m.roverX, m.roverY);
445
+ c.scale(m.size, m.size);
446
+ rover(c, 0, 0, m.elapsed, o.accent, m.facing);
447
+ c.restore();
448
+ }
449
+ for (const d of m.dust) {
450
+ c.save();
451
+ c.globalAlpha = clamp(d.life / d.ttl, 0, 0.4);
452
+ ellipse(c, d.x, d.y, d.radius, d.radius * 0.6, "#b8afa0");
453
+ c.restore();
454
+ }
455
+ if (m.phase !== "landing" || m.phaseTime > 3.35)
456
+ astronaut(c, m, o);
457
+ for (const a of m.stars) {
458
+ c.save();
459
+ const tail = 70, alpha = Math.sin(clamp(1 - a.life / a.ttl, 0, 1) * Math.PI);
460
+ const g = c.createLinearGradient(a.x - tail, a.y - tail * 0.35, a.x, a.y);
461
+ g.addColorStop(0, "#fff0bb00");
462
+ g.addColorStop(1, `rgba(255,240,187,${alpha * 0.8})`);
463
+ path(c, [a.x - tail, a.y - tail * 0.35, a.x, a.y], g, 1.5);
464
+ star(c, a.x, a.y, 3, alpha);
465
+ c.restore();
466
+ }
467
+ }
@@ -0,0 +1,94 @@
1
+ export type ExplorerPhase = "landing" | "egress" | "idle" | "walking" | "jumping" | "sampling" | "planting" | "waving" | "boarding" | "driving" | "dismounting" | "observing" | "sleeping" | "celebrating";
2
+ export type Mission = "explore" | "sample" | "flag" | "rover" | "observe" | "wave" | "jump" | "rest";
3
+ export interface MoonExplorerOptions {
4
+ /** Defaults to the full document. A scoped root is never restyled. */
5
+ root?: HTMLElement;
6
+ /** Upper edges of these elements become landing/walking surfaces. */
7
+ surfaces?: string;
8
+ /** Character scale. Clamped to 0.55–1.8. */
9
+ size?: number;
10
+ /** Animation speed, 0.3–2.5. */
11
+ pace?: number;
12
+ /** Autonomous missions continue when visible. */
13
+ autoExplore?: boolean;
14
+ /** Mouse curiosity and click-to-wave, without blocking page controls. */
15
+ interactive?: boolean;
16
+ rover?: boolean;
17
+ lander?: boolean;
18
+ shootingStars?: boolean;
19
+ /** Persistent flags and sample rocks, bounded to protect long sessions. */
20
+ maxFlags?: number;
21
+ accent?: string;
22
+ seed?: number;
23
+ maxFPS?: number;
24
+ maxDPR?: number;
25
+ zIndex?: number;
26
+ /** Respect reduced-motion by default; static never schedules animation. */
27
+ motion?: "respect" | "static";
28
+ }
29
+ export type MoonExplorerUpdate = Partial<Omit<MoonExplorerOptions, "root" | "surfaces" | "seed">>;
30
+ export interface Rect {
31
+ x: number;
32
+ y: number;
33
+ width: number;
34
+ height: number;
35
+ }
36
+ export interface LunarSurface {
37
+ id: string;
38
+ x: number;
39
+ y: number;
40
+ width: number;
41
+ }
42
+ export interface MoonWorld {
43
+ bounds: Rect;
44
+ view: Rect;
45
+ surfaces: LunarSurface[];
46
+ }
47
+ export interface ExplorerSnapshot {
48
+ state: "idle" | "running" | "paused" | "destroyed";
49
+ phase: ExplorerPhase;
50
+ mission: Mission | null;
51
+ /** Document CSS coordinates. Scrolling changes the camera, not these values. */
52
+ x: number;
53
+ y: number;
54
+ facing: 1 | -1;
55
+ surface: string;
56
+ samples: number;
57
+ flags: number;
58
+ distance: number;
59
+ roverDistance: number;
60
+ missionsCompleted: number;
61
+ elapsed: number;
62
+ reducedMotion: boolean;
63
+ }
64
+ export interface ExplorerEventData {
65
+ phase: ExplorerPhase;
66
+ mission: Mission | null;
67
+ samples: number;
68
+ flags: number;
69
+ surface: string;
70
+ }
71
+ export type ExplorerEvent = "phase" | "mission" | "sample" | "flag" | "landed";
72
+ export interface MoonExplorerController {
73
+ /** Queue a mission. Returns false when unavailable or destroyed. */
74
+ mission(kind: Mission, selector?: string): boolean;
75
+ explore(selector?: string): boolean;
76
+ collect(): boolean;
77
+ plantFlag(): boolean;
78
+ ride(): boolean;
79
+ observe(): boolean;
80
+ jump(): boolean;
81
+ wave(): boolean;
82
+ rest(): boolean;
83
+ /** A decorative shooting star, visible immediately when animation is running. */
84
+ shootingStar(): void;
85
+ /** Repeat the landing sequence on a measured surface. */
86
+ land(selector?: string): boolean;
87
+ pause(): void;
88
+ resume(): void;
89
+ refresh(): void;
90
+ update(options: MoonExplorerUpdate): void;
91
+ destroy(): void;
92
+ getState(): Readonly<ExplorerSnapshot>;
93
+ on(event: ExplorerEvent, handler: (data: ExplorerEventData) => void): () => void;
94
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/licensing.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "Fluri Themes Commercial License",
3
+ "version": "1.0",
4
+ "effectiveDate": "2026-11-14",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "url": "https://fluri.xyz/license/",
7
+ "price": {
8
+ "amount": 19,
9
+ "currency": "EUR",
10
+ "billing": "one-time"
11
+ },
12
+ "personalProjects": "unlimited",
13
+ "clientProjects": "unlimited",
14
+ "futureThemesIncluded": true,
15
+ "packages": {
16
+ "@fluritheme/fluri": {
17
+ "lastMIT": "0.2.1",
18
+ "firstCommercial": "0.3.0"
19
+ },
20
+ "@fluritheme/fluri-react": {
21
+ "lastMIT": "0.2.1",
22
+ "firstCommercial": "0.3.0"
23
+ },
24
+ "@fluritheme/spider": {
25
+ "lastMIT": "0.2.1",
26
+ "firstCommercial": "0.3.0"
27
+ },
28
+ "@fluritheme/grinch": {
29
+ "lastMIT": "0.1.1",
30
+ "firstCommercial": "0.2.0"
31
+ },
32
+ "@fluritheme/black-cat": {
33
+ "firstCommercial": "0.1.0"
34
+ },
35
+ "@fluritheme/ghost": {
36
+ "firstCommercial": "0.1.0"
37
+ },
38
+ "@fluritheme/rainy-window": {
39
+ "firstCommercial": "0.1.0"
40
+ },
41
+ "@fluritheme/tiny-builders": {
42
+ "firstCommercial": "0.1.0"
43
+ },
44
+ "@fluritheme/origami-world": {
45
+ "firstCommercial": "0.1.0"
46
+ },
47
+ "@fluritheme/moon-explorer": {
48
+ "firstCommercial": "0.1.0"
49
+ }
50
+ }
51
+ }
package/package.json CHANGED
@@ -1,6 +1,81 @@
1
1
  {
2
2
  "name": "@fluritheme/moon-explorer",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "A beautifully animated little lunar expedition for real websites: astronaut landing, low-gravity walks, rock sampling, flags, rover rides, telescope observations and shooting stars.",
5
+ "type": "module",
6
+ "license": "SEE LICENSE IN LICENSE",
7
+ "homepage": "https://fluri.xyz/",
8
+ "author": {
9
+ "name": "František Gejdoš",
10
+ "email": "fluritheme@gmail.com",
11
+ "url": "https://fluri.xyz/founder/"
12
+ },
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/fluritheme/moon-explorer.git"
16
+ },
17
+ "bugs": {
18
+ "email": "fluritheme@gmail.com"
19
+ },
20
+ "keywords": [
21
+ "moon",
22
+ "astronaut",
23
+ "space",
24
+ "lunar",
25
+ "rover",
26
+ "website-effects",
27
+ "canvas",
28
+ "animation",
29
+ "interactive",
30
+ "typescript",
31
+ "fluri",
32
+ "zero-dependencies"
33
+ ],
34
+ "sideEffects": false,
35
+ "files": [
36
+ "dist",
37
+ "README.md",
38
+ "CHANGELOG.md",
39
+ "LICENSE",
40
+ "LICENSE-MIT-LEGACY",
41
+ "LICENSING.md",
42
+ "licensing.json"
43
+ ],
44
+ "exports": {
45
+ ".": {
46
+ "types": "./dist/index.d.ts",
47
+ "import": "./dist/index.js"
48
+ },
49
+ "./package.json": "./package.json"
50
+ },
51
+ "main": "./dist/index.js",
52
+ "types": "./dist/index.d.ts",
53
+ "publishConfig": {
54
+ "access": "public"
55
+ },
56
+ "engines": {
57
+ "node": ">=22.12"
58
+ },
59
+ "packageManager": "pnpm@10.33.2",
60
+ "scripts": {
61
+ "build": "tsc -p tsconfig.build.json",
62
+ "dev": "vite --host 127.0.0.1 --port 5312",
63
+ "typecheck": "tsc --noEmit",
64
+ "test": "vitest run",
65
+ "test:browser": "playwright test",
66
+ "format": "prettier --write .",
67
+ "format:check": "prettier --check .",
68
+ "release:check": "node scripts/release-check.mjs",
69
+ "check": "pnpm typecheck && pnpm test && pnpm build && pnpm test:browser && pnpm format:check && pnpm release:check",
70
+ "prepublishOnly": "pnpm build && pnpm release:check",
71
+ "previews": "node scripts/render-previews.mjs"
72
+ },
73
+ "devDependencies": {
74
+ "@playwright/test": "1.64.1",
75
+ "@types/node": "26.6.6",
76
+ "prettier": "3.9.10",
77
+ "typescript": "6.0.3",
78
+ "vite": "8.3.6",
79
+ "vitest": "5.1.0"
80
+ }
81
+ }