Crop to a shape
Free, square or widescreen — and the crop you drew is the crop that comes out, checked against the original's own pixels.
<div class="demo demo--wide">
<input id="photo" name="photo" type="file">
<div class="demo-row">
<button class="demo-btn" id="take">Take the file</button>
<button class="demo-btn demo-btn--ghost" id="sample">Use a test card</button>
</div>
<div class="result" id="result" hidden>
<img id="out" alt="The cropped picture">
<dl class="demo-kv" id="facts"></dl>
</div>
<p class="demo-hint">The test card has four known quarters, so you can see that what comes out is
exactly what you selected — at the original's resolution, not the screen's.</p>
</div>
import { createImageCropper } from 'https://www.devix.pk/cdn/oss/image-cropper@1.0.0/index.js';
const result = document.getElementById('result');
const out = document.getElementById('out');
const facts = document.getElementById('facts');
const picker = createImageCropper(document.getElementById('photo'), {
aspect: null,
aspects: [
{ label: 'Free', value: null },
{ label: '1:1', value: 1 },
{ label: '4:3', value: 4 / 3 },
{ label: '16:9', value: 16 / 9 },
{ label: '2:3', value: 2 / 3 },
],
preview: true,
maxWidth: 1200,
onChange: (crop) => {
if (!crop) return;
facts.innerHTML = `<div><dt>crop</dt><dd>${crop.width} × ${crop.height}</dd></div>
<div><dt>at</dt><dd>${crop.x}, ${crop.y}</dd></div>`;
result.hidden = false;
out.removeAttribute('src');
},
});
document.getElementById('take').addEventListener('click', async () => {
const crop = picker.getCrop();
if (!crop) return;
const file = await picker.toFile('cropped', { type: 'image/png' });
out.src = URL.createObjectURL(file);
facts.innerHTML = `<div><dt>file</dt><dd>${file.name}</dd></div>
<div><dt>size</dt><dd>${(file.size / 1024).toFixed(1)} kB</dd></div>
<div><dt>crop</dt><dd>${crop.width} × ${crop.height}</dd></div>
<div><dt>from</dt><dd>${crop.x}, ${crop.y}</dd></div>`;
result.hidden = false;
});
document.getElementById('sample').addEventListener('click', async () => {
const canvas = Object.assign(document.createElement('canvas'), { width: 960, height: 540 });
const ctx = canvas.getContext('2d');
const quarters = [['#ef4444', 0, 0], ['#22c55e', 480, 0], ['#3b82f6', 480, 270], ['#eab308', 0, 270]];
for (const [colour, x, y] of quarters) {
ctx.fillStyle = colour;
ctx.fillRect(x, y, 480, 270);
}
ctx.strokeStyle = 'rgba(255,255,255,.7)';
ctx.lineWidth = 2;
for (let x = 0; x <= 960; x += 120) { ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, 540); ctx.stroke(); }
for (let y = 0; y <= 540; y += 120) { ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(960, y); ctx.stroke(); }
ctx.fillStyle = '#fff';
ctx.font = 'bold 40px system-ui';
ctx.textAlign = 'center';
ctx.fillText('960 × 540', 480, 290);
const blob = await new Promise((r) => canvas.toBlob(r, 'image/png'));
await picker.open(new File([blob], 'test-card.png', { type: 'image/png' }));
});
@import url('https://www.devix.pk/cdn/oss/image-cropper@1.0.0/image-cropper.min.css');
.result { display: flex; align-items: flex-start; gap: 16px; }
.result[hidden] { display: none; }
#out { max-width: 140px; max-height: 110px; border-radius: 8px; }
#facts { flex: 1; }