Spaces:
				
			
			
	
			
			
		Runtime error
		
	
	
	
			
			
	
	
	
	
		
		
		Runtime error
		
	mask and canvas as global state
Browse files
    	
        frontend/src/lib/App.svelte
    CHANGED
    
    | @@ -8,7 +8,7 @@ | |
| 8 | 
             
            	import { COLORS, EMOJIS } from '$lib/constants';
         | 
| 9 | 
             
            	import { PUBLIC_WS_INPAINTING } from '$env/static/public';
         | 
| 10 | 
             
            	import type { PromptImgObject, PromptImgKey, Presence } from '$lib/types';
         | 
| 11 | 
            -
            	import { loadingState, currZoomTransform } from '$lib/store';
         | 
| 12 |  | 
| 13 | 
             
            	import { useMyPresence, useObject, useOthers } from '$lib/liveblocks';
         | 
| 14 |  | 
| @@ -45,8 +45,6 @@ | |
| 45 | 
             
            	$: isPrompting = $myPresence?.isPrompting || false;
         | 
| 46 | 
             
            	$: isLoading = $myPresence?.isLoading || false;
         | 
| 47 |  | 
| 48 | 
            -
            	let canvasEl: HTMLCanvasElement;
         | 
| 49 | 
            -
             | 
| 50 | 
             
            	function onPaintMode(e: CustomEvent) {
         | 
| 51 | 
             
            		const mode = e.detail.mode;
         | 
| 52 | 
             
            		if (mode == 'paint' && !isPrompting) {
         | 
| @@ -67,21 +65,21 @@ | |
| 67 | 
             
            	}
         | 
| 68 |  | 
| 69 | 
             
            	function getImageCrop(cursor: { x: number; y: number }) {
         | 
| 70 | 
            -
            		const canvasCrop = document.createElement('canvas');
         | 
| 71 | 
            -
             | 
| 72 | 
            -
            		canvasCrop.width = 512;
         | 
| 73 | 
            -
            		canvasCrop.height = 512;
         | 
| 74 |  | 
| 75 | 
            -
            		 | 
|  | |
| 76 |  | 
| 77 | 
            -
            		//  | 
| 78 | 
            -
            		ctxCrop.save();
         | 
| 79 | 
            -
            		ctxCrop.clearRect(0, 0, 512, 512);
         | 
| 80 | 
            -
            		ctxCrop.globalCompositeOperation = 'source-over';
         | 
| 81 | 
            -
            		ctxCrop.drawImage(canvasEl, cursor.x, cursor.y, 512, 512, 0, 0, 512, 512);
         | 
| 82 | 
            -
            		ctxCrop.restore();
         | 
| 83 |  | 
| 84 | 
            -
            		 | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 85 |  | 
| 86 | 
             
            		return base64Crop;
         | 
| 87 | 
             
            	}
         | 
| @@ -193,7 +191,7 @@ | |
| 193 | 
             
            	<PromptModal on:prompt={onPrompt} on:close={onClose} />
         | 
| 194 | 
             
            {/if}
         | 
| 195 | 
             
            <div class="fixed top-0 left-0 z-0 w-screen h-screen">
         | 
| 196 | 
            -
            	<PaintCanvas  | 
| 197 |  | 
| 198 | 
             
            	<main class="z-10 relative">
         | 
| 199 | 
             
            		<PaintFrame
         | 
|  | |
| 8 | 
             
            	import { COLORS, EMOJIS } from '$lib/constants';
         | 
| 9 | 
             
            	import { PUBLIC_WS_INPAINTING } from '$env/static/public';
         | 
| 10 | 
             
            	import type { PromptImgObject, PromptImgKey, Presence } from '$lib/types';
         | 
| 11 | 
            +
            	import { loadingState, currZoomTransform, maskEl } from '$lib/store';
         | 
| 12 |  | 
| 13 | 
             
            	import { useMyPresence, useObject, useOthers } from '$lib/liveblocks';
         | 
| 14 |  | 
|  | |
| 45 | 
             
            	$: isPrompting = $myPresence?.isPrompting || false;
         | 
| 46 | 
             
            	$: isLoading = $myPresence?.isLoading || false;
         | 
| 47 |  | 
|  | |
|  | |
| 48 | 
             
            	function onPaintMode(e: CustomEvent) {
         | 
| 49 | 
             
            		const mode = e.detail.mode;
         | 
| 50 | 
             
            		if (mode == 'paint' && !isPrompting) {
         | 
|  | |
| 65 | 
             
            	}
         | 
| 66 |  | 
| 67 | 
             
            	function getImageCrop(cursor: { x: number; y: number }) {
         | 
| 68 | 
            +
            		// const canvasCrop = document.createElement('canvas');
         | 
|  | |
|  | |
|  | |
| 69 |  | 
| 70 | 
            +
            		// canvasCrop.width = 512;
         | 
| 71 | 
            +
            		// canvasCrop.height = 512;
         | 
| 72 |  | 
| 73 | 
            +
            		// const ctxCrop = canvasCrop.getContext('2d') as CanvasRenderingContext2D;
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
| 74 |  | 
| 75 | 
            +
            		// // crop image from point canvas
         | 
| 76 | 
            +
            		// ctxCrop.save();
         | 
| 77 | 
            +
            		// ctxCrop.clearRect(0, 0, 512, 512);
         | 
| 78 | 
            +
            		// ctxCrop.globalCompositeOperation = 'source-over';
         | 
| 79 | 
            +
            		// ctxCrop.drawImage($maskEl, cursor.x, cursor.y, 512, 512, 0, 0, 512, 512);
         | 
| 80 | 
            +
            		// ctxCrop.restore();
         | 
| 81 | 
            +
            		
         | 
| 82 | 
            +
            		const base64Crop = $maskEl.toDataURL('image/png');
         | 
| 83 |  | 
| 84 | 
             
            		return base64Crop;
         | 
| 85 | 
             
            	}
         | 
|  | |
| 191 | 
             
            	<PromptModal on:prompt={onPrompt} on:close={onClose} />
         | 
| 192 | 
             
            {/if}
         | 
| 193 | 
             
            <div class="fixed top-0 left-0 z-0 w-screen h-screen">
         | 
| 194 | 
            +
            	<PaintCanvas />
         | 
| 195 |  | 
| 196 | 
             
            	<main class="z-10 relative">
         | 
| 197 | 
             
            		<PaintFrame
         | 
    	
        frontend/src/lib/Buttons/UndoButton.svelte
    ADDED
    
    | @@ -0,0 +1,15 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            <script lang="ts">
         | 
| 2 | 
            +
            	import Undo from '$lib/Icons/Undo.svelte';
         | 
| 3 | 
            +
            	export let isActive = false;
         | 
| 4 | 
            +
            </script>
         | 
| 5 | 
            +
             | 
| 6 | 
            +
            <button
         | 
| 7 | 
            +
            	on:click
         | 
| 8 | 
            +
            	class="bg-white rounded-full p-2 {isActive ? 'text-blue-700' : 'text-gray-800'}"
         | 
| 9 | 
            +
            	title="Clear Masking"
         | 
| 10 | 
            +
            >
         | 
| 11 | 
            +
            	<Undo />
         | 
| 12 | 
            +
            </button>
         | 
| 13 | 
            +
             | 
| 14 | 
            +
            <style lang="postcss" scoped>
         | 
| 15 | 
            +
            </style>
         | 
    	
        frontend/src/lib/Icons/Undo.svelte
    ADDED
    
    | @@ -0,0 +1,22 @@ | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | 
|  | |
| 1 | 
            +
            <script lang="ts">
         | 
| 2 | 
            +
            	export let classList = '';
         | 
| 3 | 
            +
            </script>
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            <svg
         | 
| 6 | 
            +
            	class={classList}
         | 
| 7 | 
            +
            	width="20"
         | 
| 8 | 
            +
            	height="19"
         | 
| 9 | 
            +
            	viewBox="0 0 10 9"
         | 
| 10 | 
            +
            	fill="none"
         | 
| 11 | 
            +
            	xmlns="http://www.w3.org/2000/svg"
         | 
| 12 | 
            +
            >
         | 
| 13 | 
            +
            	<g opacity="0.5">
         | 
| 14 | 
            +
            		<path
         | 
| 15 | 
            +
            			d="M6.33333 2.66667H2.27167L3.46733 1.47133L3 1L1 3L3 5L3.46733 4.52833L2.27267 3.33333H6.33333C6.86377 3.33333 7.37247 3.54405 7.74755 3.91912C8.12262 4.29419 8.33333 4.8029 8.33333 5.33333C8.33333 5.86377 8.12262 6.37247 7.74755 6.74755C7.37247 7.12262 6.86377 7.33333 6.33333 7.33333H3.66667V8H6.33333C7.04058 8 7.71885 7.71905 8.21895 7.21895C8.71905 6.71885 9 6.04058 9 5.33333C9 4.62609 8.71905 3.94781 8.21895 3.44772C7.71885 2.94762 7.04058 2.66667 6.33333 2.66667Z"
         | 
| 16 | 
            +
            			fill="black"
         | 
| 17 | 
            +
            			stroke="black"
         | 
| 18 | 
            +
            			stroke-width="0.5"
         | 
| 19 | 
            +
            			stroke-linejoin="round"
         | 
| 20 | 
            +
            		/>
         | 
| 21 | 
            +
            	</g>
         | 
| 22 | 
            +
            </svg>
         | 
    	
        frontend/src/lib/PaintCanvas.svelte
    CHANGED
    
    | @@ -3,7 +3,7 @@ | |
| 3 | 
             
            	import { select } from 'd3-selection';
         | 
| 4 | 
             
            	import { onMount } from 'svelte';
         | 
| 5 | 
             
            	import { PUBLIC_UPLOADS } from '$env/static/public';
         | 
| 6 | 
            -
            	import { currZoomTransform } from '$lib/store';
         | 
| 7 | 
             
            	import { round } from '$lib/utils';
         | 
| 8 |  | 
| 9 | 
             
            	import { useMyPresence, useObject } from '$lib/liveblocks';
         | 
| @@ -15,8 +15,6 @@ | |
| 15 | 
             
            	const height = 512 * 4;
         | 
| 16 | 
             
            	const width = 512 * 4;
         | 
| 17 |  | 
| 18 | 
            -
            	export let canvasEl: HTMLCanvasElement = document.createElement('canvas');
         | 
| 19 | 
            -
             | 
| 20 | 
             
            	let containerEl: HTMLDivElement;
         | 
| 21 | 
             
            	let canvasCtx: CanvasRenderingContext2D;
         | 
| 22 |  | 
| @@ -53,14 +51,14 @@ | |
| 53 | 
             
            			.tapDistance(10)
         | 
| 54 | 
             
            			.on('zoom', zoomed);
         | 
| 55 |  | 
| 56 | 
            -
            		const selection = select(canvasEl.parentElement)
         | 
| 57 | 
             
            			.call(zoomHandler as any)
         | 
| 58 | 
             
            			.call(zoomHandler.transform as any, zoomIdentity)
         | 
| 59 | 
             
            			.call(zoomHandler.scaleTo as any, 1 / scale)
         | 
| 60 | 
             
            			.on('pointermove', handlePointerMove)
         | 
| 61 | 
             
            			.on('pointerleave', handlePointerLeave);
         | 
| 62 |  | 
| 63 | 
            -
            		canvasCtx = canvasEl.getContext('2d') as CanvasRenderingContext2D;
         | 
| 64 | 
             
            		function zoomReset() {
         | 
| 65 | 
             
            			const scale =
         | 
| 66 | 
             
            				(width + padding * 2) /
         | 
| @@ -108,7 +106,7 @@ | |
| 108 | 
             
            	}
         | 
| 109 | 
             
            	function zoomed(e: Event) {
         | 
| 110 | 
             
            		const transform = ($currZoomTransform = e.transform);
         | 
| 111 | 
            -
            		canvasEl.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`;
         | 
| 112 | 
             
            	}
         | 
| 113 |  | 
| 114 | 
             
            	// Update cursor presence to current pointer location
         | 
| @@ -137,7 +135,7 @@ | |
| 137 | 
             
            	bind:this={containerEl}
         | 
| 138 | 
             
            	class="absolute top-0 left-0 right-0 bottom-0 overflow-hidden z-0 bg-gray-800"
         | 
| 139 | 
             
            >
         | 
| 140 | 
            -
            	<canvas bind:this={canvasEl} {width} {height} class="absolute top-0 left-0 bg-white" />
         | 
| 141 | 
             
            	<slot />
         | 
| 142 | 
             
            </div>
         | 
| 143 |  | 
|  | |
| 3 | 
             
            	import { select } from 'd3-selection';
         | 
| 4 | 
             
            	import { onMount } from 'svelte';
         | 
| 5 | 
             
            	import { PUBLIC_UPLOADS } from '$env/static/public';
         | 
| 6 | 
            +
            	import { currZoomTransform, canvasEl } from '$lib/store';
         | 
| 7 | 
             
            	import { round } from '$lib/utils';
         | 
| 8 |  | 
| 9 | 
             
            	import { useMyPresence, useObject } from '$lib/liveblocks';
         | 
|  | |
| 15 | 
             
            	const height = 512 * 4;
         | 
| 16 | 
             
            	const width = 512 * 4;
         | 
| 17 |  | 
|  | |
|  | |
| 18 | 
             
            	let containerEl: HTMLDivElement;
         | 
| 19 | 
             
            	let canvasCtx: CanvasRenderingContext2D;
         | 
| 20 |  | 
|  | |
| 51 | 
             
            			.tapDistance(10)
         | 
| 52 | 
             
            			.on('zoom', zoomed);
         | 
| 53 |  | 
| 54 | 
            +
            		const selection = select($canvasEl.parentElement)
         | 
| 55 | 
             
            			.call(zoomHandler as any)
         | 
| 56 | 
             
            			.call(zoomHandler.transform as any, zoomIdentity)
         | 
| 57 | 
             
            			.call(zoomHandler.scaleTo as any, 1 / scale)
         | 
| 58 | 
             
            			.on('pointermove', handlePointerMove)
         | 
| 59 | 
             
            			.on('pointerleave', handlePointerLeave);
         | 
| 60 |  | 
| 61 | 
            +
            		canvasCtx = $canvasEl.getContext('2d') as CanvasRenderingContext2D;
         | 
| 62 | 
             
            		function zoomReset() {
         | 
| 63 | 
             
            			const scale =
         | 
| 64 | 
             
            				(width + padding * 2) /
         | 
|  | |
| 106 | 
             
            	}
         | 
| 107 | 
             
            	function zoomed(e: Event) {
         | 
| 108 | 
             
            		const transform = ($currZoomTransform = e.transform);
         | 
| 109 | 
            +
            		$canvasEl.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`;
         | 
| 110 | 
             
            	}
         | 
| 111 |  | 
| 112 | 
             
            	// Update cursor presence to current pointer location
         | 
|  | |
| 135 | 
             
            	bind:this={containerEl}
         | 
| 136 | 
             
            	class="absolute top-0 left-0 right-0 bottom-0 overflow-hidden z-0 bg-gray-800"
         | 
| 137 | 
             
            >
         | 
| 138 | 
            +
            	<canvas bind:this={$canvasEl} {width} {height} class="absolute top-0 left-0 bg-white" />
         | 
| 139 | 
             
            	<slot />
         | 
| 140 | 
             
            </div>
         | 
| 141 |  | 
    	
        frontend/src/lib/PaintFrame.svelte
    CHANGED
    
    | @@ -1,19 +1,21 @@ | |
| 1 | 
             
            <script lang="ts">
         | 
| 2 | 
            -
            	import Frame from '$lib/Frame.svelte';
         | 
| 3 | 
             
            	import PPButton from '$lib/Buttons/PPButton.svelte';
         | 
| 4 | 
             
            	import DragButton from '$lib/Buttons/DragButton.svelte';
         | 
| 5 | 
             
            	import MaskButton from '$lib/Buttons/MaskButton.svelte';
         | 
|  | |
| 6 | 
             
            	import LoadingIcon from '$lib/Icons/LoadingIcon.svelte';
         | 
| 7 |  | 
| 8 | 
             
            	import { drag } from 'd3-drag';
         | 
| 9 | 
            -
            	import { select } from 'd3-selection';
         | 
| 10 | 
             
            	import { round } from '$lib/utils';
         | 
| 11 |  | 
| 12 | 
             
            	import type { ZoomTransform } from 'd3-zoom';
         | 
| 13 | 
             
            	import { onMount, createEventDispatcher } from 'svelte';
         | 
| 14 |  | 
| 15 | 
             
            	import { useMyPresence } from '$lib/liveblocks';
         | 
| 16 | 
            -
            	import { loadingState } from '$lib/store';
         | 
|  | |
|  | |
| 17 | 
             
            	const myPresence = useMyPresence();
         | 
| 18 |  | 
| 19 | 
             
            	const dispatch = createEventDispatcher();
         | 
| @@ -22,6 +24,8 @@ | |
| 22 | 
             
            	export let color = 'black';
         | 
| 23 | 
             
            	export let interactive = false;
         | 
| 24 |  | 
|  | |
|  | |
| 25 | 
             
            	let position = {
         | 
| 26 | 
             
            		x: 768,
         | 
| 27 | 
             
            		y: 768
         | 
| @@ -40,7 +44,67 @@ | |
| 40 |  | 
| 41 | 
             
            	let offsetX = 0;
         | 
| 42 | 
             
            	let offsetY = 0;
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 43 | 
             
            	onMount(() => {
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 44 | 
             
            		function dragstarted(event: Event) {
         | 
| 45 | 
             
            			const rect = (event.sourceEvent.target as HTMLElement).getBoundingClientRect();
         | 
| 46 | 
             
            			if (event.sourceEvent instanceof TouchEvent) {
         | 
| @@ -66,6 +130,7 @@ | |
| 66 | 
             
            					y: transform.invertY(event.y)
         | 
| 67 | 
             
            				}
         | 
| 68 | 
             
            			});
         | 
|  | |
| 69 | 
             
            		}
         | 
| 70 |  | 
| 71 | 
             
            		function dragended(event: Event) {
         | 
| @@ -73,6 +138,7 @@ | |
| 73 |  | 
| 74 | 
             
            			const x = round(transform.invertX(event.x - offsetX));
         | 
| 75 | 
             
            			const y = round(transform.invertY(event.y - offsetY));
         | 
|  | |
| 76 |  | 
| 77 | 
             
            			myPresence.update({
         | 
| 78 | 
             
            				frame: {
         | 
| @@ -81,28 +147,14 @@ | |
| 81 | 
             
            				}
         | 
| 82 | 
             
            			});
         | 
| 83 | 
             
            		}
         | 
| 84 | 
            -
            		 | 
| 85 | 
            -
             | 
| 86 | 
            -
             | 
| 87 | 
            -
             | 
| 88 | 
            -
             | 
| 89 | 
            -
             | 
| 90 | 
            -
             | 
| 91 | 
            -
            		 | 
| 92 | 
            -
            		function handlePointerLeave() {
         | 
| 93 | 
            -
            			myPresence.update({
         | 
| 94 | 
            -
            				cursor: null
         | 
| 95 | 
            -
            			});
         | 
| 96 | 
            -
            		}
         | 
| 97 | 
            -
            		const dragHandler = drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
         | 
| 98 | 
            -
            		select(frameElement)
         | 
| 99 | 
            -
            			.call(dragHandler as any)
         | 
| 100 | 
            -
            			.on('pointermove', handlePointerMove)
         | 
| 101 | 
            -
            			.on('pointerleave', handlePointerLeave);
         | 
| 102 | 
            -
            	});
         | 
| 103 | 
            -
             | 
| 104 | 
            -
            	function DragMask() {
         | 
| 105 | 
            -
            		dragEnabled = !dragEnabled;
         | 
| 106 | 
             
            	}
         | 
| 107 | 
             
            </script>
         | 
| 108 |  | 
| @@ -112,6 +164,12 @@ | |
| 112 | 
             
            		style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); border-color: ${color}; transform-origin: 0 0;`}
         | 
| 113 | 
             
            	>
         | 
| 114 | 
             
            		<div class="frame">
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 115 | 
             
            			<div class="pointer-events-none touch-none">
         | 
| 116 | 
             
            				{#if $loadingState}
         | 
| 117 | 
             
            					<div class="col-span-2 row-start-1">
         | 
| @@ -128,15 +186,21 @@ | |
| 128 | 
             
            				<div class="absolute bottom-0 font-bold text-lg">{prompt}</div>
         | 
| 129 | 
             
            			</div>
         | 
| 130 | 
             
            			{#if !isDragging}
         | 
| 131 | 
            -
            				<div class="absolute top-full">
         | 
| 132 | 
             
            					<div class="py-2">
         | 
| 133 | 
             
            						<PPButton on:click={() => dispatch('paintMode', { mode: 'paint' })} />
         | 
| 134 | 
             
            					</div>
         | 
| 135 | 
             
            				</div>
         | 
| 136 | 
             
            				<div class="absolute left-full bottom-0">
         | 
| 137 | 
            -
            					<div class="px-2 | 
| 138 | 
            -
            						<DragButton isActive={dragEnabled} on:click={ | 
| 139 | 
            -
            						< | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 140 | 
             
            					</div>
         | 
| 141 | 
             
            				</div>
         | 
| 142 | 
             
            			{/if}
         | 
| @@ -144,15 +208,15 @@ | |
| 144 | 
             
            	</div>
         | 
| 145 | 
             
            	<div
         | 
| 146 | 
             
            		bind:this={frameElement}
         | 
| 147 | 
            -
            		class="absolute top-0 left-0 w-[512px] h-[512px]  | 
| 148 | 
             
            		{isDragging ? 'cursor-grabbing' : 'cursor-grab'}
         | 
| 149 | 
             
            		{dragEnabled ? 'block' : 'hidden'}"
         | 
| 150 | 
            -
            		style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k});  | 
| 151 | 
             
            	/>
         | 
| 152 | 
             
            </div>
         | 
| 153 |  | 
| 154 | 
             
            <style lang="postcss" scoped>
         | 
| 155 | 
             
            	.frame {
         | 
| 156 | 
            -
            		@apply relative grid grid-cols-3 grid-rows-3  | 
| 157 | 
             
            	}
         | 
| 158 | 
             
            </style>
         | 
|  | |
| 1 | 
             
            <script lang="ts">
         | 
|  | |
| 2 | 
             
            	import PPButton from '$lib/Buttons/PPButton.svelte';
         | 
| 3 | 
             
            	import DragButton from '$lib/Buttons/DragButton.svelte';
         | 
| 4 | 
             
            	import MaskButton from '$lib/Buttons/MaskButton.svelte';
         | 
| 5 | 
            +
            	import UndoButton from '$lib/Buttons/UndoButton.svelte';
         | 
| 6 | 
             
            	import LoadingIcon from '$lib/Icons/LoadingIcon.svelte';
         | 
| 7 |  | 
| 8 | 
             
            	import { drag } from 'd3-drag';
         | 
| 9 | 
            +
            	import { select, type Selection } from 'd3-selection';
         | 
| 10 | 
             
            	import { round } from '$lib/utils';
         | 
| 11 |  | 
| 12 | 
             
            	import type { ZoomTransform } from 'd3-zoom';
         | 
| 13 | 
             
            	import { onMount, createEventDispatcher } from 'svelte';
         | 
| 14 |  | 
| 15 | 
             
            	import { useMyPresence } from '$lib/liveblocks';
         | 
| 16 | 
            +
            	import { loadingState, canvasEl, maskEl } from '$lib/store';
         | 
| 17 | 
            +
             | 
| 18 | 
            +
            	import { toggle_class } from 'svelte/internal';
         | 
| 19 | 
             
            	const myPresence = useMyPresence();
         | 
| 20 |  | 
| 21 | 
             
            	const dispatch = createEventDispatcher();
         | 
|  | |
| 24 | 
             
            	export let color = 'black';
         | 
| 25 | 
             
            	export let interactive = false;
         | 
| 26 |  | 
| 27 | 
            +
            	let maskCtx: CanvasRenderingContext2D;
         | 
| 28 | 
            +
             | 
| 29 | 
             
            	let position = {
         | 
| 30 | 
             
            		x: 768,
         | 
| 31 | 
             
            		y: 768
         | 
|  | |
| 44 |  | 
| 45 | 
             
            	let offsetX = 0;
         | 
| 46 | 
             
            	let offsetY = 0;
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            	function cropCanvas(cursor: { x: number; y: number }) {
         | 
| 49 | 
            +
            		maskCtx.save();
         | 
| 50 | 
            +
            		maskCtx.clearRect(0, 0, 512, 512);
         | 
| 51 | 
            +
            		maskCtx.globalCompositeOperation = 'source-over';
         | 
| 52 | 
            +
            		maskCtx.drawImage($canvasEl, cursor.x, cursor.y, 512, 512, 0, 0, 512, 512);
         | 
| 53 | 
            +
            		maskCtx.restore();
         | 
| 54 | 
            +
            	}
         | 
| 55 | 
            +
            	function drawCircle(cursor: { x: number; y: number }) {
         | 
| 56 | 
            +
            		maskCtx.save();
         | 
| 57 | 
            +
            		maskCtx.globalCompositeOperation = 'destination-out';
         | 
| 58 | 
            +
            		maskCtx.beginPath();
         | 
| 59 | 
            +
            		maskCtx.arc(cursor.x, cursor.y, 20, 0, 2 * Math.PI);
         | 
| 60 | 
            +
            		maskCtx.fill();
         | 
| 61 | 
            +
            		maskCtx.restore();
         | 
| 62 | 
            +
            	}
         | 
| 63 | 
            +
             | 
| 64 | 
             
            	onMount(() => {
         | 
| 65 | 
            +
            		maskCtx = $maskEl.getContext('2d') as CanvasRenderingContext2D;
         | 
| 66 | 
            +
             | 
| 67 | 
            +
            		select(frameElement)
         | 
| 68 | 
            +
            			.call(dragMoveHandler() as any)
         | 
| 69 | 
            +
            			.call(cursorUpdate);
         | 
| 70 | 
            +
            		select($maskEl).call(maskingHandler() as any);
         | 
| 71 | 
            +
            	});
         | 
| 72 | 
            +
             | 
| 73 | 
            +
            	function cursorUpdate(selection: Selection) {
         | 
| 74 | 
            +
            		function handlePointerMove(event: PointerEvent) {
         | 
| 75 | 
            +
            			myPresence.update({
         | 
| 76 | 
            +
            				cursor: {
         | 
| 77 | 
            +
            					x: transform.invertX(event.clientX),
         | 
| 78 | 
            +
            					y: transform.invertY(event.clientY)
         | 
| 79 | 
            +
            				}
         | 
| 80 | 
            +
            			});
         | 
| 81 | 
            +
            		}
         | 
| 82 | 
            +
            		function handlePointerLeave() {
         | 
| 83 | 
            +
            			myPresence.update({
         | 
| 84 | 
            +
            				cursor: null
         | 
| 85 | 
            +
            			});
         | 
| 86 | 
            +
            		}
         | 
| 87 | 
            +
            		return selection.on('pointermove', handlePointerMove).on('pointerleave', handlePointerLeave);
         | 
| 88 | 
            +
            	}
         | 
| 89 | 
            +
            	function maskingHandler() {
         | 
| 90 | 
            +
            		function dragstarted(event: Event) {
         | 
| 91 | 
            +
            			const x = event.x / transform.k;
         | 
| 92 | 
            +
            			const y = event.y / transform.k;
         | 
| 93 | 
            +
            		}
         | 
| 94 | 
            +
             | 
| 95 | 
            +
            		function dragged(event: Event) {
         | 
| 96 | 
            +
            			const x = event.x / transform.k;
         | 
| 97 | 
            +
            			const y = event.y / transform.k;
         | 
| 98 | 
            +
            			drawCircle({ x, y });
         | 
| 99 | 
            +
            		}
         | 
| 100 | 
            +
             | 
| 101 | 
            +
            		function dragended(event: Event) {
         | 
| 102 | 
            +
            			const x = event.x / transform.k;
         | 
| 103 | 
            +
            			const y = event.y / transform.k;
         | 
| 104 | 
            +
            		}
         | 
| 105 | 
            +
            		return drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
         | 
| 106 | 
            +
            	}
         | 
| 107 | 
            +
            	function dragMoveHandler() {
         | 
| 108 | 
             
            		function dragstarted(event: Event) {
         | 
| 109 | 
             
            			const rect = (event.sourceEvent.target as HTMLElement).getBoundingClientRect();
         | 
| 110 | 
             
            			if (event.sourceEvent instanceof TouchEvent) {
         | 
|  | |
| 130 | 
             
            					y: transform.invertY(event.y)
         | 
| 131 | 
             
            				}
         | 
| 132 | 
             
            			});
         | 
| 133 | 
            +
            			cropCanvas({ x, y });
         | 
| 134 | 
             
            		}
         | 
| 135 |  | 
| 136 | 
             
            		function dragended(event: Event) {
         | 
|  | |
| 138 |  | 
| 139 | 
             
            			const x = round(transform.invertX(event.x - offsetX));
         | 
| 140 | 
             
            			const y = round(transform.invertY(event.y - offsetY));
         | 
| 141 | 
            +
            			cropCanvas({ x, y });
         | 
| 142 |  | 
| 143 | 
             
            			myPresence.update({
         | 
| 144 | 
             
            				frame: {
         | 
|  | |
| 147 | 
             
            				}
         | 
| 148 | 
             
            			});
         | 
| 149 | 
             
            		}
         | 
| 150 | 
            +
            		return drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
         | 
| 151 | 
            +
            	}
         | 
| 152 | 
            +
            	function toggleDrag() {
         | 
| 153 | 
            +
            		dragEnabled = true;
         | 
| 154 | 
            +
            	}
         | 
| 155 | 
            +
            	function toggleMask() {
         | 
| 156 | 
            +
            		dragEnabled = false;
         | 
| 157 | 
            +
            		cropCanvas(position);
         | 
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
|  | |
| 158 | 
             
            	}
         | 
| 159 | 
             
            </script>
         | 
| 160 |  | 
|  | |
| 164 | 
             
            		style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); border-color: ${color}; transform-origin: 0 0;`}
         | 
