Image Cropper
Crop an image by moving the image, not the box.
Walkthrough
1. Basic
The window stands still and the picture moves under it — drag it, scroll to zoom, pinch on a touchscreen. It is constrained, so an empty corner inside the frame is not a thing that can happen.
Loading…
<script lang="ts"> import { ImageCropper } from 'omaris'; /** A drawn picture, so the demo needs no network. */ const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(` <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"> <defs> <linearGradient id="sky" x1="0" y1="0" x2="0" y2="1"> <stop offset="0%" stop-color="#1b3a5c"/> <stop offset="60%" stop-color="#e9825c"/> <stop offset="100%" stop-color="#f3c98b"/> </linearGradient> </defs> <rect width="1200" height="800" fill="url(#sky)"/> <circle cx="820" cy="300" r="90" fill="#fff3d6"/> <path d="M0 620 L260 460 L470 620 Z" fill="#12283d"/> <path d="M330 620 L620 400 L900 620 Z" fill="#0d1f30"/> <rect y="620" width="1200" height="180" fill="#08151f"/> </svg>`)}`;</script><ImageCropper src={PHOTO} alt="Sunset over hills" aspect={16 / 9} class="w-full max-w-lg" /> 2. Ratios
ratios puts a row of aspect presets under the stage and rebinds aspect — pick one and the window reshapes, the picture re-covers it, nothing else moves. resizable adds grips to the window, so a locked ratio can still be scaled and a free one shaped by hand.
Loading…
<script lang="ts"> import { ImageCropper, Text, type ImageCropperAspect } from 'omaris'; const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(` <svg xmlns="http://www.w3.org/2000/svg" width="1200" height="800"> <rect width="1200" height="800" fill="#12233b"/> <circle cx="880" cy="240" r="110" fill="#fbbf24"/> <path d="M0 640 L300 420 L560 640 Z" fill="#1f4d3f"/> <path d="M380 640 L700 380 L1000 640 Z" fill="#14332b"/> <rect y="640" width="1200" height="160" fill="#0a1a26"/> </svg>`)}`; let aspect = $state<ImageCropperAspect>(16 / 9);</script><div class="flex w-full max-w-lg flex-col gap-2"> <ImageCropper bind:aspect src={PHOTO} alt="Sunset over hills" ratios resizable height={260} controls={['zoom', 'reset']} /> <Text variant="label-sm" tone="muted" font="mono"> aspect: {aspect === 'free' ? 'free' : aspect.toFixed(2)} </Text></div> 3. Exporting
The export replays exactly the transform you see, at whatever output size you name — so what lands in the blob is what was in the frame. onchange reports the rectangle in source pixels every time the crop settles.
Loading…
<script lang="ts"> import { Button, ImageCropper, Text, type CropRect } from 'omaris'; const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(` <svg xmlns="http://www.w3.org/2000/svg" width="900" height="900"> <rect width="900" height="900" fill="#0f172a"/> <circle cx="450" cy="380" r="180" fill="#38bdf8"/> <rect x="180" y="600" width="540" height="180" rx="40" fill="#f472b6"/> </svg>`)}`; let cropper = $state<ImageCropper | null>(null); let output = $state<string | null>(null); let rect = $state<CropRect | null>(null);</script><div class="flex w-full flex-col items-center gap-3"> <ImageCropper bind:this={cropper} src={PHOTO} alt="Avatar source" aspect={1} shape="circle" onchange={(next) => (rect = next)} class="w-full max-w-sm" /> <div class="flex items-center gap-3"> <Button size="sm" onclick={() => (output = cropper?.toDataURL('image/png', undefined, 160) ?? null)} > Export at 160px </Button> {#if output} <img src={output} alt="The exported crop" class="size-16 rounded-full" /> {/if} </div> {#if rect} <Text variant="label-sm" tone="muted" font="mono"> {Math.round(rect.width)} × {Math.round(rect.height)} at {Math.round(rect.x)}, {Math.round( rect.y )} </Text> {/if}</div> 4. Adjusting
controls takes the list you want, so straighten adds a fine-angle slider next to the quarter-turn button. Any angle constrains as tightly as a right one: tilt the picture and it grows to keep the window full.
Loading…
<script lang="ts"> import { ImageCropper, Text } from 'omaris'; const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(` <svg xmlns="http://www.w3.org/2000/svg" width="1000" height="700"> <rect width="1000" height="700" fill="#0f172a"/> <rect x="80" y="120" width="380" height="460" rx="24" fill="#38bdf8"/> <rect x="540" y="240" width="380" height="340" rx="24" fill="#f472b6"/> <path d="M0 620 L1000 560" stroke="#e2e8f0" stroke-width="8"/> </svg>`)}`; let rotation = $state(0); let flipX = $state(false);</script><div class="flex w-full max-w-md flex-col gap-2"> <ImageCropper bind:rotation bind:flipX src={PHOTO} alt="Two blocks on a horizon" aspect={4 / 3} height={240} controls={['zoom', 'straighten', 'rotate', 'flip', 'reset']} /> <Text variant="label-sm" tone="muted" font="mono"> {Math.round(rotation)}°{flipX ? ' · mirrored' : ''} </Text></div> 5. Overridden
height is a custom property, so classes.stage can replace it outright. controls={false} hands the zoom to a slider of your own through the bindable zoom, and children puts content over the stage.
Loading…
Cover photo<script lang="ts"> import { ImageCropper, Slider, Text } from 'omaris'; const PHOTO = `data:image/svg+xml;utf8,${encodeURIComponent(` <svg xmlns="http://www.w3.org/2000/svg" width="1000" height="600"> <rect width="1000" height="600" fill="#0b3d2e"/> <circle cx="300" cy="220" r="120" fill="#facc15"/> <path d="M0 520 L340 300 L700 520 Z" fill="#065f46"/> <path d="M420 520 L760 340 L1000 520 Z" fill="#064e3b"/> </svg>`)}`; let zoom = $state(1.4);</script><div class="flex w-full max-w-md flex-col gap-3"> <ImageCropper src={PHOTO} alt="Hills" aspect={3 / 1} bind:zoom grid={false} controls={false} inset={0.94} height="auto" classes={{ stage: 'h-40 rounded-shape-lg', window: 'rounded-shape-md' }} > <Text variant="label-sm" class="absolute start-3 top-3 text-white/80">Cover photo</Text> </ImageCropper> <Slider bind:value={zoom} min={1} max={4} step={0.05} label="Zoom" /></div>