dylanebert HF staff commited on
Commit
9332801
1 Parent(s): 11f52a3

on-screen controls, flexible upload

Browse files
README.md CHANGED
@@ -18,23 +18,3 @@ This simple editor showcases the realtime editing capabilities of gsplat.js.
18
  - Import gaussian splatting objects from a file (`.ply` or `.splat`) by dragging and dropping them into the editor window.
19
  - Download splats as a `.splat` file by clicking the download button in the top right corner.s
20
  - If an object is selected, only that object will be downloaded. Otherwise, all objects will be combined and downloaded.
21
- - Use the controls below to edit the splats.
22
-
23
- ## Controls
24
-
25
- ### Camera
26
-
27
- - `Middle Mouse` - Orbit camera
28
- - `Shift + Middle Mouse` - Pan camera
29
- - `Scroll Wheel` - Zoom camera
30
-
31
- ### Editing
32
-
33
- - `Left Mouse` - Select an object / confirm action
34
- - `Right Mouse` - Cancel action
35
- - `G` - Grab selected object
36
- - `R` - Rotate selected object
37
- - `S` - Scale selected object
38
- - `X` - Delete selected object / lock to X axis
39
- - `Y` - Lock to Y axis
40
- - `Z` - Lock to Z axis
 
18
  - Import gaussian splatting objects from a file (`.ply` or `.splat`) by dragging and dropping them into the editor window.
19
  - Download splats as a `.splat` file by clicking the download button in the top right corner.s
20
  - If an object is selected, only that object will be downloaded. Otherwise, all objects will be combined and downloaded.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
editor/index.html CHANGED
@@ -1,8 +1,9 @@
1
- <!doctype html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <link rel="stylesheet" href="style.css" />
 
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
  <title>gsplat.js - Editor Demo</title>
8
  </head>
@@ -16,7 +17,42 @@
16
  </dialog>
17
  </div>
18
 
19
- <button id="download-button">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 32 32">
21
  <path
22
  fill="#ddd"
@@ -25,6 +61,79 @@
25
  </svg>
26
  </button>
27
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
28
  <canvas id="canvas"> </canvas>
29
  <script type="module" src="/src/main.ts"></script>
30
  </body>
 
1
+ <!DOCTYPE html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="UTF-8" />
5
  <link rel="stylesheet" href="style.css" />
6
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" />
7
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
8
  <title>gsplat.js - Editor Demo</title>
9
  </head>
 
17
  </dialog>
18
  </div>
19
 
20
+ <button id="upload-button" class="tool-button" title="Import Splat">
21
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 32 32">
22
+ <path fill="#ddd" d="M28 19H14.83l2.58-2.59L16 15l-5 5l5 5l1.41-1.41L14.83 21H28z" />
23
+ <path
24
+ fill="#ddd"
25
+ d="M24 14v-4a1 1 0 0 0-.29-.71l-7-7A1 1 0 0 0 16 2H6a2 2 0 0 0-2 2v24a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2v-2h-2v2H6V4h8v6a2 2 0 0 0 2 2h6v2Zm-8-4V4.41L21.59 10Z"
26
+ />
27
+ </svg>
28
+ </button>
29
+
30
+ <div id="upload-modal" class="modal">
31
+ <div class="modal-content">
32
+ <span id="upload-modal-close" class="close">&times;</span>
33
+ <p>Import Splat</p>
34
+ <hr class="divider" />
35
+ <div class="modal-section">
36
+ <p>Upload a file</p>
37
+ <input type="file" id="file-input" accept=".splat,.ply" />
38
+ <label for="file-input" id="file-input-label">Choose File</label>
39
+ </div>
40
+ <div class="modal-section">
41
+ <p>Or enter a URL</p>
42
+ <input
43
+ type="text"
44
+ id="url-input"
45
+ placeholder="https://huggingface.co/datasets/dylanebert/3dgs/resolve/main/bonsai/bonsai-7k-mini.splat"
46
+ />
47
+ <button id="upload-submit">Import</button>
48
+ </div>
49
+ <div class="modal-section">
50
+ <p id="upload-error"></p>
51
+ </div>
52
+ </div>
53
+ </div>
54
+
55
+ <button id="download-button" class="tool-button" title="Download Selection">
56
  <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 32 32">
