From f32833c0232d62cc6a162c6de81d534e35faa86c Mon Sep 17 00:00:00 2001 From: Rodrigo Bertoldo de Sousa Martins Date: Thu, 17 Sep 2026 12:06:17 -0300 Subject: [PATCH] mitose e meiose Signed-off-by: Rodrigo Bertoldo de Sousa Martins --- frontend/src/App.tsx | 2 + frontend/src/components/Layout.tsx | 3 + .../cellDivision/CellDivisionScene.tsx | 217 ++++++++++++ frontend/src/data/cellDivisionStages.ts | 314 ++++++++++++++++++ frontend/src/routes/CellDivisionPage.tsx | 241 ++++++++++++++ frontend/tsconfig.tsbuildinfo | 2 +- 6 files changed, 778 insertions(+), 1 deletion(-) create mode 100644 frontend/src/components/cellDivision/CellDivisionScene.tsx create mode 100644 frontend/src/data/cellDivisionStages.ts create mode 100644 frontend/src/routes/CellDivisionPage.tsx diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e25ec82..c387634 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,7 @@ import { Layout } from "./components/Layout.js"; import AtlasPage from "./routes/AtlasPage.js"; const PlanesPage = lazy(() => import("./routes/PlanesPage.js")); const PhysicalExamPage = lazy(() => import("./routes/PhysicalExamPage.js")); +const CellDivisionPage = lazy(() => import("./routes/CellDivisionPage.js")); const QuizPage = lazy(() => import("./routes/QuizPage.js")); const AdminStatsPage = lazy(() => import("./routes/AdminStatsPage.js")); @@ -20,6 +21,7 @@ export default function App() { } /> } /> } /> + } /> } /> {/* Fora do Layout: painel interno, sem o menu/nav do atlas. */} diff --git a/frontend/src/components/Layout.tsx b/frontend/src/components/Layout.tsx index 365a5d9..a7d8d44 100644 --- a/frontend/src/components/Layout.tsx +++ b/frontend/src/components/Layout.tsx @@ -32,6 +32,9 @@ export function Layout() { Exame Físico + + Mitose e Meiose + Quiz diff --git a/frontend/src/components/cellDivision/CellDivisionScene.tsx b/frontend/src/components/cellDivision/CellDivisionScene.tsx new file mode 100644 index 0000000..cce7981 --- /dev/null +++ b/frontend/src/components/cellDivision/CellDivisionScene.tsx @@ -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 ( + + + + + + + + + + + + + + + ); +} + +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) => ( + + ))} + + ); +} diff --git a/frontend/src/data/cellDivisionStages.ts b/frontend/src/data/cellDivisionStages.ts new file mode 100644 index 0000000..26c283e --- /dev/null +++ b/frontend/src/data/cellDivisionStages.ts @@ -0,0 +1,314 @@ +// Modelo ILUSTRATIVO (mesma filosofia do CellDiagram3D.tsx e do Mannequin.tsx +// — não existe malha 3D real de mitose/meiose pra estudo, então desenhamos +// à mão com formas simples do Three.js) de divisão celular, simplificado +// pra uma célula humana com 2n=4 (2 pares de cromossomos homólogos) em vez +// de 46 — o suficiente pra mostrar os conceitos-chave (condensação, +// alinhamento, separação, citocinese; em meiose: pareamento dos homólogos, +// permutação genética/crossing-over, e a divisão reducional) sem virar +// uma bagunça visual com dezenas de cromossomos. +// +// Cada cromossomo tem 2 cromátides-irmãs (mesma cor, já que são cópias +// idênticas uma da outra) que existem como entidades independentes durante +// TODA a animação — antes de se separarem elas simplesmente ocupam a +// MESMA posição (e por isso se desenham sobrepostas, formando o "X" +// clássico do cromossomo duplicado); depois de se separarem, cada uma +// anima pra uma posição diferente e passa a se parecer com um cromossomo +// simples. Isso evita precisar de um estado "duplicado: sim/não" à parte +// — a posição já conta a história. + +export interface ChromatidTarget { + id: string; // estável durante toda a animação (mitose OU meiose) + color: string; + cell: number; // índice em `cells` neste estágio + pos: [number, number, number]; // posição local ao centro da célula + rot: number; // rotação (graus) em torno de Z — só estética +} + +export interface CellTarget { + center: [number, number, number]; + radius: number; + envelope: number; // opacidade do envelope nuclear (0 = dissolvido, 1 = intacto) + spindle: boolean; // mostra fuso mitótico (centrossomos + fibras) neste estágio + elongated: number; // 0 = esférica, 1 = alongada com sulco de clivagem (citocinese em andamento) +} + +export interface Stage { + id: string; + label: string; + description: string; + cells: CellTarget[]; + chromatids: ChromatidTarget[]; +} + +// ---- Cromossomos-base (2n = 4) ---------------------------------------- +// b/c = par homólogo 1 (materno/paterno); o/y = par homólogo 2. +const COLOR_B = "#4f7cff"; // azul +const COLOR_C = "#22d3ee"; // ciano — homólogo de B +const COLOR_O = "#f59e0b"; // laranja +const COLOR_Y = "#eab308"; // amarelo — homólogo de O + +const R = 0.8; // raio "padrão" da célula (membrana) +const POLE = 0.55; +const FINAL_POLE = 1.2; + +function pair(idA: string, idB: string, color: string, cell: number, pos: [number, number, number]): ChromatidTarget[] { + return [ + { id: idA, color, cell, pos, rot: 0 }, + { id: idB, color, cell, pos, rot: 180 }, + ]; +} + +function single(id: string, color: string, cell: number, pos: [number, number, number], rot: number): ChromatidTarget { + return { id, color, cell, pos, rot }; +} + +// ============================= MITOSE =================================== +// As 4 cromátides-irmãs (uma de cada cromossomo) vão pro polo A, as outras +// 4 pro polo B — as duas células-filhas ficam geneticamente IDÊNTICAS à +// célula-mãe (e entre si), com o mesmo número de cromossomos (2n=4). + +export const MITOSIS_STAGES: Stage[] = [ + { + id: "interfase", + label: "Interfase", + description: + "A célula não está se dividindo: cresce e, na fase S, duplica todo o seu DNA. Os cromossomos já estão duplicados, mas ainda descondensados (finos, dispersos) — por isso não aparecem como bastões nítidos ainda.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 1, spindle: false, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.28, 0.22, 0.1]), + ...pair("c1a", "c1b", COLOR_C, 0, [0.05, 0.3, -0.15]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.25, -0.15, 0.2]), + ...pair("y1a", "y1b", COLOR_Y, 0, [-0.1, -0.28, -0.1]), + ], + }, + { + id: "prof", + label: "Prófase", + description: + "A cromatina se condensa em cromossomos visíveis (cada um já duplicado, com 2 cromátides-irmãs unidas pelo centrômero — o \"X\"). Os centrossomos começam a migrar para polos opostos e o envelope nuclear começa a se desfazer.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0.6, spindle: true, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.26, 0.2, 0.08]), + ...pair("c1a", "c1b", COLOR_C, 0, [0.06, 0.28, -0.12]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.24, -0.14, 0.18]), + ...pair("y1a", "y1b", COLOR_Y, 0, [-0.1, -0.26, -0.08]), + ], + }, + { + id: "prometaf", + label: "Prometáfase", + description: + "O envelope nuclear se dissolve por completo. As fibras do fuso mitótico (vindas dos dois centrossomos) se prendem ao centrômero de cada cromossomo e começam a puxá-los em direção ao centro da célula.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.3, 0.05, 0.05]), + ...pair("c1a", "c1b", COLOR_C, 0, [-0.12, -0.05, -0.05]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.12, 0.05, 0.05]), + ...pair("y1a", "y1b", COLOR_Y, 0, [0.3, -0.05, -0.05]), + ], + }, + { + id: "metaf", + label: "Metáfase", + description: + "Os cromossomos ficam alinhados no \"equador\" da célula (placa metafásica), cada um preso a fibras do fuso vindas dos dois polos — é o ponto de checagem antes da separação começar.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.3, 0, 0]), + ...pair("c1a", "c1b", COLOR_C, 0, [-0.1, 0, 0]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.1, 0, 0]), + ...pair("y1a", "y1b", COLOR_Y, 0, [0.3, 0, 0]), + ], + }, + { + id: "anaf", + label: "Anáfase", + description: + "As cromátides-irmãs se separam no centrômero e são puxadas para polos opostos — cada uma agora é um cromossomo independente. A célula começa a se alongar.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0.4 }], + chromatids: [ + single("b1a", COLOR_B, 0, [-POLE, 0.12, 0.08], 0), + single("c1a", COLOR_C, 0, [-POLE, -0.05, -0.1], 20), + single("o1a", COLOR_O, 0, [-POLE, 0.05, -0.08], -15), + single("y1a", COLOR_Y, 0, [-POLE, -0.15, 0.05], 10), + single("b1b", COLOR_B, 0, [POLE, -0.12, -0.08], 180), + single("c1b", COLOR_C, 0, [POLE, 0.05, 0.1], 160), + single("o1b", COLOR_O, 0, [POLE, -0.05, 0.08], 195), + single("y1b", COLOR_Y, 0, [POLE, 0.15, -0.05], 170), + ], + }, + { + id: "telof", + label: "Telófase", + description: + "Um conjunto completo de cromossomos chega a cada polo e um novo envelope nuclear se forma ao redor de cada um. Os cromossomos começam a descondensar de novo e o fuso desaparece.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0.7, spindle: false, elongated: 0.85 }], + chromatids: [ + single("b1a", COLOR_B, 0, [-1.0, 0.1, 0.06], 0), + single("c1a", COLOR_C, 0, [-1.0, -0.04, -0.08], 20), + single("o1a", COLOR_O, 0, [-1.0, 0.04, -0.06], -15), + single("y1a", COLOR_Y, 0, [-1.0, -0.12, 0.04], 10), + single("b1b", COLOR_B, 0, [1.0, -0.1, -0.06], 180), + single("c1b", COLOR_C, 0, [1.0, 0.04, 0.08], 160), + single("o1b", COLOR_O, 0, [1.0, -0.04, 0.06], 195), + single("y1b", COLOR_Y, 0, [1.0, 0.12, -0.04], 170), + ], + }, + { + id: "cito", + label: "Citocinese", + description: + "A membrana se estrangula ao meio (sulco de clivagem) até formar DUAS células-filhas, cada uma com um conjunto completo e IDÊNTICO de 4 cromossomos (2n=4) — geneticamente iguais entre si e à célula original.", + cells: [ + { center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.9, spindle: false, elongated: 0 }, + { center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.9, spindle: false, elongated: 0 }, + ], + chromatids: [ + single("b1a", COLOR_B, 0, [0.1, 0.1, 0.06], 0), + single("c1a", COLOR_C, 0, [-0.04, -0.04, -0.08], 20), + single("o1a", COLOR_O, 0, [0.04, 0.04, -0.06], -15), + single("y1a", COLOR_Y, 0, [-0.12, -0.12, 0.04], 10), + single("b1b", COLOR_B, 1, [-0.1, -0.1, -0.06], 180), + single("c1b", COLOR_C, 1, [0.04, 0.04, 0.08], 160), + single("o1b", COLOR_O, 1, [-0.04, -0.04, 0.06], 195), + single("y1b", COLOR_Y, 1, [0.12, 0.12, -0.04], 170), + ], + }, +]; + +// ============================= MEIOSE ==================================== +// Meiose I (reducional): os HOMÓLOGOS se separam (não as cromátides-irmãs). +// Pra ilustrar "sortimento independente" (cada par se orienta de forma +// independente dos outros pares), mandamos b+y para um polo e c+o para o +// outro — uma combinação nova, diferente da materna/paterna original. +// Meiose II (equacional): igual a uma mitose, mas partindo de células +// HAPLOIDES já duplicadas — sem nova duplicação do DNA antes. + +export const MEIOSIS_STAGES: Stage[] = [ + { + id: "interfase", + label: "Interfase", + description: + "Igual à mitose: a célula duplica todo o seu DNA na fase S. Os cromossomos já duplicados continuam descondensados.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 1, spindle: false, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.28, 0.22, 0.1]), + ...pair("c1a", "c1b", COLOR_C, 0, [0.05, 0.3, -0.15]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.25, -0.15, 0.2]), + ...pair("y1a", "y1b", COLOR_Y, 0, [-0.1, -0.28, -0.1]), + ], + }, + { + id: "prof1", + label: "Prófase I", + description: + "Os cromossomos se condensam E os homólogos se emparelham lado a lado (sinapse), formando uma tétrade (4 cromátides). É aqui que ocorre o crossing-over — trocas de pedaços entre cromátides homólogas, que embaralham genes maternos e paternos e aumentam a variabilidade genética da espécie.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0.6, spindle: true, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.22, 0.1, 0.05]), + ...pair("c1a", "c1b", COLOR_C, 0, [-0.09, 0.1, 0.05]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.09, -0.1, -0.05]), + ...pair("y1a", "y1b", COLOR_Y, 0, [0.22, -0.1, -0.05]), + ], + }, + { + id: "metaf1", + label: "Metáfase I", + description: + "As TÉTRADES (pares de homólogos, não cromossomos isolados como na mitose) se alinham juntas na placa metafásica — essa é a principal diferença visual em relação à mitose.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.27, 0, 0]), + ...pair("c1a", "c1b", COLOR_C, 0, [-0.13, 0, 0]), + ...pair("y1a", "y1b", COLOR_Y, 0, [0.13, 0, 0]), + ...pair("o1a", "o1b", COLOR_O, 0, [0.27, 0, 0]), + ], + }, + { + id: "anaf1", + label: "Anáfase I", + description: + "Os HOMÓLOGOS se separam um do outro e vão para polos opostos — cada um ainda com suas 2 cromátides-irmãs unidas (diferente da mitose, onde são as cromátides-irmãs que se separam). É a divisão reducional: o número de cromossomos vai cair pela metade.", + cells: [{ center: [0, 0, 0], radius: R, envelope: 0, spindle: true, elongated: 0.4 }], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-POLE, 0.12, 0.05]), + ...pair("y1a", "y1b", COLOR_Y, 0, [-POLE, -0.12, -0.05]), + ...pair("c1a", "c1b", COLOR_C, 0, [POLE, 0.12, -0.05]), + ...pair("o1a", "o1b", COLOR_O, 0, [POLE, -0.12, 0.05]), + ], + }, + { + id: "cito1", + label: "Telófase I + Citocinese I", + description: + "Formam-se DUAS células, cada uma já HAPLOIDE (só 1 cromossomo de cada par, não mais 2) — mas cada cromossomo ainda duplicado (2 cromátides). Repare que a combinação em cada célula é NOVA (nem só materna, nem só paterna): é o sortimento independente em ação.", + cells: [ + { center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.7, spindle: false, elongated: 0 }, + { center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0.7, spindle: false, elongated: 0 }, + ], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.13, 0.1, 0.05]), + ...pair("y1a", "y1b", COLOR_Y, 0, [0.13, -0.1, -0.05]), + ...pair("c1a", "c1b", COLOR_C, 1, [-0.13, 0.1, -0.05]), + ...pair("o1a", "o1b", COLOR_O, 1, [0.13, -0.1, 0.05]), + ], + }, + { + id: "metaf2", + label: "Metáfase II", + description: + "Sem nova duplicação de DNA, cada uma das 2 células (agora haploides) monta seu próprio fuso e alinha seus cromossomos (ainda duplicados) na placa metafásica — igual a uma mitose comum, só que partindo de metade dos cromossomos.", + cells: [ + { center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0 }, + { center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0 }, + ], + chromatids: [ + ...pair("b1a", "b1b", COLOR_B, 0, [-0.16, 0, 0]), + ...pair("y1a", "y1b", COLOR_Y, 0, [0.16, 0, 0]), + ...pair("c1a", "c1b", COLOR_C, 1, [-0.16, 0, 0]), + ...pair("o1a", "o1b", COLOR_O, 1, [0.16, 0, 0]), + ], + }, + { + id: "anaf2", + label: "Anáfase II", + description: + "Agora sim as cromátides-irmãs se separam (igual à anáfase da mitose) dentro de cada uma das 2 células, indo para polos opostos.", + cells: [ + { center: [-FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0.4 }, + { center: [FINAL_POLE, 0, 0], radius: 0.62, envelope: 0, spindle: true, elongated: 0.4 }, + ], + chromatids: [ + single("b1a", COLOR_B, 0, [-0.4, 0.1, 0], 0), + single("y1a", COLOR_Y, 0, [-0.4, -0.1, 0], 10), + single("b1b", COLOR_B, 0, [0.4, -0.1, 0], 180), + single("y1b", COLOR_Y, 0, [0.4, 0.1, 0], 170), + single("c1a", COLOR_C, 1, [-0.4, 0.1, 0], 20), + single("o1a", COLOR_O, 1, [-0.4, -0.1, 0], -15), + single("c1b", COLOR_C, 1, [0.4, -0.1, 0], 160), + single("o1b", COLOR_O, 1, [0.4, 0.1, 0], 195), + ], + }, + { + id: "cito2", + label: "Telófase II + Citocinese II", + description: + "Cada uma das 2 células se divide em 2 — resultando em 4 células HAPLOIDES no total, cada uma com metade dos cromossomos da célula original e uma combinação genética própria. Em humanos, essas são as células germinativas (óvulos ou espermatozoides).", + cells: [ + { center: [-1.8, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 }, + { center: [-0.6, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 }, + { center: [0.6, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 }, + { center: [1.8, 0, 0], radius: 0.5, envelope: 0.9, spindle: false, elongated: 0 }, + ], + chromatids: [ + single("b1a", COLOR_B, 0, [0, 0.08, 0], 0), + single("y1a", COLOR_Y, 0, [0, -0.08, 0], 10), + single("b1b", COLOR_B, 1, [0, 0.08, 0], 180), + single("y1b", COLOR_Y, 1, [0, -0.08, 0], 170), + single("c1a", COLOR_C, 2, [0, 0.08, 0], 20), + single("o1a", COLOR_O, 2, [0, -0.08, 0], -15), + single("c1b", COLOR_C, 3, [0, 0.08, 0], 160), + single("o1b", COLOR_O, 3, [0, -0.08, 0], 195), + ], + }, +]; diff --git a/frontend/src/routes/CellDivisionPage.tsx b/frontend/src/routes/CellDivisionPage.tsx new file mode 100644 index 0000000..cce848c --- /dev/null +++ b/frontend/src/routes/CellDivisionPage.tsx @@ -0,0 +1,241 @@ +import { useEffect, useState } from "react"; +import { Canvas } from "@react-three/fiber"; +import { CameraControls } from "@react-three/drei"; +import { CellDivisionScene } from "../components/cellDivision/CellDivisionScene.js"; +import { MITOSIS_STAGES, MEIOSIS_STAGES } from "../data/cellDivisionStages.js"; + +type Mode = "mitose" | "meiose"; + +const AUTOPLAY_MS = 3200; + +export default function CellDivisionPage() { + const [mode, setMode] = useState("mitose"); + const [stageIndex, setStageIndex] = useState(0); + const [playing, setPlaying] = useState(false); + const [leftOpen, setLeftOpen] = useState(false); + const [rightOpen, setRightOpen] = useState(false); + + const stages = mode === "mitose" ? MITOSIS_STAGES : MEIOSIS_STAGES; + const stage = stages[stageIndex]; + + // Trocar de modo reinicia o estágio — misturar índice de mitose (7 + // estágios) com o array de meiose (9 estágios) fora de ordem não faz + // sentido pedagogicamente. + function changeMode(next: Mode) { + setMode(next); + setStageIndex(0); + setPlaying(false); + } + + function goTo(index: number) { + setStageIndex(Math.max(0, Math.min(stages.length - 1, index))); + } + + useEffect(() => { + if (!playing) return; + const id = setInterval(() => { + setStageIndex((i) => { + if (i >= stages.length - 1) { + setPlaying(false); + return i; + } + return i + 1; + }); + }, AUTOPLAY_MS); + return () => clearInterval(id); + }, [playing, stages.length]); + + // Reinicia autoplay do começo se já estava no fim. + function togglePlay() { + setPlaying((p) => { + if (!p && stageIndex >= stages.length - 1) setStageIndex(0); + return !p; + }); + } + + const cellCount = stage.cells.length; + + return ( +
+ + + {(leftOpen || rightOpen) && ( +
{ + setLeftOpen(false); + setRightOpen(false); + }} + /> + )} + +
+ + + + + + + + + {/* Controles de reprodução — barra fixa embaixo do canvas */} +
+ + + + + {stageIndex + 1}/{stages.length} + +
+ +
+ {cellCount} {cellCount === 1 ? "célula" : "células"} +
+
+ + +
+ ); +} diff --git a/frontend/tsconfig.tsbuildinfo b/frontend/tsconfig.tsbuildinfo index 5aecc60..9bcc0b6 100644 --- a/frontend/tsconfig.tsbuildinfo +++ b/frontend/tsconfig.tsbuildinfo @@ -1 +1 @@ -{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/celldiagram3d.tsx","./src/components/clippingcontrols.tsx","./src/components/cookieconsent.tsx","./src/components/feedbackwidget.tsx","./src/components/layertoggle.tsx","./src/components/layout.tsx","./src/components/microscopydiagram.tsx","./src/components/onlinecounter.tsx","./src/components/quickregionnav.tsx","./src/components/searchnav.tsx","./src/components/skindiagram3d.tsx","./src/components/structureinfopanel.tsx","./src/components/viewer/exammarkers.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./src/data/microscopycredits.ts","./src/data/microscopyhotspots.ts","./src/data/musclereferenceimages.ts","./src/data/physicalexam.ts","./src/hooks/usestructures.ts","./src/hooks/usesystemstructures.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/routes/adminstatspage.tsx","./src/routes/atlaspage.tsx","./src/routes/physicalexampage.tsx","./src/routes/planespage.tsx","./src/routes/quizpage.tsx","./src/store/useatlasstore.ts"],"version":"5.9.3"} \ No newline at end of file +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/celldiagram3d.tsx","./src/components/clippingcontrols.tsx","./src/components/cookieconsent.tsx","./src/components/feedbackwidget.tsx","./src/components/layertoggle.tsx","./src/components/layout.tsx","./src/components/microscopydiagram.tsx","./src/components/onlinecounter.tsx","./src/components/quickregionnav.tsx","./src/components/searchnav.tsx","./src/components/skindiagram3d.tsx","./src/components/structureinfopanel.tsx","./src/components/celldivision/celldivisionscene.tsx","./src/components/viewer/exammarkers.tsx","./src/components/viewer/mannequin.tsx","./src/components/viewer/scene.tsx","./src/components/viewer/systemmodel.tsx","./src/data/celldivisionstages.ts","./src/data/microscopycredits.ts","./src/data/microscopyhotspots.ts","./src/data/musclereferenceimages.ts","./src/data/physicalexam.ts","./src/hooks/usestructures.ts","./src/hooks/usesystemstructures.ts","./src/lib/analytics.ts","./src/lib/api.ts","./src/routes/adminstatspage.tsx","./src/routes/atlaspage.tsx","./src/routes/celldivisionpage.tsx","./src/routes/physicalexampage.tsx","./src/routes/planespage.tsx","./src/routes/quizpage.tsx","./src/store/useatlasstore.ts"],"version":"5.9.3"} \ No newline at end of file