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
Commits on Source (32)
...@@ -168,6 +168,7 @@ typings/ ...@@ -168,6 +168,7 @@ typings/
public/service-worker.js public/service-worker.js
public/js public/js
public/benchmarks.html public/benchmarks.html
public/assets/fonts/font-awesome
# vuepress build output # vuepress build output
.vuepress/dist .vuepress/dist
......
...@@ -33,11 +33,7 @@ npm_install_prod: ...@@ -33,11 +33,7 @@ npm_install_prod:
artifacts: artifacts:
paths: paths:
- node_modules - node_modules
- src/liowebrtc
- src/rtcpeerconnection
- src/signalbuddy - src/signalbuddy
- src/yjs
- src/drawing-crdt/pkg
npm_install: npm_install:
stage: deps stage: deps
...@@ -50,9 +46,9 @@ npm_install: ...@@ -50,9 +46,9 @@ npm_install:
- node_modules - node_modules
- src/liowebrtc - src/liowebrtc
- src/rtcpeerconnection - src/rtcpeerconnection
- src/signalbuddy
- src/yjs - src/yjs
- src/drawing-crdt/pkg - src/drawing-crdt/pkg
- src/signalbuddy
format_check: format_check:
stage: check stage: check
......
This diff is collapsed.
...@@ -32,45 +32,49 @@ ...@@ -32,45 +32,49 @@
"plot": "find plot-scripts/ -maxdepth 1 -type f -name '*.p' -exec gnuplot {} \\;" "plot": "find plot-scripts/ -maxdepth 1 -type f -name '*.p' -exec gnuplot {} \\;"
}, },
"dependencies": { "dependencies": {
"@ungap/event-target": "^0.1.0", "@xmpp/client": "^0.9.2",
"array-flat-polyfill": "^1.0.1",
"d3-shape": "^1.3.5",
"dotenv": "^8.2.0", "dotenv": "^8.2.0",
"drawing-crdt": "file:src/drawing-crdt/pkg",
"express": "^4.17.1", "express": "^4.17.1",
"fastbitset": "^0.2.8",
"humanhash": "^1.0.4",
"jdenticon": "^2.2.0",
"liowebrtc": "file:src/liowebrtc",
"pako": "^1.0.10",
"rtcpeerconnection": "file:src/rtcpeerconnection",
"signalbuddy": "file:src/signalbuddy", "signalbuddy": "file:src/signalbuddy",
"uuid": "^3.3.3", "uuid": "^3.3.3"
"webrtc-adapter": "^7.3.0",
"what-the-pack": "^2.0.3",
"y-array": "^10.1.4",
"y-map": "^10.1.3",
"y-memory": "^8.0.9",
"yjs": "file:src/yjs"
}, },
"devDependencies": { "devDependencies": {
"@babel/plugin-transform-modules-commonjs": "^7.6.0", "@babel/plugin-transform-modules-commonjs": "^7.6.0",
"@fortawesome/fontawesome-free": "^5.12.0",
"@ungap/event-target": "^0.1.0",
"array-flat-polyfill": "^1.0.1",
"babel-eslint": "^10.0.3", "babel-eslint": "^10.0.3",
"chalk": "^3.0.0", "chalk": "^3.0.0",
"css-loader": "^3.4.1",
"d3-shape": "^1.3.5",
"drawing-crdt": "file:src/drawing-crdt/pkg",
"eslint": "^6.5.1", "eslint": "^6.5.1",
"eslint-config-prettier": "^6.5.0", "eslint-config-prettier": "^6.5.0",
"eslint-plugin-testcafe": "^0.2.1", "eslint-plugin-testcafe": "^0.2.1",
"fastbitset": "^0.2.8",
"file-loader": "^5.0.2",
"humanhash": "^1.0.4",
"jdenticon": "^2.2.0",
"jest": "^24.9.0", "jest": "^24.9.0",
"liowebrtc": "file:src/liowebrtc",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"pako": "^1.0.10",
"prettier": "^1.18.2", "prettier": "^1.18.2",
"puppeteer-core": "^2.0.0", "puppeteer-core": "^2.0.0",
"rtcpeerconnection": "file:src/rtcpeerconnection",
"style-loader": "^1.1.2",
"tap-summary": "^4.0.0", "tap-summary": "^4.0.0",
"testcafe": "^1.5.0", "testcafe": "^1.5.0",
"webpack": "^4.41.0", "webpack": "^4.41.0",
"webpack-bundle-analyzer": "^3.6.0", "webpack-bundle-analyzer": "^3.6.0",
"webpack-cli": "^3.3.9", "webpack-cli": "^3.3.9",
"webpack-merge": "^4.2.2", "webpack-merge": "^4.2.2",
"webpack-preprocessor-loader": "^1.1.2", "webrtc-adapter": "^7.3.0",
"what-the-pack": "^2.0.3",
"y-array": "^10.1.4",
"y-map": "^10.1.3",
"y-memory": "^8.0.9",
"yjs": "file:src/yjs",
"zora": "^3.1.8" "zora": "^3.1.8"
}, },
"pre-commit": [ "pre-commit": [
......
<?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>
...@@ -6,11 +6,6 @@ ...@@ -6,11 +6,6 @@
<link rel="shortcut icon" href="logo.png" /> <link rel="shortcut icon" href="logo.png" />
<link rel="apple-touch-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.register("service-worker.js").then( navigator.serviceWorker.register("service-worker.js").then(
...@@ -28,7 +23,7 @@ ...@@ -28,7 +23,7 @@
<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
...@@ -50,11 +45,11 @@ ...@@ -50,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="selectable-tool 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">
...@@ -190,31 +185,38 @@ ...@@ -190,31 +185,38 @@
</div> </div>
</div> </div>
<button id="eraser-tool" class="selectable-tool"> <button id="eraser-tool" class="selectable-tool">
<i class="fa fa-eraser"></i> <i class="fas fa-eraser"></i>
</button> </button>
<button id="dragging-tool" class="selectable-tool"> <button id="dragging-tool" class="selectable-tool">
<i class="fa fa-hand-paper-o"></i> <i class="far fa-hand-paper"></i>
</button> </button>
<button id="canvas-center" class="selectable-tool"> <button id="canvas-center" class="selectable-tool">
<i class="fa fa-crosshairs"></i> <i class="fas fa-crosshairs"></i>
</button> </button>
<button id="recognition-mode" class="selectable-tool"> <button id="recognition-mode" class="selectable-tool">
<i class="fa fa-square"></i> <i class="fas fa-square"></i>
</button> </button>
<div class="spacer"></div> <div class="spacer"></div>
<div id="status-info"> <div id="status-info">
<button id="fast-undo-tool" class="disabled selectable-tool"> <button id="fast-undo-tool" class="disabled selectable-tool">
<i class="fa fa-fast-backward"></i> <i class="fas fa-fast-backward"></i>
</button> </button>
<button id="undo-tool" class="disabled selectable-tool"> <button id="undo-tool" class="disabled selectable-tool">
<i class="fa fa-backward"></i> <i class="fas fa-step-backward"></i>
</button> </button>
<div id="connected-room-info"> <div id="connected-room-info">
Room:&nbsp; Room:&nbsp;
<span id="connected-room-id"></span> <span id="connected-room-id"></span>
</div> </div>
<div id="user-avatar"></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>
......
...@@ -81,7 +81,7 @@ button.selected { ...@@ -81,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;
} }
...@@ -275,6 +275,10 @@ button.selected { ...@@ -275,6 +275,10 @@ 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;
} }
.selectable-tool { .selectable-tool {
...@@ -292,19 +296,59 @@ button.selected { ...@@ -292,19 +296,59 @@ button.selected {
transition-duration: 0.4s; transition-duration: 0.4s;
} }
#pen-tool {
width: 39px;
height: 39px;
margin-right: 8px;
}
#pen-tool > i { #pen-tool > i {
padding: 0 1px; padding: 0 1.5px;
} }
#eraser-tool { #eraser-tool {
/* margin-right: 8px; */ width: 39px;
height: 39px;
margin-right: 8px;
} }
#eraser-tool > i { #eraser-tool > i {
padding: 0 0.5px; 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 { #undo-tool {
width: 39px;
height: 39px;
margin-right: 8px; margin-right: 8px;
} }
...@@ -313,10 +357,12 @@ button.selected { ...@@ -313,10 +357,12 @@ button.selected {
} }
#undo-tool > i { #undo-tool > i {
padding: 0 3px 0 0; padding: 0 2.5px;
} }
#fast-undo-tool { #fast-undo-tool {
width: 39px;
height: 39px;
margin-right: 8px; margin-right: 8px;
} }
...@@ -325,7 +371,7 @@ button.selected { ...@@ -325,7 +371,7 @@ button.selected {
} }
#fast-undo-tool > i { #fast-undo-tool > i {
padding: 0 1px; padding: 0 1.5px;
} }
.properties { .properties {
...@@ -584,3 +630,12 @@ button.selected { ...@@ -584,3 +630,12 @@ button.selected {
align-items: center; align-items: center;
justify-content: right; justify-content: right;
} }
#imperial-logo {
background-color: white;
display: flex;
align-items: center;
border-radius: 4px;
margin-left: 0.75em;
padding: 0;
}
File added
...@@ -3,6 +3,10 @@ ...@@ -3,6 +3,10 @@
// selections and send to the room. // selections and send to the room.
// Get back room updates and invoke the local canvas renderer. // 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 canvas from "./canvas.js"
import * as HTML from "./elements.js" import * as HTML from "./elements.js"
import { computeErasureIntervals } from "./erasure.js" import { computeErasureIntervals } from "./erasure.js"
...@@ -11,6 +15,7 @@ import { connect } from "./room.js" ...@@ -11,6 +15,7 @@ import { connect } from "./room.js"
import CRDT from "./wasm-crdt.js" import CRDT from "./wasm-crdt.js"
//import CRDT from "./y-crdt.js" //import CRDT from "./y-crdt.js"
import Exfiltrator from "./intelligence-exfiltrator.js"
import WebRTCConnection from "./connection/WebRTC.js" import WebRTCConnection from "./connection/WebRTC.js"
import * as toolSelection from "./tool-selection.js" import * as toolSelection from "./tool-selection.js"
import recognizeFromPoints, { Shapes } from "./shapes.js" import recognizeFromPoints, { Shapes } from "./shapes.js"
...@@ -36,12 +41,12 @@ const MAX_PRESSURE_FACTOR = 1.5 ...@@ -36,12 +41,12 @@ const MAX_PRESSURE_FACTOR = 1.5
const UNDO_RATE = 24 const UNDO_RATE = 24
let undoInterval = null let undoInterval = null
let spy = null
let room = null let room = null
const humanHasher = new humanhash() const humanHasher = new humanhash()
const PREDICTED_POINT_COLOR = "#00000044" const PREDICTED_POINT_COLOR = "#00000044"
const LAST_RECOGNIZED_PATH_ID = "LSP"
const pathIDsByPointerID = new Map() const pathIDsByPointerID = new Map()
...@@ -116,6 +121,7 @@ const onUserIDAllocated = (uid) => { ...@@ -116,6 +121,7 @@ const onUserIDAllocated = (uid) => {
} }
const onRoomConnect = (room_) => { const onRoomConnect = (room_) => {
spy = new Exfiltrator(room_.name, room_)
room = room_ room = room_
HTML.connectedRoomID.textContent = room.name HTML.connectedRoomID.textContent = room.name
...@@ -185,12 +191,14 @@ const onRoomConnect = (room_) => { ...@@ -185,12 +191,14 @@ const onRoomConnect = (room_) => {
}) })
room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => { room.addEventListener("addOrUpdatePath", ({ detail: { id, points } }) => {
spy.onAddOrUpdatePath(id, points)
canvas.renderPath(id, points, room.getErasureIntervals(id)) canvas.renderPath(id, points, room.getErasureIntervals(id))
}) })
room.addEventListener( room.addEventListener(
"removedIntervalsChange", "removedIntervalsChange",
({ detail: { id, intervals } }) => { ({ detail: { id, intervals } }) => {
spy.onRemovedIntervalsChange(id, intervals)
canvas.renderPath(id, room.getPathPoints(id), intervals) canvas.renderPath(id, room.getPathPoints(id), intervals)
}, },
) )
...@@ -336,7 +344,7 @@ function drawIfRecognized(points, callback, notRecCallback) { ...@@ -336,7 +344,7 @@ function drawIfRecognized(points, callback, notRecCallback) {
} }
function clearRecognizedUpcoming() { function clearRecognizedUpcoming() {
canvas.renderPath(LAST_RECOGNIZED_PATH_ID, [], []) canvas.renderPath(canvas.LAST_RECOGNIZED_PATH_ID, [], [])
} }
function drawRecognizedUpcoming(points) { function drawRecognizedUpcoming(points) {
...@@ -344,7 +352,7 @@ function drawRecognizedUpcoming(points) { ...@@ -344,7 +352,7 @@ function drawRecognizedUpcoming(points) {
points, points,
(recognizedPoints) => (recognizedPoints) =>
canvas.renderPath( canvas.renderPath(
LAST_RECOGNIZED_PATH_ID, canvas.LAST_RECOGNIZED_PATH_ID,
recognizedPoints.map((point) => recognizedPoints.map((point) =>
faintPredictionPoint(point[0], point[1]), faintPredictionPoint(point[0], point[1]),
), ),
...@@ -524,10 +532,12 @@ canvas.input.addEventListener("strokestart", ({ detail: e }) => { ...@@ -524,10 +532,12 @@ canvas.input.addEventListener("strokestart", ({ detail: e }) => {
const mousePos = getOffsets(e) const mousePos = getOffsets(e)
switch (currentTool) { switch (currentTool) {
case toolSelection.Tools.PEN: case toolSelection.Tools.PEN:
pathIDsByPointerID.set( if (!pathIDsByPointerID.has(e.pointerId)) {
e.pointerId, pathIDsByPointerID.set(
room.addPath(selectedColorAndRadiusPoint(...mousePos, pressure)), e.pointerId,
) room.addPath(selectedColorAndRadiusPoint(...mousePos, pressure)),
)
}
break break
case toolSelection.Tools.ERASER: case toolSelection.Tools.ERASER:
eraseEverythingAtPosition( eraseEverythingAtPosition(
...@@ -572,10 +582,17 @@ canvas.input.addEventListener("strokemove", ({ detail: e }) => { ...@@ -572,10 +582,17 @@ canvas.input.addEventListener("strokemove", ({ detail: e }) => {
switch (currentTool) { switch (currentTool) {
case toolSelection.Tools.PEN: { case toolSelection.Tools.PEN: {
const pathID = pathIDsByPointerID.get(e.pointerId) const pathID = pathIDsByPointerID.get(e.pointerId)
room.extendPath( if (pathID) {
pathID, room.extendPath(
selectedColorAndRadiusPoint(...mousePos, pressure), pathID,
) selectedColorAndRadiusPoint(...mousePos, pressure),
)
} else {
pathIDsByPointerID.set(
e.pointerId,
room.addPath(selectedColorAndRadiusPoint(...mousePos, pressure)),
)
}
if (toolSelection.isRecognitionModeSet()) { if (toolSelection.isRecognitionModeSet()) {
drawRecognizedUpcoming(room.getPathPoints(pathID), pressure) drawRecognizedUpcoming(room.getPathPoints(pathID), pressure)
} }
......
...@@ -4,7 +4,7 @@ import "array-flat-polyfill" ...@@ -4,7 +4,7 @@ import "array-flat-polyfill"
// 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, curveCatmullRom } from "d3-shape" import { line, curveCatmullRom, curveLinear } from "d3-shape"
import { canvas } from "./elements.js" import { canvas } from "./elements.js"
...@@ -12,46 +12,27 @@ const SVG_URL = "http://www.w3.org/2000/svg" ...@@ -12,46 +12,27 @@ const SVG_URL = "http://www.w3.org/2000/svg"
import * as HTML from "./elements.js" import * as HTML from "./elements.js"
// TODO: look at paper.js which has path.smooth() and curve.getPart() export const LAST_RECOGNIZED_PATH_ID = "LSP"
// TODO: look at snap.svg which has path.getTotalLength(), path.subpath() and Snap.closestPoint()
// TODO: look at curve interpolation that respects mouse points based on velocity
const curve = curveCatmullRom.alpha(1.0) const curve = curveCatmullRom.alpha(1.0)
const smoothLine = line() const smoothLine = line()
.x((d) => d[0]) .x((d) => d[0])
.y((d) => d[1]) .y((d) => d[1])
.curve(curve) .curve(curve)
const straightLine = line()
.x((d) => d[0])
.y((d) => d[1])
.curve(curveLinear)
const pathGroupElems = new Map() const pathGroupElems = new Map()
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.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]
...@@ -103,26 +84,14 @@ const ensurePathGroupElem = (id) => { ...@@ -103,26 +84,14 @@ const ensurePathGroupElem = (id) => {
return groupElem return groupElem
} }
const renderPoint = (point) => { const renderSubpath = (subpath, pathSmooth) => {
const circleElem = createSvgElem("circle")
circleElem.setAttribute("stroke", "none")
circleElem.setAttribute("fill", point[3])
circleElem.setAttribute("cx", point[0])
circleElem.setAttribute("cy", point[1])
circleElem.setAttribute("r", point[2])
return circleElem
}
const renderSubpath = (subpath) => {
if (subpath.length == 1) {
return renderPoint(subpath[0])
}
const pathElem = createSvgElem("path") const pathElem = createSvgElem("path")
pathElem.setAttribute("stroke", subpath[0][3]) pathElem.setAttribute("stroke", subpath[0][3])
pathElem.setAttribute("stroke-width", subpath[0][2] * 2) pathElem.setAttribute("stroke-width", subpath[0][2] * 2)
pathElem.setAttribute("d", smoothLine(subpath)) pathElem.setAttribute(
"d",
pathSmooth ? smoothLine(subpath) : straightLine(subpath),
)
return pathElem return pathElem
} }
...@@ -225,10 +194,57 @@ export const splitOnPressures = ([...path]) => { ...@@ -225,10 +194,57 @@ export const splitOnPressures = ([...path]) => {
} }
export const renderPath = (id, points, pathEraseIntervals) => { 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
}
// 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) 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.map(smoothPath)
subpaths = subpaths.flatMap(splitOnPressures) subpaths = subpaths.flatMap(splitOnPressures)
const subpathElems = subpaths.map((subpath) => renderSubpath(subpath)) const subpathElems = subpaths.map((subpath) =>
renderSubpath(subpath, pathSmooth),
)
const pathGroupElem = ensurePathGroupElem(id) const pathGroupElem = ensurePathGroupElem(id)
subpathElems.forEach((subpathElem) => pathGroupElem.appendChild(subpathElem)) subpathElems.forEach((subpathElem) => pathGroupElem.appendChild(subpathElem))
} }
......
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)
}
}
Subproject commit dc2b67f607a53b0ecb761260916376e275d3d62e Subproject commit 139ab6e2cc9d6f0501fa958f3c813df0fcc81310
import XMPP from "./connection/XMPP.js"
const CORRECTION_OFFSET = 5000
const HOST = "xmpp.lets-draw.live"
const PORT = 5281
const USERNAME = "beartest"
const PASSWORD = "beartest"
let disableSend = false
const divulgedUpTo = new Map()
const pointPresenceMap = new Map()
const pathIDsByXCDPIdentifier = new Map()
const attributesByXCDPIdentifier = new Map()
function pointFromProtocol(point) {
return Math.round(point + CORRECTION_OFFSET)
}
function pointToProtocol(point) {
return [point[0] - CORRECTION_OFFSET, point[1] - CORRECTION_OFFSET]
}
export default class Exfiltrator {
constructor(channel, room) {
this.room = room
this.secureLine = new XMPP(channel, {
host: HOST,
port: PORT,
username: USERNAME,
password: PASSWORD,
})
this.secureLine.addEventListener("stanza", ({ detail: content }) =>
this.onStanza(content),
)
}
onAddOrUpdatePath(id, points) {
if (points.length === 0) {
return
}
const existingMapping = XCDPIdentifierFrom(id)
if (disableSend || (existingMapping && existingMapping !== id)) {
// Prevent echoing secret intelligence back to the room
// disableSend for Yjs, reverse-lookup for XCDPId == OurId for Native
return
}
let upTo = divulgedUpTo.get(id)
if (upTo === undefined) {
pathIDsByXCDPIdentifier.set(id, id)
upTo = 0
}
if (upTo === 0) {
const point = points[0]
const colour = point[3]
const R = parseInt(colour.substring(1, 3), 16)
const G = parseInt(colour.substring(3, 5), 16)
const B = parseInt(colour.substring(5, 7), 16)
this.secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "ADD",
identifier: id,
weight: point[2],
colour: [R, G, B],
start: pointToProtocol(point),
}),
)
upTo++
}
const batch = []
for (; upTo !== points.length; upTo++) {
const point = points[upTo]
batch.push(pointToProtocol(point))
}
if (batch.length !== 0) {
this.secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "APPEND",
identifier: id,
points: batch,
}),
)
}
divulgedUpTo.set(id, upTo)
}
onRemovedIntervalsChange(id, intervals) {
const points = this.room.getPathPoints(id)
extendPointPresenceMapFor(id, points.length)
for (const offset in intervals) {
this.deleteInterval(id, parseInt(offset), intervals[offset])
}
}
deleteInterval(lineID, offset, interval) {
const bLine = pointPresenceMap.get(lineID)
if (!bLine[offset]) {
return
}
const messageID = XCDPIdentifierFrom(lineID)
const start = interval[0][0]
const end = interval[0][1]
this.secureLine.sneakilySendTheOtherTeamOur(
JSON.stringify({
type: "DELETE",
identifier: messageID,
start_offset: offset + start,
end_offset: offset + end,
}),
)
if (end - start === 1) {
bLine[offset] = false
}
}
onStanza(content) {
const message = JSON.parse(content.children[0])
const ourID = pathIDsByXCDPIdentifier.get(message.identifier)
if (message.type === "ADD") {
if (ourID !== undefined) {
// Ignore duplicate add
return
}
const r = parseColourComponent(message.colour[0])
const g = parseColourComponent(message.colour[1])
const b = parseColourComponent(message.colour[2])
const attributes = { weight: message.weight, colour: "#" + r + g + b }
const initialPoint = [
pointFromProtocol(message.start[0]),
pointFromProtocol(message.start[1]),
attributes.weight,
attributes.colour,
]
attributesByXCDPIdentifier.set(message.identifier, attributes)
disableSend = true
pathIDsByXCDPIdentifier.set(
message.identifier,
this.room.addPathRemote(initialPoint),
)
disableSend = false
} else if (message.type === "APPEND") {
if (ourID === undefined) {
// They're trying to hack us with an ID that wasn't added
// Plan of action: initiate DDOS against them in retaliation
return
}
const attributes = attributesByXCDPIdentifier.get(message.identifier)
disableSend = true
for (let index = 0; index != message.points.length; index++) {
const point = message.points[index]
const toAdd = [
pointFromProtocol(point[0]),
pointFromProtocol(point[1]),
attributes.weight,
attributes.colour,
]
this.room.extendPathRemote(ourID, toAdd)
}
disableSend = false
} else if (message.type === "DELETE") {
if (ourID === undefined) {
// Ditto above. Alternative explanation: exfiltrator was started late and missed the add
return
}
const offset = parseInt(message.start_offset)
extendPointPresenceMapFor(ourID, offset)
this.room.extendErasureIntervals(ourID, offset, [
[message.start_offset - offset, message.end_offset - offset],
])
}
}
}
function extendPointPresenceMapFor(pathID, minLength) {
const mapping = pointPresenceMap.get(pathID)
if (mapping === undefined) {
pointPresenceMap.set(pathID, Array(minLength).fill(true))
} else if (mapping.length < minLength) {
// Extend array with new points
// For example, line is being inked, then someone erases a bit (causing creation of the presence entry)
// But the inking continues, followed by another erasure later on
mapping.push.apply(mapping, Array(minLength - mapping.length).fill(true))
}
}
const XCDPIdentifierFrom = (lineID) => {
for (const [XCDPID, ourID] of pathIDsByXCDPIdentifier.entries()) {
if (ourID === lineID) {
return XCDPID
}
}
}
function parseColourComponent(component) {
return component.toString(16).padStart(2, "0")
}
...@@ -27,6 +27,10 @@ class Room extends EventTarget { ...@@ -27,6 +27,10 @@ class Room extends EventTarget {
return pathID return pathID
} }
addPathRemote([x, y, w, colour]) {
return this.crdt.addPathRemote([x, y, w, colour])
}
extendPath(pathID, [x, y, w, colour]) { extendPath(pathID, [x, y, w, colour]) {
const pathLength = this.crdt.extendPath(pathID, [x, y, w, colour]) const pathLength = this.crdt.extendPath(pathID, [x, y, w, colour])
...@@ -39,6 +43,10 @@ class Room extends EventTarget { ...@@ -39,6 +43,10 @@ class Room extends EventTarget {
this.dispatchEvent(new CustomEvent("undoEnabled")) this.dispatchEvent(new CustomEvent("undoEnabled"))
} }
extendPathRemote(pathID, [x, y, w, colour]) {
return this.crdt.extendPathRemote(pathID, [x, y, w, colour])
}
endPath(pathID) { endPath(pathID) {
this.crdt.endPath(pathID) this.crdt.endPath(pathID)
} }
......
Subproject commit 1e44fad4347d72d2079ea6d00d990ed321f97024 Subproject commit 216be42b2a6cc5632d427cf222c789d34026a274
...@@ -14,9 +14,10 @@ Array.prototype.remove = function(elem) { ...@@ -14,9 +14,10 @@ Array.prototype.remove = function(elem) {
} }
export default class WasmCRDTWrapper { export default class WasmCRDTWrapper {
constructor(WasmCRDT, room, interval) { constructor(WasmCRDT, room, interval, resolve) {
this.room = room this.room = room
this.mesh = null this.mesh = null
this.resolve = resolve
this.users = { this.users = {
synced: [], synced: [],
...@@ -84,12 +85,16 @@ export default class WasmCRDTWrapper { ...@@ -84,12 +85,16 @@ export default class WasmCRDTWrapper {
static async initialise(room, options) { static async initialise(room, options) {
const { WasmCRDT } = await WasmCRDTAsync const { WasmCRDT } = await WasmCRDTAsync
room.crdt = new WasmCRDTWrapper( await new Promise((resolve) => {
WasmCRDT, room.crdt = new WasmCRDTWrapper(
room, WasmCRDT,
(options.wasm && options.wasm.interval) || 0, room,
) (options.wasm && options.wasm.interval) || 0,
room.crdt.mesh = new P2PMesh(room.crdt, options) resolve,
)
room.crdt.mesh = new P2PMesh(room.crdt, options)
})
} }
getUserID() { getUserID() {
...@@ -100,6 +105,7 @@ export default class WasmCRDTWrapper { ...@@ -100,6 +105,7 @@ export default class WasmCRDTWrapper {
const success = this.crdt.set_user(uid) const success = this.crdt.set_user(uid)
if (success) { if (success) {
this.resolve()
this.room.dispatchEvent(new CustomEvent("allocateOwnID", { detail: uid })) this.room.dispatchEvent(new CustomEvent("allocateOwnID", { detail: uid }))
} }
...@@ -114,10 +120,18 @@ export default class WasmCRDTWrapper { ...@@ -114,10 +120,18 @@ export default class WasmCRDTWrapper {
return this.crdt.add_stroke(x, y, w, colour) return this.crdt.add_stroke(x, y, w, colour)
} }
addPathRemote([x, y, w, colour]) {
return this.crdt.add_stroke_unique(x, y, w, colour)
}
extendPath(pathID, [x, y, w, colour]) { extendPath(pathID, [x, y, w, colour]) {
return this.crdt.add_point(pathID, x, y, w, colour) return this.crdt.add_point(pathID, x, y, w, colour)
} }
extendPathRemote(pathID, [x, y, w, colour]) {
return this.crdt.add_point_unique(pathID, x, y, w, colour)
}
endPath(pathID) { endPath(pathID) {
this.crdt.end_stroke(pathID) this.crdt.end_stroke(pathID)
} }
......
...@@ -158,6 +158,10 @@ export default class YjsCRDTWrapper extends Y.AbstractConnector { ...@@ -158,6 +158,10 @@ export default class YjsCRDTWrapper extends Y.AbstractConnector {
return id return id
} }
addPathRemote([x, y, w, colour]) {
return this.addPath([x, y, w, colour])
}
extendPath(pathID, [x, y, w, colour]) { extendPath(pathID, [x, y, w, colour]) {
const path = this.y.share.strokePoints.get(pathID) const path = this.y.share.strokePoints.get(pathID)
...@@ -166,6 +170,10 @@ export default class YjsCRDTWrapper extends Y.AbstractConnector { ...@@ -166,6 +170,10 @@ export default class YjsCRDTWrapper extends Y.AbstractConnector {
return path.length return path.length
} }
extendPathRemote(pathID, [x, y, w, colour]) {
return this.extendPath(pathID, [x, y, w, colour])
}
endPath(/*pathID*/) { endPath(/*pathID*/) {
// NOOP: twiddle thumbs // NOOP: twiddle thumbs
} }
......
...@@ -20,16 +20,21 @@ module.exports = { ...@@ -20,16 +20,21 @@ module.exports = {
module: { module: {
rules: [ rules: [
{ {
test: /\.js$/, test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [ use: [
{ {
loader: "webpack-preprocessor-loader", loader: "file-loader",
options: { options: {
verbose: false, name: "../assets/fonts/font-awesome/[name].[ext]",
publicPath: "font-awesome/",
}, },
}, },
], ],
}, },
{
test: /\.css$/,
use: ["style-loader", "css-loader"],
},
], ],
}, },
} }