Skip to content
Snippets Groups Projects

Compare revisions

Changes are shown as if the source revision was being merged into the target revision. Learn more about comparing revisions.

Source

Select target project
No results found

Target

Select target project
  • hlgr/drawing-app
  • sweng-group-15/drawing-app
2 results
Show changes
Showing
with 1843 additions and 300 deletions
File added
File added
File added
<svg viewBox="-10 -10 30 30" xmlns="http://www.w3.org/2000/svg">
<circle cx="10" cy="10" r="10"/>
</svg>
\ No newline at end of file
public/favicon.ico

1.12 KiB

<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 22.0.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 417.7 150.7" style="enable-background:new 0 0 417.7 150.7;" xml:space="preserve">
<style type="text/css">
.st0{fill:#003C70;}
</style>
<path class="st0" d="M28.4,68.2V30.3h7.4v37.9H28.4z"/>
<path class="st0" d="M46.7,44.3h0.1c1.8-2.4,5.2-3.8,8.3-3.8c3.3,0,6.2,1.4,7.8,3.8c2.2-2.2,5.6-3.8,8.8-3.8c6-0.1,9.1,3.7,9.2,9.9
v17.8h-7V51.7c0-3.1-1.1-6.4-4.5-6.4c-3.4,0-5.5,2.1-5.5,6.8v16.2h-7V51.7c0-3.8-1.6-6.4-4.6-6.4c-3.3,0-5.4,2.3-5.4,6.9v16h-7V41.1
h7V44.3z"/>
<path class="st0" d="M98.5,44.8c4,0,6,4.7,6,9.3c0,4.8-1.6,10.4-6.3,10.4c-3.8,0-6.5-3.8-6.5-9.5C91.8,48.7,94.1,44.8,98.5,44.8z
M91.8,41.1h-7v40.7h7V64.8c2,2.4,4.7,4,7.9,4c7.5,0,12.3-7.6,12.3-14.8c0-6-3.4-13.6-11.7-13.6c-3.2,0-6.6,1.6-8.4,4.4h-0.1V41.1z"
/>
<path class="st0" d="M122,51.4c-0.1-3.6,1.7-6.9,5.8-6.9c3.5,0,4.9,2.8,4.6,6.9H122z M139,55.3c0.5-8.1-2.9-14.8-11.1-14.8
c-7.5,0-13.5,5.3-13.5,13.7c0,8.9,6,14.7,13.9,14.7c3,0,6.8-0.9,10.3-3l-2-3.8c-1.7,1.2-4.5,2.2-7.1,2.2c-4.4,0-7.9-3.8-7.7-8.9H139
z"/>
<path class="st0" d="M149.4,45.9h0.1l1.4-2.1c0.8-1.1,2.4-3.3,4.5-3.3c1.6,0,3.3,0.9,4.8,2.4l-2.6,5c-1.2-0.6-1.9-0.9-3.3-0.9
c-2.4,0-4.8,2-4.8,7.9v13.4h-7V41.1h7V45.9z"/>
<path class="st0" d="M169.4,41.1v27.1h-7V41.1H169.4z M161.7,33c0-2.1,1.7-4.2,4.1-4.2c2.4,0,4.3,2.1,4.3,4.2c0,2.3-1.6,4.5-4.2,4.5
C163.4,37.5,161.7,35.3,161.7,33z"/>
<path class="st0" d="M188.5,52.5c0.5,8-3.1,11.5-5.9,11.5c-1.7,0-3.2-1.6-3.2-4c0-3.1,1.7-5.1,5.2-6.2L188.5,52.5z M188.4,64.2
c0,1.5,0.2,3,0.7,4h7.4c-0.8-1.9-1.1-4.4-1.1-6.6V50.5c0-8.5-6-10.1-10.8-10.1c-3.6,0-6.9,0.9-10.1,3.7l2.3,3.4c1.8-1.6,4-2.8,7-2.8
c2.3,0,4.4,1.6,4.8,4.1l-6.2,1.9c-6.1,1.8-9.8,4.7-9.8,9.9c0,5,3.4,8.2,7.5,8.2c2.4,0,4.8-1.7,6.8-3.3L188.4,64.2z"/>
<path class="st0" d="M199.8,68.2V28.4h7v39.8H199.8z"/>
<path class="st0" d="M253.1,37.1c-1.8-1-4.8-2.1-7.7-2.1c-7.6,0-13.1,5.5-13.1,14.1c0,9,6.1,14.3,13.3,14.3c2.9,0,5.5-0.8,7.4-1.9
l2.1,4.7c-2.3,1.4-6.2,2.6-9.8,2.6c-12.8,0-20.9-8.7-20.9-19.8c0-10.4,8-19.4,21-19.4c3.9,0,7.4,1.4,10,2.9L253.1,37.1z"/>
<path class="st0" d="M269.1,64.8c-5,0-6.4-5.7-6.4-10.5c0-4.5,1.6-9.9,6.4-9.9c4.9,0,6.4,5.4,6.4,9.9
C275.5,59.1,274.2,64.8,269.1,64.8z M269.1,68.9c8.3,0,13.9-6.1,13.9-14.5c0-8.8-6.7-13.9-13.9-13.9c-7.1,0-13.8,5.1-13.8,13.9
C255.3,62.7,260.8,68.9,269.1,68.9z"/>
<path class="st0" d="M286,68.2V28.4h7v39.8H286z"/>
<path class="st0" d="M297.4,68.2V28.4h7v39.8H297.4z"/>
<path class="st0" d="M314.9,51.4c-0.1-3.6,1.7-6.9,5.7-6.9c3.5,0,4.9,2.8,4.6,6.9H314.9z M331.9,55.3c0.5-8.1-2.9-14.8-11.1-14.8
c-7.5,0-13.5,5.3-13.5,13.7c0,8.9,6,14.7,13.9,14.7c3,0,6.8-0.9,10.3-3l-2-3.8c-1.7,1.2-4.5,2.2-7.1,2.2c-4.4,0-7.9-3.8-7.7-8.9
H331.9z"/>
<path class="st0" d="M372.2,51.4c-0.1-3.6,1.7-6.9,5.8-6.9c3.5,0,4.9,2.8,4.6,6.9H372.2z M389.2,55.3c0.5-8.1-2.9-14.8-11.1-14.8
c-7.5,0-13.5,5.3-13.5,13.7c0,8.9,6,14.7,13.9,14.7c3,0,6.8-0.9,10.3-3l-2-3.8c-1.7,1.2-4.5,2.2-7.1,2.2c-4.4,0-7.9-3.8-7.7-8.9
H389.2z"/>
<path class="st0" d="M348.3,44.8c4.2,0,6.4,4.4,6.4,9.1c0,5.4-2,10.6-6.7,10.6c-4.1,0-6.1-5.1-6.1-10.4
C342,48.9,343.8,44.8,348.3,44.8z M361.7,41.1h-7v3.3h-0.1c-1.5-2.4-4.6-4-7.9-4c-7.2,0-12.2,6.8-12.2,13.9
c0,8.7,4.6,14.5,11.6,14.5c4,0,6.8-2.3,8.5-4.6h0.1v3.6c0,6.3-3.6,9.2-8.2,9.2c-3.6,0-6.6-0.7-9.2-2.3l-1.4,4.4
c3.1,1.6,7.1,2.6,11.1,2.6c7.9,0,14.7-4,14.7-16V41.1z"/>
<path class="st0" d="M28.4,84.5h7.5v32.8h15.8v5.1H28.4V84.5z"/>
<path class="st0" d="M92.8,98.5c2.5-2.6,6-3.8,9.5-3.8c6.5,0,9.6,3.5,9.6,10.3v17.5h-7.1v-16.6c0-3.8-1.7-6.3-5.6-6.3
c-3.6,0-6.4,2.3-6.4,6.8v16.2h-7.1V95.3h7V98.5z"/>
<path class="st0" d="M130.2,99c4.4,0,6.7,4.4,6.7,9.1c0,5.4-2.1,10.6-7.1,10.6c-4.3,0-6.4-5.1-6.4-10.4S125.4,99,130.2,99z
M136.9,122.4h7.1V82.6h-7.1v15.9h-0.1c-1.7-2.4-4.9-3.8-8.5-3.8c-7.4,0-12.7,6.8-12.7,13.9c0,8.7,4.8,14.5,12.1,14.5
c4.3,0,7.2-2.3,9-4.6h0.1V122.4z"/>
<path class="st0" d="M162.3,119c-5.3,0-6.7-5.7-6.7-10.5c0-4.5,1.7-9.9,6.7-9.9c5.2,0,6.8,5.4,6.8,9.9
C169,113.3,167.6,119,162.3,119z M162.3,123.1c8.7,0,14.6-6.1,14.6-14.5c0-8.8-7.1-13.9-14.6-13.9c-7.4,0-14.5,5.1-14.5,13.9
C147.7,116.9,153.6,123.1,162.3,123.1z"/>
<path class="st0" d="M187.7,98.5c2.5-2.6,6-3.8,9.5-3.8c6.5,0,9.6,3.5,9.6,10.3v17.5h-7.1v-16.6c0-3.8-1.7-6.3-5.6-6.3
c-3.6,0-6.4,2.3-6.4,6.8v16.2h-7.1V95.3h7V98.5z"/>
<path class="st0" d="M67.2,119c-5.3,0-6.7-5.7-6.7-10.5c0-4.5,1.7-9.9,6.7-9.9c5.2,0,6.8,5.4,6.8,9.9C74,113.3,72.6,119,67.2,119z
M67.2,123.1c8.7,0,14.6-6.1,14.6-14.5c0-8.8-7.1-13.9-14.6-13.9c-7.4,0-14.5,5.1-14.5,13.9C52.7,116.9,58.6,123.1,67.2,123.1z"/>
</svg>
......@@ -4,49 +4,39 @@
<meta charset="UTF-8" />
<link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" href="logo.png" />
<link rel="apple-touch-icon" href="logo.png" />
<link rel="stylesheet" href="styles.css" />
<link
href="https://fonts.googleapis.com/css?family=Martel:300,400&display=swap"
rel="stylesheet"
/>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<script>
if (navigator.serviceWorker) {
navigator.serviceWorker
.register("service-worker.js")
.then(
(registration) =>
console.log(
`Service worker registered on scope ${registration.scope}`,
),
(reason) =>
console.log(`Service worker failed to register ~ ${reason}`),
)
navigator.serviceWorker.register("service-worker.js").then(
(registration) =>
console.log(
`Service worker registered on scope ${registration.scope}`,
),
(reason) =>
console.log(`Service worker failed to register ~ ${reason}`),
)
}
</script>
</head>
<body>
<div style="display:none">
Your client ID: <input id="user-id" type="text" value="" readonly="" />
</div>
<div style="display:none">
Enter peer ID: <input id="peer-id" type="text" value="" />
<button id="peer-connect">Connect</button>
</div>
<div id="top-panel">
<div class="top-bar">
<div class="dropdown">
<button class="dropdown-peers"><i class="fa fa-bars"></i></button>
<button class="dropdown-peers"><i class="fas fa-bars"></i></button>
<div class="peers">
<ul id="connected-peers">
No peers are connected
</ul>
</div>
</div>
<div id="overall-status-icon" class="synchronised">
<img
id="overall-status-icon-img"
src="synchronised.svg"
alt="Synchronisation status icon"
/>
</div>
<input
id="room-id"
type="text"
......@@ -55,11 +45,11 @@
size="25"
color="gray"
/>
<button id="room-connect">Connect <i class="fa fa-link"></i></button>
<button id="room-connect">Connect <i class="fas fa-link"></i></button>
</div>
<div id="tools-panel">
<button id="pen-tool" class="selected">
<i class="fa fa-paint-brush"></i>
<button id="pen-tool" class="selectable-tool selected">
<i class="fas fa-paint-brush"></i>
</button>
<div id="pen-properties" class="properties">
<div class="pen-contents">
......@@ -71,18 +61,42 @@
<div id="brush-colour">
<h3>Brush colour</h3>
</div>
<a href="#" style="margin-left: 1%">
<div id="rectangle"></div>
</a>
</div>
<div class="pen-body">
<div id="brush-colour">
<h3>Brush size</h3>
</div>
&nbsp;
<div id="rectangle"></div>
<div class="slide-size">
<input
type="range"
min="1"
max="10"
class="slider"
id="range"
/>
<p style="text-align: center; margin: 0px">
Size: <span id="value"></span>
</p>
</div>
</div>
</div>
</div>
<div id="palette" class="properties">
<div id="palette" class="properties" style="padding-top: 150px">
<div class="palette-selector">
<div class="wheel-header">
<span class="close">&times</span>
<h2>Select a colour from the palette</h2>
</div>
<svg
id="wheel"
viewBox="0 0 100 100"
viewBox="0 10 100 70"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
style="@media only screen and (max-width: 600px) { viewBox: -50 10 200 100 }"
>
<defs>
<path
......@@ -90,73 +104,126 @@
d="M 42.2,78.9 46.1,64.4 Q 50,65 53.88,64.48 L 57.7,78.9 Q 50,80 42.2,78.9Z"
/>
</defs>
<use xlink:href="#piePiece" fill="#2D8633" />
<use
xlink:href="#piePiece"
transform="rotate(30,50,50)"
fill="#43a1cd"
/>
<use
xlink:href="#piePiece"
transform="rotate(60,50,50)"
fill="#639b47"
/>
<use
xlink:href="#piePiece"
transform="rotate(90,50,50)"
fill="#9ac147"
/>
<use
xlink:href="#piePiece"
transform="rotate(120,50,50)"
fill="#e1e23b"
/>
<use
xlink:href="#piePiece"
transform="rotate(150,50,50)"
fill="#f7941e"
/>
<use
xlink:href="#piePiece"
transform="rotate(180,50,50)"
fill="#ba3e2e"
/>
<use
xlink:href="#piePiece"
transform="rotate(210,50,50)"
fill="#9a1d34"
/>
<use
xlink:href="#piePiece"
transform="rotate(240,50,50)"
fill="#662a6c"
/>
<use
xlink:href="#piePiece"
transform="rotate(270,50,50)"
fill="#272b66"
/>
<use
xlink:href="#piePiece"
transform="rotate(300,50,50)"
fill="#2d559f"
/>
<use
xlink:href="#piePiece"
transform="rotate(330,50,50)"
fill="#A9AA39"
/>
<a href="#">
<use class="wheelPiece" xlink:href="#piePiece" fill="#329739" />
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(30,50,50)"
fill="#639b47"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(60,50,50)"
fill="#9ac147"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(90,50,50)"
fill="#A9AA39"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(120,50,50)"
fill="#e1e23b"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(150,50,50)"
fill="#f7941e"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(180,50,50)"
fill="#ba3e2e"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(210,50,50)"
fill="#9a1d34"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(240,50,50)"
fill="#662a6c"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(270,50,50)"
fill="#272b66"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(300,50,50)"
fill="#2d559f"
/>
<use
class="wheelPiece"
xlink:href="#piePiece"
transform="rotate(330,50,50)"
fill="#43a1cd"
/>
</a>
</svg>
<div id="others">
<div id="other-palette">
<b>Other colours</b>
</div>
<label id="colours">
<input id="other-colours" type="color" />
</label>
</div>
</div>
</div>
<button id="eraser-tool"><i class="fa fa-eraser"></i></button>
<div id="connected-room-info">
You are connected to a room: <span id="connected-room-id"></span>
<button id="eraser-tool" class="selectable-tool">
<i class="fas fa-eraser"></i>
</button>
<button id="dragging-tool" class="selectable-tool">
<i class="far fa-hand-paper"></i>
</button>
<button id="canvas-center" class="selectable-tool">
<i class="fas fa-crosshairs"></i>
</button>
<button id="recognition-mode" class="selectable-tool">
<i class="fas fa-square"></i>
</button>
<div class="spacer"></div>
<div id="status-info">
<button id="fast-undo-tool" class="disabled selectable-tool">
<i class="fas fa-fast-backward"></i>
</button>
<button id="undo-tool" class="disabled selectable-tool">
<i class="fas fa-step-backward"></i>
</button>
<div id="connected-room-info">
Room:&nbsp;
<span id="connected-room-id"></span>
</div>
<div id="user-avatar"></div>
<div id="imperial-logo">
<img
src="imperial.svg"
alt="Imperial College London Logo"
height="42px"
/>
</div>
</div>
</div>
</div>
<svg id="canvas"></svg>
<div id="canvas-container">
<svg id="canvas"></svg>
</div>
<script src="js/app.js"></script>
</body>
......
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="green" stroke="green" d="M8.213 16.984c.97-1.028 2.308-1.664 3.787-1.664s2.817.636 3.787 1.664l-3.787 4.016-3.787-4.016zm-1.747-1.854c1.417-1.502 3.373-2.431 5.534-2.431s4.118.929 5.534 2.431l2.33-2.472c-2.012-2.134-4.793-3.454-7.864-3.454s-5.852 1.32-7.864 3.455l2.33 2.471zm-4.078-4.325c2.46-2.609 5.859-4.222 9.612-4.222s7.152 1.613 9.612 4.222l2.388-2.533c-3.071-3.257-7.313-5.272-12-5.272s-8.929 2.015-12 5.272l2.388 2.533z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="red" d="M8.213 16.984c.97-1.028 2.308-1.664 3.787-1.664s2.817.636 3.787 1.664l-3.787 4.016-3.787-4.016zm3.787-6.78c2.387 0 4.648.876 6.461 2.485l-.969 1.028c-1.556-1.308-3.472-2.018-5.492-2.018-2.021 0-3.937.71-5.492 2.018l-.969-1.028c1.813-1.609 4.075-2.485 6.461-2.485zm0-1c-3.071 0-5.852 1.32-7.864 3.455l2.33 2.472c1.417-1.502 3.373-2.431 5.534-2.431s4.117.929 5.534 2.431l2.33-2.472c-2.012-2.135-4.793-3.455-7.864-3.455zm0-5.204c3.949 0 7.682 1.517 10.607 4.291l-1.021 1.083c-2.656-2.452-6.023-3.791-9.586-3.791s-6.93 1.339-9.586 3.791l-1.021-1.083c2.926-2.774 6.658-4.291 10.607-4.291zm0-1c-4.687 0-8.929 2.015-12 5.272l2.388 2.533c2.46-2.609 5.859-4.222 9.612-4.222 3.754 0 7.152 1.613 9.611 4.222l2.389-2.533c-3.071-3.257-7.313-5.272-12-5.272z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="orange" d="M8.213 16.984c.97-1.028 2.308-1.664 3.787-1.664s2.817.636 3.787 1.664l-3.787 4.016-3.787-4.016zm-1.747-1.854c1.417-1.502 3.373-2.431 5.534-2.431s4.118.929 5.534 2.431l2.33-2.472c-2.012-2.134-4.793-3.454-7.864-3.454s-5.852 1.32-7.864 3.455l2.33 2.471zm5.534-11.13c3.949 0 7.681 1.517 10.607 4.291l-1.021 1.083c-2.656-2.452-6.023-3.791-9.586-3.791s-6.93 1.339-9.586 3.791l-1.021-1.083c2.926-2.774 6.658-4.291 10.607-4.291zm0-1c-4.687 0-8.929 2.015-12 5.272l2.388 2.533c2.46-2.609 5.859-4.222 9.612-4.222s7.152 1.613 9.612 4.222l2.388-2.533c-3.071-3.257-7.313-5.272-12-5.272z"/></svg>
......@@ -3,6 +3,10 @@ body {
height: 100%;
}
#console {
color: white;
}
body {
background-color: black;
margin: 0;
......@@ -10,11 +14,21 @@ body {
flex-direction: column;
}
#canvas {
#canvas-container {
width: calc(100vw - 8px);
background-color: white;
border: 4px solid red;
flex-grow: 1;
position: relative;
overflow: hidden;
}
#canvas {
position: absolute;
left: -5000px;
top: -5000px;
width: 10000px;
height: 10000px;
}
#tools-panel {
......@@ -23,11 +37,18 @@ body {
align-items: center;
}
.spacer {
flex: 1;
}
#connected-room-info {
display: none;
color: white;
flex: 1;
text-align: right;
display: flex;
align-items: center;
background-color: #4f4f4fb7;
border-radius: 4px;
justify-content: center;
padding: 0.75em;
}
button.selected {
......@@ -60,7 +81,7 @@ button.selected {
cursor: pointer;
border-radius: 4px;
transition-duration: 0.4s;
height: 100%;
height: 36px;
width: 36px;
}
......@@ -71,7 +92,7 @@ button.selected {
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 999;
padding: 16px;
padding: 8px;
}
.peers a {
......@@ -92,15 +113,113 @@ button.selected {
margin: 0;
}
@keyframes connected-peers-slide {
from {
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0px);
}
}
#connected-peers li {
animation: connected-peers-slide 0.5s forwards;
border-bottom: 1px solid black;
padding: 4px 0;
opacity: 0;
display: flex;
align-items: center;
}
#connected-peers li:nth-child(2) {
animation-delay: 0.125s;
}
#connected-peers li:nth-child(3) {
animation-delay: 0.25s;
}
#connected-peers li:nth-child(4) {
animation-delay: 0.375s;
}
#connected-peers li:nth-child(5) {
animation-delay: 0.5s;
}
#connected-peers li:nth-child(6) {
animation-delay: 0.625s;
}
#connected-peers li:nth-child(7) {
animation-delay: 0.75s;
}
#connected-peers li:nth-child(8) {
animation-delay: 0.875s;
}
#connected-peers li:last-child {
border-bottom-width: 0px;
}
.peer-quality,
.peer-status {
width: 15px;
height: 15px;
margin-right: 5px;
}
.peer-status {
border-radius: 10px;
}
.peer-status::after {
font-size: 0.5em;
margin-left: 10px;
background-color: white;
border-radius: 50%;
}
.peer-status.upload::after {
content: "▲";
padding: 0.5px;
}
.peer-status.download::after {
content: "▼";
padding: 1px;
}
.peer-status.unsynced {
color: gray;
background-color: gray;
}
@keyframes peer-status-negotiating {
from {
color: gray;
background-color: gray;
}
50%,
to {
color: orange;
background-color: orange;
}
}
.peer-status.negotiating {
animation: peer-status-negotiating 0.5s step-end infinite;
background-color: orange;
color: orange;
}
.peer-status.synced {
color: green;
background-color: green;
}
.dropdown:hover .peers {
display: block;
}
......@@ -109,6 +228,36 @@ button.selected {
background-color: #4f4f4f;
}
#overall-status-icon {
pointer-events: none;
border-radius: 50%;
width: 16px;
height: 16px;
padding: 4px;
margin-left: -18px;
margin-top: -4px;
z-index: 0;
}
@keyframes overall-status-synchronising {
to {
transform: rotate(-360deg);
}
}
#overall-status-icon.synchronising {
background-color: orange;
animation: overall-status-synchronising 2s linear infinite;
}
#overall-status-icon.synchronised {
background-color: green;
}
#overall-status-icon-img {
height: inherit;
}
#room-id {
flex: 1;
padding: 10px;
......@@ -126,9 +275,13 @@ button.selected {
border: none;
cursor: pointer;
border-radius: 4px;
width: 78px;
height: 36px;
white-space: nowrap;
overflow: hidden;
}
#pen-tool {
.selectable-tool {
background-color: #2f2f2f;
color: white;
padding: 10px;
......@@ -136,27 +289,89 @@ button.selected {
border: none;
cursor: pointer;
border-radius: 50%;
margin-right: 8px;
}
#pen-tool:hover {
.selectable-tool:hover {
background-color: #4f4f4f !important;
transition-duration: 0.4s;
}
#pen-tool {
width: 39px;
height: 39px;
margin-right: 8px;
}
#pen-tool > i {
padding: 0 1.5px;
}
#eraser-tool {
background-color: #2f2f2f;
color: white;
padding: 10px;
font-size: 16px;
border: none;
cursor: pointer;
border-radius: 50%;
width: 39px;
height: 39px;
margin-right: 8px;
}
#eraser-tool:hover {
background-color: #4f4f4f !important;
transition-duration: 0.4s;
#eraser-tool > i {
padding: 0 1.5px;
}
#dragging-tool {
width: 39px;
height: 39px;
margin-right: 8px;
}
#dragging-tool > i {
padding: 0 2.5px;
}
#canvas-center {
width: 39px;
height: 39px;
margin-right: 8px;
}
#canvas-center > i {
padding: 0 1.5px;
}
#recognition-mode {
width: 39px;
height: 39px;
margin-right: 8px;
}
#recognition-mode > i {
padding: 0 2.5px;
}
#undo-tool {
width: 39px;
height: 39px;
margin-right: 8px;
}
#undo-tool.disabled {
display: none;
}
#undo-tool > i {
padding: 0 2.5px;
}
#fast-undo-tool {
width: 39px;
height: 39px;
margin-right: 8px;
}
#fast-undo-tool.disabled {
display: none;
}
#fast-undo-tool > i {
padding: 0 1.5px;
}
.properties {
......@@ -183,7 +398,7 @@ button.selected {
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.1);
animation-name: animatetop;
animation-duration: 0.4s;
font-family: "Martel", serif;
font-family: "Martel", sans-serif;
}
.close {
......@@ -238,5 +453,189 @@ button.selected {
width: 500px;
height: 500px;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.1);
font-family: "Martel", serif;
font-family: "Martel", sans-serif;
}
.wheelPiece {
opacity: 0.8;
}
.wheelPiece:hover {
opacity: 1;
}
.wheel-header {
padding: 2px 16px;
background-color: #2f2f2f;
color: white;
text-align: center;
}
.slide-size {
width: 90%;
height: 20%;
margin-left: 2.2%;
}
.slider {
-webkit-appearance: none;
appearance: none;
width: 100%;
height: 25px;
background: #d3d3d3;
outline: none;
opacity: 0.7;
-webkit-transition: 0.2s;
transition: opacity 0.2s;
margin-top: 30px;
}
.slider:hover {
opacity: 1;
}
.slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 25px;
height: 25px;
background: #4f4f4f;
cursor: pointer;
}
.slider::-moz-range-thumb {
width: 25px;
height: 25px;
background: #4f4f4f;
cursor: pointer;
}
@media only screen and (max-width: 600px) {
.properties {
display: none;
position: fixed;
z-index: 1;
padding-top: 80px;
left: 0;
top: 0;
width: 100%;
height: 100%;
overflow: auto;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.4);
}
.palette-selector {
position: relative;
background-color: #fefefe;
margin: auto;
padding: 0;
border: 1px solid #888;
width: 80%;
height: 80%;
box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.1);
font-family: "Martel", serif;
}
.wheel-header {
padding: 2px 16px;
background-color: #2f2f2f;
color: white;
text-align: center;
}
}
#others {
margin-top: 2%;
display: flex;
align-items: center;
justify-content: center;
background-color: #3cbc8d;
color: white;
padding-top: 4px;
padding-bottom: 4px;
}
#other-colours {
background-color: #3cbc8d;
}
#colours {
background-color: blue;
height: 32px;
width: 64px;
border: 4px solid #555;
border-radius: 4px;
opacity: 0.5;
margin-left: 2%;
}
#colours:hover {
border: 4px solid #4f4f4f;
border-radius: 4px;
opacity: 1;
transition-duration: 0.4s;
}
@supports (-webkit-overflow-scrolling: touch) {
#colours {
visibility: hidden;
}
#other-colours {
visibility: visible;
}
}
@supports not (-webkit-overflow-scrolling: touch) {
#other-colours {
visibility: hidden;
}
}
@font-face {
font-family: "Martel";
font-style: normal;
font-weight: 400;
src: url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.eot");
src: local("Martel"), local("Martel-Regular"),
url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.eot?#iefix")
format("embedded-opentype"),
url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.woff2")
format("woff2"),
url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.woff")
format("woff"),
url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.ttf")
format("truetype"),
url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.svg#Martel")
format("svg");
}
.avatar {
margin-right: 5px;
}
#user-avatar {
color: white;
display: flex;
align-items: center;
background-color: #4f4f4fb7;
border-radius: 4px;
margin-left: 0.75em;
padding: 0 0.75em 0 0;
}
#status-info {
display: flex;
align-items: center;
justify-content: right;
}
#imperial-logo {
background-color: white;
display: flex;
align-items: center;
border-radius: 4px;
margin-left: 0.75em;
padding: 0;
}
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path stroke="white" fill="white" d="M23 12c0 1.042-.154 2.045-.425 3h-2.101c.335-.94.526-1.947.526-3 0-4.962-4.037-9-9-9-1.706 0-3.296.484-4.654 1.314l1.857 2.686h-6.994l2.152-7 1.85 2.673c1.683-1.049 3.658-1.673 5.789-1.673 6.074 0 11 4.925 11 11zm-6.354 7.692c-1.357.826-2.944 1.308-4.646 1.308-4.963 0-9-4.038-9-9 0-1.053.191-2.06.525-3h-2.1c-.271.955-.425 1.958-.425 3 0 6.075 4.925 11 11 11 2.127 0 4.099-.621 5.78-1.667l1.853 2.667 2.152-6.989h-6.994l1.855 2.681zm.354-10.283l-1.421-1.409-5.105 5.183-2.078-2.183-1.396 1.435 3.5 3.565 6.5-6.591z"/></svg>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path fill="white" stroke="white" d="M18.312 5.595l1.296-1.527c.727.698 1.354 1.495 1.869 2.369l-1.776.931c-.39-.648-.853-1.246-1.389-1.773zm3.844 2.179l-1.787.937c.255.647.435 1.33.533 2.039h2.021c-.117-1.043-.378-2.042-.767-2.976zm-14.81-3.46c1.358-.83 2.948-1.314 4.654-1.314 1.919 0 3.695.608 5.157 1.636l1.298-1.529c-1.814-1.319-4.04-2.107-6.455-2.107-2.131 0-4.106.624-5.789 1.673l-1.85-2.673-2.152 7h6.994l-1.857-2.686zm13.642 7.936c-.027.962-.206 1.885-.514 2.75h2.101c.249-.878.401-1.798.425-2.75h-2.012zm-15.3 6.155l-1.295 1.527c-.727-.698-1.355-1.495-1.869-2.369l1.775-.931c.39.648.853 1.246 1.389 1.773zm-3.844-2.179l1.787-.937c-.254-.647-.434-1.33-.533-2.039h-2.022c.119 1.043.379 2.042.768 2.976zm14.81 3.46c-1.357.83-2.947 1.314-4.654 1.314-1.918 0-3.695-.608-5.156-1.636l-1.299 1.529c1.814 1.319 4.041 2.107 6.455 2.107 2.131 0 4.107-.624 5.789-1.673l1.85 2.673 2.152-7h-6.994l1.857 2.686zm-13.642-7.936c.027-.962.207-1.885.513-2.75h-2.1c-.249.878-.402 1.798-.425 2.75h2.012z"/></svg>
File added
// Room connection and synchronisation.
// Translate local canvas input events to draw messages and send to the room.
// Translate local canvas input events to draw messages in light of current tool
// selections and send to the room.
// Get back room updates and invoke the local canvas renderer.
import "@fortawesome/fontawesome-free/css/fontawesome.css"
import "@fortawesome/fontawesome-free/css/regular.css"
import "@fortawesome/fontawesome-free/css/solid.css"
import * as canvas from "./canvas.js"
import * as HTML from "./elements.js"
import { computeErasureIntervals } from "./erasure.js"
import { connect } from "./room.js"
const TEST_ROOM = "imperial"
import CRDT from "./wasm-crdt.js"
//import CRDT from "./y-crdt.js"
import Exfiltrator from "./intelligence-exfiltrator.js"
import WebRTCConnection from "./connection/WebRTC.js"
import * as toolSelection from "./tool-selection.js"
import recognizeFromPoints, { Shapes } from "./shapes.js"
import * as humanhash from "humanhash"
import jdenticon from "jdenticon"
const DEFAULT_ROOM = "imperial"
const navigateToRoom = (roomID) => {
const url = new URL(location.href)
url.searchParams.set("room", roomID)
location.href = url
}
const initialRoom = new URLSearchParams(location.search).get("room")
if (initialRoom == null) {
navigateToRoom(DEFAULT_ROOM)
}
const MIN_PRESSURE_FACTOR = 0.1
const MAX_PRESSURE_FACTOR = 1.5
const UNDO_RATE = 24
let undoInterval = null
let spy = null
let room = null
const humanHasher = new humanhash()
const PREDICTED_POINT_COLOR = "#00000044"
const pathIDsByPointerID = new Map()
// Safari reports mouse touches as events with 0 pressure, but the standard requires
// it to be 0.5
function getNormalizedPressure(e) {
if (e.pressure === 0 && e.buttons) {
return 0.5
}
return e.pressure
}
// This is a quadratic such that:
// - getPressureFactor(0.0) = MIN_PRESSURE_FACTOR
// - getPressureFactor(0.5) = 1.0
// - getPressureFactor(1.0) = MAX_PRESSURE_FACTOR
// For sensible results, maintain that:
// - 0.0 <= MIN_PRESSURE_FACTOR <= 1.0
// - 1.0 <= MAX_PRESSURE_FACTOR
// For intuitive results, maintain that:
// - MAX_PRESSURE_FACTOR <= ~2.0
const getPressureFactor = (pressure) => {
const a = 2 * (MAX_PRESSURE_FACTOR + MIN_PRESSURE_FACTOR) - 4
const b = -MAX_PRESSURE_FACTOR - 3 * MIN_PRESSURE_FACTOR + 4
const c = MIN_PRESSURE_FACTOR
return a * pressure ** 2 + b * pressure + c
}
function selectedRadiusPoint(x, y, color, pressure = 0) {
return [
x,
y,
toolSelection.getStrokeRadius() * getPressureFactor(pressure),
color,
]
}
function faintPredictionPoint(x, y, pressure = 0.5) {
return selectedRadiusPoint(x, y, PREDICTED_POINT_COLOR, pressure)
}
function selectedColorAndRadiusPoint(x, y, pressure = 0.5) {
return selectedRadiusPoint(x, y, toolSelection.getStrokeColour(), pressure)
}
function eraseEverythingAtPosition(x, y, radius, room) {
const mousePos = [x, y]
room.getPaths().forEach((points, pathID) => {
const newPathIntervals = computeErasureIntervals(points, mousePos, radius)
Object.keys(newPathIntervals).forEach((pointID) =>
room.extendErasureIntervals(pathID, pointID, newPathIntervals[pointID]),
)
})
}
const onUserIDAllocated = (uid) => {
const userID = uid.replace(/-/g, "")
// Create user account
const avatarImage = document.createElement("svg")
avatarImage.innerHTML = jdenticon.toSvg(userID, 40)
avatarImage.className = "avatar"
const userAccount = document.createElement("div")
userAccount.innerHTML = humanHasher.humanize(userID, 2)
HTML.userInfo.innerHTML = ""
HTML.userInfo.appendChild(avatarImage)
HTML.userInfo.appendChild(userAccount)
}
const onRoomConnect = (room_) => {
spy = new Exfiltrator(room_.name, room_)
room = room_
HTML.connectedRoomID.textContent = room.name
HTML.connectedRoomInfoContainer.style.display = "block"
HTML.userIDElem.value = room.ownID || ""
room.addEventListener("allocateOwnID", ({ detail: id }) => {
HTML.userIDElem.value = id
})
const uid = room.getUserID()
if (uid) {
onUserIDAllocated(uid)
}
room.addEventListener("allocateOwnID", ({ detail: id }) =>
onUserIDAllocated(id),
)
room.addEventListener("userJoin", ({ detail: id }) => {
if (HTML.connectedPeers.children.length == 0) {
HTML.connectedPeers.innerHTML = ""
}
getOrInsertPeerById(id)
const peerElem = document.createElement("li")
peerElem.innerHTML = id
HTML.connectedPeers.appendChild(peerElem)
updateOverallStatusIcon()
})
room.addEventListener("userLeave", ({ detail: id }) => {
for (const peerElem of HTML.connectedPeers.children) {
if (peerElem.innerHTML == id) {
HTML.connectedPeers.removeChild(peerElem)
}
}
HTML.connectedPeers.removeChild(getOrInsertPeerById(id))
if (HTML.connectedPeers.children.length == 0) {
HTML.connectedPeers.innerHTML = "No peers are connected"
}
updateOverallStatusIcon()
})
room.addEventListener("userConnection", ({ detail: { id, quality } }) => {
const high = "/quality-high.svg"
const medium = "/quality-medium.svg"
const low = "/quality-low.svg"
const peer = getOrInsertPeerById(id).children[1]
if (quality < 0.33) {
if (!peer.src.includes(high)) {
peer.src = high
}
} else if (quality < 0.66) {
if (!peer.src.includes(medium)) {
peer.src = medium
}
} else {
if (!peer.src.includes(low)) {
peer.src = low
}
}
})
room.addEventListener("weSyncedWithPeer", ({ detail: id }) => {
getOrInsertPeerById(id).children[2].className = "peer-status upload synced"
updateOverallStatusIcon()
})
room.addEventListener("waitingForSyncStep", ({ detail: id }) => {
getOrInsertPeerById(id).children[3].className =
"peer-status download negotiating"
updateOverallStatusIcon()
})
room.addEventListener("peerSyncedWithUs", ({ detail: id }) => {
getOrInsertPeerById(id).children[3].className =
"peer-status download synced"
updateOverallStatusIcon()
})
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
canvas.renderPath(id, points)
spy.onAddOrUpdatePath(id, points)
canvas.renderPath(id, points, room.getErasureIntervals(id))
})
room.addEventListener(
"removedIntervalsChange",
({ detail: { id, intervals } }) => {
spy.onRemovedIntervalsChange(id, intervals)
canvas.renderPath(id, room.getPathPoints(id), intervals)
},
)
room.addEventListener("undoEnabled", () => {
HTML.fastUndoButton.classList.remove("disabled")
HTML.undoButton.classList.remove("disabled")
})
// TODO: Move this iPhone specific code to the general user interaction to avoid double events
/*HTML.canvas.addEventListener("touchstart", (e) => {
e.preventDefault()
let pressure = 0
let x, y
const topPanelHeight = HTML.topPanel.offsetHeight
if (
e.touches &&
e.touches[0] &&
typeof e.touches[0]["force"] !== "undefined"
) {
if (e.touches[0]["force"] > 0) {
pressure = e.touches[0]["force"]
}
x = e.touches[0].pageX
y = e.touches[0].pageY - topPanelHeight
} else {
pressure = 1.0
x = e.pageX
y = e.pageY - topPanelHeight
}
clearRecognizedUpcoming()
if (room == null) {
return
}
const currentTool = toolSelection.getTool()
const mousePos = [x, y]
if (currentTool == toolSelection.Tools.PEN) {
pathIDsByPointerID.set(
e.pointerId,
room.addPath(selectedColorAndRadiusPoint(...mousePos, pressure)),
)
} else if (currentTool == toolSelection.Tools.ERASER) {
eraseEverythingAtPosition(
...mousePos,
toolSelection.getEraseRadius(),
room,
)
}
})
HTML.canvas.addEventListener("touchmove", (e) => {
let pressure = 0
let x, y
const topPanelHeight = HTML.topPanel.offsetHeight
if (
e.touches &&
e.touches[0] &&
typeof e.touches[0]["force"] !== "undefined"
) {
if (e.touches[0]["force"] > 0) {
pressure = e.touches[0]["force"]
}
x = e.touches[0].pageX
y = e.touches[0].pageY - topPanelHeight
} else {
pressure = 1.0
x = e.pageX
y = e.pageY - topPanelHeight
}
if (room == null) {
return
}
const currentTool = toolSelection.getTool()
const mousePos = [x, y]
if (currentTool == toolSelection.Tools.PEN) {
const pathID = pathIDsByPointerID.get(e.pointerId)
room.extendPath(
pathID,
selectedColorAndRadiusPoint(...mousePos, pressure),
)
if (toolSelection.isRecognitionModeSet()) {
drawRecognizedUpcoming(room.getPoints(pathID), pressure)
}
} else if (currentTool == toolSelection.Tools.ERASER) {
eraseEverythingAtPosition(
...mousePos,
toolSelection.getEraseRadius(),
room,
)
}
})
HTML.canvas.addEventListener("touchend", (e) => {
const { pointerId } = e
const pressure = getNormalizedPressure(e)
const pathID = pathIDsByPointerID.get(pointerId)
if (toolSelection.isRecognitionModeSet()) {
drawRecognized(pathID, room.getPoints(pathID), pressure)
}
pathIDsByPointerID.delete(pointerId)
clearRecognizedUpcoming()
})
HTML.canvas.addEventListener("touchleave", (e) => {
const { pointerId } = e
const pressure = getNormalizedPressure(e)
const pathID = pathIDsByPointerID.get(pointerId)
if (toolSelection.isRecognitionModeSet()) {
drawRecognized(pathID, room.getPoints(pathID), pressure)
}
pathIDsByPointerID.delete(pointerId)
clearRecognizedUpcoming()
})*/
}
function getRecognizedShapePoints(points) {
const recognizedShape = recognizeFromPoints(points)
if (!recognizedShape.shape) return undefined
switch (recognizedShape.shape) {
case Shapes.line: {
const p1 = recognizedShape.firstPoint
const p2 = recognizedShape.lastPoint
return [p1, p2]
}
case Shapes.rectangle: {
return recognizedShape.boundingPoints
}
}
return undefined
}
function drawIfRecognized(points, callback, notRecCallback) {
const recognizedPoints = getRecognizedShapePoints(points)
if (recognizedPoints) {
callback && callback(recognizedPoints)
} else {
notRecCallback && notRecCallback()
}
}
function clearRecognizedUpcoming() {
canvas.renderPath(canvas.LAST_RECOGNIZED_PATH_ID, [], [])
}
function drawRecognizedUpcoming(points) {
drawIfRecognized(
points,
(recognizedPoints) =>
canvas.renderPath(
canvas.LAST_RECOGNIZED_PATH_ID,
recognizedPoints.map((point) =>
faintPredictionPoint(point[0], point[1]),
),
[],
),
clearRecognizedUpcoming,
)
}
function drawRecognized(pathID, points) {
drawIfRecognized(points, (newPoints) =>
room.replacePath(
pathID,
newPoints.map((point) => selectedColorAndRadiusPoint(point[0], point[1])),
),
)
clearRecognizedUpcoming()
}
const tryRoomConnect = async (roomID) => {
return await connect(roomID)
return await connect(roomID, CRDT, WebRTCConnection, {
wasm: { interval: 16 },
})
.then(onRoomConnect)
.catch((err) => alert(`Error connecting to a room:\n${err}`))
}
const ERASER_RADIUS = canvas.STROKE_RADIUS * 2
const tools = {
PEN: Symbol("pen"),
ERASER: Symbol("eraser"),
const onRoomJoinEnter = () => {
navigateToRoom(HTML.roomIDElem.value)
}
let currentTool = tools.PEN
const pathIDsByPointerID = new Map()
HTML.penButton.addEventListener("click", () => {
if (currentTool == tools.PEN) {
HTML.properties.style.display = "block"
} else {
currentTool = tools.PEN
HTML.penButton.classList.add("selected")
HTML.eraserButton.classList.remove("selected")
}
})
HTML.roomConnectButton.addEventListener("click", onRoomJoinEnter)
HTML.span.addEventListener("click", () => {
HTML.properties.style.display = "none"
})
HTML.fastUndoButton.addEventListener("click", () => {
if (room == null) return
room.fastUndo()
window.addEventListener("click", (event) => {
if (event.target == HTML.properties) {
HTML.properties.style.display = "none"
} else if (event.target == HTML.palette) {
HTML.palette.style.display = "none"
HTML.properties.style.display = "none"
if (!room.canUndo()) {
HTML.fastUndoButton.classList.add("disabled")
HTML.undoButton.classList.add("disabled")
}
})
HTML.rectangle.addEventListener("click", () => {
HTML.palette.style.display = "block"
})
const undoButtonEnd = () => clearInterval(undoInterval)
var svg = HTML.wheel.children
for (var i = 1; i < svg.length; i++) {
svg[i].addEventListener("click", (event) => {
var paletteColour = event.target.getAttribute("fill")
HTML.rectangle.style.backgroundColor = paletteColour
canvas.setStrokeColour(paletteColour)
HTML.palette.style.display = "none"
})
const undoButtonStart = () => {
undoInterval = setInterval(function() {
if (room == null) return
room.undo()
if (!room.canUndo()) {
HTML.fastUndoButton.classList.add("disabled")
HTML.undoButton.classList.add("disabled")
}
}, 1000 / UNDO_RATE)
}
HTML.eraserButton.addEventListener("click", () => {
currentTool = tools.ERASER
HTML.penButton.classList.remove("selected")
HTML.eraserButton.classList.add("selected")
})
HTML.undoButton.addEventListener("pointerup", undoButtonEnd)
HTML.undoButton.addEventListener("pointerleave", undoButtonEnd)
HTML.undoButton.addEventListener("mouseup", undoButtonEnd)
HTML.peerButton.addEventListener("click", () => {
const peerID = HTML.peerIDElem.value
if (room == null || peerID == "") {
return
HTML.undoButton.addEventListener("pointerdown", undoButtonStart)
HTML.roomIDElem.addEventListener("keydown", (event) => {
if (event.key == "Enter") {
event.target.blur()
onRoomJoinEnter()
}
room.inviteUser(peerID)
HTML.peerIDElem.value = ""
})
HTML.roomConnectButton.addEventListener("click", () => {
const selectedRoomID = HTML.roomIDElem.value
if (!selectedRoomID || selectedRoomID == room.name) {
return
const getOrInsertPeerById = (id) => {
id = id.replace(/-/g, "")
for (const peerElem of HTML.connectedPeers.children) {
const peerId = peerElem.children[4].id
if (peerId == id) {
return peerElem
}
}
if (room != null) {
room.disconnect()
room = null
const peerElem = document.createElement("li")
const avatarImage = document.createElement("svg")
avatarImage.innerHTML = jdenticon.toSvg(id, 50)
avatarImage.className = "avatar"
const quality = document.createElement("img")
quality.src = "/quality-high.svg"
quality.alt = "Peer quality icon"
quality.className = "peer-quality"
const ourStatus = document.createElement("div")
ourStatus.className = "peer-status upload unsynced"
const theirStatus = document.createElement("div")
theirStatus.className = "peer-status download unsynced"
const peerId = document.createElement("div")
peerId.style.marginLeft = "5px"
peerId.id = id
peerId.innerHTML = humanHasher.humanize(id, 2)
peerElem.appendChild(avatarImage)
peerElem.appendChild(quality)
peerElem.appendChild(ourStatus)
peerElem.appendChild(theirStatus)
peerElem.appendChild(peerId)
if (HTML.connectedPeers.children.length == 0) {
HTML.connectedPeers.innerHTML = ""
}
canvas.clear()
HTML.connectedPeers.innerHTML = "No peers are connected"
HTML.connectedPeers.appendChild(peerElem)
tryRoomConnect(selectedRoomID)
})
return peerElem
}
const getDistance = (a, b) => {
return Math.sqrt(
(a[0] - b[0]) * (a[0] - b[0]) + (a[1] - b[1]) * (a[1] - b[1]),
)
const updateOverallStatusIcon = () => {
for (const peerElem of HTML.connectedPeers.children) {
if (
!peerElem.children[2].classList.contains("synced") ||
!peerElem.children[3].classList.contains("synced")
) {
HTML.overallStatusIcon.className = "synchronising"
HTML.overallStatusIconImage.src = "synchronising.svg"
return
}
}
HTML.overallStatusIcon.className = "synchronised"
HTML.overallStatusIconImage.src = "synchronised.svg"
}
const erasePoint = ([x, y]) => {
if (room == null) {
return
let canvasDraggingStart = null
function startCanvasDragging(mousePos) {
if (canvasDraggingStart == null) {
canvasDraggingStart = mousePos
}
room.getPaths().forEach((points, pathID) => {
points.forEach((point, i) => {
if (getDistance([x, y], point) <= ERASER_RADIUS) {
room.erasePoint(pathID, i)
}
})
})
}
function stopCanvasDragging() {
canvasDraggingStart = null
}
function dragCanvas(mousePos) {
if (canvasDraggingStart) {
const [x0, y0] = canvasDraggingStart
const [x1, y1] = mousePos
const offset = [x1 - x0, y1 - y0]
canvasDraggingStart = mousePos
toolSelection.applyCanvasOffset(offset)
}
}
function getOffsets(e) {
return [e.offsetX, e.offsetY]
}
function getScreenOffsets(e) {
return [e.screenX, e.screenY]
}
canvas.input.addEventListener("strokestart", ({ detail: e }) => {
e.preventDefault()
const pressure = getNormalizedPressure(e)
clearRecognizedUpcoming()
if (room == null) {
return
}
const mousePos = [e.offsetX, e.offsetY]
if (currentTool == tools.PEN) {
pathIDsByPointerID.set(e.pointerId, room.addPath(mousePos))
} else if (currentTool == tools.ERASER) {
erasePoint(mousePos)
const currentTool = toolSelection.getTool()
const mousePos = getOffsets(e)
switch (currentTool) {
case toolSelection.Tools.PEN:
if (!pathIDsByPointerID.has(e.pointerId)) {
pathIDsByPointerID.set(
e.pointerId,
room.addPath(selectedColorAndRadiusPoint(...mousePos, pressure)),
)
}
break
case toolSelection.Tools.ERASER:
eraseEverythingAtPosition(
...mousePos,
toolSelection.getEraseRadius(),
room,
)
break
case toolSelection.Tools.DRAGGER:
startCanvasDragging(getScreenOffsets(e))
break
}
})
canvas.input.addEventListener("strokeend", ({ detail: e }) => {
pathIDsByPointerID.delete(e.pointerId)
const { pointerId } = e
const pressure = getNormalizedPressure(e)
const pathID = pathIDsByPointerID.get(pointerId)
if (room != null && pathID) {
const currentTool = toolSelection.getTool()
if (
currentTool === toolSelection.Tools.PEN &&
toolSelection.isRecognitionModeSet()
) {
drawRecognized(pathID, room.getPathPoints(pathID), pressure)
}
room.endPath(pathID)
}
pathIDsByPointerID.delete(pointerId)
clearRecognizedUpcoming()
stopCanvasDragging()
})
canvas.input.addEventListener("strokemove", ({ detail: e }) => {
if (room == null) {
return
}
const pressure = getNormalizedPressure(e)
const currentTool = toolSelection.getTool()
const mousePos = getOffsets(e)
switch (currentTool) {
case toolSelection.Tools.PEN: {
const pathID = pathIDsByPointerID.get(e.pointerId)
if (pathID) {
room.extendPath(
pathID,
selectedColorAndRadiusPoint(...mousePos, pressure),
)
} else {
pathIDsByPointerID.set(
e.pointerId,
room.addPath(selectedColorAndRadiusPoint(...mousePos, pressure)),
)
}
if (toolSelection.isRecognitionModeSet()) {
drawRecognizedUpcoming(room.getPathPoints(pathID), pressure)
}
break
}
case toolSelection.Tools.ERASER:
eraseEverythingAtPosition(
...mousePos,
toolSelection.getEraseRadius(),
room,
)
break
case toolSelection.Tools.DRAGGER:
dragCanvas(getScreenOffsets(e))
break
}
})
const mousePos = [e.offsetX, e.offsetY]
if (currentTool == tools.PEN) {
room.extendPath(pathIDsByPointerID.get(e.pointerId), mousePos)
} else if (currentTool == tools.ERASER) {
erasePoint(mousePos)
window.addEventListener("unload", () => {
if (room) {
room.disconnect()
}
})
tryRoomConnect(TEST_ROOM)
// Add the recognition hint element in advance for consistency during tests.
clearRecognizedUpcoming()
tryRoomConnect(initialRoom)
import "array-flat-polyfill"
// Local canvas rendering.
// Emit input events and receive draw calls seperately - these must be piped
// together externally if desired.
import { line, curveLinear } from "d3-shape"
import { line, curveCatmullRom, curveLinear } from "d3-shape"
import { canvas } from "./elements.js"
// TODO: switch to curve interpolation that respects mouse points based on velocity
const lineFn = line()
const SVG_URL = "http://www.w3.org/2000/svg"
import * as HTML from "./elements.js"
export const LAST_RECOGNIZED_PATH_ID = "LSP"
const curve = curveCatmullRom.alpha(1.0)
const smoothLine = line()
.x((d) => d[0])
.y((d) => d[1])
.curve(curve)
const straightLine = line()
.x((d) => d[0])
.y((d) => d[1])
.curve(curveLinear)
const pathElems = new Map()
const pathGroupElems = new Map()
const MAX_RADIUS_DELTA = 0.05
export var stroke_colour = "blue"
export const STROKE_RADIUS = 2
// Interpolate a path so that:
// - The radius delta between two adjacent points is capped at
// MAX_RADIUS_DELTA.
// If paths are too choppy, try decreasing these constants.
const smoothPath = ([...path]) => {
// Apply MAX_RADIUS_DELTA.
for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0]
const dy = path[i][1] - path[i - 1][1]
const dw = path[i][2] - path[i - 1][2]
const segmentsToSplit = Math.ceil(dw / MAX_RADIUS_DELTA)
const newPoints = []
for (let j = 1; j < segmentsToSplit; j++) {
newPoints.push([
path[i - 1][0] + (dx / segmentsToSplit) * j,
path[i - 1][1] + (dy / segmentsToSplit) * j,
path[i - 1][2] + (dw / segmentsToSplit) * j,
path[i - 1][3],
])
}
path.splice(i, 0, ...newPoints)
i += newPoints.length
}
return path
}
export const input = new EventTarget()
export const renderPath = (id, points) => {
let pathElem = pathElems.get(id)
const createSvgElem = (tagName) => document.createElementNS(SVG_URL, tagName)
const interpolate = (point0, point1, fraction) => [
point0[0] + fraction * (point1[0] - point0[0]),
point0[1] + fraction * (point1[1] - point0[1]),
point0[2] + fraction * (point1[2] - point0[2]),
point0[3],
]
const ensurePathGroupElem = (id) => {
let groupElem = pathGroupElems.get(id)
if (pathElem == null) {
pathElem = document.createElementNS("http://www.w3.org/2000/svg", "g")
if (groupElem == null) {
groupElem = createSvgElem("g")
pathElem.setAttribute("stroke", stroke_colour)
pathElem.setAttribute("stroke-width", STROKE_RADIUS * 2)
pathElem.setAttribute("fill", "none")
pathElem.setAttribute("stroke-linecap", "round")
pathElem.setAttribute("pointer-events", "none")
groupElem.setAttribute("fill", "none")
groupElem.setAttribute("stroke-linecap", "round")
groupElem.setAttribute("pointer-events", "none")
canvas.appendChild(pathElem)
pathElems.set(id, pathElem)
HTML.canvas.appendChild(groupElem)
pathGroupElems.set(id, groupElem)
}
pathElem.innerHTML = ""
groupElem.innerHTML = ""
return groupElem
}
const renderSubpath = (subpath, pathSmooth) => {
const pathElem = createSvgElem("path")
pathElem.setAttribute("stroke", subpath[0][3])
pathElem.setAttribute("stroke-width", subpath[0][2] * 2)
pathElem.setAttribute(
"d",
pathSmooth ? smoothLine(subpath) : straightLine(subpath),
)
return pathElem
}
const isValidPoint = (point) => {
return point != null && point[0] != null
}
const POINT_ERASE_LIMIT = 0.0001
const pointWasErased = (pointEraseIntervals) => {
return (
pointEraseIntervals.length > 0 &&
pointEraseIntervals[0][0] <= POINT_ERASE_LIMIT
)
}
const needToDrawLastPoint = (points, pathEraseIntervals) => {
if (points.length < 2) return true
const penultimatePointIndex = points.length - 2
const pointEraseIntervals = pathEraseIntervals[penultimatePointIndex] || null
if (
pointEraseIntervals != null &&
pointEraseIntervals.length > 0 &&
pointEraseIntervals.some((interval) => interval[1] >= 1 - POINT_ERASE_LIMIT)
) {
return false
}
return true
}
const applyErasureIntervals = (points, pathEraseIntervals) => {
if (points.length == 0) {
return pathElem
return []
}
// Push a fake path split to generate the last path
points.push([-1, -1, false])
const subpaths = []
let subpath = []
for (let i = 0; i < points.length; i++) {
const point = points[i]
if (!isValidPoint(point)) {
continue
}
const nextPoint = points[i + 1]
const pointEraseIntervals = pathEraseIntervals[i] || null
for (const point of points) {
if (point[0] === undefined) {
if (pointEraseIntervals == null) {
subpath.push(point)
continue
} else if (nextPoint == null) {
if (JSON.stringify(pointEraseIntervals) != "[[0,0]]") subpath.push(point)
continue
}
if (!pointWasErased(pointEraseIntervals) || subpath.length) {
subpath.push(point)
}
if (point[2] === false) {
if (subpath.length == 1) {
const subpathElem = document.createElementNS(
"http://www.w3.org/2000/svg",
"circle",
)
subpathElem.setAttribute("stroke", "none")
subpathElem.setAttribute("fill", stroke_colour)
subpathElem.setAttribute("cx", subpath[0][0])
subpathElem.setAttribute("cy", subpath[0][1])
subpathElem.setAttribute("r", STROKE_RADIUS)
pathElem.appendChild(subpathElem)
} else if (subpath.length > 0) {
const subpathElem = document.createElementNS(
"http://www.w3.org/2000/svg",
"path",
)
subpathElem.setAttribute("d", lineFn(subpath))
pathElem.appendChild(subpathElem)
for (const pointEraseInterval of pointEraseIntervals) {
const eraseIntervalBounds = pointEraseInterval.map((f) =>
interpolate(point, nextPoint, f),
)
const [endOfDrawnSegment, startOfNewSegment] = eraseIntervalBounds
if (pointEraseInterval[0] > POINT_ERASE_LIMIT) {
subpath.push(endOfDrawnSegment)
}
subpath = []
subpaths.push(subpath.slice())
continue
if (pointEraseInterval[1] < 1 - POINT_ERASE_LIMIT) {
subpath = [startOfNewSegment]
} else {
subpath = []
}
}
}
subpath.push(point)
if (needToDrawLastPoint(points, pathEraseIntervals)) {
subpaths.push(subpath.slice())
}
return pathElem
return subpaths.filter((subpath) => subpath.length > 0)
}
export const clear = () => {
pathElems.clear()
canvas.innerHTML = ""
export const splitOnPressures = ([...path]) => {
const subpaths = []
let w = path[0][2]
for (let i = 1; i < path.length; i++) {
if (path[i][2] != w) {
subpaths.push([...path.splice(0, i), path[0]])
w = path[0][2]
i = 1
}
}
subpaths.push(path)
return subpaths
}
// Note that the PointerEvent is passed as the detail in these 'stroke events'.
canvas.addEventListener("pointerdown", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokestart", { detail: e }))
}
})
canvas.addEventListener("pointerenter", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokestart", { detail: e }))
}
})
canvas.addEventListener("pointerup", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokeend", { detail: e }))
}
})
canvas.addEventListener("pointerleave", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokeend", { detail: e }))
export const renderPath = (id, points, pathEraseIntervals) => {
let rectShapeStartIndex = -1
const pointsWithIntervals = Object.keys(pathEraseIntervals).length
// Rect recognition hint shape: pure rect with no erasure
if (pointsWithIntervals == 0 && points.length == 5) {
rectShapeStartIndex = 0
// Recognised rect shape: rect after completely erased raw data
} else if (pointsWithIntervals > 0 && points.length > 5) {
// Check that the preceding raw data is completely erased
for (let i = 0; i < points.length - 5; i++) {
if (
!pathEraseIntervals[i] ||
pathEraseIntervals[i].length != 1 ||
pathEraseIntervals[i][0][0] != 0 ||
pathEraseIntervals[i][0][1] != 1
) {
break
}
}
rectShapeStartIndex = points.length - 5
}
})
canvas.addEventListener("pointermove", (e) => {
if (e.buttons & 1) {
input.dispatchEvent(new CustomEvent("strokemove", { detail: e }))
// Only draw the path smooth if it is not a recognised rect shape, i.e. the last five points form a cycle
const pathSmooth = !(
rectShapeStartIndex >= 0 &&
points[rectShapeStartIndex][0] == points[rectShapeStartIndex + 4][0] &&
points[rectShapeStartIndex][1] == points[rectShapeStartIndex + 4][1] &&
points[rectShapeStartIndex][2] == points[rectShapeStartIndex + 4][2] &&
points[rectShapeStartIndex][3] == points[rectShapeStartIndex + 4][3]
)
let subpaths = applyErasureIntervals(points, pathEraseIntervals)
if (subpaths.length < 1 && id != LAST_RECOGNIZED_PATH_ID) {
const pathGroupElem = pathGroupElems.get(id)
if (pathGroupElem) {
pathGroupElems.delete(id)
HTML.canvas.removeChild(pathGroupElem)
}
return
}
})
canvas.addEventListener("touchmove", (e) => e.preventDefault())
export function setStrokeColour(colour) {
stroke_colour = colour
subpaths = subpaths.map(smoothPath)
subpaths = subpaths.flatMap(splitOnPressures)
const subpathElems = subpaths.map((subpath) =>
renderSubpath(subpath, pathSmooth),
)
const pathGroupElem = ensurePathGroupElem(id)
subpathElems.forEach((subpathElem) => pathGroupElem.appendChild(subpathElem))
}
export const clear = () => {
pathGroupElems.clear()
canvas.innerHTML = ""
}
// Necessary since buttons property is non standard on iOS versions < 13.2
const isValidPointerEvent = (e, name) => {
if (name === "strokeend") return true
return e.buttons & 1 || e.pointerType === "touch"
}
const dispatchPointerEvent = (name) => (e) => {
if (isValidPointerEvent(e, name)) {
input.dispatchEvent(new CustomEvent(name, { detail: e }))
}
}
// Note that the PointerEvent is passed as the detail in these 'stroke events'.
canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("onmouseup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("mouseup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove"))
canvas.addEventListener("touchmove", (e) => e.preventDefault())
export default class AbstractConnection extends EventTarget {
constructor(options) {
super()
this.options = options
}
/*
Supported events:
- roomJoined => ()
- roomLeft => ()
- channelOpened => ({detail: uid})
- channelError => ({detail: uid})
- channelClosed => ({detail: uid})
- messageReceived => ({detail: {uid, channel, message}})
*/
getUserID() {
// => int
}
getPeerHandle(/*uid*/) {
// => opaque
}
getPeerFootprint(/*uid*/) {
// => Promise => int
}
send(/*uid, channel, message*/) {
// => void
}
broadcast(/*channel, message*/) {
// => void
}
terminatePeer(/*uid*/) {
// => void
}
destructor() {
// => void
}
}
export const userID = { uuid: null }
export const sendListener = { callback: null }
export const broadcastListener = { callback: null }
const eventListeners = new Map()
export const getEventListener = (room, event) =>
eventListeners.get(`${room}:${event}`)
class MockConnection {
constructor({ room }) {
this.room = room
setTimeout(
() =>
getEventListener(room, "roomJoined") &&
getEventListener(room, "roomJoined")(),
0,
)
}
getUserID() {
return userID.uuid
}
getPeerHandle(/*uid*/) {
return undefined
}
getPeerFootprint(/*uid*/) {
return Promise.resolve(Date.now())
}
send(uid, channel, message) {
if (sendListener.callback) {
sendListener.callback(uid, channel, message)
}
}
broadcast(channel, message) {
if (broadcastListener.callback) {
broadcastListener.callback(channel, message)
}
}
terminatePeer() {
// Twiddle thumbs
}
destructor() {
sendListener.callback = null
broadcastListener.callback = null
eventListeners.clear()
}
addEventListener(event, callback) {
eventListeners.set(`${this.room}:${event}`, callback)
}
}
export default MockConnection
import AbstractConnection from "./Connection.js"
import LioWebRTC from "liowebrtc"
export default class WebRTCConnection extends AbstractConnection {
constructor(options) {
super(options)
this.webrtc = new LioWebRTC({
url: this.options.url,
dataOnly: true,
constraints: {
minPeers: this.options.mesh.minPeers,
maxPeers: this.options.mesh.maxPeers,
},
})
this.webrtc.on("ready", () => {
this.webrtc.joinRoom(this.options.room)
})
this.webrtc.on("joinedRoom", () => {
this.dispatchEvent(new CustomEvent("roomJoined"))
})
this.webrtc.on("leftRoom", () => {
this.dispatchEvent(new CustomEvent("roomLeft"))
})
this.webrtc.on("channelOpen", (dataChannel, peer) => {
this.dispatchEvent(new CustomEvent("channelOpened", { detail: peer.id }))
})
this.webrtc.on("channelError", (dataChannel, peer) => {
this.dispatchEvent(new CustomEvent("channelError", { detail: peer.id }))
})
this.webrtc.on("channelClose", (dataChannel, peer) => {
this.dispatchEvent(new CustomEvent("channelClosed", { detail: peer.id }))
})
this.webrtc.on("receivedPeerData", (channel, message, peer) => {
// Message could have been forwarded but interface only needs to know about directly connected peers
this.dispatchEvent(
new CustomEvent("messageReceived", {
detail: {
uid: peer.forwardedBy ? peer.forwardedBy.id : peer.id,
channel,
message,
},
}),
)
})
}
getUserID() {
return this.webrtc.getMyId()
}
getPeerHandle(uid) {
return this.webrtc.getPeerById(uid)
}
getPeerFootprint(uid) {
const peer = this.webrtc.getPeerById(uid)
if (!peer) return Promise.reject()
return new Promise(function(resolve, reject) {
peer.getStats(null).then((stats) => {
let footprint = -1
stats.forEach((report) => {
if (
report.type == "candidate-pair" &&
report.bytesSent > 0 &&
report.bytesReceived > 0 &&
report.writable
) {
footprint = Math.max(footprint, report.bytesReceived)
}
})
if (footprint != -1) {
resolve(footprint)
} else {
reject()
}
})
})
}
send(uid, channel, message) {
const peer = this.webrtc.getPeerById(uid)
if (!peer) return
this.webrtc.whisper(peer, channel, message)
}
broadcast(channel, message) {
this.webrtc.shout(channel, message)
}
terminatePeer(uid) {
const peer = this.webrtc.getPeerById(uid)
if (!peer) return
peer.end()
}
destructor() {
this.webrtc.quit()
}
}
import { client, xml } from "@xmpp/client"
import uuidv4 from "uuid/v4"
const ChannelState = {
TRUE: 0,
FALSE: 1,
PROCESSING: 4,
}
const SPY_CALLSIGN = "Baguette: " // Vive la France
const GROUP_MESSAGE_ID = "I smell JOJO!" // Ur. Ugly.
const XMPP_STATUS_ROOM_CREATED = "201" // 201 Created
export default class XMPPConnection extends EventTarget {
constructor(channel, details) {
super()
this.username = SPY_CALLSIGN + uuidv4().toString()
this.channelState = ChannelState.PROCESSING
this.spyNetwork = new Set()
this.details = details
this.channel = channel
this.channelQueue = []
this.online = false
this.queue = []
this.details.fqdn = "conference." + this.details.host
const xmpp = client({
service: `wss://${details.host}:${details.port}/xmpp-websocket`,
domain: details.host,
username: details.username,
password: details.password,
})
this.xmpp = xmpp
xmpp.on("offline", () => {
this.online = false
})
xmpp.on("stanza", (stanza) => {
const stanzaId = stanza.getAttr("id")
const delayed = stanza.getChild("delay")
if (!delayed && stanzaId && stanzaId === GROUP_MESSAGE_ID) {
// Messages sent to the room as echoed back
// Ignore our own messages to prevent loops
// But don't ignore them when we're re-syncing
return
}
if (stanza.is("message")) {
const body = stanza.getChild("body")
if (body === undefined) {
return
}
try {
this.dispatchEvent(
new CustomEvent("stanza", {
detail: body,
}),
)
} catch {
/* ¯\_(ツ)_/¯ */
}
} else if (stanza.is("presence")) {
const from = stanza.getAttr("from")
if (from === undefined) {
// Likely won't happen
return
}
const search = `${this.channel}@${this.details.fqdn}/`
if (from.startsWith(search)) {
const joiner = from.substring(search.length)
if (!joiner.startsWith(SPY_CALLSIGN)) {
return
}
const change = stanza.getAttr("type")
if (change && change === "unavailable") {
this.spyNetwork.delete(joiner)
} else {
this.spyNetwork.add(joiner)
}
this.processChannelStateChange()
}
const x = stanza.getChild("x")
if (x === undefined) {
// Uncertain if this element is guaranteed inside a <presence/>
return
}
const created =
x.getChildByAttr("code", XMPP_STATUS_ROOM_CREATED) !== undefined
if (created) {
// Create an "instant room"
this.acceptDefaultRoomConfiguration()
}
}
})
xmpp.on("online", async (address) => {
/*eslint no-unused-vars: ["error", { "args": "none" }]*/
// Makes itself available
await xmpp.send(xml("presence"))
this.online = true
for (const message of this.queue) {
await this.xmpp.send(message)
}
})
xmpp.start().catch(console.error)
this.joinChannel()
}
joinChannel() {
const channelIdent = `${this.channel}@${this.details.fqdn}/${this.username}`
const presence = xml(
"presence",
{ to: channelIdent },
xml("x", { xmlns: "http://jabber.org/protocol/muc" }),
)
this.sendOrQueue(presence)
}
sendOrQueue(message) {
if (this.online) {
this.xmpp.send(message)
} else {
this.queue.push(message)
}
}
sendChannelOrQueue(message) {
switch (this.channelState) {
case ChannelState.TRUE:
this.sendOrQueue(message)
break
case ChannelState.FALSE:
return
case ChannelState.PROCESSING:
this.channelQueue.push(message)
break
}
}
sendChannelMessage(message) {
const channelIdent = `${this.channel}@${this.details.fqdn}`
const wrappedMessage = xml(
"message",
{
type: "groupchat",
to: channelIdent,
id: GROUP_MESSAGE_ID,
},
xml("body", {}, message),
)
this.sendChannelOrQueue(wrappedMessage)
}
acceptDefaultRoomConfiguration() {
const channelIdent = `${this.channel}@${this.details.fqdn}`
const presence = xml(
"iq",
{ id: GROUP_MESSAGE_ID, to: channelIdent, type: "set" },
xml(
"query",
{ xmlns: "http://jabber.org/protocol/muc#owner" },
xml("x", { xmlns: "jabber:x:data", type: "submit" }),
),
)
this.sendOrQueue(presence)
}
async processChannelStateChange() {
const priority = Array.from(this.spyNetwork).sort()
if (priority[0] === this.username) {
if (this.channelState === ChannelState.PROCESSING) {
for (const message of this.channelQueue) {
await this.sendOrQueue(message)
}
}
this.channelState = ChannelState.TRUE
} else {
this.channelState = ChannelState.FALSE
}
}
sneakilySendTheOtherTeamOur(secrets) {
this.sendChannelMessage(secrets)
}
}