ghosto 0.0.2

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.mjs ADDED
@@ -0,0 +1,1284 @@
1
+ import { Canvas, useFrame, useThree } from "@react-three/fiber";
2
+ import { Suspense, lazy, useCallback, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
3
+ import * as THREE from "three";
4
+ import { create } from "zustand";
5
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
6
+ import { get, set } from "idb-keyval";
7
+ //#region \0rolldown/runtime.js
8
+ var __defProp = Object.defineProperty;
9
+ var __exportAll = (all, no_symbols) => {
10
+ let target = {};
11
+ for (var name in all) __defProp(target, name, {
12
+ get: all[name],
13
+ enumerable: true
14
+ });
15
+ if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
16
+ return target;
17
+ };
18
+ //#endregion
19
+ //#region src/constants.ts
20
+ const BODY_BREATHE_HZ = .42;
21
+ const BODY_BASE_SCALE = .55;
22
+ const CAMERA_NEAR = .1;
23
+ const EYE_HALF_SPACING = .18;
24
+ const EYE_VERTICAL_OFFSET = .16;
25
+ const BLINK_DURATION_S = .12;
26
+ const CHANNEL_DECAY_S = {
27
+ affection: 30,
28
+ affinity: Number.POSITIVE_INFINITY,
29
+ annoyance: 3,
30
+ attention: 4,
31
+ boredom: 600,
32
+ cheer: .6,
33
+ curiosity: .8,
34
+ dizzy: .5,
35
+ energy: 1800,
36
+ excitement: .5,
37
+ fatigue: 3600,
38
+ heat: .6,
39
+ hunger: 1800,
40
+ joy: .6,
41
+ loneliness: 40,
42
+ magnetism: 1.5,
43
+ worry: 4
44
+ };
45
+ const AFFINITY_TIERS = [
46
+ {
47
+ interactions: 0,
48
+ label: "shy"
49
+ },
50
+ {
51
+ interactions: 10,
52
+ label: "curious"
53
+ },
54
+ {
55
+ interactions: 50,
56
+ label: "bold"
57
+ },
58
+ {
59
+ interactions: 200,
60
+ label: "bestFriend"
61
+ },
62
+ {
63
+ interactions: 1e3,
64
+ label: "lifelong"
65
+ }
66
+ ];
67
+ const COLOR_BODY = "oklch(0.99 0.003 250)";
68
+ const COLOR_HEAT = "oklch(0.78 0.20 50)";
69
+ //#endregion
70
+ //#region src/shader/body.frag.ts
71
+ const bodyFrag = `
72
+ varying vec3 vNormal;
73
+ varying vec3 vWorldPos;
74
+ uniform vec3 uBaseColor;
75
+ uniform vec3 uCoreColor;
76
+ uniform float uHeat;
77
+ uniform float uJoy;
78
+ uniform vec3 uPointer3D;
79
+ uniform vec3 uCameraPos;
80
+ uniform float uTime;
81
+ void main(){
82
+ vec3 N = normalize(vNormal);
83
+ vec3 V = normalize(uCameraPos - vWorldPos);
84
+ vec3 L1 = normalize(vec3(-0.4, 0.85, 0.5));
85
+ vec3 L2 = normalize(vec3(0.7, -0.2, 0.6));
86
+ float NdotL1 = max(dot(N, L1), 0.0);
87
+ float NdotL2 = max(dot(N, L2), 0.0);
88
+ float NdotV = max(dot(N, V), 0.0);
89
+ float wrap = 1.0;
90
+ float diffuse = max((dot(N, L1) + wrap) / (1.0 + wrap), 0.0) * 0.35 + 0.65;
91
+ float fillLight = NdotL2 * 0.18;
92
+ float fresnel = pow(1.0 - NdotV, 3.5);
93
+ vec3 col = uBaseColor * (diffuse + fillLight);
94
+ col *= (1.0 - fresnel * 0.18);
95
+ col += vec3(0.85, 0.88, 0.92) * fresnel * 0.05;
96
+ float dPointer = length(vWorldPos - uPointer3D);
97
+ float radial = smoothstep(0.9, 0.0, dPointer);
98
+ col = mix(col, uCoreColor, radial * uHeat * 0.3);
99
+ col += col * uJoy * 0.05;
100
+ gl_FragColor = vec4(col, 1.0);
101
+ }
102
+ `;
103
+ //#endregion
104
+ //#region src/shader/body.vert.ts
105
+ const bodyVert = `
106
+ varying vec3 vNormal;
107
+ varying vec3 vWorldPos;
108
+ varying float vDisplace;
109
+ uniform float uTime;
110
+ uniform float uExcitement;
111
+ uniform float uHeat;
112
+ uniform float uBreathePhase;
113
+ uniform vec3 uImpulses[8];
114
+ uniform float uImpulseStrength[8];
115
+ vec3 mod289(vec3 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; }
116
+ vec4 mod289v(vec4 x){ return x - floor(x * (1.0 / 289.0)) * 289.0; }
117
+ vec4 permute(vec4 x){ return mod289v(((x*34.0)+1.0)*x); }
118
+ vec4 taylorInvSqrt(vec4 r){ return 1.79284291400159 - 0.85373472095314 * r; }
119
+ float snoise(vec3 v){
120
+ const vec2 C = vec2(1.0/6.0, 1.0/3.0);
121
+ const vec4 D = vec4(0.0, 0.5, 1.0, 2.0);
122
+ vec3 i = floor(v + dot(v, C.yyy));
123
+ vec3 x0 = v - i + dot(i, C.xxx);
124
+ vec3 g = step(x0.yzx, x0.xyz);
125
+ vec3 l = 1.0 - g;
126
+ vec3 i1 = min(g.xyz, l.zxy);
127
+ vec3 i2 = max(g.xyz, l.zxy);
128
+ vec3 x1 = x0 - i1 + C.xxx;
129
+ vec3 x2 = x0 - i2 + C.yyy;
130
+ vec3 x3 = x0 - D.yyy;
131
+ i = mod289(i);
132
+ vec4 p = permute(permute(permute(
133
+ i.z + vec4(0.0, i1.z, i2.z, 1.0))
134
+ + i.y + vec4(0.0, i1.y, i2.y, 1.0))
135
+ + i.x + vec4(0.0, i1.x, i2.x, 1.0));
136
+ float n_ = 0.142857142857;
137
+ vec3 ns = n_ * D.wyz - D.xzx;
138
+ vec4 j = p - 49.0 * floor(p * ns.z * ns.z);
139
+ vec4 x_ = floor(j * ns.z);
140
+ vec4 y_ = floor(j - 7.0 * x_);
141
+ vec4 x = x_ *ns.x + ns.yyyy;
142
+ vec4 y = y_ *ns.x + ns.yyyy;
143
+ vec4 h = 1.0 - abs(x) - abs(y);
144
+ vec4 b0 = vec4(x.xy, y.xy);
145
+ vec4 b1 = vec4(x.zw, y.zw);
146
+ vec4 s0 = floor(b0)*2.0 + 1.0;
147
+ vec4 s1 = floor(b1)*2.0 + 1.0;
148
+ vec4 sh = -step(h, vec4(0.0));
149
+ vec4 a0 = b0.xzyw + s0.xzyw*sh.xxyy;
150
+ vec4 a1 = b1.xzyw + s1.xzyw*sh.zzww;
151
+ vec3 p0 = vec3(a0.xy, h.x);
152
+ vec3 p1 = vec3(a0.zw, h.y);
153
+ vec3 p2 = vec3(a1.xy, h.z);
154
+ vec3 p3 = vec3(a1.zw, h.w);
155
+ vec4 norm = taylorInvSqrt(vec4(dot(p0,p0), dot(p1,p1), dot(p2,p2), dot(p3,p3)));
156
+ p0 *= norm.x; p1 *= norm.y; p2 *= norm.z; p3 *= norm.w;
157
+ vec4 m = max(0.6 - vec4(dot(x0,x0), dot(x1,x1), dot(x2,x2), dot(x3,x3)), 0.0);
158
+ m = m * m;
159
+ return 42.0 * dot(m*m, vec4(dot(p0,x0), dot(p1,x1), dot(p2,x2), dot(p3,x3)));
160
+ }
161
+ void main(){
162
+ vec3 p = position;
163
+ float freq = mix(1.4, 3.5, uExcitement);
164
+ float amp = mix(0.0, 0.04, uExcitement);
165
+ float n = snoise(p * freq + uTime * 0.6);
166
+ p += normal * (n * amp);
167
+ p *= 1.0 + sin(uBreathePhase) * 0.018;
168
+ for(int i = 0; i < 8; i++){
169
+ float s = uImpulseStrength[i];
170
+ if(s <= 0.0) continue;
171
+ vec3 ip = uImpulses[i];
172
+ float d = length(p - ip);
173
+ p -= normal * (s * exp(-d*d * 12.0));
174
+ }
175
+ vDisplace = n * amp;
176
+ vNormal = normalize(normalMatrix * normal);
177
+ vec4 wp = modelMatrix * vec4(p, 1.0);
178
+ vWorldPos = wp.xyz;
179
+ gl_Position = projectionMatrix * viewMatrix * wp;
180
+ }
181
+ `;
182
+ //#endregion
183
+ //#region src/use-mascot-channels.ts
184
+ const ZERO = {
185
+ affection: 0,
186
+ affinity: 0,
187
+ annoyance: 0,
188
+ attention: 0,
189
+ boredom: 0,
190
+ cheer: 0,
191
+ curiosity: 0,
192
+ dizzy: 0,
193
+ energy: .5,
194
+ excitement: 0,
195
+ fatigue: 0,
196
+ heat: 0,
197
+ hunger: 0,
198
+ joy: 0,
199
+ loneliness: 0,
200
+ magnetism: 0,
201
+ worry: 0
202
+ };
203
+ const smoothedState = { ...ZERO };
204
+ let smoothK = 7;
205
+ const clamp01 = (x) => Math.max(0, Math.min(1, x));
206
+ const useMascotChannels = create((setState, getState) => ({
207
+ bump: (name, delta, cap = 1) => {
208
+ setState((s) => ({ channels: {
209
+ ...s.channels,
210
+ [name]: clamp01(Math.min(cap, s.channels[name] + delta))
211
+ } }));
212
+ },
213
+ channels: { ...ZERO },
214
+ decay: (dt) => {
215
+ const next = { ...getState().channels };
216
+ for (const key of Object.keys(next)) {
217
+ const tau = CHANNEL_DECAY_S[key];
218
+ if (Number.isFinite(tau) && tau >= .01) next[key] *= Math.exp(-dt / tau);
219
+ }
220
+ setState({ channels: next });
221
+ },
222
+ set: (name, value) => {
223
+ setState((s) => ({ channels: {
224
+ ...s.channels,
225
+ [name]: clamp01(value)
226
+ } }));
227
+ },
228
+ setSmoothK: (k) => {
229
+ smoothK = k;
230
+ },
231
+ smoothed: () => ({ ...smoothedState }),
232
+ snapshot: () => ({ ...getState().channels }),
233
+ tick: (dt) => {
234
+ const current = getState().channels;
235
+ const alpha = 1 - Math.exp(-smoothK * dt);
236
+ for (const key of Object.keys(current)) smoothedState[key] += (current[key] - smoothedState[key]) * alpha;
237
+ }
238
+ }));
239
+ //#endregion
240
+ //#region src/use-mascot-pose.ts
241
+ const useMascotPose = create((setState) => ({
242
+ bodyScreenPx: {
243
+ x: 0,
244
+ y: 0
245
+ },
246
+ homeWorld: {
247
+ x: 0,
248
+ y: .1,
249
+ z: 0
250
+ },
251
+ setBodyScreenPx: (x, y) => {
252
+ setState((s) => s.bodyScreenPx.x === x && s.bodyScreenPx.y === y ? s : { bodyScreenPx: {
253
+ x,
254
+ y
255
+ } });
256
+ },
257
+ setHomeWorld: (x, y, z) => {
258
+ setState((s) => s.homeWorld.x === x && s.homeWorld.y === y && s.homeWorld.z === z ? s : { homeWorld: {
259
+ x,
260
+ y,
261
+ z
262
+ } });
263
+ }
264
+ }));
265
+ //#endregion
266
+ //#region src/mascot-body.tsx
267
+ /** biome-ignore-all lint/nursery/noUnknownAttribute: R3F intrinsics */
268
+ /** biome-ignore-all lint/performance/noNamespaceImport: three convention */
269
+ const SPHERE_ARGS$1 = [
270
+ 1,
271
+ 64,
272
+ 48
273
+ ];
274
+ const MAX_IMPULSES = 8;
275
+ const DRAG_SPRING_K = 18;
276
+ const RELEASE_SPRING_K = 6;
277
+ const SQUASH_GAIN = .06;
278
+ const SQUASH_MAX = .45;
279
+ const MascotBody = ({ baseHue = 0, onCenter, pointer3D }) => {
280
+ const meshRef = useRef(null);
281
+ const getSmoothed = useMascotChannels((s) => s.smoothed);
282
+ const uniforms = useMemo(() => ({
283
+ uBaseColor: { value: new THREE.Color(COLOR_BODY) },
284
+ uBreathePhase: { value: 0 },
285
+ uCameraPos: { value: new THREE.Vector3() },
286
+ uCoreColor: { value: new THREE.Color(COLOR_HEAT) },
287
+ uDiscoveryAura: { value: 0 },
288
+ uExcitement: { value: 0 },
289
+ uHeat: { value: 0 },
290
+ uImpulseStrength: { value: new Float32Array(MAX_IMPULSES) },
291
+ uImpulses: { value: Array.from({ length: MAX_IMPULSES }, () => new THREE.Vector3()) },
292
+ uJoy: { value: 0 },
293
+ uPointer3D: { value: new THREE.Vector3() },
294
+ uTime: { value: 0 }
295
+ }), []);
296
+ const tmpVRef = useRef(new THREE.Vector3());
297
+ const bounceRef = useRef({
298
+ end: 0,
299
+ lastExc: 0
300
+ });
301
+ const dragRef = useRef({ active: false });
302
+ const velRef = useRef({
303
+ x: 0,
304
+ y: 0
305
+ });
306
+ const lastPosRef = useRef({
307
+ x: 0,
308
+ y: 0
309
+ });
310
+ const getHome = useMascotPose((s) => s.homeWorld);
311
+ useEffect(() => {
312
+ const onDown = (e) => {
313
+ const b = useMascotPose.getState().bodyScreenPx;
314
+ if (Math.hypot(e.clientX - b.x, e.clientY - b.y) < 75) dragRef.current.active = true;
315
+ };
316
+ const onUp = () => {
317
+ dragRef.current.active = false;
318
+ };
319
+ globalThis.addEventListener("pointerdown", onDown);
320
+ globalThis.addEventListener("pointerup", onUp);
321
+ globalThis.addEventListener("pointercancel", onUp);
322
+ return () => {
323
+ globalThis.removeEventListener("pointerdown", onDown);
324
+ globalThis.removeEventListener("pointerup", onUp);
325
+ globalThis.removeEventListener("pointercancel", onUp);
326
+ };
327
+ }, []);
328
+ useFrame((state, dt) => {
329
+ const c = getSmoothed();
330
+ const t = state.clock.elapsedTime;
331
+ uniforms.uTime.value = t;
332
+ uniforms.uExcitement.value = c.excitement;
333
+ uniforms.uHeat.value = c.heat;
334
+ uniforms.uJoy.value = c.joy;
335
+ uniforms.uBreathePhase.value = t * BODY_BREATHE_HZ * Math.PI * 2;
336
+ uniforms.uPointer3D.value.copy(pointer3D);
337
+ state.camera.getWorldPosition(uniforms.uCameraPos.value);
338
+ if (baseHue !== 0) {
339
+ const baseCol = new THREE.Color(COLOR_BODY);
340
+ baseCol.offsetHSL(baseHue / 360, 0, 0);
341
+ uniforms.uBaseColor.value.copy(baseCol);
342
+ }
343
+ if (!meshRef.current) return;
344
+ const mesh = meshRef.current;
345
+ const isDrag = dragRef.current.active;
346
+ const leanGain = Math.min(1, c.magnetism * 1.2);
347
+ const idleSwayX = (Math.sin(t * .7) * .6 + Math.sin(t * .31) * .4) * .01;
348
+ const idleSwayY = (Math.cos(t * .55) * .5 + Math.sin(t * .23) * .3) * .006;
349
+ const targetX = isDrag ? pointer3D.x : getHome.x + pointer3D.x * .32 * leanGain + idleSwayX;
350
+ const targetY = isDrag ? pointer3D.y : getHome.y + pointer3D.y * .22 * leanGain + idleSwayY;
351
+ const alpha = 1 - Math.exp(-(isDrag ? DRAG_SPRING_K : RELEASE_SPRING_K) * dt);
352
+ const cur = mesh.position;
353
+ const prevX = cur.x;
354
+ const prevY = cur.y;
355
+ cur.x += (targetX - cur.x) * alpha;
356
+ cur.y += (targetY - cur.y) * alpha;
357
+ cur.z = getHome.z;
358
+ const dxv = (cur.x - prevX) / Math.max(dt, .001);
359
+ const dyv = (cur.y - prevY) / Math.max(dt, .001);
360
+ const velAlpha = 1 - Math.exp(-12 * dt);
361
+ velRef.current.x += (dxv - velRef.current.x) * velAlpha;
362
+ velRef.current.y += (dyv - velRef.current.y) * velAlpha;
363
+ lastPosRef.current.x = cur.x;
364
+ lastPosRef.current.y = cur.y;
365
+ const speed = Math.hypot(velRef.current.x, velRef.current.y);
366
+ const squash = Math.min(SQUASH_MAX, speed * SQUASH_GAIN);
367
+ const angle = Math.atan2(velRef.current.y, velRef.current.x);
368
+ mesh.rotation.set(0, 0, angle);
369
+ const breathe = Math.sin(t * BODY_BREATHE_HZ * Math.PI * 2) * .006;
370
+ const bounce = bounceRef.current;
371
+ if (c.excitement - bounce.lastExc > .2) bounce.end = t + .22;
372
+ bounce.lastExc = c.excitement;
373
+ let bounceAmp = 0;
374
+ if (t < bounce.end) {
375
+ const phase = (bounce.end - t) / .22;
376
+ bounceAmp = Math.sin((1 - phase) * Math.PI) * .12;
377
+ }
378
+ const baseScale = BODY_BASE_SCALE + c.joy * .04 - c.fatigue * .02 + breathe + bounceAmp;
379
+ const sxStretch = 1 + squash;
380
+ const syStretch = 1 - squash * .6;
381
+ mesh.scale.set(baseScale * sxStretch, baseScale * syStretch, baseScale * (1 - squash * .3));
382
+ mesh.getWorldPosition(tmpVRef.current);
383
+ onCenter?.(tmpVRef.current);
384
+ });
385
+ return /* @__PURE__ */ jsxs("mesh", {
386
+ ref: meshRef,
387
+ children: [/* @__PURE__ */ jsx("sphereGeometry", { args: SPHERE_ARGS$1 }), /* @__PURE__ */ jsx("shaderMaterial", {
388
+ fragmentShader: bodyFrag,
389
+ uniforms,
390
+ vertexShader: bodyVert
391
+ })]
392
+ });
393
+ };
394
+ //#endregion
395
+ //#region src/shader/eye.frag.ts
396
+ const eyeVert = `
397
+ varying vec3 vNormal;
398
+ varying vec3 vWorldPos;
399
+ uniform float uClosed;
400
+ uniform float uSad;
401
+ uniform float uBlink;
402
+ void main(){
403
+ vec3 p = position;
404
+ p.x *= 0.42;
405
+ p.y *= 1.35;
406
+ p.z *= 0.42;
407
+ float blendUp = clamp(uClosed - uSad * 0.5, 0.0, 1.0);
408
+ float blendDn = clamp(uSad, 0.0, 1.0);
409
+ float blend = max(blendUp, blendDn);
410
+ float s = blendDn > blendUp ? -1.0 : 1.0;
411
+ float wideMul = mix(1.0, 3.2, blend);
412
+ float xWide = p.x * wideMul;
413
+ float archPeak = s * 0.04;
414
+ float archK = 5.5;
415
+ float archY = archPeak - s * archK * xWide * xWide;
416
+ float tubeFlat = mix(1.0, 0.14, blend);
417
+ float arched = mix(p.y, archY + p.y * tubeFlat, blend);
418
+ arched -= uBlink * 0.05;
419
+ p.x = xWide;
420
+ p.y = arched;
421
+ p.z = p.z * mix(1.0, 0.4, blend);
422
+ vec4 wp = modelMatrix * vec4(p, 1.0);
423
+ vWorldPos = wp.xyz;
424
+ vNormal = normalize(normalMatrix * normal);
425
+ gl_Position = projectionMatrix * viewMatrix * wp;
426
+ }
427
+ `;
428
+ const eyeFrag = `
429
+ varying vec3 vNormal;
430
+ varying vec3 vWorldPos;
431
+ uniform vec3 uColor;
432
+ uniform vec3 uCameraPos;
433
+ uniform float uHighlight;
434
+ void main(){
435
+ vec3 N = normalize(vNormal);
436
+ vec3 V = normalize(uCameraPos - vWorldPos);
437
+ float NdotV = max(dot(N, V), 0.0);
438
+ float rim = pow(1.0 - NdotV, 1.2);
439
+ vec3 col = uColor * (0.92 + 0.08 * rim) + vec3(0.08) * uHighlight;
440
+ gl_FragColor = vec4(col, 1.0);
441
+ }
442
+ `;
443
+ //#endregion
444
+ //#region src/use-reduced-motion.ts
445
+ const QUERY = "(prefers-reduced-motion: reduce)";
446
+ const subscribe = (cb) => {
447
+ if (typeof globalThis.matchMedia !== "function") return () => void 0;
448
+ const mql = globalThis.matchMedia(QUERY);
449
+ mql.addEventListener("change", cb);
450
+ return () => mql.removeEventListener("change", cb);
451
+ };
452
+ const getSnapshot = () => {
453
+ if (typeof globalThis.matchMedia !== "function") return false;
454
+ return globalThis.matchMedia(QUERY).matches;
455
+ };
456
+ const getServerSnapshot = () => false;
457
+ const useReducedMotion = () => useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
458
+ //#endregion
459
+ //#region src/mascot-eyes.tsx
460
+ /** biome-ignore-all lint/nursery/noUnknownAttribute: R3F intrinsics */
461
+ /** biome-ignore-all lint/performance/noNamespaceImport: three convention */
462
+ const EYE_RADIUS = .1;
463
+ const EYE_FRONT_Z = .55;
464
+ const SPHERE_ARGS = [
465
+ EYE_RADIUS,
466
+ 32,
467
+ 24
468
+ ];
469
+ const TRACK_OFFSET_GAIN = .07;
470
+ const PURSUIT_GAIN = 1;
471
+ const IDLE_AMP_X = .6;
472
+ const IDLE_AMP_Y = .4;
473
+ const SACCADE_AMP = .08;
474
+ const EYE_COLOR = new THREE.Color("#020202");
475
+ const LIGHT_DIR = new THREE.Vector3(.4, .85, .5).normalize();
476
+ const clamp = (x, lo, hi) => Math.max(lo, Math.min(hi, x));
477
+ const idleWalk = (t, s) => .55 * Math.sin(t * .27 + s[0]) + .3 * Math.sin(t * .61 + s[1]) + .18 * Math.sin(t * 1.13 + s[2]);
478
+ const SingleEye = ({ bodyCenter, pointer3D, side }) => {
479
+ const meshRef = useRef(null);
480
+ const getSmoothed = useMascotChannels((s) => s.smoothed);
481
+ const followK = useReducedMotion() ? 3 : 6;
482
+ const uniforms = useMemo(() => ({
483
+ uBlink: { value: 0 },
484
+ uCameraPos: { value: new THREE.Vector3() },
485
+ uClosed: { value: 0 },
486
+ uColor: { value: EYE_COLOR },
487
+ uHighlight: { value: 0 },
488
+ uLightDir: { value: LIGHT_DIR },
489
+ uSad: { value: 0 }
490
+ }), []);
491
+ const trackRef = useRef({
492
+ x: 0,
493
+ y: 0
494
+ });
495
+ const blinkRef = useRef({
496
+ delay: side === -1 ? .04 : 0,
497
+ end: 0,
498
+ nextAt: 0,
499
+ spring: 0,
500
+ springV: 0
501
+ });
502
+ const sideSeedsRef = useRef({
503
+ sa: side * 7.13 + 1.9,
504
+ sa2: side * 9.41 + .3,
505
+ sx: [
506
+ side * 4.13 + .7,
507
+ side * 2.91 + 1.4,
508
+ side * 5.32 + .2
509
+ ],
510
+ sy: [
511
+ side * 3.27 + 2.1,
512
+ side * 1.85 + 3.6,
513
+ side * 6.04 + .5
514
+ ]
515
+ });
516
+ useFrame((state, dt) => {
517
+ const mesh = meshRef.current;
518
+ if (!mesh) return;
519
+ let probeSearch;
520
+ try {
521
+ ({search: probeSearch} = globalThis.location);
522
+ } catch {
523
+ probeSearch = "";
524
+ }
525
+ if (probeSearch.includes("test=1")) {
526
+ const probe = globalThis;
527
+ probe.__mascotEyes ??= [];
528
+ if (!probe.__mascotEyes.includes(mesh)) probe.__mascotEyes.push(mesh);
529
+ }
530
+ const matUniforms = mesh.material.uniforms;
531
+ state.camera.getWorldPosition(matUniforms.uCameraPos.value);
532
+ const anchorX = bodyCenter.x + side * EYE_HALF_SPACING;
533
+ const anchorY = bodyCenter.y + EYE_VERTICAL_OFFSET;
534
+ const anchorZ = bodyCenter.z + EYE_FRONT_Z;
535
+ const c = getSmoothed();
536
+ const now = state.clock.elapsedTime;
537
+ const seeds = sideSeedsRef.current;
538
+ const idleX = idleWalk(now, seeds.sx) * IDLE_AMP_X;
539
+ const idleY = idleWalk(now, seeds.sy) * IDLE_AMP_Y;
540
+ const dx = pointer3D.x - anchorX;
541
+ const dy = pointer3D.y - anchorY;
542
+ const pointerTargetX = clamp(dx / 2.5, -1, 1) * PURSUIT_GAIN;
543
+ const pointerTargetY = clamp(dy / 2.5, -1, 1) * PURSUIT_GAIN;
544
+ const attentionMag = clamp(c.attention + c.curiosity * .8 + c.magnetism * .6, 0, 1);
545
+ const idleWeight = 1 - .6 * attentionMag;
546
+ const focusWeight = .4 + .6 * attentionMag;
547
+ const targetX = pointerTargetX * focusWeight + idleX * idleWeight;
548
+ const targetY = pointerTargetY * focusWeight + idleY * idleWeight;
549
+ const alpha = 1 - Math.exp(-followK * dt);
550
+ trackRef.current.x += (targetX - trackRef.current.x) * alpha;
551
+ trackRef.current.y += (targetY - trackRef.current.y) * alpha;
552
+ const exDiz = .6 + c.excitement + c.dizzy;
553
+ const saccadeX = Math.sin(now * 11.3 + seeds.sa) * Math.sin(now * 2.7 + seeds.sa2) * SACCADE_AMP * exDiz;
554
+ const saccadeY = Math.cos(now * 9.1 + seeds.sa2) * Math.sin(now * 3.1 + seeds.sa) * SACCADE_AMP * .7 * exDiz;
555
+ const closed = clamp(.95 * c.joy + .7 * c.cheer + .15 * c.affection, 0, 1);
556
+ const sad = clamp(c.loneliness * .8 + c.worry * .4 - c.joy, 0, 1);
557
+ const blink = blinkRef.current;
558
+ if (blink.nextAt === 0) blink.nextAt = now + 3 + Math.random() * 4;
559
+ if (now > blink.nextAt && now > blink.end + blink.delay) {
560
+ blink.end = now + BLINK_DURATION_S + blink.delay;
561
+ blink.nextAt = now + 3 + Math.random() * 4;
562
+ }
563
+ const force = 90 * ((now > blink.delay && now < blink.end ? Math.sin((blink.end - now) / BLINK_DURATION_S * Math.PI) : 0) - blink.spring) - 14 * blink.springV;
564
+ blink.springV += force * dt;
565
+ blink.spring += blink.springV * dt;
566
+ const blinkOut = clamp(blink.spring, 0, 1);
567
+ matUniforms.uClosed.value = closed;
568
+ matUniforms.uSad.value = sad;
569
+ matUniforms.uBlink.value = blinkOut;
570
+ matUniforms.uHighlight.value = clamp(c.joy + c.attention * .7 + .08 * Math.sin(now * 1.9), 0, 1);
571
+ const tx = (trackRef.current.x + saccadeX) * TRACK_OFFSET_GAIN;
572
+ const ty = (trackRef.current.y + saccadeY) * TRACK_OFFSET_GAIN;
573
+ mesh.position.set(anchorX + tx, anchorY + ty - sad * .04, anchorZ);
574
+ });
575
+ return /* @__PURE__ */ jsxs("mesh", {
576
+ ref: meshRef,
577
+ children: [/* @__PURE__ */ jsx("sphereGeometry", { args: SPHERE_ARGS }), /* @__PURE__ */ jsx("shaderMaterial", {
578
+ depthTest: false,
579
+ fragmentShader: eyeFrag,
580
+ uniforms,
581
+ vertexShader: eyeVert
582
+ })]
583
+ });
584
+ };
585
+ const MascotEyes = ({ bodyCenter, pointer3D }) => /* @__PURE__ */ jsxs(Fragment, { children: [/* @__PURE__ */ jsx(SingleEye, {
586
+ bodyCenter,
587
+ pointer3D,
588
+ side: -1
589
+ }), /* @__PURE__ */ jsx(SingleEye, {
590
+ bodyCenter,
591
+ pointer3D,
592
+ side: 1
593
+ })] });
594
+ //#endregion
595
+ //#region src/svg-fallback.tsx
596
+ const SvgFallback = () => /* @__PURE__ */ jsx("div", {
597
+ className: "flex size-full items-center justify-center",
598
+ children: /* @__PURE__ */ jsxs("svg", {
599
+ className: "size-40 animate-pulse text-foreground/80",
600
+ fill: "currentColor",
601
+ viewBox: "0 0 100 100",
602
+ xmlns: "http://www.w3.org/2000/svg",
603
+ children: [
604
+ /* @__PURE__ */ jsx("title", { children: "Mascot fallback" }),
605
+ /* @__PURE__ */ jsx("circle", {
606
+ cx: "50",
607
+ cy: "50",
608
+ fill: "currentColor",
609
+ opacity: "0.92",
610
+ r: "38"
611
+ }),
612
+ /* @__PURE__ */ jsx("ellipse", {
613
+ cx: "42",
614
+ cy: "46",
615
+ fill: "#0a0a14",
616
+ rx: "3",
617
+ ry: "6"
618
+ }),
619
+ /* @__PURE__ */ jsx("ellipse", {
620
+ cx: "58",
621
+ cy: "46",
622
+ fill: "#0a0a14",
623
+ rx: "3",
624
+ ry: "6"
625
+ })
626
+ ]
627
+ })
628
+ });
629
+ //#endregion
630
+ //#region src/use-history-channel.ts
631
+ const KEY$1 = "va.mascot.v1.affinity.self";
632
+ const tierFor = (interactions) => {
633
+ let chosen = AFFINITY_TIERS[0];
634
+ for (const t of AFFINITY_TIERS) if (interactions >= t.interactions) chosen = t;
635
+ return chosen;
636
+ };
637
+ const useHistoryChannel = ({ enabled = true } = {}) => {
638
+ const setCh = useMascotChannels((s) => s.set);
639
+ const recordRef = useRef(null);
640
+ useEffect(() => {
641
+ if (!enabled) return;
642
+ let cancelled = false;
643
+ const init = async () => {
644
+ recordRef.current = await get(KEY$1) ?? {
645
+ cumulativeMs: 0,
646
+ interactions: 0,
647
+ lastSeenAt: Date.now()
648
+ };
649
+ if (!cancelled) setCh("affinity", Math.min(1, recordRef.current.interactions / 1e3));
650
+ };
651
+ init().catch(() => void 0);
652
+ const bumpInteraction = () => {
653
+ const r = recordRef.current;
654
+ if (!r) return;
655
+ r.interactions += 1;
656
+ r.lastSeenAt = Date.now();
657
+ setCh("affinity", Math.min(1, r.interactions / 1e3));
658
+ };
659
+ const flush = () => {
660
+ const r = recordRef.current;
661
+ if (!r) return;
662
+ set(KEY$1, r).catch(() => void 0);
663
+ };
664
+ globalThis.addEventListener("pointerdown", bumpInteraction);
665
+ globalThis.addEventListener("keydown", bumpInteraction);
666
+ const id = globalThis.setInterval(flush, 5e3);
667
+ globalThis.addEventListener("beforeunload", flush);
668
+ return () => {
669
+ cancelled = true;
670
+ globalThis.removeEventListener("pointerdown", bumpInteraction);
671
+ globalThis.removeEventListener("keydown", bumpInteraction);
672
+ globalThis.removeEventListener("beforeunload", flush);
673
+ globalThis.clearInterval(id);
674
+ flush();
675
+ };
676
+ }, [enabled, setCh]);
677
+ return {
678
+ recordRef,
679
+ tierFor
680
+ };
681
+ };
682
+ //#endregion
683
+ //#region src/use-keyboard-channel.ts
684
+ const useKeyboardChannel = ({ enabled = true } = {}) => {
685
+ const bump = useMascotChannels((s) => s.bump);
686
+ const setCh = useMascotChannels((s) => s.set);
687
+ const stateRef = useRef({
688
+ capsLockOn: false,
689
+ composerFocused: false,
690
+ keyTimestamps: []
691
+ });
692
+ useEffect(() => {
693
+ if (!enabled) return;
694
+ const st = stateRef.current;
695
+ const onKey = (e) => {
696
+ const now = e.timeStamp;
697
+ st.keyTimestamps.push(now);
698
+ while (st.keyTimestamps[0] !== void 0 && now - st.keyTimestamps[0] > 5e3) st.keyTimestamps.shift();
699
+ const wpm = st.keyTimestamps.length / 5 * 12;
700
+ setCh("energy", Math.min(1, .4 + wpm / 200));
701
+ bump("attention", .04, 1);
702
+ if (e.key === "Enter") {
703
+ bump("cheer", .5, 1);
704
+ bump("joy", .3, 1);
705
+ } else if (e.key === "!") bump("excitement", .25, 1);
706
+ else if (e.key === "?") bump("curiosity", .2, 1);
707
+ const capsOn = e.getModifierState("CapsLock");
708
+ if (capsOn !== st.capsLockOn) {
709
+ st.capsLockOn = capsOn;
710
+ if (capsOn) bump("annoyance", .15, 1);
711
+ }
712
+ };
713
+ const onFocus = (e) => {
714
+ const t = e.target;
715
+ const focused = t instanceof HTMLTextAreaElement || t instanceof HTMLInputElement;
716
+ st.composerFocused = focused;
717
+ if (focused) bump("attention", .3, 1);
718
+ };
719
+ globalThis.addEventListener("keydown", onKey);
720
+ globalThis.addEventListener("focusin", onFocus);
721
+ globalThis.addEventListener("focusout", onFocus);
722
+ return () => {
723
+ globalThis.removeEventListener("keydown", onKey);
724
+ globalThis.removeEventListener("focusin", onFocus);
725
+ globalThis.removeEventListener("focusout", onFocus);
726
+ };
727
+ }, [
728
+ bump,
729
+ enabled,
730
+ setCh
731
+ ]);
732
+ };
733
+ //#endregion
734
+ //#region src/use-mascot-dna.ts
735
+ /** biome-ignore-all lint/suspicious/noAssignInExpressions: mulberry32 PRNG step */
736
+ /** biome-ignore-all lint/style/noParameterAssign: mulberry32 advances local copy */
737
+ /** biome-ignore-all lint/suspicious/noBitwiseOperators: mulberry32 requires xor/shift */
738
+ const mulberry32 = (a) => () => {
739
+ let t = Math.trunc(a += 1831565813);
740
+ t = Math.imul(t ^ t >>> 15, t | 1);
741
+ t ^= t + Math.imul(t ^ t >>> 7, t | 61);
742
+ return Math.trunc(t ^ t >>> 14) / 4294967296;
743
+ };
744
+ const generateSeed = () => {
745
+ const buf = new Uint32Array(1);
746
+ globalThis.crypto.getRandomValues(buf);
747
+ return buf[0] ?? Date.now();
748
+ };
749
+ const paramsFromSeed = (seed) => {
750
+ const rng = mulberry32(seed);
751
+ return {
752
+ baseHue: 240 + (rng() * 60 - 30),
753
+ bodyAspectRatio: 1 + (rng() * .2 - .1),
754
+ driftPersonality: rng(),
755
+ eyeSize: 1 + (rng() * .3 - .15),
756
+ eyeSpacing: .22 * (.9 + rng() * .2),
757
+ pulseCadence: .9 + rng() * .3,
758
+ voiceFormantOffset: rng() * 200 - 100
759
+ };
760
+ };
761
+ const KEY = "va.mascot.v1.dna.self";
762
+ const useMascotDna = () => {
763
+ const [dna, setDna] = useState(null);
764
+ useEffect(() => {
765
+ let cancelled = false;
766
+ const loadOrCreate = async () => {
767
+ const seedOverride = new URL(globalThis.location.href).searchParams.get("seed");
768
+ if (seedOverride !== null) {
769
+ const seed = Number.parseInt(seedOverride, 10);
770
+ if (!cancelled) setDna({
771
+ createdAt: Date.now(),
772
+ params: paramsFromSeed(seed),
773
+ seed
774
+ });
775
+ return;
776
+ }
777
+ const existing = await get(KEY);
778
+ if (existing) {
779
+ if (!cancelled) setDna(existing);
780
+ return;
781
+ }
782
+ const seed = generateSeed();
783
+ const rec = {
784
+ createdAt: Date.now(),
785
+ params: paramsFromSeed(seed),
786
+ seed
787
+ };
788
+ await set(KEY, rec);
789
+ if (!cancelled) setDna(rec);
790
+ };
791
+ loadOrCreate().catch(() => void 0);
792
+ return () => {
793
+ cancelled = true;
794
+ };
795
+ }, []);
796
+ return dna;
797
+ };
798
+ //#endregion
799
+ //#region src/use-needs-channel.ts
800
+ const useNeedsChannel = ({ enabled = true } = {}) => {
801
+ const set = useMascotChannels((s) => s.set);
802
+ const bump = useMascotChannels((s) => s.bump);
803
+ const lastInputAtRef = useRef(0);
804
+ useEffect(() => {
805
+ lastInputAtRef.current = Date.now();
806
+ }, []);
807
+ useEffect(() => {
808
+ if (!enabled) return;
809
+ const onInput = () => {
810
+ lastInputAtRef.current = Date.now();
811
+ };
812
+ globalThis.addEventListener("pointerdown", onInput);
813
+ globalThis.addEventListener("pointermove", onInput, { passive: true });
814
+ globalThis.addEventListener("keydown", onInput);
815
+ const id = globalThis.setInterval(() => {
816
+ const idleS = (Date.now() - lastInputAtRef.current) / 1e3;
817
+ const h = (/* @__PURE__ */ new Date()).getHours();
818
+ const energyCurve = .4 + .5 * Math.sin((h - 7) / 24 * Math.PI * 2);
819
+ set("energy", Math.max(0, Math.min(1, energyCurve)));
820
+ if (idleS > 60) bump("boredom", .02, 1);
821
+ if (idleS > 10) bump("loneliness", .01, 1);
822
+ if (h < 5 || h > 23) bump("fatigue", .02, 1);
823
+ }, 5e3);
824
+ return () => {
825
+ globalThis.removeEventListener("pointerdown", onInput);
826
+ globalThis.removeEventListener("pointermove", onInput);
827
+ globalThis.removeEventListener("keydown", onInput);
828
+ globalThis.clearInterval(id);
829
+ };
830
+ }, [
831
+ bump,
832
+ enabled,
833
+ set
834
+ ]);
835
+ };
836
+ //#endregion
837
+ //#region src/use-pointer-channel.ts
838
+ const smoothstep = (a, b, x) => {
839
+ const t = Math.max(0, Math.min(1, (x - a) / (b - a)));
840
+ return t * t * (3 - 2 * t);
841
+ };
842
+ const usePointerChannel = ({ enabled = true } = {}) => {
843
+ const bump = useMascotChannels((s) => s.bump);
844
+ const setCh = useMascotChannels((s) => s.set);
845
+ const stateRef = useRef({
846
+ insideCanvas: false,
847
+ lastClickT: 0,
848
+ lastMoveT: 0,
849
+ lastT: 0,
850
+ lastX: 0,
851
+ lastY: 0,
852
+ mascotCenterPx: {
853
+ x: 0,
854
+ y: 0
855
+ },
856
+ movePx: 0,
857
+ pressActive: false,
858
+ pressStart: null
859
+ });
860
+ useEffect(() => {
861
+ if (!enabled) return;
862
+ const st = stateRef.current;
863
+ const onEnter = () => {
864
+ st.insideCanvas = true;
865
+ bump("curiosity", .3, 1);
866
+ bump("attention", .4, 1);
867
+ bump("affection", .04, 1);
868
+ bump("joy", .1, 1);
869
+ };
870
+ const onLeave = () => {
871
+ st.insideCanvas = false;
872
+ setCh("magnetism", 0);
873
+ bump("loneliness", .25, 1);
874
+ };
875
+ const onMove = (e) => {
876
+ const t = e.timeStamp;
877
+ const dx = e.clientX - st.lastX;
878
+ const dy = e.clientY - st.lastY;
879
+ const dt = Math.max(.001, (t - st.lastT) / 1e3);
880
+ const speed = Math.hypot(dx, dy) / dt;
881
+ const jumpPx = Math.hypot(dx, dy);
882
+ st.lastX = e.clientX;
883
+ st.lastY = e.clientY;
884
+ st.lastT = t;
885
+ st.lastMoveT = t;
886
+ const closeness = 1 - smoothstep(220, 900, Math.hypot(e.clientX - st.mascotCenterPx.x, e.clientY - st.mascotCenterPx.y));
887
+ setCh("magnetism", closeness);
888
+ if (jumpPx >= 600 && st.lastMoveT - st.lastT < 100) {
889
+ bump("attention", .6, 1);
890
+ bump("curiosity", .3, 1);
891
+ bump("worry", .1, 1);
892
+ bump("dizzy", .05, 1);
893
+ }
894
+ if (st.pressActive) {
895
+ bump("joy", .05, 1);
896
+ bump("cheer", .04, 1);
897
+ bump("affection", .02, 1);
898
+ bump("attention", .12, 1);
899
+ bump("magnetism", .06, 1);
900
+ } else if (speed >= 1500) {
901
+ bump("worry", .08, 1);
902
+ bump("dizzy", .05, 1);
903
+ bump("attention", .1, 1);
904
+ } else if (speed >= 800) {
905
+ bump("excitement", .05, 1);
906
+ bump("attention", .06, 1);
907
+ } else if (speed >= 30 && speed <= 180 && closeness > .5) {
908
+ bump("attention", .06, 1);
909
+ bump("magnetism", .04, 1);
910
+ bump("joy", .02, 1);
911
+ } else if (speed <= 50 && closeness > .4) {
912
+ bump("joy", .03, 1);
913
+ bump("cheer", .02, 1);
914
+ bump("affection", .01, 1);
915
+ } else if (closeness > .15) bump("curiosity", .02, 1);
916
+ if (st.pressActive && st.pressStart) st.movePx += jumpPx;
917
+ };
918
+ const onDown = (e) => {
919
+ st.pressActive = true;
920
+ st.movePx = 0;
921
+ st.pressStart = {
922
+ t: e.timeStamp,
923
+ x: e.clientX,
924
+ y: e.clientY
925
+ };
926
+ };
927
+ const onUp = (e) => {
928
+ if (!(st.pressActive && st.pressStart)) {
929
+ st.pressActive = false;
930
+ return;
931
+ }
932
+ if (st.movePx > 4) {
933
+ bump("cheer", .4, 1);
934
+ bump("joy", .3, 1);
935
+ bump("affection", .06, 1);
936
+ } else {
937
+ const streakDt = e.timeStamp - st.lastClickT;
938
+ const streakMult = streakDt < 1e3 ? Math.min(1.6, 1 + .2 * Math.max(0, 5 - streakDt / 200)) : 1;
939
+ const dbl = streakDt < 350;
940
+ bump("joy", (dbl ? .9 : .7) * streakMult, 1);
941
+ bump("cheer", (dbl ? .7 : .5) * streakMult, 1);
942
+ bump("excitement", (dbl ? .6 : .4) * streakMult, 1);
943
+ bump("heat", .3, 1);
944
+ bump("affection", .05, 1);
945
+ }
946
+ st.lastClickT = e.timeStamp;
947
+ st.pressActive = false;
948
+ st.pressStart = null;
949
+ };
950
+ globalThis.addEventListener("pointerenter", onEnter);
951
+ globalThis.addEventListener("pointerleave", onLeave);
952
+ globalThis.addEventListener("pointermove", onMove, { passive: true });
953
+ globalThis.addEventListener("pointerdown", onDown);
954
+ globalThis.addEventListener("pointerup", onUp);
955
+ globalThis.addEventListener("pointercancel", onUp);
956
+ return () => {
957
+ globalThis.removeEventListener("pointerenter", onEnter);
958
+ globalThis.removeEventListener("pointerleave", onLeave);
959
+ globalThis.removeEventListener("pointermove", onMove);
960
+ globalThis.removeEventListener("pointerdown", onDown);
961
+ globalThis.removeEventListener("pointerup", onUp);
962
+ globalThis.removeEventListener("pointercancel", onUp);
963
+ };
964
+ }, [
965
+ bump,
966
+ enabled,
967
+ setCh
968
+ ]);
969
+ const setMascotCenter = useCallback((x, y) => {
970
+ stateRef.current.mascotCenterPx = {
971
+ x,
972
+ y
973
+ };
974
+ }, []);
975
+ return {
976
+ isInside: useCallback(() => stateRef.current.insideCanvas, []),
977
+ setMascotCenter
978
+ };
979
+ };
980
+ //#endregion
981
+ //#region src/use-webgl-context-loss.ts
982
+ const useWebglContextLoss = ({ canvasRef }) => {
983
+ const [lost, setLost] = useState(false);
984
+ useEffect(() => {
985
+ const c = canvasRef.current;
986
+ if (!c) return;
987
+ const onLost = (e) => {
988
+ e.preventDefault();
989
+ setLost(true);
990
+ };
991
+ const onRestored = () => setLost(false);
992
+ c.addEventListener("webglcontextlost", onLost);
993
+ c.addEventListener("webglcontextrestored", onRestored);
994
+ return () => {
995
+ c.removeEventListener("webglcontextlost", onLost);
996
+ c.removeEventListener("webglcontextrestored", onRestored);
997
+ };
998
+ }, [canvasRef]);
999
+ return { lost };
1000
+ };
1001
+ //#endregion
1002
+ //#region src/mascot-canvas.tsx
1003
+ /** biome-ignore-all lint/nursery/noUnknownAttribute: R3F intrinsics */
1004
+ /** biome-ignore-all lint/performance/noNamespaceImport: three convention */
1005
+ var mascot_canvas_exports = /* @__PURE__ */ __exportAll({ MascotCanvas: () => MascotCanvas });
1006
+ const CAMERA_PROPS = {
1007
+ fov: 28,
1008
+ near: CAMERA_NEAR,
1009
+ position: [
1010
+ 0,
1011
+ 0,
1012
+ 12
1013
+ ]
1014
+ };
1015
+ const DPR_RANGE = [1, 2];
1016
+ const GL_PROPS = {
1017
+ alpha: true,
1018
+ antialias: true,
1019
+ powerPreference: "high-performance"
1020
+ };
1021
+ const LIGHT_POS = [
1022
+ 1,
1023
+ 2,
1024
+ 1
1025
+ ];
1026
+ const POINT_LIGHT_POS = [
1027
+ 2,
1028
+ 3,
1029
+ 4
1030
+ ];
1031
+ const Scene = ({ bodyCenterRef, pointer3DRef, setMascotCenterPx }) => {
1032
+ const decay = useMascotChannels((s) => s.decay);
1033
+ const tick = useMascotChannels((s) => s.tick);
1034
+ const setBodyScreenPx = useMascotPose((s) => s.setBodyScreenPx);
1035
+ const { camera, gl, size } = useThree();
1036
+ useEffect(() => {
1037
+ if (!(camera instanceof THREE.PerspectiveCamera)) return;
1038
+ camera.fov = 28;
1039
+ camera.near = CAMERA_NEAR;
1040
+ camera.far = 20;
1041
+ camera.position.set(0, 0, 12);
1042
+ camera.updateProjectionMatrix();
1043
+ }, [camera]);
1044
+ useEffect(() => {
1045
+ gl.toneMapping = THREE.ACESFilmicToneMapping;
1046
+ gl.outputColorSpace = THREE.SRGBColorSpace;
1047
+ }, [gl]);
1048
+ const raycasterRef = useRef(new THREE.Raycaster());
1049
+ const ndcRef = useRef(new THREE.Vector2());
1050
+ const pointer3DTargetRef = useRef(new THREE.Vector3());
1051
+ useEffect(() => {
1052
+ const onMove = (e) => {
1053
+ const rect = gl.domElement.getBoundingClientRect();
1054
+ ndcRef.current.x = (e.clientX - rect.left) / rect.width * 2 - 1;
1055
+ ndcRef.current.y = -((e.clientY - rect.top) / rect.height) * 2 + 1;
1056
+ raycasterRef.current.setFromCamera(ndcRef.current, camera);
1057
+ const dir = raycasterRef.current.ray.direction;
1058
+ const denom = dir.z === 0 ? 1e-4 : dir.z;
1059
+ const t = -raycasterRef.current.ray.origin.z / denom;
1060
+ pointer3DTargetRef.current.copy(raycasterRef.current.ray.origin).addScaledVector(dir, t);
1061
+ };
1062
+ globalThis.addEventListener("pointermove", onMove);
1063
+ return () => globalThis.removeEventListener("pointermove", onMove);
1064
+ }, [camera, gl]);
1065
+ const domEl = gl.domElement;
1066
+ useEffect(() => {
1067
+ const update = () => {
1068
+ const rect = domEl.getBoundingClientRect();
1069
+ setMascotCenterPx(rect.left + rect.width / 2, rect.top + rect.height / 2);
1070
+ };
1071
+ update();
1072
+ const ro = new ResizeObserver(update);
1073
+ ro.observe(domEl);
1074
+ globalThis.addEventListener("scroll", update, { passive: true });
1075
+ return () => {
1076
+ ro.disconnect();
1077
+ globalThis.removeEventListener("scroll", update);
1078
+ };
1079
+ }, [domEl, setMascotCenterPx]);
1080
+ const projTmpRef = useRef(new THREE.Vector3());
1081
+ const onCenter = useCallback((v) => {
1082
+ bodyCenterRef.current.copy(v);
1083
+ const tmp = projTmpRef.current;
1084
+ tmp.copy(v).project(camera);
1085
+ setBodyScreenPx((tmp.x * .5 + .5) * size.width, (-tmp.y * .5 + .5) * size.height);
1086
+ }, [
1087
+ bodyCenterRef,
1088
+ camera,
1089
+ setBodyScreenPx,
1090
+ size.height,
1091
+ size.width
1092
+ ]);
1093
+ useEffect(() => {
1094
+ let raf = 0;
1095
+ let last = performance.now();
1096
+ const loop = () => {
1097
+ const now = performance.now();
1098
+ const dt = Math.min(.1, (now - last) / 1e3);
1099
+ last = now;
1100
+ decay(dt);
1101
+ tick(dt);
1102
+ const target = pointer3DTargetRef.current;
1103
+ const { current } = pointer3DRef;
1104
+ const alpha = 1 - Math.exp(-12 * dt);
1105
+ current.x += (target.x - current.x) * alpha;
1106
+ current.y += (target.y - current.y) * alpha;
1107
+ current.z += (target.z - current.z) * alpha;
1108
+ raf = requestAnimationFrame(loop);
1109
+ };
1110
+ raf = requestAnimationFrame(loop);
1111
+ return () => cancelAnimationFrame(raf);
1112
+ }, [
1113
+ decay,
1114
+ tick,
1115
+ pointer3DRef
1116
+ ]);
1117
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1118
+ /* @__PURE__ */ jsx("ambientLight", {
1119
+ color: "#ffffff",
1120
+ intensity: .85
1121
+ }),
1122
+ /* @__PURE__ */ jsx("directionalLight", {
1123
+ color: "#ffffff",
1124
+ intensity: 1.2,
1125
+ position: LIGHT_POS
1126
+ }),
1127
+ /* @__PURE__ */ jsx("pointLight", {
1128
+ color: "#ffffff",
1129
+ intensity: .6,
1130
+ position: POINT_LIGHT_POS
1131
+ }),
1132
+ /* @__PURE__ */ jsx(MascotBody, {
1133
+ onCenter,
1134
+ pointer3D: pointer3DRef.current
1135
+ }),
1136
+ /* @__PURE__ */ jsx(MascotEyes, {
1137
+ bodyCenter: bodyCenterRef.current,
1138
+ pointer3D: pointer3DRef.current
1139
+ })
1140
+ ] });
1141
+ };
1142
+ const MascotCanvas = () => {
1143
+ useMascotDna();
1144
+ const pointer3DRef = useRef(new THREE.Vector3());
1145
+ const bodyCenterRef = useRef(new THREE.Vector3());
1146
+ const canvasRef = useRef(null);
1147
+ const reducedMotion = useReducedMotion();
1148
+ const setSmoothK = useMascotChannels((s) => s.setSmoothK);
1149
+ useEffect(() => {
1150
+ setSmoothK(reducedMotion ? 3 : 7);
1151
+ }, [reducedMotion, setSmoothK]);
1152
+ const { setMascotCenter } = usePointerChannel({ enabled: !reducedMotion });
1153
+ useKeyboardChannel({ enabled: true });
1154
+ useNeedsChannel({ enabled: true });
1155
+ useHistoryChannel({ enabled: true });
1156
+ const { lost } = useWebglContextLoss({ canvasRef });
1157
+ const [glReady, setGlReady] = useState(false);
1158
+ useEffect(() => {
1159
+ let search;
1160
+ try {
1161
+ ({search} = globalThis.location);
1162
+ } catch {
1163
+ return;
1164
+ }
1165
+ if (!search.includes("test=1")) return;
1166
+ const probe = globalThis;
1167
+ probe.__mascot = {
1168
+ body: bodyCenterRef.current,
1169
+ channels: useMascotChannels.getState,
1170
+ lost,
1171
+ pointer3D: pointer3DRef.current
1172
+ };
1173
+ }, [lost]);
1174
+ return /* @__PURE__ */ jsxs("div", {
1175
+ className: "pointer-events-none relative size-full [&_canvas]:pointer-events-none [&>div]:pointer-events-none",
1176
+ children: [/* @__PURE__ */ jsx(Canvas, {
1177
+ camera: CAMERA_PROPS,
1178
+ dpr: DPR_RANGE,
1179
+ gl: GL_PROPS,
1180
+ onCreated: ({ gl }) => {
1181
+ canvasRef.current = gl.domElement;
1182
+ gl.domElement.style.pointerEvents = "none";
1183
+ setGlReady(true);
1184
+ },
1185
+ children: glReady ? /* @__PURE__ */ jsx(Scene, {
1186
+ bodyCenterRef,
1187
+ pointer3DRef,
1188
+ setMascotCenterPx: setMascotCenter
1189
+ }) : null
1190
+ }), lost ? /* @__PURE__ */ jsx(SvgFallback, {}) : null]
1191
+ });
1192
+ };
1193
+ //#endregion
1194
+ //#region src/mascot.tsx
1195
+ const MascotCanvas$1 = lazy(async () => {
1196
+ return { default: (await Promise.resolve().then(() => mascot_canvas_exports)).MascotCanvas };
1197
+ });
1198
+ const MascotBubbleFollower = ({ children, offsetY = -110 }) => {
1199
+ const px = useMascotPose((s) => s.bodyScreenPx);
1200
+ return /* @__PURE__ */ jsx("div", {
1201
+ className: "pointer-events-none fixed z-20 hidden md:block",
1202
+ style: useMemo(() => ({
1203
+ left: 0,
1204
+ top: 0,
1205
+ transform: `translate3d(${px.x}px, ${px.y + offsetY}px, 0) translate(-50%, -100%)`
1206
+ }), [
1207
+ px.x,
1208
+ px.y,
1209
+ offsetY
1210
+ ]),
1211
+ children: /* @__PURE__ */ jsx("div", {
1212
+ className: "pointer-events-auto",
1213
+ children
1214
+ })
1215
+ });
1216
+ };
1217
+ const MascotOverlay = ({ anchorRef }) => {
1218
+ const setHomeWorld = useMascotPose((s) => s.setHomeWorld);
1219
+ const overlayRef = useRef(null);
1220
+ useEffect(() => {
1221
+ const root = overlayRef.current;
1222
+ if (!root) return;
1223
+ const apply = () => {
1224
+ const all = root.querySelectorAll("*");
1225
+ for (const el of all) el.style.pointerEvents = "none";
1226
+ };
1227
+ apply();
1228
+ const mo = new MutationObserver(apply);
1229
+ mo.observe(root, {
1230
+ childList: true,
1231
+ subtree: true
1232
+ });
1233
+ return () => mo.disconnect();
1234
+ }, []);
1235
+ useEffect(() => {
1236
+ const update = () => {
1237
+ const el = anchorRef.current;
1238
+ if (!el) return;
1239
+ const rect = el.getBoundingClientRect();
1240
+ const cx = rect.left + rect.width / 2;
1241
+ const cy = rect.top + rect.height / 2;
1242
+ const vw = globalThis.innerWidth;
1243
+ const vh = globalThis.innerHeight;
1244
+ const fov = 28 * Math.PI / 180;
1245
+ const pxToWorld = 24 * Math.tan(fov / 2) / vh;
1246
+ setHomeWorld((cx - vw / 2) * pxToWorld, -(cy - vh / 2) * pxToWorld, 0);
1247
+ };
1248
+ update();
1249
+ const ro = new ResizeObserver(update);
1250
+ const el = anchorRef.current;
1251
+ if (el) ro.observe(el);
1252
+ globalThis.addEventListener("scroll", update, { passive: true });
1253
+ globalThis.addEventListener("resize", update);
1254
+ return () => {
1255
+ ro.disconnect();
1256
+ globalThis.removeEventListener("scroll", update);
1257
+ globalThis.removeEventListener("resize", update);
1258
+ };
1259
+ }, [anchorRef, setHomeWorld]);
1260
+ return /* @__PURE__ */ jsx("div", {
1261
+ className: "pointer-events-none fixed inset-0 z-10",
1262
+ ref: overlayRef,
1263
+ children: /* @__PURE__ */ jsx(Suspense, {
1264
+ fallback: null,
1265
+ children: /* @__PURE__ */ jsx(MascotCanvas$1, {})
1266
+ })
1267
+ });
1268
+ };
1269
+ const Mascot = ({ bubble, size = 240 }) => {
1270
+ const anchorRef = useRef(null);
1271
+ return /* @__PURE__ */ jsxs(Fragment, { children: [
1272
+ /* @__PURE__ */ jsx("div", {
1273
+ ref: anchorRef,
1274
+ style: useMemo(() => ({
1275
+ height: size,
1276
+ width: size
1277
+ }), [size])
1278
+ }),
1279
+ /* @__PURE__ */ jsx(MascotOverlay, { anchorRef }),
1280
+ bubble === void 0 ? null : /* @__PURE__ */ jsx(MascotBubbleFollower, { children: bubble })
1281
+ ] });
1282
+ };
1283
+ //#endregion
1284
+ export { Mascot, MascotBubbleFollower, MascotCanvas, useMascotChannels, useMascotPose };