From 43d038874cbe7727cb4fec63ca84b715bfdd5fff Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sat, 24 Aug 2024 22:59:22 -0500 Subject: [PATCH 01/12] update logic for close polygon on 'Enter' --- script.js | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/script.js b/script.js index e314e34..27353aa 100644 --- a/script.js +++ b/script.js @@ -425,7 +425,6 @@ document.addEventListener('keydown', function(e) { }) function draw () { - drawAllPolygons() drawCurrentPolygon() var parentPoints = getParentPoints() @@ -489,28 +488,6 @@ document.querySelector('#save-image').addEventListener('click', function(e) { saveImage() }) -function completeCurrentPolygon () { - canvas.style.cursor = 'default' - - // save current polygon points - masterPoints.push(points) - points = [] - - // dont choose a color that has already been chosen - var remaining_choices = color_choices.filter(function(x) { - return !masterColors.includes(x) - }); - - if (remaining_choices.length == 0) { - remaining_choices = color_choices - } - - rgb_color = remaining_choices[Math.floor(Math.random() * remaining_choices.length)] - masterColors.push(rgb_color) - - draw() -} - window.addEventListener('keydown', function(e) { if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { e.preventDefault() @@ -539,6 +516,8 @@ window.addEventListener('keydown', function(e) { } if (e.key === 'Enter') { - completeCurrentPolygon() + if(points.length > 2) { + closePath() + } } }) \ No newline at end of file From e8bcdcd603ebf8158a48698743fe55961475c365 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sat, 24 Aug 2024 23:55:28 -0500 Subject: [PATCH 02/12] fixed 'undo' logic--had incorrect refresh and coloring --- script.js | 49 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 36 insertions(+), 13 deletions(-) diff --git a/script.js b/script.js index 27353aa..51c9f4c 100644 --- a/script.js +++ b/script.js @@ -41,6 +41,11 @@ function clipboard(selector) { navigator.clipboard.writeText(copyText); } +function clearDrawings() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0); +} + function zoom(clicks) { // if w > 60em, stop if ((scaleFactor + clicks * scaleSpeed) * img.width > 40 * 16) { @@ -58,9 +63,7 @@ function closePath() { canvas.style.cursor = 'default'; masterPoints.push(points); masterColors.push(rgb_color); - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); - + clearDrawings(); drawAllPolygons(); points = []; @@ -203,8 +206,7 @@ canvas.addEventListener('mousemove', function(e) { ycoord.innerHTML = y; if (canvas.style.cursor == 'crosshair') { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); + clearDrawings(); drawAllPolygons(); @@ -426,7 +428,6 @@ document.addEventListener('keydown', function(e) { function draw () { drawAllPolygons() - drawCurrentPolygon() var parentPoints = getParentPoints() writePoints(parentPoints) } @@ -439,8 +440,33 @@ function highlightButtonInteraction (buttonId) { function undo () { highlightButtonInteraction('#undo') - points.pop() - draw() + if (points.length > 0) { + points.pop() + + clearDrawings() + draw() + ctx.strokeStyle = rgb_color; + var i = 0; + for (; i < points.length - 1; i++) { + drawLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); + ctx.stroke(); + // draw arc around each point + ctx.beginPath(); + ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); + // fill with white + ctx.fillStyle = 'white'; + ctx.fill(); + ctx.stroke(); + } + + // draw arc around point n + ctx.beginPath(); + ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); + // fill with white + ctx.fillStyle = 'white'; + ctx.fill(); + ctx.stroke(); + } } document.querySelector('#undo').addEventListener('click', function(e) { @@ -451,6 +477,7 @@ function discardCurrentPolygon () { highlightButtonInteraction('#discard-current') points = [] + clearDrawings() draw() } @@ -460,10 +487,7 @@ document.querySelector('#discard-current').addEventListener('click', function(e) function clearAll() { highlightButtonInteraction('#clear') - - ctx.clearRect(0, 0, canvas.width, canvas.height) - ctx.drawImage(img, 0, 0) - + clearDrawings() points = [] masterPoints = [] masterColors = [] @@ -492,7 +516,6 @@ window.addEventListener('keydown', function(e) { if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { e.preventDefault() e.stopImmediatePropagation() - undo() } From 8686b6d9fb574bf589d02c62c26a57476f580a7b Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sun, 25 Aug 2024 00:41:23 -0500 Subject: [PATCH 03/12] fix bug where coordinate values were sometimes negative, only show coord values while cursor in canvas, and properly pop final nodes in 'undo' --- script.js | 33 ++++++++++++++++++++++++++------- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/script.js b/script.js index 51c9f4c..9cf09da 100644 --- a/script.js +++ b/script.js @@ -177,6 +177,13 @@ canvas.addEventListener('wheel', function(e) { zoom(delta); }); +canvas.addEventListener('mouseleave', function(e) { + var xcoord = document.querySelector('#x'); + var ycoord = document.querySelector('#y'); + xcoord.innerHTML = ''; + ycoord.innerHTML = ''; +}); + // on canvas hover, if cursor is crosshair, draw line from last point to cursor canvas.addEventListener('mousemove', function(e) { var x = getScaledCoords(e)[0]; @@ -202,6 +209,13 @@ canvas.addEventListener('mousemove', function(e) { y = Math.round(new_y); } + // sometimes, a mousemove event is leaving the canvas and has coordinates outside the canvas + // however, due to the cursors being used, we do not need to check if it is larger than canvas.width or canvas.height + if (x < 0 || y < 0 ){ + xcoord.innerHTML = ''; + ycoord.innerHTML = ''; + return; + } xcoord.innerHTML = x; ycoord.innerHTML = y; @@ -441,10 +455,15 @@ function undo () { highlightButtonInteraction('#undo') if (points.length > 0) { + points.pop() clearDrawings() draw() + + if(points.length === 0){ + return; + } ctx.strokeStyle = rgb_color; var i = 0; for (; i < points.length - 1; i++) { @@ -459,13 +478,13 @@ function undo () { ctx.stroke(); } - // draw arc around point n - ctx.beginPath(); - ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); - // fill with white - ctx.fillStyle = 'white'; - ctx.fill(); - ctx.stroke(); + // draw arc around point n + ctx.beginPath(); + ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); + // fill with white + ctx.fillStyle = 'white'; + ctx.fill(); + ctx.stroke(); } } From 90323cde94411449e02e4f3d4e88f6b990129ebd Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sun, 25 Aug 2024 02:06:10 -0500 Subject: [PATCH 04/12] render the fills before the line strokes and nodes, this helps prevent over-darkening on overlapping regions --- script.js | 79 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 50 insertions(+), 29 deletions(-) diff --git a/script.js b/script.js index 9cf09da..5bba104 100644 --- a/script.js +++ b/script.js @@ -18,7 +18,7 @@ var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); var img = new Image(); var rgb_color = color_choices[Math.floor(Math.random() * color_choices.length)] -var opaque_color = 'rgba(0,0,0,0.5)'; +var fill_color = 'rgba(0,0,0,0.35)'; var scaleFactor = 1; var scaleSpeed = 0.01; @@ -46,6 +46,16 @@ function clearDrawings() { ctx.drawImage(img, 0, 0); } +function isClockwise(vertices) { + let sum = 0; + for (let i = 0; i < vertices.length; i++) { + const [x1, y1] = vertices[i]; + const [x2, y2] = vertices[(i + 1) % vertices.length]; + sum += (x2 - x1) * (y2 + y1); + } + return sum > 0; +} + function zoom(clicks) { // if w > 60em, stop if ((scaleFactor + clicks * scaleSpeed) * img.width > 40 * 16) { @@ -61,11 +71,15 @@ function zoom(clicks) { function closePath() { canvas.style.cursor = 'default'; + // we do this to avoid clearing overlapping polygons + if (isClockwise(points)) { + points = points.reverse(); + } masterPoints.push(points); + points = []; masterColors.push(rgb_color); clearDrawings(); drawAllPolygons(); - points = []; // dont choose a color that has already been chosen var remaining_choices = color_choices.filter(function(x) { @@ -91,12 +105,8 @@ img.onload = function() { }; function drawLine(x1, y1, x2, y2) { - ctx.beginPath(); - // set widht - ctx.lineWidth = 5; ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); - // ctx.stroke(); } function getScaledCoords(e) { @@ -107,38 +117,48 @@ function getScaledCoords(e) { } function drawAllPolygons () { - // draw all points for previous regions + // draw polygons as subpaths and fill all at once + // we do this to avoid overlapping polygons becoming opaque + ctx.beginPath(); + ctx.fillStyle = fill_color; for (var i = 0; i < masterPoints.length; i++) { var newpoints = masterPoints[i]; - // set color - ctx.strokeStyle = masterColors[i]; for (var j = 1; j < newpoints.length; j++) { - // draw all lines drawLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); - - // fill - ctx.beginPath(); - ctx.fillStyle = opaque_color; ctx.moveTo(newpoints[0][0], newpoints[0][1]); for (var j = 1; j < newpoints.length; j++) { ctx.lineTo(newpoints[j][0], newpoints[j][1]); } - ctx.closePath(); - ctx.fill(); - ctx.stroke(); + drawLine(newpoints[newpoints.length - 1][0], newpoints[newpoints.length - 1][1], newpoints[0][0], newpoints[0][1]); } - drawLine(newpoints[newpoints.length - 1][0], newpoints[newpoints.length - 1][1], newpoints[0][0], newpoints[0][1]); + } + ctx.fill(); + + ctx.lineWidth = 5; + for (var i = 0; i < masterPoints.length; i++) { + var newpoints = masterPoints[i]; + ctx.strokeStyle = masterColors[i]; + + ctx.beginPath(); + for (var j = 1; j < newpoints.length; j++) { + drawLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); + ctx.moveTo(newpoints[0][0], newpoints[0][1]); + for (var j = 1; j < newpoints.length; j++) { + ctx.lineTo(newpoints[j][0], newpoints[j][1]); + } + } + ctx.closePath(); + ctx.stroke(); + // draw arc around each point for (var j = 0; j < newpoints.length; j++) { ctx.beginPath(); - ctx.strokeStyle = masterColors[i]; ctx.arc(newpoints[j][0], newpoints[j][1], 5, 0, 2 * Math.PI); - // fill with white + ctx.closePath(); ctx.fillStyle = 'white'; ctx.fill(); ctx.stroke(); } - } } @@ -221,31 +241,36 @@ canvas.addEventListener('mousemove', function(e) { if (canvas.style.cursor == 'crosshair') { clearDrawings(); - drawAllPolygons(); for (var i = 0; i < points.length - 1; i++) { ctx.strokeStyle = rgb_color; + ctx.beginPath(); drawLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); + ctx.closePath(); ctx.stroke(); // draw arc around each point ctx.beginPath(); ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); // fill with white ctx.fillStyle = 'white'; + ctx.closePath(); ctx.fill(); ctx.stroke(); } if ((points.length > 0 && drawMode == "polygon") || (points.length > 0 && points.length < 2 && drawMode == "line")) { + ctx.beginPath(); ctx.strokeStyle = rgb_color; drawLine(points[points.length - 1][0], points[points.length - 1][1], x, y); - ctx.stroke(); // new + ctx.closePath(); + + ctx.stroke(); ctx.beginPath(); ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); - // fill with white ctx.fillStyle = 'white'; + ctx.closePath(); ctx.fill(); ctx.stroke(); } @@ -382,7 +407,7 @@ canvas.addEventListener('click', function(e) { ctx.beginPath(); ctx.strokeStyle = rgb_color; ctx.arc(x, y, 5, 0, 2 * Math.PI); - // fill with white + ctx.closePath(); ctx.fillStyle = 'white'; ctx.fill(); ctx.stroke(); @@ -390,10 +415,6 @@ canvas.addEventListener('click', function(e) { if(drawMode == "line" && points.length == 2) { closePath(); } - else { - ctx.beginPath(); - ctx.strokeStyle = rgb_color; - } // ctx.arc(x, y, 155, 0, 2 * Math.PI); // concat all points into one array From d2cf1fd35bcf577af6226d742041b948c8acce55 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Fri, 23 Aug 2024 00:46:18 -0500 Subject: [PATCH 05/12] bevel instead of miter to reduce sharp joints for small angles --- script.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/script.js b/script.js index 5bba104..bab06e7 100644 --- a/script.js +++ b/script.js @@ -16,6 +16,8 @@ var radiansPer45Degrees = Math.PI / 4; var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); +ctx.lineJoin = 'bevel'; + var img = new Image(); var rgb_color = color_choices[Math.floor(Math.random() * color_choices.length)] var fill_color = 'rgba(0,0,0,0.35)'; From 7e413c9e6f1d8c56c063567f9d9fd3ea9e8332a6 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sun, 25 Aug 2024 12:19:16 -0500 Subject: [PATCH 06/12] set bevel locally --- script.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/script.js b/script.js index bab06e7..0d12fb9 100644 --- a/script.js +++ b/script.js @@ -16,7 +16,6 @@ var radiansPer45Degrees = Math.PI / 4; var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); -ctx.lineJoin = 'bevel'; var img = new Image(); var rgb_color = color_choices[Math.floor(Math.random() * color_choices.length)] @@ -137,6 +136,7 @@ function drawAllPolygons () { ctx.fill(); ctx.lineWidth = 5; + ctx.lineJoin = 'bevel'; for (var i = 0; i < masterPoints.length; i++) { var newpoints = masterPoints[i]; ctx.strokeStyle = masterColors[i]; @@ -248,6 +248,7 @@ canvas.addEventListener('mousemove', function(e) { for (var i = 0; i < points.length - 1; i++) { ctx.strokeStyle = rgb_color; ctx.beginPath(); + ctx.lineJoin = 'bevel'; drawLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); ctx.closePath(); ctx.stroke(); @@ -264,6 +265,7 @@ canvas.addEventListener('mousemove', function(e) { if ((points.length > 0 && drawMode == "polygon") || (points.length > 0 && points.length < 2 && drawMode == "line")) { ctx.beginPath(); + ctx.lineJoin = 'bevel'; ctx.strokeStyle = rgb_color; drawLine(points[points.length - 1][0], points[points.length - 1][1], x, y); ctx.closePath(); From a4afd56b427f2709958eea4370c3bea17152c92e Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sun, 25 Aug 2024 12:33:17 -0500 Subject: [PATCH 07/12] minor edit: clean up --- script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script.js b/script.js index 0d12fb9..4f07e2f 100644 --- a/script.js +++ b/script.js @@ -35,7 +35,7 @@ var constrainAngles = false; var showNormalized = false; var modeMessage = document.querySelector('#mode'); -var coords = document.querySelector('#coords'); +// var coords = document.querySelector('#coords'); function clipboard(selector) { var copyText = document.querySelector(selector).innerText; @@ -313,7 +313,7 @@ canvas.addEventListener('drop', function(e) { ctx.drawImage(img, 0, 0); }; // show coords - document.getElementById('coords').style.display = 'inline-block'; + // document.getElementById('coords').style.display = 'inline-block'; }); function writePoints(parentPoints) { From e8b6822aa8c741cacff62e19e2aeff257d2e8936 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sun, 25 Aug 2024 12:34:57 -0500 Subject: [PATCH 08/12] minor edit: allow user to press 'l' or 'L' to enter line mode, same with polygon mode --- script.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script.js b/script.js index 4f07e2f..a958a41 100644 --- a/script.js +++ b/script.js @@ -457,10 +457,10 @@ document.querySelector('#mode-line').addEventListener('click', function(e) { }) document.addEventListener('keydown', function(e) { - if (e.key == 'l') { + if (e.key == 'l' || e.key == 'L') { setDrawMode('line') } - if (e.key == 'p') { + if (e.key == 'p' || e.key == 'P') { setDrawMode('polygon') } }) From 526881d227fec5062ee991eeee554e90e6f0a07f Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Mon, 26 Aug 2024 14:18:30 -0500 Subject: [PATCH 09/12] some refactoring and cleanup --- script.js | 216 ++++++++++++++++++++++-------------------------------- 1 file changed, 88 insertions(+), 128 deletions(-) diff --git a/script.js b/script.js index a958a41..96f112a 100644 --- a/script.js +++ b/script.js @@ -29,8 +29,8 @@ var regions = []; var masterPoints = []; var masterColors = []; -var drawMode -setDrawMode('polygon') +var drawMode; +setDrawMode('polygon'); var constrainAngles = false; var showNormalized = false; @@ -70,7 +70,7 @@ function zoom(clicks) { canvas.style.height = h + 'px'; } -function closePath() { +function onPathClose() { canvas.style.cursor = 'default'; // we do this to avoid clearing overlapping polygons if (isClockwise(points)) { @@ -105,11 +105,23 @@ img.onload = function() { ctx.drawImage(img, 0, 0); }; -function drawLine(x1, y1, x2, y2) { +function makeLine(x1, y1, x2, y2) { ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); } +function drawNode(x, y, stroke = null) { + if (stroke) { + ctx.strokeStyle = stroke; + } + ctx.beginPath(); + ctx.arc(x, y, 5, 0, 2 * Math.PI); + ctx.closePath(); + ctx.fillStyle = 'white'; + ctx.fill(); + ctx.stroke(); +} + function getScaledCoords(e) { var rect = canvas.getBoundingClientRect(); var x = e.clientX - rect.left; @@ -125,12 +137,12 @@ function drawAllPolygons () { for (var i = 0; i < masterPoints.length; i++) { var newpoints = masterPoints[i]; for (var j = 1; j < newpoints.length; j++) { - drawLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); + makeLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); ctx.moveTo(newpoints[0][0], newpoints[0][1]); for (var j = 1; j < newpoints.length; j++) { ctx.lineTo(newpoints[j][0], newpoints[j][1]); } - drawLine(newpoints[newpoints.length - 1][0], newpoints[newpoints.length - 1][1], newpoints[0][0], newpoints[0][1]); + makeLine(newpoints[newpoints.length - 1][0], newpoints[newpoints.length - 1][1], newpoints[0][0], newpoints[0][1]); } } ctx.fill(); @@ -143,7 +155,7 @@ function drawAllPolygons () { ctx.beginPath(); for (var j = 1; j < newpoints.length; j++) { - drawLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); + makeLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); ctx.moveTo(newpoints[0][0], newpoints[0][1]); for (var j = 1; j < newpoints.length; j++) { ctx.lineTo(newpoints[j][0], newpoints[j][1]); @@ -154,12 +166,7 @@ function drawAllPolygons () { // draw arc around each point for (var j = 0; j < newpoints.length; j++) { - ctx.beginPath(); - ctx.arc(newpoints[j][0], newpoints[j][1], 5, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fillStyle = 'white'; - ctx.fill(); - ctx.stroke(); + drawNode(newpoints[j][0], newpoints[j][1]); } } } @@ -249,17 +256,11 @@ canvas.addEventListener('mousemove', function(e) { ctx.strokeStyle = rgb_color; ctx.beginPath(); ctx.lineJoin = 'bevel'; - drawLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); + makeLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); ctx.closePath(); ctx.stroke(); - // draw arc around each point - ctx.beginPath(); - ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); - // fill with white - ctx.fillStyle = 'white'; - ctx.closePath(); - ctx.fill(); - ctx.stroke(); + + drawNode(points[i][0], points[i][1]); } @@ -267,16 +268,11 @@ canvas.addEventListener('mousemove', function(e) { ctx.beginPath(); ctx.lineJoin = 'bevel'; ctx.strokeStyle = rgb_color; - drawLine(points[points.length - 1][0], points[points.length - 1][1], x, y); - ctx.closePath(); - - ctx.stroke(); - ctx.beginPath(); - ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); - ctx.fillStyle = 'white'; + makeLine(points[points.length - 1][0], points[points.length - 1][1], x, y); ctx.closePath(); - ctx.fill(); ctx.stroke(); + + drawNode(points[i][0], points[i][1]); } } }); @@ -346,36 +342,22 @@ function writePoints(parentPoints) { } // create np.array list - var code_template = ` -[ -${parentPoints.map(function(points) { -return `np.array([ -${points.map(function(point) { - return `[${point[0]}, ${point[1]}]`; -}).join(',')} -])`; -}).join(',')} -] - `; + var code_template = `[\n${parentPoints.map(function(points) { + return ` np.array([${points.map(function(point) { + return `[${point[0]}, ${point[1]}]`;}).join(', ')}])`; + }).join(',\n')}\n]`; document.querySelector('#python').innerHTML = code_template; - var json_template = ` -{ -${parentPoints.map(function(points) { -return `[ -${points.map(function(point) { -return `{"x": ${point[0]}, "y": ${point[1]}}`; -}).join(',')} -]`; -}).join(',')} -} - `; + var json_template = `{\n${parentPoints.map(function(points) { + return ` [${points.map(function(point) { + return `{"x": ${point[0]}, "y": ${point[1]}}`;}).join(', ')}]`; + }).join(',\n')}\n}`; + document.querySelector('#json').innerHTML = json_template; } canvas.addEventListener('click', function(e) { - // set cursor to crosshair canvas.style.cursor = 'crosshair'; var x = getScaledCoords(e)[0]; @@ -401,26 +383,19 @@ canvas.addEventListener('click', function(e) { distY = y - points[0][1]; // stroke is 3px and centered on the circle (i.e. 1/2 * 3px) and arc radius is if(Math.sqrt(distX * distX + distY * distY) <= 6.5) { - closePath(); + onPathClose(); return; } } points.push([x, y]); - ctx.beginPath(); - ctx.strokeStyle = rgb_color; - ctx.arc(x, y, 5, 0, 2 * Math.PI); - ctx.closePath(); - ctx.fillStyle = 'white'; - ctx.fill(); - ctx.stroke(); + drawNode(x, y, rgb_color); if(drawMode == "line" && points.length == 2) { - closePath(); + onPathClose(); } - - // ctx.arc(x, y, 155, 0, 2 * Math.PI); + // concat all points into one array var parentPoints = []; @@ -442,125 +417,110 @@ document.querySelector('#normalize-checkbox').addEventListener('change', functio }); function setDrawMode(mode) { - drawMode = mode + drawMode = mode; canvas.style.cursor = 'crosshair'; - document.querySelectorAll('.t-mode').forEach(el => el.classList.remove('active')) - document.querySelector(`#mode-${mode}`).classList.add('active') + document.querySelectorAll('.t-mode').forEach(el => el.classList.remove('active')); + document.querySelector(`#mode-${mode}`).classList.add('active'); } document.querySelector('#mode-polygon').addEventListener('click', function(e) { - setDrawMode('polygon') + setDrawMode('polygon'); }) document.querySelector('#mode-line').addEventListener('click', function(e) { - setDrawMode('line') + setDrawMode('line'); }) document.addEventListener('keydown', function(e) { if (e.key == 'l' || e.key == 'L') { - setDrawMode('line') + setDrawMode('line'); } if (e.key == 'p' || e.key == 'P') { - setDrawMode('polygon') + setDrawMode('polygon'); } }) -function draw () { - drawAllPolygons() - var parentPoints = getParentPoints() - writePoints(parentPoints) +function update() { + drawAllPolygons(); + var parentPoints = getParentPoints(); + writePoints(parentPoints); } function highlightButtonInteraction (buttonId) { - document.querySelector(buttonId).classList.add('active') - setTimeout(() => document.querySelector(buttonId).classList.remove('active'), 100) + document.querySelector(buttonId).classList.add('active'); + setTimeout(() => document.querySelector(buttonId).classList.remove('active'), 100); } function undo () { - highlightButtonInteraction('#undo') + highlightButtonInteraction('#undo'); if (points.length > 0) { - - points.pop() - - clearDrawings() - draw() + points.pop(); + clearDrawings(); + update(); if(points.length === 0){ return; } + ctx.strokeStyle = rgb_color; var i = 0; for (; i < points.length - 1; i++) { - drawLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); - ctx.stroke(); - // draw arc around each point - ctx.beginPath(); - ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); - // fill with white - ctx.fillStyle = 'white'; - ctx.fill(); + makeLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); ctx.stroke(); + drawNode(points[i][0], points[i][1]); } - - // draw arc around point n - ctx.beginPath(); - ctx.arc(points[i][0], points[i][1], 5, 0, 2 * Math.PI); - // fill with white - ctx.fillStyle = 'white'; - ctx.fill(); - ctx.stroke(); + drawNode(points[i][0], points[i][1]); } } document.querySelector('#undo').addEventListener('click', function(e) { - undo() + undo(); }) function discardCurrentPolygon () { - highlightButtonInteraction('#discard-current') - - points = [] - clearDrawings() - draw() + highlightButtonInteraction('#discard-current'); + points = []; + clearDrawings(); + update(); } document.querySelector('#discard-current').addEventListener('click', function(e) { - discardCurrentPolygon() + discardCurrentPolygon(); }) function clearAll() { highlightButtonInteraction('#clear') - clearDrawings() - points = [] - masterPoints = [] - masterColors = [] - document.querySelector('#json').innerHTML = '' - document.querySelector('#python').innerHTML = '' + clearDrawings(); + points = []; + masterPoints = []; + masterColors = []; + document.querySelector('#json').innerHTML = ''; + document.querySelector('#python').innerHTML = ''; } document.querySelector('#clear').addEventListener('click', function(e) { - clearAll() + clearAll(); }) function saveImage () { - highlightButtonInteraction('#save-image') + highlightButtonInteraction('#save-image'); - var link = document.createElement('a') - link.download = 'image.png' - link.href = canvas.toDataURL() - link.click() + var link = document.createElement('a'); + link.download = 'image.png'; + link.href = canvas.toDataURL(); + link.click(); } document.querySelector('#save-image').addEventListener('click', function(e) { - saveImage() + saveImage(); }) window.addEventListener('keydown', function(e) { if (e.key === 'z' && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - e.stopImmediatePropagation() - undo() + e.preventDefault(); + e.stopImmediatePropagation(); + undo(); } if (e.key === 'Shift') { @@ -568,23 +528,23 @@ window.addEventListener('keydown', function(e) { } if (e.key === 'Escape') { - discardCurrentPolygon() + discardCurrentPolygon(); } if (e.key === 'e' && (e.ctrlKey || e.metaKey)) { - clearAll() + clearAll(); } if (e.key === 's' && (e.ctrlKey || e.metaKey)) { - e.preventDefault() - e.stopImmediatePropagation() + e.preventDefault(); + e.stopImmediatePropagation(); - saveImage() + saveImage(); } if (e.key === 'Enter') { if(points.length > 2) { - closePath() + onPathClose(); } } }) \ No newline at end of file From f690ad7663795fbd417007513debb216db6886b4 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Mon, 26 Aug 2024 14:50:44 -0500 Subject: [PATCH 10/12] cycle through colors periodically instead of randomly --- script.js | 14 +++----------- 1 file changed, 3 insertions(+), 11 deletions(-) diff --git a/script.js b/script.js index 96f112a..b6e958a 100644 --- a/script.js +++ b/script.js @@ -11,6 +11,7 @@ var color_choices = [ "#0000FF", "#CCCCCC", ]; +// if you want choices to be kind of random, shuffle the array once here var radiansPer45Degrees = Math.PI / 4; @@ -18,7 +19,7 @@ var canvas = document.getElementById('canvas'); var ctx = canvas.getContext('2d'); var img = new Image(); -var rgb_color = color_choices[Math.floor(Math.random() * color_choices.length)] +var rgb_color = "#FF00FF"; var fill_color = 'rgba(0,0,0,0.35)'; var scaleFactor = 1; @@ -81,17 +82,8 @@ function onPathClose() { masterColors.push(rgb_color); clearDrawings(); drawAllPolygons(); - - // dont choose a color that has already been chosen - var remaining_choices = color_choices.filter(function(x) { - return !masterColors.includes(x); - }); - if (remaining_choices.length == 0) { - remaining_choices = color_choices; - } - - rgb_color = remaining_choices[Math.floor(Math.random() * remaining_choices.length)]; + rgb_color = color_choices[(masterColors.length) % (color_choices.length)]; } // placeholder image From 3216f236aa870e07285057cc3a41fdb2fe34d630 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sat, 31 Aug 2024 15:37:25 -0500 Subject: [PATCH 11/12] reset color when on clear --- script.js | 1 + 1 file changed, 1 insertion(+) diff --git a/script.js b/script.js index b6e958a..3854f5d 100644 --- a/script.js +++ b/script.js @@ -487,6 +487,7 @@ function clearAll() { points = []; masterPoints = []; masterColors = []; + rgb_color = color_choices[0]; document.querySelector('#json').innerHTML = ''; document.querySelector('#python').innerHTML = ''; } From 1e34ec20124feaa365068cc7ffed9401667ed7b4 Mon Sep 17 00:00:00 2001 From: Michael Womick Date: Sat, 31 Aug 2024 17:43:24 -0500 Subject: [PATCH 12/12] pre-rendering optimizations --- script.js | 102 ++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 65 insertions(+), 37 deletions(-) diff --git a/script.js b/script.js index 3854f5d..dff0235 100644 --- a/script.js +++ b/script.js @@ -16,7 +16,11 @@ var color_choices = [ var radiansPer45Degrees = Math.PI / 4; var canvas = document.getElementById('canvas'); -var ctx = canvas.getContext('2d'); +var mainCtx = canvas.getContext('2d'); +var offScreenCanvas = document.createElement('canvas'); +var offScreenCtx = offScreenCanvas.getContext('2d'); +offScreenCanvas.width = canvas.width; +offScreenCanvas.height = canvas.height; var img = new Image(); var rgb_color = "#FF00FF"; @@ -38,14 +42,19 @@ var showNormalized = false; var modeMessage = document.querySelector('#mode'); // var coords = document.querySelector('#coords'); +function blitCachedCanvas() { + mainCtx.clearRect(0, 0, canvas.width, canvas.height); + mainCtx.drawImage(offScreenCanvas, 0, 0); +} + function clipboard(selector) { var copyText = document.querySelector(selector).innerText; navigator.clipboard.writeText(copyText); } function clearDrawings() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - ctx.drawImage(img, 0, 0); + offScreenCtx.clearRect(0, 0, offScreenCanvas.width, offScreenCanvas.height); + blitCachedCanvas(); } function isClockwise(vertices) { @@ -80,9 +89,8 @@ function onPathClose() { masterPoints.push(points); points = []; masterColors.push(rgb_color); - clearDrawings(); - drawAllPolygons(); - + drawAllPolygons(offScreenCtx); + blitCachedCanvas(); rgb_color = color_choices[(masterColors.length) % (color_choices.length)]; } @@ -94,15 +102,18 @@ img.onload = function() { canvas.style.height = img.height * scaleFactor + 'px'; canvas.width = img.width; canvas.height = img.height; - ctx.drawImage(img, 0, 0); + offScreenCanvas.width = img.width; + offScreenCanvas.height = img.height; + offScreenCtx.drawImage(img, 0, 0); + blitCachedCanvas(); }; -function makeLine(x1, y1, x2, y2) { +function makeLine(ctx, x1, y1, x2, y2) { ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); } -function drawNode(x, y, stroke = null) { +function drawNode(ctx, x, y, stroke = null) { if (stroke) { ctx.strokeStyle = stroke; } @@ -121,7 +132,9 @@ function getScaledCoords(e) { return [x / scaleFactor, y / scaleFactor]; } -function drawAllPolygons () { +function drawAllPolygons(ctx) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(img, 0, 0); // draw polygons as subpaths and fill all at once // we do this to avoid overlapping polygons becoming opaque ctx.beginPath(); @@ -129,12 +142,12 @@ function drawAllPolygons () { for (var i = 0; i < masterPoints.length; i++) { var newpoints = masterPoints[i]; for (var j = 1; j < newpoints.length; j++) { - makeLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); + makeLine(ctx, newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); ctx.moveTo(newpoints[0][0], newpoints[0][1]); for (var j = 1; j < newpoints.length; j++) { ctx.lineTo(newpoints[j][0], newpoints[j][1]); } - makeLine(newpoints[newpoints.length - 1][0], newpoints[newpoints.length - 1][1], newpoints[0][0], newpoints[0][1]); + makeLine(ctx, newpoints[newpoints.length - 1][0], newpoints[newpoints.length - 1][1], newpoints[0][0], newpoints[0][1]); } } ctx.fill(); @@ -147,7 +160,7 @@ function drawAllPolygons () { ctx.beginPath(); for (var j = 1; j < newpoints.length; j++) { - makeLine(newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); + makeLine(ctx, newpoints[j - 1][0], newpoints[j - 1][1], newpoints[j][0], newpoints[j][1]); ctx.moveTo(newpoints[0][0], newpoints[0][1]); for (var j = 1; j < newpoints.length; j++) { ctx.lineTo(newpoints[j][0], newpoints[j][1]); @@ -158,12 +171,12 @@ function drawAllPolygons () { // draw arc around each point for (var j = 0; j < newpoints.length; j++) { - drawNode(newpoints[j][0], newpoints[j][1]); + drawNode(ctx, newpoints[j][0], newpoints[j][1]); } } } -function getParentPoints () { +function getParentPoints() { var parentPoints = []; for (var i = 0; i < masterPoints.length; i++) { parentPoints.push(masterPoints[i]); @@ -240,19 +253,23 @@ canvas.addEventListener('mousemove', function(e) { xcoord.innerHTML = x; ycoord.innerHTML = y; + var ctx = mainCtx; + ctx.lineWidth = 5; + ctx.lineJoin = 'bevel'; + ctx.fillStyle = 'white'; + if (canvas.style.cursor == 'crosshair') { - clearDrawings(); - drawAllPolygons(); + blitCachedCanvas(); for (var i = 0; i < points.length - 1; i++) { ctx.strokeStyle = rgb_color; ctx.beginPath(); ctx.lineJoin = 'bevel'; - makeLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); + makeLine(ctx, points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); ctx.closePath(); ctx.stroke(); - drawNode(points[i][0], points[i][1]); + drawNode(ctx, points[i][0], points[i][1]); } @@ -260,11 +277,11 @@ canvas.addEventListener('mousemove', function(e) { ctx.beginPath(); ctx.lineJoin = 'bevel'; ctx.strokeStyle = rgb_color; - makeLine(points[points.length - 1][0], points[points.length - 1][1], x, y); + makeLine(ctx, points[points.length - 1][0], points[points.length - 1][1], x, y); ctx.closePath(); ctx.stroke(); - drawNode(points[i][0], points[i][1]); + drawNode(ctx, points[i][0], points[i][1]); } } }); @@ -297,8 +314,11 @@ canvas.addEventListener('drop', function(e) { canvas.style.height = img.height * scaleFactor + 'px'; canvas.width = img.width; canvas.height = img.height; + offScreenCanvas.width = img.width; + offScreenCanvas.height = img.height; canvas.style.borderRadius = '10px'; - ctx.drawImage(img, 0, 0); + offScreenCtx.drawImage(img, 0, 0); + blitCachedCanvas(); }; // show coords // document.getElementById('coords').style.display = 'inline-block'; @@ -382,7 +402,7 @@ canvas.addEventListener('click', function(e) { points.push([x, y]); - drawNode(x, y, rgb_color); + drawNode(mainCtx, x, y, rgb_color); if(drawMode == "line" && points.length == 2) { onPathClose(); @@ -432,8 +452,7 @@ document.addEventListener('keydown', function(e) { } }) -function update() { - drawAllPolygons(); +function rewritePoints() { var parentPoints = getParentPoints(); writePoints(parentPoints); } @@ -443,26 +462,32 @@ function highlightButtonInteraction (buttonId) { setTimeout(() => document.querySelector(buttonId).classList.remove('active'), 100); } -function undo () { +function undo() { highlightButtonInteraction('#undo'); if (points.length > 0) { points.pop(); - clearDrawings(); - update(); + blitCachedCanvas(); + rewritePoints(); if(points.length === 0){ return; } + var ctx = mainCtx; ctx.strokeStyle = rgb_color; - var i = 0; - for (; i < points.length - 1; i++) { - makeLine(points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); - ctx.stroke(); - drawNode(points[i][0], points[i][1]); + ctx.fillStyle = 'white'; + if (points.length === 1) { + drawNode(ctx, points[0][0], points[0][1]); + } + else { + drawNode(ctx, points[0][0], points[0][1]); + for (var i = 0; i < points.length - 1; i++) { + makeLine(ctx, points[i][0], points[i][1], points[i + 1][0], points[i + 1][1]); + ctx.stroke(); + drawNode(ctx, points[i + 1][0], points[i + 1][1]); + } } - drawNode(points[i][0], points[i][1]); } } @@ -473,8 +498,8 @@ document.querySelector('#undo').addEventListener('click', function(e) { function discardCurrentPolygon () { highlightButtonInteraction('#discard-current'); points = []; - clearDrawings(); - update(); + blitCachedCanvas(); + rewritePoints(); } document.querySelector('#discard-current').addEventListener('click', function(e) { @@ -483,7 +508,10 @@ document.querySelector('#discard-current').addEventListener('click', function(e) function clearAll() { highlightButtonInteraction('#clear') - clearDrawings(); + mainCtx.clearRect(0, 0, canvas.width, canvas.height); + offScreenCtx.clearRect(0, 0, offScreenCanvas.width, offScreenCanvas.height); + mainCtx.drawImage(img, 0, 0); + offScreenCtx.drawImage(img, 0, 0); points = []; masterPoints = []; masterColors = [];