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 2201 additions and 355 deletions
<?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,39 +4,26 @@ ...@@ -4,39 +4,26 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="manifest" href="manifest.json" /> <link rel="manifest" href="manifest.json" />
<link rel="shortcut icon" href="logo.png" /> <link rel="shortcut icon" href="logo.png" />
<link rel="apple-touch-icon" href="logo.png" />
<link rel="stylesheet" href="styles.css" /> <link rel="stylesheet" href="styles.css" />
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css"
/>
<script> <script>
if (navigator.serviceWorker) { if (navigator.serviceWorker) {
navigator.serviceWorker navigator.serviceWorker.register("service-worker.js").then(
.register("service-worker.js") (registration) =>
.then( console.log(
(registration) => `Service worker registered on scope ${registration.scope}`,
console.log( ),
`Service worker registered on scope ${registration.scope}`, (reason) =>
), console.log(`Service worker failed to register ~ ${reason}`),
(reason) => )
console.log(`Service worker failed to register ~ ${reason}`),
)
} }
</script> </script>
</head> </head>
<body> <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 id="top-panel">
<div class="top-bar"> <div class="top-bar">
<div class="dropdown"> <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"> <div class="peers">
<ul id="connected-peers"> <ul id="connected-peers">
No peers are connected No peers are connected
...@@ -58,11 +45,11 @@ ...@@ -58,11 +45,11 @@
size="25" size="25"
color="gray" 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>
<div id="tools-panel"> <div id="tools-panel">
<button id="pen-tool" class="selected"> <button id="pen-tool" class="selectable-tool selected">
<i class="fa fa-paint-brush"></i> <i class="fas fa-paint-brush"></i>
</button> </button>
<div id="pen-properties" class="properties"> <div id="pen-properties" class="properties">
<div class="pen-contents"> <div class="pen-contents">
...@@ -87,8 +74,7 @@ ...@@ -87,8 +74,7 @@
<input <input
type="range" type="range"
min="1" min="1"
max="100" max="10"
value="10"
class="slider" class="slider"
id="range" id="range"
/> />
...@@ -190,22 +176,54 @@ ...@@ -190,22 +176,54 @@
</svg> </svg>
<div id="others"> <div id="others">
<div id="other-palette"> <div id="other-palette">
<b>Others</b> <b>Other colours</b>
</div> </div>
<label id="colours"> <label id="colours">
<input id="other-colours" type="color" value="blue" /> <input id="other-colours" type="color" />
</label> </label>
</div> </div>
</div> </div>
</div> </div>
<button id="eraser-tool"><i class="fa fa-eraser"></i></button> <button id="eraser-tool" class="selectable-tool">
<div id="connected-room-info"> <i class="fas fa-eraser"></i>
You are connected to a room: <span id="connected-room-id"></span> </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> </div>
</div> </div>
<svg id="canvas"></svg> <div id="canvas-container">
<svg id="canvas"></svg>
</div>
<script src="js/app.js"></script> <script src="js/app.js"></script>
</body> </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 { ...@@ -3,6 +3,10 @@ body {
height: 100%; height: 100%;
} }
#console {
color: white;
}
body { body {
background-color: black; background-color: black;
margin: 0; margin: 0;
...@@ -10,11 +14,21 @@ body { ...@@ -10,11 +14,21 @@ body {
flex-direction: column; flex-direction: column;
} }
#canvas { #canvas-container {
width: calc(100vw - 8px); width: calc(100vw - 8px);
background-color: white; background-color: white;
border: 4px solid red; border: 4px solid red;
flex-grow: 1; flex-grow: 1;
position: relative;
overflow: hidden;
}
#canvas {
position: absolute;
left: -5000px;
top: -5000px;
width: 10000px;
height: 10000px;
} }
#tools-panel { #tools-panel {
...@@ -23,11 +37,18 @@ body { ...@@ -23,11 +37,18 @@ body {
align-items: center; align-items: center;
} }
.spacer {
flex: 1;
}
#connected-room-info { #connected-room-info {
display: none;
color: white; color: white;
flex: 1; display: flex;
text-align: right; align-items: center;
background-color: #4f4f4fb7;
border-radius: 4px;
justify-content: center;
padding: 0.75em;
} }
button.selected { button.selected {
...@@ -60,7 +81,7 @@ button.selected { ...@@ -60,7 +81,7 @@ button.selected {
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
transition-duration: 0.4s; transition-duration: 0.4s;
height: 100%; height: 36px;
width: 36px; width: 36px;
} }
...@@ -71,7 +92,7 @@ button.selected { ...@@ -71,7 +92,7 @@ button.selected {
min-width: 160px; min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2); box-shadow: 0px 8px 16px 0px rgba(0, 0, 0, 0.2);
z-index: 999; z-index: 999;
padding: 16px; padding: 8px;
} }
.peers a { .peers a {
...@@ -107,6 +128,8 @@ button.selected { ...@@ -107,6 +128,8 @@ button.selected {
border-bottom: 1px solid black; border-bottom: 1px solid black;
padding: 4px 0; padding: 4px 0;
opacity: 0; opacity: 0;
display: flex;
align-items: center;
} }
#connected-peers li:nth-child(2) { #connected-peers li:nth-child(2) {
...@@ -141,24 +164,47 @@ button.selected { ...@@ -141,24 +164,47 @@ button.selected {
border-bottom-width: 0px; border-bottom-width: 0px;
} }
.peer-quality,
.peer-status { .peer-status {
width: 15px; width: 15px;
height: 15px; height: 15px;
margin-left: 5px; margin-right: 5px;
display: inline-block; }
.peer-status {
border-radius: 10px; 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 { .peer-status.unsynced {
color: gray;
background-color: gray; background-color: gray;
} }
@keyframes peer-status-negotiating { @keyframes peer-status-negotiating {
from { from {
color: gray;
background-color: gray; background-color: gray;
} }
50%, 50%,
to { to {
color: orange;
background-color: orange; background-color: orange;
} }
} }
...@@ -166,9 +212,11 @@ button.selected { ...@@ -166,9 +212,11 @@ button.selected {
.peer-status.negotiating { .peer-status.negotiating {
animation: peer-status-negotiating 0.5s step-end infinite; animation: peer-status-negotiating 0.5s step-end infinite;
background-color: orange; background-color: orange;
color: orange;
} }
.peer-status.synced { .peer-status.synced {
color: green;
background-color: green; background-color: green;
} }
...@@ -227,9 +275,13 @@ button.selected { ...@@ -227,9 +275,13 @@ button.selected {
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: 4px; border-radius: 4px;
width: 78px;
height: 36px;
white-space: nowrap;
overflow: hidden;
} }
#pen-tool { .selectable-tool {
background-color: #2f2f2f; background-color: #2f2f2f;
color: white; color: white;
padding: 10px; padding: 10px;
...@@ -237,27 +289,89 @@ button.selected { ...@@ -237,27 +289,89 @@ button.selected {
border: none; border: none;
cursor: pointer; cursor: pointer;
border-radius: 50%; border-radius: 50%;
margin-right: 8px;
} }
#pen-tool:hover { .selectable-tool:hover {
background-color: #4f4f4f !important; background-color: #4f4f4f !important;
transition-duration: 0.4s; transition-duration: 0.4s;
} }
#pen-tool {
width: 39px;
height: 39px;
margin-right: 8px;
}
#pen-tool > i {
padding: 0 1.5px;
}
#eraser-tool { #eraser-tool {
background-color: #2f2f2f; width: 39px;
color: white; height: 39px;
padding: 10px; margin-right: 8px;
font-size: 16px;
border: none;
cursor: pointer;
border-radius: 50%;
} }
#eraser-tool:hover { #eraser-tool > i {
background-color: #4f4f4f !important; padding: 0 1.5px;
transition-duration: 0.4s; }
#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 { .properties {
...@@ -435,7 +549,7 @@ button.selected { ...@@ -435,7 +549,7 @@ button.selected {
margin-top: 2%; margin-top: 2%;
display: flex; display: flex;
align-items: center; align-items: center;
padding-left: 40%; justify-content: center;
background-color: #3cbc8d; background-color: #3cbc8d;
color: white; color: white;
padding-top: 4px; padding-top: 4px;
...@@ -463,8 +577,20 @@ button.selected { ...@@ -463,8 +577,20 @@ button.selected {
transition-duration: 0.4s; transition-duration: 0.4s;
} }
#other-colours { @supports (-webkit-overflow-scrolling: touch) {
visibility: hidden; #colours {
visibility: hidden;
}
#other-colours {
visibility: visible;
}
}
@supports not (-webkit-overflow-scrolling: touch) {
#other-colours {
visibility: hidden;
}
} }
@font-face { @font-face {
...@@ -484,3 +610,32 @@ button.selected { ...@@ -484,3 +610,32 @@ button.selected {
url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.svg#Martel") url("./assets/fonts/martel-v4-latin/martel-v4-latin-regular.svg#Martel")
format("svg"); 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;
}
File added
This diff is collapsed.
import "array-flat-polyfill"
// Local canvas rendering. // Local canvas rendering.
// Emit input events and receive draw calls seperately - these must be piped // Emit input events and receive draw calls seperately - these must be piped
// together externally if desired. // together externally if desired.
import { line, curveLinear } from "d3-shape" import { line, curveCatmullRom, curveLinear } from "d3-shape"
import { canvas } from "./elements.js" import { canvas } from "./elements.js"
const SVG_URL = "http://www.w3.org/2000/svg" const SVG_URL = "http://www.w3.org/2000/svg"
// TODO: switch to curve interpolation that respects mouse points based on velocity import * as HTML from "./elements.js"
const lineFn = line()
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]) .x((d) => d[0])
.y((d) => d[1]) .y((d) => d[1])
.curve(curveLinear) .curve(curveLinear)
const pathGroupElems = new Map() const pathGroupElems = new Map()
let stroke_colour = "blue" const MAX_RADIUS_DELTA = 0.05
export let stroke_radius = 1
export const MIN_STROKE_RADIUS = 0.1
export const MAX_STROKE_RADIUS = 3.9
const MAX_POINT_DISTANCE = 5
const MAX_PRESSURE_DELTA = 0.05
// Interpolate a path so that: // Interpolate a path so that:
// - The distance between two adjacent points is capped at MAX_POINT_DISTANCE. // - The radius delta between two adjacent points is capped at
// - The pressure delta between two adjacent points is capped at // MAX_RADIUS_DELTA.
// MAX_PRESSURE_DELTA
// If paths are too choppy, try decreasing these constants. // If paths are too choppy, try decreasing these constants.
const smoothPath = ([...path]) => { const smoothPath = ([...path]) => {
// Apply MAX_POINT_DISTANCE. // Apply MAX_RADIUS_DELTA.
for (let i = 1; i < path.length; i++) { for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0] const dx = path[i][0] - path[i - 1][0]
const dy = path[i][1] - path[i - 1][1] const dy = path[i][1] - path[i - 1][1]
const dw = path[i][2] - path[i - 1][2] const dw = path[i][2] - path[i - 1][2]
const distance = Math.hypot(dx, dy) const segmentsToSplit = Math.ceil(dw / MAX_RADIUS_DELTA)
const segmentsToSplit = Math.ceil(distance / MAX_POINT_DISTANCE)
const newPoints = [] const newPoints = []
for (let j = 1; j < segmentsToSplit; j++) { for (let j = 1; j < segmentsToSplit; j++) {
newPoints.push([ newPoints.push([
...@@ -44,121 +46,207 @@ const smoothPath = ([...path]) => { ...@@ -44,121 +46,207 @@ const smoothPath = ([...path]) => {
path[i - 1][1] + (dy / segmentsToSplit) * j, path[i - 1][1] + (dy / segmentsToSplit) * j,
path[i - 1][2] + (dw / segmentsToSplit) * j, path[i - 1][2] + (dw / segmentsToSplit) * j,
path[i - 1][3], path[i - 1][3],
path[i - 1][4],
]) ])
} }
path.splice(i, 0, ...newPoints) path.splice(i, 0, ...newPoints)
i += newPoints.length i += newPoints.length
} }
return path
}
// Apply MAX_PRESSURE_DELTA. export const input = new EventTarget()
for (let i = 1; i < path.length; i++) {
const dx = path[i][0] - path[i - 1][0] const createSvgElem = (tagName) => document.createElementNS(SVG_URL, tagName)
const dy = path[i][1] - path[i - 1][1]
const dw = path[i][2] - path[i - 1][2] const interpolate = (point0, point1, fraction) => [
const segmentsToSplit = Math.ceil(dw / MAX_PRESSURE_DELTA) point0[0] + fraction * (point1[0] - point0[0]),
const newPoints = [] point0[1] + fraction * (point1[1] - point0[1]),
for (let j = 1; j < segmentsToSplit; j++) { point0[2] + fraction * (point1[2] - point0[2]),
newPoints.push([ point0[3],
path[i - 1][0] + (dx / segmentsToSplit) * j, ]
path[i - 1][1] + (dy / segmentsToSplit) * j,
path[i - 1][2] + (dw / segmentsToSplit) * j, const ensurePathGroupElem = (id) => {
path[i - 1][3], let groupElem = pathGroupElems.get(id)
path[i - 1][4],
]) if (groupElem == null) {
} groupElem = createSvgElem("g")
path.splice(i, 0, ...newPoints)
i += newPoints.length groupElem.setAttribute("fill", "none")
groupElem.setAttribute("stroke-linecap", "round")
groupElem.setAttribute("pointer-events", "none")
HTML.canvas.appendChild(groupElem)
pathGroupElems.set(id, groupElem)
} }
return path
groupElem.innerHTML = ""
return groupElem
} }
const getStrokeRadius = (pressure, radius) => { 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 ( return (
MIN_STROKE_RADIUS + pointEraseIntervals.length > 0 &&
(radius + pressure) * (MAX_STROKE_RADIUS - MIN_STROKE_RADIUS) pointEraseIntervals[0][0] <= POINT_ERASE_LIMIT
) )
} }
export const input = new EventTarget() const needToDrawLastPoint = (points, pathEraseIntervals) => {
if (points.length < 2) return true
const penultimatePointIndex = points.length - 2
const pointEraseIntervals = pathEraseIntervals[penultimatePointIndex] || null
const createPathElem = (d, width) => { if (
const pathGroupElem = document.createElementNS(SVG_URL, "path") pointEraseIntervals != null &&
pathGroupElem.setAttribute("stroke-width", width) pointEraseIntervals.length > 0 &&
pathGroupElem.setAttribute("d", d) pointEraseIntervals.some((interval) => interval[1] >= 1 - POINT_ERASE_LIMIT)
return pathGroupElem ) {
return false
}
return true
} }
export const renderPath = (id, points) => { const applyErasureIntervals = (points, pathEraseIntervals) => {
points = points.filter(([x]) => x != null) if (points.length == 0) {
let colour = "" return []
let radius = 0
// Split up points into completely non-erased segments.
let segments = [[]]
for (const point of points) {
colour = point[3]
radius = point[4]
if (point[5] != false) {
segments[segments.length - 1].push(point)
} else {
segments.push([])
}
} }
segments = segments.filter((a) => a.length > 0)
let pathGroupElem = pathGroupElems.get(id) const subpaths = []
if (segments.length == 0) { let subpath = []
if (pathGroupElem != null) { for (let i = 0; i < points.length; i++) {
canvas.removeChild(pathGroupElem) const point = points[i]
pathGroupElems.delete(id) if (!isValidPoint(point)) {
continue
}
const nextPoint = points[i + 1]
const pointEraseIntervals = pathEraseIntervals[i] || null
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)
}
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)
}
subpaths.push(subpath.slice())
if (pointEraseInterval[1] < 1 - POINT_ERASE_LIMIT) {
subpath = [startOfNewSegment]
} else {
subpath = []
}
} }
return
} }
if (pathGroupElem == null) { if (needToDrawLastPoint(points, pathEraseIntervals)) {
pathGroupElem = document.createElementNS(SVG_URL, "g") subpaths.push(subpath.slice())
pathGroupElem.setAttribute("stroke", colour)
pathGroupElem.setAttribute("fill", "none")
pathGroupElem.setAttribute("stroke-linecap", "round")
pathGroupElem.setAttribute("pointer-events", "none")
canvas.appendChild(pathGroupElem)
pathGroupElems.set(id, pathGroupElem)
} }
pathGroupElem.innerHTML = "" return subpaths.filter((subpath) => subpath.length > 0)
}
for (const subpath of segments) {
if (subpath.length == 1) { export const splitOnPressures = ([...path]) => {
const circleElem = document.createElementNS(SVG_URL, "circle") const subpaths = []
circleElem.setAttribute("stroke", "none") let w = path[0][2]
circleElem.setAttribute("fill", colour) for (let i = 1; i < path.length; i++) {
circleElem.setAttribute("cx", subpath[0][0]) if (path[i][2] != w) {
circleElem.setAttribute("cy", subpath[0][1]) subpaths.push([...path.splice(0, i), path[0]])
circleElem.setAttribute("r", getStrokeRadius(subpath[0][2], radius)) w = path[0][2]
console.log(getStrokeRadius(subpath[0][2], radius)) i = 1
pathGroupElem.appendChild(circleElem) }
} else { }
// Further split up segments based on thickness. subpaths.push(path)
const subpath_ = smoothPath(subpath) return subpaths
let w = subpath_[0][2] }
for (let i = 1; i < subpath_.length; i++) {
if (subpath_[i][2] != w) { export const renderPath = (id, points, pathEraseIntervals) => {
const d = lineFn([...subpath_.splice(0, i), subpath_[0]]) let rectShapeStartIndex = -1
pathGroupElem.appendChild( const pointsWithIntervals = Object.keys(pathEraseIntervals).length
createPathElem(d, getStrokeRadius(w, radius) * 2),
) // Rect recognition hint shape: pure rect with no erasure
w = subpath_[0][2] if (pointsWithIntervals == 0 && points.length == 5) {
i = 1 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
} }
const d = lineFn(subpath_)
pathGroupElem.appendChild(
createPathElem(d, getStrokeRadius(w, radius) * 2),
)
} }
rectShapeStartIndex = points.length - 5
} }
// 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
}
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 = () => { export const clear = () => {
...@@ -167,12 +255,13 @@ export const clear = () => { ...@@ -167,12 +255,13 @@ export const clear = () => {
} }
// Necessary since buttons property is non standard on iOS versions < 13.2 // Necessary since buttons property is non standard on iOS versions < 13.2
function checkValidPointerEvent(e) { const isValidPointerEvent = (e, name) => {
if (name === "strokeend") return true
return e.buttons & 1 || e.pointerType === "touch" return e.buttons & 1 || e.pointerType === "touch"
} }
const dispatchPointerEvent = (name) => (e) => { const dispatchPointerEvent = (name) => (e) => {
if (checkValidPointerEvent(e)) { if (isValidPointerEvent(e, name)) {
input.dispatchEvent(new CustomEvent(name, { detail: e })) input.dispatchEvent(new CustomEvent(name, { detail: e }))
} }
} }
...@@ -181,18 +270,8 @@ const dispatchPointerEvent = (name) => (e) => { ...@@ -181,18 +270,8 @@ const dispatchPointerEvent = (name) => (e) => {
canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart")) canvas.addEventListener("pointerdown", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart")) canvas.addEventListener("pointerenter", dispatchPointerEvent("strokestart"))
canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend")) canvas.addEventListener("pointerup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("onmouseup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("mouseup", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend")) canvas.addEventListener("pointerleave", dispatchPointerEvent("strokeend"))
canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove")) canvas.addEventListener("pointermove", dispatchPointerEvent("strokemove"))
canvas.addEventListener("touchmove", (e) => e.preventDefault()) canvas.addEventListener("touchmove", (e) => e.preventDefault())
export function setStrokeColour(colour) {
stroke_colour = colour
}
export function getStrokeColour() {
return stroke_colour
}
export function setStrokeRadius(radius) {
stroke_radius = radius
}
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
This diff is collapsed.
This diff is collapsed.
Subproject commit 139ab6e2cc9d6f0501fa958f3c813df0fcc81310
export const userIDElem = document.getElementById("user-id")
export const peerIDElem = document.getElementById("peer-id")
export const peerButton = document.getElementById("peer-connect")
export const connectedPeers = document.getElementById("connected-peers") export const connectedPeers = document.getElementById("connected-peers")
export const overallStatusIcon = document.getElementById("overall-status-icon") export const overallStatusIcon = document.getElementById("overall-status-icon")
...@@ -12,6 +8,12 @@ export const overallStatusIconImage = document.getElementById( ...@@ -12,6 +8,12 @@ export const overallStatusIconImage = document.getElementById(
export const canvas = document.getElementById("canvas") export const canvas = document.getElementById("canvas")
export const penButton = document.getElementById("pen-tool") export const penButton = document.getElementById("pen-tool")
export const eraserButton = document.getElementById("eraser-tool") export const eraserButton = document.getElementById("eraser-tool")
export const recognitionModeButton = document.getElementById("recognition-mode")
export const draggingToolButton = document.getElementById("dragging-tool")
export const canvasCenterToolButton = document.getElementById("canvas-center")
export const fastUndoButton = document.getElementById("fast-undo-tool")
export const undoButton = document.getElementById("undo-tool")
export const roomIDElem = document.getElementById("room-id") export const roomIDElem = document.getElementById("room-id")
export const roomConnectButton = document.getElementById("room-connect") export const roomConnectButton = document.getElementById("room-connect")
...@@ -25,7 +27,9 @@ export const closeButton = document.querySelectorAll(".close") ...@@ -25,7 +27,9 @@ export const closeButton = document.querySelectorAll(".close")
export const palette = document.getElementById("palette") export const palette = document.getElementById("palette")
export const rectangle = document.getElementById("rectangle") export const rectangle = document.getElementById("rectangle")
export const wheel = document.getElementById("wheel") export const wheel = document.getElementById("wheel")
export const picker = document.getElementById("other-colours") export const strokeColorPicker = document.getElementById("other-colours")
export const slider = document.getElementById("range") export const strokeRadiusSlider = document.getElementById("range")
export const output = document.getElementById("value") export const output = document.getElementById("value")
export const labelColours = document.getElementById("colours") export const labelColours = document.getElementById("colours")
export const userInfo = document.getElementById("user-avatar")
export const topPanel = document.getElementById("top-panel")
This diff is collapsed.
This diff is collapsed.
Subproject commit 545380d3c27797799f3013ad112f9f17c64bc8c7 Subproject commit ce4a2ebe160804ed84f7b6fc3bd10c91e766bdcd
This diff is collapsed.
This diff is collapsed.