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) => (
))}
>
);
}