Spaces:
Runtime error
Runtime error
design improvments
Browse files- frontend/src/app.html +14 -11
- frontend/src/lib/App.svelte +23 -49
- frontend/src/lib/Frame.svelte +1 -1
- frontend/src/lib/Icons/LoadingIcon.svelte +11 -6
- frontend/src/lib/Menu.svelte +2 -16
- frontend/src/lib/PaintFrame.svelte +51 -29
- frontend/src/lib/PromptModal.svelte +20 -14
- frontend/src/lib/types.ts +11 -2
- frontend/src/routes/+page.svelte +2 -0
frontend/src/app.html
CHANGED
@@ -1,13 +1,16 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
<html lang="en">
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
1 |
<!DOCTYPE html>
|
2 |
<html lang="en">
|
3 |
+
|
4 |
+
<head>
|
5 |
+
<meta charset="utf-8" />
|
6 |
+
<link rel="icon" href="%sveltekit.assets%/favicon.png" />
|
7 |
+
<meta name="viewport" content="width=device-width" />
|
8 |
+
%sveltekit.head%
|
9 |
+
</head>
|
10 |
+
<!-- <body class="dark:bg-[rgb(11,15,25)] bg-white dark:text-white text-black"> -->
|
11 |
+
|
12 |
+
<body>
|
13 |
+
<div>%sveltekit.body%</div>
|
14 |
+
</body>
|
15 |
+
|
16 |
+
</html>
|
frontend/src/lib/App.svelte
CHANGED
@@ -8,12 +8,10 @@
|
|
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 |
-
|
15 |
import { base64ToBlob, uploadImage } from '$lib/utils';
|
16 |
-
|
17 |
import { nanoid } from 'nanoid';
|
18 |
|
19 |
/**
|
@@ -28,8 +26,7 @@
|
|
28 |
const initialPresence: Presence = {
|
29 |
cursor: null,
|
30 |
frame: null,
|
31 |
-
|
32 |
-
isLoading: false,
|
33 |
currentPrompt: ''
|
34 |
};
|
35 |
myPresence.update(initialPresence);
|
@@ -42,15 +39,17 @@
|
|
42 |
|
43 |
let showModal = false;
|
44 |
|
45 |
-
$: isPrompting = $myPresence?.
|
46 |
-
$: isLoading = $myPresence?.
|
47 |
|
48 |
-
|
49 |
-
|
50 |
-
|
|
|
|
|
51 |
showModal = true;
|
52 |
myPresence.update({
|
53 |
-
|
54 |
});
|
55 |
}
|
56 |
}
|
@@ -58,31 +57,12 @@
|
|
58 |
showModal = false;
|
59 |
}
|
60 |
|
61 |
-
function
|
62 |
-
console.log('
|
63 |
generateImage();
|
64 |
showModal = false;
|
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 |
-
}
|
86 |
async function generateImage() {
|
87 |
if (isLoading) return;
|
88 |
$loadingState = 'Pending';
|
@@ -90,13 +70,14 @@
|
|
90 |
const position = $myPresence.frame;
|
91 |
console.log('Generating...', prompt, position);
|
92 |
myPresence.update({
|
93 |
-
|
94 |
-
isLoading: true
|
95 |
});
|
96 |
const sessionHash = crypto.randomUUID();
|
|
|
|
|
97 |
const payload = {
|
98 |
fn_index: 0,
|
99 |
-
data: [
|
100 |
session_hash: sessionHash
|
101 |
};
|
102 |
console.log('payload', payload);
|
@@ -109,8 +90,7 @@
|
|
109 |
if (!evt.wasClean) {
|
110 |
$loadingState = 'Error';
|
111 |
myPresence.update({
|
112 |
-
|
113 |
-
isLoading: false
|
114 |
});
|
115 |
}
|
116 |
};
|
@@ -127,8 +107,7 @@
|
|
127 |
$loadingState = 'Queue full';
|
128 |
websocket.close();
|
129 |
myPresence.update({
|
130 |
-
|
131 |
-
isLoading: false
|
132 |
});
|
133 |
return;
|
134 |
case 'estimation':
|
@@ -167,8 +146,7 @@
|
|
167 |
}
|
168 |
websocket.close();
|
169 |
myPresence.update({
|
170 |
-
|
171 |
-
isLoading: false
|
172 |
});
|
173 |
return;
|
174 |
case 'process_starts':
|
@@ -188,22 +166,18 @@
|
|
188 |
{$loadingState}
|
189 |
</div>
|
190 |
{#if showModal}
|
191 |
-
<PromptModal on:
|
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
|
198 |
-
on:paintMode={onPaintMode}
|
199 |
-
transform={$currZoomTransform}
|
200 |
-
interactive={!isPrompting}
|
201 |
-
/>
|
202 |
|
203 |
<!-- When others connected, iterate through others and show their cursors -->
|
204 |
{#if $others}
|
205 |
{#each [...$others] as { connectionId, presence } (connectionId)}
|
206 |
-
{#if presence?.
|
207 |
<Frame
|
208 |
color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
|
209 |
position={presence?.frame}
|
@@ -225,7 +199,7 @@
|
|
225 |
</div>
|
226 |
|
227 |
<div class="fixed bottom-0 left-0 right-0 z-10 my-2">
|
228 |
-
<Menu on:
|
229 |
</div>
|
230 |
|
231 |
<style lang="postcss" scoped>
|
|
|
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 { Status } from '$lib/types';
|
12 |
import { loadingState, currZoomTransform, maskEl } from '$lib/store';
|
|
|
13 |
import { useMyPresence, useObject, useOthers } from '$lib/liveblocks';
|
|
|
14 |
import { base64ToBlob, uploadImage } from '$lib/utils';
|
|
|
15 |
import { nanoid } from 'nanoid';
|
16 |
|
17 |
/**
|
|
|
26 |
const initialPresence: Presence = {
|
27 |
cursor: null,
|
28 |
frame: null,
|
29 |
+
status: Status.dragging,
|
|
|
30 |
currentPrompt: ''
|
31 |
};
|
32 |
myPresence.update(initialPresence);
|
|
|
39 |
|
40 |
let showModal = false;
|
41 |
|
42 |
+
$: isPrompting = $myPresence?.status === Status.prompting || false;
|
43 |
+
$: isLoading = $myPresence?.status === Status.loading || false;
|
44 |
|
45 |
+
$: {
|
46 |
+
console.log($myPresence.status);
|
47 |
+
}
|
48 |
+
function onPrompt() {
|
49 |
+
if (!isLoading) {
|
50 |
showModal = true;
|
51 |
myPresence.update({
|
52 |
+
status: Status.prompting
|
53 |
});
|
54 |
}
|
55 |
}
|
|
|
57 |
showModal = false;
|
58 |
}
|
59 |
|
60 |
+
function onPaint() {
|
61 |
+
console.log('onPaint');
|
62 |
generateImage();
|
63 |
showModal = false;
|
64 |
}
|
65 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
66 |
async function generateImage() {
|
67 |
if (isLoading) return;
|
68 |
$loadingState = 'Pending';
|
|
|
70 |
const position = $myPresence.frame;
|
71 |
console.log('Generating...', prompt, position);
|
72 |
myPresence.update({
|
73 |
+
status: Status.loading
|
|
|
74 |
});
|
75 |
const sessionHash = crypto.randomUUID();
|
76 |
+
const base64Crop = $maskEl.toDataURL('image/png');
|
77 |
+
|
78 |
const payload = {
|
79 |
fn_index: 0,
|
80 |
+
data: [base64Crop, prompt, 0.75, 7.5, 40, 'patchmatch'],
|
81 |
session_hash: sessionHash
|
82 |
};
|
83 |
console.log('payload', payload);
|
|
|
90 |
if (!evt.wasClean) {
|
91 |
$loadingState = 'Error';
|
92 |
myPresence.update({
|
93 |
+
status: Status.ready
|
|
|
94 |
});
|
95 |
}
|
96 |
};
|
|
|
107 |
$loadingState = 'Queue full';
|
108 |
websocket.close();
|
109 |
myPresence.update({
|
110 |
+
status: Status.ready
|
|
|
111 |
});
|
112 |
return;
|
113 |
case 'estimation':
|
|
|
146 |
}
|
147 |
websocket.close();
|
148 |
myPresence.update({
|
149 |
+
status: Status.ready
|
|
|
150 |
});
|
151 |
return;
|
152 |
case 'process_starts':
|
|
|
166 |
{$loadingState}
|
167 |
</div>
|
168 |
{#if showModal}
|
169 |
+
<PromptModal on:paint={onPaint} on:close={onClose} />
|
170 |
{/if}
|
171 |
<div class="fixed top-0 left-0 z-0 w-screen h-screen">
|
172 |
<PaintCanvas />
|
173 |
|
174 |
<main class="z-10 relative">
|
175 |
+
<PaintFrame on:prompt={onPrompt} transform={$currZoomTransform} />
|
|
|
|
|
|
|
|
|
176 |
|
177 |
<!-- When others connected, iterate through others and show their cursors -->
|
178 |
{#if $others}
|
179 |
{#each [...$others] as { connectionId, presence } (connectionId)}
|
180 |
+
{#if (presence?.status === Status.prompting || presence?.status === Status.masking) && presence?.frame}
|
181 |
<Frame
|
182 |
color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
|
183 |
position={presence?.frame}
|
|
|
199 |
</div>
|
200 |
|
201 |
<div class="fixed bottom-0 left-0 right-0 z-10 my-2">
|
202 |
+
<Menu on:prompt={onPrompt} />
|
203 |
</div>
|
204 |
|
205 |
<style lang="postcss" scoped>
|
frontend/src/lib/Frame.svelte
CHANGED
@@ -33,7 +33,7 @@
|
|
33 |
</div>
|
34 |
{/if}
|
35 |
|
36 |
-
<h2 class="text-lg"
|
37 |
<div class="absolute bottom-0 font-bold text-lg">{prompt}</div>
|
38 |
</div>
|
39 |
</div>
|
|
|
33 |
</div>
|
34 |
{/if}
|
35 |
|
36 |
+
<h2 class="text-lg"></h2>
|
37 |
<div class="absolute bottom-0 font-bold text-lg">{prompt}</div>
|
38 |
</div>
|
39 |
</div>
|
frontend/src/lib/Icons/LoadingIcon.svelte
CHANGED
@@ -1,12 +1,17 @@
|
|
1 |
<svg
|
2 |
-
|
|
|
|
|
|
|
3 |
fill="none"
|
4 |
-
|
5 |
-
viewBox="0 0 24 24"
|
6 |
-
class="animate-spin block w-full opacity-60"
|
7 |
>
|
8 |
<path
|
9 |
-
|
10 |
-
|
|
|
|
|
|
|
|
|
11 |
/>
|
12 |
</svg>
|
|
|
1 |
<svg
|
2 |
+
class="animate-spin opacity-60"
|
3 |
+
width="51"
|
4 |
+
height="51"
|
5 |
+
viewBox="0 0 21 21"
|
6 |
fill="none"
|
7 |
+
xmlns="http://www.w3.org/2000/svg"
|
|
|
|
|
8 |
>
|
9 |
<path
|
10 |
+
d="M21 10.5C21 4.70101 16.299 0 10.5 0C4.70101 0 0 4.70101 0 10.5C0 16.299 4.70101 21 10.5 21C16.299 21 21 16.299 21 10.5Z"
|
11 |
+
fill="white"
|
12 |
+
/>
|
13 |
+
<path
|
14 |
+
d="M10.5006 17C9.6233 17 8.77136 16.8286 7.97021 16.4896C7.19572 16.1621 6.50122 15.6924 5.90448 15.0957C5.30774 14.499 4.83797 13.8046 4.5104 13.0302C4.1714 12.2291 4 11.3772 4 10.5C4 10.2474 4.20441 10.043 4.45708 10.043C4.70974 10.043 4.91415 10.2474 4.91415 10.5C4.91415 11.2541 5.06143 11.9854 5.35345 12.6747C5.63532 13.3399 6.0378 13.9379 6.55074 14.4508C7.06368 14.9637 7.66169 15.3674 8.32698 15.6479C9.01514 15.9387 9.74646 16.0859 10.5006 16.0859C11.2548 16.0859 11.9861 15.9387 12.6756 15.6467C13.3409 15.3648 13.9389 14.9624 14.4518 14.4495C14.9647 13.9366 15.3685 13.3387 15.6491 12.6734C15.9398 11.9854 16.0871 11.2541 16.0871 10.5C16.0871 9.7459 15.9398 9.01465 15.6478 8.32529C15.3669 7.66166 14.9604 7.05857 14.4505 6.54922C13.9417 6.03876 13.3384 5.63215 12.6743 5.35205C11.9861 5.06133 11.2548 4.91406 10.5006 4.91406C10.248 4.91406 10.0436 4.70967 10.0436 4.45703C10.0436 4.20439 10.248 4 10.5006 4C11.378 4 12.2299 4.17139 13.0311 4.51035C13.8055 4.83789 14.5 5.30762 15.0968 5.9043C15.6935 6.50098 16.162 7.19668 16.4896 7.96982C16.8286 8.7709 17 9.62275 17 10.5C17 11.3772 16.8286 12.2291 16.4896 13.0302C16.1633 13.8046 15.6935 14.499 15.0968 15.0957C14.5 15.6924 13.8043 16.1608 13.0311 16.4884C12.2299 16.8286 11.378 17 10.5006 17Z"
|
15 |
+
fill="#1F2937"
|
16 |
/>
|
17 |
</svg>
|
frontend/src/lib/Menu.svelte
CHANGED
@@ -5,7 +5,7 @@
|
|
5 |
|
6 |
const onKeyup = (e: KeyboardEvent) => {
|
7 |
if (e.key === 'Enter') {
|
8 |
-
dispatch('
|
9 |
}
|
10 |
};
|
11 |
onMount(() => {
|
@@ -17,19 +17,5 @@
|
|
17 |
</script>
|
18 |
|
19 |
<div class="grid grid-cols-1 gap-3 w-max mx-auto">
|
20 |
-
|
21 |
-
<input
|
22 |
-
id="showframes"
|
23 |
-
type="checkbox"
|
24 |
-
bind:checked={$showFrames}
|
25 |
-
class="w-4 h-4 text-blue-600 bg-gray-100 rounded border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600 cursor-pointer"
|
26 |
-
/>
|
27 |
-
<label for="showframes" class="text-white dark:text-white cursor-pointer ml-2">
|
28 |
-
Show Frames
|
29 |
-
</label>
|
30 |
-
</div> -->
|
31 |
-
<!-- <button class="button" title="Move" on:click={() => dispatch('paintMode', { mode: 'move' })}>
|
32 |
-
Move
|
33 |
-
</button> -->
|
34 |
-
<PPButton on:click={() => dispatch('paintMode', { mode: 'paint' })} />
|
35 |
</div>
|
|
|
5 |
|
6 |
const onKeyup = (e: KeyboardEvent) => {
|
7 |
if (e.key === 'Enter') {
|
8 |
+
dispatch('prompt');
|
9 |
}
|
10 |
};
|
11 |
onMount(() => {
|
|
|
17 |
</script>
|
18 |
|
19 |
<div class="grid grid-cols-1 gap-3 w-max mx-auto">
|
20 |
+
<PPButton on:click={() => dispatch('prompt')} />
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
21 |
</div>
|
frontend/src/lib/PaintFrame.svelte
CHANGED
@@ -16,13 +16,13 @@
|
|
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();
|
22 |
|
23 |
export let transform: ZoomTransform;
|
24 |
export let color = 'black';
|
25 |
-
export let interactive = false;
|
26 |
|
27 |
let maskCtx: CanvasRenderingContext2D;
|
28 |
|
@@ -35,7 +35,7 @@
|
|
35 |
let dragEnabled = true;
|
36 |
let isDragging = false;
|
37 |
$: prompt = $myPresence?.currentPrompt;
|
38 |
-
$: isLoading = $myPresence?.
|
39 |
|
40 |
$: coord = {
|
41 |
x: transform.applyX(position.x),
|
@@ -52,25 +52,30 @@
|
|
52 |
maskCtx.drawImage($canvasEl, cursor.x, cursor.y, 512, 512, 0, 0, 512, 512);
|
53 |
maskCtx.restore();
|
54 |
}
|
55 |
-
function
|
56 |
maskCtx.save();
|
57 |
maskCtx.globalCompositeOperation = 'destination-out';
|
58 |
maskCtx.beginPath();
|
59 |
-
maskCtx.
|
60 |
-
maskCtx.
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
71 |
});
|
72 |
|
73 |
-
function cursorUpdate(selection
|
74 |
function handlePointerMove(event: PointerEvent) {
|
75 |
myPresence.update({
|
76 |
cursor: {
|
@@ -87,21 +92,24 @@
|
|
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 |
-
|
|
|
|
|
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() {
|
@@ -151,25 +159,30 @@
|
|
151 |
}
|
152 |
function toggleDrag() {
|
153 |
dragEnabled = true;
|
|
|
|
|
|
|
154 |
}
|
155 |
-
function
|
156 |
dragEnabled = false;
|
157 |
cropCanvas(position);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
158 |
}
|
159 |
</script>
|
160 |
|
161 |
<div>
|
162 |
<div
|
163 |
-
class="absolute top-0 left-0"
|
164 |
-
style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k});
|
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">
|
@@ -182,23 +195,23 @@
|
|
182 |
</div>
|
183 |
{/if}
|
184 |
|
185 |
-
<h2 class="text-lg"
|
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('
|
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={
|
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>
|
@@ -208,8 +221,7 @@
|
|
208 |
</div>
|
209 |
<div
|
210 |
bind:this={frameElement}
|
211 |
-
class="absolute top-0 left-0 w-[512px] h-[512px]
|
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 |
/>
|
@@ -217,6 +229,16 @@
|
|
217 |
|
218 |
<style lang="postcss" scoped>
|
219 |
.frame {
|
220 |
-
@apply relative grid grid-cols-3 grid-rows-3 ring-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
221 |
}
|
222 |
</style>
|
|
|
16 |
import { loadingState, canvasEl, maskEl } from '$lib/store';
|
17 |
|
18 |
import { toggle_class } from 'svelte/internal';
|
19 |
+
import { Status } from './types';
|
20 |
const myPresence = useMyPresence();
|
21 |
|
22 |
const dispatch = createEventDispatcher();
|
23 |
|
24 |
export let transform: ZoomTransform;
|
25 |
export let color = 'black';
|
|
|
26 |
|
27 |
let maskCtx: CanvasRenderingContext2D;
|
28 |
|
|
|
35 |
let dragEnabled = true;
|
36 |
let isDragging = false;
|
37 |
$: prompt = $myPresence?.currentPrompt;
|
38 |
+
$: isLoading = $myPresence?.status === Status.loading || false;
|
39 |
|
40 |
$: coord = {
|
41 |
x: transform.applyX(position.x),
|
|
|
52 |
maskCtx.drawImage($canvasEl, cursor.x, cursor.y, 512, 512, 0, 0, 512, 512);
|
53 |
maskCtx.restore();
|
54 |
}
|
55 |
+
function drawLine(points: { x: number; y: number; lastx: number; lasty: number }) {
|
56 |
maskCtx.save();
|
57 |
maskCtx.globalCompositeOperation = 'destination-out';
|
58 |
maskCtx.beginPath();
|
59 |
+
maskCtx.moveTo(points.lastx, points.lasty);
|
60 |
+
maskCtx.lineTo(points.x, points.y);
|
61 |
+
maskCtx.lineWidth = 50;
|
62 |
+
maskCtx.lineCap = 'round';
|
63 |
+
maskCtx.strokeStyle = 'black';
|
64 |
+
maskCtx.stroke();
|
65 |
maskCtx.restore();
|
66 |
}
|
|
|
67 |
onMount(() => {
|
68 |
maskCtx = $maskEl.getContext('2d') as CanvasRenderingContext2D;
|
69 |
|
70 |
select(frameElement)
|
71 |
.call(dragMoveHandler() as any)
|
72 |
.call(cursorUpdate);
|
73 |
+
select($maskEl)
|
74 |
+
.call(maskingHandler() as any)
|
75 |
+
.call(cursorUpdate);
|
76 |
});
|
77 |
|
78 |
+
function cursorUpdate(selection) {
|
79 |
function handlePointerMove(event: PointerEvent) {
|
80 |
myPresence.update({
|
81 |
cursor: {
|
|
|
92 |
return selection.on('pointermove', handlePointerMove).on('pointerleave', handlePointerLeave);
|
93 |
}
|
94 |
function maskingHandler() {
|
95 |
+
let lastx: number;
|
96 |
+
let lasty: number;
|
97 |
function dragstarted(event: Event) {
|
98 |
const x = event.x / transform.k;
|
99 |
const y = event.y / transform.k;
|
100 |
+
lastx = x;
|
101 |
+
lasty = y;
|
102 |
}
|
103 |
|
104 |
function dragged(event: Event) {
|
105 |
const x = event.x / transform.k;
|
106 |
const y = event.y / transform.k;
|
107 |
+
drawLine({ x, y, lastx, lasty });
|
108 |
+
lastx = x;
|
109 |
+
lasty = y;
|
110 |
}
|
111 |
|
112 |
+
function dragended(event: Event) {}
|
|
|
|
|
|
|
113 |
return drag().on('start', dragstarted).on('drag', dragged).on('end', dragended);
|
114 |
}
|
115 |
function dragMoveHandler() {
|
|
|
159 |
}
|
160 |
function toggleDrag() {
|
161 |
dragEnabled = true;
|
162 |
+
myPresence.update({
|
163 |
+
status: Status.dragging
|
164 |
+
});
|
165 |
}
|
166 |
+
function toggleDrawMask() {
|
167 |
dragEnabled = false;
|
168 |
cropCanvas(position);
|
169 |
+
myPresence.update({
|
170 |
+
status: Status.masking
|
171 |
+
});
|
172 |
+
}
|
173 |
+
|
174 |
+
function cleanMask() {
|
175 |
+
cropCanvas(position);
|
176 |
}
|
177 |
</script>
|
178 |
|
179 |
<div>
|
180 |
<div
|
181 |
+
class="absolute top-0 left-0 pen"
|
182 |
+
style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); transform-origin: 0 0;`}
|
183 |
>
|
184 |
<div class="frame">
|
185 |
+
<canvas class={dragEnabled ? '' : 'bg-white'} bind:this={$maskEl} width="512" height="512" />
|
|
|
|
|
|
|
|
|
|
|
186 |
<div class="pointer-events-none touch-none">
|
187 |
{#if $loadingState}
|
188 |
<div class="col-span-2 row-start-1">
|
|
|
195 |
</div>
|
196 |
{/if}
|
197 |
|
198 |
+
<h2 class="text-lg" />
|
199 |
<div class="absolute bottom-0 font-bold text-lg">{prompt}</div>
|
200 |
</div>
|
201 |
{#if !isDragging}
|
202 |
<div class="absolute top-full ">
|
203 |
<div class="py-2">
|
204 |
+
<PPButton on:click={() => dispatch('prompt')} />
|
205 |
</div>
|
206 |
</div>
|
207 |
<div class="absolute left-full bottom-0">
|
208 |
<div class="px-2">
|
209 |
<DragButton isActive={dragEnabled} on:click={toggleDrag} />
|
210 |
<div class="flex bg-white rounded-full mt-3">
|
211 |
+
<MaskButton isActive={!dragEnabled} on:click={toggleDrawMask} />
|
212 |
{#if !dragEnabled}
|
213 |
<span class="border-gray-800 border-opacity-50 border-r-2 my-2" />
|
214 |
+
<UndoButton on:click={cleanMask} />
|
215 |
{/if}
|
216 |
</div>
|
217 |
</div>
|
|
|
221 |
</div>
|
222 |
<div
|
223 |
bind:this={frameElement}
|
224 |
+
class="absolute top-0 left-0 w-[512px] h-[512px] hand
|
|
|
225 |
{dragEnabled ? 'block' : 'hidden'}"
|
226 |
style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); transform-origin: 0 0;`}
|
227 |
/>
|
|
|
229 |
|
230 |
<style lang="postcss" scoped>
|
231 |
.frame {
|
232 |
+
@apply relative grid grid-cols-3 grid-rows-3 ring-8 ring-blue-500 w-[512px] h-[512px];
|
233 |
+
}
|
234 |
+
.hand {
|
235 |
+
cursor: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAUCAYAAABvVQZ0AAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAHSSURBVHgBzVQ9LENRFD4VozZWP4ku/jqJAYNoEQaWNpEwIYJFBINY6UCn6mKTKhYNSTuwVFBiaEgai9bP4CWKUftqv845vU+k7evr2C/5cu6799zvnHvOvQ+gUmEqNimEsKLZQ3YgA0j6TiNXeJPJlIZygEK1yFAmo4rj0Kkg0Jj4DyHyMxKyIt/I+zH5LJrau8V76lPMLa6KjU2vyKhZsbHl1YTX8/dX5X071eyPdX5xDRrr68BiNsNJ+AxsrS1sCf6DIEQub2hoNxJjxO7ivHnMNZqzzlHAIJBIvkBPV6cm7JC11RULWMw1LELRhwf6IPXxxSSRyMU1ztk5mKpmyX9aV0x2KUoitMHW1sxHjd3HWYQyGh7sY1+Z3ZTRMfcpCxLxHwZhZnIc63TEC3TU3iEXj2XdqGGOomKyBhxNq1fi6ZVF3J5tyK+rPGqHXmZX6OAgR61eVCc9UBDE332rzlu3uj0+WRs7GKGxoY5MWi8zZWZygp1KZUSg6yIR1RNzYQeV2/MQLC/MQqmM5HoYb8CDNl/w0GUTlpFLVDPfzi5myZ0DW3szX5Ex5whYLGYFp/pRTAEjyHcaFoX4RvqKPXRTOaJoHJDrmoKMlv0Lqhj8AlEeE/77ZUZMAAAAAElFTkSuQmCC')
|
236 |
+
8 8,
|
237 |
+
pointer;
|
238 |
+
}
|
239 |
+
.pen {
|
240 |
+
cursor: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAExSURBVHgBnZJBjkRQEIZLt45YSIwTsLUQbuAqc4LpG3AUO9ueE5idpSFhSyKxsWCBDfKmyozZtcafVLz38r6/Sr0COCDGmEwBZ4TgB0bDfuUchvM8Z7ZtM1VVGa13m9BFuh1FEZNlmdERmfxJvryCi6JwNU2DOI4hCAJAkyVIHMe1mzCVStloS+F53lJJ0yytcA/BFKZprqXfT8FPM/u+r4ZhGJRl2ZyCkyRp0jRlfd8z13X3wyTs7oPg1YDeejeM4ghcN7quAz0ZgoC/AY7jAM/zrSRJb88M+Ov12s7zLN9ut+UAewDjOEJd18u3qqrN2eeHYfgWBMGmy1mWARnRetU0TZ9bBhecpnes4H+iVhj7AaIotoZh3DcNLMsq0MDquu6xHpKhoihf2A8LExRbBj/Uih2c7AwcBQAAAABJRU5ErkJggg==')
|
241 |
+
8 8,
|
242 |
+
pointer;
|
243 |
}
|
244 |
</style>
|
frontend/src/lib/PromptModal.svelte
CHANGED
@@ -1,6 +1,7 @@
|
|
1 |
<script lang="ts">
|
2 |
import { createEventDispatcher, onMount } from 'svelte';
|
3 |
import { useMyPresence } from '$lib/liveblocks';
|
|
|
4 |
|
5 |
const dispatch = createEventDispatcher();
|
6 |
let prompt = '';
|
@@ -28,14 +29,14 @@
|
|
28 |
prompt = newPrompt;
|
29 |
myPresence.update({
|
30 |
currentPrompt: prompt,
|
31 |
-
|
32 |
});
|
33 |
}, 100);
|
34 |
}
|
35 |
function onPrompt() {
|
36 |
if (prompt.trim() !== '') {
|
37 |
console.log('Prompting with: ', prompt);
|
38 |
-
dispatch('
|
39 |
}
|
40 |
}
|
41 |
function onInput(event: Event) {
|
@@ -45,7 +46,7 @@
|
|
45 |
function cancel() {
|
46 |
myPresence.update({
|
47 |
currentPrompt: '',
|
48 |
-
|
49 |
});
|
50 |
dispatch('close');
|
51 |
}
|
@@ -56,20 +57,25 @@
|
|
56 |
on:submit|preventDefault={onPrompt}
|
57 |
on:click={cancel}
|
58 |
>
|
59 |
-
<
|
60 |
-
|
61 |
-
|
62 |
-
|
63 |
-
|
64 |
-
|
65 |
-
|
66 |
-
|
67 |
-
|
68 |
-
|
|
|
|
|
|
|
|
|
|
|
69 |
</form>
|
70 |
|
71 |
<style lang="postcss" scoped>
|
72 |
.input {
|
73 |
-
@apply
|
74 |
}
|
75 |
</style>
|
|
|
1 |
<script lang="ts">
|
2 |
import { createEventDispatcher, onMount } from 'svelte';
|
3 |
import { useMyPresence } from '$lib/liveblocks';
|
4 |
+
import { Status } from '$lib/types';
|
5 |
|
6 |
const dispatch = createEventDispatcher();
|
7 |
let prompt = '';
|
|
|
29 |
prompt = newPrompt;
|
30 |
myPresence.update({
|
31 |
currentPrompt: prompt,
|
32 |
+
status: Status.prompting
|
33 |
});
|
34 |
}, 100);
|
35 |
}
|
36 |
function onPrompt() {
|
37 |
if (prompt.trim() !== '') {
|
38 |
console.log('Prompting with: ', prompt);
|
39 |
+
dispatch('paint');
|
40 |
}
|
41 |
}
|
42 |
function onInput(event: Event) {
|
|
|
46 |
function cancel() {
|
47 |
myPresence.update({
|
48 |
currentPrompt: '',
|
49 |
+
status: Status.ready
|
50 |
});
|
51 |
dispatch('close');
|
52 |
}
|
|
|
57 |
on:submit|preventDefault={onPrompt}
|
58 |
on:click={cancel}
|
59 |
>
|
60 |
+
<div class="flex bg-white rounded-2xl px-2 w-full max-w-md">
|
61 |
+
<input
|
62 |
+
bind:this={inputEl}
|
63 |
+
on:click|stopPropagation
|
64 |
+
on:input={onInput}
|
65 |
+
class="input"
|
66 |
+
placeholder="Type a prompt..."
|
67 |
+
title="Input prompt to generate image and obtain palette"
|
68 |
+
type="text"
|
69 |
+
name="prompt"
|
70 |
+
/>
|
71 |
+
<button on:click|preventDefault={onPrompt} class="font-mono border-l-2 pl-2" type="submit"
|
72 |
+
>Paint</button
|
73 |
+
>
|
74 |
+
</div>
|
75 |
</form>
|
76 |
|
77 |
<style lang="postcss" scoped>
|
78 |
.input {
|
79 |
+
@apply flex-grow text-sm m-2 p-0 disabled:opacity-50 italic placeholder:text-black text-black placeholder:text-opacity-50 border-0 focus:outline-none focus:border-gray-400 focus:ring-1;
|
80 |
}
|
81 |
</style>
|
frontend/src/lib/types.ts
CHANGED
@@ -1,3 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
export type Presence = {
|
2 |
cursor: {
|
3 |
x: number;
|
@@ -7,8 +16,7 @@ export type Presence = {
|
|
7 |
x: number;
|
8 |
y: number;
|
9 |
} | null;
|
10 |
-
|
11 |
-
isLoading: boolean;
|
12 |
currentPrompt: string
|
13 |
}
|
14 |
|
@@ -26,3 +34,4 @@ export type PromptImgObject = {
|
|
26 |
};
|
27 |
|
28 |
export type PromptImgKey = string;
|
|
|
|
1 |
+
export enum Status {
|
2 |
+
ready = 'ready',
|
3 |
+
loading = 'loading',
|
4 |
+
prompting = 'prompting',
|
5 |
+
processing = 'processing',
|
6 |
+
dragging = 'dragging',
|
7 |
+
masking = 'masking',
|
8 |
+
}
|
9 |
+
|
10 |
export type Presence = {
|
11 |
cursor: {
|
12 |
x: number;
|
|
|
16 |
x: number;
|
17 |
y: number;
|
18 |
} | null;
|
19 |
+
status: Status;
|
|
|
20 |
currentPrompt: string
|
21 |
}
|
22 |
|
|
|
34 |
};
|
35 |
|
36 |
export type PromptImgKey = string;
|
37 |
+
|
frontend/src/routes/+page.svelte
CHANGED
@@ -21,6 +21,8 @@
|
|
21 |
let client: Client;
|
22 |
|
23 |
onMount(() => {
|
|
|
|
|
24 |
// Add random id to room param if not set, and return the id string
|
25 |
// e.g. /?room=758df70b5e94c13289df6
|
26 |
roomId = 'multiplayer-SD';
|
|
|
21 |
let client: Client;
|
22 |
|
23 |
onMount(() => {
|
24 |
+
document.addEventListener('wheel', (e) => e.preventDefault(), { passive: false });
|
25 |
+
|
26 |
// Add random id to room param if not set, and return the id string
|
27 |
// e.g. /?room=758df70b5e94c13289df6
|
28 |
roomId = 'multiplayer-SD';
|