@divinci-ai/robot-avatar 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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/blend.d.ts +8 -0
- package/dist/blend.d.ts.map +1 -0
- package/dist/blend.js +17 -0
- package/dist/blend.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/render-gate.d.ts +32 -0
- package/dist/render-gate.d.ts.map +1 -0
- package/dist/render-gate.js +72 -0
- package/dist/render-gate.js.map +1 -0
- package/dist/scene/AnimatedHeart.d.ts +18 -0
- package/dist/scene/AnimatedHeart.d.ts.map +1 -0
- package/dist/scene/AnimatedHeart.js +63 -0
- package/dist/scene/AnimatedHeart.js.map +1 -0
- package/dist/scene/LogoRobot.d.ts +24 -0
- package/dist/scene/LogoRobot.d.ts.map +1 -0
- package/dist/scene/LogoRobot.js +285 -0
- package/dist/scene/LogoRobot.js.map +1 -0
- package/dist/scene/RobotScene.d.ts +43 -0
- package/dist/scene/RobotScene.d.ts.map +1 -0
- package/dist/scene/RobotScene.js +43 -0
- package/dist/scene/RobotScene.js.map +1 -0
- package/dist/scene/index.d.ts +16 -0
- package/dist/scene/index.d.ts.map +1 -0
- package/dist/scene/index.js +15 -0
- package/dist/scene/index.js.map +1 -0
- package/dist/scene/useGroupMotion.d.ts +12 -0
- package/dist/scene/useGroupMotion.d.ts.map +1 -0
- package/dist/scene/useGroupMotion.js +47 -0
- package/dist/scene/useGroupMotion.js.map +1 -0
- package/dist/scene/useRobotSignals.d.ts +28 -0
- package/dist/scene/useRobotSignals.d.ts.map +1 -0
- package/dist/scene/useRobotSignals.js +101 -0
- package/dist/scene/useRobotSignals.js.map +1 -0
- package/dist/types.d.ts +15 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +8 -0
- package/dist/types.js.map +1 -0
- package/package.json +46 -0
- package/src/blend.ts +16 -0
- package/src/index.ts +14 -0
- package/src/render-gate.ts +102 -0
- package/src/scene/AnimatedHeart.tsx +95 -0
- package/src/scene/LogoRobot.tsx +361 -0
- package/src/scene/RobotScene.tsx +102 -0
- package/src/scene/index.ts +16 -0
- package/src/scene/useGroupMotion.ts +61 -0
- package/src/scene/useRobotSignals.ts +126 -0
- package/src/types.ts +16 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared interaction signals for the rigged robot. One window-listener setup,
|
|
3
|
+
* one mutable ref every body part reads from. Provides:
|
|
4
|
+
* - lookX/lookY: where his attention should go — the cursor when it's moving,
|
|
5
|
+
* the focused input when you're typing, otherwise the user (screen center).
|
|
6
|
+
* - keystroke reactions + idle fidgets, each with a fresh random seed/style so
|
|
7
|
+
* the motion never loops identically.
|
|
8
|
+
*/
|
|
9
|
+
import { useEffect, useRef } from "react";
|
|
10
|
+
import { useFrame } from "@react-three/fiber";
|
|
11
|
+
export const BASE_BODY_YAW = 0.08;
|
|
12
|
+
export function useRobotSignals(speakerTarget) {
|
|
13
|
+
const sig = useRef({
|
|
14
|
+
lookX: 0,
|
|
15
|
+
lookY: 0,
|
|
16
|
+
reactUntil: 0,
|
|
17
|
+
reactSeed: 0,
|
|
18
|
+
fidgetUntil: 0,
|
|
19
|
+
fidgetSeed: 0,
|
|
20
|
+
reduced: false,
|
|
21
|
+
});
|
|
22
|
+
const mouse = useRef({ x: 0, y: 0, at: -10 });
|
|
23
|
+
const focus = useRef({ x: 0, y: 0, active: false });
|
|
24
|
+
const nextFidget = useRef(5);
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
const norm = (px, py) => [
|
|
27
|
+
(px / window.innerWidth) * 2 - 1,
|
|
28
|
+
(py / window.innerHeight) * 2 - 1,
|
|
29
|
+
];
|
|
30
|
+
const onMove = (e) => {
|
|
31
|
+
const [x, y] = norm(e.clientX, e.clientY);
|
|
32
|
+
mouse.current = { x, y, at: performance.now() / 1000 };
|
|
33
|
+
};
|
|
34
|
+
const onKey = () => {
|
|
35
|
+
sig.current.reactUntil = performance.now() / 1000 + 0.5 + Math.random() * 0.3;
|
|
36
|
+
sig.current.reactSeed = Math.random();
|
|
37
|
+
};
|
|
38
|
+
const onFocusIn = (e) => {
|
|
39
|
+
const el = e.target;
|
|
40
|
+
if (el && typeof el.getBoundingClientRect === "function") {
|
|
41
|
+
const tag = el.tagName;
|
|
42
|
+
if (tag === "INPUT" || tag === "TEXTAREA" || el.isContentEditable) {
|
|
43
|
+
const r = el.getBoundingClientRect();
|
|
44
|
+
const [x, y] = norm(r.left + r.width / 2, r.top + r.height / 2);
|
|
45
|
+
focus.current = { x, y, active: true };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const onFocusOut = () => {
|
|
50
|
+
focus.current.active = false;
|
|
51
|
+
};
|
|
52
|
+
// ── prefers-reduced-motion (live) ────────────────────────────────
|
|
53
|
+
const mq = window.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
54
|
+
sig.current.reduced = mq?.matches ?? false;
|
|
55
|
+
const onReduceChange = (e) => {
|
|
56
|
+
sig.current.reduced = e.matches;
|
|
57
|
+
};
|
|
58
|
+
mq?.addEventListener?.("change", onReduceChange);
|
|
59
|
+
window.addEventListener("mousemove", onMove, { passive: true });
|
|
60
|
+
window.addEventListener("keydown", onKey);
|
|
61
|
+
window.addEventListener("focusin", onFocusIn);
|
|
62
|
+
window.addEventListener("focusout", onFocusOut);
|
|
63
|
+
return () => {
|
|
64
|
+
mq?.removeEventListener?.("change", onReduceChange);
|
|
65
|
+
window.removeEventListener("mousemove", onMove);
|
|
66
|
+
window.removeEventListener("keydown", onKey);
|
|
67
|
+
window.removeEventListener("focusin", onFocusIn);
|
|
68
|
+
window.removeEventListener("focusout", onFocusOut);
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
useFrame(() => {
|
|
72
|
+
const t = performance.now() / 1000;
|
|
73
|
+
const s = sig.current;
|
|
74
|
+
let tx = 0;
|
|
75
|
+
let ty = 0;
|
|
76
|
+
// The injected speaker target loses to a moving cursor (the user always
|
|
77
|
+
// wins the robot's attention) and is ignored under reduced motion.
|
|
78
|
+
const sp = s.reduced || !speakerTarget ? null : speakerTarget();
|
|
79
|
+
if (t - mouse.current.at < 2.5) {
|
|
80
|
+
tx = mouse.current.x;
|
|
81
|
+
ty = mouse.current.y;
|
|
82
|
+
}
|
|
83
|
+
else if (sp) {
|
|
84
|
+
tx = sp[0];
|
|
85
|
+
ty = sp[1];
|
|
86
|
+
}
|
|
87
|
+
else if (focus.current.active) {
|
|
88
|
+
tx = focus.current.x;
|
|
89
|
+
ty = focus.current.y;
|
|
90
|
+
}
|
|
91
|
+
s.lookX += (tx - s.lookX) * 0.08;
|
|
92
|
+
s.lookY += (ty - s.lookY) * 0.08;
|
|
93
|
+
if (!s.reduced && t > nextFidget.current && t > s.reactUntil) {
|
|
94
|
+
s.fidgetUntil = t + 1.4 + Math.random() * 0.8;
|
|
95
|
+
s.fidgetSeed = Math.random();
|
|
96
|
+
nextFidget.current = t + 6 + Math.random() * 8;
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return sig;
|
|
100
|
+
}
|
|
101
|
+
//# sourceMappingURL=useRobotSignals.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useRobotSignals.js","sourceRoot":"","sources":["../../src/scene/useRobotSignals.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the RobotHero component.
|
|
3
|
+
*
|
|
4
|
+
* Kept in a standalone module (no React/three imports) so the light
|
|
5
|
+
* wrapper can import them without pulling three.js into the main bundle.
|
|
6
|
+
*/
|
|
7
|
+
export type AvatarState = "idle" | "thinking" | "happy" | "wave";
|
|
8
|
+
export interface RobotAvatarColors {
|
|
9
|
+
body: string;
|
|
10
|
+
trim: string;
|
|
11
|
+
heart: string;
|
|
12
|
+
eye: string;
|
|
13
|
+
shadow: string;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,UAAU,GAAG,OAAO,GAAG,MAAM,CAAC;AAEjE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;CAChB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
|
package/package.json
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@divinci-ai/robot-avatar",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The Divinci robot mascot — procedural react-three-fiber avatar shared by the SDK docs hero, the web app, the Divinci Agent panel, and the Chrome extension.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "dist/index.js",
|
|
7
|
+
"types": "dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./scene": {
|
|
14
|
+
"types": "./dist/scene/index.d.ts",
|
|
15
|
+
"default": "./dist/scene/index.js"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src",
|
|
21
|
+
"README.md"
|
|
22
|
+
],
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"@react-three/drei": ">=9.114.0",
|
|
25
|
+
"@react-three/fiber": ">=8.17.0",
|
|
26
|
+
"react": ">=18.3.0",
|
|
27
|
+
"three": ">=0.169.0"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@react-three/drei": "^10.7.7",
|
|
31
|
+
"@react-three/fiber": "^9.6.1",
|
|
32
|
+
"@types/react": "^19.2.17",
|
|
33
|
+
"@types/three": "^0.185.0",
|
|
34
|
+
"react": "^19.2.7",
|
|
35
|
+
"three": "^0.185.0",
|
|
36
|
+
"typescript": "^5.5.0",
|
|
37
|
+
"vitest": "^3.2.4"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"scripts": {
|
|
43
|
+
"build": "tsc",
|
|
44
|
+
"test": "vitest run --passWithNoTests"
|
|
45
|
+
}
|
|
46
|
+
}
|
package/src/blend.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tiny hex-color blend used by consumers to derive the robot's palette from a
|
|
3
|
+
* theme (web client) or brand constants (docs hero). Lives in the light entry
|
|
4
|
+
* so wrappers can compute colors without touching three.js.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Blend two hex colors; t=0 → a, t=1 → b. */
|
|
8
|
+
export function blendHex(a: string, b: string, t: number): string {
|
|
9
|
+
const m = a.replace("#", "").trim();
|
|
10
|
+
const n = b.replace("#", "").trim();
|
|
11
|
+
if (m.length < 6 || n.length < 6) return a;
|
|
12
|
+
const ra = [parseInt(m.slice(0, 2), 16), parseInt(m.slice(2, 4), 16), parseInt(m.slice(4, 6), 16)];
|
|
13
|
+
const rb = [parseInt(n.slice(0, 2), 16), parseInt(n.slice(2, 4), 16), parseInt(n.slice(4, 6), 16)];
|
|
14
|
+
const mix = ra.map((v, i) => Math.round(v + (rb[i] - v) * t));
|
|
15
|
+
return "#" + mix.map((v) => Math.max(0, Math.min(255, v)).toString(16).padStart(2, "0")).join("");
|
|
16
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @divinci-ai/robot-avatar — light entry (NO three.js).
|
|
3
|
+
*
|
|
4
|
+
* Import this from main bundles; lazy-import "@divinci-ai/robot-avatar/scene"
|
|
5
|
+
* for the WebGL chunk:
|
|
6
|
+
*
|
|
7
|
+
* const RobotScene = lazy(() =>
|
|
8
|
+
* import("@divinci-ai/robot-avatar/scene").then((m) => ({ default: m.RobotScene }))
|
|
9
|
+
* );
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export type { AvatarState, RobotAvatarColors } from "./types.js";
|
|
13
|
+
export { blendHex } from "./blend.js";
|
|
14
|
+
export { useRenderGate, type RenderGate, type RenderGateOptions } from "./render-gate.js";
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useRenderGate — shared "should the robot render right now?" gate.
|
|
3
|
+
*
|
|
4
|
+
* Returns `active: false` when any of these hold:
|
|
5
|
+
* - the container is scrolled out of the viewport (IntersectionObserver)
|
|
6
|
+
* - the tab is hidden (visibilitychange)
|
|
7
|
+
* - the user has been idle for `idleMs` (optional; pass 0/undefined to skip)
|
|
8
|
+
*
|
|
9
|
+
* Consumers feed `active` into RobotScene's `paused` prop, which sets the R3F
|
|
10
|
+
* frameloop to "never" — the WebGL context stays alive but stops burning
|
|
11
|
+
* GPU/battery. This generalizes the SDK docs hero's gate controller so the web
|
|
12
|
+
* app / agent panel / extension iframe get the same behavior for free.
|
|
13
|
+
*
|
|
14
|
+
* Lives in the light entry: React-only, no three.js.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { useEffect, useState } from "react";
|
|
18
|
+
|
|
19
|
+
export interface RenderGateOptions {
|
|
20
|
+
/** Pause after this many ms without pointer/key/scroll activity. 0 = never. */
|
|
21
|
+
idleMs?: number;
|
|
22
|
+
/** IntersectionObserver threshold for "visible" (default 0.05). */
|
|
23
|
+
threshold?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RenderGate<T extends HTMLElement> {
|
|
27
|
+
/**
|
|
28
|
+
* Attach to the element wrapping the canvas. A callback ref (not a ref
|
|
29
|
+
* object) so the type is identical under react 18 and 19 typings.
|
|
30
|
+
*/
|
|
31
|
+
ref: (node: T | null) => void;
|
|
32
|
+
/** True when the robot should animate. */
|
|
33
|
+
active: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function useRenderGate<T extends HTMLElement = HTMLDivElement>(
|
|
37
|
+
options: RenderGateOptions = {}
|
|
38
|
+
): RenderGate<T> {
|
|
39
|
+
const { idleMs = 0, threshold = 0.05 } = options;
|
|
40
|
+
const [node, setNode] = useState<T | null>(null);
|
|
41
|
+
const [active, setActive] = useState(true);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
let visible = true;
|
|
45
|
+
let shown = !document.hidden;
|
|
46
|
+
let idle = false;
|
|
47
|
+
let idleTimer: number | undefined;
|
|
48
|
+
|
|
49
|
+
const recompute = (): void => {
|
|
50
|
+
setActive(visible && shown && !idle);
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const bumpIdle = (): void => {
|
|
54
|
+
if (!idleMs) return;
|
|
55
|
+
if (idle) {
|
|
56
|
+
idle = false;
|
|
57
|
+
recompute();
|
|
58
|
+
}
|
|
59
|
+
window.clearTimeout(idleTimer);
|
|
60
|
+
idleTimer = window.setTimeout(() => {
|
|
61
|
+
idle = true;
|
|
62
|
+
recompute();
|
|
63
|
+
}, idleMs);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
let io: IntersectionObserver | undefined;
|
|
67
|
+
if (node && typeof IntersectionObserver !== "undefined") {
|
|
68
|
+
io = new IntersectionObserver(
|
|
69
|
+
(entries) => {
|
|
70
|
+
visible = entries.some((e) => e.isIntersecting);
|
|
71
|
+
recompute();
|
|
72
|
+
},
|
|
73
|
+
{ threshold }
|
|
74
|
+
);
|
|
75
|
+
io.observe(node);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const onVisibility = (): void => {
|
|
79
|
+
shown = !document.hidden;
|
|
80
|
+
recompute();
|
|
81
|
+
};
|
|
82
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
83
|
+
|
|
84
|
+
const activityEvents = ["pointermove", "pointerdown", "keydown", "wheel", "touchstart", "scroll"] as const;
|
|
85
|
+
if (idleMs) {
|
|
86
|
+
activityEvents.forEach((ev) => window.addEventListener(ev, bumpIdle, { passive: true }));
|
|
87
|
+
bumpIdle();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
recompute();
|
|
91
|
+
return () => {
|
|
92
|
+
io?.disconnect();
|
|
93
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
94
|
+
if (idleMs) {
|
|
95
|
+
activityEvents.forEach((ev) => window.removeEventListener(ev, bumpIdle));
|
|
96
|
+
window.clearTimeout(idleTimer);
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
}, [node, idleMs, threshold]);
|
|
100
|
+
|
|
101
|
+
return { ref: setNode, active };
|
|
102
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AnimatedHeart — the Divinci heart rendered in code (not baked into the GLB)
|
|
3
|
+
* so it can be alive: it slowly phases through brand colors, is slightly
|
|
4
|
+
* transparent, and gently pulses.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import React, { useMemo, useRef } from "react";
|
|
8
|
+
import { useFrame } from "@react-three/fiber";
|
|
9
|
+
import * as THREE from "three";
|
|
10
|
+
import type { MutableRefObject } from "react";
|
|
11
|
+
import type { RobotSignals } from "./useRobotSignals.js";
|
|
12
|
+
|
|
13
|
+
export interface AnimatedHeartProps {
|
|
14
|
+
position?: [number, number, number];
|
|
15
|
+
scale?: number;
|
|
16
|
+
cycleSeconds?: number;
|
|
17
|
+
opacity?: number;
|
|
18
|
+
stretchY?: number;
|
|
19
|
+
sig?: MutableRefObject<RobotSignals>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeHeartGeometry(): THREE.ExtrudeGeometry {
|
|
23
|
+
const s = new THREE.Shape();
|
|
24
|
+
s.moveTo(0, 0.3);
|
|
25
|
+
s.bezierCurveTo(0, 0.3, -0.2, 0, -0.5, 0);
|
|
26
|
+
s.bezierCurveTo(-0.95, 0, -0.95, 0.5, -0.95, 0.5);
|
|
27
|
+
s.bezierCurveTo(-0.95, 0.78, -0.6, 1.05, 0, 1.35);
|
|
28
|
+
s.bezierCurveTo(0.6, 1.05, 0.95, 0.78, 0.95, 0.5);
|
|
29
|
+
s.bezierCurveTo(0.95, 0.5, 0.95, 0, 0.5, 0);
|
|
30
|
+
s.bezierCurveTo(0.2, 0, 0, 0.3, 0, 0.3);
|
|
31
|
+
const geo = new THREE.ExtrudeGeometry(s, {
|
|
32
|
+
depth: 0.55,
|
|
33
|
+
bevelEnabled: true,
|
|
34
|
+
bevelThickness: 0.14,
|
|
35
|
+
bevelSize: 0.14,
|
|
36
|
+
bevelSegments: 5,
|
|
37
|
+
curveSegments: 28,
|
|
38
|
+
});
|
|
39
|
+
geo.center();
|
|
40
|
+
geo.rotateZ(Math.PI);
|
|
41
|
+
return geo;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function AnimatedHeart({
|
|
45
|
+
position = [0, 0.2, 0.45],
|
|
46
|
+
scale = 0.5,
|
|
47
|
+
cycleSeconds = 6,
|
|
48
|
+
opacity = 0.8,
|
|
49
|
+
stretchY = 1,
|
|
50
|
+
sig,
|
|
51
|
+
}: AnimatedHeartProps): React.ReactElement {
|
|
52
|
+
const geometry = useMemo(makeHeartGeometry, []);
|
|
53
|
+
const mesh = useRef<THREE.Mesh>(null);
|
|
54
|
+
const material = useRef<THREE.MeshStandardMaterial>(null);
|
|
55
|
+
const color = useMemo(() => new THREE.Color(), []);
|
|
56
|
+
|
|
57
|
+
useFrame(() => {
|
|
58
|
+
const t = performance.now() / 1000;
|
|
59
|
+
const s = sig?.current;
|
|
60
|
+
const reduced = s?.reduced ?? false;
|
|
61
|
+
// Reduced motion: hold a static mid-cycle color, no hue drift / pulse / bob.
|
|
62
|
+
const phase = reduced ? 0 : Math.sin((t * 2 * Math.PI) / cycleSeconds);
|
|
63
|
+
const hue = (0.89 + 0.11 * phase) % 1;
|
|
64
|
+
color.setHSL(hue, 0.7, 0.56);
|
|
65
|
+
if (material.current) {
|
|
66
|
+
material.current.color.copy(color);
|
|
67
|
+
material.current.emissive.copy(color);
|
|
68
|
+
}
|
|
69
|
+
if (mesh.current) {
|
|
70
|
+
if (reduced) {
|
|
71
|
+
mesh.current.scale.set(scale, scale * stretchY, scale);
|
|
72
|
+
mesh.current.position.y = position[1];
|
|
73
|
+
} else {
|
|
74
|
+
const reacting = s ? t < s.reactUntil : false;
|
|
75
|
+
const pulse = 1 + (reacting ? 0.2 : 0.05) * Math.sin(t * (reacting ? 9 : 2.4));
|
|
76
|
+
mesh.current.scale.set(scale * pulse, scale * stretchY * pulse, scale * pulse);
|
|
77
|
+
mesh.current.position.y = position[1] + (reacting ? Math.abs(Math.sin(t * 9)) * 0.06 : 0);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
return (
|
|
83
|
+
<mesh ref={mesh} geometry={geometry} position={position} scale={scale}>
|
|
84
|
+
<meshStandardMaterial
|
|
85
|
+
ref={material}
|
|
86
|
+
transparent
|
|
87
|
+
opacity={opacity}
|
|
88
|
+
roughness={0.2}
|
|
89
|
+
metalness={0.0}
|
|
90
|
+
emissiveIntensity={0.35}
|
|
91
|
+
depthWrite={false}
|
|
92
|
+
/>
|
|
93
|
+
</mesh>
|
|
94
|
+
);
|
|
95
|
+
}
|