Skip to content

Instantly share code, notes, and snippets.

@ericek111
Last active August 8, 2026 15:49
Show Gist options
  • Select an option

  • Save ericek111/f65103ae5075c3bfdd0d046f6f62051a to your computer and use it in GitHub Desktop.

Select an option

Save ericek111/f65103ae5075c3bfdd0d046f6f62051a to your computer and use it in GitHub Desktop.
A simple LLM-coded magnifier app
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Local Image Magnifier</title>
<style>
:root {
color-scheme: dark;
--bg: #111418;
--panel: #1b2027;
--panel-2: #242a33;
--text: #eef2f6;
--muted: #9ea9b6;
--line: #343c48;
--accent: #4fb3ff;
--danger: #ff6b6b;
--shadow: 0 16px 50px rgba(0, 0, 0, 0.35);
}
* {
box-sizing: border-box;
}
html,
body {
height: 100%;
}
body {
margin: 0;
overflow: hidden;
background:
linear-gradient(45deg, rgba(255, 255, 255, 0.035) 25%, transparent 25%),
linear-gradient(-45deg, rgba(255, 255, 255, 0.035) 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, rgba(255, 255, 255, 0.035) 75%),
linear-gradient(-45deg, transparent 75%, rgba(255, 255, 255, 0.035) 75%),
var(--bg);
background-position: 0 0, 0 12px, 12px -12px, -12px 0;
background-size: 24px 24px;
color: var(--text);
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
button,
input {
font: inherit;
}
.app {
display: grid;
grid-template-rows: auto 1fr;
height: 100%;
min-width: 320px;
}
.toolbar {
display: flex;
align-items: center;
gap: 10px;
min-height: 64px;
padding: 10px 14px;
border-bottom: 1px solid var(--line);
background: rgba(27, 32, 39, 0.94);
box-shadow: var(--shadow);
backdrop-filter: blur(12px);
z-index: 5;
}
.brand {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 150px;
margin-right: 6px;
}
.brand strong {
font-size: 14px;
line-height: 1.1;
}
.brand span {
color: var(--muted);
font-size: 12px;
line-height: 1.2;
}
.controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
width: 100%;
}
.control-group {
display: inline-flex;
align-items: center;
gap: 8px;
}
.control-group.open-group {
margin-right: 10px;
padding-right: 18px;
border-right: 1px solid var(--line);
}
.button,
.file-label,
.toggle,
.zoom-field,
.meta-label {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 7px;
min-height: 36px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel-2);
color: var(--text);
cursor: pointer;
user-select: none;
}
.button,
.file-label {
min-width: 36px;
padding: 0 11px;
}
.button:hover,
.file-label:hover,
.toggle:hover {
border-color: #566170;
background: #2b323d;
}
.button.active {
border-color: rgba(79, 179, 255, 0.9);
background: rgba(79, 179, 255, 0.18);
color: #dff2ff;
}
.button:active,
.file-label:active {
transform: translateY(1px);
}
.button:disabled {
cursor: not-allowed;
opacity: 0.45;
}
.file-label input {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
pointer-events: none;
}
.zoom-field {
width: 92px;
padding: 0 10px;
border: 1px solid var(--line);
border-radius: 8px;
background: #151a20;
color: var(--text);
text-align: center;
font-variant-numeric: tabular-nums;
font-size: 13px;
}
.meta-label {
min-height: 36px;
padding: 0 10px;
background: #151a20;
color: var(--muted);
font-size: 13px;
font-variant-numeric: tabular-nums;
cursor: default;
}
.zoom-field:focus {
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 3px rgba(79, 179, 255, 0.16);
}
.toggle {
position: relative;
padding: 0 10px;
font-size: 13px;
white-space: nowrap;
}
.toggle input {
width: 16px;
height: 16px;
margin: 0;
accent-color: var(--accent);
}
.hint {
margin-left: auto;
color: var(--muted);
font-size: 12px;
line-height: 1.25;
text-align: right;
}
.stage {
position: relative;
overflow: hidden;
min-height: 0;
cursor: grab;
touch-action: none;
}
.stage.selecting {
cursor: crosshair;
}
.stage.dragging {
cursor: grabbing;
}
.stage.drop-ready::after {
content: "";
position: absolute;
inset: 18px;
border: 2px dashed var(--accent);
border-radius: 12px;
background: rgba(79, 179, 255, 0.08);
pointer-events: none;
z-index: 4;
}
.empty {
position: absolute;
inset: 0;
display: grid;
place-items: center;
padding: 24px;
text-align: center;
pointer-events: none;
}
.empty-box {
width: min(520px, 100%);
padding: 28px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(27, 32, 39, 0.9);
box-shadow: var(--shadow);
}
.empty-box h1 {
margin: 0 0 10px;
font-size: clamp(24px, 5vw, 44px);
line-height: 1;
}
.empty-box p {
margin: 0;
color: var(--muted);
line-height: 1.5;
}
canvas {
display: block;
width: 100%;
height: 100%;
}
.toast {
position: absolute;
right: 16px;
bottom: 16px;
max-width: min(360px, calc(100vw - 32px));
padding: 10px 12px;
border: 1px solid rgba(255, 107, 107, 0.45);
border-radius: 8px;
background: rgba(51, 20, 24, 0.95);
color: #ffdede;
font-size: 13px;
box-shadow: var(--shadow);
opacity: 0;
transform: translateY(8px);
transition: opacity 160ms ease, transform 160ms ease;
pointer-events: none;
z-index: 6;
}
.toast.visible {
opacity: 1;
transform: translateY(0);
}
@media (max-width: 760px) {
.toolbar {
align-items: flex-start;
flex-direction: column;
min-height: 0;
}
.brand {
min-width: 0;
}
.hint {
display: none;
}
.controls {
width: 100%;
}
.control-group.open-group {
margin-right: 0;
padding-right: 0;
border-right: 0;
}
.zoom-field {
width: 82px;
}
}
</style>
</head>
<body>
<main class="app">
<header class="toolbar">
<div class="brand">
<strong>Local Image Magnifier</strong>
<span>No upload leaves this browser</span>
</div>
<div class="controls">
<div class="control-group open-group">
<label class="file-label" title="Open an image file">
Open
<input id="fileInput" type="file" accept="image/*">
</label>
</div>
<div class="control-group">
<button class="button" id="zoomOut" type="button" title="Zoom out one step">-</button>
<input class="zoom-field" id="zoomInput" type="text" value="100%" inputmode="decimal" title="Enter zoom percentage">
<button class="button" id="zoomIn" type="button" title="Zoom in one step">+</button>
</div>
<button class="button" id="fitImage" type="button" title="Fit image to window">Fit</button>
<button class="button" id="actualSize" type="button" title="Show one image pixel per screen pixel">1:1</button>
<button class="button" id="centerImage" type="button" title="Center the current image position">Center</button>
<button class="button" id="selectToggle" type="button" title="Draw or resize a pixel-snapped selection">Select</button>
<button class="button" id="saveSelection" type="button" title="Save the current selection as a PNG" disabled>Save PNG</button>
<button class="button" id="fullscreenToggle" type="button" title="Toggle fullscreen">Fullscreen</button>
<div class="meta-label" id="resolutionLabel">No image</div>
<label class="toggle" title="Use nearest-neighbor scaling for crisp pixels">
<input id="pixelated" type="checkbox" checked>
Pixelated
</label>
<label class="toggle" title="Mouse wheel moves between integer zoom levels">
<input id="integerWheel" type="checkbox">
Integer wheel
</label>
<div class="hint">Wheel zooms at cursor. Drag pans; middle-drag pans while selecting.</div>
</div>
</header>
<section class="stage" id="stage" tabindex="0" aria-label="Image viewer">
<canvas id="canvas"></canvas>
<div class="empty" id="empty">
<div class="empty-box">
<h1>Drop, paste, or open a picture</h1>
<p>Use the mouse wheel for smooth map-style zooming, toggle integer wheel steps for exact scaling, and turn Pixelated off to smooth scaling.</p>
</div>
</div>
<div class="toast" id="toast"></div>
</section>
</main>
<script>
const stage = document.getElementById("stage");
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const empty = document.getElementById("empty");
const toast = document.getElementById("toast");
const fileInput = document.getElementById("fileInput");
const zoomOut = document.getElementById("zoomOut");
const zoomIn = document.getElementById("zoomIn");
const fitImage = document.getElementById("fitImage");
const actualSize = document.getElementById("actualSize");
const centerImageButton = document.getElementById("centerImage");
const selectToggle = document.getElementById("selectToggle");
const saveSelection = document.getElementById("saveSelection");
const fullscreenToggle = document.getElementById("fullscreenToggle");
const resolutionLabel = document.getElementById("resolutionLabel");
const zoomInput = document.getElementById("zoomInput");
const pixelated = document.getElementById("pixelated");
const integerWheel = document.getElementById("integerWheel");
const state = {
image: null,
scale: 1,
offsetX: 0,
offsetY: 0,
dragging: false,
dragStartX: 0,
dragStartY: 0,
dragOffsetX: 0,
dragOffsetY: 0,
selectMode: false,
selection: null,
selectionAction: null,
dpr: 1
};
const minScale = 0.01;
const maxScale = 128;
const integerLevels = [
0.03125, 0.0625, 0.125, 0.25, 0.5,
1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 24, 32, 48, 64, 96, 128
];
function clamp(value, min, max) {
return Math.min(max, Math.max(min, value));
}
function showToast(message) {
toast.textContent = message;
toast.classList.add("visible");
window.clearTimeout(showToast.timer);
showToast.timer = window.setTimeout(() => toast.classList.remove("visible"), 2600);
}
function formatZoom(scale) {
const percent = scale * 100;
if (percent >= 100) {
return `${Math.round(percent)}%`;
}
if (percent >= 10) {
return `${Math.round(percent * 10) / 10}%`;
}
return `${Math.round(percent * 100) / 100}%`;
}
function syncZoomInput() {
if (document.activeElement !== zoomInput) {
zoomInput.value = formatZoom(state.scale);
}
}
function syncUi() {
resolutionLabel.textContent = state.image
? `${state.image.naturalWidth} x ${state.image.naturalHeight} px`
: "No image";
selectToggle.classList.toggle("active", state.selectMode);
selectToggle.setAttribute("aria-pressed", String(state.selectMode));
saveSelection.disabled = !state.image || !normalizedSelection();
stage.classList.toggle("selecting", state.selectMode);
}
function parseZoomInput(value) {
const normalized = value.trim().replace("%", "");
if (!normalized) return null;
const parsed = Number(normalized);
if (!Number.isFinite(parsed) || parsed <= 0) return null;
return parsed / 100;
}
function resizeCanvas() {
const rect = stage.getBoundingClientRect();
state.dpr = window.devicePixelRatio || 1;
canvas.width = Math.max(1, Math.round(rect.width * state.dpr));
canvas.height = Math.max(1, Math.round(rect.height * state.dpr));
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
draw();
}
function draw() {
const rect = stage.getBoundingClientRect();
ctx.setTransform(state.dpr, 0, 0, state.dpr, 0, 0);
ctx.clearRect(0, 0, rect.width, rect.height);
empty.style.display = state.image ? "none" : "grid";
syncZoomInput();
syncUi();
if (!state.image) {
return;
}
ctx.imageSmoothingEnabled = !pixelated.checked;
ctx.imageSmoothingQuality = "high";
const width = state.image.naturalWidth * state.scale;
const height = state.image.naturalHeight * state.scale;
ctx.drawImage(state.image, state.offsetX, state.offsetY, width, height);
drawSelection();
}
function drawSelection() {
const selection = normalizedSelection();
if (!selection) return;
const x = state.offsetX + selection.x * state.scale;
const y = state.offsetY + selection.y * state.scale;
const width = selection.w * state.scale;
const height = selection.h * state.scale;
const label = `${selection.w} x ${selection.h} px`;
ctx.save();
ctx.fillStyle = "rgba(79, 179, 255, 0.16)";
ctx.strokeStyle = "#4fb3ff";
ctx.lineWidth = 1;
ctx.setLineDash([6, 4]);
ctx.fillRect(x, y, width, height);
ctx.strokeRect(x + 0.5, y + 0.5, width, height);
ctx.setLineDash([]);
const handles = selectionHandles(selection);
ctx.fillStyle = "#eef2f6";
ctx.strokeStyle = "#111418";
ctx.lineWidth = 1;
for (const handle of handles) {
ctx.fillRect(handle.x - 4, handle.y - 4, 8, 8);
ctx.strokeRect(handle.x - 4.5, handle.y - 4.5, 9, 9);
}
ctx.font = "12px Inter, ui-sans-serif, system-ui, sans-serif";
ctx.textBaseline = "middle";
const metrics = ctx.measureText(label);
const labelWidth = Math.ceil(metrics.width) + 12;
const labelHeight = 22;
const rect = stage.getBoundingClientRect();
const labelX = clamp(x + width - labelWidth, 4, rect.width - labelWidth - 4);
const labelY = clamp(y + height - labelHeight, 4, rect.height - labelHeight - 4);
ctx.fillStyle = "rgba(17, 20, 24, 0.88)";
ctx.strokeStyle = "rgba(79, 179, 255, 0.8)";
ctx.lineWidth = 1;
ctx.fillRect(labelX, labelY, labelWidth, labelHeight);
ctx.strokeRect(labelX + 0.5, labelY + 0.5, labelWidth, labelHeight);
ctx.fillStyle = "#eef2f6";
ctx.fillText(label, labelX + 6, labelY + labelHeight / 2);
ctx.restore();
}
function fitToStage() {
if (!state.image) return;
const rect = stage.getBoundingClientRect();
const padding = 32;
const scaleX = (rect.width - padding) / state.image.naturalWidth;
const scaleY = (rect.height - padding) / state.image.naturalHeight;
state.scale = clamp(Math.min(scaleX, scaleY), minScale, maxScale);
centerImage();
draw();
}
function centerImage() {
if (!state.image) return;
const rect = stage.getBoundingClientRect();
state.offsetX = (rect.width - state.image.naturalWidth * state.scale) / 2;
state.offsetY = (rect.height - state.image.naturalHeight * state.scale) / 2;
}
function imagePointFromEvent(event) {
const rect = stage.getBoundingClientRect();
const x = (event.clientX - rect.left - state.offsetX) / state.scale;
const y = (event.clientY - rect.top - state.offsetY) / state.scale;
return {
x: clamp(Math.round(x), 0, state.image.naturalWidth),
y: clamp(Math.round(y), 0, state.image.naturalHeight)
};
}
function screenPointFromImagePoint(x, y) {
return {
x: state.offsetX + x * state.scale,
y: state.offsetY + y * state.scale
};
}
function normalizedSelection(selection = state.selection) {
if (!selection) return null;
const x1 = clamp(Math.min(selection.x, selection.x + selection.w), 0, state.image.naturalWidth);
const y1 = clamp(Math.min(selection.y, selection.y + selection.h), 0, state.image.naturalHeight);
const x2 = clamp(Math.max(selection.x, selection.x + selection.w), 0, state.image.naturalWidth);
const y2 = clamp(Math.max(selection.y, selection.y + selection.h), 0, state.image.naturalHeight);
const normalized = {
x: Math.round(x1),
y: Math.round(y1),
w: Math.round(x2 - x1),
h: Math.round(y2 - y1)
};
return normalized.w > 0 && normalized.h > 0 ? normalized : null;
}
function selectionHandles(selection) {
const left = state.offsetX + selection.x * state.scale;
const top = state.offsetY + selection.y * state.scale;
const right = state.offsetX + (selection.x + selection.w) * state.scale;
const bottom = state.offsetY + (selection.y + selection.h) * state.scale;
const centerX = (left + right) / 2;
const centerY = (top + bottom) / 2;
return [
{ name: "nw", x: left, y: top },
{ name: "n", x: centerX, y: top },
{ name: "ne", x: right, y: top },
{ name: "e", x: right, y: centerY },
{ name: "se", x: right, y: bottom },
{ name: "s", x: centerX, y: bottom },
{ name: "sw", x: left, y: bottom },
{ name: "w", x: left, y: centerY }
];
}
function hitSelection(event) {
const selection = normalizedSelection();
if (!selection) return { type: "create" };
const rect = stage.getBoundingClientRect();
const point = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
const handleRadius = 8;
for (const handle of selectionHandles(selection)) {
if (Math.abs(point.x - handle.x) <= handleRadius && Math.abs(point.y - handle.y) <= handleRadius) {
return { type: "resize", handle: handle.name, selection };
}
}
const topLeft = screenPointFromImagePoint(selection.x, selection.y);
const bottomRight = screenPointFromImagePoint(selection.x + selection.w, selection.y + selection.h);
if (
point.x >= topLeft.x &&
point.x <= bottomRight.x &&
point.y >= topLeft.y &&
point.y <= bottomRight.y
) {
return { type: "move", selection };
}
return { type: "create" };
}
function beginSelection(event) {
const point = imagePointFromEvent(event);
const hit = hitSelection(event);
state.selectionAction = {
type: hit.type,
handle: hit.handle,
startPoint: point,
startSelection: hit.selection || normalizedSelection()
};
if (hit.type === "create") {
state.selection = { x: point.x, y: point.y, w: 0, h: 0 };
}
}
function updateSelection(event) {
if (!state.selectionAction) return;
const point = imagePointFromEvent(event);
const action = state.selectionAction;
if (action.type === "create") {
state.selection.w = point.x - state.selection.x;
state.selection.h = point.y - state.selection.y;
} else if (action.type === "move" && action.startSelection) {
const dx = point.x - action.startPoint.x;
const dy = point.y - action.startPoint.y;
state.selection = {
x: clamp(action.startSelection.x + dx, 0, state.image.naturalWidth - action.startSelection.w),
y: clamp(action.startSelection.y + dy, 0, state.image.naturalHeight - action.startSelection.h),
w: action.startSelection.w,
h: action.startSelection.h
};
} else if (action.type === "resize" && action.startSelection) {
const start = action.startSelection;
let left = start.x;
let top = start.y;
let right = start.x + start.w;
let bottom = start.y + start.h;
if (action.handle.includes("w")) left = point.x;
if (action.handle.includes("e")) right = point.x;
if (action.handle.includes("n")) top = point.y;
if (action.handle.includes("s")) bottom = point.y;
state.selection = {
x: clamp(Math.min(left, right), 0, state.image.naturalWidth),
y: clamp(Math.min(top, bottom), 0, state.image.naturalHeight),
w: clamp(Math.max(left, right), 0, state.image.naturalWidth) - clamp(Math.min(left, right), 0, state.image.naturalWidth),
h: clamp(Math.max(top, bottom), 0, state.image.naturalHeight) - clamp(Math.min(top, bottom), 0, state.image.naturalHeight)
};
}
draw();
}
function finishSelection() {
state.selection = normalizedSelection();
state.selectionAction = null;
draw();
}
function saveSelectionAsPng() {
const selection = normalizedSelection();
if (!state.image || !selection) {
showToast("Create a selection first.");
return;
}
const output = document.createElement("canvas");
output.width = selection.w;
output.height = selection.h;
const outputContext = output.getContext("2d");
outputContext.imageSmoothingEnabled = false;
outputContext.drawImage(
state.image,
selection.x,
selection.y,
selection.w,
selection.h,
0,
0,
selection.w,
selection.h
);
output.toBlob(blob => {
if (!blob) {
showToast("The selected area could not be saved.");
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = `selection-${selection.w}x${selection.h}.png`;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1000);
}, "image/png");
}
function setScaleAtPoint(nextScale, pointX, pointY) {
if (!state.image) return;
const oldScale = state.scale;
nextScale = clamp(nextScale, minScale, maxScale);
const imageX = (pointX - state.offsetX) / oldScale;
const imageY = (pointY - state.offsetY) / oldScale;
state.scale = nextScale;
state.offsetX = pointX - imageX * nextScale;
state.offsetY = pointY - imageY * nextScale;
draw();
}
function stepScale(direction, pointX, pointY) {
const current = state.scale;
let index = integerLevels.findIndex(level => level > current * 1.00001);
if (direction < 0) {
const lower = integerLevels.filter(level => level < current / 1.00001);
index = lower.length ? integerLevels.indexOf(lower[lower.length - 1]) : 0;
} else if (index === -1) {
index = integerLevels.length - 1;
}
setScaleAtPoint(integerLevels[index], pointX, pointY);
}
async function loadFile(file) {
if (!file || !file.type.startsWith("image/")) {
showToast("Please choose an image file.");
return;
}
const url = URL.createObjectURL(file);
const image = new Image();
image.onload = () => {
if (state.imageUrl) {
URL.revokeObjectURL(state.imageUrl);
}
state.imageUrl = url;
state.image = image;
state.selection = null;
state.selectionAction = null;
fitToStage();
stage.focus();
};
image.onerror = () => {
URL.revokeObjectURL(url);
showToast("That image could not be opened.");
};
image.src = url;
}
function firstImageFile(items) {
for (const item of items) {
if (item.kind === "file") {
const file = item.getAsFile();
if (file && file.type.startsWith("image/")) {
return file;
}
}
}
return null;
}
fileInput.addEventListener("change", event => {
loadFile(event.target.files[0]);
event.target.value = "";
});
stage.addEventListener("wheel", event => {
if (!state.image) return;
event.preventDefault();
const rect = stage.getBoundingClientRect();
const pointX = event.clientX - rect.left;
const pointY = event.clientY - rect.top;
if (integerWheel.checked) {
stepScale(event.deltaY > 0 ? -1 : 1, pointX, pointY);
return;
}
const zoomFactor = Math.exp(-event.deltaY * 0.0015);
setScaleAtPoint(state.scale * zoomFactor, pointX, pointY);
}, { passive: false });
stage.addEventListener("pointerdown", event => {
if (!state.image || (event.button !== 0 && event.button !== 1)) return;
event.preventDefault();
if (state.selectMode && event.button === 0) {
beginSelection(event);
stage.setPointerCapture(event.pointerId);
draw();
return;
}
state.dragging = true;
state.dragStartX = event.clientX;
state.dragStartY = event.clientY;
state.dragOffsetX = state.offsetX;
state.dragOffsetY = state.offsetY;
stage.classList.add("dragging");
stage.setPointerCapture(event.pointerId);
});
stage.addEventListener("pointermove", event => {
if (state.selectionAction) {
if (event.buttons & 4) {
state.offsetX += event.movementX;
state.offsetY += event.movementY;
}
updateSelection(event);
return;
}
if (!state.dragging) return;
state.offsetX = state.dragOffsetX + event.clientX - state.dragStartX;
state.offsetY = state.dragOffsetY + event.clientY - state.dragStartY;
draw();
});
stage.addEventListener("pointerup", event => {
if (state.selectionAction) {
if (event.button !== 0 && event.buttons & 1) {
return;
}
finishSelection();
stage.releasePointerCapture(event.pointerId);
return;
}
state.dragging = false;
stage.classList.remove("dragging");
stage.releasePointerCapture(event.pointerId);
});
stage.addEventListener("pointercancel", () => {
state.dragging = false;
state.selectionAction = null;
stage.classList.remove("dragging");
draw();
});
stage.addEventListener("auxclick", event => {
if (event.button === 1) {
event.preventDefault();
}
});
stage.addEventListener("dragover", event => {
event.preventDefault();
stage.classList.add("drop-ready");
});
stage.addEventListener("dragleave", event => {
if (!stage.contains(event.relatedTarget)) {
stage.classList.remove("drop-ready");
}
});
stage.addEventListener("drop", event => {
event.preventDefault();
stage.classList.remove("drop-ready");
loadFile(event.dataTransfer.files[0]);
});
document.addEventListener("paste", event => {
const file = firstImageFile(event.clipboardData.items);
if (file) {
loadFile(file);
}
});
zoomOut.addEventListener("click", () => {
const rect = stage.getBoundingClientRect();
stepScale(-1, rect.width / 2, rect.height / 2);
});
zoomIn.addEventListener("click", () => {
const rect = stage.getBoundingClientRect();
stepScale(1, rect.width / 2, rect.height / 2);
});
fitImage.addEventListener("click", fitToStage);
actualSize.addEventListener("click", () => {
const rect = stage.getBoundingClientRect();
setScaleAtPoint(1, rect.width / 2, rect.height / 2);
});
pixelated.addEventListener("change", draw);
selectToggle.addEventListener("click", () => {
state.selectMode = !state.selectMode;
draw();
});
saveSelection.addEventListener("click", saveSelectionAsPng);
centerImageButton.addEventListener("click", () => {
centerImage();
draw();
});
fullscreenToggle.addEventListener("click", async () => {
try {
if (document.fullscreenElement) {
await document.exitFullscreen();
} else {
await document.documentElement.requestFullscreen();
}
} catch (error) {
showToast("Fullscreen is not available in this browser context.");
}
});
document.addEventListener("fullscreenchange", () => {
fullscreenToggle.textContent = document.fullscreenElement ? "Exit Fullscreen" : "Fullscreen";
window.setTimeout(resizeCanvas, 60);
});
zoomInput.addEventListener("keydown", event => {
if (event.key === "Enter") {
zoomInput.blur();
}
});
zoomInput.addEventListener("blur", () => {
const nextScale = parseZoomInput(zoomInput.value);
if (!nextScale) {
syncZoomInput();
showToast("Enter a positive zoom percentage.");
return;
}
const rect = stage.getBoundingClientRect();
setScaleAtPoint(nextScale, rect.width / 2, rect.height / 2);
syncZoomInput();
});
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
</script>
</body>
</html>
@ericek111

Copy link
Copy Markdown
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment