ejfox radames HF staff commited on
Commit
bb88c4d
0 Parent(s):

Duplicate from huggingface-projects/stable-diffusion-multiplayer

Browse files

Co-authored-by: Radamés Ajna <radames@users.noreply.huggingface.co>

This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .devcontainer/Dockerfile +11 -0
  2. .devcontainer/devcontainer.json +35 -0
  3. .devcontainer/packages.txt +32 -0
  4. .devcontainer/setup-python-tools.sh +64 -0
  5. .gitattributes +31 -0
  6. .github/workflows/size.yml +16 -0
  7. .github/workflows/sync.yml +20 -0
  8. .gitignore +21 -0
  9. .nvmrc +1 -0
  10. .vscode/extensions.json +6 -0
  11. .vscode/settings.json +3 -0
  12. Makefile +12 -0
  13. README.md +14 -0
  14. frontend/.env.development.example +3 -0
  15. frontend/.env.example +3 -0
  16. frontend/.eslintignore +13 -0
  17. frontend/.eslintrc.cjs +20 -0
  18. frontend/.gitignore +12 -0
  19. frontend/.npmrc +1 -0
  20. frontend/.prettierignore +13 -0
  21. frontend/.prettierrc +8 -0
  22. frontend/README.md +38 -0
  23. frontend/package.json +52 -0
  24. frontend/postcss.config.cjs +6 -0
  25. frontend/src/app.css +25 -0
  26. frontend/src/app.d.ts +29 -0
  27. frontend/src/app.html +15 -0
  28. frontend/src/lib/About.svelte +67 -0
  29. frontend/src/lib/App.svelte +240 -0
  30. frontend/src/lib/Buttons/AboutButton.svelte +43 -0
  31. frontend/src/lib/Buttons/DragButton.svelte +20 -0
  32. frontend/src/lib/Buttons/MaskButton.svelte +18 -0
  33. frontend/src/lib/Buttons/PPButton.svelte +29 -0
  34. frontend/src/lib/Buttons/RoomsSelector.svelte +136 -0
  35. frontend/src/lib/Buttons/ShareWithCommunity.svelte +80 -0
  36. frontend/src/lib/Buttons/UndoButton.svelte +20 -0
  37. frontend/src/lib/Cursor.svelte +35 -0
  38. frontend/src/lib/Frame.svelte +39 -0
  39. frontend/src/lib/Icons/Cursor.svelte +24 -0
  40. frontend/src/lib/Icons/IconCommunity.svelte +25 -0
  41. frontend/src/lib/Icons/LiveBlocks.svelte +117 -0
  42. frontend/src/lib/Icons/LoadingIcon.svelte +23 -0
  43. frontend/src/lib/Icons/Mask.svelte +17 -0
  44. frontend/src/lib/Icons/Move.svelte +20 -0
  45. frontend/src/lib/Icons/People.svelte +29 -0
  46. frontend/src/lib/Icons/Pin.svelte +16 -0
  47. frontend/src/lib/Icons/Room.svelte +16 -0
  48. frontend/src/lib/Icons/Undo.svelte +21 -0
  49. frontend/src/lib/Menu.svelte +57 -0
  50. frontend/src/lib/PaintCanvas.svelte +284 -0
.devcontainer/Dockerfile ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.245.2/containers/codespaces-linux/.devcontainer/base.Dockerfile
2
+
3
+ FROM mcr.microsoft.com/vscode/devcontainers/universal:2-focal
4
+
5
+ USER root
6
+
7
+ COPY packages.txt /root/packages.txt
8
+ RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
9
+ && xargs -r -a /root/packages.txt apt-get install -y
10
+
11
+ USER codespace
.devcontainer/devcontainer.json ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "postCreateCommand": "oryx build -p virtualenv_name=.venv --log-file /tmp/oryx-build.log --manifest-dir /tmp || echo 'Could not auto-build. Skipping.' && make set-env",
3
+ "hostRequirements": {
4
+ "gpus": 1
5
+ },
6
+ "features": {
7
+ "ghcr.io/devcontainers/features/nvidia-cuda:1": {
8
+ "installCudnn": true
9
+ }
10
+ },
11
+ "build": {
12
+ "dockerfile": "Dockerfile"
13
+ },
14
+ "customizations": {
15
+ "vscode": {
16
+ "extensions": [
17
+ "GitHub.vscode-pull-request-github",
18
+ "bradlc.vscode-tailwindcss",
19
+ "svelte.svelte-vscode",
20
+ "csstools.postcss"
21
+ ]
22
+ }
23
+ },
24
+ "mounts": [
25
+ "source=codespaces-linux-var-lib-docker,target=/var/lib/docker,type=volume"
26
+ ],
27
+ "runArgs": [
28
+ "--cap-add=SYS_PTRACE",
29
+ "--security-opt",
30
+ "seccomp=unconfined",
31
+ "--privileged",
32
+ "--init"
33
+ ],
34
+ "remoteUser": "codespace"
35
+ }
.devcontainer/packages.txt ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ python3-opencv
2
+ libopencv-dev
3
+ libopencv-core-dev
4
+ pkg-config
5
+ libopencv-imgcodecs-dev
6
+ libopencv-dev
7
+ libopencv-contrib-dev
8
+ build-essential
9
+ cmake
10
+ git
11
+ pkg-config
12
+ libgtk-3-dev
13
+ libavcodec-dev
14
+ libavformat-dev
15
+ libswscale-dev
16
+ libv4l-dev
17
+ libxvidcore-dev
18
+ libx264-dev
19
+ libjpeg-dev
20
+ libpng-dev
21
+ libtiff-dev
22
+ gfortran
23
+ openexr
24
+ libatlas-base-dev
25
+ python3-dev
26
+ python3-numpy
27
+ libtbb2
28
+ libtbb-dev
29
+ libdc1394-22-dev
30
+ libopenexr-dev
31
+ libgstreamer-plugins-base1.0-dev
32
+ libgstreamer1.0-dev
.devcontainer/setup-python-tools.sh ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env bash
2
+
3
+ set -e
4
+
5
+ PYTHON=${1:-"python"}
6
+ USERNAME=${2-"automatic"}
7
+
8
+ # Make sure we run the command as non-root user
9
+ sudoUserIf() {
10
+ if [ "$(id -u)" -eq 0 ] && [ "${USERNAME}" != "root" ]; then
11
+ sudo -u ${USERNAME} "$@"
12
+ else
13
+ "$@"
14
+ fi
15
+ }
16
+
17
+ installPythonPackage() {
18
+ PACKAGE=${1:-""}
19
+ VERSION=${2:-"latest"}
20
+
21
+ # pip skips installation if the package is already installed
22
+ echo "Installing $PACKAGE..."
23
+ if [ "${VERSION}" = "latest" ]; then
24
+ sudoUserIf ${PYTHON} -m pip install ${PACKAGE} --no-cache-dir
25
+ else
26
+ sudoUserIf ${PYTHON} -m pip install ${PACKAGE}=="${VERSION}" --no-cache-dir
27
+ fi
28
+ }
29
+
30
+ # If in automatic mode, determine if a user already exists, if not use vscode
31
+ if [ "${USERNAME}" = "auto" ] || [ "${USERNAME}" = "automatic" ]; then
32
+ USERNAME=""
33
+ POSSIBLE_USERS=("vscode" "node" "codespace" "$(awk -v val=1000 -F ":" '$3==val{print $1}' /etc/passwd)")
34
+ for CURRENT_USER in ${POSSIBLE_USERS[@]}; do
35
+ if id -u ${CURRENT_USER} > /dev/null 2>&1; then
36
+ USERNAME=${CURRENT_USER}
37
+ break
38
+ fi
39
+ done
40
+ if [ "${USERNAME}" = "" ]; then
41
+ USERNAME=vscode
42
+ fi
43
+ elif [ "${USERNAME}" = "none" ]; then
44
+ USERNAME=root
45
+ USER_UID=0
46
+ USER_GID=0
47
+ fi
48
+
49
+ # Make sure that Python is available
50
+ if ! ${PYTHON} --version > /dev/null ; then
51
+ echo "You need to install Python before installing packages"
52
+ exit 1
53
+ fi
54
+
55
+ installPythonPackage "numpy" "latest"
56
+ installPythonPackage "pandas" "latest"
57
+ installPythonPackage "scipy" "latest"
58
+ installPythonPackage "matplotlib" "latest"
59
+ installPythonPackage "seaborn" "latest"
60
+ installPythonPackage "scikit-learn" "latest"
61
+ installPythonPackage "tensorflow" "latest"
62
+ installPythonPackage "keras" "latest"
63
+ installPythonPackage "torch" "latest"
64
+ installPythonPackage "requests" "latest"
.gitattributes ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ *.7z filter=lfs diff=lfs merge=lfs -text
2
+ *.arrow filter=lfs diff=lfs merge=lfs -text
3
+ *.bin filter=lfs diff=lfs merge=lfs -text
4
+ *.bz2 filter=lfs diff=lfs merge=lfs -text
5
+ *.ftz filter=lfs diff=lfs merge=lfs -text
6
+ *.gz filter=lfs diff=lfs merge=lfs -text
7
+ *.h5 filter=lfs diff=lfs merge=lfs -text
8
+ *.joblib filter=lfs diff=lfs merge=lfs -text
9
+ *.lfs.* filter=lfs diff=lfs merge=lfs -text
10
+ *.model filter=lfs diff=lfs merge=lfs -text
11
+ *.msgpack filter=lfs diff=lfs merge=lfs -text
12
+ *.npy filter=lfs diff=lfs merge=lfs -text
13
+ *.npz filter=lfs diff=lfs merge=lfs -text
14
+ *.onnx filter=lfs diff=lfs merge=lfs -text
15
+ *.ot filter=lfs diff=lfs merge=lfs -text
16
+ *.parquet filter=lfs diff=lfs merge=lfs -text
17
+ *.pickle filter=lfs diff=lfs merge=lfs -text
18
+ *.pkl filter=lfs diff=lfs merge=lfs -text
19
+ *.pb filter=lfs diff=lfs merge=lfs -text
20
+ *.pt filter=lfs diff=lfs merge=lfs -text
21
+ *.pth filter=lfs diff=lfs merge=lfs -text
22
+ *.rar filter=lfs diff=lfs merge=lfs -text
23
+ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
24
+ *.tar.* filter=lfs diff=lfs merge=lfs -text
25
+ *.tflite filter=lfs diff=lfs merge=lfs -text
26
+ *.tgz filter=lfs diff=lfs merge=lfs -text
27
+ *.wasm filter=lfs diff=lfs merge=lfs -text
28
+ *.xz filter=lfs diff=lfs merge=lfs -text
29
+ *.zip filter=lfs diff=lfs merge=lfs -text
30
+ *.zst filter=lfs diff=lfs merge=lfs -text
31
+ *tfevents* filter=lfs diff=lfs merge=lfs -text
.github/workflows/size.yml ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Check file size
2
+ on: # or directly `on: [push]` to run the action on every push on any branch
3
+ pull_request:
4
+ branches: [main]
5
+
6
+ # to run this workflow manually from the Actions tab
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ sync-to-hub:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - name: Check large files
14
+ uses: ActionsDesk/lfs-warning@v2.0
15
+ with:
16
+ filesizelimit: 10485760 # this is 10MB so we can sync to HF Spaces
.github/workflows/sync.yml ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ name: Sync to Hugging Face hub
2
+ on:
3
+ push:
4
+ tags:
5
+ - '*'
6
+
7
+ # to run this workflow manually from the Actions tab
8
+ workflow_dispatch:
9
+
10
+ jobs:
11
+ sync-to-hub:
12
+ runs-on: ubuntu-latest
13
+ steps:
14
+ - uses: actions/checkout@v2
15
+ with:
16
+ fetch-depth: 0
17
+ - name: Push to hub
18
+ env:
19
+ HF_TOKEN: ${{ secrets.HF_TOKEN }}
20
+ run: git push https://radames:$HF_TOKEN@huggingface.co/spaces/huggingface-projects/stable-diffusion-multiplayer main --force
.gitignore ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
14
+ venv/
15
+ __pycache__/
16
+ flagged/
17
+ data
18
+ data.db
19
+ data.json
20
+ rooms.db
21
+ sd-multiplayer-data/
.nvmrc ADDED
@@ -0,0 +1 @@
 
