Skip to content

Commit 2e49362

Browse files
Merge pull request #6 from Santiago13dev/santidev
botón para capturar el globo como PNG
2 parents 14f7295 + 9f12209 commit 2e49362

File tree

4 files changed

+180
-5
lines changed

4 files changed

+180
-5
lines changed

frontend/app/page.tsx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
import { useState, useMemo, useEffect } from "react";
44
import dynamic from "next/dynamic";
55
import Playback from "../components/Playback"; // timeline play/pause/slider
6+
import LayerToggles, { LayerOptions } from "../components/LayerToggles";
7+
import SnapshotButton from "../components/SnapshotButton";
68

79
// Carga en cliente
810
const Globe = dynamic(() => import("../components/Globe"), { ssr: false });
@@ -30,6 +32,14 @@ export default function Home() {
3032

3133
// Timeline (minutos)
3234
const [liveT, setLiveT] = useState(0);
35+
const [getPng, setGetPng] = useState<(() => string) | null>(null);
36+
const [layers, setLayers] = useState<LayerOptions>({
37+
atmosphere: true,
38+
graticule: true,
39+
equator: true,
40+
stars: true,
41+
stand: true,
42+
});
3343

3444
// Chequeo de texturas para evitar crash del loader
3545
const [texOk, setTexOk] = useState<boolean | null>(null);
@@ -227,6 +237,12 @@ export default function Home() {
227237
liveMinutes={liveT}
228238
liveVpKmS={vp}
229239
liveVsKmS={vs}
240+
showAtmosphere={layers.atmosphere}
241+
showGraticule={layers.graticule}
242+
showEquator={layers.equator}
243+
showStars={layers.stars}
244+
showStand={layers.stand}
245+
onReadyCapture={(fn) => setGetPng(() => fn)}
230246
/>
231247
)}
232248

