diff --git a/Week-2/Task-4/r3f/src/App.jsx b/Week-2/Task-4/r3f/src/App.jsx index 625ff5b..54e8e94 100644 --- a/Week-2/Task-4/r3f/src/App.jsx +++ b/Week-2/Task-4/r3f/src/App.jsx @@ -1,6 +1,172 @@ -export default function App() { +import { useEffect, useMemo, useRef, useState } from 'react' +import { Canvas, useFrame, useThree } from '@react-three/fiber' +import * as THREE from 'three' + +const STEPS = [ + { + label: 'Step 1: Group at rest', + detail: 'Parent group is centered and children keep their offsets.', + cameraPosition: [0, 1.7, 6], + lookAt: [0, 0, 0], + groupPosition: [0, 0, 0], + groupRotation: [0, 0, 0], + spinSpeed: 0, + }, + { + label: 'Step 2: Parent rotation', + detail: 'Only parent rotates; children move together without changing local layout.', + cameraPosition: [1.7, 1.8, 5.6], + lookAt: [0, 0, 0], + groupPosition: [0, 0, 0], + groupRotation: [0.15, 0.55, 0], + spinSpeed: 0.015, + }, + { + label: 'Step 3: Parent move + rotate', + detail: 'Parent shifts and rotates as one unit while child spacing stays intact.', + cameraPosition: [0.6, 2.4, 7], + lookAt: [0.4, 0.35, 0], + groupPosition: [0.7, 0.4, -0.4], + groupRotation: [0.35, 1.2, 0.2], + spinSpeed: 0.03, + }, +] + +function GuidedGroupScene({ stepIndex }) { + const { camera } = useThree() + const groupRef = useRef() + + const targets = useMemo( + () => ({ + cameraPosition: new THREE.Vector3(), + cameraLookAt: new THREE.Vector3(), + lookCurrent: new THREE.Vector3(), + groupPosition: new THREE.Vector3(), + groupRotation: new THREE.Euler(), + spinSpeed: 0, + initialized: false, + }), + [] + ) + + useEffect(() => { + const step = STEPS[stepIndex] + + targets.cameraPosition.set(...step.cameraPosition) + targets.cameraLookAt.set(...step.lookAt) + targets.groupPosition.set(...step.groupPosition) + targets.groupRotation.set(...step.groupRotation) + targets.spinSpeed = step.spinSpeed + + if (groupRef.current && !targets.initialized) { + camera.position.copy(targets.cameraPosition) + targets.lookCurrent.copy(targets.cameraLookAt) + groupRef.current.position.copy(targets.groupPosition) + groupRef.current.rotation.copy(targets.groupRotation) + targets.initialized = true + } + }, [camera, stepIndex, targets]) + + useFrame(() => { + if (!groupRef.current) return + + groupRef.current.position.lerp(targets.groupPosition, 0.1) + + groupRef.current.rotation.x += (targets.groupRotation.x - groupRef.current.rotation.x) * 0.1 + groupRef.current.rotation.y += (targets.groupRotation.y - groupRef.current.rotation.y) * 0.1 + groupRef.current.rotation.z += (targets.groupRotation.z - groupRef.current.rotation.z) * 0.1 + + groupRef.current.rotation.y += targets.spinSpeed * 0.2 + + camera.position.lerp(targets.cameraPosition, 0.08) + targets.lookCurrent.lerp(targets.cameraLookAt, 0.08) + camera.lookAt(targets.lookCurrent) + }) + return ( - <> + <> + + + + + + + + + + + + + + + + + + + + + + + + + + ) +} + +export default function App() { + const [stepIndex, setStepIndex] = useState(0) + const step = STEPS[stepIndex] + + useEffect(() => { + function onKeyDown(event) { + if (event.key === '1') setStepIndex(0) + if (event.key === '2') setStepIndex(1) + if (event.key === '3') setStepIndex(2) + } + + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, []) + + return ( +
+
+

Task 4: Group Motion

+

{step.label}

+

{step.detail}

+ +
+ {STEPS.map((_, index) => ( + + ))} +
+ +
+ + +
+ +

Simple parent group animation. Keys: 1, 2, 3.

+
+ + + + + +
) } diff --git a/Week-2/Task-4/r3f/src/index.css b/Week-2/Task-4/r3f/src/index.css index 379b86e..7fe707b 100644 --- a/Week-2/Task-4/r3f/src/index.css +++ b/Week-2/Task-4/r3f/src/index.css @@ -4,5 +4,98 @@ body, width: 100%; height: 100%; margin: 0; - background-color: black; +} + +body { + overflow: hidden; + background: #edf3fa; + font-family: "Avenir Next", "Segoe UI", sans-serif; +} + +canvas { + display: block; + width: 100%; + height: 100%; +} + +.app-shell { + position: relative; + width: 100%; + height: 100%; +} + +#guide { + position: fixed; + top: 14px; + left: 14px; + z-index: 10; + max-width: 300px; + background: rgba(255, 255, 255, 0.88); + border: 1px solid rgba(0, 0, 0, 0.1); + border-radius: 10px; + padding: 10px; + box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12); + display: flex; + flex-direction: column; + gap: 8px; +} + +#guide h2 { + margin: 0; + font-size: 14px; + color: #203448; +} + +#step-label { + margin: 0; + font-size: 13px; + font-weight: 700; + color: #1b2530; +} + +#step-detail { + margin: 0; + font-size: 12px; + color: #4f647a; + line-height: 1.35; +} + +.row { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +button { + border: 1px solid #cfd8e3; + background: #fff; + color: #203448; + border-radius: 8px; + padding: 7px 9px; + font-size: 12px; + cursor: pointer; + transition: all 0.2s ease; +} + +button:hover { + border-color: #8ea2b8; + transform: translateY(-1px); +} + +button.step-btn.active { + background: #203448; + border-color: #203448; + color: #f4f8fb; +} + +button:disabled { + opacity: 0.5; + cursor: not-allowed; + transform: none; +} + +#hint { + margin: 0; + font-size: 11px; + color: #566f86; }