1
+ 18
.vscode/extensions.json ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ {
2
+ "recommendations": [
3
+ "bradlc.vscode-tailwindcss",
4
+ "svelte.svelte-vscode"
5
+ ],
6
+ }
.vscode/settings.json ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ {
2
+ "terminal.integrated.defaultProfile.linux": "zsh"
3
+ }
Makefile ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ set-env:
2
+ pip install -r requirements.txt
3
+ cd frontend && npm install && cp .env.development.example .env.development && cp .env.example .env
4
+ build-client:
5
+ cd frontend && npm install && npm run build && rm -rf ../static && cp -r build/ ../static/
6
+ build-dev:
7
+ cd frontend && npm install && npm run build-dev && rm -rf ../static && cp -r build/ ../static/
8
+ run-front-dev:
9
+ cd frontend && npm install && npm run dev
10
+ run-prod:
11
+ python app.py
12
+ build-all: run-prod
README.md ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ title: Stable Diffusion Multiplayer
3
+ emoji: 👥⚙️🎨
4
+ colorFrom: yellow
5
+ colorTo: gray
6
+ sdk: gradio
7
+ sdk_version: 3.4
8
+ app_file: run.py
9
+ fullWidth: true
10
+ pinned: false
11
+ duplicated_from: huggingface-projects/stable-diffusion-multiplayer
12
+ ---
13
+
14
+ Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
frontend/.env.development.example ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ PUBLIC_WS_INPAINTING="ws://0.0.0.0:7860/gradio/queue/join"
2
+ PUBLIC_UPLOADS="https://d26smi9133w0oo.cloudfront.net"
3
+ PUBLIC_API_BASE="/server/api"
frontend/.env.example ADDED
@@ -0,0 +1,3 @@
 
 
 
1
+ PUBLIC_WS_INPAINTING="wss://spaces.huggingface.tech/huggingface-projects/stable-diffusion-multiplayer/gradio/queue/join"
2
+ PUBLIC_UPLOADS="https://d26smi9133w0oo.cloudfront.net"
3
+ PUBLIC_API_BASE="/api"
frontend/.eslintignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
frontend/.eslintrc.cjs ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ root: true,
3
+ parser: '@typescript-eslint/parser',
4
+ extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5
+ plugins: ['svelte3', '@typescript-eslint'],
6
+ ignorePatterns: ['*.cjs'],
7
+ overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8
+ settings: {
9
+ 'svelte3/typescript': () => require('typescript')
10
+ },
11
+ parserOptions: {
12
+ sourceType: 'module',
13
+ ecmaVersion: 2020
14
+ },
15
+ env: {
16
+ browser: true,
17
+ es2017: true,
18
+ node: true
19
+ }
20
+ };
frontend/.gitignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ .vercel
10
+ .output
11
+ venv/
12
+ __pycache__/
frontend/.npmrc ADDED
@@ -0,0 +1 @@
 
