|
<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; |
|
|
|
|
|
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 }) => { |
|
|
|
|
|
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})`; |
|
} |
|
|
|
|
|
function handlePointerMove(event: PointerEvent) { |
|
event.preventDefault(); |
|
const x = $currZoomTransform.invertX(event.clientX); |
|
const y = $currZoomTransform.invertY(event.clientY); |
|
|
|
myPresence.update({ |
|
cursor: { |
|
x, |
|
y |
|
} |
|
}); |
|
} |
|
|
|
|
|
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> |
|
|