mitose e meiose
Signed-off-by: Rodrigo Bertoldo de Sousa Martins <rodrigo@martinsevieira.adv.br>
This commit is contained in:
1 parent
a972670b7d
commit
f32833c023
6 files changed
+778
-1
No files matched your search
@@ -0,0 +1,217 @@
|
||||
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 (
|
||||
<group>
|
||||
<mesh position={[-0.045, 0.08, 0]} rotation={[0, 0, Math.PI / 7]}>
|
||||
<capsuleGeometry args={[0.022, 0.14, 3, 6]} />
|
||||
<meshStandardMaterial color={color} roughness={0.5} />
|
||||
</mesh>
|
||||
<mesh position={[0.045, 0.08, 0]} rotation={[0, 0, -Math.PI / 7]}>
|
||||
<capsuleGeometry args={[0.022, 0.14, 3, 6]} />
|
||||
<meshStandardMaterial color={color} roughness={0.5} />
|
||||
</mesh>
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.03, 10, 10]} />
|
||||
<meshStandardMaterial color={color} roughness={0.5} />
|
||||
</mesh>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
interface ChromatidTargetWorld {
|
||||
pos: THREE.Vector3;
|
||||
rotDeg: number;
|
||||
color: string;
|
||||
}
|
||||
|
||||
function AnimatedChromatid({ id, targets }: { id: string; targets: Map<string, ChromatidTargetWorld> }) {
|
||||
// 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<THREE.Group>(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 (
|
||||
<group ref={ref}>
|
||||
<ChromatidShape color={initialColor} />
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
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<THREE.Group>(null);
|
||||
const membraneRef = useRef<THREE.Mesh>(null);
|
||||
const membraneMatRef = useRef<THREE.MeshStandardMaterial>(null);
|
||||
const envelopeMatRef = useRef<THREE.MeshStandardMaterial>(null);
|
||||
const spindleRef = useRef<THREE.Group>(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 (
|
||||
<group ref={groupRef}>
|
||||
{/* membrana plasmática — translúcida, não recebe clique (só decorativa) */}
|
||||
<mesh ref={membraneRef} raycast={() => null}>
|
||||
<sphereGeometry args={[0.8, 28, 28]} />
|
||||
<meshStandardMaterial
|
||||
ref={membraneMatRef}
|
||||
color="#93c5fd"
|
||||
transparent
|
||||
opacity={0.15}
|
||||
side={THREE.DoubleSide}
|
||||
depthWrite={false}
|
||||
/>
|
||||
</mesh>
|
||||
{/* envelope nuclear — esfera um pouco menor, desfaz/refaz conforme a fase */}
|
||||
<mesh raycast={() => null}>
|
||||
<sphereGeometry args={[0.42, 24, 24]} />
|
||||
<meshStandardMaterial ref={envelopeMatRef} color="#c4b5fd" transparent opacity={0} depthWrite={false} />
|
||||
</mesh>
|
||||
{/* fuso mitótico: 2 centrossomos + fibras finas em leque */}
|
||||
<group ref={spindleRef}>
|
||||
{[-1, 1].map((side) => (
|
||||
<group key={side} position={[side * 0.65, 0, 0]}>
|
||||
<mesh>
|
||||
<sphereGeometry args={[0.035, 10, 10]} />
|
||||
<meshStandardMaterial color="#f8fafc" />
|
||||
</mesh>
|
||||
{[-0.25, -0.1, 0.1, 0.25].map((offset, i) => (
|
||||
<mesh
|
||||
key={i}
|
||||
position={[side * -0.32, offset * 0.6, offset]}
|
||||
rotation={[0, 0, side > 0 ? Math.PI / 2 + offset * 0.3 : -Math.PI / 2 + offset * 0.3]}
|
||||
>
|
||||
<cylinderGeometry args={[0.004, 0.004, 0.64, 4]} />
|
||||
<meshStandardMaterial color="#f8fafc" transparent opacity={0.4} />
|
||||
</mesh>
|
||||
))}
|
||||
</group>
|
||||
))}
|
||||
</group>
|
||||
</group>
|
||||
);
|
||||
}
|
||||
|
||||
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<CellSlotState[]>(
|
||||
() =>
|
||||
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<string, ChromatidTargetWorld>();
|
||||
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 (
|
||||
<>
|
||||
<color attach="background" args={["#0b1220"]} />
|
||||
<ambientLight intensity={0.75} />
|
||||
<directionalLight position={[2, 3, 2]} intensity={1} />
|
||||
<directionalLight position={[-2, -1, -2]} intensity={0.3} />
|
||||
|
||||
{cellStates.map((state, i) => (
|
||||
<AnimatedCellSlot key={i} target={stage.cells[i] ?? null} state={state} />
|
||||
))}
|
||||
|
||||
{chromatidIds.map((id) => (
|
||||
<AnimatedChromatid key={id} id={id} targets={chromatidTargets} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user