| 165 | 
             
            	>
         | 
| 166 | 
             
            		<div class="frame">
         | 
| 167 | 
            +
            			<canvas
         | 
| 168 | 
            +
            				class={dragEnabled || isLoading ? '' : 'bg-white'}
         | 
| 169 | 
            +
            				bind:this={$maskEl}
         | 
| 170 | 
            +
            				width="512"
         | 
| 171 | 
            +
            				height="512"
         | 
| 172 | 
            +
            			/>
         | 
| 173 | 
             
            			<div class="pointer-events-none touch-none">
         | 
| 174 | 
             
            				{#if $loadingState}
         | 
| 175 | 
             
            					<div class="col-span-2 row-start-1">
         | 
|  | |
| 186 | 
             
            				<div class="absolute bottom-0 font-bold text-lg">{prompt}</div>
         | 
| 187 | 
             
            			</div>
         | 
| 188 | 
             
            			{#if !isDragging}
         | 
| 189 | 
            +
            				<div class="absolute top-full ">
         | 
| 190 | 
             
            					<div class="py-2">
         | 
| 191 | 
             
            						<PPButton on:click={() => dispatch('paintMode', { mode: 'paint' })} />
         | 
| 192 | 
             
            					</div>
         | 
| 193 | 
             
            				</div>
         | 
| 194 | 
             
            				<div class="absolute left-full bottom-0">
         | 
| 195 | 
            +
            					<div class="px-2">
         | 
| 196 | 
            +
            						<DragButton isActive={dragEnabled} on:click={toggleDrag} />
         | 
| 197 | 
            +
            						<div class="flex bg-white rounded-full mt-3">
         | 
| 198 | 
            +
            							<MaskButton isActive={!dragEnabled} on:click={toggleMask} />
         | 
| 199 | 
            +
            							{#if !dragEnabled}
         | 
| 200 | 
            +
            								<span class="border-gray-800 border-opacity-50 border-r-2 my-2" />
         | 
| 201 | 
            +
            								<UndoButton on:click={() => {}} />
         | 
| 202 | 
            +
            							{/if}
         | 
| 203 | 
            +
            						</div>
         | 
| 204 | 
             
            					</div>
         | 
| 205 | 
             
            				</div>
         | 
| 206 | 
             
            			{/if}
         | 
|  | |
| 208 | 
             
            	</div>
         | 
| 209 | 
             
            	<div
         | 
| 210 | 
             
            		bind:this={frameElement}
         | 
| 211 | 
            +
            		class="absolute top-0 left-0 w-[512px] h-[512px] ring-2 ring-black
         | 
| 212 | 
             
            		{isDragging ? 'cursor-grabbing' : 'cursor-grab'}
         | 
| 213 | 
             
            		{dragEnabled ? 'block' : 'hidden'}"
         | 
| 214 | 
            +
            		style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); transform-origin: 0 0;`}
         | 
| 215 | 
             
            	/>
         | 
| 216 | 
             
            </div>
         | 
| 217 |  | 
| 218 | 
             
            <style lang="postcss" scoped>
         | 
| 219 | 
             
            	.frame {
         | 
| 220 | 
            +
            		@apply relative grid grid-cols-3 grid-rows-3 ring-2 ring-blue-500 w-[512px] h-[512px];
         | 
| 221 | 
             
            	}
         | 
| 222 | 
             
            </style>
         | 
    	
        frontend/src/lib/store.ts
    CHANGED
    
    | @@ -3,4 +3,5 @@ import { type ZoomTransform, zoomIdentity } from 'd3-zoom'; | |
| 3 |  | 
| 4 | 
             
            export const loadingState = writable<string>('');
         | 
| 5 | 
             
            export const currZoomTransform = writable<ZoomTransform>(zoomIdentity);
         | 
| 6 | 
            -
             | 
|  | 
|  | |
| 3 |  | 
| 4 | 
             
            export const loadingState = writable<string>('');
         | 
| 5 | 
             
            export const currZoomTransform = writable<ZoomTransform>(zoomIdentity);
         | 
| 6 | 
            +
            export const canvasEl = writable<HTMLCanvasElement>();
         | 
| 7 | 
            +
            export const maskEl = writable<HTMLCanvasElement>();
         | 

