121 lines
4.0 KiB
TypeScript
121 lines
4.0 KiB
TypeScript
import { useRef, useMemo } from 'react';
|
|
import { useFrame } from '@react-three/fiber';
|
|
import { Edges } from '@react-three/drei';
|
|
import * as THREE from 'three';
|
|
|
|
interface ResizableBox3DProps {
|
|
l1: number;
|
|
l2: number;
|
|
h: number;
|
|
}
|
|
|
|
function WindParticles({ l1, l2, h }: { l1: number, l2: number, h: number }) {
|
|
const count = 40;
|
|
const meshRef = useRef<THREE.InstancedMesh>(null);
|
|
|
|
// Create dummy object to compute matrix for each instance
|
|
const dummy = useMemo(() => new THREE.Object3D(), []);
|
|
|
|
// Generate random starting positions
|
|
const particles = useMemo(() => {
|
|
const temp = [];
|
|
for (let i = 0; i < count; i++) {
|
|
temp.push({
|
|
x: (Math.random() - 0.5) * (l1 * 3), // Spread wider than the building
|
|
y: Math.random() * (h * 1.5), // Spread higher than the building
|
|
z: (Math.random() * 40) + l2 / 2 + 5, // Start in front of the building
|
|
speed: 10 + Math.random() * 15, // Random speeds
|
|
offset: Math.random() * 100 // Phase offset
|
|
});
|
|
}
|
|
return temp;
|
|
}, [l1, l2, h, count]);
|
|
|
|
useFrame((_state, delta) => {
|
|
if (!meshRef.current) return;
|
|
|
|
particles.forEach((particle, i) => {
|
|
// Move particle along Z axis (wind direction)
|
|
particle.z -= particle.speed * delta;
|
|
|
|
// If particle passed the building, reset it to the front
|
|
if (particle.z < -l2 - 10) {
|
|
particle.z = (Math.random() * 20) + l2 / 2 + 10;
|
|
particle.x = (Math.random() - 0.5) * (l1 * 3);
|
|
particle.y = Math.random() * (h * 1.5);
|
|
}
|
|
|
|
// Simple avoidance / turbulence effect around the building
|
|
let currentX = particle.x;
|
|
let currentY = particle.y;
|
|
|
|
// If particle is close to the front face, push it out
|
|
if (particle.z > -l2/2 && particle.z < l2/2 + 2) {
|
|
const isHittingFront = Math.abs(particle.x) < l1/2 + 0.5 && particle.y < h + 0.5;
|
|
if (isHittingFront) {
|
|
// Push to the sides or up
|
|
if (Math.abs(particle.x) > (l1/2) * 0.7) {
|
|
currentX += Math.sign(particle.x) * (20 * delta);
|
|
} else {
|
|
currentY += 20 * delta;
|
|
}
|
|
}
|
|
}
|
|
|
|
dummy.position.set(currentX, currentY, particle.z);
|
|
dummy.scale.set(0.05, 0.05, Math.max(0.5, particle.speed * 0.1)); // Stretch by speed
|
|
dummy.updateMatrix();
|
|
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
|
});
|
|
|
|
meshRef.current.instanceMatrix.needsUpdate = true;
|
|
});
|
|
|
|
return (
|
|
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
|
<cylinderGeometry args={[1, 1, 1, 8]} />
|
|
<meshBasicMaterial color="#60a5fa" transparent opacity={0.4} />
|
|
</instancedMesh>
|
|
);
|
|
}
|
|
|
|
export function ResizableBox3D({ l1, l2, h }: ResizableBox3DProps) {
|
|
const meshRef = useRef<THREE.Mesh>(null);
|
|
|
|
// Create target scale and position based on inputs
|
|
const targetScale = useMemo(() => new THREE.Vector3(l1, h, l2), [l1, l2, h]);
|
|
|
|
useFrame((_state, delta) => {
|
|
if (meshRef.current) {
|
|
// Smoothly animate the scale
|
|
meshRef.current.scale.lerp(targetScale, delta * 5);
|
|
// Smoothly animate the position to keep the base on the ground
|
|
meshRef.current.position.lerp(
|
|
new THREE.Vector3(0, meshRef.current.scale.y / 2, 0),
|
|
delta * 5
|
|
);
|
|
}
|
|
});
|
|
|
|
return (
|
|
<group>
|
|
{/* Ground plane for reference */}
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, 0]} receiveShadow>
|
|
<planeGeometry args={[100, 100]} />
|
|
<meshStandardMaterial color="#333" depthWrite={false} transparent opacity={0.2} />
|
|
</mesh>
|
|
<gridHelper args={[100, 40, '#666', '#444']} position={[0, 0.01, 0]} />
|
|
|
|
{/* The Animated Box */}
|
|
<mesh ref={meshRef} castShadow receiveShadow>
|
|
<boxGeometry args={[1, 1, 1]} />
|
|
<meshStandardMaterial color="#4f46e5" transparent opacity={0.7} roughness={0.2} metalness={0.8} />
|
|
<Edges scale={1} threshold={15} color="#818cf8" />
|
|
</mesh>
|
|
|
|
{/* Wind Flow Animation */}
|
|
<WindParticles l1={l1} l2={l2} h={h} />
|
|
</group>
|
|
);
|
|
}
|