A thumbnail can have enough pixels and still cut off the thing that makes it recognizable. Before exporting several versions, it helps to make the crop itself visible.
This small, dependency-free HTML page displays a local still image, dims the area a crop will discard, and lets you move the retained rectangle. It does not upload the file or save an edited copy. Save the code as crop-overlay.html and open it in a modern browser.
What to check
Start with the intended final aspect ratio. Move the horizontal and vertical sliders until the important detail stays inside the blue box. A centered crop is only a starting point: a face near an edge, a long garment or an asymmetric icon may need a different position.
For an example, a 1600 × 900 image cropped to 1:1 retains 900 × 900 pixels. The horizontal slider moves that square across the 700 pixels of spare width. The vertical slider does nothing in this case because no height is spare. This is expected, not a broken control.
The complete page
<!doctype html>
<meta charset="utf-8">
<title>Crop planning overlay</title>
<style>
body{font:16px system-ui;max-width:800px;margin:24px auto;padding:0 16px}
canvas{max-width:100%;height:auto;background:#ddd;display:block;margin:16px 0}
label{display:block;margin:12px 0} input[type=range]{width:240px}
</style>
<h1>Preview what a crop removes</h1>
<p>This page uses your local image. The blue box is the retained area.</p>
<label>Image <input id="file" type="file" accept="image/png,image/jpeg,image/webp"></label>
<label>Target ratio <select id="ratio"><option value="1">1:1</option><option value="0.8">4:5</option><option value="1.7777777778">16:9</option></select></label>
<label>Horizontal position <input id="x" type="range" min="0" max="1" step="0.01" value="0.5"></label>
<label>Vertical position <input id="y" type="range" min="0" max="1" step="0.01" value="0.5"></label>
<p id="status" role="status">Choose a still image.</p>
<canvas id="canvas" width="720" height="480"></canvas>
<script>
const $ = id => document.getElementById(id);
const ctx = $('canvas').getContext('2d');
let bitmap, request = 0;
function cropRect(w, h, ratio, x, y) {
const cw = Math.min(w, h * ratio), ch = cw / ratio;
return {x: (w-cw)*x, y: (h-ch)*y, w: cw, h: ch};
}
function draw() {
if (!bitmap) return;
const s = Math.min(720/bitmap.width, 600/bitmap.height, 1);
const c = $('canvas');
c.width = Math.max(1, Math.round(bitmap.width*s));
c.height = Math.max(1, Math.round(bitmap.height*s));
const r = cropRect(bitmap.width, bitmap.height,
Number($('ratio').value), Number($('x').value), Number($('y').value));
const sx = c.width/bitmap.width, sy = c.height/bitmap.height;
ctx.drawImage(bitmap, 0, 0, c.width, c.height);
ctx.fillStyle = 'rgba(0,0,0,.55)';
ctx.fillRect(0, 0, c.width, c.height);
ctx.drawImage(bitmap, r.x,r.y,r.w,r.h, r.x*sx,r.y*sy,r.w*sx,r.h*sy);
ctx.strokeStyle = '#00bfff'; ctx.lineWidth = 3;
ctx.strokeRect(r.x*sx,r.y*sy,r.w*sx,r.h*sy);
$('status').textContent = `Retained: ${Math.round(r.w)} × ${Math.round(r.h)} px. ` +
`Source: ${bitmap.width} × ${bitmap.height} px. This is a preview, not an export.`;
}
$('file').onchange = async () => {
const id = ++request, file = $('file').files[0];
bitmap?.close(); bitmap = undefined;
ctx.clearRect(0,0,$('canvas').width,$('canvas').height);
if (!file) { $('status').textContent = 'Choose a still image.'; return; }
if (file.size > 12*1024*1024) { $('status').textContent = 'Choose a file under 12 MiB.'; return; }
try {
const next = await createImageBitmap(file);
if (id !== request) { next.close(); return; }
bitmap = next; draw();
} catch { if (id === request) $('status').textContent = 'This image could not be decoded.'; }
};
for (const id of ['ratio','x','y']) $(id).oninput = draw;
</script>
Why this geometry works
The largest crop at a chosen ratio fits within both source dimensions. Its width is the smaller of the source width and sourceHeight * ratio; its height is that width divided by the ratio. Each position slider selects a fraction of the unused space. The preview scales to fit 720 × 600 pixels without enlarging a small source image. The dimensions in the status line refer to the source crop, not the on-screen preview.
The second drawImage call redraws only the selected source rectangle over a dimmed full image. MDN documents the source and destination rectangle form of drawImage. The request counter prevents an older asynchronous image decode from replacing a newer selection; discarded bitmaps are closed.
Four useful review cases
I build the following products, so these are examples from my own tools, not independent endorsements:
- For an Emote Resizer source, check whether the expression or silhouette survives a square crop before making tiny exports. This preview does not simulate tiny-size legibility or GIF animation.
- For a PawMotif portrait, keep ears, whiskers and distinctive markings away from the edge. Decide on a frame ratio before ordering a separate print; the product itself supplies digital artwork.
- For a TryOnSwap outfit concept, preserve the hem and relevant accessories when making a vertical crop. Keeping them in frame does not validate AI garment accuracy or real-world fit.
- Before assembling a Free Image Merger comparison, review each source at the same target ratio. A consistent crop makes scale and framing differences easier to notice.
Limits and a small check plan
This is a planning overlay. It does not export, strip metadata, validate platform upload rules, manage color profiles or promise print quality. Only still PNG, JPEG and WebP are offered. The 12 MiB file limit reduces accidental large inputs but does not bound decoded memory: a highly compressed image can still be enormous. Close the page if a device struggles with a large file.
Try a landscape source with a square crop, a portrait source with a wide crop, and a square source at 1:1. At the two ends of either slider, the crop should stay within the image. For a ratio already matching the image, both sliders should leave the full image unchanged. Finally, select two files quickly and check that the last selection is the one shown.
The crop geometry was checked separately for landscape, portrait and matching-ratio cases. Browser decoding, color appearance and performance remain device-dependent. The code and this article were prepared with AI assistance and should be reviewed before adapting them into a production upload flow.
Top comments (0)