<script lang="ts"> import { zoom, zoomIdentity } from 'd3-zoom'; import { select } from 'd3-selection'; import { onMount } from 'svelte'; import { PUBLIC_UPLOADS } from '$env/static/public'; import { currZoomTransform, canvasEl } from '$lib/store'; import { useMyPresence, useObject } from '$lib/liveblocks'; import type { PromptImgObject } from '$lib/types'; const myPresence = useMyPresence(); const promptImgStorage = useObject('promptImgStorage'); const height = 512 * 5; const width = 512 * 5; let containerEl: HTMLDivElement; let canvasCtx: CanvasRenderingContext2D; // keep track of images already rendered const imagesOnCanvas = new Set(); function getpromptImgList(promptImgList: PromptImgObject[]): PromptImgObject[] { if (promptImgList) { const list: PromptImgObject[] = Object.values(promptImgList).sort((a, b) => a.date - b.date); return list.filter(({ id }) => !imagesOnCanvas.has(id)); } return []; } let promptImgList: PromptImgObject[] = []; $: promptImgList = getpromptImgList($promptImgStorage?.toObject()); $: if (promptImgList) { renderImages(promptImgList); } onMount(() => { const padding = 200; const scale = (width + padding * 2) / (containerEl.clientHeight > containerEl.clientWidth ? containerEl.clientWidth : containerEl.clientHeight); const zoomHandler = zoom() .scaleExtent([1 / scale / 2, 3]) .translateExtent([ [-padding, -padding], [width + padding, height + padding] ]) .tapDistance(10) .on('zoom', zoomed); const selection = select($canvasEl.parentElement) .call(zoomHandler as any) .call(zoomHandler.transform as any, zoomIdentity) .call(zoomHandler.scaleTo as any, 1 / scale) .on('pointermove', handlePointerMove) .on('pointerleave', handlePointerLeave); canvasCtx = $canvasEl.getContext('2d') as CanvasRenderingContext2D; function zoomReset() { const scale = (width + padding * 2) / (containerEl.clientHeight > containerEl.clientWidth ? containerEl.clientWidth : containerEl.clientHeight); selection.call(zoomHandler.transform as any, zoomIdentity); selection.call(zoomHandler.scaleTo as any, 1 / scale); } window.addEventListener('resize', zoomReset); return () => { window.removeEventListener('resize', zoomReset); }; }); type ImageRendered = { img: HTMLImageElement; position: { x: number; y: number }; id: string; }; function renderImages(promptImgList: PromptImgObject[]) { Promise.all( promptImgList.map( ({ imgURL, position, id }) => new Promise<ImageRendered>((resolve) => { const img = new Image(); img.crossOrigin = 'anonymous'; img.onload = () => { const res: ImageRendered = { img, position, id }; canvasCtx.drawImage(img, position.x, position.y, img.width, img.height); resolve(res); }; img.src = `${PUBLIC_UPLOADS}/${imgURL}`; }) ) ).then((images) => { images.forEach(({ img, position, id }) => { // keep track of images already rendered //re draw in order imagesOnCanvas.add(id); canvasCtx.drawImage(img, position.x, position.y, img.width, img.height); }); }); } function zoomed(e: Event) { const transform = ($currZoomTransform = e.transform); $canvasEl.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`; } // Update cursor presence to current pointer location function handlePointerMove(event: PointerEvent) { event.preventDefault(); const x = $currZoomTransform.invertX(event.clientX); const y = $currZoomTransform.invertY(event.clientY); myPresence.update({ cursor: { x, y } }); } // When the pointer leaves the page, set cursor presence to null function handlePointerLeave() { myPresence.update({ cursor: null }); } </script> <div bind:this={containerEl} class="absolute top-0 left-0 right-0 bottom-0 overflow-hidden z-0 bg-gray-800" > <canvas bind:this={$canvasEl} {width} {height} class="absolute top-0 left-0 bg-white" /> <slot /> </div> <style lang="postcss" scoped> canvas { transform-origin: 0 0; } </style>