57
  <path
58
  fill="#ddd"
 
61
  </svg>
62
  </button>
63
 
64
+ <button id="controls-display-button" class="tool-button active" title="Show/Hide Controls">
65
+ <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 32 32">
66
+ <path
67
+ fill="#ddd"
68
+ d="M16 2a14 14 0 1 0 14 14A14 14 0 0 0 16 2m0 26a12 12 0 1 1 12-12a12 12 0 0 1-12 12"
69
+ />
70
+ <circle cx="16" cy="23.5" r="1.5" fill="#ddd" />
71
+ <path
72
+ fill="#ddd"
73
+ d="M17 8h-1.5a4.49 4.49 0 0 0-4.5 4.5v.5h2v-.5a2.5 2.5 0 0 1 2.5-2.5H17a2.5 2.5 0 0 1 0 5h-2v4.5h2V17a4.5 4.5 0 0 0 0-9"
74
+ />
75
+ </svg>
76
+ </button>
77
+
78
+ <div id="controls-display" class="active">
79
+ <p>Controls</p>
80
+ <hr class="divider" />
81
+ <p>Camera</p>
82
+ <div class="control-item">
83
+ <p class="control-name">Orbit</p>
84
+ <p class="control-icon">MMB / Alt + LMB</p>
85
+ </div>
86
+ <div class="control-item">
87
+ <p class="control-name">Pan</p>
88
+ <p class="control-icon">Shift + MMB / Alt + RMB</p>
89
+ </div>
90
+ <div class="control-item">
91
+ <p class="control-name">Zoom</p>
92
+ <p class="control-icon">Scroll</p>
93
+ </div>
94
+ <hr class="divider" />
95
+ <p>Actions</p>
96
+ <div class="control-item">
97
+ <p class="control-name">Select</p>
98
+ <p class="control-icon">LMB</p>
99
+ </div>
100
+ <div class="control-item">
101
+ <p class="control-name">Grab</p>
102
+ <p class="control-icon">G</p>
103
+ </div>
104
+ <div class="control-item">
105
+ <p class="control-name">Rotate</p>
106
+ <p class="control-icon">R</p>
107
+ </div>
108
+ <div class="control-item">
109
+ <p class="control-name">Scale</p>
110
+ <p class="control-icon">S</p>
111
+ </div>
112
+ <div class="control-item">
113
+ <p class="control-name">Delete</p>
114
+ <p class="control-icon">X</p>
115
+ </div>
116
+ <hr class="divider" />
117
+ <p>During Action</p>
118
+ <div class="control-item">
119
+ <p class="control-name">Lock X Axis</p>
120
+ <p class="control-icon">X</p>
121
+ </div>
122
+ <div class="control-item">
123
+ <p class="control-name">Lock Y Axis</p>
124
+ <p class="control-icon">Y</p>
125
+ </div>
126
+ <div class="control-item">
127
+ <p class="control-name">Lock Z Axis</p>
128
+ <p class="control-icon">Z</p>
129
+ </div>
130
+ <hr class="divider" />
131
+ <div class="control-item">
132
+ <p class="control-name">Undo</p>
133
+ <p class="control-icon">Ctrl + Z</p>
134
+ </div>
135
+ </div>
136
+
137
  <canvas id="canvas"> </canvas>
138
  <script type="module" src="/src/main.ts"></script>
139
  </body>
editor/src/Controls.ts CHANGED
@@ -7,7 +7,9 @@ class Controls {
7
  this._inputHandlers = inputHandlers;
8
 
9
  window.addEventListener("keydown", this.handleInput.bind(this));
 
10
  canvas.addEventListener("mousemove", this.handleInput.bind(this));
 
11
  canvas.addEventListener("click", this.handleInput.bind(this));
12
  canvas.addEventListener("contextmenu", this.handleInput.bind(this));
13
  }
 
7
  this._inputHandlers = inputHandlers;
8
 
9
  window.addEventListener("keydown", this.handleInput.bind(this));
