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 2368 additions and 527 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"
/> />
...@@ -193,19 +179,51 @@ ...@@ -193,19 +179,51 @@
<b>Other colours</b> <b>Other colours</b>
</div> </div>
<label id="colours"> <label id="colours">
<input id="other-colours" type="color" value="#0000ff" /> <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>
......
...@@ -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 {
...@@ -254,9 +275,13 @@ button.selected { ...@@ -254,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;
...@@ -264,27 +289,89 @@ button.selected { ...@@ -264,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 {
...@@ -523,3 +610,32 @@ button.selected { ...@@ -523,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, curveBasis } 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]) .x((d) => d[0])
.y((d) => d[1]) .y((d) => d[1])
.curve(curveBasis) .curve(curve)
const straightLine = line()
.x((d) => d[0])
.y((d) => d[1])
.curve(curveLinear)
const pathGroupElems = new Map() const pathGroupElems = new Map()
let strokeColour = "#0000ff"
let strokeRadius = 5
const MAX_POINT_DISTANCE = 5
const MAX_RADIUS_DELTA = 0.05 const MAX_RADIUS_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 radius delta between two adjacent points is capped at
// MAX_RADIUS_DELTA // MAX_RADIUS_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.
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 distance = Math.hypot(dx, dy)
const segmentsToSplit = Math.ceil(distance / MAX_POINT_DISTANCE)
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[i - 1][4],
])
}
path.splice(i, 0, ...newPoints)
i += newPoints.length
}
// Apply MAX_RADIUS_DELTA. // 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]
...@@ -62,7 +46,6 @@ const smoothPath = ([...path]) => { ...@@ -62,7 +46,6 @@ 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)
...@@ -73,76 +56,197 @@ const smoothPath = ([...path]) => { ...@@ -73,76 +56,197 @@ const smoothPath = ([...path]) => {
export const input = new EventTarget() export const input = new EventTarget()
const createPathElem = (d, width) => { const createSvgElem = (tagName) => document.createElementNS(SVG_URL, tagName)
const pathGroupElem = document.createElementNS(SVG_URL, "path")
pathGroupElem.setAttribute("stroke-width", width) const interpolate = (point0, point1, fraction) => [
pathGroupElem.setAttribute("d", d) point0[0] + fraction * (point1[0] - point0[0]),
return pathGroupElem 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 (groupElem == null) {
groupElem = createSvgElem("g")
groupElem.setAttribute("fill", "none")
groupElem.setAttribute("stroke-linecap", "round")
groupElem.setAttribute("pointer-events", "none")
HTML.canvas.appendChild(groupElem)
pathGroupElems.set(id, groupElem)
}
groupElem.innerHTML = ""
return groupElem
} }
export const renderPath = (id, points) => { const renderSubpath = (subpath, pathSmooth) => {
points = points.filter(([x]) => x != null) const pathElem = createSvgElem("path")
let colour = "" pathElem.setAttribute("stroke", subpath[0][3])
pathElem.setAttribute("stroke-width", subpath[0][2] * 2)
// Split up points into completely non-erased segments. pathElem.setAttribute(
let segments = [[]] "d",
for (const point of points) { pathSmooth ? smoothLine(subpath) : straightLine(subpath),
colour = point[3] )
if (point[4] != false) { return pathElem
segments[segments.length - 1].push(point) }
} else {
segments.push([]) 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
} }
segments = segments.filter((a) => a.length > 0) return true
}
let pathGroupElem = pathGroupElems.get(id) const applyErasureIntervals = (points, pathEraseIntervals) => {
if (points.length == 0) {
return []
}
if (segments.length == 0) { const subpaths = []
if (pathGroupElem != null) {
canvas.removeChild(pathGroupElem) let subpath = []
pathGroupElems.delete(id) 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
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", subpath[0][2]) w = path[0][2]
pathGroupElem.appendChild(circleElem) i = 1
} else { }
// Further split up segments based on thickness. }
const subpath_ = smoothPath(subpath) subpaths.push(path)
let w = subpath_[0][2] return subpaths
for (let i = 1; i < subpath_.length; i++) { }
if (subpath_[i][2] != w) {
const d = lineFn([...subpath_.splice(0, i), subpath_[0]]) export const renderPath = (id, points, pathEraseIntervals) => {
pathGroupElem.appendChild(createPathElem(d, w * 2)) let rectShapeStartIndex = -1
w = subpath_[0][2] const pointsWithIntervals = Object.keys(pathEraseIntervals).length
i = 1
} // 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
} }
const d = lineFn(subpath_)
pathGroupElem.appendChild(createPathElem(d, w * 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 = () => {
...@@ -151,12 +255,13 @@ export const clear = () => { ...@@ -151,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 }))
} }
} }
...@@ -165,41 +270,8 @@ const dispatchPointerEvent = (name) => (e) => { ...@@ -165,41 +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) {
strokeColour = colour
}
export function getStrokeColour() {
return strokeColour
}
const MIN_PRESSURE_FACTOR = 0.1
const MAX_PRESSURE_FACTOR = 1.5
// 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
}
export function getStrokeRadius(pressure) {
return strokeRadius * getPressureFactor(pressure)
}
export function setStrokeRadius(radius) {
strokeRadius = 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
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()
}
}
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.
This diff is collapsed.
This diff is collapsed.
This diff is collapsed.
...@@ -45,6 +45,17 @@ app.use((request, response, next) => { ...@@ -45,6 +45,17 @@ app.use((request, response, next) => {
next() next()
}) })
app.get("/", (request, response, next) => {
if (request.query.room == null) {
const url = new URL(
`${request.protocol}://${request.get("host")}${request.originalUrl}`,
)
url.searchParams.set("room", "imperial")
return response.redirect(url)
}
next()
})
app.use("/", express.static("public")) app.use("/", express.static("public"))
server.listen(port, host, () => { server.listen(port, host, () => {
......
...@@ -7,24 +7,7 @@ self.addEventListener("activate", (event) => { ...@@ -7,24 +7,7 @@ self.addEventListener("activate", (event) => {
}) })
const CACHE_NAME = "APP-V0" const CACHE_NAME = "APP-V0"
const FILES_TO_CACHE = [ const FILES_TO_CACHE = [FILES_TO_CACHE_LIST] // eslint-disable-line no-undef
"/index.html",
"/styles.css",
"/js/app.js",
"/js/queue.js",
"/logo.png",
"/manifest.json",
"/assets/fonts/martel-v4-latin/martel-v4-latin-regular.eot",
"/assets/fonts/martel-v4-latin/martel-v4-latin-regular.svg",
"/assets/fonts/martel-v4-latin/martel-v4-latin-regular.ttf",
"/assets/fonts/martel-v4-latin/martel-v4-latin-regular.woff",
"/assets/fonts/martel-v4-latin/martel-v4-latin-regular.woff2",
"/synchronising.svg",
"/synchronised.svg",
"/quality-high.svg",
"/quality-medium.svg",
"/quality-low.svg",
]
const FILE_ALIASES = new Map([["/", "/index.html"]]) const FILE_ALIASES = new Map([["/", "/index.html"]])
self.addEventListener("install", async () => { self.addEventListener("install", async () => {
...@@ -40,7 +23,11 @@ self.addEventListener("activate", async () => { ...@@ -40,7 +23,11 @@ self.addEventListener("activate", async () => {
}) })
const normalizePath = (path) => { const normalizePath = (path) => {
let normalizedPath = path.replace(/\/+/g, "/").replace(/$(?<!^)\/$/, "") let normalizedPath = path.replace(/\/+/g, "/")
// TODO: Is this a correct replacement for replace(/$(?<!^)\/$/, "") (compatible with Safari)?
if (normalizedPath != "/" && normalizedPath.endsWith("/")) {
normalizedPath = normalizedPath.slice(0, -1)
}
normalizedPath = FILE_ALIASES.get(normalizedPath) || normalizedPath normalizedPath = FILE_ALIASES.get(normalizedPath) || normalizedPath
return normalizedPath return normalizedPath
} }
......
This diff is collapsed.