Files
BrainWind/app/src/components/three/CylinderAirflowSystem.tsx
T

155 lines
5.1 KiB
TypeScript

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 (agora positivo para ficar vermelho)
} 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 ? '#f87171' : '#ef4444'); // Vermelho (Turbulento/Pressão)
} else if (p.currentCpe < 0) {
const intensity = Math.min(1, Math.abs(p.currentCpe));
colorObj.set(isDark ? '#c084fc' : '#a855f7').lerp(new THREE.Color(isDark ? '#7e22ce' : '#6b21a8'), intensity); // Roxo (Acelerado/Sucção)
} else {
colorObj.set(isDark ? '#60a5fa' : '#3b82f6'); // Azul (Corrente livre)
}
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>
);
}