Spaces:
Runtime error
Runtime error
liveblocks
Browse files- frontend/package.json +2 -1
- frontend/src/lib/Canvas.svelte +103 -0
- frontend/src/lib/Cursor.svelte +40 -0
- frontend/src/lib/store.ts +14 -0
- frontend/src/lib/types.ts +13 -0
- frontend/src/lib/utils.ts +1 -1
- frontend/src/routes/+page.svelte +39 -0
frontend/package.json
CHANGED
@@ -36,6 +36,7 @@
|
|
36 |
},
|
37 |
"type": "module",
|
38 |
"dependencies": {
|
39 |
-
"@fontsource/fira-mono": "^4.5.0"
|
|
|
40 |
}
|
41 |
}
|
|
|
36 |
},
|
37 |
"type": "module",
|
38 |
"dependencies": {
|
39 |
+
"@fontsource/fira-mono": "^4.5.0",
|
40 |
+
"@liveblocks/client": "^0.18.2"
|
41 |
}
|
42 |
}
|
frontend/src/lib/Canvas.svelte
ADDED
@@ -0,0 +1,103 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import Cursor from '$lib/Cursor.svelte';
|
3 |
+
import type { Room } from '@liveblocks/client';
|
4 |
+
import { onDestroy } from 'svelte';
|
5 |
+
/**
|
6 |
+
* The main Liveblocks code for the example.
|
7 |
+
* Check in src/routes/index.svelte to see the setup code.
|
8 |
+
*/
|
9 |
+
|
10 |
+
export let room: Room;
|
11 |
+
|
12 |
+
// Get initial values for presence and others
|
13 |
+
let myPresence = room.getPresence();
|
14 |
+
let others = room.getOthers();
|
15 |
+
|
16 |
+
// Subscribe to further changes
|
17 |
+
const unsubscribeMyPresence = room.subscribe('my-presence', (presence) => {
|
18 |
+
myPresence = presence;
|
19 |
+
});
|
20 |
+
|
21 |
+
const unsubscribeOthers = room.subscribe('others', (otherUsers) => {
|
22 |
+
others = otherUsers;
|
23 |
+
});
|
24 |
+
|
25 |
+
// Unsubscribe when unmounting
|
26 |
+
onDestroy(() => {
|
27 |
+
unsubscribeMyPresence();
|
28 |
+
unsubscribeOthers();
|
29 |
+
});
|
30 |
+
|
31 |
+
// Update cursor presence to current pointer location
|
32 |
+
function handlePointerMove(event: PointerEvent) {
|
33 |
+
event.preventDefault();
|
34 |
+
room.updatePresence({
|
35 |
+
cursor: {
|
36 |
+
x: Math.round(event.clientX),
|
37 |
+
y: Math.round(event.clientY)
|
38 |
+
}
|
39 |
+
});
|
40 |
+
}
|
41 |
+
|
42 |
+
// When the pointer leaves the page, set cursor presence to null
|
43 |
+
function handlePointerLeave() {
|
44 |
+
room.updatePresence({
|
45 |
+
cursor: null
|
46 |
+
});
|
47 |
+
}
|
48 |
+
|
49 |
+
const COLORS = [
|
50 |
+
'#E57373',
|
51 |
+
'#9575CD',
|
52 |
+
'#4FC3F7',
|
53 |
+
'#81C784',
|
54 |
+
'#FFF176',
|
55 |
+
'#FF8A65',
|
56 |
+
'#F06292',
|
57 |
+
'#7986CB'
|
58 |
+
];
|
59 |
+
</script>
|
60 |
+
|
61 |
+
<main on:pointerleave={handlePointerLeave} on:pointermove={handlePointerMove}>
|
62 |
+
<!-- Show the current user's cursor location -->
|
63 |
+
<div class="text">
|
64 |
+
{myPresence?.cursor
|
65 |
+
? `${myPresence.cursor.x} × ${myPresence.cursor.y}`
|
66 |
+
: 'Move your cursor to broadcast its position to other people in the room.'}
|
67 |
+
</div>
|
68 |
+
|
69 |
+
<!-- When others connected, iterate through others and show their cursors -->
|
70 |
+
{#if others}
|
71 |
+
{#each [...others] as { connectionId, presence } (connectionId)}
|
72 |
+
{#if presence?.cursor}
|
73 |
+
<Cursor
|
74 |
+
color={COLORS[connectionId % COLORS.length]}
|
75 |
+
x={presence.cursor.x}
|
76 |
+
y={presence.cursor.y}
|
77 |
+
/>
|
78 |
+
{/if}
|
79 |
+
{/each}
|
80 |
+
{/if}
|
81 |
+
</main>
|
82 |
+
|
83 |
+
<style lang="postcss" scoped>
|
84 |
+
main {
|
85 |
+
@apply fixed top-0 left-0 w-screen h-screen flex flex-col items-center justify-center touch-none bg-white;
|
86 |
+
/* position: absolute;
|
87 |
+
top: 0;
|
88 |
+
left: 0;
|
89 |
+
width: 100vw;
|
90 |
+
height: 100vh;
|
91 |
+
display: flex;
|
92 |
+
place-content: center;
|
93 |
+
place-items: center;
|
94 |
+
touch-action: none;
|
95 |
+
background-color: white; */
|
96 |
+
}
|
97 |
+
|
98 |
+
.text {
|
99 |
+
max-width: 380px;
|
100 |
+
margin: 0 16px;
|
101 |
+
text-align: center;
|
102 |
+
}
|
103 |
+
</style>
|
frontend/src/lib/Cursor.svelte
ADDED
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<script lang="ts">
|
2 |
+
import { spring } from 'svelte/motion';
|
3 |
+
|
4 |
+
export let color = '';
|
5 |
+
export let x = 0;
|
6 |
+
export let y = 0;
|
7 |
+
|
8 |
+
// Spring animation for cursor
|
9 |
+
const coords = spring(
|
10 |
+
{ x, y },
|
11 |
+
{
|
12 |
+
stiffness: 0.07,
|
13 |
+
damping: 0.35
|
14 |
+
}
|
15 |
+
);
|
16 |
+
|
17 |
+
// Update spring when x and y change
|
18 |
+
$: coords.set({ x, y });
|
19 |
+
</script>
|
20 |
+
|
21 |
+
<svg
|
22 |
+
class="cursor"
|
23 |
+
fill="none"
|
24 |
+
height="36"
|
25 |
+
style={`transform: translateX(${$coords.x}px) translateY(${$coords.y}px)`}
|
26 |
+
viewBox="0 0 24 36"
|
27 |
+
width="24"
|
28 |
+
xmlns="http://www.w3.org/2000/svg"
|
29 |
+
>
|
30 |
+
<path
|
31 |
+
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
|
32 |
+
fill={color}
|
33 |
+
/>
|
34 |
+
</svg>
|
35 |
+
|
36 |
+
<style lang="postcss" scoped>
|
37 |
+
.cursor {
|
38 |
+
@apply absolute top-0 left-0;
|
39 |
+
}
|
40 |
+
</style>
|
frontend/src/lib/store.ts
CHANGED
@@ -1,3 +1,17 @@
|
|
1 |
import { writable } from 'svelte/store';
|
|
|
|
|
|
|
2 |
export const loadingState = writable<string>('');
|
3 |
export const isLoading = writable<boolean>(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
import { writable } from 'svelte/store';
|
2 |
+
import type { User } from '$lib/types';
|
3 |
+
import { browser } from '$app/environment';
|
4 |
+
|
5 |
export const loadingState = writable<string>('');
|
6 |
export const isLoading = writable<boolean>(false);
|
7 |
+
|
8 |
+
const initialUser: User = crypto.randomUUID();
|
9 |
+
|
10 |
+
export const currentUser = writable<User>(
|
11 |
+
browser ? JSON.parse(localStorage['user'] || JSON.stringify(initialUser)) : initialUser
|
12 |
+
);
|
13 |
+
currentUser.subscribe((value) => {
|
14 |
+
if (browser) {
|
15 |
+
return (localStorage['user'] = JSON.stringify(value));
|
16 |
+
}
|
17 |
+
});
|
frontend/src/lib/types.ts
CHANGED
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export type Presence = {
|
2 |
+
cursor: {
|
3 |
+
x: number;
|
4 |
+
y: number;
|
5 |
+
} | null;
|
6 |
+
};
|
7 |
+
|
8 |
+
export type Storage = {
|
9 |
+
// animals: LiveList<string>,
|
10 |
+
// ...
|
11 |
+
};
|
12 |
+
|
13 |
+
export type User = string;
|
frontend/src/lib/utils.ts
CHANGED
@@ -39,4 +39,4 @@ function slugify(text: string) {
|
|
39 |
.replace(/\-\-+/g, '-')
|
40 |
.replace(/^-+/, '')
|
41 |
.replace(/-+$/, '');
|
42 |
-
}
|
|
|
39 |
.replace(/\-\-+/g, '-')
|
40 |
.replace(/^-+/, '')
|
41 |
.replace(/-+$/, '');
|
42 |
+
}
|
frontend/src/routes/+page.svelte
CHANGED
@@ -1,11 +1,45 @@
|
|
1 |
<script lang="ts">
|
|
|
2 |
import { isLoading, loadingState } from '$lib/store';
|
3 |
import { PUBLIC_WS_ENDPOINT, PUBLIC_DEV_MODE } from '$env/static/public';
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
const apiUrl =
|
6 |
PUBLIC_DEV_MODE === 'DEV'
|
7 |
? 'http://localhost:7860'
|
8 |
: '/embed/huggingface-projects/color-palette-generator-sd';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
9 |
</script>
|
10 |
|
11 |
<div class="max-w-screen-md mx-auto px-3 py-8 relative z-0">
|
@@ -26,6 +60,11 @@
|
|
26 |
</button>
|
27 |
</form>
|
28 |
</div>
|
|
|
|
|
|
|
|
|
|
|
29 |
</div>
|
30 |
|
31 |
<style lang="postcss" scoped>
|
|
|
1 |
<script lang="ts">
|
2 |
+
import { onMount } from 'svelte';
|
3 |
import { isLoading, loadingState } from '$lib/store';
|
4 |
import { PUBLIC_WS_ENDPOINT, PUBLIC_DEV_MODE } from '$env/static/public';
|
5 |
+
import type { Client, Room } from '@liveblocks/client';
|
6 |
+
import { createClient } from '@liveblocks/client';
|
7 |
+
import { currentUser } from '$lib/store';
|
8 |
+
|
9 |
+
import Canvas from '$lib/Canvas.svelte';
|
10 |
+
import type { Presence, Storage } from '$lib/types';
|
11 |
|
12 |
const apiUrl =
|
13 |
PUBLIC_DEV_MODE === 'DEV'
|
14 |
? 'http://localhost:7860'
|
15 |
: '/embed/huggingface-projects/color-palette-generator-sd';
|
16 |
+
|
17 |
+
let client: Client;
|
18 |
+
let room: Room;
|
19 |
+
let roomId = 'sveltekit-live-cursors';
|
20 |
+
|
21 |
+
$: {
|
22 |
+
console.log('whoami', $currentUser);
|
23 |
+
}
|
24 |
+
|
25 |
+
onMount(() => {
|
26 |
+
client = createClient({
|
27 |
+
publicApiKey: 'pk_live_6o9jIg1m7lFJp5kc7HgYgE3S'
|
28 |
+
});
|
29 |
+
|
30 |
+
room = client.enter<Presence, Storage /* UserMeta, RoomEvent */>(roomId, {
|
31 |
+
initialPresence: {
|
32 |
+
cursor: null
|
33 |
+
},
|
34 |
+
initialStorage: {}
|
35 |
+
});
|
36 |
+
console.log('room', room);
|
37 |
+
return () => {
|
38 |
+
if (client && room) {
|
39 |
+
client.leave(roomId);
|
40 |
+
}
|
41 |
+
};
|
42 |
+
});
|
43 |
</script>
|
44 |
|
45 |
<div class="max-w-screen-md mx-auto px-3 py-8 relative z-0">
|
|
|
60 |
</button>
|
61 |
</form>
|
62 |
</div>
|
63 |
+
<div class="relative">
|
64 |
+
{#if room}
|
65 |
+
<Canvas {room} />
|
66 |
+
{/if}
|
67 |
+
</div>
|
68 |
</div>
|
69 |
|
70 |
<style lang="postcss" scoped>
|