// Canvas — renders the actual session photo, the warp/heal results,
// and overlays. All images are
tags served by /api/image/...
const Canvas = ({ ctx }) => {
const { state, set } = ctx;
const [splitPos, setSplitPos] = React.useState(50);
const [pressing, setPressing] = React.useState(false);
const handleSplitDrag = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const move = (ev) => {
const x = ((ev.clientX - rect.left) / rect.width) * 100;
setSplitPos(Math.max(0, Math.min(100, x)));
};
const up = () => {
window.removeEventListener("mousemove", move);
window.removeEventListener("mouseup", up);
};
window.addEventListener("mousemove", move);
window.addEventListener("mouseup", up);
};
const session = state.session;
if (!session) return ;
// Resolve the "projection" image — preference: active heal result, else live warp, else original
const activeResult = state.results.find(r => r.id === state.activeResult);
const projectionUrl =
activeResult && activeResult.kind !== "original" ? activeResult.url
: state.warpImageUrl
|| session.image_url;
const projectionLabel =
activeResult && activeResult.kind === "heal" ? "Photoreal heal"
: state.warpImageUrl ? "Warp preview"
: "Baseline";
const originalUrl = session.image_url;
const compareMode = state.compareMode;
return (
{[["toggle", "Hold to compare"], ["split", "Split slider"], ["onion", "Onion-skin"], ["side", "Side-by-side"]].map(([m, label]) => (
))}
VIEW · {session.view.tag} · yaw {session.view.yaw}°
{compareMode === "side" && (
)}
{compareMode === "split" && (
Before
After
)}
{compareMode === "onion" && (
)}
{compareMode === "toggle" && (
setPressing(true)}
onMouseUp={() => setPressing(false)}
onMouseLeave={() => setPressing(false)}
style={{ cursor: "pointer", userSelect: "none" }}
>
)}
{state.healing &&
}
SESSION · {session.session_id}
VIEW · {session.view.tag}
LANDMARKS · {session.landmarks_count}
IPD · {session.measurements.ipd_px}px
SYMMETRY · {session.measurements.symmetry}
);
};
const EmptyCanvas = ({ ctx }) => (
NO SESSION · Upload a portrait or pick a sample
Begin with a portrait.
Drag & drop, browse, or pick a sample on the left.
NO SESSION
STATUS · ready
);
const PhotoFrame = ({ url, label, mono, sub, view, overlays }) => (
);
const FaceCorners = ({ view, label, sub, mono }) => (
<>
{mono}
{label}
{sub}
VIEW · {(view.tag || view).toString().toUpperCase()}
{view.yaw != null ? `YAW ${view.yaw}°` : ""}
>
);
const HealProgress = () => {
const [progress, setProgress] = React.useState(0.05);
React.useEffect(() => {
let raf;
const start = Date.now();
const tick = () => {
const t = (Date.now() - start) / 30000;
setProgress(Math.min(0.92, t));
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
return () => cancelAnimationFrame(raf);
}, []);
const elapsed = progress * 30;
const stage = progress < 0.15 ? 0 : progress < 0.85 ? 1 : 2;
return (
{stage === 0 ? "Stage 01 · Queued" : stage === 1 ? "Stage 02 · Generating" : "Stage 03 · Blending"}
Synthesizing your photoreal heal.
= 1 ? "done" : "active"}>QUEUED
= 2 ? "done" : (stage === 1 ? "active" : "")}>GENERATING
BLENDING
fal.ai · flux-general/inpainting · diffusion can take up to a minute
);
};
Object.assign(window, { Canvas, FaceCorners, HealProgress, EmptyCanvas });