1
+ engine-strict=true
frontend/.prettierignore ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .DS_Store
2
+ node_modules
3
+ /build
4
+ /.svelte-kit
5
+ /package
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+
10
+ # Ignore files for PNPM, NPM and YARN
11
+ pnpm-lock.yaml
12
+ package-lock.json
13
+ yarn.lock
frontend/.prettierrc ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
1
+ {
2
+ "useTabs": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "none",
5
+ "printWidth": 100,
6
+ "pluginSearchDirs": ["."],
7
+ "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
8
+ }
frontend/README.md ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # create-svelte
2
+
3
+ Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte).
4
+
5
+ ## Creating a project
6
+
7
+ If you're seeing this, you've probably already done this step. Congrats!
8
+
9
+ ```bash
10
+ # create a new project in the current directory
11
+ npm create svelte@latest
12
+
13
+ # create a new project in my-app
14
+ npm create svelte@latest my-app
15
+ ```
16
+
17
+ ## Developing
18
+
19
+ Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server:
20
+
21
+ ```bash
22
+ npm run dev
23
+
24
+ # or start the server and open the app in a new browser tab
25
+ npm run dev -- --open
26
+ ```
27
+
28
+ ## Building
29
+
30
+ To create a production version of your app:
31
+
32
+ ```bash
33
+ npm run build
34
+ ```
35
+
36
+ You can preview the production build with `npm run preview`.
37
+
38
+ > To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment.
frontend/package.json ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.0.1",
4
+ "scripts": {
5
+ "dev": "NODE_ENV=development vite --config vite.config.dev.ts dev",
6
+ "build-dev": "vite build --mode development",
7
+ "build": "vite build",
8
+ "preview": "vite preview",
9
+ "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
10
+ "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
11
+ "lint": "prettier --check . && eslint .",
12
+ "format": "prettier --write ."
13
+ },
14
+ "devDependencies": {
15
+ "@sveltejs/adapter-static": "^1.0.0-next.43",
16
+ "@sveltejs/kit": "^1.0.0-next.504",
17
+ "@tailwindcss/forms": "^0.5.3",
18
+ "@tailwindcss/line-clamp": "^0.4.2",
19
+ "@types/cookie": "^0.5.1",
20
+ "@types/d3-array": "^3.0.3",
21
+ "@types/d3-drag": "^3.0.1",
22
+ "@types/d3-selection": "^3.0.3",
23
+ "@types/d3-zoom": "^3.0.1",
24
+ "@typescript-eslint/eslint-plugin": "^5.38.0",
25
+ "@typescript-eslint/parser": "^5.38.0",
26
+ "autoprefixer": "^10.4.12",
27
+ "d3-scale": "^4.0.2",
28
+ "eslint": "^8.24.0",
29
+ "eslint-config-prettier": "^8.3.0",
30
+ "eslint-plugin-svelte3": "^4.0.0",
31
+ "postcss": "^8.4.16",
32
+ "prettier": "^2.6.2",
33
+ "prettier-plugin-svelte": "^2.7.1",
34
+ "svelte": "^3.46.0",
35
+ "svelte-check": "^2.9.1",
36
+ "svelte-preprocess": "^4.10.7",
37
+ "tailwindcss": "^3.1.8",
38
+ "tslib": "^2.3.1",
39
+ "typescript": "^4.7.4",
40
+ "vite": "^3.1.3"
41
+ },
42
+ "type": "module",
43
+ "dependencies": {
44
+ "@fontsource/fira-mono": "^4.5.0",
45
+ "@liveblocks/client": "^0.18.2",
46
+ "d3-array": "^3.2.0",
47
+ "d3-drag": "^3.0.0",
48
+ "d3-selection": "^3.0.0",
49
+ "d3-zoom": "^3.0.0",
50
+ "nanoid": "^4.0.0"
51
+ }
52
+ }
frontend/postcss.config.cjs ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
frontend/src/app.css ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ @tailwind base;
2
+ @tailwind components;
3
+ @tailwind utilities;
4
+
5
+ /* Firefox */
6
+ .x-scroll {
7
+ scrollbar-width: thin;
8
+ scrollbar-color: white #2F6DCB;
9
+ }
10
+
11
+ /* Chrome, Edge, and Safari */
12
+ .x-scroll::-webkit-scrollbar {
13
+ width: 4px;
14
+ }
15
+
16
+ .x-scroll::-webkit-scrollbar-track {
17
+ background: white;
18
+ border-radius: 100px;
19
+ }
20
+
21
+ .x-scroll::-webkit-scrollbar-thumb {
22
+ background-color: #2F6DCB;
23
+ border-radius: 100px;
24
+ border: 2px solid #2F6DCB;
25
+ }
frontend/src/app.d.ts ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // See https://kit.svelte.dev/docs/types#app
2
+ // for information about these interfaces
3
+ // and what to do when importing types
4
+ import type { ZoomTransform } from 'd3-zoom';
5
+
6
+ declare global {
7
+ namespace App {
8
+ // interface Locals {}
9
+ // interface PageData {}
10
+ // interface Platform {}
11
+ interface Window {
12
+ parentIFrame: unknown;
13
+ }
14
+ }
15
+ interface Error {
16
+ code: number;
17
+ }
18
+ interface Event {
19
+ relatedTarget: EventTarget | null;
20
+ transform: ZoomTransform;
21
+ x: number;
22
+ y: number;
23
+ subject: {
24
+ x: number;
25
+ y: number;
26
+ }
27
+ sourceEvent: PointerEvent | MouseEvent | TouchEvent
28
+ }
29
+ }
frontend/src/app.html ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
8
+ <script src=""></script>
9
+ %sveltekit.head%
10
+ </head>
11
+ <body class="bg-blue-200">
12
+ <div>%sveltekit.body%</div>
13
+ </body>
14
+
15
+ </html>
frontend/src/lib/About.svelte ADDED
@@ -0,0 +1,67 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import LiveBlocks from '$lib/Icons/LiveBlocks.svelte';
3
+ export let classList = '';
4
+ </script>
5
+
6
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
7
+ <div
8
+ on:click
9
+ class="fixed {classList} w-screen h-screen top-0 left-0 bottom-0 right-0 z-50 items-center justify-center bg-black text-white bg-opacity-90 px-3"
10
+ >
11
+ <div class="max-w-md">
12
+ <h2 class="font-bold text-xl font-mono mb-8">Stable Difussion Multiplayer</h2>
13
+ <ul>
14
+ <li class="mb-2">
15
+ Powered by <a
16
+ href="https://huggingface.co/docs/hub/spaces-gpus"
17
+ class="text-blue-400 hover:text-blue-600 underline"
18
+ >Hugging Face GPU Spaces
19
+ </a>
20
+ </li>
21
+ <li class="mb-2">
22
+ <a
23
+ href="https://huggingface.co/docs/diffusers/index"
24
+ class="text-blue-400 hover:text-blue-600 underline"
25
+ >
26
+ Diffusers
27
+ </a>
28
+ and
29
+ <a
30
+ href="https://github.com/runwayml/stable-diffusion"
31
+ target="_blank"
32
+ class="text-blue-400 hover:text-blue-600 underline"
33
+ rel="noopener noreferrer"
34
+ >
35
+ Runwayml Inpainting Stable Diffusion</a
36
+ >
37
+ </li>
38
+ <li class="mb-2">
39
+ Thanks to <a
40
+ href="https://twitter.com/lkwq007"
41
+ target="_blank"
42
+ rel="noopener noreferrer"
43
+ class="text-blue-400 hover:text-blue-600 underline"
44
+ >
45
+ Lnyan</a
46
+ >
47
+ for the original outpainting technique implemented on
48
+ <a
49
+ href="https://github.com/lkwq007/stablediffusion-infinity"
50
+ target="_blank"
51
+ rel="noopener noreferrer"
52
+ class="text-blue-400 hover:text-blue-600 underline"
53
+ >Stable Diffusion Infinity
54
+ </a>
55
+ </li>
56
+
57
+ <li class="mb-2">
58
+ <p class="text-base flex items-center">
59
+ Multiplayer API by
60
+ <a href="https://liveblocks.io" target="_blank" rel="noopener noreferrer">
61
+ <LiveBlocks classList={'ml-2'} />
62
+ </a>
63
+ </p>
64
+ </li>
65
+ </ul>
66
+ </div>
67
+ </div>
frontend/src/lib/App.svelte ADDED
@@ -0,0 +1,240 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Cursor from '$lib/Cursor.svelte';
3
+ import Frame from '$lib/Frame.svelte';
4
+ import PaintFrame from '$lib/PaintFrame.svelte';
5
+ import PaintCanvas from '$lib/PaintCanvas.svelte';
6
+ import Menu from '$lib/Menu.svelte';
7
+ import PromptModal from '$lib/PromptModal.svelte';
8
+ import { COLORS, FRAME_SIZE } from '$lib/constants';
9
+ import { PUBLIC_WS_INPAINTING } from '$env/static/public';
10
+ import type { PromptImgKey } from '$lib/types';
11
+ import { Status } from '$lib/types';
12
+ import { LiveObject } from '@liveblocks/client';
13
+
14
+ import {
15
+ loadingState,
16
+ currZoomTransform,
17
+ maskEl,
18
+ selectedRoomID,
19
+ isRenderingCanvas
20
+ } from '$lib/store';
21
+ import { useMyPresence, useObject, useOthers } from '$lib/liveblocks';
22
+
23
+ const myPresence = useMyPresence();
24
+ const others = useOthers();
25
+ let showModal = false;
26
+ function getKey(position: { x: number; y: number }): PromptImgKey {
27
+ return `${position.x}_${position.y}`;
28
+ }
29
+
30
+ const promptImgStorage = useObject('promptImgStorage');
31
+
32
+ $: isLoading = $myPresence?.status === Status.loading || $isRenderingCanvas || false;
33
+ function onShowModal(e: CustomEvent) {
34
+ if (isLoading) return;
35
+ showModal = e.detail.showModal;
36
+ if (showModal) {
37
+ myPresence.update({
38
+ status: Status.prompting
39
+ });
40
+ } else {
41
+ myPresence.update({
42
+ status: Status.ready
43
+ });
44
+ }
45
+ }
46
+
47
+ function onPaint() {
48
+ showModal = false;
49
+ generateImage();
50
+ }
51
+ function canPaint(position: { x: number; y: number }): boolean {
52
+ if (!$others) return true;
53
+ let canPaint = true;
54
+ for (const { presence } of $others) {
55
+ if (
56
+ position.x < presence.frame.x + FRAME_SIZE &&
57
+ position.x + FRAME_SIZE > presence.frame.x &&
58
+ position.y < presence.frame.y + FRAME_SIZE &&
59
+ position.y + FRAME_SIZE > presence.frame.y
60
+ ) {
61
+ // can paint if presence is only dragging
62
+ if (presence.status === Status.ready || presence.status === Status.dragging) {
63
+ canPaint = true;
64
+ continue;
65
+ }
66
+ canPaint = false;
67
+ break;
68
+ }
69
+ }
70
+ return canPaint;
71
+ }
72
+
73
+ function clearStateMsg(t = 5000) {
74
+ setTimeout(() => {
75
+ $loadingState = '';
76
+ }, t);
77
+ }
78
+ async function generateImage() {
79
+ if (isLoading) return;
80
+ const prompt = $myPresence.currentPrompt;
81
+ const position = $myPresence.frame;
82
+ $loadingState = 'Pending';
83
+ if (!canPaint(position)) {
84
+ $loadingState = 'Someone is already painting here';
85
+ myPresence.update({
86
+ status: Status.ready
87
+ });
88
+ clearStateMsg();
89
+ return;
90
+ }
91
+ const imageKey = getKey(position);
92
+ const room = $selectedRoomID || 'default';
93
+ console.log('Generating...', prompt, position);
94
+ myPresence.update({
95
+ status: Status.loading
96
+ });
97
+ const sessionHash = crypto.randomUUID();
98
+ const base64Crop = $maskEl.toDataURL('image/webp');
99
+
100
+ const hashpayload = {
101
+ fn_index: 0,
102
+ session_hash: sessionHash
103
+ };
104
+
105
+ const datapayload = {
106
+ data: [base64Crop, prompt, 0.75, 7.5, 40, 'patchmatch', room, imageKey]
107
+ };
108
+
109
+ const websocket = new WebSocket(PUBLIC_WS_INPAINTING);
110
+ // websocket.onopen = async function (event) {
111
+ // websocket.send(JSON.stringify({ hash: sessionHash }));
112
+ // };
113
+ websocket.onclose = (evt) => {
114
+ if (!evt.wasClean) {
115
+ $loadingState = 'Error';
116
+ myPresence.update({
117
+ status: Status.ready
118
+ });
119
+ }
120
+ };
121
+ websocket.onmessage = async function (event) {
122
+ try {
123
+ const data = JSON.parse(event.data);
124
+ $loadingState = '';
125
+ switch (data.msg) {
126
+ case 'send_hash':
127
+ websocket.send(JSON.stringify(hashpayload));
128
+ break;
129
+ case 'send_data':
130
+ $loadingState = 'Sending Data';
131
+ websocket.send(JSON.stringify({ ...hashpayload, ...datapayload }));
132
+ break;
133
+ case 'queue_full':
134
+ $loadingState = 'Queue full';
135
+ websocket.close();
136
+ myPresence.update({
137
+ status: Status.ready
138
+ });
139
+ return;
140
+ case 'estimation':
141
+ const { rank, queue_size } = data;
142
+ $loadingState = `On queue ${rank}/${queue_size}`;
143
+ break;
144
+ case 'process_generating':
145
+ $loadingState = data.success ? 'Generating' : 'Error';
146
+ break;
147
+ case 'process_completed':
148
+ try {
149
+ const params = data.output.data[0] as {
150
+ is_nsfw: boolean;
151
+ image: {
152
+ url: string;
153
+ filename: string;
154
+ };
155
+ };
156
+ const isNSWF = params.is_nsfw;
157
+ if (isNSWF) {
158
+ throw new Error('NFSW');
159
+ }
160
+ // const imgBlob = await base64ToBlob(imgBase64);
161
+ const promptImgParams = {
162
+ imgURL: params.image.filename
163
+ };
164
+ // const imgURL = await uploadImage(imgBlob, promptImgParams);
165
+ $promptImgStorage.set(imageKey, new LiveObject(promptImgParams));
166
+ // $promptImgStorage.set(imageKey, promptImgParams);
167
+
168
+ console.log(params.image.url);
169
+ $loadingState = data.success ? 'Complete' : 'Error';
170
+ clearStateMsg();
171
+ myPresence.update({
172
+ status: Status.ready,
173
+ currentPrompt: ''
174
+ });
175
+ } catch (err) {
176
+ const tError = err as Error;
177
+ $loadingState = tError?.message;
178
+ myPresence.update({
179
+ status: Status.ready
180
+ });
181
+ clearStateMsg(10000);
182
+ }
183
+ websocket.close();
184
+ return;
185
+ case 'process_starts':
186
+ $loadingState = 'Processing';
187
+ break;
188
+ }
189
+ } catch (e) {
190
+ console.error(e);
191
+ $loadingState = 'Error';
192
+ }
193
+ };
194
+ }
195
+ </script>
196
+
197
+ <!-- Show the current user's cursor location -->
198
+ <div class="text touch-none pointer-events-none">
199
+ {$loadingState}
200
+ </div>
201
+ {#if showModal}
202
+ <PromptModal
203
+ on:paint={onPaint}
204
+ initPrompt={$myPresence?.currentPrompt}
205
+ on:showModal={onShowModal}
206
+ />
207
+ {/if}
208
+ <div class="fixed top-0 left-0 z-0 w-screen h-screen min-h-[600px]">
209
+ <PaintCanvas />
210
+
211
+ <main class="z-10 relative">
212
+ <!-- When others connected, iterate through others and show their cursors -->
213
+ {#if $others}
214
+ {#each [...$others] as { connectionId, presence } (connectionId)}
215
+ {#if (presence?.status === Status.loading || presence?.status === Status.prompting || presence?.status === Status.masking) && presence?.frame}
216
+ <Frame
217
+ status={presence.status}
218
+ position={presence?.frame}
219
+ prompt={presence?.currentPrompt}
220
+ transform={$currZoomTransform}
221
+ />
222
+ {/if}
223
+ {#if presence?.cursor}
224
+ <Cursor
225
+ color={COLORS[1 + (connectionId % (COLORS.length - 1))]}
226
+ position={presence?.cursor}
227
+ transform={$currZoomTransform}
228
+ />
229
+ {/if}
230
+ {/each}
231
+ {/if}
232
+ <PaintFrame transform={$currZoomTransform} on:showModal={onShowModal} />
233
+ </main>
234
+ </div>
235
+ <div class="fixed bottom-0 md:bottom-16 left-0 right-0 z-10 my-2">
236
+ <Menu {isLoading} on:showModal={onShowModal} />
237
+ </div>
238
+
239
+ <style lang="postcss" scoped>
240
+ </style>
frontend/src/lib/Buttons/AboutButton.svelte ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { onMount } from 'svelte';
3
+ import ShareWithCommunity from './ShareWithCommunity.svelte';
4
+
5
+ export let isLoading = false;
6
+
7
+ let open = true;
8
+
9
+ onMount(() => {
10
+ autoOpen();
11
+ window.addEventListener('resize', autoOpen);
12
+ return () => {
13
+ window.removeEventListener('resize', autoOpen);
14
+ };
15
+ });
16
+ function autoOpen() {
17
+ open = window.innerWidth > 768;
18
+ }
19
+ </script>
20
+
21
+ <div class="md:w-[210px] text-sm md:flex flex-col order-last md:order-none">
22
+ <details {open}>
23
+ <summary class="md:hidden">More</summary>
24
+ <p class="inline">
25
+ Instructions: move the <span class="text-blue-700 bg-blue-300/60 px-0.5">blue square</span>,
26
+ click "🖍 Paint".
27
+ </p>
28
+ <div class="flex justify-between py-2 gap-3">
29
+ <button on:click class="items-center inline-flex">
30
+ <svg width=".9em" height=".9em" viewBox="0 0 24 24" class="mr-1"
31
+ ><path
32
+ fill="currentColor"
33
+ fill-rule="evenodd"
34
+ d="M12 2C6.477 2 2 6.477 2 12s4.477 10 10 10s10-4.477 10-10S17.523 2 12 2zm0 7a1 1 0 0 0-1 1a1 1 0 1 1-2 0a3 3 0 1 1 4.44 2.633a1.404 1.404 0 0 0-.383.288a.303.303 0 0 0-.057.085v.494a1 1 0 1 1-2 0V13c0-.58.253-1.047.539-1.38c.281-.33.63-.572.94-.742A1 1 0 0 0 12 9zm.999 4.011v-.004v.005zM12 15a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2H12z"
35
+ clip-rule="evenodd"
36
+ /></svg
37
+ >
38
+ About
39
+ </button>
40
+ <ShareWithCommunity {isLoading} />
41
+ </div>
42
+ </details>
43
+ </div>
frontend/src/lib/Buttons/DragButton.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Move from '$lib/Icons/Move.svelte';
3
+ export let isActive = false;
4
+ export let isLoading = false;
5
+ export let className = '';
6
+ </script>
7
+
8
+ <button
9
+ on:click
10
+ disabled={isLoading}
11
+ class="{className} shadow-lg bg-white rounded-full disabled:opacity-50 {isActive
12
+ ? 'text-blue-700'
13
+ : 'text-gray-800'}"
14
+ title="Drag to Move"
15
+ >
16
+ <Move />
17
+ </button>
18
+
19
+ <style lang="postcss" scoped>
20
+ </style>
frontend/src/lib/Buttons/MaskButton.svelte ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Mask from '$lib/Icons/Mask.svelte';
3
+ export let isActive = false;
4
+ export let isLoading = false;
5
+ export let className = '';
6
+ </script>
7
+
8
+ <button
9
+ on:click
10
+ disabled={isLoading}
11
+ class="{className} rounded-full disabled:opacity-50 {isActive ? 'text-white' : 'text-blue-200'}"
12
+ title="Draw to Mask"
13
+ >
14
+ <Mask classList="text-3xl" />
15
+ </button>
16
+
17
+ <style lang="postcss" scoped>
18
+ </style>
frontend/src/lib/Buttons/PPButton.svelte ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let isLoading = false;
3
+ </script>
4
+
5
+ <button
6
+ on:click
7
+ disabled={isLoading}
8
+ class="button-paint {isLoading ? 'cursor-wait' : 'cursor-pointer'}"
9
+ title="Prompt and Paint"
10
+ >
11
+ {#if isLoading}
12
+ <span>painting... </span>{:else}
13
+ <span
14
+ class="rounded-sm h-4 w-4 flex justify-center items-center border-2 border-dashed border-violet-700 mr-2"
15
+ >
16
+ +
17
+ </span>
18
+ <span>Prompt + Paint</span>
19
+ {/if}
20
+ </button>
21
+
22
+ <style lang="postcss" scoped>
23
+ /* .button {
24
+ @apply disabled:opacity-50 dark:bg-white dark:text-black bg-black text-white rounded-2xl text-xs shadow-sm focus:outline-none focus:border-gray-400;
25
+ } */
26
+ .button-paint {
27
+ @apply text-xs md:text-sm font-mono bg-blue-100 text-blue-900 min-w-[25ch] flex justify-center items-center disabled:opacity-50 rounded-xl px-3 py-1 shadow-sm focus:outline-none focus:border-gray-400;
28
+ }
29
+ </style>
frontend/src/lib/Buttons/RoomsSelector.svelte ADDED
@@ -0,0 +1,136 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { page } from '$app/stores';
3
+ import Room from '$lib/Icons/Room.svelte';
4
+ import Pin from '$lib/Icons/Pin.svelte';
5
+ import People from '$lib/Icons/People.svelte';
6
+ import LoadingIcon from '$lib/Icons/LoadingIcon.svelte';
7
+ import { onMount } from 'svelte';
8
+ import { selectedRoomID } from '$lib/store';
9
+ import { MAX_CAPACITY } from '$lib/constants';
10
+ import { useRooms } from '$lib/liveblocks';
11
+ import type { RoomResponse } from '$lib/types';
12
+
13
+ export let isLoading = false;
14
+ let boxEl: HTMLElement;
15
+
16
+ const rooms = useRooms();
17
+
18
+ let collapsed = true;
19
+ $: selectedRoom = $rooms.find((room) => room.room_id === $selectedRoomID);
20
+ $: loadingRooms = $rooms.length > 0;
21
+
22
+ function clickHandler(event: Event) {
23
+ if (boxEl && !boxEl.contains(event.target as Node)) {
24
+ collapsed = true;
25
+ }
26
+ }
27
+ onMount(() => {
28
+ window.addEventListener('pointerdown', clickHandler, true);
29
+ return () => {
30
+ window.removeEventListener('pointerdown', clickHandler, true);
31
+ };
32
+ });
33
+
34
+ function changeRoom(room: RoomResponse) {
35
+ $selectedRoomID = room.room_id;
36
+ collapsed = true;
37
+ $page.url.searchParams.set('roomid', room.room_id);
38
+ window.location.search = `?${$page.url.searchParams.toString()}`;
39
+ window.parent.postMessage({ queryString: `?${$page.url.searchParams.toString()}` }, '*');
40
+ }
41
+ </script>
42
+
43
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
44
+ <div class="min-w-[20ch]">
45
+ {#if loadingRooms && selectedRoom}
46
+ <div
47
+ class="text-sm rounded-2xl md:text-smtext-gray-700 py-1 font-medium tracking-tight relative ring-1 ring-blue-500 px-2"
48
+ title="Choose a different room"
49
+ bind:this={boxEl}
50
+ >
51
+ {#if !collapsed}
52
+ <div
53
+ class="absolute left-0 right-0 bottom-full rounded-xl bg-blue-600 px-1 overflow-hidden z-0"
54
+ >
55
+ <ul class="relative overflow-hidden overflow-y-scroll max-h-72 w-full x-scroll">
56
+ <li class="grid-row gap-2 pb-2 sticky top-0 py-2 bg-blue-600 font-semibold">
57
+ <Room />
58
+ <span> room </span>
59
+ <span />
60
+ <People />
61
+ <span> players </span>
62
+ </li>
63
+ {#each $rooms as room}
64
+ <li>
65
+ <!-- svelte-ignore a11y-invalid-attribute -->
66
+ <a
67
+ href="#"
68
+ on:click|preventDefault={() => changeRoom(room)}
69
+ class="grid-row gap-2 hover:bg-gray-300
70
+ {room.room_id === $selectedRoomID ? 'text-black' : ''}"
71
+ >
72
+ <span>
73
+ {#if room.room_id === $selectedRoomID}
74
+ <Pin />
75
+ {/if}
76
+ </span>
77
+ <span>{room.room_id} </span>
78
+ <span />
79
+ <span />
80
+ <span>{room.users_count} / {MAX_CAPACITY}</span>
81
+ </a>
82
+ </li>
83
+ {/each}
84
+ </ul>
85
+ <div class="border-t-2 border-t-gray-400 border-opacity-50" />
86
+ </div>
87
+ {/if}
88
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
89
+ <div
90
+ class={isLoading ? 'cursor-wait' : 'cursor-pointer'}
91
+ on:click={() => (isLoading ? null : (collapsed = !collapsed))}
92
+ >
93
+ {#if selectedRoom}
94
+ <div class="grid-row gap-2">
95
+ <Room />
96
+ <span>
97
+ {selectedRoom?.room_id}
98
+ </span>
99
+ <span />
100
+ <People />
101
+ <span>
102
+ {selectedRoom?.users_count} / {MAX_CAPACITY}
103
+ </span>
104
+ </div>
105
+ {:else}
106
+ <div class="grid-row gap-2">
107
+ <Room />
108
+ <span>
109
+ Loading...
110
+ <People />
111
+ <span> ... / ... </span>
112
+ </span>
113
+ </div>
114
+ {/if}
115
+ </div>
116
+ </div>
117
+ {:else}
118
+ <div
119
+ class="bg-gradient-to-r from-transparent via-blue-900/20 to-transparent py-1.5 text-blue-700 rounded-full flex justify-center items-center"
120
+ >
121
+ <LoadingIcon classList="animate-spin mr-2 text-sm" /> loading rooms
122
+ </div>
123
+ {/if}
124
+ </div>
125
+
126
+ <style lang="postcss" scoped>
127
+ .grid-row {
128
+ display: grid;
129
+ grid-template-columns: 0.5fr 2fr 1fr 0.5fr 2fr;
130
+ align-items: center;
131
+ justify-items: flex-start;
132
+ }
133
+ .grid-row span {
134
+ white-space: nowrap;
135
+ }
136
+ </style>
frontend/src/lib/Buttons/ShareWithCommunity.svelte ADDED
@@ -0,0 +1,80 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import IconCommunity from '$lib/Icons/IconCommunity.svelte';
3
+ import LoadingIcon from '$lib/Icons/LoadingIcon.svelte';
4
+ import { uploadImage } from '$lib/utils';
5
+ import { canvasEl, selectedRoomID } from '$lib/store';
6
+ import { nanoid } from 'nanoid';
7
+
8
+ export let isLoading = false;
9
+
10
+ let isUploading: boolean = false;
11
+ async function handleClick() {
12
+ if (isUploading) {
13
+ return;
14
+ }
15
+ const blob: Blob = await new Promise((resolve) => {
16
+ $canvasEl.toBlob(resolve as BlobCallback, 'image/jpeg', 0.95);
17
+ });
18
+ isUploading = true;
19
+
20
+ await createCommunityPost(blob);
21
+
22
+ isUploading = false;
23
+ }
24
+
25
+ async function createCommunityPost(canvasBlob: Blob) {
26
+ let canvasURL: {
27
+ url: string;
28
+ filename: string;
29
+ } | null = null;
30
+ try {
31
+ canvasURL = await uploadImage(canvasBlob, {
32
+ prompt: 'canvas',
33
+ position: { x: 0, y: 0 },
34
+ date: new Date().getTime(),
35
+ id: nanoid(),
36
+ room: $selectedRoomID || 'default'
37
+ });
38
+ } catch (err) {
39
+ console.error(err);
40
+ }
41
+ if (!canvasURL) {
42
+ console.error('Failed to upload image');
43
+ return;
44
+ }
45
+ const canvasImage = `<img src="${canvasURL.url}" style="width:100%" width="1000" height="1000">`;
46
+ const descriptionMd = `#### Stable Diffusion Multiplayer:
47
+ ### [${$selectedRoomID}](https://huggingface.co/spaces/huggingface-projects/stable-diffusion-multiplayer?roomid=${$selectedRoomID})
48
+
49
+ <div style="display: flex; overflow: scroll; column-gap: 0.75rem;">
50
+ ${canvasImage}
51
+ </div>`;
52
+
53
+ const params = new URLSearchParams({
54
+ title: `Room ${$selectedRoomID}`,
55
+ description: descriptionMd
56
+ });
57
+ const paramsStr = params.toString();
58
+ window.open(
59
+ `https://huggingface.co/spaces/huggingface-projects/stable-diffusion-multiplayer/discussions/new?${paramsStr}`,
60
+ '_blank'
61
+ );
62
+ }
63
+ </script>
64
+
65
+ <!-- svelte-ignore a11y-click-events-have-key-events -->
66
+ <button
67
+ class="{isLoading || isUploading
68
+ ? 'cursor-wait'
69
+ : 'cursor-pointer'} text-xs font-mono flex items-center justify-center bg-black gap-x-1 rounded-xl px-2 py-1 whitespace-nowrap"
70
+ on:click={handleClick}
71
+ disabled={isUploading || isLoading}
72
+ title="Share with community"
73
+ >
74
+ {#if isUploading}
75
+ <LoadingIcon classList={'animate-spin max-w-[25px] text-white'} />
76
+ {:else}
77
+ <IconCommunity />
78
+ {/if}
79
+ <p class="text-white font-semibold">Share to community</p>
80
+ </button>
frontend/src/lib/Buttons/UndoButton.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import Undo from '$lib/Icons/Undo.svelte';
3
+ export let isActive = false;
4
+ export let isLoading = false;
5
+ export let className = '';
6
+ </script>
7
+
8
+ <button
9
+ on:click
10
+ disabled={isLoading}
11
+ class="{className} bg-white rounded-full disabled:opacity-50 bg-white rounded-full {isActive
12
+ ? 'text-blue-700'
13
+ : 'text-gray-800'}"
14
+ title="Clear Masking"
15
+ >
16
+ <Undo />
17
+ </button>
18
+
19
+ <style lang="postcss" scoped>
20
+ </style>
frontend/src/lib/Cursor.svelte ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import type { ZoomTransform } from 'd3-zoom';
3
+ import Cursor from '$lib/Icons/Cursor.svelte';
4
+ export let transform: ZoomTransform;
5
+ export let color = '';
6
+ export let position = { x: 0, y: 0 };
7
+
8
+ $: coord = {
9
+ x: transform.applyX(position.x),
10
+ y: transform.applyY(position.y)
11
+ };
12
+ </script>
13
+
14
+ <div
15
+ class="cursor text-4xl"
16
+ style={`transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k});`}
17
+ >
18
+ <Cursor classList={'block z-0 col-span-2 row-span-2 text-8xl'} fill={color} />
19
+ <!--
20
+ {#if emoji}
21
+ <div
22
+ class="absolute right-0 col-start-2 row-start-2 text-8xl"
23
+ style={`text-shadow: 0px 5px 5px ${color}`}
24
+ >
25
+ {emoji}
26
+ </div>
27
+ {/if} -->
28
+ </div>
29
+
30
+ <style lang="postcss" scoped>
31
+ .cursor {
32
+ @apply absolute top-0 left-0 grid grid-cols-3 touch-none pointer-events-none;
33
+ transform-origin: 0 0;
34
+ }
35
+ </style>
frontend/src/lib/Frame.svelte ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import LoadingIcon from '$lib/Icons/LoadingIcon.svelte';
3
+ import { FRAME_SIZE } from '$lib/constants';
4
+ import type { ZoomTransform } from 'd3-zoom';
5
+ import { Status } from '$lib/types';
6
+
7
+ export let transform: ZoomTransform;
8
+ export let position = { x: 0, y: 0 };
9
+ export let prompt = '';
10
+ export let status: Status;
11
+
12
+ $: coord = {
13
+ x: transform.applyX(position.x),
14
+ y: transform.applyY(position.y)
15
+ };
16
+ </script>
17
+
18
+ <div
19
+ class="absolute top-0 left-0 border-8 border-dashed border-black flex items-center justify-center bg-black/60"
20
+ style={`width: ${FRAME_SIZE}px;
21
+ height: ${FRAME_SIZE}px;
22
+ transform: translateX(${coord.x}px) translateY(${coord.y}px) scale(${transform.k}); transform-origin: 0 0;`}
23
+ >
24
+ <div class="pointer-events-none touch-none">
25
+ <div class="font-bold !text-4xl text-white rounded-2xl text-center p-10">
26
+ {#if status === Status.loading || status === Status.processing}
27
+ <LoadingIcon classList={'animate-spin text-4xl inline mr-2 mx-auto text-white mb-4'} />
28
+ <p class="text-4xl">Someone is painting:</p>
29
+ {:else if status === Status.masking}
30
+ <p class="text-4xl">Someone is masking</p>
31
+ {:else if status === Status.prompting}
32
+ <p class="text-4xl">Someone is typing:</p>
33
+ {/if}
34
+ {#if prompt}
35
+ <span class="italic font-normal line-clamp-4">"{prompt}"</span>
36
+ {/if}
37
+ </div>
38
+ </div>
39
+ </div>
frontend/src/lib/Icons/Cursor.svelte ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ export let fill = '';
4
+ </script>
5
+
6
+ <svg
7
+ class={classList}
8
+ width="1em"
9
+ height="1em"
10
+ viewBox="0 0 12 12"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M3.09022 1.77278C2.25499 1.40456 1.40358 2.25597 1.7718 3.0912L4.57875 9.45818C4.95365 10.3086 6.18455 10.2323 6.4516 9.34213L6.9637 7.63515C7.06029 7.31317 7.31219 7.06126 7.63417 6.96467L9.34117 6.45258C10.2313 6.18552 10.3076 4.95463 9.45722 4.57972L3.09022 1.77278Z"
15
+ {fill}
16
+ />
17
+ <path
18
+ fill-rule="evenodd"
19
+ clip-rule="evenodd"
20
+ d="M3.66305 9.86091L0.856101 3.49392C0.119663 1.82347 1.82248 0.120639 3.49294 0.857077L9.85994 3.66402C11.5607 4.41383 11.4082 6.87562 9.62784 7.40973L7.92085 7.92182L7.40875 9.62881C6.87465 11.4092 4.41286 11.5617 3.66305 9.86091ZM1.77113 3.09053C1.40291 2.2553 2.25432 1.40388 3.08955 1.7721L9.45654 4.57905C10.3069 4.95395 10.2307 6.18485 9.34049 6.4519L7.6335 6.964C7.31152 7.06059 7.05961 7.3125 6.96302 7.63448L6.45093 9.34146C6.18387 10.2316 4.95298 10.3079 4.57807 9.45751L1.77113 3.09053Z"
21
+ fill="white"
22
+ fill-opacity="0.8"
23
+ />
24
+ </svg>
frontend/src/lib/Icons/IconCommunity.svelte ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classNames = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classNames}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ focusable="false"
11
+ role="img"
12
+ width="1em"
13
+ height="1em"
14
+ preserveAspectRatio="xMidYMid meet"
15
+ viewBox="0 0 32 32"
16
+ >
17
+ <path
18
+ d="M20.6081 3C21.7684 3 22.8053 3.49196 23.5284 4.38415C23.9756 4.93678 24.4428 5.82749 24.4808 7.16133C24.9674 7.01707 25.4353 6.93643 25.8725 6.93643C26.9833 6.93643 27.9865 7.37587 28.696 8.17411C29.6075 9.19872 30.0124 10.4579 29.8361 11.7177C29.7523 12.3177 29.5581 12.8555 29.2678 13.3534C29.8798 13.8646 30.3306 14.5763 30.5485 15.4322C30.719 16.1032 30.8939 17.5006 29.9808 18.9403C30.0389 19.0342 30.0934 19.1319 30.1442 19.2318C30.6932 20.3074 30.7283 21.5229 30.2439 22.6548C29.5093 24.3704 27.6841 25.7219 24.1397 27.1727C21.9347 28.0753 19.9174 28.6523 19.8994 28.6575C16.9842 29.4379 14.3477 29.8345 12.0653 29.8345C7.87017 29.8345 4.8668 28.508 3.13831 25.8921C0.356375 21.6797 0.754104 17.8269 4.35369 14.1131C6.34591 12.058 7.67023 9.02782 7.94613 8.36275C8.50224 6.39343 9.97271 4.20438 12.4172 4.20438H12.4179C12.6236 4.20438 12.8314 4.2214 13.0364 4.25468C14.107 4.42854 15.0428 5.06476 15.7115 6.02205C16.4331 5.09583 17.134 4.359 17.7682 3.94323C18.7242 3.31737 19.6794 3 20.6081 3ZM20.6081 5.95917C20.2427 5.95917 19.7963 6.1197 19.3039 6.44225C17.7754 7.44319 14.8258 12.6772 13.7458 14.7131C13.3839 15.3952 12.7655 15.6837 12.2086 15.6837C11.1036 15.6837 10.2408 14.5497 12.1076 13.1085C14.9146 10.9402 13.9299 7.39584 12.5898 7.1776C12.5311 7.16799 12.4731 7.16355 12.4172 7.16355C11.1989 7.16355 10.6615 9.33114 10.6615 9.33114C10.6615 9.33114 9.0863 13.4148 6.38031 16.206C3.67434 18.998 3.5346 21.2388 5.50675 24.2246C6.85185 26.2606 9.42666 26.8753 12.0653 26.8753C14.8021 26.8753 17.6077 26.2139 19.1799 25.793C19.2574 25.7723 28.8193 22.984 27.6081 20.6107C27.4046 20.212 27.0693 20.0522 26.6471 20.0522C24.9416 20.0522 21.8393 22.6726 20.5057 22.6726C20.2076 22.6726 19.9976 22.5416 19.9116 22.222C19.3433 20.1173 28.552 19.2325 27.7758 16.1839C27.639 15.6445 27.2677 15.4256 26.746 15.4263C24.4923 15.4263 19.4358 19.5181 18.3759 19.5181C18.2949 19.5181 18.2368 19.4937 18.2053 19.4419C17.6743 18.557 17.9653 17.9394 21.7082 15.6009C25.4511 13.2617 28.0783 11.8545 26.5841 10.1752C26.4121 9.98141 26.1684 9.8956 25.8725 9.8956C23.6001 9.89634 18.2311 14.9403 18.2311 14.9403C18.2311 14.9403 16.7821 16.496 15.9057 16.496C15.7043 16.496 15.533 16.4139 15.4169 16.2112C14.7956 15.1296 21.1879 10.1286 21.5484 8.06535C21.7928 6.66715 21.3771 5.95917 20.6081 5.95917Z"
19
+ fill="#FF9D00"
20
+ />
21
+ <path
22
+ d="M5.50686 24.2246C3.53472 21.2387 3.67446 18.9979 6.38043 16.206C9.08641 13.4147 10.6615 9.33111 10.6615 9.33111C10.6615 9.33111 11.2499 6.95933 12.59 7.17757C13.93 7.39581 14.9139 10.9401 12.1069 13.1084C9.29997 15.276 12.6659 16.7489 13.7459 14.713C14.8258 12.6772 17.7747 7.44316 19.304 6.44221C20.8326 5.44128 21.9089 6.00204 21.5484 8.06532C21.188 10.1286 14.795 15.1295 15.4171 16.2118C16.0391 17.2934 18.2312 14.9402 18.2312 14.9402C18.2312 14.9402 25.0907 8.49588 26.5842 10.1752C28.0776 11.8545 25.4512 13.2616 21.7082 15.6008C17.9646 17.9393 17.6744 18.557 18.2054 19.4418C18.7372 20.3266 26.9998 13.1351 27.7759 16.1838C28.5513 19.2324 19.3434 20.1173 19.9117 22.2219C20.48 24.3274 26.3979 18.2382 27.6082 20.6107C28.8193 22.9839 19.2574 25.7722 19.18 25.7929C16.0914 26.62 8.24723 28.3726 5.50686 24.2246Z"
23
+ fill="#FFD21E"
24
+ />
25
+ </svg>
frontend/src/lib/Icons/LiveBlocks.svelte ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ width="123"
8
+ height="44"
9
+ viewBox="0 0 123 44"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <rect
14
+ opacity="0.5"
15
+ x="0.25"
16
+ y="0.25"
17
+ width="122.5"
18
+ height="43.5"
19
+ rx="6.75"
20
+ fill="#1F2937"
21
+ stroke="#3A4C64"
22
+ stroke-width="0.5"
23
+ />
24
+ <path
25
+ d="M44.9102 16.8569H45.8514V14.778H47.1691C48.364 14.778 49.1497 14.0577 49.1497 12.9528C49.1497 11.8397 48.364 11.1277 47.1691 11.1277H44.9102V16.8569ZM47.0954 11.9461C47.7666 11.9461 48.1921 12.3308 48.1921 12.9528C48.1921 13.5667 47.7584 13.9595 47.0872 13.9595H45.8514V11.9461H47.0954Z"
26
+ fill="#A3BAD9"
27
+ />
28
+ <path
29
+ d="M51.5649 16.9059C52.8417 16.9059 53.701 16.0465 53.701 14.7615C53.701 13.4847 52.8417 12.6172 51.5649 12.6172C50.2881 12.6172 49.4287 13.4847 49.4287 14.7615C49.4287 16.0465 50.2881 16.9059 51.5649 16.9059ZM51.5649 16.1529C50.8201 16.1529 50.3372 15.5718 50.3372 14.7615C50.3372 13.9513 50.8201 13.3702 51.5649 13.3702C52.3097 13.3702 52.8007 13.9513 52.8007 14.7615C52.8007 15.5718 52.3097 16.1529 51.5649 16.1529Z"
30
+ fill="#A3BAD9"
31
+ />
32
+ <path
33
+ d="M55.233 16.857H56.1906L57.1073 14.0088L57.9912 16.857H58.9406L60.332 12.6665H59.4317L58.4414 15.8748L57.5247 12.6665H56.7144L55.7568 15.8748L54.7992 12.6665H53.8662L55.233 16.857Z"
34
+ fill="#A3BAD9"
35
+ />
36
+ <path
37
+ d="M62.6424 16.9059C63.6818 16.9059 64.3857 16.2757 64.533 15.449H63.6491C63.5263 15.9156 63.1335 16.1856 62.5933 16.1856C61.8976 16.1856 61.4475 15.7191 61.4229 15.0071V14.9498H64.5821C64.5985 14.8352 64.6067 14.7206 64.6067 14.6142C64.5821 13.3865 63.7719 12.6172 62.5605 12.6172C61.3247 12.6172 60.498 13.4766 60.498 14.7697C60.498 16.0547 61.3247 16.9059 62.6424 16.9059ZM61.4556 14.3032C61.5211 13.7057 62.004 13.3292 62.5687 13.3292C63.1826 13.3292 63.6164 13.6812 63.69 14.3032H61.4556Z"
38
+ fill="#A3BAD9"
39
+ />
40
+ <path
41
+ d="M67.4411 12.6665C66.7536 12.6665 66.418 12.9448 66.2052 13.2967L66.107 12.6665H65.3213V16.857H66.2052V14.7536C66.2052 14.107 66.4589 13.4931 67.2447 13.4931H67.6375V12.6665H67.4411Z"
42
+ fill="#A3BAD9"
43
+ />
44
+ <path
45
+ d="M69.9725 16.9059C71.0119 16.9059 71.7158 16.2757 71.8631 15.449H70.9792C70.8564 15.9156 70.4635 16.1856 69.9234 16.1856C69.2277 16.1856 68.7775 15.7191 68.753 15.0071V14.9498H71.9122C71.9286 14.8352 71.9368 14.7206 71.9368 14.6142C71.9122 13.3865 71.1019 12.6172 69.8906 12.6172C68.6548 12.6172 67.8281 13.4766 67.8281 14.7697C67.8281 16.0547 68.6548 16.9059 69.9725 16.9059ZM68.7857 14.3032C68.8512 13.7057 69.3341 13.3292 69.8988 13.3292C70.5127 13.3292 70.9464 13.6812 71.0201 14.3032H68.7857Z"
46
+ fill="#A3BAD9"
47
+ />
48
+ <path
49
+ d="M75.8352 13.2475C75.5651 12.8792 75.1149 12.6173 74.4765 12.6173C73.3391 12.6173 72.4961 13.4685 72.4961 14.7616C72.4961 16.1039 73.3391 16.906 74.4765 16.906C75.1395 16.906 75.5733 16.5949 75.8433 16.2266L75.9497 16.8569H76.7191V11.1277H75.8352V13.2475ZM74.6238 16.1612C73.8875 16.1612 73.4046 15.5883 73.4046 14.7616C73.4046 13.9432 73.8875 13.3702 74.6238 13.3702C75.3605 13.3702 75.8433 13.9432 75.8433 14.778C75.8433 15.5883 75.3605 16.1612 74.6238 16.1612Z"
50
+ fill="#A3BAD9"
51
+ />
52
+ <path
53
+ d="M81.9193 12.6173C81.2727 12.6173 80.839 12.9119 80.5607 13.2884V11.1277H79.6768V16.8569H80.4461L80.5525 16.2512C80.8144 16.6277 81.2646 16.906 81.9193 16.906C83.0652 16.906 83.9 16.0466 83.9 14.7616C83.9 13.4112 83.0652 12.6173 81.9193 12.6173ZM81.772 16.1612C81.0354 16.1612 80.5525 15.5801 80.5525 14.7534C80.5525 13.9432 81.0354 13.3702 81.772 13.3702C82.5086 13.3702 82.9997 13.9432 82.9997 14.7616C82.9997 15.5883 82.5086 16.1612 81.772 16.1612Z"
54
+ fill="#A3BAD9"
55
+ />
56
+ <path
57
+ d="M85.6277 16.8815L85.4641 17.2908C85.3331 17.6345 85.2513 17.7164 84.9075 17.7164H84.2855V18.4939H85.2349C85.8487 18.4939 86.0288 18.1911 86.2907 17.5117L88.1895 12.6665H87.2483L86.1106 15.8503L84.9402 12.6665H83.999L85.6277 16.8815Z"
58
+ fill="#A3BAD9"
59
+ />
60
+ <path d="M45.1162 32.4821H47.2144V22.0654H45.1162V32.4821Z" fill="#A3BAD9" />
61
+ <path
62
+ d="M49.7329 24.0148C50.4175 24.0148 50.9532 23.5088 50.9532 22.8392C50.9532 22.1546 50.4175 21.6487 49.7329 21.6487C49.0484 21.6487 48.5127 22.1546 48.5127 22.8392C48.5127 23.5088 49.0484 24.0148 49.7329 24.0148ZM48.6764 32.482H50.7895V24.8332H48.6764V32.482Z"
63
+ fill="#A3BAD9"
64
+ />
65
+ <path
66
+ d="M54.2461 32.4823H56.8503L59.6033 24.8335H57.4009L55.5854 30.3394L53.7402 24.8335H51.4932L54.2461 32.4823Z"
67
+ fill="#A3BAD9"
68
+ />
69
+ <path
70
+ d="M63.718 32.5713C65.7567 32.5713 67.0811 31.3808 67.3192 29.8927H65.2507C65.0722 30.607 64.4769 30.979 63.6287 30.979C62.5722 30.979 61.8728 30.3243 61.8281 29.2379V29.1338H67.3787C67.4234 28.8957 67.4382 28.6427 67.4382 28.4195C67.4085 26.1725 65.8609 24.7439 63.5692 24.7439C61.2031 24.7439 59.6406 26.2915 59.6406 28.6725C59.6406 31.0385 61.1734 32.5713 63.718 32.5713ZM61.8728 27.7647C61.9918 26.857 62.6763 26.3064 63.5841 26.3064C64.5365 26.3064 65.1763 26.8272 65.3251 27.7647H61.8728Z"
71
+ fill="#A3BAD9"
72
+ />
73
+ <path
74
+ d="M72.9898 24.744C71.9332 24.744 71.1891 25.1904 70.6832 25.8005V22.0654H68.585V32.4821H70.4153L70.6237 31.4404C71.1148 32.0952 71.8737 32.5714 72.9749 32.5714C75.0434 32.5714 76.5315 31.0089 76.5315 28.6428C76.5315 26.2172 75.0434 24.744 72.9898 24.744ZM72.5136 30.8601C71.3678 30.8601 70.6534 29.9523 70.6534 28.6428C70.6534 27.3482 71.3678 26.4553 72.5136 26.4553C73.6594 26.4553 74.4035 27.3482 74.4035 28.6577C74.4035 29.9672 73.6594 30.8601 72.5136 30.8601Z"
75
+ fill="#A3BAD9"
76
+ />
77
+ <path d="M77.6973 32.4821H79.7955V22.0654H77.6973V32.4821Z" fill="#A3BAD9" />
78
+ <path
79
+ d="M84.9927 32.5713C87.4332 32.5713 89.0254 31.0237 89.0254 28.6576C89.0254 26.3064 87.4332 24.7439 84.9927 24.7439C82.5522 24.7439 80.96 26.3064 80.96 28.6576C80.96 31.0088 82.5522 32.5713 84.9927 32.5713ZM84.9927 30.86C83.8171 30.86 83.1028 29.9522 83.1028 28.6576C83.1028 27.3629 83.8171 26.4552 84.9927 26.4552C86.1683 26.4552 86.8975 27.3629 86.8975 28.6576C86.8975 29.9522 86.1683 30.86 84.9927 30.86Z"
80
+ fill="#A3BAD9"
81
+ />
82
+ <path
83
+ d="M93.7816 32.5713C95.865 32.5713 97.2489 31.4254 97.5465 29.5951H95.3441C95.1953 30.3689 94.6447 30.86 93.8263 30.86C92.7251 30.86 92.0257 29.9671 92.0257 28.6576C92.0257 27.3629 92.7251 26.4552 93.8263 26.4552C94.6447 26.4552 95.1655 26.9314 95.3441 27.6903H97.5316C97.234 25.8748 95.9394 24.7439 93.856 24.7439C91.4304 24.7439 89.8828 26.2915 89.8828 28.6576C89.8828 31.0683 91.356 32.5713 93.7816 32.5713Z"
84
+ fill="#A3BAD9"
85
+ />
86
+ <path
87
+ d="M98.6514 32.4821H100.75V30.622L101.895 29.3869L103.83 32.4821H106.256L103.354 27.8392L106.107 24.8333H103.741L100.75 28.0773V22.0654H98.6514V32.4821Z"
88
+ fill="#A3BAD9"
89
+ />
90
+ <path
91
+ d="M106.188 29.9375C106.277 31.5744 107.72 32.5714 109.848 32.5714C111.902 32.5714 113.33 31.6042 113.33 30.0863C113.33 28.3601 111.872 27.9583 110.116 27.7798C109.015 27.6458 108.36 27.5863 108.36 27.006C108.36 26.5149 108.896 26.2173 109.714 26.2173C110.562 26.2173 111.143 26.5893 111.202 27.1994H113.211C113.107 25.6369 111.693 24.7292 109.64 24.7292C107.676 24.7143 106.351 25.7113 106.351 27.2292C106.351 28.8065 107.735 29.2083 109.521 29.4167C110.756 29.5804 111.277 29.625 111.277 30.25C111.277 30.7857 110.741 31.0685 109.863 31.0685C108.836 31.0685 108.256 30.6071 108.182 29.9375H106.188Z"
92
+ fill="#A3BAD9"
93
+ />
94
+ <path
95
+ d="M32.4824 8.67236H11.649C10.0053 8.67236 8.67285 10.0048 8.67285 11.6486V32.4819C8.67285 34.1256 10.0053 35.4581 11.649 35.4581H32.4824C34.1261 35.4581 35.4586 34.1256 35.4586 32.4819V11.6486C35.4586 10.0048 34.1261 8.67236 32.4824 8.67236Z"
96
+ fill="url(#paint0_radial_253_107)"
97
+ />
98
+ <path
99
+ fill-rule="evenodd"
100
+ clip-rule="evenodd"
101
+ d="M24.8953 17.78H15.0332L17.8712 20.7599V24.875L24.8953 17.78ZM19.2193 26.294H29.0814L26.2434 23.3141V19.199L19.2193 26.294Z"
102
+ fill="white"
103
+ />
104
+ <defs>
105
+ <radialGradient
106
+ id="paint0_radial_253_107"
107
+ cx="0"
108
+ cy="0"
109
+ r="1"
110
+ gradientUnits="userSpaceOnUse"
111
+ gradientTransform="translate(4.7524 1.97593) rotate(45.3703) scale(40.7748 35.4976)"
112
+ >
113
+ <stop stop-color="#FF0099" />
114
+ <stop offset="1" stop-color="#FF7A00" />
115
+ </radialGradient>
116
+ </defs>
117
+ </svg>
frontend/src/lib/Icons/LoadingIcon.svelte ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ xmlns="http://www.w3.org/2000/svg"
8
+ xmlns:xlink="http://www.w3.org/1999/xlink"
9
+ aria-hidden="true"
10
+ fill="none"
11
+ focusable="false"
12
+ role="img"
13
+ width="1em"
14
+ height="1em"
15
+ preserveAspectRatio="xMidYMid meet"
16
+ viewBox="0 0 24 24"
17
+ >
18
+ <circle class="opacity-50" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" />
19
+ <path
20
+ fill="currentColor"
21
+ d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
22
+ />
23
+ </svg>
frontend/src/lib/Icons/Mask.svelte ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="-2 -2 14 14"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M3.07066 9.3043L8.07078 4.30418L5.69582 1.92922L0.695701 6.92934C0.626865 6.99826 0.577967 7.08453 0.554193 7.17899L0 10L2.82047 9.44581C2.91516 9.42213 3.00179 9.37317 3.07066 9.3043V9.3043ZM9.68493 2.69003C9.88667 2.48823 10 2.21457 10 1.92922C10 1.64388 9.88667 1.37022 9.68493 1.16842L8.83158 0.315069C8.62978 0.11333 8.35612 0 8.07078 0C7.78543 0 7.51177 0.11333 7.30997 0.315069L6.45662 1.16842L8.83158 3.54338L9.68493 2.69003Z"
15
+ fill="currentColor"
16
+ />
17
+ </svg>
frontend/src/lib/Icons/Move.svelte ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ width="1em"
8
+ height="1em"
9
+ viewBox="0 0 11 11"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ d="M2.4 4.26667L1 5.66667L2.4 7.06667M4.26667 2.4L5.66667 1L7.06667 2.4M7.06667 8.93333L5.66667 10.3333L4.26667 8.93333M8.93333 4.26667L10.3333 5.66667L8.93333 7.06667M1 5.66667H10.3333M5.66667 1V10.3333"
15
+ stroke="currentColor"
16
+ stroke-width="0.933333"
17
+ stroke-linecap="round"
18
+ stroke-linejoin="round"
19
+ />
20
+ </svg>
frontend/src/lib/Icons/People.svelte ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ height="1em"
8
+ width="1em"
9
+ viewBox="0 0 9 7"
10
+ fill="none"
11
+ xmlns="http://www.w3.org/2000/svg"
12
+ >
13
+ <path
14
+ fill-rule="evenodd"
15
+ clip-rule="evenodd"
16
+ d="M6.41113 3.73486C6.97158 4.11531 7.3643 4.63076 7.3643 5.31801V6.54526H8.59155C8.81654 6.54526 9.00063 6.36118 9.00063 6.13618V5.31801C9.00063 4.42621 7.5402 3.8985 6.41113 3.73486Z"
17
+ fill="currentColor"
18
+ />
19
+ <path
20
+ d="M3.27305 3.27266C4.17677 3.27266 4.90938 2.54005 4.90938 1.63633C4.90938 0.732611 4.17677 0 3.27305 0C2.36933 0 1.63672 0.732611 1.63672 1.63633C1.63672 2.54005 2.36933 3.27266 3.27305 3.27266Z"
21
+ fill="currentColor"
22
+ />
23
+ <path
24
+ fill-rule="evenodd"
25
+ clip-rule="evenodd"
26
+ d="M5.72716 3.27266C6.63124 3.27266 7.36349 2.54041 7.36349 1.63633C7.36349 0.732258 6.63124 0 5.72716 0C5.53489 0 5.3549 0.0409082 5.18308 0.0981798C5.53511 0.533533 5.72715 1.07646 5.72715 1.63633C5.72715 2.1962 5.53511 2.73913 5.18308 3.17448C5.3549 3.23176 5.53489 3.27266 5.72716 3.27266ZM3.27266 3.68175C2.18041 3.68175 0 4.22992 0 5.31808V6.13624C0 6.36124 0.184087 6.54533 0.409083 6.54533H6.13625C6.36124 6.54533 6.54533 6.36124 6.54533 6.13624V5.31808C6.54533 4.22992 4.36492 3.68175 3.27266 3.68175Z"
27
+ fill="currentColor"
28
+ />
29
+ </svg>
frontend/src/lib/Icons/Pin.svelte ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ height="1em"
8
+ viewBox="0 0 7 9"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <path
13
+ d="M3.5 4.58599C3.74063 4.58599 3.94669 4.4961 4.11819 4.31633C4.2894 4.13687 4.375 3.92102 4.375 3.66879C4.375 3.41656 4.2894 3.20056 4.11819 3.02079C3.94669 2.84132 3.74063 2.75159 3.5 2.75159C3.25937 2.75159 3.05346 2.84132 2.88225 3.02079C2.71075 3.20056 2.625 3.41656 2.625 3.66879C2.625 3.92102 2.71075 4.13687 2.88225 4.31633C3.05346 4.4961 3.25937 4.58599 3.5 4.58599ZM3.5 7.95669C4.38958 7.10064 5.04948 6.32285 5.47969 5.62334C5.9099 4.92413 6.125 4.30318 6.125 3.76051C6.125 2.92739 5.87154 2.24515 5.36462 1.71378C4.858 1.18273 4.23646 0.917197 3.5 0.917197C2.76354 0.917197 2.14185 1.18273 1.63494 1.71378C1.12831 2.24515 0.875 2.92739 0.875 3.76051C0.875 4.30318 1.0901 4.92413 1.52031 5.62334C1.95052 6.32285 2.61042 7.10064 3.5 7.95669ZM3.5 9C3.44167 9 3.38333 8.98853 3.325 8.9656C3.26667 8.94267 3.21562 8.9121 3.17188 8.87389C2.10729 7.8879 1.3125 6.97269 0.7875 6.12825C0.2625 5.28352 0 4.49427 0 3.76051C0 2.61401 0.351896 1.70064 1.05569 1.02038C1.75919 0.340127 2.57396 0 3.5 0C4.42604 0 5.24081 0.340127 5.94431 1.02038C6.6481 1.70064 7 2.61401 7 3.76051C7 4.49427 6.7375 5.28352 6.2125 6.12825C5.6875 6.97269 4.89271 7.8879 3.82812 8.87389C3.78438 8.9121 3.73333 8.94267 3.675 8.9656C3.61667 8.98853 3.55833 9 3.5 9Z"
14
+ fill="white"
15
+ />
16
+ </svg>
frontend/src/lib/Icons/Room.svelte ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg class={classList} height="1em" viewBox="0 0 9 9" fill="none" xmlns="http://www.w3.org/2000/svg">
6
+ <rect x="1" y="1" width="7" height="7" rx="1" stroke="#2F6DCB" />
7
+ <rect
8
+ x="3.05566"
9
+ y="3.05566"
10
+ width="2.88889"
11
+ height="2.88889"
12
+ rx="0.5"
13
+ fill="#2F6DCB"
14
+ stroke="#2F6DCB"
15
+ />
16
+ </svg>
frontend/src/lib/Icons/Undo.svelte ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ export let classList = '';
3
+ </script>
4
+
5
+ <svg
6
+ class={classList}
7
+ width="1em"
8
+ viewBox="0 0 10 9"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ >
12
+ <g opacity="0.5">
13
+ <path
14
+ 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"
15
+ fill="black"
16
+ stroke="black"
17
+ stroke-width="0.5"
18
+ stroke-linejoin="round"
19
+ />
20
+ </g>
21
+ </svg>
frontend/src/lib/Menu.svelte ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import RoomsSelector from '$lib/Buttons/RoomsSelector.svelte';
3
+ import AboutButton from '$lib/Buttons/AboutButton.svelte';
4
+ import { toggleAbout } from '$lib/store';
5
+ import { createEventDispatcher } from 'svelte';
6
+
7
+ const dispatch = createEventDispatcher();
8
+ // const broadcast = useBroadcastEvent();
9
+
10
+ export let isLoading = false;
11
+ </script>
12
+
13
+ <svelte:window
14
+ on:keypress={(e) => {
15
+ if (e.key === 'Enter') {
16
+ dispatch('showModal', { showModal: true });
17
+ }
18
+ }}
19
+ />
20
+ <div class="flex flex-col md:flex-row items-center justify-between px-4 md:px-12 gap-3 md:gap-0">
21
+ <AboutButton
22
+ {isLoading}
23
+ on:click={() => {
24
+ $toggleAbout = !$toggleAbout;
25
+ }}
26
+ />
27
+
28
+ <button
29
+ on:click={() => dispatch('showModal', { showModal: true })}
30
+ title="Click to prompt, and paint. The generated image will show up in the frame."
31
+ disabled={isLoading}
32
+ class="{isLoading
33
+ ? 'cursor-wait'
34
+ : 'cursor-pointer'} order-first md:order-none text-xl md:text-3xl bg-blue-600 text-white px-4 py-1 md:px-6 md:py-2 rounded-2xl ring ring-blue-500 font-semibold shadow-2xl shadow-blue-500 self-center flex items-center hover:saturate-150"
35
+ ><span class="mr-3">🖍</span>Paint
36
+ <span
37
+ class="bg-blue-800 text-gray-300 rounded-lg px-2 py-0.5 text-base ml-4 hidden sm:flex items-center translate-y-[2px]"
38
+ ><svg
39
+ class="text-sm mr-1.5"
40
+ width="1em"
41
+ height="1em"
42
+ viewBox="0 0 10 13"
43
+ fill="currentColor"
44
+ xmlns="http://www.w3.org/2000/svg"
45
+ >
46
+ <path
47
+ fill-rule="evenodd"
48
+ clip-rule="evenodd"
49
+ d="M8.5 8.5V0H9.5V9.5H1.70711L4.03553 11.8284C4.2308 12.0237 4.2308 12.3403 4.03553 12.5355C3.84027 12.7308 3.52369 12.7308 3.32843 12.5355L0.146447 9.35355C-0.0488155 9.15829 -0.0488155 8.84171 0.146447 8.64645L3.32843 5.46447C3.52369 5.2692 3.84027 5.2692 4.03553 5.46447C4.2308 5.65973 4.2308 5.97631 4.03553 6.17157L1.70711 8.5H8.5Z"
50
+ fill="currentColor"
51
+ />
52
+ </svg>
53
+ Enter</span
54
+ ></button
55
+ >
56
+ <RoomsSelector {isLoading} />
57
+ </div>
frontend/src/lib/PaintCanvas.svelte ADDED
@@ -0,0 +1,284 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <script lang="ts">
2
+ import { zoom, zoomIdentity } from 'd3-zoom';
3
+ import { min } from 'd3-array';
4
+ import { select } from 'd3-selection';
5
+ import { onMount } from 'svelte';
6
+ import { PUBLIC_UPLOADS } from '$env/static/public';
7
+ import {
8
+ currZoomTransform,
9
+ canvasEl,
10
+ isRenderingCanvas,
11
+ canvasSize,
12
+ selectedRoomID
13
+ } from '$lib/store';
14
+
15
+ import { useMyPresence, useObject } from '$lib/liveblocks';
16
+ import { LiveObject } from '@liveblocks/client';
17
+
18
+ import type { PromptImgObject } from '$lib/types';
19
+ import { FRAME_SIZE, GRID_SIZE } from '$lib/constants';
20
+
21
+ const myPresence = useMyPresence();
22
+ const promptImgStorage = useObject('promptImgStorage');
23
+
24
+ const height = $canvasSize.height;
25
+ const width = $canvasSize.width;
26
+
27
+ let containerEl: HTMLDivElement;
28
+ let canvasCtx: CanvasRenderingContext2D;
29
+
30
+ const imagesOnCanvas = new Set();
31
+
32
+ function getpromptImgList(
33
+ promptImgList: Record<string, LiveObject<PromptImgObject> | PromptImgObject>
34
+ ): PromptImgObject[] {
35
+ if (promptImgList) {
36
+ //sorted by last updated
37
+ const roomid = $selectedRoomID || '';
38
+ const canvasPixels = new Map();
39
+ for (const x of Array.from(Array(width / GRID_SIZE).keys())) {
40
+ for (const y of Array.from(Array(height / GRID_SIZE).keys())) {
41
+ canvasPixels.set(`${x * GRID_SIZE}_${y * GRID_SIZE}`, null);
42
+ }
43
+ }
44
+
45
+ // Object.values(promptImgList).map((e) => {
46
+ // let obj;
47
+ // if (e instanceof LiveObject) {
48
+ // obj = e.toObject();
49
+ // } else {
50
+ // obj = e;
51
+ // }
52
+ // // const obj = promptImg.toObject();
53
+ // if (obj.position) {
54
+ // const key = `${obj.position.x}_${obj.position.y}`;
55
+ // console.log('key', key);
56
+ // const promptImgParams = {
57
+ // imgURL: obj.imgURL
58
+ // };
59
+ // $promptImgStorage.set(key, promptImgParams);
60
+ // }
61
+ // });
62
+
63
+ const list: PromptImgObject[] = Object.values(promptImgList)
64
+ .map((e) => {
65
+ if (e instanceof LiveObject) {
66
+ return e.toObject();
67
+ } else {
68
+ return e;
69
+ }
70
+ })
71
+ .map((e) => {
72
+ const split_str = e.imgURL.split(/-|.jpg|.webp/);
73
+ const date = parseInt(split_str[0]);
74
+ const id = split_str[1];
75
+ const [x, y] = split_str[2].split('_');
76
+ const prompt = split_str.slice(3).join(' ');
77
+ return {
78
+ id,
79
+ date,
80
+ position: {
81
+ x: parseInt(x),
82
+ y: parseInt(y)
83
+ },
84
+ imgURL: e.imgURL,
85
+ prompt,
86
+ room: roomid
87
+ };
88
+ })
89
+ .sort((a, b) => b.date - a.date);
90
+
91
+ // init
92
+ for (const promptImg of list) {
93
+ const x = promptImg.position.x;
94
+ const y = promptImg.position.y;
95
+ for (const i of [...Array(FRAME_SIZE / GRID_SIZE).keys()]) {
96
+ for (const j of [...Array(FRAME_SIZE / GRID_SIZE).keys()]) {
97
+ const key = `${x + i * GRID_SIZE}_${y + j * GRID_SIZE}`;
98
+ if (!canvasPixels.get(key)) {
99
+ canvasPixels.set(key, promptImg.id);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ const ids = new Set([...canvasPixels.values()]);
105
+ const filteredImages = list.filter((promptImg) => ids.has(promptImg.id));
106
+
107
+ // remove images that are under other images
108
+ list
109
+ .filter((promptImg) => !ids.has(promptImg.id))
110
+ .map((promptImg) => {
111
+ const key = `${promptImg.position.x}_${promptImg.position.y}`;
112
+ $promptImgStorage.delete(key);
113
+ console.log('deleted', key)
114
+ });
115
+ return filteredImages.reverse().filter((promptImg) => !imagesOnCanvas.has(promptImg.id));
116
+ }
117
+ return [];
118
+ }
119
+ let promptImgList: PromptImgObject[] = [];
120
+ $: promptImgList = getpromptImgList($promptImgStorage?.toObject());
121
+
122
+ $: if (promptImgList) {
123
+ renderImages(promptImgList);
124
+ }
125
+
126
+ function to_bbox(
127
+ W: number,
128
+ H: number,
129
+ center: { x: number; y: number },
130
+ w: number,
131
+ h: number,
132
+ margin: number
133
+ ) {
134
+ //https://bl.ocks.org/fabiovalse/b9224bfd64ca96c47f8cdcb57b35b8e2
135
+ const kw = (W - margin) / w;
136
+ const kh = (H - margin) / h;
137
+ const k = min([kw, kh]) || 1;
138
+ const x = W / 2 - center.x * k;
139
+ const y = H / 2 - center.y * k;
140
+ return zoomIdentity.translate(x, y).scale(k);
141
+ }
142
+ onMount(() => {
143
+ const padding = 50;
144
+ const scale =
145
+ (width + padding * 2) /
146
+ (containerEl.clientHeight > containerEl.clientWidth
147
+ ? containerEl.clientWidth
148
+ : containerEl.clientHeight);
149
+ const zoomHandler = zoom()
150
+ .scaleExtent([1 / scale / 2, 3])
151
+ // .translateExtent([
152
+ // [-padding, -padding],
153
+ // [width + padding, height + padding]
154
+ // ])
155
+ .tapDistance(10)
156
+ .on('zoom', zoomed);
157
+
158
+ const selection = select($canvasEl.parentElement)
159
+ .call(zoomHandler as any)
160
+ .call(
161
+ zoomHandler.transform as any,
162
+ to_bbox(
163
+ containerEl.clientWidth,
164
+ containerEl.clientHeight,
165
+ { x: width / 2, y: height / 2 },
166
+ width,
167
+ height,
168
+ padding
169
+ )
170
+ )
171
+ // .call(zoomHandler.scaleTo as any, 1 / scale)
172
+ .on('pointermove', handlePointerMove)
173
+ .on('pointerleave', handlePointerLeave);
174
+
175
+ canvasCtx = $canvasEl.getContext('2d') as CanvasRenderingContext2D;
176
+ function zoomReset() {
177
+ const scale =
178
+ (width + padding * 2) /
179
+ (containerEl.clientHeight > containerEl.clientWidth
180
+ ? containerEl.clientWidth
181
+ : containerEl.clientHeight);
182
+ zoomHandler.scaleExtent([1 / scale / 2, 3]);
183
+ selection.call(
184
+ zoomHandler.transform as any,
185
+ to_bbox(
186
+ containerEl.clientWidth,
187
+ containerEl.clientHeight,
188
+ { x: width / 2, y: height / 2 },
189
+ width,
190
+ height,
191
+ padding
192
+ )
193
+ );
194
+ }
195
+ window.addEventListener('resize', zoomReset);
196
+ return () => {
197
+ window.removeEventListener('resize', zoomReset);
198
+ };
199
+ });
200
+
201
+ type ImageRendered = {
202
+ img: HTMLImageElement;
203
+ position: { x: number; y: number };
204
+ id: string;
205
+ };
206
+ async function renderImages(promptImgList: PromptImgObject[]) {
207
+ if (promptImgList.length === 0) return;
208
+ $isRenderingCanvas = true;
209
+
210
+ await Promise.allSettled(
211
+ promptImgList.map(
212
+ ({ imgURL, position, id, room }) =>
213
+ new Promise<ImageRendered>((resolve, reject) => {
214
+ const img = new Image();
215
+ img.crossOrigin = 'anonymous';
216
+ img.onload = () => {
217
+ const res: ImageRendered = { img, position, id };
218
+ canvasCtx.drawImage(img, position.x, position.y, img.width, img.height);
219
+ resolve(res);
220
+ };
221
+ img.onerror = (err) => {
222
+ reject(err);
223
+ };
224
+ img.src = `${PUBLIC_UPLOADS}/${room}/${imgURL}`;
225
+ })
226
+ )
227
+ ).then((values) => {
228
+ const images = values
229
+ .filter((v) => v.status === 'fulfilled')
230
+ .map((v) => (v as PromiseFulfilledResult<ImageRendered>).value);
231
+ images.forEach(({ img, position, id }) => {
232
+ // keep track of images already rendered
233
+ //re draw in order
234
+ imagesOnCanvas.add(id);
235
+ canvasCtx.drawImage(img, position.x, position.y, img.width, img.height);
236
+ });
237
+ });
238
+ $isRenderingCanvas = false;
239
+ }
240
+ function zoomed(e: Event) {
241
+ const transform = ($currZoomTransform = e.transform);
242
+ $canvasEl.style.transform = `translate(${transform.x}px, ${transform.y}px) scale(${transform.k})`;
243
+ }
244
+
245
+ // Update cursor presence to current pointer location
246
+ function handlePointerMove(event: PointerEvent) {
247
+ event.preventDefault();
248
+ const x = $currZoomTransform.invertX(event.clientX);
249
+ const y = $currZoomTransform.invertY(event.clientY);
250
+
251
+ myPresence.update({
252
+ cursor: {
253
+ x,
254
+ y
255
+ }
256
+ });
257
+ }
258
+
259
+ // When the pointer leaves the page, set cursor presence to null
260
+ function handlePointerLeave() {
261
+ myPresence.update({
262
+ cursor: null
263
+ });
264
+ }
265
+ </script>
266
+
267
+ <div
268
+ bind:this={containerEl}
269
+ class="absolute top-0 left-0 right-0 bottom-0 overflow-hidden z-0 bg-blue-200"
270
+ >
271
+ <canvas
272
+ bind:this={$canvasEl}
273
+ {width}
274
+ {height}
275
+ class="absolute top-0 left-0 bg-white shadow-2xl shadow-blue-500/20"
276
+ />
277
+ <slot />
278
+ </div>
279
+
280
+ <style lang="postcss" scoped>
281
+ canvas {
282
+ transform-origin: 0 0;
283
+ }
284
+ </style>