10
+ canvas.addEventListener("mousedown", this.handleInput.bind(this));
11
  canvas.addEventListener("mousemove", this.handleInput.bind(this));
12
+ canvas.addEventListener("mouseup", this.handleInput.bind(this));
13
  canvas.addEventListener("click", this.handleInput.bind(this));
14
  canvas.addEventListener("contextmenu", this.handleInput.bind(this));
15
  }
editor/src/DefaultMode.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  import { UndoManager } from "./UndoManager";
2
  import { ModeManager } from "./ModeManager";
3
  import { SelectionManager } from "./SelectionManager";
@@ -43,8 +44,15 @@ class DefaultMode implements InputMode {
43
  }
44
  };
45
 
46
- const handleClick = () => {
 
 
 
 
 
47
  const mousePosition = engine.mouseManager.currentMousePosition;
 
 
48
  const result = engine.intersectionTester.testPoint(mousePosition.x, mousePosition.y);
49
  if (result !== null) {
50
  SelectionManager.selectedSplat = result;
@@ -59,7 +67,8 @@ class DefaultMode implements InputMode {
59
  engine.keyboardManager.registerKey("x", handleDelete);
60
  engine.keyboardManager.registerKey("z", handleUndo);
61
  engine.keyboardManager.registerKey("a", handleClearSelection);
62
- engine.mouseManager.registerMouse("click", handleClick);
 
63
  engine.orbitControls.enabled = true;
64
 
65
  this.exit = () => {
@@ -69,7 +78,8 @@ class DefaultMode implements InputMode {
69
  engine.keyboardManager.unregisterKey("x");
70
  engine.keyboardManager.unregisterKey("z");
71
  engine.keyboardManager.unregisterKey("a");
72
- engine.mouseManager.unregisterMouse("click");
 
73
  engine.orbitControls.enabled = false;
74
  };
75
  }
 
1
+ import * as SPLAT from "gsplat";
2
  import { UndoManager } from "./UndoManager";
3
  import { ModeManager } from "./ModeManager";
4
  import { SelectionManager } from "./SelectionManager";
 
44
  }
45
  };
46
 
47
+ let mouseDownPosition: SPLAT.Vector3;
48
+ const handleMouseDown = () => {
49
+ mouseDownPosition = engine.mouseManager.currentMousePosition;
50
+ };
51
+
52
+ const handleMouseUp = () => {
53
  const mousePosition = engine.mouseManager.currentMousePosition;
54
+ const distance = mousePosition.distanceTo(mouseDownPosition);
55
+ if (distance > 0.01) return;
56
  const result = engine.intersectionTester.testPoint(mousePosition.x, mousePosition.y);
57
  if (result !== null) {
58
  SelectionManager.selectedSplat = result;
 
67
  engine.keyboardManager.registerKey("x", handleDelete);
68
  engine.keyboardManager.registerKey("z", handleUndo);
69
  engine.keyboardManager.registerKey("a", handleClearSelection);
70
+ engine.mouseManager.registerMouse("mousedown", handleMouseDown);
71
+ engine.mouseManager.registerMouse("mouseup", handleMouseUp);
72
  engine.orbitControls.enabled = true;
73
 
74
  this.exit = () => {
 
78
  engine.keyboardManager.unregisterKey("x");
79
  engine.keyboardManager.unregisterKey("z");
80
  engine.keyboardManager.unregisterKey("a");
81
+ engine.mouseManager.unregisterMouse("mousedown");
82
+ engine.mouseManager.unregisterMouse("mouseup");
83
  engine.orbitControls.enabled = false;
84
  };
85
  }
editor/src/OrbitControls.ts CHANGED
@@ -6,10 +6,10 @@ class OrbitControls {
6
  minAngle: number = -90;
7
  maxAngle: number = 90;
8
  minZoom: number = 0.1;
9
- maxZoom: number = 30;
10
  orbitSpeed: number = 1.75;
11
  panSpeed: number = 1.25;
12
- zoomSpeed: number = 1.75;
13
  dampening: number = 0.5;
14
  setCameraTarget: (newTarget: SPLAT.Vector3) => void = () => {};
15
  update: () => void;
@@ -20,8 +20,8 @@ class OrbitControls {
20
  canvas: HTMLElement,
21
  alpha: number = 0.5,
22
  beta: number = 0.5,
23
- radius: number = 5,
24
- inputTarget: SPLAT.Vector3 = new SPLAT.Vector3(),
25
  ) {
26
  let target = inputTarget.clone();
27
 
@@ -78,16 +78,24 @@ class OrbitControls {
78
  panning = e.shiftKey;
79
  lastX = e.clientX;
80
  lastY = e.clientY;
 
 
 
 
 
 
 
 
 
 
81
  }
82
  };
83
 
84
  const onMouseUp = (e: MouseEvent) => {
85
  preventDefault(e);
86
 
87
- if (e.button === 1) {
88
- dragging = false;
89
- panning = false;
90
- }
91
  };
92
 
93
  const onMouseMove = (e: MouseEvent) => {
@@ -112,7 +120,7 @@ class OrbitControls {
112
  desiredBeta += dy * this.orbitSpeed * 0.003;
113
  desiredBeta = Math.min(
114
  Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
115
- (this.maxAngle * Math.PI) / 180,
116
  );
117
  }
118
 
@@ -194,7 +202,7 @@ class OrbitControls {
194
  desiredBeta += dy * this.orbitSpeed * 0.003;
195
  desiredBeta = Math.min(
196
  Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
197
- (this.maxAngle * Math.PI) / 180,
198
  );
199
 
200
  lastX = e.touches[0].clientX;
 
6
  minAngle: number = -90;
7
  maxAngle: number = 90;
8
  minZoom: number = 0.1;
9
+ maxZoom: number = 50;
10
  orbitSpeed: number = 1.75;
11
  panSpeed: number = 1.25;
12
+ zoomSpeed: number = 2;
13
  dampening: number = 0.5;
14
  setCameraTarget: (newTarget: SPLAT.Vector3) => void = () => {};
15
  update: () => void;
 
20
  canvas: HTMLElement,
21
  alpha: number = 0.5,
22
  beta: number = 0.5,
23
+ radius: number = 13,
24
+ inputTarget: SPLAT.Vector3 = new SPLAT.Vector3()
25
  ) {
26
  let target = inputTarget.clone();
27
 
 
78
  panning = e.shiftKey;
79
  lastX = e.clientX;
80
  lastY = e.clientY;
81
+ } else if (e.altKey && e.button === 0) {
82
+ dragging = true;
83
+ panning = false;
84
+ lastX = e.clientX;
85
+ lastY = e.clientY;
86
+ } else if (e.altKey && e.button === 2) {
87
+ dragging = true;
88
+ panning = true;
89
+ lastX = e.clientX;
90
+ lastY = e.clientY;
91
  }
92
  };
93
 
94
  const onMouseUp = (e: MouseEvent) => {
95
  preventDefault(e);
96
 
97
+ dragging = false;
98
+ panning = false;
 
 
99
  };
100
 
101
  const onMouseMove = (e: MouseEvent) => {
 
120
  desiredBeta += dy * this.orbitSpeed * 0.003;
121
  desiredBeta = Math.min(
122
  Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
123
+ (this.maxAngle * Math.PI) / 180
124
  );
125
  }
126
 
 
202
  desiredBeta += dy * this.orbitSpeed * 0.003;
203
  desiredBeta = Math.min(
204
  Math.max(desiredBeta, (this.minAngle * Math.PI) / 180),
205
+ (this.maxAngle * Math.PI) / 180
206
  );
207
 
208
  lastX = e.touches[0].clientX;
editor/src/main.ts CHANGED
@@ -5,7 +5,16 @@ import { SelectionManager } from "./SelectionManager";
5
  const canvas = document.getElementById("canvas") as HTMLCanvasElement;
6
  const progressDialog = document.getElementById("progress-dialog") as HTMLDialogElement;
7
  const progressIndicator = document.getElementById("progress-indicator") as HTMLProgressElement;
 
8
  const downloadButton = document.getElementById("download-button") as HTMLButtonElement;
 
 
 
 
 
 
 
 
9
 
10
  const engine = new Engine(canvas);
11
 
@@ -14,22 +23,30 @@ async function selectFile(file: File) {
14
  if (loading) return;
15
  SelectionManager.selectedSplat = null;
16
  loading = true;
17
- // Check if .splat file
18
  if (file.name.endsWith(".splat")) {
 
 
19
  await SPLAT.Loader.LoadFromFileAsync(file, engine.scene, (progress: number) => {
20
- console.log("Loading SPLAT file: " + progress);
21
  });
 
22
  } else if (file.name.endsWith(".ply")) {
23
  const format = "";
24
  // const format = "polycam"; // Uncomment to load a Polycam PLY file
 
 
25
  await SPLAT.PLYLoader.LoadFromFileAsync(
26
  file,
27
  engine.scene,
28
  (progress: number) => {
29
- console.log("Loading PLY file: " + progress);
30
  },
31
  format
32
  );
 
 
 
 
33
  }
34
  loading = false;
35
  }
@@ -64,6 +81,14 @@ async function main() {
64
  }
65
  });
66
 
 
 
 
 
 
 
 
 
67
  downloadButton.addEventListener("click", () => {
68
  if (SelectionManager.selectedSplat !== null) {
69
  SelectionManager.selectedSplat.saveToFile();
@@ -72,6 +97,43 @@ async function main() {
72
  }
73
  });
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  window.addEventListener("click", () => {
76
  window.focus();
77
  });
 
5
  const canvas = document.getElementById("canvas") as HTMLCanvasElement;
6
  const progressDialog = document.getElementById("progress-dialog") as HTMLDialogElement;
7
  const progressIndicator = document.getElementById("progress-indicator") as HTMLProgressElement;
8
+ const uploadButton = document.getElementById("upload-button") as HTMLButtonElement;
9
  const downloadButton = document.getElementById("download-button") as HTMLButtonElement;
10
+ const controlsDisplayButton = document.getElementById("controls-display-button") as HTMLButtonElement;
11
+ const controlsDisplay = document.getElementById("controls-display") as HTMLDivElement;
12
+ const uploadModal = document.getElementById("upload-modal") as HTMLDialogElement;
13
+ const uploadModalClose = document.getElementById("upload-modal-close") as HTMLButtonElement;
14
+ const fileInput = document.getElementById("file-input") as HTMLInputElement;
15
+ const urlInput = document.getElementById("url-input") as HTMLInputElement;
16
+ const uploadSubmit = document.getElementById("upload-submit") as HTMLButtonElement;
17
+ const uploadError = document.getElementById("upload-error") as HTMLDivElement;
18
 
19
  const engine = new Engine(canvas);
20
 
 
23
  if (loading) return;
24
  SelectionManager.selectedSplat = null;
25
  loading = true;
 
26
  if (file.name.endsWith(".splat")) {
27
+ uploadModal.style.display = "none";
28
+ progressDialog.showModal();
29
  await SPLAT.Loader.LoadFromFileAsync(file, engine.scene, (progress: number) => {
30
+ progressIndicator.value = progress * 100;
31
  });
32
+ progressDialog.close();
33
  } else if (file.name.endsWith(".ply")) {
34
  const format = "";
35
  // const format = "polycam"; // Uncomment to load a Polycam PLY file
36
+ uploadModal.style.display = "none";
37
+ progressDialog.showModal();
38
  await SPLAT.PLYLoader.LoadFromFileAsync(
39
  file,
40
  engine.scene,
41
  (progress: number) => {
42
+ progressIndicator.value = progress * 100;
43
  },
44
  format
45
  );
46
+ progressDialog.close();
47
+ } else {
48
+ uploadError.style.display = "block";
49
+ uploadError.innerText = `Invalid file type: ${file.name}`;
50
  }
51
  loading = false;
52
  }
 
81
  }
