JS
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="importmap">
{
"imports": {
"react": "https://esm.sh/react@18.2.0",
"react/": "https://esm.sh/react@18.2.0/",
"react-dom": "https://esm.sh/react-dom@18.2.0",
"react-dom/client": "https://esm.sh/react-dom@18.2.0/client",
"three": "https://esm.sh/three@0.160.0",
"@react-three/fiber": "https://esm.sh/@react-three/fiber@8.15.16?external=react,react-dom,three",
"@react-three/drei": "https://esm.sh/@react-three/drei@9.99.0?external=react,react-dom,three,@react-three/fiber",
"@react-three/postprocessing": "https://esm.sh/@react-three/postprocessing@2.16.0?external=react,react-dom,three,@react-three/fiber",
"postprocessing": "https://esm.sh/postprocessing@6.35.0?external=three",
"uuid": "https://esm.sh/uuid@9.0.1"
}
}
</script>
<script type="text/babel" data-type="module" data-presets="react, typescript">
import React, { useRef, useState, useMemo, useEffect, Suspense, useLayoutEffect } from 'react';
import ReactDOM from 'react-dom/client';
import { Canvas, useFrame, useThree } from '@react-three/fiber';
import { EffectComposer, Bloom } from '@react-three/postprocessing';
import * as THREE from 'three';
// ------------------------------------------------------------------
// COMPONENT: Fireflies (Unchanged)
// ------------------------------------------------------------------
const fireflyVertexShader = `
uniform float uTime;
attribute float aScale;
attribute vec3 aOffset;
varying float vAlpha;
varying float vPhase;
void main() {
vec3 pos = position;
float t = uTime * 0.5;
pos.x += sin(t + aOffset.x) * 3.0 + sin(t * 0.3 + aOffset.y) * 1.5;
pos.y += cos(t * 0.8 + aOffset.y) * 1.5 + sin(t * 0.2 + aOffset.x) * 0.5;
pos.z += sin(t * 0.6 + aOffset.z) * 3.0;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
gl_PointSize = (300.0 * aScale) / -mvPosition.z;
vPhase = aOffset.x;
vAlpha = 1.0;
}
`;
const fireflyFragmentShader = `
uniform float uTime;
varying float vAlpha;
varying float vPhase;
void main() {
vec2 uv = gl_PointCoord.xy - 0.5;
float r = length(uv);
if (r > 0.5) discard;
float glow = 1.0 - smoothstep(0.0, 0.5, r);
glow = pow(glow, 2.0);
float flicker = 0.6 + 0.4 * sin(uTime * 3.0 + vPhase * 10.0);
float flash = smoothstep(0.8, 1.0, sin(uTime * 1.0 + vPhase * 20.0));
float intensity = flicker + flash;
vec3 color = vec3(1.0, 0.9, 0.4);
gl_FragColor = vec4(color, glow * vAlpha * intensity);
}
`;
const Fireflies = ({ count = 15 }) => {
const points = useRef(null);
const uniforms = useRef({ uTime: { value: 0 } });
const { positions, scales, offsets } = useMemo(() => {
const p = new Float32Array(count * 3);
const s = new Float32Array(count);
const o = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
p[i * 3] = (Math.random() - 0.5) * 20 - 5;
p[i * 3 + 1] = Math.random() * 8 + 1;
p[i * 3 + 2] = (Math.random() - 0.5) * 40 - 5;
s[i] = Math.random() * 0.5 + 0.5;
o[i * 3] = Math.random() * 100;
o[i * 3 + 1] = Math.random() * 100;
o[i * 3 + 2] = Math.random() * 100;
}
return { positions: p, scales: s, offsets: o };
}, [count]);
useFrame((state) => {
uniforms.current.uTime.value = state.clock.getElapsedTime();
});
return (
<points ref={points}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" count={count} array={positions} itemSize={3} />
<bufferAttribute attach="attributes-aScale" count={count} array={scales} itemSize={1} />
<bufferAttribute attach="attributes-aOffset" count={count} array={offsets} itemSize={3} />
</bufferGeometry>
<shaderMaterial
vertexShader={fireflyVertexShader}
fragmentShader={fireflyFragmentShader}
uniforms={uniforms.current}
transparent={true}
depthWrite={false}
depthTest={true}
blending={THREE.AdditiveBlending}
/>
</points>
);
};
// ------------------------------------------------------------------
// COMPONENT: Dust (Unchanged)
// ------------------------------------------------------------------
const dustVertexShader = `
uniform float uTime;
attribute float aScale;
attribute vec3 aRandom;
varying float vAlpha;
void main() {
vec3 pos = position;
float speed = 0.2 + aRandom.x * 0.3;
float yOffset = mod(pos.y + uTime * speed + aRandom.y * 10.0, 25.0) - 2.0;
pos.y = yOffset;
pos.x += sin(uTime * 0.3 + aRandom.z * 10.0) * 0.5;
pos.z += cos(uTime * 0.2 + aRandom.x * 10.0) * 0.5;
vec4 mvPosition = modelViewMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
gl_PointSize = (150.0 * aScale) / -mvPosition.z;
float dist = length(mvPosition.xyz);
float nearFade = smoothstep(0.5, 3.0, dist);
float farFade = 1.0 - smoothstep(30.0, 50.0, dist);
vAlpha = nearFade * farFade * 0.15;
}
`;
const dustFragmentShader = `
varying float vAlpha;
void main() {
vec2 uv = gl_PointCoord.xy - 0.5;
float r = length(uv);
if (r > 0.5) discard;
float glow = 1.0 - smoothstep(0.0, 0.5, r);
vec3 dustColor = vec3(0.8, 0.75, 0.6);
gl_FragColor = vec4(dustColor, glow * vAlpha);
}
`;
const Dust = ({ count }) => {
const points = useRef(null);
const uniforms = useRef({ uTime: { value: 0 } });
const { positions, scales, randoms } = useMemo(() => {
const p = new Float32Array(count * 3);
const s = new Float32Array(count);
const r = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
p[i * 3] = (Math.random() - 0.5) * 60;
p[i * 3 + 1] = Math.random() * 20;
p[i * 3 + 2] = (Math.random() - 0.5) * 60 - 10;
s[i] = Math.random() * 0.5 + 0.5;
r[i * 3] = Math.random();
r[i * 3 + 1] = Math.random();
r[i * 3 + 2] = Math.random();
}
return { positions: p, scales: s, randoms: r };
}, [count]);
useFrame((state) => {
uniforms.current.uTime.value = state.clock.getElapsedTime();
});
return (
<points ref={points}>
<bufferGeometry>
<bufferAttribute attach="attributes-position" count={count} array={positions} itemSize={3} />
<bufferAttribute attach="attributes-aScale" count={count} array={scales} itemSize={1} />
<bufferAttribute attach="attributes-aRandom" count={count} array={randoms} itemSize={3} />
</bufferGeometry>
<shaderMaterial
vertexShader={dustVertexShader}
fragmentShader={dustFragmentShader}
uniforms={uniforms.current}
transparent={true}
depthWrite={false}
blending={THREE.AdditiveBlending}
/>
</points>
);
};
// ------------------------------------------------------------------
// COMPONENT: OrganicGrass (Unchanged)
// ------------------------------------------------------------------
const grassVertexShader = `
uniform float uTime;
uniform vec3 uLionPos;
uniform float uScroll;
attribute float aScale;
attribute float aCurve;
attribute float aLean;
varying vec2 vUv;
varying float vHeight;
varying float vVariation;
varying float vDist;
varying vec3 vWorldPos;
vec3 mod289(vec3 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec2 mod289(vec2 x) { return x - floor(x * (1.0 / 289.0)) * 289.0; }
vec3 permute(vec3 x) { return mod289(((x*34.0)+1.0)*x); }
float snoise(vec2 v) {
const vec4 C = vec4(0.211324865405187, 0.366025403784439, -0.577350269189626, 0.024390243902439);
vec2 i = floor(v + dot(v, C.yy) );
vec2 x0 = v - i + dot(i, C.xx);
vec2 i1 = (x0.x > x0.y) ? vec2(1.0, 0.0) : vec2(0.0, 1.0);
vec4 x12 = x0.xyxy + C.xxzz;
x12.xy -= i1;
i = mod289(i);
vec3 p = permute( permute( i.y + vec3(0.0, i1.y, 1.0 )) + i.x + vec3(0.0, i1.x, 1.0 ));
vec3 m = max(0.5 - vec3(dot(x0,x0), dot(x12.xy,x12.xy), dot(x12.zw,x12.zw)), 0.0);
m = m*m ; m = m*m ;
vec3 x = 2.0 * fract(p * C.www) - 1.0;
vec3 h = abs(x) - 0.5;
vec3 ox = floor(x + 0.5);
vec3 a0 = x - ox;
m *= 1.79284291400159 - 0.85373472095314 * ( a0*a0 + h*h );
vec3 g;
g.x = a0.x * x0.x + h.x * x0.y;
g.yz = a0.yz * x12.xz + h.yz * x12.yw;
return 130.0 * dot(m, g);
}
void main() {
vUv = uv;
vHeight = uv.y;
vVariation = aCurve;
vec3 pos = position;
float widthTaper = smoothstep(1.0, 0.0, uv.y);
pos.x *= widthTaper;
vec4 instancePos = instanceMatrix * vec4(0.0, 0.0, 0.0, 1.0);
vec4 worldPos = modelMatrix * instancePos;
vWorldPos = worldPos.xyz;
float dist = distance(cameraPosition, worldPos.xyz);
vDist = dist;
float noiseFreq = 0.05;
float timeSpeed = 0.2;
float turbulence = snoise(instancePos.xz * noiseFreq + uTime * timeSpeed);
float leanAmount = (aLean + turbulence) * uv.y * uv.y;
pos.x += leanAmount;
pos.z += leanAmount * 0.3;
float distToLion = distance(worldPos.xyz, uLionPos);
float presenceRadius = 3.5;
float presence = smoothstep(presenceRadius, 0.0, distToLion) * smoothstep(0.1, 0.6, uScroll);
float randomStatic = fract(sin(dot(instancePos.xz, vec2(12.9898, 78.233))) * 43758.5453) - 0.5;
pos.x += randomStatic * 1.0 * presence * uv.y;
pos.z += randomStatic * 1.0 * presence * uv.y;
float partRadius = 2.5;
float partProgress = smoothstep(0.6, 0.95, uScroll);
float partStrength = smoothstep(partRadius, 0.0, distToLion) * partProgress;
vec2 dirFromLion = normalize(worldPos.xz - uLionPos.xz);
float pushDistance = 2.0;
pos.x += dirFromLion.x * partStrength * pushDistance * uv.y;
pos.z += dirFromLion.y * partStrength * pushDistance * uv.y;
float flatten = 1.0 - (partStrength * 0.8);
pos.y *= flatten;
pos.y *= aScale;
float thicken = 1.0 + smoothstep(10.0, 50.0, dist) * 1.5;
pos.x *= thicken;
vec4 mvPosition = modelViewMatrix * instanceMatrix * vec4(pos, 1.0);
gl_Position = projectionMatrix * mvPosition;
}
`;
const grassFragmentShader = `
varying vec2 vUv;
varying float vHeight;
varying float vVariation;
varying float vDist;
varying vec3 vWorldPos;
uniform float uDensityForeground;
uniform float uDensityMidground;
uniform float uDensityBackground;
uniform vec3 uColorDeep;
uniform vec3 uColorVibrant;
uniform vec3 uColorGrey;
uniform vec3 uLionPos;
uniform float uScroll;
uniform float uTime;
float rand(vec2 co){
return fract(sin(dot(co.xy ,vec2(12.9898,78.233))) * 43758.5453);
}
void main() {
float density = 1.0;
if (vDist < 20.0) {
density = mix(uDensityForeground, uDensityMidground, vDist / 20.0);
} else if (vDist < 60.0) {
density = mix(uDensityMidground, uDensityBackground, (vDist - 20.0) / 40.0);
} else {
density = uDensityBackground;
}
float randomSeed = rand(vec2(vVariation, 1.0));
if (randomSeed > density) discard;
float shape = 1.0 - abs(vUv.x - 0.5) * 2.0;
if (shape < 0.2) discard;
vec3 tipColor = mix(uColorVibrant, uColorGrey, vVariation);
float brightness = 0.8 + 0.4 * fract(vVariation * 43.21);
tipColor *= brightness;
float rootDarkness = 0.5 + 0.5 * fract(vVariation * 12.34);
vec3 baseColor = uColorDeep * rootDarkness;
vec3 finalColor = mix(baseColor, tipColor, smoothstep(0.0, 1.0, vHeight));
float fakeLight = 0.8 + 0.4 * smoothstep(0.0, 1.0, vUv.x);
finalColor *= fakeLight;
float distToLion = distance(vWorldPos, uLionPos);
float hintRadius = 3.5;
float hintStrength = smoothstep(hintRadius, 0.0, distToLion) * smoothstep(0.1, 0.6, uScroll);
vec3 hintColor = vec3(0.55, 0.45, 0.25);
float dangerRadius = 2.2;
float dangerStrength = smoothstep(dangerRadius, 0.0, distToLion) * smoothstep(0.6, 1.0, uScroll);
vec3 dangerColor = vec3(0.9, 0.3, 0.0);
finalColor = mix(finalColor, hintColor, hintStrength * 0.7);
finalColor = mix(finalColor, dangerColor, dangerStrength * 0.9);
float darkness = smoothstep(20.0, 70.0, vDist);
finalColor = mix(finalColor, vec3(0.0), darkness);
gl_FragColor = vec4(finalColor, 1.0);
}
`;
const groundPlane = new THREE.Plane(new THREE.Vector3(0, 1, 0), 0);
const targetLionPos = new THREE.Vector3();
const tempVec = new THREE.Vector3();
const OrganicGrass = ({ count, densities, colors, scrollRef }) => {
const meshRef = useRef(null);
const { camera, raycaster, pointer } = useThree();
const uniforms = useRef({
uTime: { value: 0 },
uScroll: { value: 0 },
uLionPos: { value: new THREE.Vector3(0, 0, -25) },
uDensityForeground: { value: 1.0 },
uDensityMidground: { value: 0.5 },
uDensityBackground: { value: 0.1 },
uColorDeep: { value: new THREE.Color('#1b5605') },
uColorVibrant: { value: new THREE.Color('#386a25') },
uColorGrey: { value: new THREE.Color('#656b63') }
});
const { scales, leans, curves } = useMemo(() => {
const s = new Float32Array(count);
const l = new Float32Array(count);
const c = new Float32Array(count);
for (let i = 0; i < count; i++) {
s[i] = 0.5 + Math.random() * 2.0;
l[i] = (Math.random() - 0.5) * 2.0;
c[i] = Math.random();
}
return { scales: s, leans: l, curves: c };
}, [count]);
useLayoutEffect(() => {
if (!meshRef.current) return;
const temp = new THREE.Object3D();
for (let i = 0; i < count; i++) {
let x;
const rDist = Math.random();
if (rDist < 0.5) {
x = (Math.random() - 0.5) * 30;
} else if (rDist < 0.8) {
x = (Math.random() - 0.5) * 60;
} else {
x = (Math.random() - 0.5) * 120;
}
const z = 15 - Math.random() * 60;
temp.position.set(x, 0, z);
temp.rotation.y = Math.random() * Math.PI * 2;
temp.updateMatrix();
meshRef.current.setMatrixAt(i, temp.matrix);
}
meshRef.current.instanceMatrix.needsUpdate = true;
}, [count]);
useFrame((state) => {
uniforms.current.uTime.value = state.clock.getElapsedTime();
uniforms.current.uScroll.value = scrollRef.current;
if (scrollRef.current > 0.75) {
raycaster.setFromCamera(state.pointer, camera);
const hit = raycaster.ray.intersectPlane(groundPlane, tempVec);
if (hit) {
targetLionPos.copy(hit);
}
} else {
targetLionPos.set(0, 0, -25);
}
uniforms.current.uLionPos.value.lerp(targetLionPos, 0.04);
uniforms.current.uDensityForeground.value = densities.f;
uniforms.current.uDensityMidground.value = densities.m;
uniforms.current.uDensityBackground.value = densities.b;
uniforms.current.uColorDeep.value.set(colors.deep);
uniforms.current.uColorVibrant.value.set(colors.vibrant);
uniforms.current.uColorGrey.value.set(colors.grey);
});
return (
<instancedMesh ref={meshRef} args={[null, null, count]} frustumCulled={false}>
<planeGeometry args={[0.06, 1.0, 1, 4]}>
<instancedBufferAttribute attach="attributes-aScale" args={[scales, 1]} />
<instancedBufferAttribute attach="attributes-aLean" args={[leans, 1]} />
<instancedBufferAttribute attach="attributes-aCurve" args={[curves, 1]} />
</planeGeometry>
<shaderMaterial
vertexShader={grassVertexShader}
fragmentShader={grassFragmentShader}
uniforms={uniforms.current}
side={THREE.DoubleSide}
depthWrite={true}
transparent={false}
/>
</instancedMesh>
);
};
// ------------------------------------------------------------------
// COMPONENT: VignetteOverlay
// ------------------------------------------------------------------
const VignetteOverlay = ({ strength, radius, noise, darkness }) => {
const innerStop = Math.max(0, radius * 100);
const outerStop = Math.min(150, (radius + 0.5) * 100);
const blurStyle = {
WebkitBackdropFilter: `blur(${strength * 4}px)`,
backdropFilter: `blur(${strength * 4}px)`,
WebkitMaskImage: `radial-gradient(circle, transparent ${innerStop}%, black ${outerStop}%)`,
maskImage: `radial-gradient(circle, transparent ${innerStop}%, black ${outerStop}%)`,
};
const darkStyle = {
background: `radial-gradient(circle, transparent ${innerStop}%, rgba(0,0,0, ${darkness}) ${outerStop}%)`,
};
const noiseBg = `url("data:image/svg+xml,%3Csvg viewBox='0 0 200 200' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.8' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)' opacity='1'/%3E%3C/svg%3E")`;
return (
<React.Fragment>
<div className="fixed inset-0 pointer-events-none z-[5] transition-all duration-100 ease-linear" style={blurStyle} />
<div className="fixed inset-0 pointer-events-none z-[6] transition-all duration-100 ease-linear" style={darkStyle} />
<div className="fixed inset-0 pointer-events-none z-[7] mix-blend-overlay opacity-50 transition-opacity duration-100 ease-linear" style={{ backgroundImage: noiseBg, opacity: noise, backgroundSize: '100px 100px' }} />
</React.Fragment>
);
};
// ------------------------------------------------------------------
// UPDATED: ScrollManager (Logic Only)
// ------------------------------------------------------------------
const ScrollManager = ({ scrollRef }) => {
useEffect(() => {
const handleScroll = () => {
// This must match the height of your Webflow 'Scrolly Track' (400vh)
const trackHeight = window.innerHeight * 4;
const scrollableDistance = trackHeight - window.innerHeight;
const currentScroll = window.scrollY;
let progress = currentScroll / scrollableDistance;
progress = Math.min(Math.max(progress, 0), 1);
scrollRef.current = progress;
};
window.addEventListener('scroll', handleScroll);
handleScroll();
return () => window.removeEventListener('scroll', handleScroll);
}, [scrollRef]);
return null;
};
const CameraRig = ({ scrollRef }) => {
const { camera } = useThree();
const currentPos = useRef(new THREE.Vector3(-4, 4, 14));
const currentLook = useRef(new THREE.Vector3(0, 0, 0));
useFrame((state) => {
const r = scrollRef.current;
const t = state.clock.getElapsedTime();
const p1 = new THREE.Vector3(-4, 4, 14);
const p2 = new THREE.Vector3(-7, 2, -5);
const p3 = new THREE.Vector3(-10, 20, -20);
const l1 = new THREE.Vector3(5, 0, -5);
const l2 = new THREE.Vector3(0, 0.5, -25);
const l3 = new THREE.Vector3(0, -10, -20);
let targetPos = new THREE.Vector3();
let targetLook = new THREE.Vector3();
if (r < 0.5) {
const n = r * 2;
const ease = n * n;
targetPos.lerpVectors(p1, p2, ease);
targetLook.lerpVectors(l1, l2, ease);
} else {
const n = (r - 0.5) * 2;
const ease = 1 - Math.pow(1 - n, 3);
targetPos.lerpVectors(p2, p3, ease);
targetLook.lerpVectors(l2, l3, ease);
}
targetPos.y += Math.sin(t * 0.5) * 0.2;
currentPos.current.lerp(targetPos, 0.05);
currentLook.current.lerp(targetLook, 0.05);
camera.position.copy(currentPos.current);
camera.lookAt(currentLook.current);
});
return null;
};
// ------------------------------------------------------------------
// UPDATED: App Component (Clean)
// ------------------------------------------------------------------
const App = () => {
const scrollRef = useRef(0);
const [lomoConfig] = useState({ strength: 5.8, radius: 0.47, noise: 0.3, darkness: 0.35 });
const densities = { f: 1.0, m: 0.5, b: 0.1 };
const colors = { deep: '#061400', vibrant: '#7fa446', grey: '#663915' };
return (
<div className="w-full h-full relative bg-black text-white selection:bg-amber-500 selection:text-black">
<ScrollManager scrollRef={scrollRef} />
<div className="absolute inset-0 w-full h-full z-0">
<Canvas flat dpr={[1, 1.5]} gl={{ antialias: false, powerPreference: "high-performance", depth: true }} camera={{ fov: 45, near: 0.1, far: 100 }}>
<color attach="background" args={['#000000']} />
<Suspense fallback={null}>
<CameraRig scrollRef={scrollRef} />
<ambientLight intensity={0.1} />
<spotLight position={[10, 20, 10]} angle={0.5} penumbra={1} intensity={2} color="#ffffff" />
<pointLight position={[-10, 2, -10]} intensity={0.5} color="#D4AF37" />
<OrganicGrass count={100000} densities={densities} colors={colors} scrollRef={scrollRef} />
<Dust count={500} />
<Fireflies count={15} />
</Suspense>
<EffectComposer enableNormalPass={false}>
<Bloom luminanceThreshold={0.2} mipmapBlur intensity={1.0} radius={0.6} />
</EffectComposer>
</Canvas>
</div>
<VignetteOverlay strength={lomoConfig.strength} radius={lomoConfig.radius} noise={lomoConfig.noise} darkness={lomoConfig.darkness} />
</div>
);
};
// ------------------------------------------------------------------
// RENDER
// ------------------------------------------------------------------
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);
</script>