Spaces:
Running
Running
init
Browse files- .DS_Store +0 -0
- app.js +30 -0
- index.html +79 -22
- multiplayer.js +141 -0
- style.css +0 -28
.DS_Store
ADDED
Binary file (6.15 kB). View file
|
app.js
ADDED
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
const board = document.getElementById("board");
|
2 |
+
const parent = board.parentElement;
|
3 |
+
|
4 |
+
let transforms = { x: 0, y: 0, scale: 1 };
|
5 |
+
|
6 |
+
const panzoom = Panzoom(board, {
|
7 |
+
startScale: transforms.scale,
|
8 |
+
startX: transforms.x,
|
9 |
+
startY: transforms.y,
|
10 |
+
canvas: true,
|
11 |
+
});
|
12 |
+
|
13 |
+
parent.addEventListener("wheel", panzoom.zoomWithWheel);
|
14 |
+
// No function bind needed
|
15 |
+
|
16 |
+
// This demo binds to shift + wheel
|
17 |
+
parent.addEventListener("wheel", function (event) {
|
18 |
+
if (!event.shiftKey) return;
|
19 |
+
panzoom.zoomWithWheel(event);
|
20 |
+
});
|
21 |
+
|
22 |
+
board.addEventListener("panzoomchange", (event) => {
|
23 |
+
transforms = event.detail; // => { x: 0, y: 0, scale: 1 }
|
24 |
+
});
|
25 |
+
|
26 |
+
setTimeout(() => {
|
27 |
+
html2canvas(board, { useCORS: true }).then((canvas) => {
|
28 |
+
document.body.appendChild(canvas);
|
29 |
+
});
|
30 |
+
}, 4000);
|
index.html
CHANGED
@@ -1,24 +1,81 @@
|
|
1 |
<!DOCTYPE html>
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
24 |
</html>
|
1 |
<!DOCTYPE html>
|
2 |
+
<html lang="en">
|
3 |
+
<meta charset="UTF-8" />
|
4 |
+
<title>Stable Diffusion Multiplayer</title>
|
5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
6 |
+
<script src="https://unpkg.com/@panzoom/panzoom@4.5.1/dist/panzoom.min.js"></script>
|
7 |
+
<script src="//unpkg.com/alpinejs" defer></script>
|
8 |
+
<script src="https://cdn.tailwindcss.com"></script>
|
9 |
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html2canvas/1.4.1/html2canvas.min.js"></script>
|
10 |
+
|
11 |
+
<body>
|
12 |
+
<!-- UI -->
|
13 |
+
<div class="absolute z-10 p-8">
|
14 |
+
<h1 class="text-4xl font-bold">Stable Diffusion together</h1>
|
15 |
+
|
16 |
+
<p class="text-xl text-gray-500">
|
17 |
+
Infinite multiplayer canvas for Stable Diffusion
|
18 |
+
</p>
|
19 |
+
</div>
|
20 |
+
<div
|
21 |
+
class="fixed bottom-16 inset-x-0 flex items-center justify-center z-50 space-x-2"
|
22 |
+
>
|
23 |
+
<button
|
24 |
+
class="text-2xl bg-black text-white py-2 px-5 rounded-2xl hover:bg-gradient-to-br from-red-700 to-blue-700 shadow-2xl font-bold"
|
25 |
+
>
|
26 |
+
<span class="mr-2">✋</span> Move
|
27 |
+
</button>
|
28 |
+
<button
|
29 |
+
class="text-2xl bg-gray-400 text-gray-200 py-2 px-5 rounded-2xl hover:bg-gradient-to-br from-red-700 to-blue-700 shadow-2xl font-bold"
|
30 |
+
>
|
31 |
+
<span class="mr-2">✏️</span> Paint
|
32 |
+
</button>
|
33 |
+
</div>
|
34 |
+
|
35 |
+
<!-- BOARD -->
|
36 |
+
<div class="h-screen w-screen overflow-hidden bg-gray-200 relative">
|
37 |
+
<svg
|
38 |
+
class="cursor absolute"
|
39 |
+
id="cursor-template"
|
40 |
+
width="48"
|
41 |
+
height="72"
|
42 |
+
viewBox="0 0 24 36"
|
43 |
+
fill="transparent"
|
44 |
+
xmlns="http://www.w3.org/2000/svg"
|
45 |
+
>
|
46 |
+
<path
|
47 |
+
d="M5.65376 12.3673H5.46026L5.31717 12.4976L0.500002 16.8829L0.500002 1.19841L11.7841 12.3673H5.65376Z"
|
48 |
+
/>
|
49 |
+
</svg>
|
50 |
+
<div
|
51 |
+
id="cursors-container"
|
52 |
+
class="w-[2560px] h-[2560px] absolute inset-0 z-20"
|
53 |
+
></div>
|
54 |
+
<div id="text" class="absolute bg-black z-30 text-white"></div>
|
55 |
+
<div
|
56 |
+
id="board"
|
57 |
+
class="w-[2560px] bg-white h-[2560px] bg-whtie relative border-gray-300 border"
|
58 |
+
>
|
59 |
+
<div
|
60 |
+
class="top-[256px] left-[256px] h-[512px] w-[512px] absolute border-2 border-indigo-300 bg-gradient-to-t from-indigo-500/50 z-10 to-transparent text-indigo-600 text-2xl flex flex-col justify-between p-4"
|
61 |
+
>
|
62 |
+
<div class="self-end">User 12</div>
|
63 |
+
<div class="self-center font-semibold">Click to paint</div>
|
64 |
+
<div>a rabbit eating a red carrot aezaze azeaezaza aze azae</div>
|
65 |
+
</div>
|
66 |
+
|
67 |
+
<div
|
68 |
+
class="bg-[url('http://placekitten.com/300/300')] bg-cover top-[512px] left-[512px] h-[512px] w-[512px] absolute border-2 border-red-600 text-red-700 text-2xl flex flex-col justify-between overflow-hidden"
|
69 |
+
>
|
70 |
+
<!-- <img
|
71 |
+
@click="open = true"
|
72 |
+
src="http://placekitten.com/300/300"
|
73 |
+
class="w-full h-full"
|
74 |
+
/> -->
|
75 |
+
</div>
|
76 |
+
</div>
|
77 |
+
</div>
|
78 |
+
<script src="./app.js"></script>
|
79 |
+
<script src="./multiplayer.js" type="module"></script>
|
80 |
+
</body>
|
81 |
</html>
|
multiplayer.js
ADDED
@@ -0,0 +1,141 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createClient } from "https://cdn.skypack.dev/@liveblocks/client";
|
2 |
+
|
3 |
+
let PUBLIC_KEY = "pk_test_L8JkCoBm0bYACwp5oOJQsj2n";
|
4 |
+
let roomId = "javascript-live-cursors";
|
5 |
+
|
6 |
+
overrideApiKeyAndRoomId();
|
7 |
+
|
8 |
+
if (!/^pk_(live|test)/.test(PUBLIC_KEY)) {
|
9 |
+
console.warn(
|
10 |
+
`Replace "${PUBLIC_KEY}" by your public key from https://liveblocks.io/dashboard/apikeys.\n` +
|
11 |
+
`Learn more: https://github.com/liveblocks/liveblocks/tree/main/examples/javascript-live-cursors#getting-started.`
|
12 |
+
);
|
13 |
+
}
|
14 |
+
|
15 |
+
const client = createClient({
|
16 |
+
publicApiKey: PUBLIC_KEY,
|
17 |
+
});
|
18 |
+
|
19 |
+
const room = client.enter(roomId, { initialPresence: { cursor: null } });
|
20 |
+
|
21 |
+
const cursorsContainer = document.getElementById("cursors-container");
|
22 |
+
const text = document.getElementById("text");
|
23 |
+
|
24 |
+
room.subscribe("my-presence", (presence) => {
|
25 |
+
const cursor = presence?.cursor ?? null;
|
26 |
+
|
27 |
+
text.innerHTML = cursor
|
28 |
+
? `${cursor.x} × ${cursor.y}`
|
29 |
+
: "Move your cursor to broadcast its position to other people in the room.";
|
30 |
+
});
|
31 |
+
|
32 |
+
/**
|
33 |
+
* Subscribe to every others presence updates.
|
34 |
+
* The callback will be called if you or someone else enters or leaves the room
|
35 |
+
* or when someone presence is updated
|
36 |
+
*/
|
37 |
+
room.subscribe("others", (others, event) => {
|
38 |
+
switch (event.type) {
|
39 |
+
case "reset": {
|
40 |
+
// Clear all cursors
|
41 |
+
cursorsContainer.innerHTML = "";
|
42 |
+
for (const user of others.toArray()) {
|
43 |
+
updateCursor(user);
|
44 |
+
}
|
45 |
+
break;
|
46 |
+
}
|
47 |
+
case "leave": {
|
48 |
+
deleteCursor(event.user);
|
49 |
+
break;
|
50 |
+
}
|
51 |
+
case "enter":
|
52 |
+
case "update": {
|
53 |
+
updateCursor(event.user);
|
54 |
+
break;
|
55 |
+
}
|
56 |
+
}
|
57 |
+
});
|
58 |
+
|
59 |
+
// get mouse position relative to an element
|
60 |
+
function getMousePosition(event, element) {
|
61 |
+
const rect = element.getBoundingClientRect();
|
62 |
+
return {
|
63 |
+
x: event.clientX - rect.left,
|
64 |
+
y: event.clientY - rect.top,
|
65 |
+
};
|
66 |
+
}
|
67 |
+
|
68 |
+
// Get mouse position related to a transform and a scale
|
69 |
+
// function getMousePosition(event, transform, scale) {
|
70 |
+
// console.log(scale);
|
71 |
+
// const rect = event.target.getBoundingClientRect();
|
72 |
+
// const x = (event.offsetX - transform.x) / scale;
|
73 |
+
// const y = (event.offsetY - transform.y) / scale;
|
74 |
+
// return { x, y };
|
75 |
+
// }
|
76 |
+
|
77 |
+
document.addEventListener("pointermove", (e) => {
|
78 |
+
e.preventDefault();
|
79 |
+
// const { offsetX, offsetY, clientX, clientY } = e;
|
80 |
+
// console.log(offsetX, offsetY, clientX, clientY);
|
81 |
+
// console.log(getMousePosition(e, board));
|
82 |
+
room.updatePresence({
|
83 |
+
cursor: getMousePosition(e, board),
|
84 |
+
});
|
85 |
+
});
|
86 |
+
|
87 |
+
document.addEventListener("pointerleave", (e) => {
|
88 |
+
room.updatePresence({ cursor: null });
|
89 |
+
});
|
90 |
+
|
91 |
+
const COLORS = ["#DC2626", "#D97706", "#059669", "#7C3AED", "#DB2777"];
|
92 |
+
|
93 |
+
// Update cursor position based on user presence
|
94 |
+
function updateCursor(user) {
|
95 |
+
const cursor = getCursorOrCreate(user.connectionId);
|
96 |
+
|
97 |
+
if (user.presence?.cursor) {
|
98 |
+
cursor.style.transform = `translateX(${user.presence.cursor.x}px) translateY(${user.presence.cursor.y}px)`;
|
99 |
+
cursor.style.opacity = "1";
|
100 |
+
} else {
|
101 |
+
cursor.style.opacity = "0";
|
102 |
+
}
|
103 |
+
}
|
104 |
+
|
105 |
+
function getCursorOrCreate(connectionId) {
|
106 |
+
let cursor = document.getElementById(`cursor-${connectionId}`);
|
107 |
+
|
108 |
+
if (cursor == null) {
|
109 |
+
cursor = document.getElementById("cursor-template").cloneNode(true);
|
110 |
+
cursor.id = `cursor-${connectionId}`;
|
111 |
+
cursor.style.fill = COLORS[connectionId % COLORS.length];
|
112 |
+
cursorsContainer.appendChild(cursor);
|
113 |
+
}
|
114 |
+
|
115 |
+
return cursor;
|
116 |
+
}
|
117 |
+
|
118 |
+
function deleteCursor(user) {
|
119 |
+
const cursor = document.getElementById(`cursor-${user.connectionId}`);
|
120 |
+
if (cursor) {
|
121 |
+
cursor.parentNode.removeChild(cursor);
|
122 |
+
}
|
123 |
+
}
|
124 |
+
|
125 |
+
/**
|
126 |
+
* This function is used when deploying an example on liveblocks.io.
|
127 |
+
* You can ignore it completely if you run the example locally.
|
128 |
+
*/
|
129 |
+
function overrideApiKeyAndRoomId() {
|
130 |
+
const query = new URLSearchParams(window?.location?.search);
|
131 |
+
const apiKey = query.get("apiKey");
|
132 |
+
const roomIdSuffix = query.get("roomId");
|
133 |
+
|
134 |
+
if (apiKey) {
|
135 |
+
PUBLIC_KEY = apiKey;
|
136 |
+
}
|
137 |
+
|
138 |
+
if (roomIdSuffix) {
|
139 |
+
roomId = `${roomId}-${roomIdSuffix}`;
|
140 |
+
}
|
141 |
+
}
|
style.css
DELETED
@@ -1,28 +0,0 @@
|
|
1 |
-
body {
|
2 |
-
padding: 2rem;
|
3 |
-
font-family: -apple-system, BlinkMacSystemFont, "Arial", sans-serif;
|
4 |
-
}
|
5 |
-
|
6 |
-
h1 {
|
7 |
-
font-size: 16px;
|
8 |
-
margin-top: 0;
|
9 |
-
}
|
10 |
-
|
11 |
-
p {
|
12 |
-
color: rgb(107, 114, 128);
|
13 |
-
font-size: 15px;
|
14 |
-
margin-bottom: 10px;
|
15 |
-
margin-top: 5px;
|
16 |
-
}
|
17 |
-
|
18 |
-
.card {
|
19 |
-
max-width: 620px;
|
20 |
-
margin: 0 auto;
|
21 |
-
padding: 16px;
|
22 |
-
border: 1px solid lightgray;
|
23 |
-
border-radius: 16px;
|
24 |
-
}
|
25 |
-
|
26 |
-
.card p:last-child {
|
27 |
-
margin-bottom: 0;
|
28 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|