82
  });
83
 
84
+ uploadButton.addEventListener("click", () => {
85
+ uploadModal.style.display = "block";
86
+ });
87
+
88
+ uploadModalClose.addEventListener("click", () => {
89
+ uploadModal.style.display = "none";
90
+ });
91
+
92
  downloadButton.addEventListener("click", () => {
93
  if (SelectionManager.selectedSplat !== null) {
94
  SelectionManager.selectedSplat.saveToFile();
 
97
  }
98
  });
99
 
100
+ controlsDisplayButton.addEventListener("click", () => {
101
+ controlsDisplayButton.classList.toggle("active");
102
+ controlsDisplay.classList.toggle("active");
103
+ });
104
+
105
+ fileInput.addEventListener("change", () => {
106
+ if (fileInput.files != null) {
107
+ selectFile(fileInput.files[0]);
108
+ }
109
+ });
110
+
111
+ uploadSubmit.addEventListener("click", async () => {
112
+ let url = urlInput.value;
113
+ if (url === "") {
114
+ url = urlInput.placeholder;
115
+ }
116
+ if (url.endsWith(".splat")) {
117
+ uploadModal.style.display = "none";
118
+ progressDialog.showModal();
119
+ await SPLAT.Loader.LoadAsync(url, engine.scene, (progress) => (progressIndicator.value = progress * 100));
120
+ progressDialog.close();
121
+ } else if (url.endsWith(".ply")) {
122
+ uploadModal.style.display = "none";
123
+ progressDialog.showModal();
124
+ await SPLAT.PLYLoader.LoadAsync(
125
+ url,
126
+ engine.scene,
127
+ (progress) => (progressIndicator.value = progress * 100)
128
+ );
129
+ progressDialog.close();
130
+ } else {
131
+ uploadError.style.display = "block";
132
+ uploadError.innerText = `Invalid file type: ${url}`;
133
+ return;
134
+ }
135
+ });
136
+
137
  window.addEventListener("click", () => {
138
  window.focus();
139
  });
