diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx
index 7ad4ee2..493fa4c 100644
--- a/frontend/src/App.tsx
+++ b/frontend/src/App.tsx
@@ -11,6 +11,7 @@ import IntentionsRipples from './components/IntentionsRipples';
import MoodCollageStudio from './components/MoodCollageStudio';
import NextStepsBridge from './components/NextStepsBridge';
import GuidedImageryPlayer from './components/GuidedImageryPlayer';
+import EmotionExplorer3D from './components/EmotionExplorer3D';
function Home() {
return (
@@ -27,6 +28,7 @@ function Home() {
'Collage Studio',
'Next Steps Bridge',
'Guided Imagery',
+ 'Emotion Explorer 3D',
].map((item) => (
{item}
@@ -56,6 +58,7 @@ export default function App() {
} />
} />
} />
+
} />
);
diff --git a/frontend/src/components/EmotionExplorer3D.tsx b/frontend/src/components/EmotionExplorer3D.tsx
new file mode 100644
index 0000000..aa70a5f
--- /dev/null
+++ b/frontend/src/components/EmotionExplorer3D.tsx
@@ -0,0 +1,220 @@
+import { useEffect, useMemo, useRef, useState } from 'react';
+
+type EmotionZone = {
+ id: string;
+ name: string;
+ color: string;
+ energy: number;
+ description: string;
+};
+
+const zones: EmotionZone[] = [
+ {
+ id: 'clarity',
+ name: 'Clarity Spire',
+ color: '#7cd4f7',
+ energy: 82,
+ description: 'A calm ridge where clarity rises like a lighthouse beam.',
+ },
+ {
+ id: 'wonder',
+ name: 'Wonder Bloom',
+ color: '#b694ff',
+ energy: 74,
+ description: 'Curious sparks that lift the canopy into a soft lavender glow.',
+ },
+ {
+ id: 'joy',
+ name: 'Joy Harbor',
+ color: '#f6b0f4',
+ energy: 91,
+ description: 'Playful currents and rhythmic pulses guide you forward.',
+ },
+ {
+ id: 'resolve',
+ name: 'Resolve Gate',
+ color: '#8cf0a6',
+ energy: 69,
+ description: 'Steady footfalls, anchored in emerald light.',
+ },
+ {
+ id: 'gratitude',
+ name: 'Gratitude Vale',
+ color: '#ffd479',
+ energy: 88,
+ description: 'Warm constellations thread between gilded arches.',
+ },
+];
+
+function useStarfield(canvasRef: React.RefObject
) {
+ useEffect(() => {
+ const canvas = canvasRef.current;
+ if (!canvas) return;
+
+ const ctx = canvas.getContext('2d');
+ if (!ctx) return;
+
+ let animationFrame = 0;
+ let stars = Array.from({ length: 180 }, () => ({
+ x: Math.random(),
+ y: Math.random(),
+ z: Math.random(),
+ radius: 0.6 + Math.random() * 1.8,
+ }));
+
+ const resize = () => {
+ canvas.width = canvas.clientWidth * window.devicePixelRatio;
+ canvas.height = canvas.clientHeight * window.devicePixelRatio;
+ };
+
+ resize();
+ window.addEventListener('resize', resize);
+
+ const render = () => {
+ if (!canvas) return;
+ const { width, height } = canvas;
+ ctx.clearRect(0, 0, width, height);
+ ctx.fillStyle = 'rgba(8, 12, 28, 0.6)';
+ ctx.fillRect(0, 0, width, height);
+
+ stars = stars.map((star) => ({
+ ...star,
+ y: star.y + 0.0008 + star.z * 0.0012,
+ }));
+
+ stars.forEach((star) => {
+ const x = star.x * width;
+ const y = (star.y % 1) * height;
+ ctx.beginPath();
+ ctx.fillStyle = `rgba(255, 255, 255, ${0.2 + star.z * 0.6})`;
+ ctx.arc(x, y, star.radius, 0, Math.PI * 2);
+ ctx.fill();
+ });
+
+ animationFrame = requestAnimationFrame(render);
+ };
+
+ render();
+
+ return () => {
+ window.removeEventListener('resize', resize);
+ cancelAnimationFrame(animationFrame);
+ };
+ }, [canvasRef]);
+}
+
+export default function EmotionExplorer3D() {
+ const [activeZone, setActiveZone] = useState(zones[0]);
+ const [tilt, setTilt] = useState({ x: -8, y: 12 });
+ const canvasRef = useRef(null);
+ const stageRef = useRef(null);
+ const orbitOffsets = useMemo(() => zones.map((_, index) => index * 72), []);
+
+ useStarfield(canvasRef);
+
+ const handleMove = (event: React.MouseEvent) => {
+ if (!stageRef.current) return;
+ const rect = stageRef.current.getBoundingClientRect();
+ const x = (event.clientX - rect.left) / rect.width;
+ const y = (event.clientY - rect.top) / rect.height;
+ setTilt({
+ x: -12 + y * 24,
+ y: -18 + x * 36,
+ });
+ };
+
+ return (
+
+
+ Emotion Explorer 3D
+
+ A playable 3D-inspired vista built with layered depth, animated lighting, and responsive motion. Hover to
+ tilt the world and choose a zone to tune the mood journey.
+
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 8 }, (_, index) => (
+
+ ))}
+
+
+ {zones.map((zone, index) => (
+ setActiveZone(zone)}
+ >
+ {zone.name}
+
+
+ ))}
+
+
+
+ {zones.map((zone, index) => (
+
+ ))}
+
+
+
+
+
{activeZone.name}
+
{activeZone.description}
+
+
Energy
+
+
{activeZone.energy}%
+
+
+
+
Explorer Controls
+
+ Hover to rotate the world.
+ Click a zone orb to focus its energy.
+ Track energy flow with the HUD meter.
+
+
+
+
+
+
+ Scene Highlights
+
+ Layered 3D transforms build a floating island without external 3D engines.
+ Orbiting emotion orbs are mapped to unique color and energy signatures.
+ Animated starfield and gradient lighting deepen the immersive feel.
+
+
+
+ );
+}
diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx
index 1628972..a0992f0 100644
--- a/frontend/src/components/Layout.tsx
+++ b/frontend/src/components/Layout.tsx
@@ -19,6 +19,7 @@ const navItems = [
{ label: 'Mood Collage', to: '/mood-collage' },
{ label: 'Next Steps', to: '/next-steps' },
{ label: 'Guided Imagery', to: '/guided-imagery' },
+ { label: 'Emotion Explorer 3D', to: '/emotion-explorer-3d' },
];
const ambient = new Howl({
diff --git a/frontend/src/components/TrajectoriesScene.tsx b/frontend/src/components/TrajectoriesScene.tsx
index 52f1996..1b07046 100644
--- a/frontend/src/components/TrajectoriesScene.tsx
+++ b/frontend/src/components/TrajectoriesScene.tsx
@@ -44,9 +44,8 @@ export default function TrajectoriesScene() {
position: 'absolute',
inset: 0,
transform: `translateX(${(idx + 1) * 10}%)`,
- background: `linear-gradient(90deg, transparent, ${lane.color}, transparent)`
- repeat
- scroll,
+ background: `linear-gradient(90deg, transparent, ${lane.color}, transparent)`,
+ backgroundRepeat: 'repeat',
animation: 'slide 6s linear infinite',
}}
/>
diff --git a/frontend/src/styles/global.css b/frontend/src/styles/global.css
index 4ceadff..ab3a0b2 100644
--- a/frontend/src/styles/global.css
+++ b/frontend/src/styles/global.css
@@ -63,6 +63,238 @@ select {
width: 100%;
}
+.emotion-explorer-3d {
+ display: grid;
+ gap: 16px;
+}
+
+.canvas-shell {
+ height: 70vh;
+ min-height: 420px;
+ border-radius: 24px;
+ overflow: hidden;
+ border: 1px solid rgba(255, 255, 255, 0.12);
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.35);
+ position: relative;
+ background: radial-gradient(circle at top, rgba(124, 212, 247, 0.12), transparent 60%),
+ radial-gradient(circle at bottom, rgba(11, 20, 40, 0.9), rgba(5, 8, 22, 0.95)),
+ #050814;
+ display: grid;
+ place-items: center;
+}
+
+.starfield {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.horizon {
+ position: relative;
+ width: 100%;
+ height: 100%;
+ transform-style: preserve-3d;
+ perspective: 1200px;
+ display: grid;
+ place-items: center;
+ transition: transform 0.4s ease;
+}
+
+.glow-ring {
+ position: absolute;
+ width: 380px;
+ height: 380px;
+ border-radius: 50%;
+ border: 1px solid rgba(124, 212, 247, 0.35);
+ box-shadow: 0 0 120px rgba(124, 212, 247, 0.3);
+ transform: translateZ(-80px);
+}
+
+.island-base,
+.island-plate,
+.island-core {
+ position: absolute;
+ border-radius: 999px;
+ transform-style: preserve-3d;
+}
+
+.island-base {
+ width: 360px;
+ height: 110px;
+ background: linear-gradient(180deg, #14283c, #0b1625);
+ transform: translateZ(-120px) rotateX(70deg);
+ box-shadow: 0 40px 80px rgba(0, 0, 0, 0.45);
+}
+
+.island-plate {
+ width: 320px;
+ height: 90px;
+ background: linear-gradient(180deg, #1e3b50, #102338);
+ transform: translateZ(-40px) rotateX(72deg);
+}
+
+.island-core {
+ width: 280px;
+ height: 70px;
+ background: linear-gradient(180deg, #2f5a6a, #152a3f);
+ transform: translateZ(40px) rotateX(78deg);
+ box-shadow: 0 20px 40px rgba(124, 212, 247, 0.2);
+}
+
+.bridge-steps {
+ position: absolute;
+ width: 200px;
+ height: 200px;
+ transform: translateZ(60px);
+}
+
+.bridge-steps span {
+ --step: 0;
+ position: absolute;
+ width: 40px;
+ height: 12px;
+ background: linear-gradient(90deg, rgba(246, 176, 244, 0.8), rgba(124, 212, 247, 0.8));
+ border-radius: 999px;
+ left: calc(12px + (var(--step) * 18px));
+ top: calc(140px - (var(--step) * 10px));
+ transform: rotateX(65deg) rotateZ(-8deg);
+ opacity: calc(1 - (var(--step) * 0.08));
+ box-shadow: 0 8px 14px rgba(0, 0, 0, 0.35);
+}
+
+.orbital-track {
+ position: absolute;
+ width: 420px;
+ height: 420px;
+ transform: translateZ(80px);
+}
+
+.emotion-orb {
+ --orbit: 0deg;
+ --orb-color: #7cd4f7;
+ position: absolute;
+ top: 50%;
+ left: 50%;
+ transform: rotate(var(--orbit)) translateX(190px) translateY(-50%);
+ background: transparent;
+ border: none;
+ display: grid;
+ place-items: center;
+ cursor: pointer;
+ color: var(--text);
+}
+
+.emotion-orb .orb-core {
+ width: 26px;
+ height: 26px;
+ border-radius: 50%;
+ background: radial-gradient(circle, #ffffff, var(--orb-color));
+ box-shadow: 0 0 18px var(--orb-color), 0 0 40px rgba(255, 255, 255, 0.3);
+ transition: transform 0.3s ease, box-shadow 0.3s ease;
+}
+
+.emotion-orb .orb-label {
+ margin-top: 8px;
+ font-size: 12px;
+ font-weight: 600;
+ text-shadow: 0 0 10px rgba(0, 0, 0, 0.6);
+}
+
+.emotion-orb.active .orb-core {
+ transform: scale(1.25);
+ box-shadow: 0 0 22px var(--orb-color), 0 0 60px var(--orb-color);
+}
+
+.tower {
+ position: absolute;
+ width: 80px;
+ height: 180px;
+ transform: translateZ(120px) translateY(-60px);
+ display: grid;
+ place-items: center;
+}
+
+.tower-top {
+ width: 60px;
+ height: 60px;
+ border-radius: 16px;
+ background: linear-gradient(145deg, #1b2e44, #0f1f35);
+ box-shadow: 0 16px 30px rgba(0, 0, 0, 0.5);
+}
+
+.tower-beam {
+ width: 6px;
+ height: 140px;
+ background: linear-gradient(180deg, rgba(124, 212, 247, 0.9), transparent);
+ margin-top: -10px;
+ box-shadow: 0 0 18px rgba(124, 212, 247, 0.6);
+ animation: beamPulse 4s ease-in-out infinite;
+}
+
+.arches {
+ position: absolute;
+ width: 420px;
+ height: 420px;
+ transform: translateZ(100px);
+}
+
+.arch {
+ --arch-index: 0;
+ --arch-color: #7cd4f7;
+ position: absolute;
+ width: 120px;
+ height: 120px;
+ border: 2px solid rgba(255, 255, 255, 0.25);
+ border-radius: 50%;
+ transform: rotate(calc(var(--arch-index) * 72deg)) translateX(150px);
+ box-shadow: 0 0 24px rgba(255, 255, 255, 0.15);
+ border-color: var(--arch-color);
+ opacity: 0.7;
+}
+
+.hud {
+ position: absolute;
+ inset: auto 24px 24px 24px;
+ display: grid;
+ grid-template-columns: repeat(auto-fit, minmax(220px, 1fr));
+ gap: 16px;
+ pointer-events: none;
+}
+
+.hud .panel {
+ pointer-events: auto;
+}
+
+.hud-meter {
+ display: grid;
+ gap: 6px;
+ margin-top: 12px;
+}
+
+.meter-track {
+ height: 10px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.1);
+ overflow: hidden;
+}
+
+.meter-fill {
+ height: 100%;
+ border-radius: inherit;
+ box-shadow: 0 0 12px rgba(255, 255, 255, 0.4);
+}
+
+@keyframes beamPulse {
+ 0%,
+ 100% {
+ opacity: 0.6;
+ }
+ 50% {
+ opacity: 1;
+ }
+}
+
button.primary {
background: linear-gradient(120deg, var(--accent), var(--accent-2));
border: none;