radames commited on
Commit
d6e3c15
1 Parent(s): 95be2ae

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
- const ctxCrop = canvasCrop.getContext('2d') as CanvasRenderingContext2D;
 
76
 
77
- // crop image from point canvas
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
- const base64Crop = canvasCrop.toDataURL('image/png');
 
 
 
 
 
 
 
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 bind:canvasEl />
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
- function handlePointerMove(event: PointerEvent) {
85
- myPresence.update({
86
- cursor: {
87
- x: transform.invertX(event.clientX),
88
- y: transform.invertY(event.clientY)
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 flex flex-col gap-2">
138
- <DragButton isActive={dragEnabled} on:click={() => (dragEnabled = true)} />
139
- <MaskButton isActive={!dragEnabled} on:click={() => (dragEnabled = false)} />
 
 
 
 
 
 
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] border-2 border-black
148
  {isDragging ? 'cursor-grabbing' : 'cursor-grab'}
149
  {dragEnabled ? 'block' : 'hidden'}"
150
- style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); border-color: ${color}; transform-origin: 0 0;`}
151
  />
152
  </div>
153
 
154
  <style lang="postcss" scoped>
155
  .frame {
156
- @apply relative grid grid-cols-3 grid-rows-3 border-2 border-spacing-3 border-sky-500 w-[512px] h-[512px];
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>();