10m node viewer w/ quadtree
This commit is contained in:
188
src/App.tsx
Normal file
188
src/App.tsx
Normal file
@@ -0,0 +1,188 @@
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { Renderer } from "./renderer";
|
||||
|
||||
export default function App() {
|
||||
const canvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const [status, setStatus] = useState("Generating 10M particles & building spatial index…");
|
||||
const [stats, setStats] = useState({
|
||||
fps: 0,
|
||||
drawn: 0,
|
||||
mode: "",
|
||||
zoom: 0,
|
||||
ptSize: 0,
|
||||
});
|
||||
const [error, setError] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const canvas = canvasRef.current;
|
||||
if (!canvas) return;
|
||||
|
||||
let renderer: Renderer;
|
||||
try {
|
||||
renderer = new Renderer(canvas);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : String(e));
|
||||
return;
|
||||
}
|
||||
|
||||
// Build quadtree + upload (runs once, ~200ms)
|
||||
const buildMs = renderer.init();
|
||||
setStatus("");
|
||||
console.log(`Init complete in ${buildMs.toFixed(0)}ms`);
|
||||
|
||||
// ── Input handling ──
|
||||
let dragging = false;
|
||||
let lastX = 0;
|
||||
let lastY = 0;
|
||||
|
||||
const onDown = (e: MouseEvent) => {
|
||||
dragging = true;
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
};
|
||||
const onMove = (e: MouseEvent) => {
|
||||
if (!dragging) return;
|
||||
renderer.pan(e.clientX - lastX, e.clientY - lastY);
|
||||
lastX = e.clientX;
|
||||
lastY = e.clientY;
|
||||
};
|
||||
const onUp = () => {
|
||||
dragging = false;
|
||||
};
|
||||
const onWheel = (e: WheelEvent) => {
|
||||
e.preventDefault();
|
||||
const factor = e.deltaY > 0 ? 0.9 : 1 / 0.9;
|
||||
renderer.zoomAt(factor, e.clientX, e.clientY);
|
||||
};
|
||||
|
||||
canvas.addEventListener("mousedown", onDown);
|
||||
window.addEventListener("mousemove", onMove);
|
||||
window.addEventListener("mouseup", onUp);
|
||||
canvas.addEventListener("wheel", onWheel, { passive: false });
|
||||
|
||||
// ── Render loop ──
|
||||
let frameCount = 0;
|
||||
let lastTime = performance.now();
|
||||
let raf = 0;
|
||||
|
||||
const frame = () => {
|
||||
const result = renderer.render();
|
||||
frameCount++;
|
||||
|
||||
const now = performance.now();
|
||||
if (now - lastTime >= 500) {
|
||||
const fps = (frameCount / (now - lastTime)) * 1000;
|
||||
setStats({
|
||||
fps: Math.round(fps),
|
||||
drawn: result.drawnCount,
|
||||
mode: result.mode,
|
||||
zoom: result.zoom,
|
||||
ptSize: result.ptSize,
|
||||
});
|
||||
frameCount = 0;
|
||||
lastTime = now;
|
||||
}
|
||||
|
||||
raf = requestAnimationFrame(frame);
|
||||
};
|
||||
raf = requestAnimationFrame(frame);
|
||||
|
||||
return () => {
|
||||
cancelAnimationFrame(raf);
|
||||
canvas.removeEventListener("mousedown", onDown);
|
||||
window.removeEventListener("mousemove", onMove);
|
||||
window.removeEventListener("mouseup", onUp);
|
||||
canvas.removeEventListener("wheel", onWheel);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div style={{ width: "100vw", height: "100vh", overflow: "hidden", background: "#000" }}>
|
||||
<canvas
|
||||
ref={canvasRef}
|
||||
style={{ display: "block", width: "100%", height: "100%" }}
|
||||
/>
|
||||
|
||||
{/* Loading overlay */}
|
||||
{status && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(0,0,0,0.9)",
|
||||
color: "#0f0",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
{status}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Error overlay */}
|
||||
{error && (
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
inset: 0,
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
background: "rgba(0,0,0,0.9)",
|
||||
color: "#f44",
|
||||
fontFamily: "monospace",
|
||||
fontSize: "16px",
|
||||
}}
|
||||
>
|
||||
Error: {error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* HUD */}
|
||||
{!status && !error && (
|
||||
<>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
top: 10,
|
||||
left: 10,
|
||||
background: "rgba(0,0,0,0.75)",
|
||||
color: "#0f0",
|
||||
fontFamily: "monospace",
|
||||
padding: "8px 12px",
|
||||
fontSize: "12px",
|
||||
lineHeight: "1.6",
|
||||
borderRadius: "4px",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
<div>FPS: {stats.fps}</div>
|
||||
<div>Drawn: {stats.drawn.toLocaleString()} / 10,000,000</div>
|
||||
<div>Mode: {stats.mode}</div>
|
||||
<div>Zoom: {stats.zoom < 0.01 ? stats.zoom.toExponential(2) : stats.zoom.toFixed(2)} px/unit</div>
|
||||
<div>Pt Size: {stats.ptSize.toFixed(1)}px</div>
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
position: "absolute",
|
||||
bottom: 10,
|
||||
left: 10,
|
||||
background: "rgba(0,0,0,0.75)",
|
||||
color: "#888",
|
||||
fontFamily: "monospace",
|
||||
padding: "6px 10px",
|
||||
fontSize: "11px",
|
||||
borderRadius: "4px",
|
||||
pointerEvents: "none",
|
||||
}}
|
||||
>
|
||||
Drag to pan · Scroll to zoom
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user