editor/style.css CHANGED
@@ -5,6 +5,7 @@ html {
5
  overflow: hidden;
6
  background-color: #fff;
7
  font-family: sans-serif;
 
8
  }
9
 
10
  canvas {
@@ -12,23 +13,23 @@ canvas {
12
  height: 100vh;
13
  }
14
 
 
 
 
 
 
 
15
  dialog {
16
  width: 100%;
17
  text-align: center;
18
  max-width: 20em;
19
  color: white;
20
- background-color: #000;
21
- border: none;
22
  position: relative;
23
  transform: translate(-50%, -50%);
24
  }
25
 
26
- #progress-container {
27
- position: absolute;
28
- top: 50%;
29
- left: 50%;
30
- }
31
-
32
  progress {
33
  width: 100%;
34
  height: 1em;
@@ -49,7 +50,7 @@ progress::-moz-progress-bar {
49
  background-color: #eee;
50
  }
51
 
52
- #download-button {
53
  position: absolute;
54
  top: 8px;
55
  right: 8px;
@@ -61,14 +62,167 @@ progress::-moz-progress-bar {
61
  cursor: pointer;
62
  }
63
 
64
- #download-button:hover {
65
- background-color: #000000ee;
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
- #download-button:active {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  background-color: #000000cc;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  }
71
 
72
- #download-button:focus {
73
- outline: none;
 
74
  }
 
