Features
- Scroll or pinch to zoom in/out
- Programmatic zoom controls (zoom in/out buttons)
- Image rotation at 90-degree increments
- Image cropping based on current zoom state
- Real-time zoom percentage display
- State management and subscriptions
Vanilla JavaScript
- HTML
- JavaScript
<div id="image-wheel-container" class="h-[300px] w-[200px]">
<img src="/sample.avif" alt="Zoomable image" />
</div>
<p id="currentZoom">Current zoom: 100%</p>
<div>
<button id="zoomInBtn">Zoom in</button>
<button id="zoomOutBtn">Zoom out</button>
<button id="cropImgBtn">Crop image</button>
<button id="rotateImgBtn">Rotate</button>
</div>
<img id="cropImg" hidden alt="Cropped" />
import { createZoomImageWheel, cropImage } from "@zoom-image/core";
const container = document.getElementById("image-wheel-container");
const result = createZoomImageWheel(container);
const controller = new AbortController();
const cropImg = document.getElementById("cropImg");
const currentZoom = document.getElementById("currentZoom");
const zoomInBtn = document.getElementById("zoomInBtn");
const zoomOutBtn = document.getElementById("zoomOutBtn");
const cropImgBtn = document.getElementById("cropImgBtn");
const rotateImgBtn = document.getElementById("rotateImgBtn");
// Subscribe to zoom state changes
result.subscribe(({ state }) => {
currentZoom.textContent = `Current zoom: ${Math.round(state.currentZoom * 100)}%`;
});
// Zoom in button
zoomInBtn.addEventListener(
"click",
() => {
result.setState({
currentZoom: result.getState().currentZoom + 0.5,
});
},
{ signal: controller.signal }
);
// Zoom out button
zoomOutBtn.addEventListener(
"click",
() => {
result.setState({
currentZoom: result.getState().currentZoom - 0.5,
});
},
{ signal: controller.signal }
);
// Crop image
async function handleCropImage() {
const currentState = result.getState();
const croppedImage = await cropImage({
image: container.querySelector("img"),
currentZoom: currentState.currentZoom,
positionX: currentState.currentPositionX,
positionY: currentState.currentPositionY,
rotation: currentState.currentRotation,
});
cropImg.src = croppedImage;
cropImg.hidden = false;
}
cropImgBtn.addEventListener("click", handleCropImage, {
signal: controller.signal,
});
// Rotate image
rotateImgBtn.addEventListener(
"click",
() => {
result.setState({
currentRotation: result.getState().currentRotation + 90,
});
},
{ signal: controller.signal }
);
// Cleanup
function cleanup() {
result.cleanup();
controller.abort();
}
React
import { cropImage } from "@zoom-image/core";
import { useZoomImageWheel } from "@zoom-image/react";
import { useEffect, useMemo, useRef, useState } from "react";
function WheelZoomExample() {
const imageContainerRef = useRef<HTMLDivElement>(null);
const [croppedImage, setCroppedImage] = useState<string | null>(null);
const {
createZoomImage,
zoomImageState,
setZoomImageState,
} = useZoomImageWheel();
// Initialize zoom on mount
useEffect(() => {
if (imageContainerRef.current) {
createZoomImage(imageContainerRef.current);
}
}, [createZoomImage]);
const croppedImageClasses = useMemo(() => {
if (zoomImageState.currentRotation % 180 === 90) {
return "h-[200px] w-[300px]";
}
return "h-[300px] w-[200px]";
}, [zoomImageState.currentRotation]);
async function handleCropImage() {
const cropped = await cropImage({
currentZoom: zoomImageState.currentZoom,
image: imageContainerRef.current?.querySelector("img") as HTMLImageElement,
positionX: zoomImageState.currentPositionX,
positionY: zoomImageState.currentPositionY,
rotation: zoomImageState.currentRotation,
});
setCroppedImage(cropped);
}
function zoomIn() {
setZoomImageState({
currentZoom: zoomImageState.currentZoom + 0.5,
});
}
function zoomOut() {
setZoomImageState({
currentZoom: zoomImageState.currentZoom - 0.5,
});
}
function rotate() {
setZoomImageState({
currentRotation: zoomImageState.currentRotation + 90,
});
}
return (
<div>
<p>Current zoom: {Math.round(zoomImageState.currentZoom * 100)}%</p>
<p>Scroll inside the image to zoom in/out</p>
<div className="flex items-center gap-4">
<div className="h-[300px] w-[300px] bg-black grid place-content-center">
<div
ref={imageContainerRef}
className="h-[300px] w-[200px] cursor-crosshair"
>
<img
className="h-full w-full"
alt="Zoomable"
src="/sample.avif"
/>
</div>
</div>
{croppedImage && (
<img
src={croppedImage}
className={croppedImageClasses}
alt="Cropped"
/>
)}
</div>
<div className="flex space-x-2 mt-4">
<button onClick={zoomIn}>Zoom in</button>
<button onClick={zoomOut}>Zoom out</button>
<button onClick={handleCropImage}>Crop image</button>
<button onClick={rotate}>Rotate</button>
</div>
</div>
);
}
export default WheelZoomExample;
Vue
<script setup lang="ts">
import { cropImage } from "@zoom-image/core";
import { useZoomImageWheel } from "@zoom-image/vue";
import { computed, onMounted, ref } from "vue";
const imageContainerRef = ref<HTMLDivElement>();
const croppedImage = ref<string>();
const {
createZoomImage,
zoomImageState,
setZoomImageState,
} = useZoomImageWheel();
const croppedImageClasses = computed(() => {
if (zoomImageState.currentRotation % 180 === 90) {
return "h-[200px] w-[300px]";
}
return "h-[300px] w-[200px]";
});
const handleCropImage = async () => {
croppedImage.value = await cropImage({
currentZoom: zoomImageState.currentZoom,
image: (imageContainerRef.value as HTMLDivElement).querySelector("img") as HTMLImageElement,
positionX: zoomImageState.currentPositionX,
positionY: zoomImageState.currentPositionY,
rotation: zoomImageState.currentRotation,
});
};
const zoomIn = () => {
setZoomImageState({
currentZoom: zoomImageState.currentZoom + 0.5,
});
};
const zoomOut = () => {
setZoomImageState({
currentZoom: zoomImageState.currentZoom - 0.5,
});
};
const rotate = () => {
setZoomImageState({
currentRotation: zoomImageState.currentRotation + 90,
});
if (croppedImage.value) {
handleCropImage();
}
};
onMounted(() => {
createZoomImage(imageContainerRef.value as HTMLDivElement);
});
</script>
<template>
<div>
<p>Scroll / Pinch inside the image to see zoom in-out effect</p>
<p>Current zoom: {{ `${Math.round(zoomImageState.currentZoom * 100)}%` }}</p>
<div class="flex items-center gap-4">
<div class="mt-1 grid h-[300px] w-[300px] place-content-center bg-black">
<div ref="imageContainerRef" class="h-[300px] w-[200px] cursor-crosshair">
<img class="h-full w-full" alt="Large Pic" src="/sample.avif" />
</div>
</div>
<img
v-if="croppedImage"
:src="croppedImage"
:class="croppedImageClasses"
alt="Cropped"
/>
</div>
<div class="flex space-x-2 mt-4">
<button @click="zoomIn">Zoom in</button>
<button @click="zoomOut">Zoom out</button>
<button @click="handleCropImage">Crop image</button>
<button @click="rotate">Rotate</button>
</div>
</div>
</template>
Svelte
<script lang="ts">
import { cropImage, type ZoomImageWheelState } from "@zoom-image/core";
import { useZoomImageWheel } from "@zoom-image/svelte";
import { onMount } from "svelte";
let imageContainer: HTMLDivElement;
let croppedImage: string = "";
let {
createZoomImage,
zoomImageState,
setZoomImageState,
} = useZoomImageWheel();
let zoomImageStateValue: ZoomImageWheelState;
zoomImageState.subscribe((value) => {
zoomImageStateValue = value;
});
$: croppedImageClasses =
zoomImageStateValue.currentRotation % 180 === 90
? "h-[200px] w-[300px]"
: "h-[300px] w-[200px]";
async function handleCropImage() {
croppedImage = await cropImage({
currentZoom: zoomImageStateValue.currentZoom,
image: imageContainer.querySelector("img") as HTMLImageElement,
positionX: zoomImageStateValue.currentPositionX,
positionY: zoomImageStateValue.currentPositionY,
rotation: zoomImageStateValue.currentRotation,
});
}
function rotate() {
setZoomImageState({
currentRotation: zoomImageStateValue.currentRotation + 90,
});
if (croppedImage) {
handleCropImage();
}
}
onMount(() => createZoomImage(imageContainer));
</script>
<div>
<p>Current zoom: {`${Math.round(zoomImageStateValue.currentZoom * 100)}%`}</p>
<p>Scroll inside the image to see zoom in-out effect</p>
<div class="flex items-center gap-4">
<div class="mt-1 grid h-[300px] w-[300px] place-content-center bg-black">
<div bind:this={imageContainer} class="h-[300px] w-[200px] cursor-crosshair">
<img class="h-full w-full" alt="Large Pic" src="/sample.avif" />
</div>
</div>
{#if croppedImage !== ""}
<img src={croppedImage} class={croppedImageClasses} alt="Cropped" />
{/if}
</div>
<div class="flex space-x-2 mt-4">
<button
on:click={() => {
setZoomImageState({
currentZoom: zoomImageStateValue.currentZoom + 0.5,
});
}}
>
Zoom in
</button>
<button
on:click={() => {
setZoomImageState({
currentZoom: zoomImageStateValue.currentZoom - 0.5,
});
}}
>
Zoom out
</button>
<button on:click={handleCropImage}>Crop image</button>
<button on:click={rotate}>Rotate</button>
</div>
</div>
Angular
import { AfterViewInit, Component, ElementRef, ViewChild } from "@angular/core";
import { CommonModule } from "@angular/common";
import { ZoomImageWheelService } from "@zoom-image/angular";
import { ZoomImageWheelState, cropImage } from "@zoom-image/core";
@Component({
selector: "app-wheel-zoom",
templateUrl: "./wheel-zoom.component.html",
providers: [ZoomImageWheelService],
imports: [CommonModule],
})
export class WheelZoomComponent implements AfterViewInit {
@ViewChild("imageContainer") imageContainerRef?: ElementRef<HTMLDivElement>;
croppedImage: string = "";
zoomImageState: ZoomImageWheelState = this.zoomService.zoomImageState;
constructor(private zoomService: ZoomImageWheelService) {}
ngAfterViewInit(): void {
if (this.imageContainerRef) {
this.zoomService.createZoomImage(
this.imageContainerRef.nativeElement
);
this.zoomService.zoomImageState$.subscribe((state) => {
this.zoomImageState = state;
});
}
}
getCurrentZoom() {
return `${Math.round(this.zoomImageState.currentZoom * 100)}%`;
}
zoomIn() {
this.zoomService.setZoomImageState({
currentZoom: this.zoomImageState.currentZoom + 0.5,
});
}
zoomOut() {
this.zoomService.setZoomImageState({
currentZoom: this.zoomImageState.currentZoom - 0.5,
});
}
rotate() {
const currentRotation = this.zoomImageState.currentRotation + 90;
this.zoomService.setZoomImageState({ currentRotation });
if (this.croppedImage) {
this.handleCropImage(currentRotation);
}
}
async handleCropImage(currentRotation = this.zoomImageState.currentRotation) {
this.croppedImage = await cropImage({
currentZoom: this.zoomImageState.currentZoom,
image: (this.imageContainerRef?.nativeElement as HTMLDivElement)
.querySelector("img") as HTMLImageElement,
positionX: this.zoomImageState.currentPositionX,
positionY: this.zoomImageState.currentPositionY,
rotation: currentRotation,
});
}
getCroppedImageClasses() {
if (this.zoomImageState.currentRotation % 180 === 90) {
return "h-[200px] w-[300px]";
}
return "h-[300px] w-[200px]";
}
}
Key Configuration Options
maxZoom
Sets the maximum zoom level (default: 4).const result = createZoomImageWheel(container, {
maxZoom: 5,
});
wheelZoomRatio
Controls zoom increment per scroll event (default: 0.1).const result = createZoomImageWheel(container, {
wheelZoomRatio: 0.2, // Faster zooming
});
zoomImageSource
Specifies a high-resolution image for zoomed view.const result = createZoomImageWheel(container, {
zoomImageSource: "/high-res-image.jpg",
});