import { useMemo, useRef } from "react"; import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; import type { Stage } from "../../data/cellDivisionStages.js"; // Cena 3D ilustrativa (mesmo princípio do CellDiagram3D.tsx e do // Mannequin.tsx: sem malha real disponível pra esse conteúdo, então // desenhamos à mão com primitivas do Three.js) — recebe só o ESTÁGIO alvo // atual (`stage`) e anima suavemente toda célula/cromátide até lá sempre // que o estágio muda, via interpolação por frame (useFrame), sem precisar // de nenhuma biblioteca de animação. const MAX_CELLS = 4; // Fator de suavização independente de framerate: a cada frame, anda uma // fração `k` do caminho que falta até o alvo — converge suave em ~0.4s // sem "overshoot" e sem depender de quantos FPS o navegador está rendendo. function smoothing(delta: number, halfLifeSeconds = 0.18) { return 1 - Math.pow(0.5, delta / halfLifeSeconds); } function ChromatidShape({ color }: { color: string }) { return ( ); } interface ChromatidTargetWorld { pos: THREE.Vector3; rotDeg: number; color: string; } function AnimatedChromatid({ id, targets }: { id: string; targets: Map }) { // Posição/rotação são controladas 100% via useFrame (nunca como prop // JSX reativa) — se passássemos `position={...}` no JSX, o // react-three-fiber reaplicaria (e "teleportaria") o objeto pra lá toda // vez que o mapa `targets` mudasse de identidade (ou seja, a cada troca // de estágio), brigando com o lerp suave abaixo. No primeiro quadro o // grupo nasce em (0,0,0) e "voa" suavemente até a posição inicial — // efeito colateral aceitável (só acontece uma vez, ao montar). const ref = useRef(null); const initialColor = targets.get(id)?.color ?? "#94a3b8"; useFrame((_, delta) => { const target = targets.get(id); if (!ref.current || !target) return; const k = smoothing(delta); ref.current.position.lerp(target.pos, k); const targetRad = THREE.MathUtils.degToRad(target.rotDeg); ref.current.rotation.z = THREE.MathUtils.lerp(ref.current.rotation.z, targetRad, k); }); return ( ); } interface CellSlotState { position: THREE.Vector3; radius: { value: number }; envelope: { value: number }; spindle: { value: number }; elongated: { value: number }; } function AnimatedCellSlot({ target, state, }: { target: { center: [number, number, number]; radius: number; envelope: number; spindle: boolean; elongated: number } | null; state: CellSlotState; }) { const groupRef = useRef(null); const membraneRef = useRef(null); const membraneMatRef = useRef(null); const envelopeMatRef = useRef(null); const spindleRef = useRef(null); useFrame((_, delta) => { const k = smoothing(delta); const targetPos = target ? new THREE.Vector3(...target.center) : state.position.clone(); const targetRadius = target ? target.radius : 0; const targetEnvelope = target ? target.envelope : 0; const targetSpindle = target?.spindle ? 1 : 0; const targetElongated = target ? target.elongated : 0; state.position.lerp(targetPos, k); state.radius.value = THREE.MathUtils.lerp(state.radius.value, targetRadius, k); state.envelope.value = THREE.MathUtils.lerp(state.envelope.value, targetEnvelope, k); state.spindle.value = THREE.MathUtils.lerp(state.spindle.value, targetSpindle, k); state.elongated.value = THREE.MathUtils.lerp(state.elongated.value, targetElongated, k); if (groupRef.current) groupRef.current.position.copy(state.position); if (membraneRef.current) { const r = state.radius.value; const stretch = 1 + state.elongated.value * 0.7; membraneRef.current.scale.set(r / 0.8, (r / 0.8) * stretch, r / 0.8); } if (membraneMatRef.current) membraneMatRef.current.opacity = Math.min(0.22, state.radius.value * 0.28); if (envelopeMatRef.current) envelopeMatRef.current.opacity = state.envelope.value * 0.35; if (spindleRef.current) spindleRef.current.visible = state.spindle.value > 0.05; if (spindleRef.current) spindleRef.current.scale.setScalar(state.spindle.value); }); return ( {/* membrana plasmática — translúcida, não recebe clique (só decorativa) */} null}> {/* envelope nuclear — esfera um pouco menor, desfaz/refaz conforme a fase */} null}> {/* fuso mitótico: 2 centrossomos + fibras finas em leque */} {[-1, 1].map((side) => ( {[-0.25, -0.1, 0.1, 0.25].map((offset, i) => ( 0 ? Math.PI / 2 + offset * 0.3 : -Math.PI / 2 + offset * 0.3]} > ))} ))} ); } export function CellDivisionScene({ stage }: { stage: Stage }) { // Estado imperativo (fora do React) por slot de célula — persiste entre // mudanças de estágio pra animação continuar de onde estava, sem exigir // que o número de células seja o mesmo do estágio anterior. const cellStates = useMemo( () => Array.from({ length: MAX_CELLS }, () => ({ position: new THREE.Vector3(0, 0, 0), radius: { value: 0 }, envelope: { value: 0 }, spindle: { value: 0 }, elongated: { value: 0 }, })), [], ); const chromatidTargets = useMemo(() => { const map = new Map(); for (const c of stage.chromatids) { const cell = stage.cells[c.cell]; if (!cell) continue; map.set(c.id, { pos: new THREE.Vector3(cell.center[0] + c.pos[0], cell.center[1] + c.pos[1], cell.center[2] + c.pos[2]), rotDeg: c.rot, color: c.color, }); } return map; // eslint-disable-next-line react-hooks/exhaustive-deps }, [stage.id]); // IDs fixos (as mesmas 8 cromátides existem em todo estágio de mitose OU // meiose) — pegos do primeiro estágio pra não remontar componentes ao // trocar de estágio (só a posição-alvo muda). const chromatidIds = useMemo(() => stage.chromatids.map((c) => c.id), []); // eslint-disable-line react-hooks/exhaustive-deps return ( <> {cellStates.map((state, i) => ( ))} {chromatidIds.map((id) => ( ))} ); }