Preview selected input before runs
This commit is contained in:
parent
0f60e025d1
commit
a52ea7022d
4 changed files with 143 additions and 6 deletions
|
|
@ -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<T extends HTMLElement>(id: string): T {
|
|||
const fileInput = byId<HTMLInputElement>("fileInput");
|
||||
const dropzone = byId<HTMLButtonElement>("dropzone");
|
||||
const dropzoneLabel = byId<HTMLSpanElement>("dropzoneLabel");
|
||||
const inputPreviewCanvas = byId<HTMLCanvasElement>("inputPreviewCanvas");
|
||||
const pickFileButton = byId<HTMLButtonElement>("pickFileButton");
|
||||
const runButton = byId<HTMLButtonElement>("runButton");
|
||||
const statusBadge = byId<HTMLSpanElement>("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<ImageAsset> {
|
||||
const dataUrl = await new Promise<string>((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<void> {
|
||||
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<void> {
|
|||
}
|
||||
}
|
||||
|
||||
function acceptFile(file: File): void {
|
||||
async function acceptFile(file: File): Promise<void> {
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -38,6 +38,10 @@
|
|||
<strong>Drop image</strong>
|
||||
<span id="dropzoneLabel">PNG, sprite scrap, cursed logo. Whatever started this.</span>
|
||||
</button>
|
||||
<figure class="input-preview-card">
|
||||
<div class="viz-label">Selected Source</div>
|
||||
<canvas id="inputPreviewCanvas"></canvas>
|
||||
</figure>
|
||||
</section>
|
||||
|
||||
<section class="card section-block">
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in a new issue