@@ -243,6 +259,10 @@ export default function Home() {
243259
arrivals={resp?.arrivals ?? []}
244260
intensity={resp?.intensity ?? null}
245261
/>
262+
{/* Panel de capas */}
263+
<div className="mt-4">
264+
<LayerToggles value={layers} onChange={setLayers} />
265+
</div>
246266

247267
{!resp && !error && (
248268
<p className="text-slate-400 text-sm mt-3">
@@ -251,7 +271,15 @@ export default function Home() {
251271
)}
252272
{error && <p className="text-red-400 text-sm mt-3 break-words">Error: {error}</p>}
253273
</aside>
274+
<div className="flex flex-wrap items-end gap-2">
275+
{/* inputs y botón Simular */}
276+
<button className="btn-primary" onClick={simular} disabled={loading || !center || texOk === false}>
277+
{loading ? "Simulando..." : "Simular"}
278+
</button>
254279

280+
{/* ⬇︎ Botón de captura */}
281+
<SnapshotButton getPng={getPng ?? undefined} />
282+
</div>
255283
{/* Timeline debajo del globo, ocupando las mismas 3 columnas */}
256284
<div className="lg:col-span-3">
257285
<div className="mt-2">

frontend/components/Globe.tsx

Lines changed: 59 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import * as THREE from "three";
44
import { Canvas, useFrame } from "@react-three/fiber";
55
import { OrbitControls, Stars, useTexture } from "@react-three/drei";
66
import { useMemo, useRef } from "react";
7+
import { useEffect } from "react";
78

89
export type Center = { lat: number; lon: number };
910
export type RingSet = { p?: number[]; s?: number[] };
@@ -13,7 +14,14 @@ type GlobeProps = {
1314
rings?: RingSet | null;
1415
liveMinutes?: number; // minutos transcurridos para frentes vivos
1516
liveVpKmS?: number; // velocidad P (km/s)
16-
liveVsKmS?: number; // velocidad S (km/s)
17+
liveVsKmS?: number;
18+
showAtmosphere?: boolean;
19+
showGraticule?: boolean;
20+
showEquator?: boolean;
21+
showStars?: boolean;
22+
showStand?: boolean; // velocidad S (km/s)
23+
onReadyCapture?: (fn: () => string) => void; // NUEVO: entrega una función capture()
24+
1725
};
1826

1927
/* ------------------- utilidades geo ------------------- */
@@ -218,12 +226,20 @@ function World({
218226
liveMinutes,
219227
liveVpKmS,
220228
liveVsKmS,
229+
showAtmosphere = true,
230+
showGraticule = true,
231+
showEquator = true,
221232
}: {
222233
center: Center;
223234
rings?: RingSet | null;
224235
liveMinutes?: number;
225236
liveVpKmS?: number;
226237
liveVsKmS?: number;
238+
showAtmosphere?: boolean;
239+
showGraticule?: boolean;
240+
showEquator?: boolean;
241+
showStars?: boolean;
242+
showStand?: boolean;
227243
}) {
228244
const worldRef = useRef<THREE.Group>(null!);
229245

@@ -275,24 +291,57 @@ function World({
275291
);
276292
}
277293

294+
import { useThree } from "@react-three/fiber"; // ya lo tienes por Canvas/useFrame
295+
296+
function CaptureProvider({
297+
onReadyCapture,
298+
}: {
299+
onReadyCapture?: (fn: () => string) => void;
300+
}) {
301+
const { gl, scene, camera } = useThree();
302+
303+
// Registramos una función que renderiza y devuelve el PNG
304+
React.useEffect(() => {
305+
if (!onReadyCapture) return;
306+
const capture = () => {
307+
gl.render(scene, camera);
308+
return gl.domElement.toDataURL("image/png");
309+
};
310+
onReadyCapture(capture);
311+
}, [onReadyCapture, gl, scene, camera]);
312+
313+
return null;
314+
}
278315
/* ============================ GLOBE ============================ */
279316
export default function Globe({
280317
center,
281318
rings,
282319
liveMinutes,
283320
liveVpKmS,
284321
liveVsKmS,
322+
onReadyCapture,
323+
showAtmosphere = true,
324+
showGraticule = true,
325+
showEquator = true,
326+
showStars = true,
327+
showStand = true,
285328
}: GlobeProps) {
286329
if (!center) return null;
287330

288331
return (
289332
<Canvas
290-
camera={{ position: [0, 0, 3.1], fov: 42 }}
291-
dpr={[1, 2]}
292-
gl={{ antialias: true, alpha: true, toneMapping: THREE.ACESFilmicToneMapping }}
333+
camera={{ position: [0, 0, 3.1], fov: 42 }}
334+
dpr={[1, 2]}
335+
gl={{
336+
antialias: true,
337+
alpha: true,
338+
toneMapping: THREE.ACESFilmicToneMapping,
339+
preserveDrawingBuffer: true, // ⬅︎ IMPORTANTE para toDataURL
340+
}}
293341
style={{ width: "100%", height: "100%" }}
294342
>
295343
{/* Luces */}
344+
<CaptureProvider onReadyCapture={onReadyCapture} />
296345
<hemisphereLight intensity={0.5} color={"#cde8ff"} groundColor={"#0b1220"} />
297346
<directionalLight position={[5, 3, 5]} intensity={1.2} />
298347
<pointLight position={[-4, -3, -4]} intensity={0.5} />
@@ -307,8 +356,13 @@ export default function Globe({
307356
liveMinutes={liveMinutes}
308357
liveVpKmS={liveVpKmS}
309358
liveVsKmS={liveVsKmS}
359+
showAtmosphere={showAtmosphere}
360+
showGraticule={showGraticule}
361+
showEquator={showEquator}
362+
showStars={showStars}
363+
showStand={showStand}
310364
/>
311-
<Stand />
365+
{showStand && <Stand />}
312366

313367
{/* Controles */}
314368
<OrbitControls
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"use client";
2+
3+
export type LayerOptions = {
4+
atmosphere: boolean;
5+
graticule: boolean;
6+
equator: boolean;
7+
stars: boolean;
8+
stand: boolean;
9+
};
10+
11+
export default function LayerToggles({
12+
value,
13+
onChange,
14+
}: {
15+
value: LayerOptions;
16+
onChange: (v: LayerOptions) => void;
17+
}) {
18+
const set = (k: keyof LayerOptions) =>
19+
(e: React.ChangeEvent<HTMLInputElement>) =>
20+
onChange({ ...value, [k]: e.target.checked });
21+
22+
const reset = () =>
23+
onChange({ atmosphere: true, graticule: true, equator: true, stars: true, stand: true });
24+
25+
return (
26+
<div className="card p-3">
27+
<div className="mb-2 font-semibold text-sm">Capas del globo</div>
28+
29+
<div className="grid grid-cols-2 gap-2 text-sm">
30+
<label className="flex items-center gap-2">
31+
<input type="checkbox" checked={value.atmosphere} onChange={set("atmosphere")} />
32+
Atmósfera
33+
</label>
34+
35+
<label className="flex items-center gap-2">
36+
<input type="checkbox" checked={value.graticule} onChange={set("graticule")} />
37+
Retícula
38+
</label>
39+
40+
<label className="flex items-center gap-2">
41+
<input type="checkbox" checked={value.equator} onChange={set("equator")} />
42+
Ecuador
43+
</label>
44+
45+
<label className="flex items-center gap-2">
46+
<input type="checkbox" checked={value.stars} onChange={set("stars")} />
47+
Estrellas
48+
</label>
49+
50+
<label className="flex items-center gap-2">
51+
<input type="checkbox" checked={value.stand} onChange={set("stand")} />
52+
Base
53+
</label>
54+
</div>
55+
56+
<div className="mt-3">
57+
<button className="input px-3 py-2" onClick={reset}>Reset</button>
58+
</div>
59+
</div>
60+
);
61+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
"use client";
2+
3+
type Props = {
4+
getPng?: () => string | Promise<string>;
5+
fileName?: string;
6+
className?: string;
7+
};
8+
9+
export default function SnapshotButton({
10+
getPng,
11+
fileName,
12+
className = "btn-primary",
13+
}: Props) {
14+
const handleClick = async () => {
15+
if (!getPng) return;
16+
const data = await getPng();
17+
const a = document.createElement("a");
18+
a.href = data;
19+
a.download =
20+
fileName ??
21+
`SismoView_${new Date().toISOString().replace(/[:.]/g, "-")}.png`;
22+
document.body.appendChild(a);
23+
a.click();
24+
a.remove();
25+
};
26+
27+
return (
28+
<button className={className} onClick={handleClick} disabled={!getPng}>
29+
Capturar PNG
30+
</button>
31+
);
32+
}

0 commit comments

Comments
 (0)