<!DOCTYPE html> <html> <head> <style type="text/css"> * { touch-action: none; } body { background-color: red; } button { background-color: #D8D8D8; border: 1px solid #999999; border-radius: 5px; } button.selected { box-shadow: 0 0 2px 2px #0597f8; } #board { touch-action: none; box-shadow: 1px 1px 1px 0.5px rgba(0, 0, 0, 0.1); cursor: crosshair; position: absolute; left: 0px; top: 0px;; } #ruler { background-color: lightgreen; opacity: 0.75; width: 600px; height: 150px; position: absolute; z-index: 20; touch-action: none; } #ruler #rulerTopLeft { position: relative; top: 0px; left: 0px; width: 0px; height: 0px; background-color: blue; } /* ruler DOM object top/left */ #helper, #helper1, #helper2, #helper3, #helper4 { background-color: fuchsia; width: 10px; height: 10px; position: absolute; z-index: 21; touch-action: none; left: 200px; top: 200px; } #helper1, #helper2 { width: 150px; height: 150px; border-radius: 75px; opacity: 0.8; } /* first touch pointer */ #helper1 { background-color: yellow; } /* second touch pointer */ #helper2{ background-color: orange; } /* transform origin */ #helper3{ background-color: lime; border-radius: 5px; } /* ruler "top/left" */ #helper4{ background-color: red; border-radius: 5px; } </style> <script type="application/javascript" src="js/main.js"></script> <script type="application/javascript" src="js/Board.js"></script> <script type="application/javascript" src="js/Pen.js"></script> <script type="application/javascript" src="js/PointerManager.js"></script> <script type="application/javascript" src="js/Ruler.js"></script> <script type="application/javascript"> let redrawAllTimeout; function init() { Board.init("board"); Ruler.init(document.getElementById("ruler")); setColor("#000000"); setLineWidth("8"); // Attach event listener var pointerDown = function (e) { //console.log("down"); clearTimeout(redrawAllTimeout); PointerManager.registerPointer(e); if (e.pointerType == "pen") { // start new free hand stroke Board.strokeData.push([]); // check if we must follow the ruler collectStrokeInfo(e); } else if (e.pointerType == "touch") { } } var pointerMove = function (e) { if (e.pointerType == "pen") { collectStrokeInfo(e); } } var pointerCancel = function (e) { //console.log("cancel"); if (e.pointerType == "pen") { drawCurve(Board.ctx, Board.strokeData[Board.strokeData.length - 1]); clearTimeout(redrawAllTimeout); redrawAllTimeout = setTimeout(redrawAll, 1000); } PointerManager.unregisterPointer(e); } Board.canvas.addEventListener("pointerdown", pointerDown); Board.canvas.addEventListener("pointermove", pointerMove); Board.canvas.addEventListener("pointerup", pointerCancel); Board.canvas.addEventListener("pointerleave", pointerCancel); // set drawing call setInterval(_ => { if (Board.strokeData[Board.strokeData.length - 1] && Board.strokeData[Board.strokeData.length - 1].length) { drawCurve(Board.ctx, Board.strokeData[Board.strokeData.length - 1]) } }, 30); } function redrawAll() { Board.ctx.clearRect(0, 0, Board.canvas.width, Board.canvas.height); Board.ctx.fillStyle = Board.bgColor; Board.ctx.fillRect(0, 0, Board.canvas.width, Board.canvas.height); for (var stroke of Board.strokeData) { drawCurve(Board.ctx, stroke); } } // Draw method function collectStrokeInfo(e) { if (PointerManager.isRegistered(e.pointerId)) { var pos = PointerManager.getPointerPos(e); if (Ruler.inRulerSnappingArea(pos)) { console.log("SNAP to ruler"); let pointOnRulerPos = Ruler.getNearestPointOnRuler(pos); pointOnRulerPos.x *= Board.resolution; pointOnRulerPos.y *= Board.resolution; pos = pointOnRulerPos; } else { pos = Board.getPointerPos(e); } // save stroke info Board.strokeData[Board.strokeData.length - 1].push({ x: pos.x, y: pos.y, width: getLineWidth(e), pen: JSON.parse(JSON.stringify(Pen)) }); console.log(Board.strokeData[Board.strokeData.length - 1]); } } function drawCurve(ctx, ptsa, tension, isClosed, numOfSegments, showPoints) { ctx.beginPath(); drawLines(ctx, getCurvePoints(ptsa, tension, isClosed, numOfSegments)); if (showPoints) { ctx.beginPath(); for (var i = 0; i < ptsa.length - 1; i += 2) ctx.rect(ptsa[i] - 2, ptsa[i + 1] - 2, 4, 4); } ctx.stroke(); } function getCurvePoints(pts, tension, isClosed, numOfSegments) { // use input value if provided, or use a default value tension = (typeof tension != "undefined") ? tension : 0.5; isClosed = isClosed ? isClosed : false; numOfSegments = numOfSegments ? numOfSegments : 16; var _pts = [], res = [], // clone array x, y, // our x,y coords t1x, t2x, t1y, t2y, // tension vectors c1, c2, c3, c4, // cardinal points st, t, i; // steps based on num. of segments // clone array so we don"t change the original _pts = pts.slice(0); // The algorithm require a previous and next point to the actual point array. // Check if we will draw closed or open curve. // If closed, copy end points to beginning and first points to end // If open, duplicate first points to befinning, end points to end if (isClosed) { _pts.unshift(pts[pts.length - 1]); _pts.unshift(pts[pts.length - 1]); _pts.push(pts[0]); } else { _pts.unshift(pts[0]); //copy 1. point and insert at beginning _pts.push(pts[pts.length - 1]); //copy last point and append } // ok, lets start.. // 1. loop goes through point array // 2. loop goes through each segment between the 2 pts + 1e point before and after for (var i = 1; i < (_pts.length - 2); i++) { for (var t = 0; t <= numOfSegments; t++) { // calc tension vectors t1x = (_pts[i + 1].x - _pts[i - 1].x) * tension; t2x = (_pts[i + 2].x - _pts[i].x) * tension; t1y = (_pts[i + 1].y - _pts[i - 1].y) * tension; t2y = (_pts[i + 2].y - _pts[i].y) * tension; // calc step st = t / numOfSegments; // calc cardinals c1 = 2 * Math.pow(st, 3) - 3 * Math.pow(st, 2) + 1; c2 = -(2 * Math.pow(st, 3)) + 3 * Math.pow(st, 2); c3 = Math.pow(st, 3) - 2 * Math.pow(st, 2) + st; c4 = Math.pow(st, 3) - Math.pow(st, 2); // calc x and y cords with common control vectors x = c1 * _pts[i].x + c2 * _pts[i + 1].x + c3 * t1x + c4 * t2x; y = c1 * _pts[i].y + c2 * _pts[i + 1].y + c3 * t1y + c4 * t2y; //store points in array res.push({ x: x, y: y, width: lerp(_pts[i].width, _pts[i + 1].width, t / numOfSegments), pen: pts[i].pen} ); } } return res; } function lerp (start, end, pos){ return (1 - pos) * start + pos * end; } function drawLines(ctx, pts) { for (i = 0; i < pts.length - 2; i++) { ctx.beginPath(); ctx.lineWidth = pts[i].width; ctx.strokeStyle = pts[i].pen.colors.fg; ctx.moveTo(pts[i].x, pts[i].y); ctx.lineTo(pts[i + 1].x, pts[i + 1].y); ctx.stroke(); } } function getLineWidth(e) { return e.pressure * Pen.lineWidth; } function setColor(btn) { Pen.colors.fg = (btn.dataset) ? btn.dataset.color : btn; document.querySelectorAll("button[data-color]").forEach(b => b.classList.remove("selected")); document.querySelector("button[data-color='" + Pen.colors.fg + "']").classList.add("selected"); } function setLineWidth(btn) { Pen.lineWidth = (btn.dataset) ? btn.dataset.lineWidth : btn; document.querySelectorAll("button[data-line-width]").forEach(b => b.classList.remove("selected")); document.querySelector("button[data-line-width='" + Pen.lineWidth + "']").classList.add("selected"); } </script> </head> <body onload="init();"> <canvas touch-action="none" id="board" width="640" height="640" style="width:320px; height:320px;"> Opps, you cannot play draw N guess with this browser! </canvas> <button style="position:absolute; top:20px; left: 20px; width: 80px; height: 80px;" onclick="Board.save();">save</button> <button style="position:absolute; top:20px; left: 120px; width: 80px; height: 80px;" onclick="Board.clear();">clear</button> <button data-color="#000000" style="position:absolute; top:20px; left: 220px; width: 35px; height: 35px; background-color: black; color: white" onclick="setColor(this);"> </button> <button data-color="#FF0000" style="position:absolute; top:20px; left: 265px; width: 35px; height: 35px; background-color: red;" onclick="setColor(this);"> </button> <button data-color="#4169E1" style="position:absolute; top:65px; left: 220px; width: 35px; height: 35px; background-color: royalblue;" onclick="setColor(this);"> </button> <button data-color="#32CD32" style="position:absolute; top:65px; left: 265px; width: 35px; height: 35px; background-color: limegreen;" onclick="setColor(this);"> </button> <button data-line-width="4" style="position:absolute; top:20px; left: 320px; width: 35px; height: 35px;" onclick="setLineWidth(this);">4</button> <button data-line-width="8" style="position:absolute; top:20px; left: 365px; width: 35px; height: 35px;" onclick="setLineWidth(this);">8</button> <button data-line-width="16" style="position:absolute; top:65px; left: 320px; width: 35px; height: 35px;" onclick="setLineWidth(this);">16</button> <button data-line-width="24" style="position:absolute; top:65px; left: 365px; width: 35px; height: 35px;" onclick="setLineWidth(this);">24</button> <div id="ruler"><div id="rulerTopLeft">top left</div><div id="rulerValues"></div></div> <div id="helper"></div> <div id="helper1"></div> <div id="helper2"></div> <div id="helper3"></div> <div id="helper4"></div> </body> </html>