import { useEffect, useRef } from 'react' import type { BuiltinPhantom } from '../phantom/builtins.ts' import type { Phantom } from '../phantom/phantomTypes.ts' import { phantomExtent } from '../phantom/phantomTypes.ts' function PhantomPreview({ phantom }: { phantom: Phantom }) { const ref = useRef(null) useEffect(() => { const canvas = ref.current if (!canvas) return const ctx = canvas.getContext('2d') if (!ctx) return const W = canvas.width const H = canvas.height ctx.clearRect(0, 0, W, H) const ext = phantomExtent(phantom) const span = Math.max(ext.max[0] - ext.min[0], ext.max[1] - ext.min[1], 1e-6) * 1.15 const cx = (ext.min[0] + ext.max[0]) / 2 const cy = (ext.min[1] + ext.max[1]) / 2 const scale = Math.min(W, H) / span // Depth shading by z so the 3-D shape reads. const zmin = ext.min[2] const zmax = ext.max[2] const zrange = zmax - zmin || 1 ctx.globalCompositeOperation = 'lighter' for (let i = 0; i < phantom.ns; i++) { const px = W / 2 + (phantom.x[i] - cx) * scale const py = H / 2 - (phantom.y[i] - cy) * scale const zt = (phantom.z[i] - zmin) / zrange const shade = Math.round(60 + 150 * zt) ctx.fillStyle = `rgba(${Math.round(shade * 0.35)}, ${shade}, ${Math.round(shade * 0.9)}, 0.5)` ctx.fillRect(px - 1, py - 1, 2, 2) } ctx.globalCompositeOperation = 'source-over' }, [phantom]) return } export function PhantomPanel({ builtins, selectedId, onSelect, phantom, loading, }: { builtins: BuiltinPhantom[] selectedId: string onSelect: (id: string) => void phantom: Phantom | null loading: boolean }) { return (

Phantom

{builtins.map((b) => ( ))}
{loading &&
Loading phantom…
} {!loading && phantom && } {!loading && phantom && (
{phantom.ns.toLocaleString()} spins
T1 {(phantom.t1[0] * 1000).toFixed(0)} ms · T2 {(phantom.t2[0] * 1000).toFixed(0)} ms
XY projection, shaded by z
)}

.phantom format. A single water-like tissue, densely and uniformly sampled.

) }