🚀 Auto-deploy: BrainWind atualizado em 29/07/2026 10:27:37
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
import { useRef, useMemo, useEffect } from 'react';
|
||||
import { useFrame } from '@react-three/fiber';
|
||||
import * as THREE from 'three';
|
||||
import { useCanvasTheme } from '../../lib/theme';
|
||||
|
||||
export interface CylinderAirflowSystemProps {
|
||||
diameter: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export function CylinderAirflowSystem({ diameter, height }: CylinderAirflowSystemProps) {
|
||||
const isDark = useCanvasTheme() === 'dark';
|
||||
const count = 450;
|
||||
const meshRef = useRef<THREE.InstancedMesh>(null);
|
||||
const R = diameter / 2;
|
||||
|
||||
const particles = useMemo(() => {
|
||||
const temp = [];
|
||||
const spanX = Math.max(15, R * 5);
|
||||
const spanZ = Math.max(10, R * 3.5);
|
||||
for (let i = 0; i < count; i++) {
|
||||
temp.push({
|
||||
position: new THREE.Vector3(
|
||||
(Math.random() - 0.5) * spanX * 2,
|
||||
Math.random() * (height * 1.3),
|
||||
(Math.random() - 0.5) * spanZ * 2
|
||||
),
|
||||
baseSpeed: 0.12 + Math.random() * 0.08,
|
||||
wobbleSpeed: Math.random() * 0.05,
|
||||
wobbleOffset: Math.random() * Math.PI * 2,
|
||||
currentCpe: 0,
|
||||
});
|
||||
}
|
||||
return temp;
|
||||
}, [count, R, height]);
|
||||
|
||||
const dummy = useMemo(() => new THREE.Object3D(), []);
|
||||
const colorObj = useMemo(() => new THREE.Color(), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (meshRef.current) {
|
||||
for (let i = 0; i < count; i++) {
|
||||
meshRef.current.setColorAt(i, new THREE.Color('#ffffff'));
|
||||
}
|
||||
if (meshRef.current.instanceColor) {
|
||||
meshRef.current.instanceColor.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}, [count]);
|
||||
|
||||
useFrame((state) => {
|
||||
if (!meshRef.current) return;
|
||||
const time = state.clock.elapsedTime;
|
||||
const spanX = Math.max(15, R * 5);
|
||||
const spanZ = Math.max(10, R * 3.5);
|
||||
|
||||
particles.forEach((p, i) => {
|
||||
let deflectX = 1.0;
|
||||
let deflectY = 0;
|
||||
let deflectZ = 0;
|
||||
|
||||
const px = p.position.x;
|
||||
const py = p.position.y;
|
||||
const pz = p.position.z;
|
||||
|
||||
const r = Math.sqrt(px * px + pz * pz);
|
||||
const safeR = Math.max(R + 0.3, r);
|
||||
|
||||
// Proteção para não entrar no cilindro
|
||||
if (r < R + 0.25 && py <= height) {
|
||||
const angle = Math.atan2(pz, px);
|
||||
p.position.x = Math.cos(angle) * (R + 0.25);
|
||||
p.position.z = Math.sin(angle) * (R + 0.25);
|
||||
}
|
||||
|
||||
// Cálculo aerodinâmico (escoamento potencial ao redor de cilindro)
|
||||
if (py <= height * 1.05 && r < R * 3.0) {
|
||||
const r2 = safeR * safeR;
|
||||
const R2 = R * R;
|
||||
// Velocidades de escoamento potencial em 2D ao redor do cilindro
|
||||
deflectX = 1 - (R2 * (px * px - pz * pz)) / (r2 * r2);
|
||||
deflectZ = -(2 * R2 * px * pz) / (r2 * r2);
|
||||
|
||||
// Esteira turbulenta na região à jusante (+X)
|
||||
if (px > 0 && Math.abs(pz) < R * 1.3) {
|
||||
deflectZ += Math.sin(time * 5 + px * 0.8 + p.wobbleOffset) * 0.35;
|
||||
deflectX = Math.max(0.4, deflectX);
|
||||
}
|
||||
}
|
||||
|
||||
// Determina coeficiente para cor da partícula
|
||||
if (py <= height && r < R * 2.2) {
|
||||
if (px < -R * 0.3 && Math.abs(pz) < R * 0.8) {
|
||||
p.currentCpe = 0.8; // Barlavento (pressão positiva)
|
||||
} else if (Math.abs(px) <= R * 0.6 && r < R * 1.5) {
|
||||
p.currentCpe = -1.2; // Laterais (forte sucção)
|
||||
} else if (px > R * 0.3 && Math.abs(pz) < R * 1.2) {
|
||||
p.currentCpe = -0.5; // Esteira (sucção moderada)
|
||||
} else {
|
||||
p.currentCpe = 0;
|
||||
}
|
||||
} else {
|
||||
p.currentCpe = 0;
|
||||
}
|
||||
|
||||
const moveVec = new THREE.Vector3(deflectX, deflectY, deflectZ).normalize();
|
||||
const currentSpeed = p.baseSpeed * Math.max(0.3, Math.min(1.5, deflectX));
|
||||
|
||||
p.position.addScaledVector(moveVec, currentSpeed);
|
||||
|
||||
// Reposição quando sai do volume
|
||||
if (p.position.x > spanX) {
|
||||
p.position.x = -spanX;
|
||||
p.position.y = Math.random() * (height * 1.3);
|
||||
p.position.z = (Math.random() - 0.5) * spanZ * 2;
|
||||
}
|
||||
|
||||
dummy.position.copy(p.position);
|
||||
dummy.lookAt(p.position.clone().add(moveVec));
|
||||
dummy.rotateX(Math.PI / 2);
|
||||
dummy.scale.set(1, 1 + currentSpeed * 6, 1);
|
||||
dummy.updateMatrix();
|
||||
meshRef.current!.setMatrixAt(i, dummy.matrix);
|
||||
|
||||
if (p.currentCpe > 0) {
|
||||
colorObj
|
||||
.set(isDark ? '#ef4444' : '#dc2626')
|
||||
.lerp(new THREE.Color(isDark ? '#fcd34d' : '#f59e0b'), 1 - Math.min(1, p.currentCpe));
|
||||
} else if (p.currentCpe < 0) {
|
||||
const intensity = Math.min(1, Math.abs(p.currentCpe));
|
||||
colorObj
|
||||
.set(isDark ? '#7dd3fc' : '#38bdf8')
|
||||
.lerp(new THREE.Color(isDark ? '#1e40af' : '#1d4ed8'), intensity);
|
||||
} else {
|
||||
colorObj.set(isDark ? '#f1f5f9' : '#64748b');
|
||||
}
|
||||
meshRef.current!.setColorAt(i, colorObj);
|
||||
});
|
||||
|
||||
meshRef.current.instanceMatrix.needsUpdate = true;
|
||||
if (meshRef.current.instanceColor) {
|
||||
meshRef.current.instanceColor.needsUpdate = true;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<instancedMesh ref={meshRef} args={[undefined, undefined, count]}>
|
||||
<cylinderGeometry args={[0.01, 0.01, 1.5, 4]} />
|
||||
<meshBasicMaterial
|
||||
color="#ffffff"
|
||||
transparent
|
||||
opacity={isDark ? 0.6 : 0.8}
|
||||
blending={isDark ? THREE.AdditiveBlending : THREE.NormalBlending}
|
||||
depthWrite={false}
|
||||
/>
|
||||
</instancedMesh>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user