Browse Source

Touch controls weweweweyayayaya

master
A.Olokhtonov 1 year ago
parent
commit
f24711cced
  1. 47
      client/cursor.js
  2. 92
      client/default.css
  3. 0
      client/favicon_old.png
  4. 54
      client/icons/draw.svg
  5. 74
      client/icons/erase.svg
  6. 49
      client/icons/favicon.svg
  7. 54
      client/icons/redo.svg
  8. 49
      client/icons/ruler.svg
  9. 54
      client/icons/undo.svg
  10. 23
      client/index.html
  11. 78
      client/index.js
  12. 6
      client/math.js
  13. 6
      client/send.js
  14. 23
      client/tools.js
  15. 2
      client/touch.css
  16. 359
      client/touch.js
  17. 10
      client/websocket.js
  18. 4
      server/http.js
  19. 16
      server/send.js

47
client/cursor.js

@ -44,10 +44,13 @@ function on_down(e) { @@ -44,10 +44,13 @@ function on_down(e) {
storage.cursor.x = x;
storage.cursor.y = y;
if (storage.tool === 'brush') {
if (storage.tools.active === 'pencil') {
const predraw = predraw_event(x, y);
storage.current_stroke.push(predraw);
fire_event(predraw);
} else if (storage.tools.active === 'ruler') {
storage.ruler_origin.x = x;
storage.ruler_origin.y = y;
}
}
}
@ -77,7 +80,7 @@ function on_move(e) { @@ -77,7 +80,7 @@ function on_move(e) {
}
if (storage.state.drawing) {
if (storage.tool === 'brush') {
if (storage.tools.active === 'pencil') {
const width = storage.cursor.width;
storage.ctx1.beginPath();
@ -91,7 +94,7 @@ function on_move(e) { @@ -91,7 +94,7 @@ function on_move(e) {
storage.current_stroke.push(predraw);
fire_event(predraw);
} else if (storage.tool === 'eraser') {
} else if (storage.tools.active === 'eraser') {
const erased = strokes_intersect_line(last_x, last_y, x, y);
storage.erased.push(...erased);
@ -108,6 +111,23 @@ function on_move(e) { @@ -108,6 +111,23 @@ function on_move(e) {
}
}
}
} else if (storage.tools.active === 'ruler') {
const old_ruler = [
{'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y},
{'x': last_x, 'y': last_y}
];
const stats = stroke_stats(old_ruler, storage.cursor.width);
const bbox = stats.bbox;
storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin);
storage.ctx1.beginPath();
storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
} else {
console.error('fuck');
}
@ -150,11 +170,11 @@ async function on_up(e) { @@ -150,11 +170,11 @@ async function on_up(e) {
}
if (storage.state.drawing && e.button === 0) {
if (storage.tool === 'brush') {
if (storage.tools.active === 'pencil') {
const event = stroke_event();
storage.current_stroke = [];
await queue_event(event);
} else if (storage.tool === 'eraser') {
} else if (storage.tools.active === 'eraser') {
const events = eraser_events();
storage.erased = [];
if (events.length > 0) {
@ -162,6 +182,9 @@ async function on_up(e) { @@ -162,6 +182,9 @@ async function on_up(e) {
await queue_event(event);
}
}
} else if (storage.tools.active === 'ruler') {
const event = ruler_event(storage.cursor.x, storage.cursor.y);
await queue_event(event);
} else {
console.error('fuck');
}
@ -173,16 +196,6 @@ async function on_up(e) { @@ -173,16 +196,6 @@ async function on_up(e) {
}
function on_keydown(e) {
if (e.code === 'KeyE') {
storage.tool = 'eraser';
return;
}
if (e.code === 'KeyB') {
storage.tool = 'brush';
return;
}
if (e.code === 'Space' && !storage.state.drawing) {
storage.state.moving = true;
storage.state.spacedown = true;
@ -190,8 +203,7 @@ function on_keydown(e) { @@ -190,8 +203,7 @@ function on_keydown(e) {
}
if (e.code === 'KeyZ' && e.ctrlKey) {
const event = undo_event();
queue_event(event);
undo();
return;
}
}
@ -204,6 +216,7 @@ function on_keyup(e) { @@ -204,6 +216,7 @@ function on_keyup(e) {
}
function on_leave(e) {
// TODO: broken when "moving"
if (storage.state.moving) {
storage.state.moving = false;
storage.state.holding = false;

92
client/default.css

@ -2,6 +2,7 @@ html, body { @@ -2,6 +2,7 @@ html, body {
margin: 0;
padding: 0;
overflow: hidden;
touch-action: none;
}
.dhide {
@ -18,6 +19,16 @@ html, body { @@ -18,6 +19,16 @@ html, body {
pointer-events: none;
}
#toucher {
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 5; /* above all canvases, but below tools */
cursor: crosshair;
}
.canvas.white {
opacity: 0;
}
@ -28,8 +39,10 @@ html, body { @@ -28,8 +39,10 @@ html, body {
#canvas0 {
z-index: 1;
box-sizing: border-box;
border: 1px solid black;
background: #eee;
background-position: 0px 0px;
background-size: 32px 32px;
background-image: radial-gradient(rgba(0, 0, 0, 0.1) 1px, transparent 10%);
}
#canvas1 {
@ -37,50 +50,61 @@ html, body { @@ -37,50 +50,61 @@ html, body {
opacity: 0.3;
}
.toolbar {
.tools-wrapper {
position: fixed;
left: 20px;
top: 20px;
background: #eee;
border: 1px solid #ddd;
padding: 10px;
border-radius: 5px;
box-shadow: 0px 2px 2px 0px rgba(0, 0, 0, 0.1);
bottom: 0;
width: 100%;
height: 32px;
display: flex;
gap: 10px;
justify-content: center;
align-items: end;
z-index: 10;
align-items: center;
}
.toolbar #brush-width {
width: 7ch;
height: 30px;
padding: 5px;
box-sizing: border-box;
border: none;
cursor: crosshair;
.tools {
display: flex;
align-items: center;
justify-content: center;
background: #333;
border-radius: 5px;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
height: 42px;
padding-left: 10px;
padding-right: 10px;
}
.toolbar #brush-color {
padding: 0;
height: 30px;
width: 30px;
border: none;
.tool {
cursor: pointer;
padding-left: 10px;
padding-right: 10px;
height: 100%;
display: flex;
align-items: center;
background: #333;
transition: transform .1s ease-in-out;
user-select: none;
}
#brush-preview {
border-radius: 50%;
width: 5px;
height: 5px;
background: black;
position: absolute;
pointer-events: none;
z-index: 11;
.tool:hover {
background: #888;
}
.tool.active {
transform: translateY(-10px);
border-top-right-radius: 5px;
border-top-left-radius: 5px;
background: #333;
}
.toolbar #brush-color::-moz-color-swatch {
border: none;
.tool img {
height: 24px;
width: 24px;
filter: invert(100%);
}
.toolbar {
visibility: hidden;
}
.floating-image {

0
client/favicon.png → client/favicon_old.png

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 957 B

54
client/icons/draw.svg

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="29.716999mm"
height="30.635mm"
viewBox="0 0 29.716999 30.635"
version="1.1"
id="svg5"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="draw.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview7"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="6.7277216"
inkscape:cx="45.334813"
inkscape:cy="49.273739"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs2" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-74.769935,-147.21149)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 95.762931,151.36667 -1.459957,1.45995 -4.171293,-4.17129 -7.480518,7.48052 1.501665,1.50166 6.020569,-6.02056 2.669628,2.66962 -3.164437,3.16444 5.325352,5.32535 6.08434,-6.08434 z"
id="path1057"
sodipodi:nodetypes="ccccccccccc" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 87.28337,159.84623 5.401522,5.40152 -10.91227,11.04308 H 76.3711 v -5.53233 z"
id="path1785"
sodipodi:nodetypes="cccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

74
client/icons/erase.svg

@ -0,0 +1,74 @@ @@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="18.310799mm"
height="13.563546mm"
viewBox="0 0 18.310798 13.563546"
version="1.1"
id="svg4722"
sodipodi:docname="erase.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview4724"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="9.5144352"
inkscape:cx="25.697794"
inkscape:cy="26.223312"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs4719">
<inkscape:path-effect
effect="fillet_chamfer"
id="path-effect26362"
is_visible="true"
lpeversion="1"
nodesatellites_param="F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1 @ F,0,0,1,0,0.79375,0,1"
unit="px"
method="auto"
mode="F"
radius="3"
chamfer_steps="1"
flexible="false"
use_knot_distance="true"
apply_no_radius="true"
apply_with_radius="true"
only_selected="false"
hide_knots="false" />
</defs>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-71.863816,-97.210284)">
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 80.965463,98.271529 c -2.680135,2.680131 -5.360269,5.360271 -8.040401,8.040401 -0.309979,0.30998 -0.316694,0.8191 -0.015,1.13714 l 2.133265,2.24889 a 1.8470002,1.8470002 23.255659 0 0 1.340018,0.57587 h 3.418044 a 1.909119,1.909119 157.42404 0 0 1.353526,-0.56275 l 5.610134,-5.63997 c 0.309156,-0.3108 0.308489,-0.81404 -0.0015,-1.12402 -1.55852,-1.55852 -3.117043,-3.11704 -4.675566,-4.675561 -0.309979,-0.309979 -0.812554,-0.309979 -1.122532,0 z"
id="path5932"
sodipodi:nodetypes="cccccc"
inkscape:path-effect="#path-effect26362"
inkscape:original-d="m 81.526729,97.710263 c -3.054312,3.054307 -6.108624,6.108627 -9.162933,9.162937 l 3.225801,3.40063 h 5.005544 l 6.729686,-6.76547 c -1.932697,-1.9327 -3.865398,-3.865399 -5.798098,-5.798097 z" />
<path
style="fill:none;stroke:#000000;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:miter;stroke-dasharray:none;stroke-opacity:1"
d="m 85.793244,110.02383 h 3.63137"
id="path6432"
sodipodi:nodetypes="cc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.0 KiB

49
client/icons/favicon.svg

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="10mm"
height="10mm"
viewBox="0 0 9.9999997 10"
version="1.1"
id="svg31118"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
sodipodi:docname="favicon.svg"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview31120"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="19.02887"
inkscape:cx="21.730139"
inkscape:cy="19.62807"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs31115" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-102.35789,-119.40706)">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 103.07394,121.49803 c 0,0 1.8926,-1.50652 2.48887,-0.86207 0.80348,0.8684 -2.24958,2.41185 -1.41823,3.25361 1.77046,1.79265 5.16721,-4.69796 6.96607,-2.9338 1.82345,1.78827 -4.48394,5.16682 -2.83582,7.11786 0.74072,0.87686 3.36701,-0.72189 3.36701,-0.72189"
id="path33421"
sodipodi:nodetypes="caaaac" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

54
client/icons/redo.svg

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="7.3109999mm"
height="7.8540001mm"
viewBox="0 0 7.3109999 7.8540001"
version="1.1"
id="svg14027"
sodipodi:docname="redo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview14029"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="26.910887"
inkscape:cx="11.129325"
inkscape:cy="18.7099"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs14024" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-119.35516,-62.667286)">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 125.71554,68.856558 h -2.53401 c -2.08565,0 -2.77743,-0.237506 -2.77743,-1.919928 0,-1.682421 0.59993,-1.919928 2.77743,-1.919928 h 2.53401"
id="path14513"
sodipodi:nodetypes="cszsc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 124.57539,63.936869 1.14015,1.079833 -1.13791,1.077584"
id="path14515"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

49
client/icons/ruler.svg

@ -0,0 +1,49 @@ @@ -0,0 +1,49 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="27.691999mm"
height="27.691999mm"
viewBox="0 0 27.691999 27.691999"
version="1.1"
id="svg10490"
sodipodi:docname="ruler.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview10492"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="9.5144352"
inkscape:cx="50.922623"
inkscape:cy="49.976692"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs10487" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-71.559908,-125.1583)">
<path
id="path10611"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 90.427939,127.32683 -16.699494,16.6995 6.65543,6.65543 2.08721,-2.08721 1.205669,-1.20567 -2.90901,-2.90901 1.414211,-1.41421 2.90901,2.90901 1.555266,-1.55527 1.206692,-1.20669 -2.90901,-2.90901 1.414008,-1.41401 2.90901,2.90901 1.554079,-1.55408 1.205965,-1.20596 -2.91047,-2.91048 1.414007,-1.414 2.910471,2.91047 1.555178,-1.55518 2.087209,-2.08721 z"
sodipodi:nodetypes="ccccccccccccccccccccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

54
client/icons/undo.svg

@ -0,0 +1,54 @@ @@ -0,0 +1,54 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
width="7.3109999mm"
height="7.8540001mm"
viewBox="0 0 7.3109999 7.8540001"
version="1.1"
id="svg14027"
sodipodi:docname="undo.svg"
inkscape:version="1.2.2 (b0a8486541, 2022-12-01)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg">
<sodipodi:namedview
id="namedview14029"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:document-units="mm"
showgrid="false"
inkscape:zoom="26.910887"
inkscape:cx="11.129325"
inkscape:cy="18.7099"
inkscape:window-width="2558"
inkscape:window-height="1413"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="layer1" />
<defs
id="defs14024" />
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-119.35516,-62.667286)">
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 120.4041,68.856558 h 2.53401 c 2.08565,0 2.77743,-0.237506 2.77743,-1.919928 0,-1.682421 -0.59993,-1.919928 -2.77743,-1.919928 h -2.53401"
id="path14513"
sodipodi:nodetypes="cszsc" />
<path
style="fill:none;stroke:#000000;stroke-width:1;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
d="m 121.54425,63.936869 -1.14015,1.079833 1.13791,1.077584"
id="path14515"
sodipodi:nodetypes="ccc" />
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

23
client/index.html

@ -3,25 +3,42 @@ @@ -3,25 +3,42 @@
<head>
<meta charset="utf-8">
<title>Desk</title>
<link rel="icon" type="image/png" href="favicon.png">
<link rel="stylesheet" type="text/css" href="default.css">
<script type="text/javascript" src="index.js"></script>
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<link rel="shortcut icon" href="icons/favicon.svg" id="favicon">
<link rel="stylesheet" type="text/css" href="default.css?v=5">
<link rel="stylesheet" type="text/css" href="touch.css?v=2">
<script type="text/javascript" src="index.js?v=5"></script>
<script type="text/javascript" src="cursor.js"></script>
<script type="text/javascript" src="touch.js?v=18"></script>
<script type="text/javascript" src="websocket.js"></script>
<script type="text/javascript" src="send.js"></script>
<script type="text/javascript" src="recv.js"></script>
<script type="text/javascript" src="math.js"></script>
<script type="text/javascript" src="draw.js"></script>
<script type="text/javascript" src="tools.js?v=2"></script>
</head>
<body>
<div class="toolbar">
<input type="color" id="brush-color">
<input type="number" min="1" id="brush-width">
</div>
<div class="tools-wrapper">
<div class="tools">
<div class="tool" data-tool="pencil"><img draggable="false" src="icons/draw.svg"></div>
<div class="tool" data-tool="ruler"><img draggable="false" src="icons/ruler.svg"></div>
<div class="tool" data-tool="eraser"><img draggable="false" src="icons/erase.svg"></div>
<div class="tool" data-tool="undo"><img draggable="false" src="icons/undo.svg"></div>
<!-- <div class="tool" data-tool="redo"><img draggable="false" src="icons/redo.svg"></div> -->
</div>
</div>
<div id="brush-preview" class="dhide"></div>
<canvas class="canvas white" id="canvas0"></canvas>
<canvas class="canvas" id="canvas1"></canvas>
<div class="canvas" id="canvas-images"></div>
<div id="toucher"></div>
</body>
</html>

78
client/index.js

@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', main); @@ -6,6 +6,7 @@ document.addEventListener('DOMContentLoaded', main);
const EVENT = Object.freeze({
PREDRAW: 10,
STROKE: 20,
RULER: 21, /* gets re-written with EVENT.STROKE before sending to server */
UNDO: 30,
REDO: 31,
IMAGE: 40,
@ -23,10 +24,12 @@ const MESSAGE = Object.freeze({ @@ -23,10 +24,12 @@ const MESSAGE = Object.freeze({
});
const config = {
ws_url: 'wss://desk.local/ws/',
image_url: 'https://desk.local/images/',
ws_url: 'ws://192.168.100.2/ws/',
image_url: 'http://192.168.100.2/images/',
sync_timeout: 1000,
ws_reconnect_timeout: 2000,
second_finger_timeout: 500,
buffer_first_touchmoves: 5,
};
const storage = {
@ -41,8 +44,25 @@ const storage = { @@ -41,8 +44,25 @@ const storage = {
'moving_image_original_x': null,
'moving_image_original_y': null,
'touch': {
'moves': 0,
'drawing': false,
'moving': false,
'waiting_for_second_finger': false,
'position': { 'x': null, 'y': null },
'screen_position': { 'x': null, 'y': null },
'finger_distance': null,
'buffered': [],
'ids': [],
},
'tools': {
'active': null,
'active_element': null,
},
'ruler_origin': {},
'erased': [],
'tool': 'brush',
'predraw': {},
'timers': {},
'me': {},
@ -191,6 +211,20 @@ function stroke_event() { @@ -191,6 +211,20 @@ function stroke_event() {
};
}
function ruler_event(x, y) {
const points = [];
points.push(predraw_event(storage.ruler_origin.x, storage.ruler_origin.y));
points.push(predraw_event(x, y));
return {
'type': EVENT.RULER,
'points': points,
'width': storage.cursor.width,
'color': color_to_u32(storage.cursor.color),
};
}
function undo_event() {
return { 'type': EVENT.UNDO };
}
@ -242,20 +276,30 @@ function find_stroke_backwards(stroke_id) { @@ -242,20 +276,30 @@ function find_stroke_backwards(stroke_id) {
return null;
}
function queue_undo() {
const event = undo_event();
queue_event(event);
}
function main() {
const url = new URL(window.location.href);
const parts = url.pathname.split('/');
storage.desk_id = parts.length > 0 ? parts[parts.length - 1] : 0;
ws_connect();
ws_connect(true);
elements.canvas0 = document.getElementById('canvas0');
elements.canvas1 = document.getElementById('canvas1');
elements.images = document.getElementById('canvas-images');
tools_init();
// TODO: remove
elements.brush_color = document.getElementById('brush-color');
elements.brush_width = document.getElementById('brush-width');
elements.brush_preview = document.getElementById('brush-preview');
elements.toucher = document.getElementById('toucher');
elements.brush_color.value = storage.cursor.color;
elements.brush_width.value = storage.cursor.width;
@ -277,21 +321,25 @@ function main() { @@ -277,21 +321,25 @@ function main() {
storage.ctx1.lineJoin = storage.ctx1.lineCap = storage.ctx0.lineJoin = storage.ctx0.lineCap = 'round';
storage.ctx1.lineWidth = storage.ctx0.lineWidth = storage.cursor.width;
window.addEventListener('pointerdown', on_down)
window.addEventListener('pointermove', on_move)
window.addEventListener('pointerup', on_up);
window.addEventListener('pointercancel', on_up);
window.addEventListener('keydown', on_keydown);
window.addEventListener('keyup', on_keyup);
window.addEventListener('resize', on_resize);
window.addEventListener('wheel', on_wheel);
window.addEventListener('touchstart', cancel);
window.addEventListener('contextmenu', cancel);
elements.toucher.addEventListener('mousedown', on_down)
elements.toucher.addEventListener('mousemove', on_move)
elements.toucher.addEventListener('mouseup', on_up);
elements.toucher.addEventListener('keydown', on_keydown);
elements.toucher.addEventListener('keyup', on_keyup);
elements.toucher.addEventListener('resize', on_resize);
elements.toucher.addEventListener('contextmenu', cancel);
elements.toucher.addEventListener('wheel', on_wheel);
elements.toucher.addEventListener('touchstart', on_touchstart);
elements.toucher.addEventListener('touchmove', on_touchmove);
elements.toucher.addEventListener('touchend', on_touchend);
elements.toucher.addEventListener('touchcancel', on_touchend);
elements.brush_color.addEventListener('input', update_brush);
elements.brush_width.addEventListener('input', update_brush);
elements.canvas0.addEventListener('dragover', on_move);
elements.canvas0.addEventListener('drop', on_drop);
elements.canvas0.addEventListener('pointerleave', on_leave);
elements.canvas0.addEventListener('mouseleave', on_leave);
}

6
client/math.js

@ -216,4 +216,10 @@ function strokes_intersect_line(x1, y1, x2, y2) { @@ -216,4 +216,10 @@ function strokes_intersect_line(x1, y1, x2, y2) {
}
return result;
}
function dist_v2(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}

6
client/send.js

@ -150,6 +150,12 @@ function push_event(event) { @@ -150,6 +150,12 @@ function push_event(event) {
break;
}
case EVENT.RULER: {
event.type = EVENT.STROKE;
storage.queue.push(event);
break;
}
case EVENT.ERASER:
case EVENT.IMAGE:
case EVENT.IMAGE_MOVE:

23
client/tools.js

@ -0,0 +1,23 @@ @@ -0,0 +1,23 @@
function tools_switch(tool) {
if (storage.tools.active_element) {
storage.tools.active_element.classList.remove('active');
}
storage.tools.active = tool;
storage.tools.active_element = document.querySelector(`.tool[data-tool="${tool}"`);
storage.tools.active_element.classList.add('active');
}
function tools_init() {
const pencil = document.querySelector('.tool[data-tool="pencil"]');
const ruler = document.querySelector('.tool[data-tool="ruler"]');
const eraser = document.querySelector('.tool[data-tool="eraser"]');
const undo = document.querySelector('.tool[data-tool="undo"]');
pencil.addEventListener('click', () => tools_switch('pencil'));
ruler.addEventListener('click', () => tools_switch('ruler'));
eraser.addEventListener('click', () => tools_switch('eraser'));
undo.addEventListener('click', queue_undo);
tools_switch('pencil');
}

2
client/touch.css

@ -0,0 +1,2 @@ @@ -0,0 +1,2 @@
@media (pointer:none), (pointer:coarse) {
}

359
client/touch.js

@ -0,0 +1,359 @@ @@ -0,0 +1,359 @@
function on_touchstart(e) {
e.preventDefault();
if (storage.touch.drawing) {
return;
}
// First finger(s) down?
if (storage.touch.ids.length === 0) {
// We only handle 1 and 2
if (e.changedTouches.length > 2) {
return;
}
storage.touch.ids.length = 0;
for (const touch of e.changedTouches) {
storage.touch.ids.push(touch.identifier);
}
if (e.changedTouches.length === 1) {
const touch = e.changedTouches[0];
const x = Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom);
const y = Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom);
storage.touch.position.x = x;
storage.touch.position.y = y;
// We give a bit of time to add a second finger
storage.touch.waiting_for_second_finger = true;
storage.touch.moves = 0;
storage.touch.buffered.length = 0;
storage.ruler_origin.x = x;
storage.ruler_origin.y = y;
setTimeout(() => {
storage.touch.waiting_for_second_finger = false;
}, config.second_finger_timeout);
}
return;
}
// There are touches already
if (storage.touch.waiting_for_second_finger) {
if (e.changedTouches.length === 1) {
const changed_touch = e.changedTouches[0];
storage.touch.screen_position.x = changed_touch.clientX;
storage.touch.screen_position.y = changed_touch.clientY;
storage.touch.ids.push(e.changedTouches[0].identifier);
let first_finger_position = null;
let second_finger_position = null;
// A separate loop because touches might be in different order ? (question mark)
// IMPORTANT: e.touches, not e.changedTouches!
for (const touch of e.touches) {
const x = touch.clientX;
const y = touch.clientY;
if (touch.identifier === storage.touch.ids[0]) {
first_finger_position = {'x': x, 'y': y};
}
if (touch.identifier === storage.touch.ids[1]) {
second_finger_position = {'x': x, 'y': y};
}
}
storage.touch.finger_distance = dist_v2(
first_finger_position, second_finger_position);
// console.log(storage.touch.finger_distance);
}
return;
}
}
function on_touchmove(e) {
if (storage.touch.ids.length === 1 && !storage.touch.moving) {
storage.touch.moves += 1;
if (storage.touch.moves > config.buffer_first_touchmoves) {
storage.touch.waiting_for_second_finger = false; // Immediately start drawing on move
storage.touch.drawing = true;
if (storage.ctx1.lineWidth !== storage.cursor.width) {
storage.ctx1.lineWidth = storage.cursor.width;
}
} else {
let drawing_touch = null;
for (const touch of e.changedTouches) {
if (touch.identifier === storage.touch.ids[0]) {
drawing_touch = touch;
break;
}
}
if (!drawing_touch) {
return;
}
const last_x = storage.touch.position.x;
const last_y = storage.touch.position.y;
const x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
storage.touch.buffered.push({
'last_x': last_x,
'last_y': last_y,
'x': x,
'y': y,
});
storage.touch.position.x = x;
storage.touch.position.y = y;
}
}
if (storage.touch.drawing) {
let drawing_touch = null;
for (const touch of e.changedTouches) {
if (touch.identifier === storage.touch.ids[0]) {
drawing_touch = touch;
break;
}
}
if (!drawing_touch) {
return;
}
const last_x = storage.touch.position.x;
const last_y = storage.touch.position.y;
const x = storage.touch.position.x = Math.max(Math.round((drawing_touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const y = storage.touch.position.y = Math.max(Math.round((drawing_touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
if (storage.tools.active === 'pencil') {
if (storage.touch.buffered.length > 0) {
for (const p of storage.touch.buffered) {
storage.ctx1.beginPath();
storage.ctx1.moveTo(p.last_x, p.last_y);
storage.ctx1.lineTo(p.x, p.y);
storage.ctx1.stroke();
const predraw = predraw_event(p.x, p.y);
storage.current_stroke.push(predraw);
fire_event(predraw);
}
storage.touch.buffered.length = 0;
}
storage.ctx1.beginPath();
storage.ctx1.moveTo(last_x, last_y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
const predraw = predraw_event(x, y);
storage.current_stroke.push(predraw);
fire_event(predraw);
storage.touch.position.x = x;
storage.touch.position.y = y;
return;
} else if (storage.tools.active === 'eraser') {
const erase_step = (last_x, last_y, x, y) => {
const erased = strokes_intersect_line(last_x, last_y, x, y);
storage.erased.push(...erased);
if (erased.length > 0) {
for (const other_event of storage.events) {
for (const stroke_id of erased) {
if (stroke_id === other_event.stroke_id) {
if (!other_event.deleted) {
other_event.deleted = true;
const stats = stroke_stats(other_event.points, storage.cursor.width);
redraw_region(stats.bbox);
}
}
}
}
}
};
if (storage.touch.buffered.length > 0) {
for (const p of storage.touch.buffered) {
erase_step(p.last_x, p.last_y, p.x, p.y);
}
storage.touch.buffered.length = 0;
}
erase_step(last_x, last_y, x, y);
} else if (storage.tools.active === 'ruler') {
const old_ruler = [
{'x': storage.ruler_origin.x, 'y': storage.ruler_origin.y},
{'x': last_x, 'y': last_y}
];
const stats = stroke_stats(old_ruler, storage.cursor.width);
const bbox = stats.bbox;
storage.ctx1.clearRect(bbox.xmin, bbox.ymin, bbox.xmax - bbox.xmin, bbox.ymax - bbox.ymin);
storage.ctx1.beginPath();
storage.ctx1.moveTo(storage.ruler_origin.x, storage.ruler_origin.y);
storage.ctx1.lineTo(x, y);
storage.ctx1.stroke();
} else {
console.error('fuck');
}
}
if (storage.touch.ids.length === 2) {
storage.touch.moving = true;
let first_finger_position_screen = null;
let second_finger_position_screen = null;
let first_finger_position_canvas = null;
let second_finger_position_canvas = null;
// A separate loop because touches might be in different order ? (question mark)
// IMPORTANT: e.touches, not e.changedTouches!
for (const touch of e.touches) {
const x = touch.clientX;
const y = touch.clientY;
const xc = Math.max(Math.round((touch.clientX + storage.canvas.offset_x) / storage.canvas.zoom), 0);
const yc = Math.max(Math.round((touch.clientY + storage.canvas.offset_y) / storage.canvas.zoom), 0);
if (touch.identifier === storage.touch.ids[0]) {
first_finger_position_screen = {'x': x, 'y': y};
first_finger_position_canvas = {'x': xc, 'y': yc};
}
if (touch.identifier === storage.touch.ids[1]) {
second_finger_position_screen = {'x': x, 'y': y};
second_finger_position_canvas = {'x': xc, 'y': yc};
}
}
const new_finger_distance = dist_v2(
first_finger_position_screen, second_finger_position_screen);
const zoom_center = {
'x': (first_finger_position_canvas.x + second_finger_position_canvas.x) / 2.0,
'y': (first_finger_position_canvas.y + second_finger_position_canvas.y) / 2.0
};
for (const touch of e.changedTouches) {
// The second finger to be down is considered the "main" one
// Movement of the second finger is ignored
if (touch.identifier === storage.touch.ids[1]) {
const x = Math.round(touch.clientX);
const y = Math.round(touch.clientY);
const dx = x - storage.touch.screen_position.x;
const dy = y - storage.touch.screen_position.y;
const old_zoom = storage.canvas.zoom;
const old_offset_x = storage.canvas.offset_x;
const old_offset_y = storage.canvas.offset_y;
storage.canvas.offset_x -= dx;
storage.canvas.offset_y -= dy;
// console.log(new_finger_distance, storage.touch.finger_distance);
const scale_by = new_finger_distance / storage.touch.finger_distance;
const dz = storage.canvas.zoom * (scale_by - 1.0);
const zoom_offset_y = Math.round(dz * zoom_center.y);
const zoom_offset_x = Math.round(dz * zoom_center.x);
if (storage.min_zoom <= storage.canvas.zoom * scale_by && storage.canvas.zoom * scale_by <= storage.max_zoom) {
storage.canvas.zoom *= scale_by;
storage.canvas.offset_x += zoom_offset_x;
storage.canvas.offset_y += zoom_offset_y;
}
storage.touch.finger_distance = new_finger_distance;
if (storage.canvas.offset_x !== old_offset_x || storage.canvas.offset_y !== old_offset_y || old_zoom !== storage.canvas.zoom) {
move_canvas();
}
storage.touch.screen_position.x = x;
storage.touch.screen_position.y = y;
break;
}
}
return;
}
}
async function on_touchend(e) {
for (const touch of e.changedTouches) {
if (storage.touch.drawing) {
if (storage.touch.ids[0] == touch.identifier) {
storage.touch.drawing = false;
if (storage.tools.active === 'pencil') {
const event = stroke_event();
storage.current_stroke = [];
await queue_event(event);
} else if (storage.tools.active === 'eraser') {
const events = eraser_events();
storage.erased = [];
if (events.length > 0) {
for (const event of events) {
await queue_event(event);
}
}
} else if (storage.tools.active === 'ruler') {
const event = ruler_event(storage.touch.position.x, storage.touch.position.y);
await queue_event(event);
} else {
console.error('fuck');
}
}
}
const index = storage.touch.ids.indexOf(touch.identifier);
if (index !== -1) {
storage.touch.ids.splice(index, 1);
}
if (storage.touch.moving && storage.touch.ids.length === 0) {
// Only allow drawing again when ALL fingers have been lifted
storage.touch.moving = false;
}
}
if (storage.touch.ids.length === 0) {
waiting_for_second_finger = false;
}
}

10
client/websocket.js

@ -1,4 +1,12 @@ @@ -1,4 +1,12 @@
function ws_connect() {
// Firefox does randomized exponential backoff for failed websocket requests
// This means we can't have [1. long downtime] and [2. fast reconnect] at the sime time
//
// We abuse the fact that HTTP requests are NOT backoffed, and use those to monitor
// the server. When we see that the server is up - we attempt an actual websocket connection
//
// Details best described here: https://github.com/kee-org/KeeFox/issues/189
function ws_connect(first_connect = false) {
const session_id = ls.getItem('sessionId') || '0';
const desk_id = storage.desk_id;

4
server/http.js

@ -5,8 +5,6 @@ import * as storage from './storage'; @@ -5,8 +5,6 @@ import * as storage from './storage';
export async function route(req) {
const url = new URL(req.url);
console.log(url.pathname);
if (url.pathname === '/api/image') {
const desk_id = url.searchParams.get('deskId') || '0';
const formData = await req.formData();
@ -18,5 +16,7 @@ export async function route(req) { @@ -18,5 +16,7 @@ export async function route(req) {
storage.put_image(image_id, desk_id);
return new Response(image_id);
} else if (url.pathname === '/api/ping') {
return new Response('pong');
}
}

16
server/send.js

@ -72,6 +72,10 @@ function create_session(ws, desk_id) { @@ -72,6 +72,10 @@ function create_session(ws, desk_id) {
}
export async function send_init(ws) {
if (!ws) {
return;
}
const session_id = ws.data.session_id;
const desk_id = ws.data.desk_id;
const desk = desks[desk_id];
@ -120,6 +124,10 @@ export async function send_init(ws) { @@ -120,6 +124,10 @@ export async function send_init(ws) {
}
export function send_ack(ws, lsn) {
if (!ws) {
return;
}
const size = 1 + 4; // opcode + lsn
const s = ser.create(size);
@ -132,6 +140,10 @@ export function send_ack(ws, lsn) { @@ -132,6 +140,10 @@ export function send_ack(ws, lsn) {
}
export function send_fire(ws, user_id, event) {
if (!ws) {
return;
}
const s = ser.create(1 + 4 + event_size(event));
ser.u8(s, MESSAGE.FIRE);
@ -149,6 +161,10 @@ async function sync_session(session_id) { @@ -149,6 +161,10 @@ async function sync_session(session_id) {
const session = sessions[session_id];
const desk = desks[session.desk_id];
if (!session.ws) {
return;
}
if (session.state !== SESSION.READY) {
return;
}

Loading…
Cancel
Save