163 lines
4.1 KiB
Svelte
163 lines
4.1 KiB
Svelte
<script lang="ts">
|
|
import ZoomControls from "../components/ZoomControls.svelte";
|
|
import { RawImageData } from "../models/RawImageData";
|
|
|
|
type ZoomableProps = {
|
|
minZoom?: number;
|
|
maxZoom?: number;
|
|
zoomStep?: number;
|
|
img: RawImageData;
|
|
height?: string | number;
|
|
};
|
|
|
|
let {
|
|
minZoom = 0.5,
|
|
maxZoom = 10,
|
|
zoomStep = 0.1,
|
|
img,
|
|
height,
|
|
}: ZoomableProps = $props();
|
|
|
|
let scale = $state(1);
|
|
|
|
// DOM refs
|
|
let container = $state<HTMLElement>();
|
|
let image = $state<HTMLElement>();
|
|
|
|
// Drag state
|
|
type DragState = {
|
|
startX: number;
|
|
startY: number;
|
|
scrollLeft: number;
|
|
scrollTop: number;
|
|
};
|
|
|
|
let dragState: DragState | undefined = $state({
|
|
startX: 0,
|
|
startY: 0,
|
|
scrollLeft: 0,
|
|
scrollTop: 0,
|
|
});
|
|
|
|
// Zoom handlers
|
|
function handleZoom(event: WheelEvent) {
|
|
if (!container || !image || !event.ctrlKey) return;
|
|
|
|
event.preventDefault();
|
|
const delta = -Math.sign(event.deltaY);
|
|
const newScale = Math.min(
|
|
Math.max(scale + delta * zoomStep, minZoom),
|
|
maxZoom,
|
|
);
|
|
|
|
if (newScale !== scale) {
|
|
const rect = image.getBoundingClientRect();
|
|
const x = event.clientX;
|
|
const y = event.clientY;
|
|
|
|
const scaleChange = newScale / scale;
|
|
container.scrollLeft = x * scaleChange - x + container.scrollLeft;
|
|
container.scrollTop = y * scaleChange - y + container.scrollTop;
|
|
|
|
scale = newScale;
|
|
}
|
|
}
|
|
|
|
// Drag handlers
|
|
function handleDragStart(event: MouseEvent) {
|
|
if (!container) return;
|
|
|
|
event.preventDefault();
|
|
dragState = {
|
|
startX: event.pageX - container.offsetLeft,
|
|
startY: event.pageY - container.offsetTop,
|
|
scrollLeft: container.scrollLeft,
|
|
scrollTop: container.scrollTop,
|
|
};
|
|
}
|
|
|
|
function handleDragMove(event: MouseEvent) {
|
|
if (!container || !dragState) return;
|
|
|
|
event.preventDefault();
|
|
const x = event.pageX - container.offsetLeft;
|
|
const y = event.pageY - container.offsetTop;
|
|
|
|
container.scrollLeft = dragState.scrollLeft - (x - dragState.startX);
|
|
container.scrollTop = dragState.scrollTop - (y - dragState.startY);
|
|
}
|
|
|
|
function handleDragEnd(event: MouseEvent) {
|
|
event.preventDefault();
|
|
dragState = undefined;
|
|
}
|
|
|
|
function resetZoom() {
|
|
scale = 1;
|
|
if (container) {
|
|
container.scrollLeft = 0;
|
|
container.scrollTop = 0;
|
|
}
|
|
}
|
|
|
|
// Event listener management
|
|
$effect(() => {
|
|
if (container) {
|
|
container.addEventListener("wheel", handleZoom, { passive: false });
|
|
return () => container.removeEventListener("wheel", handleZoom);
|
|
}
|
|
});
|
|
|
|
</script>
|
|
|
|
<div class="relative w-full h-full">
|
|
<ZoomControls
|
|
{scale}
|
|
onZoomIn={() => (scale = Math.min(scale + zoomStep, maxZoom))}
|
|
onZoomOut={() => (scale = Math.max(scale - zoomStep, minZoom))}
|
|
{resetZoom}
|
|
/>
|
|
<div
|
|
class="container"
|
|
bind:this={container}
|
|
onmousedown={handleDragStart}
|
|
onmousemove={handleDragMove}
|
|
onmouseup={handleDragEnd}
|
|
onmouseleave={handleDragEnd}
|
|
role="presentation"
|
|
>
|
|
<div
|
|
class="image-container"
|
|
bind:this={image}
|
|
style:transform="scale({scale})"
|
|
style:height={height + "px"}
|
|
>
|
|
<img
|
|
alt="rendered-page"
|
|
src={img.toObjectUrl()}
|
|
style:max-height={height + "px"}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<style lang="postcss">
|
|
.container {
|
|
@apply w-full h-full bg-forge-dark;
|
|
overflow: auto;
|
|
cursor: grab;
|
|
}
|
|
|
|
.container:active {
|
|
cursor: grabbing;
|
|
}
|
|
|
|
.image-container {
|
|
transform-origin: 0 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
align-items: center;
|
|
}
|
|
</style>
|