diff --git a/Week-2/Task-3/r3f/src/App.jsx b/Week-2/Task-3/r3f/src/App.jsx index 625ff5b..4ad0299 100644 --- a/Week-2/Task-3/r3f/src/App.jsx +++ b/Week-2/Task-3/r3f/src/App.jsx @@ -1,6 +1,177 @@ -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: Focus on object A', + detail: 'Camera frames object A only.', + cameraPosition: [0, 1.4, 5], + lookAt: [-1.2, 0, 0], + a: { position: [-1.2, 0, 0], rotation: [0, 0.25, 0], visible: true }, + b: { position: [1.4, 0, 0], rotation: [0, 0, 0], visible: false }, + }, + { + label: 'Step 2: Move camera + reveal object B', + detail: 'Camera shifts right and object B becomes visible.', + cameraPosition: [2.2, 1.4, 5], + lookAt: [1.4, 0, 0], + a: { position: [-1.2, 0, 0], rotation: [0.2, 0.8, 0], visible: true }, + b: { position: [1.4, 0, 0], rotation: [0, 0.2, 0], visible: true }, + }, + { + label: 'Step 3: Final combined scene', + detail: 'Both objects stay visible in one balanced view.', + cameraPosition: [0.3, 2.2, 7], + lookAt: [0, 0, 0], + a: { position: [-1.5, 0, 0], rotation: [0.3, 1.1, 0], visible: true }, + b: { position: [1.5, 0, 0], rotation: [0.1, 0.6, 0], visible: true }, + }, +] + +function GuidedScene({ stepIndex }) { + const { camera } = useThree() + const objectARef = useRef() + const objectBRef = useRef() + const initializedRef = useRef(false) + + const targets = useMemo( + () => ({ + cameraPosition: new THREE.Vector3(), + cameraLookAt: new THREE.Vector3(), + lookCurrent: new THREE.Vector3(), + aPosition: new THREE.Vector3(), + aRotation: new THREE.Euler(), + bPosition: new THREE.Vector3(), + bRotation: new THREE.Euler(), + }), + [] + ) + + useEffect(() => { + const step = STEPS[stepIndex] + + targets.cameraPosition.set(...step.cameraPosition) + targets.cameraLookAt.set(...step.lookAt) + + targets.aPosition.set(...step.a.position) + targets.aRotation.set(...step.a.rotation) + + targets.bPosition.set(...step.b.position) + targets.bRotation.set(...step.b.rotation) + + if (objectARef.current) objectARef.current.visible = step.a.visible + if (objectBRef.current) objectBRef.current.visible = step.b.visible + + if (!initializedRef.current && objectARef.current && objectBRef.current) { + camera.position.copy(targets.cameraPosition) + targets.lookCurrent.copy(targets.cameraLookAt) + objectARef.current.position.copy(targets.aPosition) + objectARef.current.rotation.copy(targets.aRotation) + objectBRef.current.position.copy(targets.bPosition) + objectBRef.current.rotation.copy(targets.bRotation) + initializedRef.current = true + } + }, [camera, stepIndex, targets]) + + useFrame(() => { + if (!objectARef.current || !objectBRef.current) return + + objectARef.current.position.lerp(targets.aPosition, 0.1) + objectBRef.current.position.lerp(targets.bPosition, 0.1) + + objectARef.current.rotation.x += (targets.aRotation.x - objectARef.current.rotation.x) * 0.1 + objectARef.current.rotation.y += (targets.aRotation.y - objectARef.current.rotation.y) * 0.1 + objectARef.current.rotation.z += (targets.aRotation.z - objectARef.current.rotation.z) * 0.1 + + objectBRef.current.rotation.x += (targets.bRotation.x - objectBRef.current.rotation.x) * 0.1 + objectBRef.current.rotation.y += (targets.bRotation.y - objectBRef.current.rotation.y) * 0.1 + objectBRef.current.rotation.z += (targets.bRotation.z - objectBRef.current.rotation.z) * 0.1 + + 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) + if (event.key === 'ArrowLeft') setStepIndex((value) => Math.max(value - 1, 0)) + if (event.key === 'ArrowRight') setStepIndex((value) => Math.min(value + 1, STEPS.length - 1)) + } + + window.addEventListener('keydown', onKeyDown) + return () => window.removeEventListener('keydown', onKeyDown) + }, []) + + return ( +
+
+

Step Guided Flow

+

{step.label}

+

{step.detail}

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

Use buttons or keys 1, 2, 3.

+
+ + + + + +
) } diff --git a/Week-2/Task-3/r3f/src/index.css b/Week-2/Task-3/r3f/src/index.css index 77c24e1..6b972c4 100644 --- a/Week-2/Task-3/r3f/src/index.css +++ b/Week-2/Task-3/r3f/src/index.css @@ -8,11 +8,94 @@ body, body { overflow: hidden; - background: #101418; + background: #eaf0f7; + 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: 290px; + 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; } \ No newline at end of file