From a52ea7022da39b52c34686d637fd35622a8ba530 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 24 Apr 2026 16:07:54 +0100 Subject: [PATCH] Preview selected input before runs --- frontend/src/app.ts | 70 +++++++++++++++++++++++++-- src/repixelizer/gui_static/app.js | 68 ++++++++++++++++++++++++-- src/repixelizer/gui_static/index.html | 4 ++ src/repixelizer/gui_static/styles.css | 7 +++ 4 files changed, 143 insertions(+), 6 deletions(-) diff --git a/frontend/src/app.ts b/frontend/src/app.ts index 5e70532..6529f6c 100644 --- a/frontend/src/app.ts +++ b/frontend/src/app.ts @@ -46,6 +46,7 @@ type AppState = { jobId: string | null; status: string; statusText: string; + inputPreviewImage: ImageAsset | null; sourceImage: ImageAsset | null; preprocessedImage: ImageAsset | null; edgeMapImage: ImageAsset | null; @@ -85,6 +86,7 @@ function byId(id: string): T { const fileInput = byId("fileInput"); const dropzone = byId("dropzone"); const dropzoneLabel = byId("dropzoneLabel"); +const inputPreviewCanvas = byId("inputPreviewCanvas"); const pickFileButton = byId("pickFileButton"); const runButton = byId("runButton"); const statusBadge = byId("statusBadge"); @@ -157,6 +159,7 @@ const state: AppState = { jobId: null, status: "idle", statusText: "Drop a fake sprite in here and let the machine explain itself for once.", + inputPreviewImage: null, sourceImage: null, preprocessedImage: null, edgeMapImage: null, @@ -319,6 +322,52 @@ async function drawAsset(canvas: HTMLCanvasElement, asset: ImageAsset | null): P context.drawImage(image, 0, 0, canvas.width, canvas.height); } +async function fileToImageAsset(file: File): Promise { + const dataUrl = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result !== "string") { + reject(new Error("Failed to read image preview.")); + return; + } + resolve(reader.result); + }; + reader.onerror = () => reject(reader.error ?? new Error("Failed to read image preview.")); + reader.readAsDataURL(file); + }); + const image = await loadImage({ dataUrl, width: 1, height: 1 }); + return { + dataUrl, + width: image.naturalWidth || 1, + height: image.naturalHeight || 1, + }; +} + +async function renderInputPreview(): Promise { + const context = inputPreviewCanvas.getContext("2d"); + if (!context) { + return; + } + const asset = state.inputPreviewImage; + if (!asset) { + inputPreviewCanvas.width = 320; + inputPreviewCanvas.height = 180; + context.clearRect(0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); + context.fillStyle = "#081012"; + context.fillRect(0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); + context.fillStyle = "rgba(255,255,255,0.2)"; + context.font = "16px sans-serif"; + context.fillText("No source loaded yet", 20, 32); + return; + } + const image = await loadImage(asset); + inputPreviewCanvas.width = image.naturalWidth || asset.width; + inputPreviewCanvas.height = image.naturalHeight || asset.height; + context.imageSmoothingEnabled = false; + context.clearRect(0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); + context.drawImage(image, 0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); +} + function readNestedNumber(root: JsonRecord | null, path: string[]): number | null { let current: unknown = root; for (const key of path) { @@ -913,10 +962,24 @@ async function startRun(): Promise { } } -function acceptFile(file: File): void { +async function acceptFile(file: File): Promise { state.file = file; dropzoneLabel.textContent = file.name; setStatus("idle", `${file.name} is loaded. Hit run when you want the machine to start sweating.`); + try { + const previewImage = await fileToImageAsset(file); + if (state.file !== file) { + return; + } + state.inputPreviewImage = previewImage; + } catch (error) { + if (state.file !== file) { + return; + } + state.inputPreviewImage = null; + setStatus("failed", error instanceof Error ? error.message : "Failed to load image preview."); + } + await renderInputPreview(); } dropzone.addEventListener("click", () => fileInput.click()); @@ -924,7 +987,7 @@ pickFileButton.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", () => { const file = fileInput.files?.[0]; if (file) { - acceptFile(file); + void acceptFile(file); } }); @@ -935,7 +998,7 @@ dropzone.addEventListener("drop", (event) => { event.preventDefault(); const file = event.dataTransfer?.files?.[0]; if (file) { - acceptFile(file); + void acceptFile(file); } }); @@ -1097,4 +1160,5 @@ setPaintColor(state.paintColor); syncEditorCursorState(); zoomValue.textContent = `${state.zoom}x`; renderEventLog(); +void renderInputPreview(); void renderEverything(); diff --git a/src/repixelizer/gui_static/app.js b/src/repixelizer/gui_static/app.js index 6f4f6dc..1e3fead 100644 --- a/src/repixelizer/gui_static/app.js +++ b/src/repixelizer/gui_static/app.js @@ -9,6 +9,7 @@ function byId(id) { const fileInput = byId("fileInput"); const dropzone = byId("dropzone"); const dropzoneLabel = byId("dropzoneLabel"); +const inputPreviewCanvas = byId("inputPreviewCanvas"); const pickFileButton = byId("pickFileButton"); const runButton = byId("runButton"); const statusBadge = byId("statusBadge"); @@ -77,6 +78,7 @@ const state = { jobId: null, status: "idle", statusText: "Drop a fake sprite in here and let the machine explain itself for once.", + inputPreviewImage: null, sourceImage: null, preprocessedImage: null, edgeMapImage: null, @@ -225,6 +227,50 @@ async function drawAsset(canvas, asset) { context.clearRect(0, 0, canvas.width, canvas.height); context.drawImage(image, 0, 0, canvas.width, canvas.height); } +async function fileToImageAsset(file) { + const dataUrl = await new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = () => { + if (typeof reader.result !== "string") { + reject(new Error("Failed to read image preview.")); + return; + } + resolve(reader.result); + }; + reader.onerror = () => reject(reader.error ?? new Error("Failed to read image preview.")); + reader.readAsDataURL(file); + }); + const image = await loadImage({ dataUrl, width: 1, height: 1 }); + return { + dataUrl, + width: image.naturalWidth || 1, + height: image.naturalHeight || 1, + }; +} +async function renderInputPreview() { + const context = inputPreviewCanvas.getContext("2d"); + if (!context) { + return; + } + const asset = state.inputPreviewImage; + if (!asset) { + inputPreviewCanvas.width = 320; + inputPreviewCanvas.height = 180; + context.clearRect(0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); + context.fillStyle = "#081012"; + context.fillRect(0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); + context.fillStyle = "rgba(255,255,255,0.2)"; + context.font = "16px sans-serif"; + context.fillText("No source loaded yet", 20, 32); + return; + } + const image = await loadImage(asset); + inputPreviewCanvas.width = image.naturalWidth || asset.width; + inputPreviewCanvas.height = image.naturalHeight || asset.height; + context.imageSmoothingEnabled = false; + context.clearRect(0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); + context.drawImage(image, 0, 0, inputPreviewCanvas.width, inputPreviewCanvas.height); +} function readNestedNumber(root, path) { let current = root; for (const key of path) { @@ -791,17 +837,32 @@ async function startRun() { addLog("Failure", error instanceof Error ? error.message : "Failed to start GUI job."); } } -function acceptFile(file) { +async function acceptFile(file) { state.file = file; dropzoneLabel.textContent = file.name; setStatus("idle", `${file.name} is loaded. Hit run when you want the machine to start sweating.`); + try { + const previewImage = await fileToImageAsset(file); + if (state.file !== file) { + return; + } + state.inputPreviewImage = previewImage; + } + catch (error) { + if (state.file !== file) { + return; + } + state.inputPreviewImage = null; + setStatus("failed", error instanceof Error ? error.message : "Failed to load image preview."); + } + await renderInputPreview(); } dropzone.addEventListener("click", () => fileInput.click()); pickFileButton.addEventListener("click", () => fileInput.click()); fileInput.addEventListener("change", () => { const file = fileInput.files?.[0]; if (file) { - acceptFile(file); + void acceptFile(file); } }); dropzone.addEventListener("dragover", (event) => { @@ -811,7 +872,7 @@ dropzone.addEventListener("drop", (event) => { event.preventDefault(); const file = event.dataTransfer?.files?.[0]; if (file) { - acceptFile(file); + void acceptFile(file); } }); runButton.addEventListener("click", () => { @@ -947,4 +1008,5 @@ setPaintColor(state.paintColor); syncEditorCursorState(); zoomValue.textContent = `${state.zoom}x`; renderEventLog(); +void renderInputPreview(); void renderEverything(); diff --git a/src/repixelizer/gui_static/index.html b/src/repixelizer/gui_static/index.html index 663e497..2e612b3 100644 --- a/src/repixelizer/gui_static/index.html +++ b/src/repixelizer/gui_static/index.html @@ -38,6 +38,10 @@ Drop image PNG, sprite scrap, cursed logo. Whatever started this. +
+
Selected Source
+ +
diff --git a/src/repixelizer/gui_static/styles.css b/src/repixelizer/gui_static/styles.css index c0569e8..26772ed 100644 --- a/src/repixelizer/gui_static/styles.css +++ b/src/repixelizer/gui_static/styles.css @@ -590,6 +590,7 @@ p { gap: 10px; } +.input-preview-card, .viz-card { margin: 0; padding: 10px; @@ -606,6 +607,7 @@ p { text-transform: uppercase; } +.input-preview-card canvas, .viz-card canvas { width: 100%; height: auto; @@ -619,6 +621,11 @@ p { 0 0 14px rgba(255, 216, 74, 0.06); } +.input-preview-card canvas { + max-height: 280px; + object-fit: contain; +} + .metrics-panel { display: grid; gap: 10px;