@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/CHANGELOG.md +5 -0
- package/LICENSE +56 -0
- package/LICENSE-MIT-LEGACY +23 -0
- package/LICENSING.md +22 -0
- package/README.md +159 -2
- package/dist/index.d.ts +4 -0
- package/dist/index.js +428 -0
- package/dist/model.d.ts +100 -0
- package/dist/model.js +429 -0
- package/dist/render.d.ts +13 -0
- package/dist/render.js +467 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.js +1 -0
- package/licensing.json +51 -0
- package/package.json +79 -4
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
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -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.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
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
|
+
}
|