5
  overflow: hidden;
6
  background-color: #fff;
7
  font-family: sans-serif;
8
+ font-size: 14px;
9
  }
10
 
11
  canvas {
 
13
  height: 100vh;
14
  }
15
 
16
+ #progress-container {
17
+ position: fixed;
18
+ top: 50%;
19
+ left: 50%;
20
+ }
21
+
22
  dialog {
23
  width: 100%;
24
  text-align: center;
25
  max-width: 20em;
26
  color: white;
27
+ background-color: #222;
28
+ border: 1px solid #888;
29
  position: relative;
30
  transform: translate(-50%, -50%);
31
  }
32
 
 
 
 
 
 
 
33
  progress {
34
  width: 100%;
35
  height: 1em;
 
50
  background-color: #eee;
51
  }
52
 
53
+ .tool-button {
54
  position: absolute;
55
  top: 8px;
56
  right: 8px;
 
62
  cursor: pointer;
63
  }
64
 
65
+ .tool-button:hover {
66
+ background-color: #222222cc;
67
+ }
68
+
69
+ .tool-button:active {
70
+ background-color: #000000cc;
71
+ }
72
+
73
+ .tool-button:focus {
74
+ outline: inherit;
75
+ }
76
+
77
+ .tool-button.active {
78
+ outline: white solid 1px;
79
  }
