feat(how-it-works): integrate scroll-driven 3D experience
Migrate the standalone Vite + React Three Fiber experience into the existing Next.js site as the body of the How It Works page, replacing the Miles3 / WhyChooseDoormile / TheDoormileWay content sections while preserving the Elementor hero, global Header/Footer, layout, routing and SEO. - New self-contained module: src/modules/how-it-works-3d/ (R3F scene, hooks, zustand store, animations, curves, constants, utils, scoped CSS). App.jsx → Experience3D.jsx; 3d_scene.jsx → models/Scene3D.jsx. - 32MB GLB moved to public/models/3d_scene_final.glb; useGLTF paths updated. - Client-only entry via dynamic ssr:false loader (Experience3DLoader). - Self-managed fixed pin (tall section + absolute stage toggled absolute(top)→fixed→absolute(bottom) from ScrollTrigger pin state), mirroring the site's StrategySection, since the fixed header + ancestor overflow:hidden break CSS sticky / GSAP pin. - experience.css fully scoped under .dm-hiw-3d to avoid colliding with the site's Elementor CSS. - Global Lenis disabled on /how-it-works; module runs its own tuned Lenis; jump-to-section scroll math made spacer-relative. - Added zustand + maath; ESLint-ignored the ported module. Rendering fixes (root causes found by driving headless Chrome): - Bump three 0.171 → 0.184 to match @react-three/fiber@9.6 / drei@10.7 / postprocessing@6.39 (0.171 silently failed to render this GLB and caused the EffectComposer getContextAttributes().alpha crash). Other 3D routes verified. - EffectComposer: Bloom + Vignette only. SSAO needs a NormalPass (v3 dropped the old `disableNormalPass`), and that extra full-scene pass exhausted the WebGL context on this heavy scene. - Cap Canvas dpr to [1,1.5] to bound framebuffer memory on retina displays. - Defer Canvas mount via IntersectionObserver (mountScene), matching StrategySection, to ease StrictMode/first-render GPU pressure. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
93
src/modules/how-it-works-3d/components/StreetLights.jsx
Normal file
93
src/modules/how-it-works-3d/components/StreetLights.jsx
Normal file
@@ -0,0 +1,93 @@
|
||||
import React, { useRef, useEffect } from 'react'
|
||||
import { useFrame } from '@react-three/fiber'
|
||||
import * as THREE from 'three'
|
||||
import { useSceneStore } from '../store/useSceneStore'
|
||||
|
||||
// The exact calculated world coordinates of the 10 street light heads in the scene
|
||||
const streetLightsData = [
|
||||
{ pos: [0, 4.2, -4.56], target: [0, 0, -4.56] },
|
||||
{ pos: [9.113, 4.2, 0.944], target: [9.113, 0, 0.944] },
|
||||
{ pos: [-10.158, 4.2, -9.874], target: [-10.158, 0, -9.874] },
|
||||
{ pos: [3.513, 4.2, 9.195], target: [3.513, 0, 9.195] },
|
||||
{ pos: [3.96, 4.2, -21.17], target: [3.96, 0, -21.17] },
|
||||
{ pos: [12.25, 4.2, -16.7], target: [12.25, 0, -16.7] },
|
||||
{ pos: [3.052, 4.2, -12.335], target: [3.052, 0, -12.335] },
|
||||
{ pos: [-2.03, 4.2, -16.89], target: [-2.03, 0, -16.89] },
|
||||
{ pos: [-27.151, 3.98, -9], target: [-27.151, 0, -9] }
|
||||
]
|
||||
|
||||
const bulbOffColor = new THREE.Color('#333333')
|
||||
const bulbOnColor = new THREE.Color('#ffdf6d')
|
||||
const emissiveOffColor = new THREE.Color('#000000')
|
||||
const emissiveOnColor = new THREE.Color('#ffdf6d')
|
||||
|
||||
function SingleStreetLight({ pos, targetPos }) {
|
||||
const lightRef = useRef()
|
||||
const targetRef = useRef()
|
||||
const bulbRef = useRef()
|
||||
|
||||
useEffect(() => {
|
||||
if (lightRef.current && targetRef.current) {
|
||||
lightRef.current.target = targetRef.current
|
||||
lightRef.current.target.updateMatrixWorld()
|
||||
}
|
||||
}, [])
|
||||
|
||||
useFrame(() => {
|
||||
// Day-to-Night factor (disabled: streetlights stay off)
|
||||
const nightFactor = 0
|
||||
|
||||
// Smoothly scale spotlights intensity
|
||||
if (lightRef.current) {
|
||||
lightRef.current.intensity = nightFactor * 12.0
|
||||
}
|
||||
|
||||
// Interpolate light bulb material colors to simulate glowing filament
|
||||
if (bulbRef.current) {
|
||||
bulbRef.current.material.color.lerpColors(bulbOffColor, bulbOnColor, nightFactor)
|
||||
bulbRef.current.material.emissive.lerpColors(emissiveOffColor, emissiveOnColor, nightFactor)
|
||||
}
|
||||
})
|
||||
|
||||
return (
|
||||
<group>
|
||||
{/* Spotlight casting cone of light downward */}
|
||||
<spotLight
|
||||
ref={lightRef}
|
||||
position={pos}
|
||||
intensity={0}
|
||||
distance={12}
|
||||
angle={Math.PI / 4.5}
|
||||
penumbra={0.6}
|
||||
decay={1.2}
|
||||
color="#ffdf6d"
|
||||
castShadow={false} // Disabled for peak frame rate, main shadow is cast by directionalLight
|
||||
/>
|
||||
{/* Glowing bulb mesh placed exactly at the light coordinates */}
|
||||
<mesh ref={bulbRef} position={pos}>
|
||||
<sphereGeometry args={[0.16, 16, 16]} />
|
||||
<meshStandardMaterial
|
||||
color="#333333"
|
||||
emissive="#000000"
|
||||
emissiveIntensity={3.5}
|
||||
roughness={0.1}
|
||||
/>
|
||||
</mesh>
|
||||
<object3D ref={targetRef} position={targetPos} />
|
||||
</group>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(function StreetLights() {
|
||||
return (
|
||||
<group>
|
||||
{streetLightsData.map((light, index) => (
|
||||
<SingleStreetLight
|
||||
key={index}
|
||||
pos={light.pos}
|
||||
targetPos={light.target}
|
||||
/>
|
||||
))}
|
||||
</group>
|
||||
)
|
||||
})
|
||||
Reference in New Issue
Block a user