@mazees/mark 5.0.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 +29 -0
- package/README.md +209 -0
- package/bin/mark.js +27 -0
- package/out/renderer/assets/icon-DHo2CYph.svg +1 -0
- package/out/renderer/assets/index-G2fADWO-.js +4768 -0
- package/out/renderer/assets/index-f3mVse3c.css +1 -0
- package/out/renderer/assets/music-cover-CG4vi3kZ.png +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CJDn6rn8.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-400-normal-CqVvlrh5.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-BIdkeU1p.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-500-normal-DMPDjHtT.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-ClASKHrr.woff +0 -0
- package/out/renderer/assets/poppins-devanagari-600-normal-STEjXBNN.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-O-jipLrW.woff2 +0 -0
- package/out/renderer/assets/poppins-devanagari-700-normal-fHs-vx92.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-BOb3E3N0.woff +0 -0
- package/out/renderer/assets/poppins-latin-400-normal-cpxAROuN.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-C8OXljZJ.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-500-normal-DGXqpDMm.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-BJdTmd5m.woff +0 -0
- package/out/renderer/assets/poppins-latin-600-normal-zEkxB9Mr.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-BVuQR_eA.woff +0 -0
- package/out/renderer/assets/poppins-latin-700-normal-Qrb0O0WB.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-DaBSavcJ.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-400-normal-by3JarPu.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CK-6C4Hw.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-500-normal-CgAe2rWW.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-CAhIAdZj.woff2 +0 -0
- package/out/renderer/assets/poppins-latin-ext-600-normal-Df5ffKXP.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-DctTR6Tg.woff +0 -0
- package/out/renderer/assets/poppins-latin-ext-700-normal-cby-RkWa.woff2 +0 -0
- package/out/renderer/favicon.ico +0 -0
- package/out/renderer/icon-256.png +0 -0
- package/out/renderer/icon.svg +1 -0
- package/out/renderer/index.html +25 -0
- package/out/renderer/manifest.json +25 -0
- package/out/renderer/robot-simulator.html +1640 -0
- package/package.json +106 -0
- package/src/cli/index.js +332 -0
- package/src/cli/monitor.js +413 -0
- package/src/cli/theme.js +347 -0
- package/src/launcher/MarkTray.cs +227 -0
- package/src/main/browser-agent.js +1132 -0
- package/src/main/git-service.js +90 -0
- package/src/main/google/google-calendar.js +69 -0
- package/src/main/google/google-drive.js +263 -0
- package/src/main/google/google-gmail.js +131 -0
- package/src/main/google/google-service.js +241 -0
- package/src/main/node-tools.js +23 -0
- package/src/main/pc-agent-scripts/mouse-locker.ps1 +76 -0
- package/src/main/pc-agent-scripts/ocr-region.ps1 +185 -0
- package/src/main/pc-agent-scripts/pc-daemon.ps1 +726 -0
- package/src/main/pc-agent-scripts/pc-overlay.ps1 +253 -0
- package/src/main/pc-agent-scripts/pick-folder.ps1 +33 -0
- package/src/main/pc-agent-scripts/read-ui.ps1 +181 -0
- package/src/main/pc-agent-scripts/take-screenshot.ps1 +24 -0
- package/src/main/pc-agent-scripts/win-action.ps1 +380 -0
- package/src/main/plugins/plugin-loader.js +306 -0
- package/src/main/skills/skill-manager.js +265 -0
- package/src/main/syntax-validator.js +455 -0
- package/src/main/task-daemon.js +117 -0
- package/src/main/telegram/telegram-service.js +835 -0
- package/src/main/tools/browser-tools.js +516 -0
- package/src/main/tools/file-tools.js +1126 -0
- package/src/main/tools/git-tools.js +79 -0
- package/src/main/tools/google-tools.js +566 -0
- package/src/main/tools/index.js +36 -0
- package/src/main/tools/media-tools.js +42 -0
- package/src/main/tools/system-tools.js +295 -0
- package/src/main/tools/task-tools.js +73 -0
- package/src/main/tools/telegram-tools.js +101 -0
- package/src/renderer/index.html +24 -0
- package/src/renderer/public/favicon.ico +0 -0
- package/src/renderer/public/icon-256.png +0 -0
- package/src/renderer/public/icon.svg +1 -0
- package/src/renderer/public/manifest.json +25 -0
- package/src/renderer/public/robot-simulator.html +1640 -0
- package/src/renderer/src/App.jsx +304 -0
- package/src/renderer/src/api/ai/awareness.js +294 -0
- package/src/renderer/src/api/ai/chatSummarizer.js +79 -0
- package/src/renderer/src/api/ai/contextCompactor.js +129 -0
- package/src/renderer/src/api/ai/contextManager.js +1130 -0
- package/src/renderer/src/api/ai/core.js +167 -0
- package/src/renderer/src/api/ai/index.js +4 -0
- package/src/renderer/src/api/ai/memoryGroomer.js +101 -0
- package/src/renderer/src/api/ai/persona.js +115 -0
- package/src/renderer/src/api/ai/planning.js +317 -0
- package/src/renderer/src/api/ai/relationship.js +177 -0
- package/src/renderer/src/api/ai/skillSynthesizer.js +168 -0
- package/src/renderer/src/api/ai/tools.js +167 -0
- package/src/renderer/src/api/ai/utils.js +359 -0
- package/src/renderer/src/api/db.js +1013 -0
- package/src/renderer/src/api/oramaStore.js +592 -0
- package/src/renderer/src/api/ragPipeline.js +87 -0
- package/src/renderer/src/api/subagent/subagentExecutor.js +780 -0
- package/src/renderer/src/api/subagent/subagentPrompt.js +44 -0
- package/src/renderer/src/api/subagent/subagentStore.js +173 -0
- package/src/renderer/src/api/taskExecutor.js +18 -0
- package/src/renderer/src/api/taskStore.js +315 -0
- package/src/renderer/src/api/tools/core-tools.js +707 -0
- package/src/renderer/src/api/tools/index.js +151 -0
- package/src/renderer/src/api/turnPairMigrator.js +200 -0
- package/src/renderer/src/api/vectorMemory.js +136 -0
- package/src/renderer/src/api/wakeWord.js +101 -0
- package/src/renderer/src/api/web-bridge.js +885 -0
- package/src/renderer/src/api/webSpeech.js +178 -0
- package/src/renderer/src/api/workspaceRag.js +76 -0
- package/src/renderer/src/assets/electron.svg +10 -0
- package/src/renderer/src/assets/icon.svg +1 -0
- package/src/renderer/src/assets/main.css +589 -0
- package/src/renderer/src/assets/music-cover.png +0 -0
- package/src/renderer/src/assets/wavy-lines.svg +25 -0
- package/src/renderer/src/components/Chat/ApprovalBubble.jsx +127 -0
- package/src/renderer/src/components/Chat/CodeBlock.jsx +83 -0
- package/src/renderer/src/components/Chat/DurableTaskBubble.jsx +455 -0
- package/src/renderer/src/components/Chat/FileDiffModal.jsx +328 -0
- package/src/renderer/src/components/Chat/GeminiWidgets.jsx +82 -0
- package/src/renderer/src/components/Chat/MemoryFooterBubble.jsx +34 -0
- package/src/renderer/src/components/Chat/MessageBubble.jsx +459 -0
- package/src/renderer/src/components/Chat/PluginExecutionBubble.jsx +41 -0
- package/src/renderer/src/components/Chat/ThinkingBubble.jsx +316 -0
- package/src/renderer/src/components/Chat/YoutubeSearchBubble.jsx +43 -0
- package/src/renderer/src/components/Chat/YoutubeSummaryBubble.jsx +40 -0
- package/src/renderer/src/components/Chat/index.js +11 -0
- package/src/renderer/src/components/ChatList.jsx +434 -0
- package/src/renderer/src/components/GlobalCameraManager.jsx +67 -0
- package/src/renderer/src/components/Versions.jsx +15 -0
- package/src/renderer/src/components/YoutubeMusicPlayer.jsx +375 -0
- package/src/renderer/src/components/camera/CameraPreview.jsx +236 -0
- package/src/renderer/src/components/config/AiEngineSection.jsx +363 -0
- package/src/renderer/src/components/config/CameraSection.jsx +100 -0
- package/src/renderer/src/components/config/PermissionsSection.jsx +130 -0
- package/src/renderer/src/components/config/ShortcutSection.jsx +102 -0
- package/src/renderer/src/components/config/VoiceSection.jsx +110 -0
- package/src/renderer/src/components/config/index.js +5 -0
- package/src/renderer/src/components/core/Avatar.jsx +2197 -0
- package/src/renderer/src/components/core/ConfirmModal.jsx +80 -0
- package/src/renderer/src/components/core/DraggableHoloCard.jsx +125 -0
- package/src/renderer/src/components/core/FloatingMenu.jsx +272 -0
- package/src/renderer/src/components/core/HoloCard.jsx +64 -0
- package/src/renderer/src/components/core/InputBar.jsx +1346 -0
- package/src/renderer/src/components/core/ProcessPanel.jsx +216 -0
- package/src/renderer/src/components/core/ResponseArea.jsx +149 -0
- package/src/renderer/src/components/core/StatusIndicator.jsx +59 -0
- package/src/renderer/src/components/core/ThoughtNeuralFlow.jsx +168 -0
- package/src/renderer/src/components/core/ToolClustersDeck.jsx +210 -0
- package/src/renderer/src/components/core/native-skills.js +43 -0
- package/src/renderer/src/components/neural-core/EmotionalCortexLobe.jsx +359 -0
- package/src/renderer/src/components/neural-core/NodeDissectionModal.jsx +57 -0
- package/src/renderer/src/components/neural-core/ProceduralMatrixLobe.jsx +193 -0
- package/src/renderer/src/components/neural-core/ProceduralSkillModal.jsx +53 -0
- package/src/renderer/src/components/neural-core/ResetAiModal.jsx +198 -0
- package/src/renderer/src/components/neural-core/SynapticMemoryLobe.jsx +204 -0
- package/src/renderer/src/components/neural-core/traitMeta.js +102 -0
- package/src/renderer/src/components/subagent/AgentChatFeed.jsx +603 -0
- package/src/renderer/src/components/subagent/AgentInspector.jsx +224 -0
- package/src/renderer/src/components/subagent/AgentSidebar.jsx +216 -0
- package/src/renderer/src/components/subagent/SubagentIntercom.jsx +572 -0
- package/src/renderer/src/components/subagent/SubagentTopologyMap.jsx +423 -0
- package/src/renderer/src/contexts/ApprovalContext.jsx +425 -0
- package/src/renderer/src/contexts/ChatContext.jsx +61 -0
- package/src/renderer/src/contexts/YoutubeMusicContext.jsx +333 -0
- package/src/renderer/src/data/faqData.js +202 -0
- package/src/renderer/src/hooks/agent/executeAgentTool.js +895 -0
- package/src/renderer/src/hooks/agent/index.js +4 -0
- package/src/renderer/src/hooks/agent/useMarkMusic.js +75 -0
- package/src/renderer/src/hooks/agent/useMarkPlan.js +2739 -0
- package/src/renderer/src/hooks/agent/useMarkState.js +173 -0
- package/src/renderer/src/hooks/agent/useMarkYoutube.js +82 -0
- package/src/renderer/src/hooks/agent/useRelationalGrowth.js +201 -0
- package/src/renderer/src/hooks/telegram/useTelegramBot.js +63 -0
- package/src/renderer/src/hooks/useAwareness.js +289 -0
- package/src/renderer/src/hooks/useChatArchiver.js +52 -0
- package/src/renderer/src/hooks/useConfirm.jsx +68 -0
- package/src/renderer/src/hooks/useMarkAgent.js +454 -0
- package/src/renderer/src/hooks/useMemoryGroomer.js +152 -0
- package/src/renderer/src/hooks/useVAD.js +511 -0
- package/src/renderer/src/hooks/useWebSpeechWakeWord.js +122 -0
- package/src/renderer/src/main.jsx +12 -0
- package/src/renderer/src/pages/ChatStudio.jsx +922 -0
- package/src/renderer/src/pages/Configuration.jsx +360 -0
- package/src/renderer/src/pages/GoogleWorkspace.jsx +293 -0
- package/src/renderer/src/pages/Guidebook.jsx +917 -0
- package/src/renderer/src/pages/Knowledge.jsx +241 -0
- package/src/renderer/src/pages/MarkHome.jsx +491 -0
- package/src/renderer/src/pages/NeuralCore.jsx +786 -0
- package/src/renderer/src/pages/Plugins.jsx +630 -0
- package/src/renderer/src/pages/SkillEditor.jsx +380 -0
- package/src/renderer/src/pages/Skills.jsx +219 -0
- package/src/renderer/src/pages/Subagents.jsx +342 -0
- package/src/renderer/src/pages/TelegramBot.jsx +343 -0
- package/src/renderer/src/utils/chatContentParser.js +203 -0
- package/src/renderer/src/utils/toolClusters.js +201 -0
- package/src/server/bin/sha3_wasm_bg.wasm +0 -0
- package/src/server/config-manager.js +57 -0
- package/src/server/index.js +195 -0
- package/src/server/launcher.js +87 -0
- package/src/server/memory/db-store.js +758 -0
- package/src/server/memory/orama-store.js +177 -0
- package/src/server/memory/vector-engine.js +109 -0
- package/src/server/routes/ai.routes.js +280 -0
- package/src/server/routes/awareness.routes.js +62 -0
- package/src/server/routes/chat.routes.js +61 -0
- package/src/server/routes/config.routes.js +41 -0
- package/src/server/routes/index.js +35 -0
- package/src/server/routes/integrations.routes.js +116 -0
- package/src/server/routes/memory.routes.js +256 -0
- package/src/server/routes/plugins.routes.js +72 -0
- package/src/server/routes/skills.routes.js +139 -0
- package/src/server/routes/subagents.routes.js +70 -0
- package/src/server/routes/tasks.routes.js +175 -0
- package/src/server/routes/workspace.routes.js +127 -0
- package/src/server/services/ai/ai-utils.js +161 -0
- package/src/server/services/ai/openai-provider.js +757 -0
- package/src/server/services/ai/web-provider.js +729 -0
- package/src/server/services/ai-bridge.js +46 -0
- package/src/server/services/deepseek-web.js +956 -0
- package/src/server/services/gemini-web.js +170 -0
- package/src/server/services/notification-service.js +91 -0
- package/src/server/services/updater.js +125 -0
- package/src/server/tools/awareness-tracker.js +292 -0
- package/src/server/tools/group-tools.js +1164 -0
- package/src/server/tools/media-tools.js +244 -0
- package/src/server/tools/pc-agent.js +327 -0
- package/src/server/tools/screen-service.js +51 -0
- package/src/server/ws-hub.js +168 -0
- package/src/shared/parsers/mark-tag-parser.js +275 -0
|
@@ -0,0 +1,2197 @@
|
|
|
1
|
+
/* eslint-disable react/prop-types */
|
|
2
|
+
import { useEffect, useRef, useCallback, memo } from 'react'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mapping warna terpadu berdasarkan Mood & Status AI Mark
|
|
6
|
+
*/
|
|
7
|
+
const getMoodColor = (mood = 'neutral', status = 'idle') => {
|
|
8
|
+
if (status === 'error' || status === 'bonk_angry' || status === 'shield_block') {
|
|
9
|
+
return { hex: '#ef4444', glow: 'bg-red-500/40' }
|
|
10
|
+
}
|
|
11
|
+
if (status === 'bonk_annoyed') {
|
|
12
|
+
return { hex: '#f97316', glow: 'bg-orange-500/40' }
|
|
13
|
+
}
|
|
14
|
+
if (status === 'bonk_dizzy') {
|
|
15
|
+
return { hex: '#a855f7', glow: 'bg-purple-500/40' }
|
|
16
|
+
}
|
|
17
|
+
if (status === 'music_groove') {
|
|
18
|
+
return { hex: '#06b6d4', glow: 'bg-cyan-400/40' }
|
|
19
|
+
}
|
|
20
|
+
if (status === 'processing') {
|
|
21
|
+
return { hex: '#3b82f6', glow: 'bg-blue-500/40' }
|
|
22
|
+
}
|
|
23
|
+
if (status === 'drag_lift') {
|
|
24
|
+
return { hex: '#38bdf8', glow: 'bg-sky-400/40' }
|
|
25
|
+
}
|
|
26
|
+
if (status === 'tickle') {
|
|
27
|
+
return { hex: '#facc15', glow: 'bg-yellow-400/40' }
|
|
28
|
+
}
|
|
29
|
+
if (status === 'pet_enjoy' || status === 'pet_purr' || status === 'blush_soft') {
|
|
30
|
+
return { hex: '#1fb854', glow: 'bg-emerald-400/40' }
|
|
31
|
+
}
|
|
32
|
+
switch (mood) {
|
|
33
|
+
case 'joy':
|
|
34
|
+
return { hex: '#facc15', glow: 'bg-yellow-400/40' }
|
|
35
|
+
case 'sadness':
|
|
36
|
+
return { hex: '#3b82f6', glow: 'bg-blue-500/40' }
|
|
37
|
+
case 'fear':
|
|
38
|
+
return { hex: '#a855f7', glow: 'bg-purple-500/40' }
|
|
39
|
+
case 'anger':
|
|
40
|
+
return { hex: '#ef4444', glow: 'bg-red-500/40' }
|
|
41
|
+
case 'disgust':
|
|
42
|
+
return { hex: '#84cc16', glow: 'bg-lime-400/40' }
|
|
43
|
+
case 'anxiety':
|
|
44
|
+
return { hex: '#f97316', glow: 'bg-orange-500/40' }
|
|
45
|
+
case 'envy':
|
|
46
|
+
return { hex: '#14b8a6', glow: 'bg-teal-500/40' }
|
|
47
|
+
case 'embarrassment':
|
|
48
|
+
return { hex: '#ec4899', glow: 'bg-pink-500/40' }
|
|
49
|
+
case 'ennui':
|
|
50
|
+
return { hex: '#6b7280', glow: 'bg-gray-500/40' }
|
|
51
|
+
default: // neutral
|
|
52
|
+
return { hex: '#1fb854', glow: 'bg-green-500/40' }
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Avatar (2.5D Cyber-Droid Companion)
|
|
58
|
+
* Komponen avatar robot otonom MARK berbasis SVG volumetric 3D transforms.
|
|
59
|
+
* Menyediakan pergerakan hidup mandiri, tatapan mata presisi di tengah saat berbicara,
|
|
60
|
+
* responsivitas terhadap 10 mood, sentuhan interaktif fisik, dan seluruh status operasional AI.
|
|
61
|
+
*/
|
|
62
|
+
const Avatar = ({
|
|
63
|
+
status = 'idle',
|
|
64
|
+
intensity = 0,
|
|
65
|
+
mood = 'neutral',
|
|
66
|
+
onClick = null,
|
|
67
|
+
onInteract = null,
|
|
68
|
+
className = ''
|
|
69
|
+
}) => {
|
|
70
|
+
// DOM element references
|
|
71
|
+
const robotRootRef = useRef(null)
|
|
72
|
+
const robotHeadRef = useRef(null)
|
|
73
|
+
const robotBodyRef = useRef(null)
|
|
74
|
+
const handLeftRef = useRef(null)
|
|
75
|
+
const handRightRef = useRef(null)
|
|
76
|
+
const antLeftRef = useRef(null)
|
|
77
|
+
const antRightRef = useRef(null)
|
|
78
|
+
const antBulbLeftRef = useRef(null)
|
|
79
|
+
const antBulbRightRef = useRef(null)
|
|
80
|
+
const earLeftRef = useRef(null)
|
|
81
|
+
const earRightRef = useRef(null)
|
|
82
|
+
const earGlowLeftRef = useRef(null)
|
|
83
|
+
const earGlowRightRef = useRef(null)
|
|
84
|
+
const groundShadowRef = useRef(null)
|
|
85
|
+
const eyesContainerRef = useRef(null)
|
|
86
|
+
const leftEyeRef = useRef(null)
|
|
87
|
+
const rightEyeRef = useRef(null)
|
|
88
|
+
const mouthRef = useRef(null)
|
|
89
|
+
const thrusterRef = useRef(null)
|
|
90
|
+
const desktopReflectionRef = useRef(null)
|
|
91
|
+
const prevStatusRef = useRef(status)
|
|
92
|
+
|
|
93
|
+
// State refs for animation loop
|
|
94
|
+
const stateRef = useRef({
|
|
95
|
+
status: 'idle',
|
|
96
|
+
mood: 'neutral',
|
|
97
|
+
intensity: 0,
|
|
98
|
+
isSleeping: false,
|
|
99
|
+
isBlinking: false,
|
|
100
|
+
eyeScaleX: 1,
|
|
101
|
+
eyeScaleY: 1,
|
|
102
|
+
// Physics variables
|
|
103
|
+
rotX: 0,
|
|
104
|
+
rotY: 0,
|
|
105
|
+
rotZ: 0,
|
|
106
|
+
targetRotX: 0,
|
|
107
|
+
targetRotY: 0,
|
|
108
|
+
targetRotZ: 0,
|
|
109
|
+
velRotX: 0,
|
|
110
|
+
velRotY: 0,
|
|
111
|
+
velRotZ: 0,
|
|
112
|
+
// Saccades
|
|
113
|
+
eyeSaccadeX: 0,
|
|
114
|
+
eyeSaccadeY: 0,
|
|
115
|
+
targetSaccadeX: 0,
|
|
116
|
+
targetSaccadeY: 0,
|
|
117
|
+
// Continuous phase accumulators (prevent frequency jump stutter)
|
|
118
|
+
breathPhase: 0,
|
|
119
|
+
swayPhase: 0,
|
|
120
|
+
currentBreathDepth: 7.5,
|
|
121
|
+
currentSwayDepth: 3.5,
|
|
122
|
+
currentRollDepth: 1.0,
|
|
123
|
+
// Quirks
|
|
124
|
+
lastQuirkTime: 0,
|
|
125
|
+
nextQuirkInterval: 2200,
|
|
126
|
+
quirkPitchOffset: 0,
|
|
127
|
+
quirkRollOffset: 0,
|
|
128
|
+
quirkAltitudeOffset: 0,
|
|
129
|
+
targetAltitudeOffset: 0,
|
|
130
|
+
// Dedicated Listening Mode
|
|
131
|
+
listeningSide: 1, // 1 = look right, -1 = look left
|
|
132
|
+
earScale: 1.0,
|
|
133
|
+
// Antenna
|
|
134
|
+
antLeftRot: 0,
|
|
135
|
+
antRightRot: 0,
|
|
136
|
+
antVelLeft: 0,
|
|
137
|
+
antVelRight: 0,
|
|
138
|
+
// Hands
|
|
139
|
+
handLeftX: 0,
|
|
140
|
+
handLeftY: 0,
|
|
141
|
+
handRightX: 0,
|
|
142
|
+
handRightY: 0,
|
|
143
|
+
// Poke physics
|
|
144
|
+
pokeOffsetY: 0,
|
|
145
|
+
pokeVelocityY: 0,
|
|
146
|
+
// Awareness reading scan
|
|
147
|
+
isSeeingDesktop: false,
|
|
148
|
+
desktopForwardZ: 0,
|
|
149
|
+
desktopDownwardY: 0,
|
|
150
|
+
desktopTimeout: null,
|
|
151
|
+
// --- Tactile Interactive State Engine ---
|
|
152
|
+
tactileState: null,
|
|
153
|
+
tactileTimer: null,
|
|
154
|
+
bonkCount: 0,
|
|
155
|
+
lastBonkTime: 0,
|
|
156
|
+
petAccumulator: 0,
|
|
157
|
+
petStartTime: 0,
|
|
158
|
+
tickleAccumulator: 0,
|
|
159
|
+
isPointerDown: false,
|
|
160
|
+
pointerDownZone: null,
|
|
161
|
+
pointerDownPos: { x: 0, y: 0 },
|
|
162
|
+
lastPointerPos: { x: 0, y: 0 },
|
|
163
|
+
isDragging: false,
|
|
164
|
+
dragOffset: { x: 0, y: 0 },
|
|
165
|
+
dragVelocity: { x: 0, y: 0 },
|
|
166
|
+
dropAnim: { active: false, y: 0, velY: 0, squashX: 1, squashY: 1 },
|
|
167
|
+
dizzyAngle: 0,
|
|
168
|
+
purrJitter: { x: 0, y: 0 },
|
|
169
|
+
shieldProgress: 0,
|
|
170
|
+
corePulseScale: 1
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
// Render face eyes and mouth SVG based on state
|
|
174
|
+
const renderFace = useCallback(() => {
|
|
175
|
+
const s = stateRef.current
|
|
176
|
+
const effectiveStatus = s.tactileState || (s.isSleeping ? 'sleeping' : s.status)
|
|
177
|
+
const color = getMoodColor(s.mood, effectiveStatus).hex
|
|
178
|
+
|
|
179
|
+
const leftEye = leftEyeRef.current
|
|
180
|
+
const rightEye = rightEyeRef.current
|
|
181
|
+
const mouth = mouthRef.current
|
|
182
|
+
if (!leftEye || !rightEye || !mouth) return
|
|
183
|
+
|
|
184
|
+
// 1. DIZZY SPIRAL (Combo bonk 5-7x)
|
|
185
|
+
if (effectiveStatus === 'bonk_dizzy') {
|
|
186
|
+
const rot = Math.round(s.dizzyAngle || 0)
|
|
187
|
+
const dizzyEye = `
|
|
188
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
189
|
+
<g style="transform-origin: 20px 20px; transform: rotate(${rot}deg)">
|
|
190
|
+
<path d="M 20 20 m 0 0 a 2.5 2.5 0 0 1 2.5 2.5 a 5 5 0 0 1 -5 2.5 a 7.5 7.5 0 0 1 -5 -7.5 a 10 10 0 0 1 10 -5 a 13 13 0 0 1 11 11 a 16 16 0 0 1 -16 14" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
|
|
191
|
+
</g>
|
|
192
|
+
</svg>
|
|
193
|
+
`
|
|
194
|
+
leftEye.innerHTML = dizzyEye
|
|
195
|
+
rightEye.innerHTML = dizzyEye
|
|
196
|
+
mouth.innerHTML = `
|
|
197
|
+
<svg viewBox="0 0 60 20" class="w-12 h-4">
|
|
198
|
+
<path d="M 18 10 Q 24 4 30 10 T 42 10" fill="none" stroke="${color}" stroke-width="2.8" stroke-linecap="round"/>
|
|
199
|
+
</svg>
|
|
200
|
+
`
|
|
201
|
+
return
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// 2. SHIELD BLOCK / ANGRY BONK (Combo bonk 8x+)
|
|
205
|
+
if (effectiveStatus === 'shield_block' || effectiveStatus === 'bonk_angry') {
|
|
206
|
+
const angryLeft = `
|
|
207
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
208
|
+
<path d="M 6 10 L 34 22 L 28 30 L 6 18 Z" fill="#ef4444"/>
|
|
209
|
+
</svg>
|
|
210
|
+
`
|
|
211
|
+
const angryRight = `
|
|
212
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
213
|
+
<path d="M 34 10 L 6 22 L 12 30 L 34 18 Z" fill="#ef4444"/>
|
|
214
|
+
</svg>
|
|
215
|
+
`
|
|
216
|
+
leftEye.innerHTML = angryLeft
|
|
217
|
+
rightEye.innerHTML = angryRight
|
|
218
|
+
mouth.innerHTML = `
|
|
219
|
+
<svg viewBox="0 0 60 20" class="w-12 h-4">
|
|
220
|
+
<rect x="20" y="7" width="20" height="9" rx="2" fill="none" stroke="#ef4444" stroke-width="2"/>
|
|
221
|
+
<line x1="26" y1="7" x2="26" y2="16" stroke="#ef4444" stroke-width="1.5"/>
|
|
222
|
+
<line x1="34" y1="7" x2="34" y2="16" stroke="#ef4444" stroke-width="1.5"/>
|
|
223
|
+
<line x1="20" y1="11.5" x2="40" y2="11.5" stroke="#ef4444" stroke-width="1.5"/>
|
|
224
|
+
</svg>
|
|
225
|
+
`
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// 3. ANNOYED BONK (Combo bonk 4-6x)
|
|
230
|
+
if (effectiveStatus === 'bonk_annoyed') {
|
|
231
|
+
const annoyedLeft = `
|
|
232
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
233
|
+
<line x1="8" y1="11" x2="32" y2="17" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/>
|
|
234
|
+
<ellipse cx="20" cy="22" rx="7" ry="4" fill="${color}"/>
|
|
235
|
+
</svg>
|
|
236
|
+
`
|
|
237
|
+
const annoyedRight = `
|
|
238
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
239
|
+
<line x1="8" y1="17" x2="32" y2="11" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/>
|
|
240
|
+
<ellipse cx="20" cy="22" rx="7" ry="4" fill="${color}"/>
|
|
241
|
+
</svg>
|
|
242
|
+
`
|
|
243
|
+
leftEye.innerHTML = annoyedLeft
|
|
244
|
+
rightEye.innerHTML = annoyedRight
|
|
245
|
+
mouth.innerHTML = `
|
|
246
|
+
<svg viewBox="0 0 60 20" class="w-10 h-4">
|
|
247
|
+
<path d="M 22 14 L 30 10 L 38 13" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
248
|
+
</svg>
|
|
249
|
+
`
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// 3b. GENTLE BONK (Combo bonk 1-3x: ouch wince, squint eyes + wavy mouth)
|
|
254
|
+
if (effectiveStatus === 'bonk_1') {
|
|
255
|
+
const bonkEye = `
|
|
256
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
257
|
+
<path d="M 8 22 L 20 14 L 32 22" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round" stroke-linejoin="round"/>
|
|
258
|
+
</svg>
|
|
259
|
+
`
|
|
260
|
+
leftEye.innerHTML = bonkEye
|
|
261
|
+
rightEye.innerHTML = bonkEye
|
|
262
|
+
mouth.innerHTML = `
|
|
263
|
+
<svg viewBox="0 0 60 20" class="w-10 h-4">
|
|
264
|
+
<path d="M 22 13 Q 30 7 38 13" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
265
|
+
</svg>
|
|
266
|
+
`
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// 4. PETTING / PURRING / BLUSHING (Happy eyes + blush lines + cat mouth)
|
|
271
|
+
if (
|
|
272
|
+
effectiveStatus === 'pet_enjoy' ||
|
|
273
|
+
effectiveStatus === 'pet_purr' ||
|
|
274
|
+
effectiveStatus === 'blush_soft'
|
|
275
|
+
) {
|
|
276
|
+
const petEye = `
|
|
277
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
278
|
+
<path d="M 8 20 Q 20 8 32 20" fill="none" stroke="${color}" stroke-width="4.8" stroke-linecap="round"/>
|
|
279
|
+
<line x1="10" y1="28" x2="16" y2="28" stroke="#f43f5e" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
|
|
280
|
+
<line x1="24" y1="28" x2="30" y2="28" stroke="#f43f5e" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
|
|
281
|
+
</svg>
|
|
282
|
+
`
|
|
283
|
+
leftEye.innerHTML = petEye
|
|
284
|
+
rightEye.innerHTML = petEye
|
|
285
|
+
mouth.innerHTML = `
|
|
286
|
+
<svg viewBox="0 0 60 20" class="w-12 h-5">
|
|
287
|
+
<path d="M 20 8 Q 25 15 30 11 Q 35 15 40 8" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
288
|
+
</svg>
|
|
289
|
+
`
|
|
290
|
+
return
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// 5. TICKLE (Squint joy laughing)
|
|
294
|
+
if (effectiveStatus === 'tickle') {
|
|
295
|
+
const tickleEye = `
|
|
296
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
297
|
+
<path d="M 9 22 Q 20 10 31 22" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
298
|
+
</svg>
|
|
299
|
+
`
|
|
300
|
+
leftEye.innerHTML = tickleEye
|
|
301
|
+
rightEye.innerHTML = tickleEye
|
|
302
|
+
mouth.innerHTML = `
|
|
303
|
+
<svg viewBox="0 0 60 26" class="w-12 h-6">
|
|
304
|
+
<path d="M 20 10 Q 30 24 40 10 Z" fill="${color}" opacity="0.9"/>
|
|
305
|
+
</svg>
|
|
306
|
+
`
|
|
307
|
+
return
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// 6. POKED EYE (Left or Right eye wink)
|
|
311
|
+
if (effectiveStatus === 'poke_eye_l') {
|
|
312
|
+
leftEye.innerHTML = `
|
|
313
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
314
|
+
<line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
315
|
+
</svg>
|
|
316
|
+
`
|
|
317
|
+
rightEye.innerHTML = `
|
|
318
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
319
|
+
<circle cx="20" cy="20" r="10" fill="none" stroke="${color}" stroke-width="4"/>
|
|
320
|
+
<circle cx="20" cy="20" r="4.5" fill="${color}"/>
|
|
321
|
+
</svg>
|
|
322
|
+
`
|
|
323
|
+
mouth.innerHTML = `
|
|
324
|
+
<svg viewBox="0 0 60 20" class="w-8 h-4">
|
|
325
|
+
<ellipse cx="30" cy="11" rx="4" ry="6" fill="${color}"/>
|
|
326
|
+
</svg>
|
|
327
|
+
`
|
|
328
|
+
return
|
|
329
|
+
}
|
|
330
|
+
if (effectiveStatus === 'poke_eye_r') {
|
|
331
|
+
leftEye.innerHTML = `
|
|
332
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
333
|
+
<circle cx="20" cy="20" r="10" fill="none" stroke="${color}" stroke-width="4"/>
|
|
334
|
+
<circle cx="20" cy="20" r="4.5" fill="${color}"/>
|
|
335
|
+
</svg>
|
|
336
|
+
`
|
|
337
|
+
rightEye.innerHTML = `
|
|
338
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
339
|
+
<line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
340
|
+
</svg>
|
|
341
|
+
`
|
|
342
|
+
mouth.innerHTML = `
|
|
343
|
+
<svg viewBox="0 0 60 20" class="w-8 h-4">
|
|
344
|
+
<ellipse cx="30" cy="11" rx="4" ry="6" fill="${color}"/>
|
|
345
|
+
</svg>
|
|
346
|
+
`
|
|
347
|
+
return
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
// 7. POKED ANTENNA
|
|
351
|
+
if (effectiveStatus === 'poke_antenna') {
|
|
352
|
+
const antEye = `
|
|
353
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
354
|
+
<path d="M 10 20 Q 15 14 20 20 T 30 20" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
355
|
+
</svg>
|
|
356
|
+
`
|
|
357
|
+
leftEye.innerHTML = antEye
|
|
358
|
+
rightEye.innerHTML = antEye
|
|
359
|
+
mouth.innerHTML = `
|
|
360
|
+
<svg viewBox="0 0 60 20" class="w-10 h-4">
|
|
361
|
+
<path d="M 22 10 L 26 6 L 30 14 L 34 6 L 38 10" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
362
|
+
</svg>
|
|
363
|
+
`
|
|
364
|
+
return
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// 8. POKED CORE OR STARTLE WAKE
|
|
368
|
+
if (effectiveStatus === 'poke_core' || effectiveStatus === 'startle_wake') {
|
|
369
|
+
const shockEye = `
|
|
370
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
371
|
+
<circle cx="20" cy="20" r="12" fill="none" stroke="${color}" stroke-width="3.5"/>
|
|
372
|
+
<circle cx="20" cy="20" r="5" fill="#ffffff"/>
|
|
373
|
+
</svg>
|
|
374
|
+
`
|
|
375
|
+
leftEye.innerHTML = shockEye
|
|
376
|
+
rightEye.innerHTML = shockEye
|
|
377
|
+
mouth.innerHTML = `
|
|
378
|
+
<svg viewBox="0 0 60 20" class="w-8 h-5">
|
|
379
|
+
<ellipse cx="30" cy="11" rx="5" ry="7" fill="${color}"/>
|
|
380
|
+
</svg>
|
|
381
|
+
`
|
|
382
|
+
return
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// 9. DRAGGING / LIFT IN AIR
|
|
386
|
+
if (effectiveStatus === 'drag_lift') {
|
|
387
|
+
const liftEye = `
|
|
388
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
389
|
+
<ellipse cx="20" cy="18" rx="8" ry="11" fill="none" stroke="${color}" stroke-width="3.5"/>
|
|
390
|
+
<circle cx="20" cy="23" r="4" fill="${color}"/>
|
|
391
|
+
</svg>
|
|
392
|
+
`
|
|
393
|
+
leftEye.innerHTML = liftEye
|
|
394
|
+
rightEye.innerHTML = liftEye
|
|
395
|
+
mouth.innerHTML = `
|
|
396
|
+
<svg viewBox="0 0 60 20" class="w-8 h-5">
|
|
397
|
+
<ellipse cx="30" cy="11" rx="5" ry="7" fill="${color}"/>
|
|
398
|
+
</svg>
|
|
399
|
+
`
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// 10. MUSIC GROOVE
|
|
404
|
+
if (effectiveStatus === 'music_groove') {
|
|
405
|
+
const eqEye = `
|
|
406
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
407
|
+
<rect x="9" y="14" width="4" height="12" rx="2" fill="${color}"/>
|
|
408
|
+
<rect x="18" y="8" width="4" height="24" rx="2" fill="${color}"/>
|
|
409
|
+
<rect x="27" y="16" width="4" height="8" rx="2" fill="${color}"/>
|
|
410
|
+
</svg>
|
|
411
|
+
`
|
|
412
|
+
leftEye.innerHTML = eqEye
|
|
413
|
+
rightEye.innerHTML = eqEye
|
|
414
|
+
mouth.innerHTML = `
|
|
415
|
+
<svg viewBox="0 0 60 20" class="w-12 h-4">
|
|
416
|
+
<path d="M 20 9 Q 30 18 40 9" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
|
|
417
|
+
</svg>
|
|
418
|
+
`
|
|
419
|
+
return
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// 11. PROCESSING (Native tool HUD)
|
|
423
|
+
if (effectiveStatus === 'processing') {
|
|
424
|
+
const procEye = `
|
|
425
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
426
|
+
<path d="M 12 11 L 8 11 L 8 29 L 12 29" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
|
|
427
|
+
<path d="M 28 11 L 32 11 L 32 29 L 28 29" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
|
|
428
|
+
<circle cx="20" cy="20" r="3.2" fill="${color}"/>
|
|
429
|
+
</svg>
|
|
430
|
+
`
|
|
431
|
+
leftEye.innerHTML = procEye
|
|
432
|
+
rightEye.innerHTML = procEye
|
|
433
|
+
mouth.innerHTML = `
|
|
434
|
+
<div class="flex items-center gap-1 py-1">
|
|
435
|
+
<span class="w-2 h-1 bg-primary animate-pulse"></span>
|
|
436
|
+
<span class="w-2 h-1 bg-primary animate-pulse [animation-delay:0.15s]"></span>
|
|
437
|
+
<span class="w-2 h-1 bg-primary animate-pulse [animation-delay:0.3s]"></span>
|
|
438
|
+
</div>
|
|
439
|
+
`
|
|
440
|
+
return
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// 12. SLEEPING STATE (Closed calm rounded curves)
|
|
444
|
+
if (effectiveStatus === 'sleeping') {
|
|
445
|
+
const sleepEye = `
|
|
446
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
447
|
+
<path d="M 8 22 Q 20 25 32 22" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
448
|
+
</svg>
|
|
449
|
+
`
|
|
450
|
+
leftEye.innerHTML = sleepEye
|
|
451
|
+
rightEye.innerHTML = sleepEye
|
|
452
|
+
mouth.innerHTML = `
|
|
453
|
+
<svg viewBox="0 0 60 20" class="w-12 h-5">
|
|
454
|
+
<path d="M 22 10 Q 30 14 38 10" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
455
|
+
</svg>
|
|
456
|
+
`
|
|
457
|
+
return
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// 13. ERROR STATE (Sharp digital crosses)
|
|
461
|
+
if (effectiveStatus === 'error') {
|
|
462
|
+
const errEye = `
|
|
463
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
464
|
+
<line x1="10" y1="10" x2="30" y2="30" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
465
|
+
<line x1="30" y1="10" x2="10" y2="30" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
466
|
+
</svg>
|
|
467
|
+
`
|
|
468
|
+
leftEye.innerHTML = errEye
|
|
469
|
+
rightEye.innerHTML = errEye
|
|
470
|
+
mouth.innerHTML = `
|
|
471
|
+
<svg viewBox="0 0 60 20" class="w-14 h-5">
|
|
472
|
+
<path d="M 15 10 L 22 6 L 30 14 L 38 6 L 45 10" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>
|
|
473
|
+
</svg>
|
|
474
|
+
`
|
|
475
|
+
return
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// 14. AWARENESS / READING DESKTOP EYES (Focused eyes curved slightly downward)
|
|
479
|
+
if (s.isSeeingDesktop || effectiveStatus === 'awareness' || effectiveStatus === 'nudge') {
|
|
480
|
+
const readingEye = `
|
|
481
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
482
|
+
<path d="M 11 15 C 11 9, 29 9, 29 15 C 29 27, 11 27, 11 15 Z" fill="${color}"/>
|
|
483
|
+
<circle cx="20" cy="18" r="3.2" fill="#ffffff"/>
|
|
484
|
+
</svg>
|
|
485
|
+
`
|
|
486
|
+
leftEye.innerHTML = readingEye
|
|
487
|
+
rightEye.innerHTML = readingEye
|
|
488
|
+
mouth.innerHTML = `
|
|
489
|
+
<svg viewBox="0 0 60 20" class="w-8 h-3">
|
|
490
|
+
<line x1="25" y1="10" x2="35" y2="10" stroke="${color}" stroke-width="2" stroke-linecap="round"/>
|
|
491
|
+
</svg>
|
|
492
|
+
`
|
|
493
|
+
return
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 15. LISTENING / HEARING STATE (Focused attentive eyes, dilated alert pupils, inquisitive brows, quiet mouth)
|
|
497
|
+
if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
|
|
498
|
+
if (s.isBlinking) {
|
|
499
|
+
const blinkEye = `
|
|
500
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
501
|
+
<line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
502
|
+
</svg>
|
|
503
|
+
`
|
|
504
|
+
leftEye.innerHTML = blinkEye
|
|
505
|
+
rightEye.innerHTML = blinkEye
|
|
506
|
+
} else {
|
|
507
|
+
const pupilOffset = (s.listeningSide || 1) * 2.5
|
|
508
|
+
const leftBrowY = s.listeningSide === -1 ? '4' : '7'
|
|
509
|
+
const rightBrowY = s.listeningSide === 1 ? '4' : '7'
|
|
510
|
+
const listeningLeftEye = `
|
|
511
|
+
<svg viewBox="0 0 40 40" class="w-full h-full overflow-visible">
|
|
512
|
+
<path d="M 8 9 Q 20 ${leftBrowY} 32 8" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
|
|
513
|
+
<path d="M 11 14 C 11 8, 29 8, 29 14 C 29 27, 11 27, 11 14 Z" fill="${color}"/>
|
|
514
|
+
<circle cx="${20 + pupilOffset}" cy="16.5" r="4.6" fill="#ffffff" opacity="0.98"/>
|
|
515
|
+
<circle cx="${21 + pupilOffset}" cy="15.2" r="1.8" fill="#ffffff"/>
|
|
516
|
+
</svg>
|
|
517
|
+
`
|
|
518
|
+
const listeningRightEye = `
|
|
519
|
+
<svg viewBox="0 0 40 40" class="w-full h-full overflow-visible">
|
|
520
|
+
<path d="M 8 8 Q 20 ${rightBrowY} 32 9" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round" opacity="0.85"/>
|
|
521
|
+
<path d="M 11 14 C 11 8, 29 8, 29 14 C 29 27, 11 27, 11 14 Z" fill="${color}"/>
|
|
522
|
+
<circle cx="${20 + pupilOffset}" cy="16.5" r="4.6" fill="#ffffff" opacity="0.98"/>
|
|
523
|
+
<circle cx="${21 + pupilOffset}" cy="15.2" r="1.8" fill="#ffffff"/>
|
|
524
|
+
</svg>
|
|
525
|
+
`
|
|
526
|
+
leftEye.innerHTML = listeningLeftEye
|
|
527
|
+
rightEye.innerHTML = listeningRightEye
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
mouth.innerHTML = `
|
|
531
|
+
<svg viewBox="0 0 60 20" class="w-10 h-3">
|
|
532
|
+
<line x1="23" y1="10" x2="37" y2="10" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>
|
|
533
|
+
</svg>
|
|
534
|
+
`
|
|
535
|
+
return
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
// 16. ELASTIC BLINKING STATE (Squash down to thin sleek slit)
|
|
539
|
+
if (s.isBlinking) {
|
|
540
|
+
const blinkEye = `
|
|
541
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
542
|
+
<line x1="8" y1="20" x2="32" y2="20" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
543
|
+
</svg>
|
|
544
|
+
`
|
|
545
|
+
leftEye.innerHTML = blinkEye
|
|
546
|
+
rightEye.innerHTML = blinkEye
|
|
547
|
+
} else if (effectiveStatus === 'speaking' || s.intensity > 0.05) {
|
|
548
|
+
// 17. SPEAKING STATE: Eyes locked forward with dead-center pupil (X:20, Y:17)
|
|
549
|
+
const speakPupilR = 3.8 + Math.min(1.0, s.intensity * 1.5)
|
|
550
|
+
const speakEye = `
|
|
551
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
552
|
+
<path d="M 12 13 C 12 6.5, 28 6.5, 28 13 C 28 27.5, 12 27.5, 12 13 Z" fill="${color}"/>
|
|
553
|
+
<circle cx="20" cy="17" r="${speakPupilR}" fill="#ffffff" opacity="0.98"/>
|
|
554
|
+
</svg>
|
|
555
|
+
`
|
|
556
|
+
leftEye.innerHTML = speakEye
|
|
557
|
+
rightEye.innerHTML = speakEye
|
|
558
|
+
} else {
|
|
559
|
+
// 18. MOOD SPECIFIC DIGITAL EYES
|
|
560
|
+
let leftEyeSvg = ''
|
|
561
|
+
let rightEyeSvg = ''
|
|
562
|
+
|
|
563
|
+
switch (s.mood) {
|
|
564
|
+
case 'joy':
|
|
565
|
+
leftEyeSvg = `
|
|
566
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
567
|
+
<path d="M 8 26 Q 20 8 32 26" fill="none" stroke="${color}" stroke-width="5" stroke-linecap="round"/>
|
|
568
|
+
</svg>
|
|
569
|
+
`
|
|
570
|
+
rightEyeSvg = leftEyeSvg
|
|
571
|
+
break
|
|
572
|
+
case 'anger':
|
|
573
|
+
leftEyeSvg = `
|
|
574
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
575
|
+
<path d="M 6 12 L 34 22 L 30 30 L 6 18 Z" fill="${color}"/>
|
|
576
|
+
</svg>
|
|
577
|
+
`
|
|
578
|
+
rightEyeSvg = `
|
|
579
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
580
|
+
<path d="M 34 12 L 6 22 L 10 30 L 34 18 Z" fill="${color}"/>
|
|
581
|
+
</svg>
|
|
582
|
+
`
|
|
583
|
+
break
|
|
584
|
+
case 'sadness':
|
|
585
|
+
leftEyeSvg = `
|
|
586
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
587
|
+
<path d="M 8 16 Q 20 28 32 16" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
588
|
+
<circle cx="20" cy="30" r="3.5" fill="#38bdf8"/>
|
|
589
|
+
</svg>
|
|
590
|
+
`
|
|
591
|
+
rightEyeSvg = `
|
|
592
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
593
|
+
<path d="M 8 16 Q 20 28 32 16" fill="none" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
594
|
+
<circle cx="20" cy="30" r="3.5" fill="#38bdf8"/>
|
|
595
|
+
</svg>
|
|
596
|
+
`
|
|
597
|
+
break
|
|
598
|
+
case 'fear':
|
|
599
|
+
leftEyeSvg = `
|
|
600
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
601
|
+
<circle cx="20" cy="20" r="13" fill="none" stroke="${color}" stroke-width="4"/>
|
|
602
|
+
<circle cx="20" cy="20" r="4.5" fill="${color}"/>
|
|
603
|
+
</svg>
|
|
604
|
+
`
|
|
605
|
+
rightEyeSvg = leftEyeSvg
|
|
606
|
+
break
|
|
607
|
+
case 'anxiety':
|
|
608
|
+
leftEyeSvg = `
|
|
609
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
610
|
+
<path d="M 20 20 A 4 4 0 0 1 24 20 A 8 8 0 0 1 16 20 A 12 12 0 0 1 28 20" fill="none" stroke="${color}" stroke-width="3.5" stroke-linecap="round"/>
|
|
611
|
+
</svg>
|
|
612
|
+
`
|
|
613
|
+
rightEyeSvg = leftEyeSvg
|
|
614
|
+
break
|
|
615
|
+
case 'envy':
|
|
616
|
+
leftEyeSvg = `
|
|
617
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
618
|
+
<path d="M 6 18 Q 20 12 34 20 Q 20 28 6 18 Z" fill="${color}" opacity="0.4"/>
|
|
619
|
+
<line x1="18" y1="12" x2="22" y2="28" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
620
|
+
</svg>
|
|
621
|
+
`
|
|
622
|
+
rightEyeSvg = `
|
|
623
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
624
|
+
<path d="M 6 20 Q 20 12 34 18 Q 20 28 6 20 Z" fill="${color}" opacity="0.4"/>
|
|
625
|
+
<line x1="22" y1="12" x2="18" y2="28" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
626
|
+
</svg>
|
|
627
|
+
`
|
|
628
|
+
break
|
|
629
|
+
case 'embarrassment':
|
|
630
|
+
leftEyeSvg = `
|
|
631
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
632
|
+
<path d="M 12 14 L 28 22 L 12 30" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
633
|
+
</svg>
|
|
634
|
+
`
|
|
635
|
+
rightEyeSvg = `
|
|
636
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
637
|
+
<path d="M 28 14 L 12 22 L 28 30" fill="none" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
638
|
+
</svg>
|
|
639
|
+
`
|
|
640
|
+
break
|
|
641
|
+
case 'ennui':
|
|
642
|
+
leftEyeSvg = `
|
|
643
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
644
|
+
<line x1="8" y1="18" x2="32" y2="18" stroke="${color}" stroke-width="4.5" stroke-linecap="round"/>
|
|
645
|
+
<circle cx="20" cy="24" r="5" fill="${color}"/>
|
|
646
|
+
</svg>
|
|
647
|
+
`
|
|
648
|
+
rightEyeSvg = leftEyeSvg
|
|
649
|
+
break
|
|
650
|
+
case 'disgust':
|
|
651
|
+
leftEyeSvg = `
|
|
652
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
653
|
+
<line x1="10" y1="20" x2="30" y2="16" stroke="${color}" stroke-width="4" stroke-linecap="round"/>
|
|
654
|
+
</svg>
|
|
655
|
+
`
|
|
656
|
+
rightEyeSvg = `
|
|
657
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
658
|
+
<circle cx="20" cy="20" r="10" fill="none" stroke="${color}" stroke-width="3.5"/>
|
|
659
|
+
<circle cx="20" cy="20" r="4" fill="${color}"/>
|
|
660
|
+
</svg>
|
|
661
|
+
`
|
|
662
|
+
break
|
|
663
|
+
default:
|
|
664
|
+
leftEyeSvg = `
|
|
665
|
+
<svg viewBox="0 0 40 40" class="w-full h-full">
|
|
666
|
+
<path d="M 12 13 C 12 6.5, 28 6.5, 28 13 C 28 27.5, 12 27.5, 12 13 Z" fill="${color}"/>
|
|
667
|
+
<circle cx="20" cy="17" r="3.6" fill="#ffffff" opacity="0.95"/>
|
|
668
|
+
</svg>
|
|
669
|
+
`
|
|
670
|
+
rightEyeSvg = leftEyeSvg
|
|
671
|
+
break
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
leftEye.innerHTML = leftEyeSvg
|
|
675
|
+
rightEye.innerHTML = rightEyeSvg
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// 19. MOUTH / LIP-FLAP
|
|
679
|
+
if (effectiveStatus === 'speaking' || s.intensity > 0.05) {
|
|
680
|
+
const scale = Math.max(0.2, s.intensity)
|
|
681
|
+
const openH = Math.round(5 + scale * 16)
|
|
682
|
+
const openW = Math.round(12 + scale * 20)
|
|
683
|
+
|
|
684
|
+
mouth.innerHTML = `
|
|
685
|
+
<svg viewBox="0 0 60 30" class="w-14 h-6">
|
|
686
|
+
<ellipse cx="30" cy="15" rx="${openW / 2}" ry="${openH / 2}" fill="${color}" opacity="0.3"/>
|
|
687
|
+
<ellipse cx="30" cy="15" rx="${Math.max(2, openW / 2 - 2)}" ry="${Math.max(2, openH / 2 - 2)}" fill="#ffffff"/>
|
|
688
|
+
</svg>
|
|
689
|
+
`
|
|
690
|
+
} else if (effectiveStatus === 'thinking') {
|
|
691
|
+
mouth.innerHTML = `
|
|
692
|
+
<div class="flex items-center gap-1.5 py-1">
|
|
693
|
+
<span class="w-1.5 h-1.5 rounded-full bg-[#1fb854] animate-ping"></span>
|
|
694
|
+
<span class="w-1.5 h-1.5 rounded-full bg-[#1fb854] animate-ping [animation-delay:0.2s]"></span>
|
|
695
|
+
<span class="w-1.5 h-1.5 rounded-full bg-[#1fb854] animate-ping [animation-delay:0.4s]"></span>
|
|
696
|
+
</div>
|
|
697
|
+
`
|
|
698
|
+
} else {
|
|
699
|
+
let mouthSvg = ''
|
|
700
|
+
switch (s.mood) {
|
|
701
|
+
case 'joy':
|
|
702
|
+
mouthSvg = `<path d="M 20 8 Q 30 20 40 8" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`
|
|
703
|
+
break
|
|
704
|
+
case 'anger':
|
|
705
|
+
mouthSvg = `<path d="M 20 18 Q 30 10 40 18" fill="none" stroke="${color}" stroke-width="3" stroke-linecap="round"/>`
|
|
706
|
+
break
|
|
707
|
+
case 'sadness':
|
|
708
|
+
mouthSvg = `<path d="M 22 18 Q 30 10 38 18" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
|
|
709
|
+
break
|
|
710
|
+
case 'anxiety':
|
|
711
|
+
mouthSvg = `<path d="M 20 14 Q 25 10 30 14 Q 35 18 40 14" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
|
|
712
|
+
break
|
|
713
|
+
case 'envy':
|
|
714
|
+
mouthSvg = `<path d="M 24 16 Q 32 18 40 12" fill="none" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
|
|
715
|
+
break
|
|
716
|
+
default:
|
|
717
|
+
mouthSvg = `<line x1="24" y1="14" x2="36" y2="14" stroke="${color}" stroke-width="2.5" stroke-linecap="round"/>`
|
|
718
|
+
break
|
|
719
|
+
}
|
|
720
|
+
mouth.innerHTML = `
|
|
721
|
+
<svg viewBox="0 0 60 26" class="w-12 h-4">
|
|
722
|
+
${mouthSvg}
|
|
723
|
+
</svg>
|
|
724
|
+
`
|
|
725
|
+
}
|
|
726
|
+
}, [])
|
|
727
|
+
|
|
728
|
+
// Update squash & stretch scales on eye boxes
|
|
729
|
+
const updateEyeMesh = useCallback(() => {
|
|
730
|
+
const s = stateRef.current
|
|
731
|
+
if (leftEyeRef.current) {
|
|
732
|
+
leftEyeRef.current.style.transform = `scale(${s.eyeScaleX}, ${s.eyeScaleY})`
|
|
733
|
+
}
|
|
734
|
+
if (rightEyeRef.current) {
|
|
735
|
+
rightEyeRef.current.style.transform = `scale(${s.eyeScaleX}, ${s.eyeScaleY})`
|
|
736
|
+
}
|
|
737
|
+
}, [])
|
|
738
|
+
|
|
739
|
+
// Sync props to ref
|
|
740
|
+
useEffect(() => {
|
|
741
|
+
if (status === 'listening' && prevStatusRef.current !== 'listening') {
|
|
742
|
+
// Pick random side: 1 for right, -1 for left
|
|
743
|
+
stateRef.current.listeningSide = Math.random() > 0.5 ? 1 : -1
|
|
744
|
+
}
|
|
745
|
+
prevStatusRef.current = status
|
|
746
|
+
|
|
747
|
+
stateRef.current.status = status
|
|
748
|
+
stateRef.current.mood = mood
|
|
749
|
+
stateRef.current.intensity = intensity
|
|
750
|
+
|
|
751
|
+
const isAwareness = status === 'awareness' || status === 'nudge'
|
|
752
|
+
stateRef.current.isSeeingDesktop = isAwareness
|
|
753
|
+
|
|
754
|
+
if (desktopReflectionRef.current) {
|
|
755
|
+
desktopReflectionRef.current.style.opacity = isAwareness ? '1' : '0'
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
if (antBulbLeftRef.current && antBulbRightRef.current) {
|
|
759
|
+
if (isAwareness || status === 'listening') {
|
|
760
|
+
antBulbLeftRef.current.classList.add('animate-pulse')
|
|
761
|
+
antBulbRightRef.current.classList.add('animate-pulse')
|
|
762
|
+
} else {
|
|
763
|
+
antBulbLeftRef.current.classList.remove('animate-pulse')
|
|
764
|
+
antBulbRightRef.current.classList.remove('animate-pulse')
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (isAwareness) {
|
|
769
|
+
stateRef.current.antVelLeft = -14
|
|
770
|
+
stateRef.current.antVelRight = 14
|
|
771
|
+
stateRef.current.targetSaccadeX = -6
|
|
772
|
+
stateRef.current.targetSaccadeY = 2
|
|
773
|
+
setTimeout(() => {
|
|
774
|
+
stateRef.current.targetSaccadeX = 6
|
|
775
|
+
}, 900)
|
|
776
|
+
setTimeout(() => {
|
|
777
|
+
stateRef.current.targetSaccadeX = 0
|
|
778
|
+
stateRef.current.targetSaccadeY = 0
|
|
779
|
+
}, 2200)
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Immediate face re-render upon status / mood changes (colors applied strictly to face)
|
|
783
|
+
renderFace()
|
|
784
|
+
}, [status, mood, renderFace])
|
|
785
|
+
|
|
786
|
+
// Direct high-speed intensity listener (avoids parent React re-renders)
|
|
787
|
+
useEffect(() => {
|
|
788
|
+
const handleIntensityEvent = (e) => {
|
|
789
|
+
stateRef.current.intensity = typeof e.detail === 'number' ? e.detail : 0
|
|
790
|
+
}
|
|
791
|
+
window.addEventListener('mark-intensity', handleIntensityEvent)
|
|
792
|
+
return () => window.removeEventListener('mark-intensity', handleIntensityEvent)
|
|
793
|
+
}, [])
|
|
794
|
+
|
|
795
|
+
// Listen for sleep state from useAwareness
|
|
796
|
+
useEffect(() => {
|
|
797
|
+
const handleSleepingEvent = (e) => {
|
|
798
|
+
const sleeping = Boolean(e.detail?.isSleeping)
|
|
799
|
+
stateRef.current.isSleeping = sleeping
|
|
800
|
+
renderFace()
|
|
801
|
+
}
|
|
802
|
+
window.addEventListener('mark:sleeping', handleSleepingEvent)
|
|
803
|
+
return () => window.removeEventListener('mark:sleeping', handleSleepingEvent)
|
|
804
|
+
}, [renderFace])
|
|
805
|
+
|
|
806
|
+
// Blinking loop with natural squash & stretch
|
|
807
|
+
useEffect(() => {
|
|
808
|
+
let blinkTimer = null
|
|
809
|
+
|
|
810
|
+
const triggerBlink = () => {
|
|
811
|
+
const s = stateRef.current
|
|
812
|
+
if (s.isSleeping || s.status === 'sleeping') {
|
|
813
|
+
blinkTimer = setTimeout(triggerBlink, 3000)
|
|
814
|
+
return
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
s.isBlinking = true
|
|
818
|
+
s.eyeScaleX = 1.15
|
|
819
|
+
s.eyeScaleY = 0.15
|
|
820
|
+
updateEyeMesh()
|
|
821
|
+
renderFace()
|
|
822
|
+
|
|
823
|
+
setTimeout(() => {
|
|
824
|
+
s.isBlinking = false
|
|
825
|
+
s.eyeScaleX = 0.95
|
|
826
|
+
s.eyeScaleY = 1.1
|
|
827
|
+
updateEyeMesh()
|
|
828
|
+
renderFace()
|
|
829
|
+
|
|
830
|
+
setTimeout(() => {
|
|
831
|
+
s.eyeScaleX = 1
|
|
832
|
+
s.eyeScaleY = 1
|
|
833
|
+
updateEyeMesh()
|
|
834
|
+
}, 80)
|
|
835
|
+
|
|
836
|
+
// 20% chance of natural double-blink
|
|
837
|
+
if (Math.random() < 0.2) {
|
|
838
|
+
setTimeout(() => {
|
|
839
|
+
s.isBlinking = true
|
|
840
|
+
s.eyeScaleY = 0.15
|
|
841
|
+
updateEyeMesh()
|
|
842
|
+
renderFace()
|
|
843
|
+
setTimeout(() => {
|
|
844
|
+
s.isBlinking = false
|
|
845
|
+
s.eyeScaleY = 1
|
|
846
|
+
updateEyeMesh()
|
|
847
|
+
renderFace()
|
|
848
|
+
}, 90)
|
|
849
|
+
}, 110)
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
const nextDelay = 2600 + Math.random() * 3200
|
|
853
|
+
blinkTimer = setTimeout(triggerBlink, nextDelay)
|
|
854
|
+
}, 120)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
blinkTimer = setTimeout(triggerBlink, 1500)
|
|
858
|
+
return () => clearTimeout(blinkTimer)
|
|
859
|
+
}, [renderFace, updateEyeMesh])
|
|
860
|
+
|
|
861
|
+
// Autonomous quirks loop (tilt, nod, stretch, glance)
|
|
862
|
+
const updateAutonomousQuirks = useCallback((now) => {
|
|
863
|
+
const s = stateRef.current
|
|
864
|
+
if (
|
|
865
|
+
s.tactileState ||
|
|
866
|
+
s.isDragging ||
|
|
867
|
+
s.dropAnim.active ||
|
|
868
|
+
s.isSeeingDesktop ||
|
|
869
|
+
s.status === 'speaking' ||
|
|
870
|
+
s.status === 'listening' ||
|
|
871
|
+
s.status === 'hearing' ||
|
|
872
|
+
s.status === 'music_groove' ||
|
|
873
|
+
s.isSleeping ||
|
|
874
|
+
s.status === 'sleeping'
|
|
875
|
+
) {
|
|
876
|
+
s.targetRotY = 0
|
|
877
|
+
s.targetRotZ = 0
|
|
878
|
+
s.targetSaccadeX = 0
|
|
879
|
+
s.targetSaccadeY = 0
|
|
880
|
+
s.quirkPitchOffset = 0
|
|
881
|
+
s.quirkRollOffset = 0
|
|
882
|
+
s.targetAltitudeOffset = 0
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
if (now - s.lastQuirkTime >= s.nextQuirkInterval) {
|
|
887
|
+
s.lastQuirkTime = now
|
|
888
|
+
s.nextQuirkInterval = 2000 + Math.random() * 1200
|
|
889
|
+
|
|
890
|
+
const roll = Math.random()
|
|
891
|
+
// 1. Curious Head Tilt (Memiringkan kepala penasaran)
|
|
892
|
+
if (roll < 0.3) {
|
|
893
|
+
s.quirkRollOffset = Math.random() > 0.5 ? 10 : -10
|
|
894
|
+
s.quirkPitchOffset = -4
|
|
895
|
+
s.targetRotY = (Math.random() - 0.5) * 8
|
|
896
|
+
s.targetSaccadeX = s.quirkRollOffset * 0.4
|
|
897
|
+
setTimeout(() => {
|
|
898
|
+
s.quirkRollOffset = 0
|
|
899
|
+
s.quirkPitchOffset = 0
|
|
900
|
+
s.targetRotY = 0
|
|
901
|
+
s.targetSaccadeX = 0
|
|
902
|
+
}, 1200)
|
|
903
|
+
}
|
|
904
|
+
// 2. Subtle Listening Nod (Mengangguk kecil menyimak)
|
|
905
|
+
else if (roll < 0.55) {
|
|
906
|
+
s.quirkPitchOffset = -8
|
|
907
|
+
s.quirkRollOffset = 0
|
|
908
|
+
setTimeout(() => {
|
|
909
|
+
s.quirkPitchOffset = 0
|
|
910
|
+
}, 400)
|
|
911
|
+
}
|
|
912
|
+
// 3. Floating Stretch Breath (Helaan nafas apung halus)
|
|
913
|
+
else if (roll < 0.75) {
|
|
914
|
+
s.targetAltitudeOffset = -10
|
|
915
|
+
s.quirkPitchOffset = 3
|
|
916
|
+
setTimeout(() => {
|
|
917
|
+
s.targetAltitudeOffset = 0
|
|
918
|
+
s.quirkPitchOffset = 0
|
|
919
|
+
}, 1000)
|
|
920
|
+
}
|
|
921
|
+
// 4. Casual Glance Around (Menoleh santai)
|
|
922
|
+
else {
|
|
923
|
+
s.targetRotY = Math.random() > 0.5 ? 12 : -12
|
|
924
|
+
s.quirkPitchOffset = -2
|
|
925
|
+
s.quirkRollOffset = (Math.random() - 0.5) * 4
|
|
926
|
+
s.targetSaccadeX = s.targetRotY * 0.35
|
|
927
|
+
setTimeout(() => {
|
|
928
|
+
s.targetRotY = 0
|
|
929
|
+
s.targetSaccadeX = 0
|
|
930
|
+
s.quirkPitchOffset = 0
|
|
931
|
+
s.quirkRollOffset = 0
|
|
932
|
+
}, 1400)
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
}, [])
|
|
936
|
+
|
|
937
|
+
// Interactive tactile state setter
|
|
938
|
+
const setTactile = useCallback(
|
|
939
|
+
(newState, durationMs = 1200) => {
|
|
940
|
+
const s = stateRef.current
|
|
941
|
+
if (s.tactileTimer) clearTimeout(s.tactileTimer)
|
|
942
|
+
s.tactileState = newState
|
|
943
|
+
renderFace()
|
|
944
|
+
if (durationMs > 0) {
|
|
945
|
+
s.tactileTimer = setTimeout(() => {
|
|
946
|
+
s.tactileState = null
|
|
947
|
+
renderFace()
|
|
948
|
+
}, durationMs)
|
|
949
|
+
}
|
|
950
|
+
},
|
|
951
|
+
[renderFace]
|
|
952
|
+
)
|
|
953
|
+
|
|
954
|
+
// Pointer Interaction Engine (Down, Move, Up, Cancel)
|
|
955
|
+
const handlePointerUp = useCallback(
|
|
956
|
+
(e) => {
|
|
957
|
+
const s = stateRef.current
|
|
958
|
+
if (!s.isPointerDown) return
|
|
959
|
+
s.isPointerDown = false
|
|
960
|
+
|
|
961
|
+
try {
|
|
962
|
+
if (e.currentTarget?.hasPointerCapture?.(e.pointerId)) {
|
|
963
|
+
e.currentTarget.releasePointerCapture(e.pointerId)
|
|
964
|
+
}
|
|
965
|
+
} catch (_) {}
|
|
966
|
+
|
|
967
|
+
const totalDist = Math.hypot(e.clientX - s.pointerDownPos.x, e.clientY - s.pointerDownPos.y)
|
|
968
|
+
|
|
969
|
+
// 1. Release from vertical lift (drop straight down with physics)
|
|
970
|
+
if (s.isDragging) {
|
|
971
|
+
s.isDragging = false
|
|
972
|
+
s.dropAnim.active = true
|
|
973
|
+
s.dropAnim.y = s.dragOffset.y
|
|
974
|
+
s.dropAnim.velY = Math.min(60, Math.max(-100, s.dragVelocity.y))
|
|
975
|
+
s.dropAnim.squashX = 1
|
|
976
|
+
s.dropAnim.squashY = 1
|
|
977
|
+
s.dragOffset.x = 0
|
|
978
|
+
s.dragOffset.y = 0
|
|
979
|
+
s.tactileState = 'drop_bounce'
|
|
980
|
+
renderFace()
|
|
981
|
+
if (typeof onInteract === 'function') onInteract('drop_bounce')
|
|
982
|
+
return
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// 2. Finish pet or tickle
|
|
986
|
+
if (s.tactileState === 'pet_enjoy' || s.tactileState === 'pet_purr') {
|
|
987
|
+
setTimeout(() => {
|
|
988
|
+
if (s.tactileState === 'pet_enjoy' || s.tactileState === 'pet_purr') {
|
|
989
|
+
s.tactileState = null
|
|
990
|
+
renderFace()
|
|
991
|
+
}
|
|
992
|
+
}, 1600)
|
|
993
|
+
return
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
if (s.tactileState === 'tickle') {
|
|
997
|
+
setTimeout(() => {
|
|
998
|
+
if (s.tactileState === 'tickle') {
|
|
999
|
+
s.tactileState = null
|
|
1000
|
+
renderFace()
|
|
1001
|
+
}
|
|
1002
|
+
}, 1200)
|
|
1003
|
+
return
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// 3. Short Click / Bonk Combo on Head / Visor (totalDist < 20)
|
|
1007
|
+
if (totalDist < 20) {
|
|
1008
|
+
const zone = s.pointerDownZone
|
|
1009
|
+
if (zone === 'head' || zone === 'visor') {
|
|
1010
|
+
const now = performance.now()
|
|
1011
|
+
if (now - s.lastBonkTime < 2400) {
|
|
1012
|
+
s.bonkCount++
|
|
1013
|
+
} else {
|
|
1014
|
+
s.bonkCount = 1
|
|
1015
|
+
}
|
|
1016
|
+
s.lastBonkTime = now
|
|
1017
|
+
|
|
1018
|
+
if (s.bonkCount <= 3) {
|
|
1019
|
+
// Stage 1: Gentle Bonk (Ketukan 1-3x)
|
|
1020
|
+
s.pokeVelocityY = 160
|
|
1021
|
+
s.velRotX = 60
|
|
1022
|
+
s.antVelLeft = 40
|
|
1023
|
+
s.antVelRight = -40
|
|
1024
|
+
setTactile('bonk_1', 1200)
|
|
1025
|
+
if (typeof onInteract === 'function') onInteract('bonk_1', { count: s.bonkCount })
|
|
1026
|
+
} else if (s.bonkCount <= 6) {
|
|
1027
|
+
// Stage 2: Annoyed Bonk (Ketukan 4-6x)
|
|
1028
|
+
s.pokeVelocityY = 200
|
|
1029
|
+
s.velRotX = 80
|
|
1030
|
+
s.velRotY = (Math.random() - 0.5) * 50
|
|
1031
|
+
setTactile('bonk_annoyed', 1600)
|
|
1032
|
+
if (typeof onInteract === 'function') onInteract('bonk_annoyed', { count: s.bonkCount })
|
|
1033
|
+
} else if (s.bonkCount <= 9) {
|
|
1034
|
+
// Stage 3: Dizzy Bonk (Ketukan 7-9x)
|
|
1035
|
+
s.pokeVelocityY = 140
|
|
1036
|
+
s.dizzyAngle = 0
|
|
1037
|
+
setTactile('bonk_dizzy', 2800)
|
|
1038
|
+
if (typeof onInteract === 'function') onInteract('bonk_dizzy', { count: s.bonkCount })
|
|
1039
|
+
} else {
|
|
1040
|
+
// Stage 4: Shield Block (Ketukan 10x+)
|
|
1041
|
+
setTactile('shield_block', 3200)
|
|
1042
|
+
if (typeof onInteract === 'function') onInteract('shield_block', { count: s.bonkCount })
|
|
1043
|
+
}
|
|
1044
|
+
} else if (zone === 'body' || zone === 'belly') {
|
|
1045
|
+
s.pokeVelocityY = 140
|
|
1046
|
+
s.velRotY += (Math.random() - 0.5) * 14
|
|
1047
|
+
if (typeof onInteract === 'function') onInteract('poke_body')
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
if (typeof onClick === 'function') {
|
|
1051
|
+
onClick()
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
},
|
|
1055
|
+
[onClick, onInteract, renderFace, setTactile]
|
|
1056
|
+
)
|
|
1057
|
+
|
|
1058
|
+
const handlePointerDown = useCallback(
|
|
1059
|
+
(e) => {
|
|
1060
|
+
const s = stateRef.current
|
|
1061
|
+
const zoneEl = e.target.closest('[data-zone]')
|
|
1062
|
+
const zone = zoneEl?.dataset?.zone || (e.clientY < 180 ? 'head' : 'body')
|
|
1063
|
+
|
|
1064
|
+
try {
|
|
1065
|
+
e.currentTarget.setPointerCapture(e.pointerId)
|
|
1066
|
+
} catch (_) {}
|
|
1067
|
+
|
|
1068
|
+
s.isPointerDown = true
|
|
1069
|
+
s.isDragging = false
|
|
1070
|
+
s.dragOffset.x = 0
|
|
1071
|
+
s.dragOffset.y = 0
|
|
1072
|
+
s.pointerDownZone = zone
|
|
1073
|
+
s.pointerDownPos = { x: e.clientX, y: e.clientY }
|
|
1074
|
+
s.lastPointerPos = { x: e.clientX, y: e.clientY }
|
|
1075
|
+
s.pointerDownTime = performance.now()
|
|
1076
|
+
s.petAccumulator = 0
|
|
1077
|
+
s.petStartTime = performance.now()
|
|
1078
|
+
s.tickleAccumulator = 0
|
|
1079
|
+
|
|
1080
|
+
// If Mark was sleeping, wake up with startle!
|
|
1081
|
+
if (s.isSleeping || s.status === 'sleeping') {
|
|
1082
|
+
s.isSleeping = false
|
|
1083
|
+
s.pokeVelocityY = -36
|
|
1084
|
+
s.antVelLeft = -35
|
|
1085
|
+
s.antVelRight = 35
|
|
1086
|
+
setTactile('startle_wake', 1600)
|
|
1087
|
+
if (typeof onInteract === 'function') onInteract('startle_wake')
|
|
1088
|
+
return
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Zone-specific immediate tactile reactions
|
|
1092
|
+
if (zone === 'eye-left') {
|
|
1093
|
+
s.rotX = -12
|
|
1094
|
+
s.rotY = -8
|
|
1095
|
+
s.velRotX = -20
|
|
1096
|
+
setTactile('poke_eye_l', 1200)
|
|
1097
|
+
if (typeof onInteract === 'function') onInteract('poke_eye_l')
|
|
1098
|
+
} else if (zone === 'eye-right') {
|
|
1099
|
+
s.rotX = -12
|
|
1100
|
+
s.rotY = 8
|
|
1101
|
+
s.velRotX = -20
|
|
1102
|
+
setTactile('poke_eye_r', 1200)
|
|
1103
|
+
if (typeof onInteract === 'function') onInteract('poke_eye_r')
|
|
1104
|
+
} else if (zone === 'antenna-left') {
|
|
1105
|
+
s.antVelLeft = -80
|
|
1106
|
+
s.velRotZ = -10
|
|
1107
|
+
setTactile('poke_antenna', 1200)
|
|
1108
|
+
if (typeof onInteract === 'function') onInteract('poke_antenna_l')
|
|
1109
|
+
} else if (zone === 'antenna-right') {
|
|
1110
|
+
s.antVelRight = 80
|
|
1111
|
+
s.velRotZ = 10
|
|
1112
|
+
setTactile('poke_antenna', 1200)
|
|
1113
|
+
if (typeof onInteract === 'function') onInteract('poke_antenna_r')
|
|
1114
|
+
} else if (zone === 'core') {
|
|
1115
|
+
s.pokeVelocityY = -14
|
|
1116
|
+
s.corePulseScale = 2.4
|
|
1117
|
+
setTactile('poke_core', 1400)
|
|
1118
|
+
if (typeof onInteract === 'function') onInteract('poke_core')
|
|
1119
|
+
}
|
|
1120
|
+
},
|
|
1121
|
+
[onInteract, setTactile]
|
|
1122
|
+
)
|
|
1123
|
+
|
|
1124
|
+
const handlePointerMove = useCallback(
|
|
1125
|
+
(e) => {
|
|
1126
|
+
const s = stateRef.current
|
|
1127
|
+
if (!s.isPointerDown) return
|
|
1128
|
+
|
|
1129
|
+
// Safety check: if mouse button is not held, release immediately to prevent stuck drag
|
|
1130
|
+
if (e.buttons === 0) {
|
|
1131
|
+
handlePointerUp(e)
|
|
1132
|
+
return
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
const dx = e.clientX - s.pointerDownPos.x
|
|
1136
|
+
const dy = e.clientY - s.pointerDownPos.y
|
|
1137
|
+
const stepDx = e.clientX - s.lastPointerPos.x
|
|
1138
|
+
const stepDy = e.clientY - s.lastPointerPos.y
|
|
1139
|
+
const stepDist = Math.hypot(stepDx, stepDy)
|
|
1140
|
+
s.lastPointerPos = { x: e.clientX, y: e.clientY }
|
|
1141
|
+
|
|
1142
|
+
const zone = s.pointerDownZone
|
|
1143
|
+
|
|
1144
|
+
// 1. Stroke / Petting detection on Head or Visor (only when NOT lifting)
|
|
1145
|
+
if (!s.isDragging && (zone === 'head' || zone === 'visor')) {
|
|
1146
|
+
if (Math.abs(stepDx) > 1.2 && Math.abs(dy) < 50) {
|
|
1147
|
+
s.petAccumulator += stepDist
|
|
1148
|
+
const petDuration = performance.now() - s.petStartTime
|
|
1149
|
+
if (s.petAccumulator > 70) {
|
|
1150
|
+
if (s.tactileTimer) clearTimeout(s.tactileTimer)
|
|
1151
|
+
if (petDuration > 2200) {
|
|
1152
|
+
if (s.tactileState !== 'pet_purr') {
|
|
1153
|
+
s.tactileState = 'pet_purr'
|
|
1154
|
+
renderFace()
|
|
1155
|
+
if (typeof onInteract === 'function') onInteract('pet_purr')
|
|
1156
|
+
}
|
|
1157
|
+
} else {
|
|
1158
|
+
if (s.tactileState !== 'pet_enjoy') {
|
|
1159
|
+
s.tactileState = 'pet_enjoy'
|
|
1160
|
+
renderFace()
|
|
1161
|
+
if (typeof onInteract === 'function') onInteract('petting')
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
s.targetRotZ = stepDx * 0.4
|
|
1165
|
+
s.targetRotX = 4
|
|
1166
|
+
return
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
|
|
1171
|
+
// 2. Tickle detection on Belly / Body (only when NOT lifting)
|
|
1172
|
+
if (!s.isDragging && (zone === 'belly' || zone === 'body')) {
|
|
1173
|
+
if (stepDist > 2.5 && Math.hypot(dx, dy) < 40) {
|
|
1174
|
+
s.tickleAccumulator += stepDist
|
|
1175
|
+
if (s.tickleAccumulator > 85) {
|
|
1176
|
+
if (s.tactileState !== 'tickle') {
|
|
1177
|
+
s.tactileState = 'tickle'
|
|
1178
|
+
renderFace()
|
|
1179
|
+
if (typeof onInteract === 'function') onInteract('tickle')
|
|
1180
|
+
}
|
|
1181
|
+
s.velRotY += (Math.random() - 0.5) * 22
|
|
1182
|
+
return
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
// 3. Vertical Lift only: Dragging UPWARDS (dy < -25) and dominantly vertical
|
|
1188
|
+
if (
|
|
1189
|
+
!s.isDragging &&
|
|
1190
|
+
dy < -25 &&
|
|
1191
|
+
Math.abs(dy) > Math.abs(dx) * 1.1 &&
|
|
1192
|
+
s.tactileState !== 'pet_enjoy' &&
|
|
1193
|
+
s.tactileState !== 'pet_purr' &&
|
|
1194
|
+
s.tactileState !== 'tickle'
|
|
1195
|
+
) {
|
|
1196
|
+
s.isDragging = true
|
|
1197
|
+
s.tactileState = 'drag_lift'
|
|
1198
|
+
renderFace()
|
|
1199
|
+
if (typeof onInteract === 'function') onInteract('drag_lift')
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
if (s.isDragging) {
|
|
1203
|
+
// Lock X strictly to 0 so avatar NEVER moves sideways or flies away
|
|
1204
|
+
s.dragOffset.x = 0
|
|
1205
|
+
// Clamp vertical lift between -140px and 0px
|
|
1206
|
+
s.dragOffset.y = Math.max(-140, Math.min(0, dy))
|
|
1207
|
+
s.dragVelocity.x = 0
|
|
1208
|
+
s.dragVelocity.y = Math.max(-400, Math.min(400, (stepDy / 0.016) * 0.2))
|
|
1209
|
+
}
|
|
1210
|
+
},
|
|
1211
|
+
[handlePointerUp, onInteract, renderFace]
|
|
1212
|
+
)
|
|
1213
|
+
|
|
1214
|
+
const handlePointerCancel = useCallback(
|
|
1215
|
+
(e) => {
|
|
1216
|
+
handlePointerUp(e)
|
|
1217
|
+
},
|
|
1218
|
+
[handlePointerUp]
|
|
1219
|
+
)
|
|
1220
|
+
|
|
1221
|
+
// Global window pointer listener to guarantee release is never lost
|
|
1222
|
+
useEffect(() => {
|
|
1223
|
+
const handleGlobalUp = (e) => {
|
|
1224
|
+
if (stateRef.current.isPointerDown) {
|
|
1225
|
+
handlePointerUp(e)
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
window.addEventListener('pointerup', handleGlobalUp)
|
|
1229
|
+
window.addEventListener('pointercancel', handleGlobalUp)
|
|
1230
|
+
return () => {
|
|
1231
|
+
window.removeEventListener('pointerup', handleGlobalUp)
|
|
1232
|
+
window.removeEventListener('pointercancel', handleGlobalUp)
|
|
1233
|
+
}
|
|
1234
|
+
}, [handlePointerUp])
|
|
1235
|
+
|
|
1236
|
+
// Main 60 FPS requestAnimationFrame physics ticker
|
|
1237
|
+
useEffect(() => {
|
|
1238
|
+
let animId
|
|
1239
|
+
let prevTime = performance.now()
|
|
1240
|
+
stateRef.current.lastQuirkTime = performance.now()
|
|
1241
|
+
|
|
1242
|
+
renderFace()
|
|
1243
|
+
|
|
1244
|
+
const updateFrame = (now) => {
|
|
1245
|
+
const dt = Math.min(0.05, (now - prevTime) / 1000)
|
|
1246
|
+
prevTime = now
|
|
1247
|
+
const t = now * 0.001
|
|
1248
|
+
const s = stateRef.current
|
|
1249
|
+
const effectiveStatus = s.tactileState || (s.isSleeping ? 'sleeping' : s.status)
|
|
1250
|
+
|
|
1251
|
+
// 1. Autonomous quirks
|
|
1252
|
+
updateAutonomousQuirks(now)
|
|
1253
|
+
|
|
1254
|
+
// 2. Eye saccades lerp
|
|
1255
|
+
if (s.tactileState === 'bonk_dizzy') {
|
|
1256
|
+
s.targetSaccadeX = Math.sin(t * 10) * 4
|
|
1257
|
+
s.targetSaccadeY = Math.cos(t * 10) * 4
|
|
1258
|
+
} else if (s.status === 'speaking') {
|
|
1259
|
+
s.targetSaccadeX = 0
|
|
1260
|
+
s.targetSaccadeY = 0
|
|
1261
|
+
} else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
|
|
1262
|
+
s.targetSaccadeX = (s.listeningSide || 1) * 3
|
|
1263
|
+
s.targetSaccadeY = -1
|
|
1264
|
+
}
|
|
1265
|
+
const saccadeSpeed = s.status === 'speaking' ? 0.35 : 0.16
|
|
1266
|
+
s.eyeSaccadeX += (s.targetSaccadeX - s.eyeSaccadeX) * saccadeSpeed
|
|
1267
|
+
s.eyeSaccadeY += (s.targetSaccadeY - s.eyeSaccadeY) * saccadeSpeed
|
|
1268
|
+
|
|
1269
|
+
// 3. Multi-Harmonic organic breathing & sway
|
|
1270
|
+
let breathSpeed = 1.6
|
|
1271
|
+
let breathDepth = 7.5
|
|
1272
|
+
let swayDepth = 3.5
|
|
1273
|
+
let rollDepth = 1.0
|
|
1274
|
+
|
|
1275
|
+
if (s.tactileState === 'pet_purr') {
|
|
1276
|
+
breathSpeed = 3.2
|
|
1277
|
+
breathDepth = 3.5
|
|
1278
|
+
swayDepth = 1.0
|
|
1279
|
+
s.purrJitter.x = (Math.random() - 0.5) * 3.0
|
|
1280
|
+
s.purrJitter.y = (Math.random() - 0.5) * 3.0
|
|
1281
|
+
} else if (effectiveStatus === 'sleeping') {
|
|
1282
|
+
breathSpeed = 0.8
|
|
1283
|
+
breathDepth = 4.5
|
|
1284
|
+
swayDepth = 1.2
|
|
1285
|
+
} else if (effectiveStatus === 'speaking') {
|
|
1286
|
+
breathSpeed = 2.2
|
|
1287
|
+
breathDepth = 6.5
|
|
1288
|
+
swayDepth = 0
|
|
1289
|
+
rollDepth = 0
|
|
1290
|
+
} else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
|
|
1291
|
+
breathSpeed = 1.3
|
|
1292
|
+
breathDepth = 5.5
|
|
1293
|
+
swayDepth = 1.2
|
|
1294
|
+
rollDepth = 0.5
|
|
1295
|
+
} else if (effectiveStatus === 'music_groove') {
|
|
1296
|
+
breathSpeed = 2.4
|
|
1297
|
+
breathDepth = 9.0
|
|
1298
|
+
swayDepth = 8.0
|
|
1299
|
+
rollDepth = 2.5
|
|
1300
|
+
} else if (s.isSeeingDesktop) {
|
|
1301
|
+
breathSpeed = 1.8
|
|
1302
|
+
breathDepth = 5
|
|
1303
|
+
} else if (effectiveStatus === 'thinking') {
|
|
1304
|
+
breathSpeed = 1.2
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Continuous phase accumulation
|
|
1308
|
+
s.breathPhase = (s.breathPhase + dt * breathSpeed) % (Math.PI * 2)
|
|
1309
|
+
s.swayPhase = (s.swayPhase + dt * breathSpeed * 0.65) % (Math.PI * 2)
|
|
1310
|
+
|
|
1311
|
+
// Lerp depth amplitudes smoothly
|
|
1312
|
+
s.currentBreathDepth += (breathDepth - s.currentBreathDepth) * dt * 3.5
|
|
1313
|
+
s.currentSwayDepth += (swayDepth - s.currentSwayDepth) * dt * 3.5
|
|
1314
|
+
s.currentRollDepth += (rollDepth - s.currentRollDepth) * dt * 3.5
|
|
1315
|
+
|
|
1316
|
+
const harmonicY = Math.sin(s.breathPhase) * s.currentBreathDepth
|
|
1317
|
+
const harmonicX = Math.cos(s.swayPhase) * s.currentSwayDepth
|
|
1318
|
+
const harmonicRoll = Math.sin(s.breathPhase * 0.5) * s.currentRollDepth
|
|
1319
|
+
|
|
1320
|
+
const breathScaleY = 1 + harmonicY * 0.0016
|
|
1321
|
+
const breathScaleX = 1 - harmonicY * 0.0008
|
|
1322
|
+
|
|
1323
|
+
// Damped quirk altitude offset
|
|
1324
|
+
s.quirkAltitudeOffset += (s.targetAltitudeOffset - s.quirkAltitudeOffset) * dt * 4
|
|
1325
|
+
|
|
1326
|
+
// Poke spring dampening (physically accurate damped harmonic oscillator)
|
|
1327
|
+
const springK = 150
|
|
1328
|
+
const dampC = 14
|
|
1329
|
+
s.pokeVelocityY += (-springK * s.pokeOffsetY - dampC * s.pokeVelocityY) * dt
|
|
1330
|
+
s.pokeOffsetY += s.pokeVelocityY * dt
|
|
1331
|
+
|
|
1332
|
+
// Damped desktop look transitions
|
|
1333
|
+
const targetDesktopForwardZ = s.isSeeingDesktop ? 35 : 0
|
|
1334
|
+
const targetDesktopDownwardY = s.isSeeingDesktop ? -12 : 0
|
|
1335
|
+
s.desktopForwardZ += (targetDesktopForwardZ - s.desktopForwardZ) * dt * 6
|
|
1336
|
+
s.desktopDownwardY += (targetDesktopDownwardY - s.desktopDownwardY) * dt * 6
|
|
1337
|
+
|
|
1338
|
+
// Dizzy spiral rotation
|
|
1339
|
+
if (s.tactileState === 'bonk_dizzy') {
|
|
1340
|
+
s.dizzyAngle = (s.dizzyAngle + dt * 360) % 360
|
|
1341
|
+
renderFace()
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
// Shield progress lerping
|
|
1345
|
+
const targetShield = s.tactileState === 'shield_block' ? 1.0 : 0.0
|
|
1346
|
+
s.shieldProgress += (targetShield - s.shieldProgress) * dt * 10
|
|
1347
|
+
|
|
1348
|
+
// Drop bounce physics (anti-gravity magnetic suspension spring - smooth boing-boing without gepeng deformation)
|
|
1349
|
+
if (s.dropAnim.active) {
|
|
1350
|
+
const springK = 85
|
|
1351
|
+
const dampC = 9
|
|
1352
|
+
const accelY = -springK * s.dropAnim.y - dampC * s.dropAnim.velY
|
|
1353
|
+
s.dropAnim.velY += accelY * dt
|
|
1354
|
+
s.dropAnim.y += s.dropAnim.velY * dt
|
|
1355
|
+
|
|
1356
|
+
// Subtle antenna inertia during drop bounce
|
|
1357
|
+
s.antVelLeft += s.dropAnim.velY * 0.08
|
|
1358
|
+
s.antVelRight -= s.dropAnim.velY * 0.08
|
|
1359
|
+
|
|
1360
|
+
// Check if settled back to hover level
|
|
1361
|
+
if (Math.abs(s.dropAnim.y) < 0.6 && Math.abs(s.dropAnim.velY) < 6) {
|
|
1362
|
+
s.dropAnim.y = 0
|
|
1363
|
+
s.dropAnim.velY = 0
|
|
1364
|
+
s.dropAnim.active = false
|
|
1365
|
+
if (s.tactileState === 'drop_bounce') {
|
|
1366
|
+
s.tactileState = null
|
|
1367
|
+
renderFace()
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1370
|
+
}
|
|
1371
|
+
|
|
1372
|
+
// 4. Spring-damper for head rotation (Pitch, Yaw, Roll)
|
|
1373
|
+
let addPitch = effectiveStatus === 'sleeping' ? 14 : 0
|
|
1374
|
+
let addRoll = effectiveStatus === 'thinking' ? 7 : s.mood === 'joy' ? 3 : 0
|
|
1375
|
+
|
|
1376
|
+
let effectiveTargetX = s.targetRotX + s.quirkPitchOffset + addPitch
|
|
1377
|
+
let effectiveTargetY = s.targetRotY
|
|
1378
|
+
let effectiveTargetZ = s.targetRotZ + s.quirkRollOffset + addRoll
|
|
1379
|
+
|
|
1380
|
+
if (s.tactileState === 'bonk_dizzy') {
|
|
1381
|
+
effectiveTargetX = Math.sin(t * 6) * 12
|
|
1382
|
+
effectiveTargetY = Math.cos(t * 6) * 18
|
|
1383
|
+
effectiveTargetZ = Math.sin(t * 6) * 14
|
|
1384
|
+
} else if (effectiveStatus === 'music_groove') {
|
|
1385
|
+
effectiveTargetX = Math.sin(t * 8) * 10
|
|
1386
|
+
effectiveTargetY = Math.sin(t * 4) * 8
|
|
1387
|
+
effectiveTargetZ = Math.sin(t * 4) * 5
|
|
1388
|
+
} else if (s.isSeeingDesktop) {
|
|
1389
|
+
addPitch = -16
|
|
1390
|
+
effectiveTargetX = -14 + addPitch
|
|
1391
|
+
effectiveTargetY = Math.sin(t * 1.8) * 4
|
|
1392
|
+
effectiveTargetZ = 0
|
|
1393
|
+
} else if (effectiveStatus === 'speaking') {
|
|
1394
|
+
const speakNod = Math.sin(t * 7.5) * (s.intensity * 2.2)
|
|
1395
|
+
addPitch = -1.5 + speakNod
|
|
1396
|
+
effectiveTargetX = addPitch
|
|
1397
|
+
effectiveTargetY = 0
|
|
1398
|
+
effectiveTargetZ = 0
|
|
1399
|
+
} else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
|
|
1400
|
+
const side = s.listeningSide || 1
|
|
1401
|
+
effectiveTargetX = -4
|
|
1402
|
+
effectiveTargetY = side * 20
|
|
1403
|
+
effectiveTargetZ = side * -6
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
// Spring damper calculation
|
|
1407
|
+
s.velRotX += ((effectiveTargetX - s.rotX) * 140 - s.velRotX * 16) * dt
|
|
1408
|
+
s.velRotY += ((effectiveTargetY - s.rotY) * 140 - s.velRotY * 16) * dt
|
|
1409
|
+
s.velRotZ += ((effectiveTargetZ - s.rotZ) * 140 - s.velRotZ * 16) * dt
|
|
1410
|
+
s.rotX += s.velRotX * dt
|
|
1411
|
+
s.rotY += s.velRotY * dt
|
|
1412
|
+
s.rotZ += s.velRotZ * dt
|
|
1413
|
+
|
|
1414
|
+
// 5. Apply Rigid Compound Transforms around Throat Pivot
|
|
1415
|
+
let curHarmonicX = harmonicX
|
|
1416
|
+
let curHarmonicY = harmonicY + s.pokeOffsetY + s.desktopDownwardY + s.quirkAltitudeOffset
|
|
1417
|
+
let scaleX = breathScaleX
|
|
1418
|
+
let scaleY = breathScaleY
|
|
1419
|
+
|
|
1420
|
+
if (s.isDragging) {
|
|
1421
|
+
curHarmonicX = 0
|
|
1422
|
+
curHarmonicY = s.dragOffset.y
|
|
1423
|
+
} else if (s.dropAnim.active) {
|
|
1424
|
+
curHarmonicX = harmonicX
|
|
1425
|
+
curHarmonicY = harmonicY + s.dropAnim.y
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
if (s.tactileState === 'pet_purr') {
|
|
1429
|
+
curHarmonicX += s.purrJitter.x
|
|
1430
|
+
curHarmonicY += s.purrJitter.y
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
if (robotRootRef.current) {
|
|
1434
|
+
robotRootRef.current.style.transform = `translate3d(${curHarmonicX}px, ${curHarmonicY}px, ${s.desktopForwardZ}px) scale3d(${scaleX}, ${scaleY}, 1)`
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
// Head: Rotates smoothly with pitch, yaw, and harmonic roll
|
|
1438
|
+
if (robotHeadRef.current) {
|
|
1439
|
+
robotHeadRef.current.style.transform = `rotateX(${s.rotX}deg) rotateY(${s.rotY}deg) rotateZ(${s.rotZ + harmonicRoll}deg)`
|
|
1440
|
+
}
|
|
1441
|
+
|
|
1442
|
+
// Torso: Follows directly beneath the throat socket
|
|
1443
|
+
if (robotBodyRef.current) {
|
|
1444
|
+
const torsoRotX = s.rotX * 0.35
|
|
1445
|
+
const torsoRotY = effectiveStatus === 'speaking' ? 0 : s.rotY * 0.35
|
|
1446
|
+
robotBodyRef.current.style.transform = `translateZ(-5px) rotateX(${torsoRotX}deg) rotateY(${torsoRotY}deg)`
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
// Ground Shadow
|
|
1450
|
+
if (groundShadowRef.current) {
|
|
1451
|
+
const liftAmount = Math.max(0, -curHarmonicY)
|
|
1452
|
+
const shadowScale = Math.max(0.4, 1.0 - liftAmount * 0.0035)
|
|
1453
|
+
const shadowOpacity = Math.max(0.15, 0.75 - liftAmount * 0.004)
|
|
1454
|
+
groundShadowRef.current.style.transform = `translateZ(-60px) rotateX(75deg) scale(${shadowScale})`
|
|
1455
|
+
groundShadowRef.current.style.opacity = shadowOpacity
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
// 6. Ears Enlarge & Acoustic Pulse Animation
|
|
1459
|
+
const targetEarScale =
|
|
1460
|
+
effectiveStatus === 'listening' || effectiveStatus === 'hearing'
|
|
1461
|
+
? 1.55 + Math.sin(t * 6) * 0.04 + (s.intensity || 0) * 0.35
|
|
1462
|
+
: 1.0
|
|
1463
|
+
s.earScale += (targetEarScale - s.earScale) * dt * 10
|
|
1464
|
+
|
|
1465
|
+
if (earLeftRef.current) {
|
|
1466
|
+
earLeftRef.current.style.transform = `scale(${s.earScale})`
|
|
1467
|
+
}
|
|
1468
|
+
if (earRightRef.current) {
|
|
1469
|
+
earRightRef.current.style.transform = `scale(${s.earScale})`
|
|
1470
|
+
}
|
|
1471
|
+
if (earGlowLeftRef.current && earGlowRightRef.current) {
|
|
1472
|
+
const earOpacity =
|
|
1473
|
+
effectiveStatus === 'listening' || effectiveStatus === 'hearing'
|
|
1474
|
+
? Math.min(1.0, 0.75 + (s.intensity || 0) * 0.5)
|
|
1475
|
+
: 0.25
|
|
1476
|
+
earGlowLeftRef.current.style.opacity = earOpacity
|
|
1477
|
+
earGlowRightRef.current.style.opacity = earOpacity
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
// 7. Antenna Spring Damping
|
|
1481
|
+
let antTargetLeft = -s.velRotY * 0.35
|
|
1482
|
+
let antTargetRight = -s.velRotY * 0.35
|
|
1483
|
+
|
|
1484
|
+
if (effectiveStatus === 'speaking') {
|
|
1485
|
+
antTargetLeft = Math.sin(t * 6) * (s.intensity * 6)
|
|
1486
|
+
antTargetRight = -Math.sin(t * 6) * (s.intensity * 6)
|
|
1487
|
+
} else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
|
|
1488
|
+
const side = s.listeningSide || 1
|
|
1489
|
+
if (side === -1) {
|
|
1490
|
+
antTargetLeft = 24
|
|
1491
|
+
antTargetRight = -6
|
|
1492
|
+
} else {
|
|
1493
|
+
antTargetLeft = 6
|
|
1494
|
+
antTargetRight = -24
|
|
1495
|
+
}
|
|
1496
|
+
} else if (effectiveStatus === 'music_groove') {
|
|
1497
|
+
antTargetLeft = Math.sin(t * 8) * 20
|
|
1498
|
+
antTargetRight = -Math.sin(t * 8) * 20
|
|
1499
|
+
} else if (effectiveStatus === 'thinking') {
|
|
1500
|
+
antTargetLeft = -s.velRotY * 0.35 - 15
|
|
1501
|
+
antTargetRight = -s.velRotY * 0.35 + 15
|
|
1502
|
+
}
|
|
1503
|
+
|
|
1504
|
+
s.antVelLeft += (antTargetLeft - s.antLeftRot) * 200 * dt - s.antVelLeft * 14 * dt
|
|
1505
|
+
s.antVelRight += (antTargetRight - s.antRightRot) * 200 * dt - s.antVelRight * 14 * dt
|
|
1506
|
+
s.antLeftRot += s.antVelLeft * dt
|
|
1507
|
+
s.antRightRot += s.antVelRight * dt
|
|
1508
|
+
|
|
1509
|
+
if (antLeftRef.current) antLeftRef.current.style.transform = `rotate(${s.antLeftRot}deg)`
|
|
1510
|
+
if (antRightRef.current) antRightRef.current.style.transform = `rotate(${s.antRightRot}deg)`
|
|
1511
|
+
|
|
1512
|
+
// 8. Floating Hand Physics
|
|
1513
|
+
let targetHandLY = Math.sin(s.breathPhase * 0.9) * 5
|
|
1514
|
+
let targetHandLX = Math.cos(s.breathPhase * 0.9) * 2.5
|
|
1515
|
+
let targetHandRY = Math.sin(s.breathPhase * 0.9 + 1.2) * 5
|
|
1516
|
+
let targetHandRX = Math.cos(s.breathPhase * 0.9 + 1.2) * 2.5
|
|
1517
|
+
let handRotL = -s.rotY * 0.2
|
|
1518
|
+
let handRotR = -s.rotY * 0.2
|
|
1519
|
+
|
|
1520
|
+
if (s.shieldProgress > 0.05) {
|
|
1521
|
+
targetHandLX = 42 * s.shieldProgress
|
|
1522
|
+
targetHandLY = -68 * s.shieldProgress
|
|
1523
|
+
targetHandRX = -42 * s.shieldProgress
|
|
1524
|
+
targetHandRY = -68 * s.shieldProgress
|
|
1525
|
+
handRotL = 42 * s.shieldProgress
|
|
1526
|
+
handRotR = -42 * s.shieldProgress
|
|
1527
|
+
} else if (s.isDragging) {
|
|
1528
|
+
targetHandLY = -30 + Math.sin(t * 14) * 12
|
|
1529
|
+
targetHandLX = 15 + Math.cos(t * 14) * 10
|
|
1530
|
+
targetHandRY = -30 - Math.sin(t * 14) * 12
|
|
1531
|
+
targetHandRX = -15 - Math.cos(t * 14) * 10
|
|
1532
|
+
} else if (effectiveStatus === 'processing') {
|
|
1533
|
+
targetHandLY = -26 + Math.sin(t * 14) * 6
|
|
1534
|
+
targetHandRY = -26 + Math.cos(t * 14) * 6
|
|
1535
|
+
targetHandLX = 12 + Math.sin(t * 10) * 4
|
|
1536
|
+
targetHandRX = -12 - Math.sin(t * 10) * 4
|
|
1537
|
+
} else if (effectiveStatus === 'music_groove') {
|
|
1538
|
+
targetHandLY = -15 + Math.sin(t * 8) * 12
|
|
1539
|
+
targetHandRY = -15 - Math.sin(t * 8) * 12
|
|
1540
|
+
} else if (s.isSeeingDesktop) {
|
|
1541
|
+
targetHandLY = -40
|
|
1542
|
+
targetHandLX = 12
|
|
1543
|
+
targetHandRY = -40
|
|
1544
|
+
targetHandRX = -12
|
|
1545
|
+
} else if (effectiveStatus === 'listening' || effectiveStatus === 'hearing') {
|
|
1546
|
+
const side = s.listeningSide || 1
|
|
1547
|
+
if (side === -1) {
|
|
1548
|
+
targetHandLY = -20
|
|
1549
|
+
targetHandLX = 14
|
|
1550
|
+
targetHandRY = 2
|
|
1551
|
+
targetHandRX = -3
|
|
1552
|
+
} else {
|
|
1553
|
+
targetHandRY = -20
|
|
1554
|
+
targetHandRX = -14
|
|
1555
|
+
targetHandLY = 2
|
|
1556
|
+
targetHandLX = 3
|
|
1557
|
+
}
|
|
1558
|
+
} else if (effectiveStatus === 'thinking') {
|
|
1559
|
+
targetHandLY = -34
|
|
1560
|
+
targetHandLX = 16
|
|
1561
|
+
} else if (effectiveStatus === 'speaking') {
|
|
1562
|
+
const voicePulse = Math.sin(t * 8) * (s.intensity * 12)
|
|
1563
|
+
targetHandLY = -10 + voicePulse
|
|
1564
|
+
targetHandLX = -4
|
|
1565
|
+
targetHandRY = -10 - voicePulse
|
|
1566
|
+
targetHandRX = 4
|
|
1567
|
+
} else if (effectiveStatus === 'sleeping') {
|
|
1568
|
+
targetHandLY = 18
|
|
1569
|
+
targetHandRY = 18
|
|
1570
|
+
} else if (s.mood === 'joy') {
|
|
1571
|
+
targetHandLY -= 8
|
|
1572
|
+
targetHandRY -= 8
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
s.handLeftY += (targetHandLY - s.handLeftY) * 0.2
|
|
1576
|
+
s.handLeftX += (targetHandLX - s.handLeftX) * 0.2
|
|
1577
|
+
s.handRightY += (targetHandRY - s.handRightY) * 0.2
|
|
1578
|
+
s.handRightX += (targetHandRX - s.handRightX) * 0.2
|
|
1579
|
+
|
|
1580
|
+
if (handLeftRef.current) {
|
|
1581
|
+
handLeftRef.current.style.transform = `translate3d(${s.handLeftX}px, ${s.handLeftY}px, ${
|
|
1582
|
+
s.shieldProgress > 0.05 ? 40 : s.isSeeingDesktop ? 30 : 18
|
|
1583
|
+
}px) rotateZ(${handRotL}deg)`
|
|
1584
|
+
}
|
|
1585
|
+
if (handRightRef.current) {
|
|
1586
|
+
handRightRef.current.style.transform = `translate3d(${s.handRightX}px, ${s.handRightY}px, ${
|
|
1587
|
+
s.shieldProgress > 0.05 ? 40 : s.isSeeingDesktop ? 30 : 18
|
|
1588
|
+
}px) rotateZ(${handRotR}deg)`
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
// 9. Eyes Parallax
|
|
1592
|
+
if (eyesContainerRef.current) {
|
|
1593
|
+
const finalEyeX = effectiveStatus === 'speaking' ? 0 : s.rotY * 0.35 + s.eyeSaccadeX
|
|
1594
|
+
const finalEyeY = effectiveStatus === 'speaking' ? 0 : -s.rotX * 0.25 + s.eyeSaccadeY
|
|
1595
|
+
eyesContainerRef.current.style.transform = `translate3d(${finalEyeX}px, ${finalEyeY}px, 10px)`
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
// 10. Thruster visual during dragging
|
|
1599
|
+
if (thrusterRef.current) {
|
|
1600
|
+
if (s.isDragging) {
|
|
1601
|
+
thrusterRef.current.style.opacity = '1'
|
|
1602
|
+
thrusterRef.current.style.transform = `translateZ(-35px) scale(${1.8 + Math.sin(t * 22) * 0.35})`
|
|
1603
|
+
thrusterRef.current.style.backgroundColor = '#38bdf8'
|
|
1604
|
+
} else {
|
|
1605
|
+
thrusterRef.current.style.opacity = '0.25'
|
|
1606
|
+
thrusterRef.current.style.transform = 'translateZ(-35px) scale(1)'
|
|
1607
|
+
thrusterRef.current.style.backgroundColor = 'rgba(255, 255, 255, 0.25)'
|
|
1608
|
+
}
|
|
1609
|
+
}
|
|
1610
|
+
|
|
1611
|
+
// Update face elements if speaking or dynamic
|
|
1612
|
+
if (effectiveStatus === 'speaking') {
|
|
1613
|
+
renderFace()
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
animId = requestAnimationFrame(updateFrame)
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
animId = requestAnimationFrame(updateFrame)
|
|
1620
|
+
return () => cancelAnimationFrame(animId)
|
|
1621
|
+
}, [renderFace, updateAutonomousQuirks])
|
|
1622
|
+
|
|
1623
|
+
return (
|
|
1624
|
+
<div
|
|
1625
|
+
className={`relative w-[320px] h-[440px] [perspective:1000px] select-none touch-none ${className}`}
|
|
1626
|
+
title="MARK AI Companion"
|
|
1627
|
+
>
|
|
1628
|
+
<style>{`
|
|
1629
|
+
@keyframes avatarScanline {
|
|
1630
|
+
0% { transform: translateY(-100%); }
|
|
1631
|
+
100% { transform: translateY(160%); }
|
|
1632
|
+
}
|
|
1633
|
+
@keyframes avatarGlitchShake {
|
|
1634
|
+
0% { transform: translate(0, 0); }
|
|
1635
|
+
25% { transform: translate(-3px, 2px); }
|
|
1636
|
+
50% { transform: translate(3px, -2px); }
|
|
1637
|
+
75% { transform: translate(-2px, -1px); }
|
|
1638
|
+
100% { transform: translate(0, 0); }
|
|
1639
|
+
}
|
|
1640
|
+
@keyframes avatarCoreHeartbeat {
|
|
1641
|
+
0%, 100% { transform: scale(1); opacity: 0.9; }
|
|
1642
|
+
15% { transform: scale(1.2); opacity: 1; }
|
|
1643
|
+
30% { transform: scale(1.05); opacity: 0.92; }
|
|
1644
|
+
45% { transform: scale(1.14); opacity: 0.98; }
|
|
1645
|
+
60% { transform: scale(1); opacity: 0.9; }
|
|
1646
|
+
}
|
|
1647
|
+
.avatar-preserve-3d {
|
|
1648
|
+
transform-style: preserve-3d;
|
|
1649
|
+
}
|
|
1650
|
+
.avatar-core-beat {
|
|
1651
|
+
animation: avatarCoreHeartbeat 2.4s cubic-bezier(0.4, 0, 0.6, 1) infinite;
|
|
1652
|
+
}
|
|
1653
|
+
.avatar-eye-mesh {
|
|
1654
|
+
transition: transform 120ms cubic-bezier(0.2, 0.8, 0.2, 1);
|
|
1655
|
+
transform-origin: center center;
|
|
1656
|
+
}
|
|
1657
|
+
`}</style>
|
|
1658
|
+
|
|
1659
|
+
{/* Robot Root Pivot (Handles organic breathing, floating, & tactile interactions) */}
|
|
1660
|
+
<div
|
|
1661
|
+
ref={robotRootRef}
|
|
1662
|
+
className="relative w-full h-full avatar-preserve-3d cursor-pointer"
|
|
1663
|
+
onPointerDown={handlePointerDown}
|
|
1664
|
+
onPointerMove={handlePointerMove}
|
|
1665
|
+
onPointerUp={handlePointerUp}
|
|
1666
|
+
onPointerCancel={handlePointerCancel}
|
|
1667
|
+
>
|
|
1668
|
+
{/* ── 1. UNIFIED 3D HEAD ASSEMBLY (Throat pivot joint 130px 165px) ── */}
|
|
1669
|
+
<div
|
|
1670
|
+
ref={robotHeadRef}
|
|
1671
|
+
className="absolute left-[30px] top-[18px] w-[260px] h-[180px] avatar-preserve-3d"
|
|
1672
|
+
style={{ transformOrigin: '130px 165px' }}
|
|
1673
|
+
>
|
|
1674
|
+
{/* Background Helmet Shell & Antennas (Single unified SVG with 3D gradients) */}
|
|
1675
|
+
<svg
|
|
1676
|
+
viewBox="0 0 260 180"
|
|
1677
|
+
className="absolute inset-0 w-full h-full pointer-events-none drop-shadow-[0_16px_36px_rgba(0,0,0,0.9)]"
|
|
1678
|
+
style={{ overflow: 'visible' }}
|
|
1679
|
+
>
|
|
1680
|
+
<defs>
|
|
1681
|
+
<radialGradient id="avatarHelmetGrad" cx="38%" cy="32%" r="68%">
|
|
1682
|
+
<stop offset="0%" stopColor="#243f30" />
|
|
1683
|
+
<stop offset="45%" stopColor="#14241c" />
|
|
1684
|
+
<stop offset="100%" stopColor="#080e0a" />
|
|
1685
|
+
</radialGradient>
|
|
1686
|
+
<linearGradient id="avatarHelmetRim" x1="0" y1="0" x2="0" y2="1">
|
|
1687
|
+
<stop offset="0%" stopColor="#2a4738" />
|
|
1688
|
+
<stop offset="100%" stopColor="#14241c" />
|
|
1689
|
+
</linearGradient>
|
|
1690
|
+
<filter id="avatar-neon-glow" x="-20%" y="-20%" width="140%" height="140%">
|
|
1691
|
+
<feGaussianBlur stdDeviation="3" result="blur" />
|
|
1692
|
+
<feComposite in="SourceGraphic" in2="blur" operator="over" />
|
|
1693
|
+
</filter>
|
|
1694
|
+
</defs>
|
|
1695
|
+
|
|
1696
|
+
{/* Left Wing Antenna */}
|
|
1697
|
+
<g
|
|
1698
|
+
ref={antLeftRef}
|
|
1699
|
+
data-zone="antenna-left"
|
|
1700
|
+
className="origin-[55px_90px] cursor-pointer pointer-events-auto"
|
|
1701
|
+
>
|
|
1702
|
+
<path
|
|
1703
|
+
d="M 55 90 L 16 40 L 22 24 L 46 48 Z"
|
|
1704
|
+
fill="#13221b"
|
|
1705
|
+
stroke="#19362d"
|
|
1706
|
+
strokeWidth="2.5"
|
|
1707
|
+
/>
|
|
1708
|
+
<circle
|
|
1709
|
+
ref={antBulbLeftRef}
|
|
1710
|
+
cx="19"
|
|
1711
|
+
cy="32"
|
|
1712
|
+
r="7"
|
|
1713
|
+
fill="#ffffff"
|
|
1714
|
+
filter="url(#avatar-neon-glow)"
|
|
1715
|
+
opacity="0.92"
|
|
1716
|
+
/>
|
|
1717
|
+
<circle cx="19" cy="32" r="2.5" fill="#ffffff" opacity="0.98" />
|
|
1718
|
+
</g>
|
|
1719
|
+
|
|
1720
|
+
{/* Right Wing Antenna */}
|
|
1721
|
+
<g
|
|
1722
|
+
ref={antRightRef}
|
|
1723
|
+
data-zone="antenna-right"
|
|
1724
|
+
className="origin-[205px_90px] cursor-pointer pointer-events-auto"
|
|
1725
|
+
>
|
|
1726
|
+
<path
|
|
1727
|
+
d="M 205 90 L 244 40 L 238 24 L 214 48 Z"
|
|
1728
|
+
fill="#13221b"
|
|
1729
|
+
stroke="#19362d"
|
|
1730
|
+
strokeWidth="2.5"
|
|
1731
|
+
/>
|
|
1732
|
+
<circle
|
|
1733
|
+
ref={antBulbRightRef}
|
|
1734
|
+
cx="241"
|
|
1735
|
+
cy="32"
|
|
1736
|
+
r="7"
|
|
1737
|
+
fill="#ffffff"
|
|
1738
|
+
filter="url(#avatar-neon-glow)"
|
|
1739
|
+
opacity="0.92"
|
|
1740
|
+
/>
|
|
1741
|
+
<circle cx="241" cy="32" r="2.5" fill="#ffffff" opacity="0.98" />
|
|
1742
|
+
</g>
|
|
1743
|
+
|
|
1744
|
+
{/* Main Helmet Curved 3D Head Shell (Sleek, Clean, No badges/text) */}
|
|
1745
|
+
<ellipse
|
|
1746
|
+
data-zone="head"
|
|
1747
|
+
cx="130"
|
|
1748
|
+
cy="95"
|
|
1749
|
+
rx="78"
|
|
1750
|
+
ry="70"
|
|
1751
|
+
fill="url(#avatarHelmetGrad)"
|
|
1752
|
+
stroke="url(#avatarHelmetRim)"
|
|
1753
|
+
strokeWidth="3"
|
|
1754
|
+
className="cursor-pointer pointer-events-auto"
|
|
1755
|
+
/>
|
|
1756
|
+
|
|
1757
|
+
{/* Chin Bevel (Hugs neck seamlessly) */}
|
|
1758
|
+
<path d="M 104 156 Q 130 166 156 156 L 150 164 Q 130 170 110 164 Z" fill="#080e0a" />
|
|
1759
|
+
|
|
1760
|
+
{/* Left Earphone Side Pod (Enlarges & illuminates during listening) */}
|
|
1761
|
+
<g ref={earLeftRef} className="origin-[52px_95px] avatar-preserve-3d">
|
|
1762
|
+
<rect
|
|
1763
|
+
x="47"
|
|
1764
|
+
y="78"
|
|
1765
|
+
width="10"
|
|
1766
|
+
height="34"
|
|
1767
|
+
rx="5"
|
|
1768
|
+
fill="#19362d"
|
|
1769
|
+
stroke="#254a3e"
|
|
1770
|
+
strokeWidth="2"
|
|
1771
|
+
/>
|
|
1772
|
+
<rect
|
|
1773
|
+
ref={earGlowLeftRef}
|
|
1774
|
+
x="50"
|
|
1775
|
+
y="84"
|
|
1776
|
+
width="4"
|
|
1777
|
+
height="22"
|
|
1778
|
+
rx="2"
|
|
1779
|
+
fill="#ffffff"
|
|
1780
|
+
className="transition-opacity duration-300"
|
|
1781
|
+
filter="url(#avatar-neon-glow)"
|
|
1782
|
+
opacity="0.35"
|
|
1783
|
+
/>
|
|
1784
|
+
</g>
|
|
1785
|
+
|
|
1786
|
+
{/* Right Earphone Side Pod (Enlarges & illuminates during listening) */}
|
|
1787
|
+
<g ref={earRightRef} className="origin-[208px_95px] avatar-preserve-3d">
|
|
1788
|
+
<rect
|
|
1789
|
+
x="203"
|
|
1790
|
+
y="78"
|
|
1791
|
+
width="10"
|
|
1792
|
+
height="34"
|
|
1793
|
+
rx="5"
|
|
1794
|
+
fill="#19362d"
|
|
1795
|
+
stroke="#254a3e"
|
|
1796
|
+
strokeWidth="2"
|
|
1797
|
+
/>
|
|
1798
|
+
<rect
|
|
1799
|
+
ref={earGlowRightRef}
|
|
1800
|
+
x="206"
|
|
1801
|
+
y="84"
|
|
1802
|
+
width="4"
|
|
1803
|
+
height="22"
|
|
1804
|
+
rx="2"
|
|
1805
|
+
fill="#ffffff"
|
|
1806
|
+
className="transition-opacity duration-300"
|
|
1807
|
+
filter="url(#avatar-neon-glow)"
|
|
1808
|
+
opacity="0.35"
|
|
1809
|
+
/>
|
|
1810
|
+
</g>
|
|
1811
|
+
|
|
1812
|
+
{/* Forehead Ambient Light Sensor Dot */}
|
|
1813
|
+
<circle cx="130" cy="36" r="3" fill="#ffffff" opacity="0.8" />
|
|
1814
|
+
</svg>
|
|
1815
|
+
|
|
1816
|
+
{/* Deep Curved 3D Glass Visor Screen */}
|
|
1817
|
+
<div
|
|
1818
|
+
data-zone="visor"
|
|
1819
|
+
className="absolute left-[56px] top-[40px] w-[148px] h-[108px] rounded-[34px] bg-gradient-to-b from-[#0a140f] to-[#040806] border-2 border-[#19362d] overflow-hidden shadow-[inset_0_4px_22px_rgba(0,0,0,0.98)] avatar-preserve-3d flex items-center justify-center cursor-pointer pointer-events-auto"
|
|
1820
|
+
style={{ transform: 'translateZ(16px)' }}
|
|
1821
|
+
>
|
|
1822
|
+
{/* Minimalist Desktop Wireframe Reflection (Active during Awareness / Melihat Layar) */}
|
|
1823
|
+
<div
|
|
1824
|
+
ref={desktopReflectionRef}
|
|
1825
|
+
className="absolute inset-0 pointer-events-none opacity-0 transition-opacity duration-500 flex flex-col p-3 z-0"
|
|
1826
|
+
>
|
|
1827
|
+
<div className="w-full h-3 border-b border-[#1fb854]/30 flex items-center justify-between px-1 pb-1">
|
|
1828
|
+
<div className="flex gap-1">
|
|
1829
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[#1fb854]/50" />
|
|
1830
|
+
<div className="w-1.5 h-1.5 rounded-full bg-[#1fb854]/30" />
|
|
1831
|
+
</div>
|
|
1832
|
+
<div className="w-6 h-1 rounded-full bg-[#1fb854]/25" />
|
|
1833
|
+
</div>
|
|
1834
|
+
<div className="flex-1 flex flex-col gap-1 pt-1.5 px-0.5 opacity-40">
|
|
1835
|
+
<div className="w-3/4 h-1 rounded bg-[#1fb854]/40" />
|
|
1836
|
+
<div className="w-1/2 h-1 rounded bg-[#1fb854]/30" />
|
|
1837
|
+
<div className="w-5/6 h-1 rounded bg-[#1fb854]/40" />
|
|
1838
|
+
<div className="w-2/3 h-1 rounded bg-[#1fb854]/30" />
|
|
1839
|
+
<div className="w-4/5 h-1 rounded bg-[#1fb854]/40" />
|
|
1840
|
+
</div>
|
|
1841
|
+
<div className="absolute inset-x-0 top-0 h-4 bg-gradient-to-b from-[#1fb854]/15 to-transparent pointer-events-none animate-[avatarScanline_2.5s_linear_infinite]" />
|
|
1842
|
+
</div>
|
|
1843
|
+
|
|
1844
|
+
{/* Specular Curved Highlight */}
|
|
1845
|
+
<div className="absolute -top-10 -left-10 w-56 h-30 bg-gradient-to-b from-white/12 via-white/3 to-transparent rotate-[-22deg] pointer-events-none rounded-full blur-[1px]" />
|
|
1846
|
+
|
|
1847
|
+
{/* LED Cyber Face (Elastic Eyes + Animated Mouth) */}
|
|
1848
|
+
<div
|
|
1849
|
+
className="relative w-full h-full flex flex-col items-center justify-center z-10 avatar-preserve-3d"
|
|
1850
|
+
style={{ transform: 'translateZ(10px)' }}
|
|
1851
|
+
>
|
|
1852
|
+
{/* Eyes Container */}
|
|
1853
|
+
<div
|
|
1854
|
+
ref={eyesContainerRef}
|
|
1855
|
+
className="flex items-center justify-center gap-7 pointer-events-auto"
|
|
1856
|
+
>
|
|
1857
|
+
<div
|
|
1858
|
+
ref={leftEyeRef}
|
|
1859
|
+
data-zone="eye-left"
|
|
1860
|
+
className="w-10 h-10 flex items-center justify-center avatar-eye-mesh cursor-pointer pointer-events-auto"
|
|
1861
|
+
/>
|
|
1862
|
+
<div
|
|
1863
|
+
ref={rightEyeRef}
|
|
1864
|
+
data-zone="eye-right"
|
|
1865
|
+
className="w-10 h-10 flex items-center justify-center avatar-eye-mesh cursor-pointer pointer-events-auto"
|
|
1866
|
+
/>
|
|
1867
|
+
</div>
|
|
1868
|
+
|
|
1869
|
+
{/* Mouth Container */}
|
|
1870
|
+
<div ref={mouthRef} className="h-5 flex items-center justify-center mt-1" />
|
|
1871
|
+
</div>
|
|
1872
|
+
</div>
|
|
1873
|
+
</div>
|
|
1874
|
+
|
|
1875
|
+
{/* ── 2. BODY CHASSIS & INTEGRATED NECK COLLAR (Centered: X: 95px, Y: 172px) ── */}
|
|
1876
|
+
<div
|
|
1877
|
+
ref={robotBodyRef}
|
|
1878
|
+
className="absolute left-[95px] top-[172px] w-[130px] h-[126px] avatar-preserve-3d"
|
|
1879
|
+
style={{
|
|
1880
|
+
transformOrigin: '65px 12px',
|
|
1881
|
+
transform: 'translateZ(-5px)'
|
|
1882
|
+
}}
|
|
1883
|
+
>
|
|
1884
|
+
<svg
|
|
1885
|
+
viewBox="0 0 130 126"
|
|
1886
|
+
className="w-full h-full drop-shadow-[0_12px_30px_rgba(0,0,0,0.85)]"
|
|
1887
|
+
>
|
|
1888
|
+
<defs>
|
|
1889
|
+
<radialGradient id="avatarBodyGrad" cx="50%" cy="40%" r="65%">
|
|
1890
|
+
<stop offset="0%" stopColor="#1a2e23" />
|
|
1891
|
+
<stop offset="60%" stopColor="#13221b" />
|
|
1892
|
+
<stop offset="100%" stopColor="#080e0a" />
|
|
1893
|
+
</radialGradient>
|
|
1894
|
+
</defs>
|
|
1895
|
+
|
|
1896
|
+
{/* Integrated Cyber Neck Socket */}
|
|
1897
|
+
<ellipse
|
|
1898
|
+
cx="65"
|
|
1899
|
+
cy="14"
|
|
1900
|
+
rx="26"
|
|
1901
|
+
ry="9"
|
|
1902
|
+
fill="#0d1611"
|
|
1903
|
+
stroke="#19362d"
|
|
1904
|
+
strokeWidth="2.5"
|
|
1905
|
+
/>
|
|
1906
|
+
<ellipse cx="65" cy="14" rx="16" ry="5" fill="#080e0a" />
|
|
1907
|
+
|
|
1908
|
+
{/* Body Outer Shell */}
|
|
1909
|
+
<path
|
|
1910
|
+
data-zone="body"
|
|
1911
|
+
d="M 32 30 L 98 30 C 114 30, 120 50, 114 90 C 108 113, 88 120, 65 120 C 42 120, 22 113, 16 90 C 10 50, 16 30, 32 30 Z"
|
|
1912
|
+
fill="url(#avatarBodyGrad)"
|
|
1913
|
+
stroke="#19362d"
|
|
1914
|
+
strokeWidth="3"
|
|
1915
|
+
className="cursor-pointer pointer-events-auto"
|
|
1916
|
+
/>
|
|
1917
|
+
|
|
1918
|
+
{/* Belly Tactile Zone (for tickle interactions) */}
|
|
1919
|
+
<path
|
|
1920
|
+
data-zone="belly"
|
|
1921
|
+
d="M 38 72 L 92 72 C 86 112, 44 112, 38 72 Z"
|
|
1922
|
+
fill="transparent"
|
|
1923
|
+
className="cursor-pointer pointer-events-auto"
|
|
1924
|
+
/>
|
|
1925
|
+
|
|
1926
|
+
{/* Cyber Armor Plates */}
|
|
1927
|
+
<path
|
|
1928
|
+
d="M 30 45 L 56 45 L 52 80 L 28 75 Z"
|
|
1929
|
+
fill="#19362d"
|
|
1930
|
+
stroke="#254a3e"
|
|
1931
|
+
strokeWidth="1.5"
|
|
1932
|
+
/>
|
|
1933
|
+
<path
|
|
1934
|
+
d="M 100 45 L 74 45 L 78 80 L 102 75 Z"
|
|
1935
|
+
fill="#19362d"
|
|
1936
|
+
stroke="#254a3e"
|
|
1937
|
+
strokeWidth="1.5"
|
|
1938
|
+
/>
|
|
1939
|
+
|
|
1940
|
+
{/* Central Arc Reactor Heartbeat */}
|
|
1941
|
+
<g data-zone="core" className="cursor-pointer pointer-events-auto">
|
|
1942
|
+
<circle cx="65" cy="70" r="14" fill="#09120e" stroke="#19362d" strokeWidth="2.5" />
|
|
1943
|
+
<g className="avatar-core-beat origin-[65px_70px]">
|
|
1944
|
+
<circle
|
|
1945
|
+
cx="65"
|
|
1946
|
+
cy="70"
|
|
1947
|
+
r="9"
|
|
1948
|
+
fill="#ffffff"
|
|
1949
|
+
filter="url(#avatar-neon-glow)"
|
|
1950
|
+
opacity="0.88"
|
|
1951
|
+
/>
|
|
1952
|
+
<circle cx="65" cy="70" r="4.5" fill="#ffffff" opacity="0.98" />
|
|
1953
|
+
</g>
|
|
1954
|
+
</g>
|
|
1955
|
+
|
|
1956
|
+
{/* Power Telemetry Bars */}
|
|
1957
|
+
<rect x="50" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
|
|
1958
|
+
<rect x="59" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
|
|
1959
|
+
<rect x="68" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
|
|
1960
|
+
<rect x="77" y="95" width="6" height="3" rx="1.5" fill="#ffffff" opacity="0.75" />
|
|
1961
|
+
|
|
1962
|
+
{/* Bottom Thruster Vent Nozzle */}
|
|
1963
|
+
<path d="M 50 117 L 80 117 L 74 125 L 56 125 Z" fill="#0d1712" />
|
|
1964
|
+
</svg>
|
|
1965
|
+
</div>
|
|
1966
|
+
|
|
1967
|
+
{/* ── 3. FLOATING HANDS (5-Finger Cybernetic Claws) ── */}
|
|
1968
|
+
<div
|
|
1969
|
+
ref={handLeftRef}
|
|
1970
|
+
data-zone="hand-left"
|
|
1971
|
+
className="absolute left-[36px] top-[215px] w-[44px] h-[52px] avatar-preserve-3d cursor-pointer pointer-events-auto"
|
|
1972
|
+
style={{ transform: 'translateZ(18px)' }}
|
|
1973
|
+
>
|
|
1974
|
+
<svg
|
|
1975
|
+
viewBox="0 0 50 60"
|
|
1976
|
+
className="w-full h-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.65)]"
|
|
1977
|
+
>
|
|
1978
|
+
{/* Thumb (Outer Left) */}
|
|
1979
|
+
<g transform="rotate(28, 11, 20)">
|
|
1980
|
+
<rect
|
|
1981
|
+
x="5.5"
|
|
1982
|
+
y="15"
|
|
1983
|
+
width="6.2"
|
|
1984
|
+
height="19"
|
|
1985
|
+
rx="3.1"
|
|
1986
|
+
fill="#14241c"
|
|
1987
|
+
stroke="#19362d"
|
|
1988
|
+
strokeWidth="1.5"
|
|
1989
|
+
/>
|
|
1990
|
+
<line x1="6" y1="24.5" x2="11.2" y2="24.5" stroke="#254a3e" strokeWidth="1" />
|
|
1991
|
+
<circle cx="8.6" cy="30.5" r="2.1" fill="#ffffff" opacity="0.95" />
|
|
1992
|
+
</g>
|
|
1993
|
+
|
|
1994
|
+
{/* 4 Lower Fingers */}
|
|
1995
|
+
{/* Index */}
|
|
1996
|
+
<rect
|
|
1997
|
+
x="12.5"
|
|
1998
|
+
y="18"
|
|
1999
|
+
width="5.8"
|
|
2000
|
+
height="26"
|
|
2001
|
+
rx="2.9"
|
|
2002
|
+
fill="#14241c"
|
|
2003
|
+
stroke="#19362d"
|
|
2004
|
+
strokeWidth="1.5"
|
|
2005
|
+
/>
|
|
2006
|
+
<line x1="13" y1="31" x2="17.8" y2="31" stroke="#254a3e" strokeWidth="1" />
|
|
2007
|
+
<circle cx="15.4" cy="40.5" r="2" fill="#ffffff" opacity="0.95" />
|
|
2008
|
+
|
|
2009
|
+
{/* Middle (Longest) */}
|
|
2010
|
+
<rect
|
|
2011
|
+
x="19.7"
|
|
2012
|
+
y="18"
|
|
2013
|
+
width="6.2"
|
|
2014
|
+
height="30"
|
|
2015
|
+
rx="3.1"
|
|
2016
|
+
fill="#14241c"
|
|
2017
|
+
stroke="#19362d"
|
|
2018
|
+
strokeWidth="1.5"
|
|
2019
|
+
/>
|
|
2020
|
+
<line x1="20.2" y1="33" x2="25.4" y2="33" stroke="#254a3e" strokeWidth="1" />
|
|
2021
|
+
<circle cx="22.8" cy="44.5" r="2.2" fill="#ffffff" opacity="0.95" />
|
|
2022
|
+
|
|
2023
|
+
{/* Ring */}
|
|
2024
|
+
<rect
|
|
2025
|
+
x="27.3"
|
|
2026
|
+
y="18"
|
|
2027
|
+
width="5.8"
|
|
2028
|
+
height="27"
|
|
2029
|
+
rx="2.9"
|
|
2030
|
+
fill="#14241c"
|
|
2031
|
+
stroke="#19362d"
|
|
2032
|
+
strokeWidth="1.5"
|
|
2033
|
+
/>
|
|
2034
|
+
<line x1="27.8" y1="31" x2="32.6" y2="31" stroke="#254a3e" strokeWidth="1" />
|
|
2035
|
+
<circle cx="30.2" cy="41.5" r="2" fill="#ffffff" opacity="0.95" />
|
|
2036
|
+
|
|
2037
|
+
{/* Pinky (Shortest) */}
|
|
2038
|
+
<rect
|
|
2039
|
+
x="34.5"
|
|
2040
|
+
y="18"
|
|
2041
|
+
width="5.2"
|
|
2042
|
+
height="22"
|
|
2043
|
+
rx="2.6"
|
|
2044
|
+
fill="#14241c"
|
|
2045
|
+
stroke="#19362d"
|
|
2046
|
+
strokeWidth="1.5"
|
|
2047
|
+
/>
|
|
2048
|
+
<line x1="35" y1="29" x2="39.2" y2="29" stroke="#254a3e" strokeWidth="1" />
|
|
2049
|
+
<circle cx="37.1" cy="36.5" r="1.8" fill="#ffffff" opacity="0.95" />
|
|
2050
|
+
|
|
2051
|
+
{/* Palm Base Chassis (Drawn over finger roots) */}
|
|
2052
|
+
<rect
|
|
2053
|
+
x="11"
|
|
2054
|
+
y="7"
|
|
2055
|
+
width="30"
|
|
2056
|
+
height="22"
|
|
2057
|
+
rx="11"
|
|
2058
|
+
fill="#13221b"
|
|
2059
|
+
stroke="#19362d"
|
|
2060
|
+
strokeWidth="2.2"
|
|
2061
|
+
/>
|
|
2062
|
+
<ellipse
|
|
2063
|
+
cx="26"
|
|
2064
|
+
cy="17"
|
|
2065
|
+
rx="7"
|
|
2066
|
+
ry="5.5"
|
|
2067
|
+
fill="#0c1611"
|
|
2068
|
+
stroke="#19362d"
|
|
2069
|
+
strokeWidth="1"
|
|
2070
|
+
/>
|
|
2071
|
+
<circle cx="26" cy="17" r="2" fill="#ffffff" opacity="0.35" />
|
|
2072
|
+
</svg>
|
|
2073
|
+
</div>
|
|
2074
|
+
|
|
2075
|
+
<div
|
|
2076
|
+
ref={handRightRef}
|
|
2077
|
+
data-zone="hand-right"
|
|
2078
|
+
className="absolute left-[240px] top-[215px] w-[44px] h-[52px] avatar-preserve-3d cursor-pointer pointer-events-auto"
|
|
2079
|
+
style={{ transform: 'translateZ(18px)' }}
|
|
2080
|
+
>
|
|
2081
|
+
<svg
|
|
2082
|
+
viewBox="0 0 50 60"
|
|
2083
|
+
className="w-full h-full drop-shadow-[0_6px_14px_rgba(0,0,0,0.65)]"
|
|
2084
|
+
>
|
|
2085
|
+
{/* Thumb (Outer Right) */}
|
|
2086
|
+
<g transform="rotate(-28, 39, 20)">
|
|
2087
|
+
<rect
|
|
2088
|
+
x="38.3"
|
|
2089
|
+
y="15"
|
|
2090
|
+
width="6.2"
|
|
2091
|
+
height="19"
|
|
2092
|
+
rx="3.1"
|
|
2093
|
+
fill="#14241c"
|
|
2094
|
+
stroke="#19362d"
|
|
2095
|
+
strokeWidth="1.5"
|
|
2096
|
+
/>
|
|
2097
|
+
<line x1="38.8" y1="24.5" x2="44" y2="24.5" stroke="#254a3e" strokeWidth="1" />
|
|
2098
|
+
<circle cx="41.4" cy="30.5" r="2.1" fill="#ffffff" opacity="0.95" />
|
|
2099
|
+
</g>
|
|
2100
|
+
|
|
2101
|
+
{/* Pinky (Left side) */}
|
|
2102
|
+
<rect
|
|
2103
|
+
x="10.3"
|
|
2104
|
+
y="18"
|
|
2105
|
+
width="5.2"
|
|
2106
|
+
height="22"
|
|
2107
|
+
rx="2.6"
|
|
2108
|
+
fill="#14241c"
|
|
2109
|
+
stroke="#19362d"
|
|
2110
|
+
strokeWidth="1.5"
|
|
2111
|
+
/>
|
|
2112
|
+
<line x1="10.8" y1="29" x2="15" y2="29" stroke="#254a3e" strokeWidth="1" />
|
|
2113
|
+
<circle cx="12.9" cy="36.5" r="1.8" fill="#ffffff" opacity="0.95" />
|
|
2114
|
+
|
|
2115
|
+
{/* Ring */}
|
|
2116
|
+
<rect
|
|
2117
|
+
x="16.9"
|
|
2118
|
+
y="18"
|
|
2119
|
+
width="5.8"
|
|
2120
|
+
height="27"
|
|
2121
|
+
rx="2.9"
|
|
2122
|
+
fill="#14241c"
|
|
2123
|
+
stroke="#19362d"
|
|
2124
|
+
strokeWidth="1.5"
|
|
2125
|
+
/>
|
|
2126
|
+
<line x1="17.4" y1="31" x2="22.2" y2="31" stroke="#254a3e" strokeWidth="1" />
|
|
2127
|
+
<circle cx="19.8" cy="41.5" r="2" fill="#ffffff" opacity="0.95" />
|
|
2128
|
+
|
|
2129
|
+
{/* Middle */}
|
|
2130
|
+
<rect
|
|
2131
|
+
x="24.1"
|
|
2132
|
+
y="18"
|
|
2133
|
+
width="6.2"
|
|
2134
|
+
height="30"
|
|
2135
|
+
rx="3.1"
|
|
2136
|
+
fill="#14241c"
|
|
2137
|
+
stroke="#19362d"
|
|
2138
|
+
strokeWidth="1.5"
|
|
2139
|
+
/>
|
|
2140
|
+
<line x1="24.6" y1="33" x2="29.8" y2="33" stroke="#254a3e" strokeWidth="1" />
|
|
2141
|
+
<circle cx="27.2" cy="44.5" r="2.2" fill="#ffffff" opacity="0.95" />
|
|
2142
|
+
|
|
2143
|
+
{/* Index */}
|
|
2144
|
+
<rect
|
|
2145
|
+
x="31.7"
|
|
2146
|
+
y="18"
|
|
2147
|
+
width="5.8"
|
|
2148
|
+
height="26"
|
|
2149
|
+
rx="2.9"
|
|
2150
|
+
fill="#14241c"
|
|
2151
|
+
stroke="#19362d"
|
|
2152
|
+
strokeWidth="1.5"
|
|
2153
|
+
/>
|
|
2154
|
+
<line x1="32.2" y1="31" x2="37" y2="31" stroke="#254a3e" strokeWidth="1" />
|
|
2155
|
+
<circle cx="34.6" cy="40.5" r="2" fill="#ffffff" opacity="0.95" />
|
|
2156
|
+
|
|
2157
|
+
{/* Palm Base Chassis */}
|
|
2158
|
+
<rect
|
|
2159
|
+
x="9"
|
|
2160
|
+
y="7"
|
|
2161
|
+
width="30"
|
|
2162
|
+
height="22"
|
|
2163
|
+
rx="11"
|
|
2164
|
+
fill="#13221b"
|
|
2165
|
+
stroke="#19362d"
|
|
2166
|
+
strokeWidth="2.2"
|
|
2167
|
+
/>
|
|
2168
|
+
<ellipse
|
|
2169
|
+
cx="24"
|
|
2170
|
+
cy="17"
|
|
2171
|
+
rx="7"
|
|
2172
|
+
ry="5.5"
|
|
2173
|
+
fill="#0c1611"
|
|
2174
|
+
stroke="#19362d"
|
|
2175
|
+
strokeWidth="1"
|
|
2176
|
+
/>
|
|
2177
|
+
<circle cx="24" cy="17" r="2" fill="#ffffff" opacity="0.35" />
|
|
2178
|
+
</svg>
|
|
2179
|
+
</div>
|
|
2180
|
+
|
|
2181
|
+
{/* ── 4. THRUSTER PARTICLES & SHADOW AT BOTTOM ── */}
|
|
2182
|
+
<div
|
|
2183
|
+
ref={thrusterRef}
|
|
2184
|
+
className="absolute left-[118px] top-[305px] w-[84px] h-[22px] rounded-full bg-white/25 blur-[10px] pointer-events-none"
|
|
2185
|
+
style={{ transform: 'translateZ(-35px)' }}
|
|
2186
|
+
/>
|
|
2187
|
+
<div
|
|
2188
|
+
ref={groundShadowRef}
|
|
2189
|
+
className="absolute left-[90px] top-[360px] w-[140px] h-[26px] rounded-full bg-black/75 blur-[8px] pointer-events-none"
|
|
2190
|
+
style={{ transform: 'translateZ(-60px) rotateX(75deg)' }}
|
|
2191
|
+
/>
|
|
2192
|
+
</div>
|
|
2193
|
+
</div>
|
|
2194
|
+
)
|
|
2195
|
+
}
|
|
2196
|
+
|
|
2197
|
+
export default memo(Avatar)
|