80
 
81
+ #upload-button {
82
+ top: 8px;
83
+ }
84
+
85
+ #download-button {
86
+ top: 48px;
87
+ }
88
+
89
+ #controls-display-button {
90
+ top: 88px;
91
+ }
92
+
93
+ #controls-display {
94
+ display: none;
95
+ position: absolute;
96
+ top: 96px;
97
+ right: 44px;
98
  background-color: #000000cc;
99
+ margin: 0;
100
+ padding: 6px;
101
+ color: #ddd;
102
+ pointer-events: none;
103
+ }
104
+
105
+ #controls-display.active {
106
+ display: block;
107
+ }
108
+
109
+ #controls-display p {
110
+ margin: 0;
111
+ padding: 1px;
112
+ }
113
+
114
+ .divider {
115
+ width: 256px;
116
+ border: 0;
117
+ border-top: 1px solid #ddd;
118
+ margin: 6px 0;
119
+ }
120
+
121
+ .control-item {
122
+ display: flex;
123
+ justify-content: space-between;
124
+ }
125
+
126
+ .control-name {
127
+ text-align: left;
128
+ color: #aaa;
129
+ }
130
+
131
+ .control-value {
132
+ text-align: right;
133
+ }
134
+
135
+ .modal {
136
+ display: none;
137
+ position: fixed;
138
+ z-index: 1;
139
+ left: 0;
140
+ top: 0;
141
+ width: 100%;
142
+ height: 100%;
143
+ overflow: auto;
144
+ background-color: rgba(0, 0, 0, 0.5);
145
+ }
146
+
147
+ .modal-content {
148
+ background-color: #222222;
149
+ margin: 15% auto;
150
+ padding: 16px;
151
+ border: 1px solid #888;
152
+ width: 80%;
153
+ color: #ddd;
154
+ }
155
+
156
+ .modal-section {
157
+ padding: 4px 0;
158
+ }
159
+
160
+ .modal .divider {
161
+ width: calc(100% - 32px);
162
+ }
163
+
164
+ .close {
165
+ float: right;
166
+ font-size: 24px;
167
+ }
168
+
169
+ .close:hover,
170
+ .closer:focus {
171
+ color: #aaa;
172
+ text-decoration: none;
173
+ cursor: pointer;
174
+ }
175
+
176
+ #url-input {
177
+ width: calc(100% - 96px);
178
+ background-color: #000;
179
+ border: 1px solid #888;
180
+ color: #ddd;
181
+ padding: 4px;
182
+ margin: 0;
183
+ }
184
+
185
+ #upload-submit {
186
+ width: 80px;
187
+ background-color: #000;
188
+ border: 1px solid #888;
189
+ color: #ddd;
190
+ padding: 4px;
191
+ margin: 0;
192
+ cursor: pointer;
193
+ }
194
+
195
+ #upload-submit:hover {
196
+ background-color: #222;
197
+ }
198
+
199
+ #upload-submit:focus {
200
+ outline: inherit;
201
+ }
202
+
203
+ input[type="file"] {
204
+ display: none;
205
+ }
206
+
207
+ #file-input-label {
208
+ width: 80px;
209
+ background-color: #000;
210
+ border: 1px solid #888;
211
+ color: #ddd;
212
+ padding: 4px;
213
+ margin: 0;
214
+ cursor: pointer;
215
+ }
216
+
217
+ #file-input-label:hover {
218
+ background-color: #222;
219
+ }
220
+
221
+ #file-input-label:focus {
222
+ outline: inherit;
223
  }
224
 
225
+ #upload-error {
226
+ display: none;
227
+ color: #f00;
228
  }
style.css CHANGED
@@ -25,4 +25,4 @@ p {
25
 
26
  .card p:last-child {
27
  margin-bottom: 0;
28
- }
 
25
 
26
  .card p:last-child {
27
  margin-bottom: 0;
28
+ }