Problem: I'm working with an HTML canvas. My canvas has a background image that multiple people can draw over in real-time (via socket.io), but drawing breaks if you've zoomed in.
Cause: To calculate where to start and end a line, I normalize input upon capture to be between 0 and 1 inclusive, like so:
// Pseudocode
line.x = mousePosition.x / canvas.width;
line.y = mousePosition.y / canvas.height;
Because of this, the canvas can be of any size and in any position.
To implement a zoom-on-scroll functionality, I simply translate based on the current mouse position, scale the canvas by a factor of 2, then translate back the negative value of the current mouse position (as recommended here).
Here's where the problem lies
When I zoom, the canvas doesn't seem to have a notion of it's original size.
For instance, let's say I have a 1000px square canvas. Using my normalized x and y above, the upper left corner is 0, 0 and the lower right is 1, 1.
I then zoom into the center through scaling by a factor of 2. I would expect that my new upper left would be 0.5, 0.5 and my lower right would be 0.75, 0.75, but it isn't. Even when I zoom in, the upper left is still 0, 0 and the lower right is 1, 1.
The result is that when I zoom in and draw, the lines appear where they would as if I were not zoomed at all. If I zoomed into the center and "drew" in the upper left, I'd see nothing until I scrolled out to see that the line was actually getting drawn on the original upper left.
What I need to know: When zoomed, is there a way to get a read on what your new origin is relative to the un-zoomed canvas, or what amount of the canvas is hidden? Either of these would let me zoom in and draw and have it track correctly.
If I'm totally off base here and there's a better way to approach this, I'm all ears. If you need additional information, I'll provide what I can.
It's not clear to me what you mean by "zoomed".
Zoomed =
made the canvas a different size?
changed the transform on the canvas
used CSS transform?
used CSS zoom?
I'm going to assume it's transform on the canvas in which case it's something like
function getElementRelativeMousePosition(e) {
return [e.offsetX, e.offsetY];
}
function getCanvasRelativeMousePosition(e) {
const pos = getElementRelativeMousePosition(e);
pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
return pos;
}
function getComputedMousePosition(e) {
const pos = getCanvasRelativeMousePosition(e);
const p = new DOMPoint(...pos);
const point = inverseOriginTransform.transformPoint(p);
return [point.x, point.y];
}
Where inverseOriginTransform is the inverse of whatever transform you're using to zoom and scroll the contents of the canvas.
const settings = {
zoom: 1,
xoffset: 0,
yoffset: 0,
};
const canvas = document.querySelector('canvas');
const ctx = canvas.getContext('2d');
const lines = [
[[100, 10], [200, 30]],
[[50, 50], [100, 30]],
];
let newStart;
let newEnd;
let originTransform = new DOMMatrix();
let inverseOriginTransform = new DOMMatrix();
function setZoomAndOffsetTransform() {
originTransform = new DOMMatrix();
originTransform.translateSelf(settings.xoffset, settings.yoffset);
originTransform.scaleSelf(settings.zoom, settings.zoom);
inverseOriginTransform = originTransform.inverse();
}
const ui = document.querySelector('#ui')
addSlider(settings, 'zoom', ui, 0.25, 3, draw);
addSlider(settings, 'xoffset', ui, -100, +100, draw);
addSlider(settings, 'yoffset', ui, -100, +100, draw);
draw();
function updateAndDraw() {
draw();
}
function getElementRelativeMousePosition(e) {
return [e.offsetX, e.offsetY];
}
function getCanvasRelativeMousePosition(e) {
const pos = getElementRelativeMousePosition(e);
pos[0] = pos[0] * ctx.canvas.width / ctx.canvas.clientWidth;
pos[1] = pos[1] * ctx.canvas.height / ctx.canvas.clientHeight;
return pos;
}
function getTransformRelativeMousePosition(e) {
const pos = getCanvasRelativeMousePosition(e);
const p = new DOMPoint(...pos);
const point = inverseOriginTransform.transformPoint(p);
return [point.x, point.y];
}
canvas.addEventListener('mousedown', (e) => {
const pos = getTransformRelativeMousePosition(e);
if (newStart) {
} else {
newStart = pos;
newEnd = pos;
}
});
canvas.addEventListener('mousemove', (e) => {
if (newStart) {
newEnd = getTransformRelativeMousePosition(e);
draw();
}
});
canvas.addEventListener('mouseup', (e) => {
if (newStart) {
lines.push([newStart, newEnd]);
newStart = undefined;
}
});
function draw() {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
setZoomAndOffsetTransform();
ctx.setTransform(
originTransform.a,
originTransform.b,
originTransform.c,
originTransform.d,
originTransform.e,
originTransform.f);
ctx.beginPath();
for (const line of lines) {
ctx.moveTo(...line[0]);
ctx.lineTo(...line[1]);
}
if (newStart) {
ctx.moveTo(...newStart);
ctx.lineTo(...newEnd);
}
ctx.stroke();
ctx.restore();
}
function addSlider(obj, prop, parent, min, max, callback) {
const valueRange = max - min;
const sliderRange = 100;
const div = document.createElement('div');
div.class = 'range';
const input = document.createElement('input');
input.type = 'range';
input.min = 0;
input.max = sliderRange;
const label = document.createElement('span');
label.textContent = `${prop}: `;
const valueElem = document.createElement('span');
function setInputValue(v) {
input.value = (v - min) * sliderRange / valueRange;
}
input.addEventListener('input', (e) => {
const v = parseFloat(input.value) * valueRange / sliderRange + min;
valueElem.textContent = v.toFixed(1);
obj[prop] = v;
callback();
});
const v = obj[prop];
valueElem.textContent = v.toFixed(1);
setInputValue(v);
div.appendChild(input);
div.appendChild(label);
div.appendChild(valueElem);
parent.appendChild(div);
}
canvas { border: 1px solid black; }
#app { display: flex; }
<div id="app"><canvas></canvas><div id="ui"></div>
Note: I didn't bother making zoom always zoom from the center. To do so would require adjusting xoffset and yoffset as the zoom changes.
Use HTMLElement.prototype.getBoundingClientRect() to get displayed size and position of canvas in DOM. From the displayed size and origin size, calculates the scale of the canvas.
Example:
canvas.addEventListener("click", function (event) {
var b = canvas.getBoundingClientRect();
var scale = canvas.width / parseFloat(b.width);
var x = (event.clientX - b.left) * scale;
var y = (event.clientY - b.top) * scale;
// Marks mouse position
var ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.stroke();
});
Related
This animation (based on the answer of
Вася Воронцов) loads the computer very much. I do this animation in canvas. Animation loads proccesor very much. Here the light follows the cursor and leaves traces. Animation works correctly but proccesor loads very much.
Deleting and changing the radii of circles is done by saving their coordinates.
The effect is controlled by changing the variables radius (circle radius), period (time for which the circle disappears), color (circle color), blur (blur radius) and cursor radius (pointer circle radius).
How to optimize this animation so that it loads the computer less?
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var width = document.body.offsetWidth;
var height = document.body.offsetHeight;
var points = [],
cursor = [-10, -10];
var t = 0;
var radius = 100;
var period = 2100;
var color = "rgba(239, 91, 59, .5)";
var blur = 600;
canvas.style.width = canvas.width = width;
canvas.style.height = canvas.height = height;
context.fillStyle = color;
var filter = context.filter = "blur(" + 50 + "px)";
var dr = radius / period;
function draw() {
context.clearRect(0, 0, width, height);
let i = 0;
let deleted = 0;
let dt = -t + (t = window.performance.now());
context.beginPath();
while (i++ < points.length-1) {
let p = points[i];
p[2] += dt;
let r = radius - p[2] * dr;
context.moveTo(p[0], p[1]);
if (p[2] <= period) {
context.arc(p[0], p[1], r, 0, 2*Math.PI, true);
} else deleted = i;
}
context.fill();
points.splice(0, deleted);
context.beginPath();
context.arc(cursor[0], cursor[1], 20, 0, 2*Math.PI, true);
context.filter = "none";
context.fill();
context.filter = filter;
window.requestAnimationFrame(draw);
}
window.onmousemove = function(event) {
let x = event.pageX;
let y = event.pageY;
let backwardX = 0;
let backwardY = 0;
backwardX += (x-backwardX) / 5
backwardY += (y-backwardY) / 5
points.push([x, y, 0]);
cursor = [x, y];
}
t = window.performance.now();
window.requestAnimationFrame(draw);
body {
height: 100%;
width: 100%;
position: absolute;
cursor: none;
margin: 0;
}
<canvas id="canvas"></canvas>
PS: Question in Russian.
It's slow because you have a lot of overdraw. Each frame, a large number of points is being drawn, and each point touches a lot of pixels.
You can achieve something that looks very similar if you realize that the canvas retains its contents between frames. So every frame, you could do something like this:
Fade the canvas towards white by drawing a nearly transparent white rectangle over it.
Draw one new blurred point, at the current cursor location.
The circle that follows the mouse can easily be achieved by overlaying a separate element on top of the canvas, for example a <div>. Use transform: translate(x, y); to move it, which is more performant than using left/top because it's a compositor-only property. Add will-change: transform; for an extra potential performance boost.
I'm trying to implement zoom in an HTML5 Canvas. I came across other threads that explain how to do it, but they take advantage of the canvas' context, storing previous transformations. I want to avoid that.
So far I managed to do the following (https://jsfiddle.net/wfqzr538/)
<!DOCTYPE html>
<html>
<body style="margin: 0">
<canvas id="canvas" width="400" height="400" style="border: 1px solid #d3d3d3"></canvas>
<script>
const canvas = document.getElementById("canvas");
const context = canvas.getContext("2d");
let cursorX = 0,
cursorY = 0;
let zoom = 1;
canvas.onmousemove = mouseMove;
window.onkeydown = keyDown;
draw();
function mouseMove(evt) {
cursorX = evt.clientX;
cursorY = evt.clientY;
}
function keyDown(evt) {
if (evt.key == "p") {
zoom += 0.1;
}
if (evt.key == "m") {
zoom -= 0.1;
}
draw();
}
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
const translationX = -cursorX * (zoom - 1);
const translationY = -cursorY * (zoom - 1);
context.translate(translationX, translationY);
context.scale(zoom, zoom);
context.fillRect(100, 100, 30, 30);
context.scale(1 / zoom, 1 / zoom);
context.translate(-translationX, -translationY);
}
</script>
</body>
</html>
The code above works If I zoom at the same location, but breaks as soon as I change it. For example, if I zoom in twice at the top left corner of the square, it works. However, if I zoom once at the top left corner, followed by zooming at the right bottom corner, it breaks.
I've been trying to fix this for a while now. I think it has something with not taking into account previous translations made in the canvas, but I'm not sure.
I'd really appreciate some help.
Not keeping previous state
If you don't want to keep the previous transform state then the is no way to do what you are trying to do.
There are many way to keep the previous state
Previous world state
You can transform all object's world state, in effect embedding the previous transform in the object's coordinates.
Eg with zoom and translate
object.x = object.x * zoom + translate.x;
object.y = object.y * zoom + translate.y;
object.w *= zoom;
object.h *= zoom;
then draw using default transform
ctx.setTransform(1,0,0,1,0,0);
ctx.fillRect(object.x, object.y, object.w, object.h);
Previous transformation state
To zoom at a point (absolute pixel coordinate) you need to know where the previous origin was so you can workout how far the zoom point is from that origin and move it correctly when zooming.
Your code does not keep track of the origin, in effect it assumes it is always at 0,0.
Example
The example tracks the previous transform state using an array that represents the transform. It is equivalent to what your code defines as translation and zoom.
// from your code
context.translate(translationX, translationY);
context.scale(zoom, zoom);
// is
transform = [zoom, 0, 0, zoom, translationX, translationY];
The example also changes the rate of zooming. In your code you add and subtract from the zoom, this will result in negative zooms, and when zooming in it will take forever to get to large scales. The scale is apply as a scalar eg zoom *= SCLAE_FACTOR
The function zoomAt zooms in or out at a given point on the canvas
const ctx = canvas.getContext("2d");
const transform = [1,0,0,1,0,0];
const SCALE_FACTOR = 1.1;
const pointer = {x: 0, y: 0};
var zoom = 1;
canvas.addEventListener("mousemove", mouseMove);
addEventListener("keydown", keyDown);
draw();
function mouseMove(evt) {
pointer.x = evt.clientX;
pointer.y = evt.clientY;
drawPointer();
}
function keyDown(evt) {
if (evt.key === "p") { zoomAt(SCALE_FACTOR, pointer) }
if (evt.key === "m") { zoomAt(1 / SCALE_FACTOR, pointer) }
}
function zoomAt(amount, point) {
zoom *= amount;
transform[0] = zoom; // scale x
transform[3] = zoom; // scale y
transform[4] = point.x - (point.x - transform[4]) * amount;
transform[5] = point.y - (point.y - transform[5]) * amount;
draw();
}
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.setTransform(...transform);
ctx.fillRect(100, 100, 30, 30);
}
function drawPointer() {
draw();
ctx.lineWidth = 1;
ctx.strokeStyle = "black";
const invScale = 1 / transform[0]; // Assumes uniform scale
const x = (pointer.x - transform[4]) * invScale;
const y = (pointer.y - transform[5]) * invScale;
const size = 10 * invScale;
ctx.beginPath();
ctx.lineTo(x - size, y);
ctx.lineTo(x + size, y);
ctx.moveTo(x, y - size);
ctx.lineTo(x, y + size);
ctx.setTransform(1, 0, 0, 1, 0, 0); // to ensure line width is 1 px
ctx.stroke();
ctx.font = "16px arial";
ctx.fillText("Pointer X: " + x.toFixed(2) + " Y: " + y.toFixed(2), 10, 20);
}
* {margin: 0px}
canvas { border: 1px solid #aaa }
<canvas id="canvas" width="400" height="400"></canvas>
Update
I have added the function drawPointer which uses the transform to calculate the pointers world position, render a cross hair at the position and display the coordinates.
I'm trying to create a zoomable canvas with rectangles arranged in a grid using pixi.js. Everything works smoothly except that the grid creates heavy moire effects. My knowledge about pixijs and webgl is only very superficial but I'm suspecting that something with the antialiasing is not working as I expect it to. I'm drawing the rectangles using a 2048x2048px texture I create beforehand in separate canvas. I make it that big so I do this so I can zoom in all the way while still having a sharp rectangle. I also tried using app.renderer.generateTexture(graphics) but got a similar result.
The black rectangles are drawn using pixi.js and the red ones are drawn using SVG as a reference. There is still moire occurring in the SVG as well but it is much less. Any ideas how I can get closer to what the SVG version looks like? You can find a a working version here.
Here's the relevant code I use to setup the pixi.js application:
// PIXI SETUP
const app = new Application({
view: canvasRef,
width,
height,
transparent: true,
antialias: false,
autoDensity: true,
resolution: devicePixelRatio,
resizeTo: window
});
const particleContainer = new ParticleContainer(2500, {
scale: true,
position: true,
rotation: true,
uvs: true,
alpha: true
});
app.stage.addChild(particleContainer);
// TEXTURE
const size = 2048;
const canvas = document.createElement("canvas");
canvas.width = size;
canvas.height = size;
const ctx = canvas.getContext("2d");
ctx.fillStyle = "#000000";
ctx.fillRect(0, 0, size, size);
ctx.fill();
const texture = PIXI.Texture.from(canvas);
// RECTANGLE GRID
const size = 10;
for(let i=0; i<2500; i++) {
const particle = Sprite.from(texture);
particle.x = i % 50 * size * 1.5;
particle.y = Math.floor(i / 50) * size * 1.5;
particle.anchor.set(0);
particle.width = size;
particle.height = size;
parent.addChild(particle);
}
Don't render sub pixel detail.
The best way to maintain a grid while avoiding artifacts is to not render grid steps below the resolution of the canvas. Eg if you have zoomed out by 100 then do not draw grids less than 100 pixels.
As this can result in grid steps popping in and out you can fade grids in and out depending on the zoom level.
The example shows one way to do this. It still has some artifacts, these are hard to avoid, but this eliminates the horrid moire patterns you get when you render all the detail at every zoom level.
The grid is defined as 2D repeating patterns to reduce rendering overhead.
Also I find grid lines more problematic than grid squares (Demo has both)
This is very basic and can be adapted to any type of grid layout.
requestAnimationFrame(mainLoop);
const ctx = canvas.getContext("2d");
const size = 138;
const grids = createPatterns(size, 4, "#222", "#EEE", "#69B", "#246");
var zoom = 1;
var zoomTarget = 16;
var zoomC = 0;
var gridType = 0;
var origin = {x: ctx.canvas.width / 2, y: ctx.canvas.height / 2};
const scales = [0,0,0];
function createPatterns(size, lineWidth, color1, color2, color3, color4){
function grid(col1, col2) {
ctx.fillStyle = col1;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = col2;
ctx.fillRect(0, 0, size, lineWidth);
ctx.fillRect(0, 0, lineWidth, size);
}
function grid2(col1, col2) {
ctx.fillStyle = col1;
ctx.fillRect(0, 0, size, size);
ctx.fillStyle = col2;
ctx.fillRect(0, 0, size / 2, size / 2);
ctx.fillRect( size / 2, size / 2, size / 2, size / 2);
}
const patterns = [];
const ctx = Object.assign(document.createElement("canvas"), {width: size, height: size}).getContext("2d");
grid(color1, color2)
patterns[0] = ctx.createPattern(ctx.canvas, "repeat");
grid2(color3, color4)
patterns[1] = ctx.createPattern(ctx.canvas, "repeat");
return patterns;
}
function drawGrid(ctx, grid, zoom, origin, smooth = true) {
function zoomAlpha(logScale) {
const t = logScale % 3;
return t < 1 ? t % 1 : t > 2 ? 1 - (t - 2) % 1 : 1;
}
function fillScale(scale) {
ctx.setTransform(scale / 8, 0, 0, scale / 8, origin.x, origin.y);
ctx.globalAlpha = zoomAlpha(Math.log2(scale));
ctx.fill();
}
ctx.fillStyle = grid;
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.imageSmoothingEnabled = smooth;
ctx.beginPath();
ctx.rect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalAlpha = 1;
const l2 = Math.log2(zoom);
scales[0] = 2 ** ((l2 + 122) % 3); // zoom limit 1 / (2 ** 122) (well beyond number precision)
scales[1] = 2 ** ((l2 + 123) % 3);
scales[2] = 2 ** ((l2 + 124) % 3);
scales.sort((a,b) => a - b);
fillScale(scales[0]);
fillScale(scales[1]);
fillScale(scales[2]);
ctx.globalAlpha = 1;
}
function mainLoop() {
if (innerWidth !== ctx.canvas.width || innerHeight !== ctx.canvas.height) {
origin.x = (ctx.canvas.width = innerWidth) / 2;
origin.y = (ctx.canvas.height = innerHeight) / 2;
zoomTarget = 16;
zoom = 1;
}
zoomC += (zoomTarget - zoom) * 0.3;
zoomC *= 0.02;
zoom += zoomC;
if (gridType === 0) {
drawGrid(ctx, grids[0], zoom, origin);
} else {
drawGrid(ctx, grids[1], zoom, origin, false);
}
requestAnimationFrame(mainLoop);
}
zoomIn.addEventListener("click", () => zoomTarget *= 2);
zoomOut.addEventListener("click", () => zoomTarget *= 1/2);
toggle.addEventListener("click", () => gridType = (gridType + 1) % 2);
* { margin: 0px;}
canvas { position: absolute; top: 0px;left: 0px; }
.UI { position: absolute; top: 14px; left: 14px; }
<canvas id="canvas"></canvas>
<div class="UI">
<button id="zoomIn">Zoom In</button><button id="zoomOut">Zoom Out</button><button id="toggle">Toggle grid type</button>
</div>
My question is: how should I do the logical and mathematical part, in order to calculate the position of each arc, when an event is emitted: "click", "mouseover", etc.
Points to consider:
. Line width
. They are rounded lines.
. Arc length in percentage.
. Which element is first, ej: z-index position.
My source code
Thank you for your time.
There is a convenient isPointInStroke() method, just for that.
To take the z-index into consideration, just check in the reverse order you drew:
const ctx = canvas.getContext('2d');
const centerX = canvas.width/2;
const centerY = canvas.height/2;
const rad = Math.min(centerX, centerY) * 0.75;
const pathes = [
{
a: Math.PI/4,
color: 'white'
},
{
a: Math.PI/1.5,
color: 'cyan'
},
{
a: Math.PI,
color: 'violet'
},
{
a: Math.PI*2,
color: 'gray'
}
];
pathes.forEach(obj => {
const p = new Path2D();
p.arc(centerX, centerY, rad, -Math.PI/2, obj.a-Math.PI/2);
obj.path = p;
});
ctx.lineWidth = 12;
ctx.lineCap = 'round';
function draw() {
ctx.clearRect(0,0,canvas.width,canvas.height);
// since we sorted first => higher z-index
for(let i = pathes.length-1; i>=0; i--) {
let p = pathes[i];
ctx.strokeStyle = p.hovered ? 'green' : p.color;
ctx.stroke(p.path);
};
}
function checkHovering(evt) {
const rect = canvas.getBoundingClientRect();
const x = evt.clientX - rect.left;
const y = evt.clientY - rect.top;
let found = false;
pathes.forEach(obj => {
if(found) {
obj.hovered = false;
}
else {
found = obj.hovered = ctx.isPointInStroke(obj.path, x, y);
}
});
draw();
}
draw();
canvas.onmousemove = canvas.onclick = checkHovering;
canvas{background: lightgray}
<canvas id="canvas"></canvas>
And if you need IE support, this polyfill should do.
It's much better to "remember" the objects you drew, rather than drawing them and trying to gather what they are from what you drew. So, for example, you could store the render information: (I don't know typescript)
let curves = [{start: 30, length: 40, color: "white"}/*...*/];
Then, render it:
ctx.fillStyle = curve.color;
ctx.arc(CENTER_X, CENTER_Y, RADIUS, percentToRadians(curve.start), percentToRadians(curve.start + curve.length));
Then, to retrieve the information, simply reference curves. The z values depend on the order of the render queue (curves).
Of course, you probably could gather that data from the canvas, but I wouldn't recommend it.
I have a canvas on which user can draw point on click on any part of it. As one know we must give the user the possibility to undo an action that he as done. This is where I'am stuck, how can I program the codes to allow the user to remove the point on double click on the the point he wants to remove ?
<canvas id="canvas" width="160" height="160" style="cursor:crosshair;"></canvas>
1- Codes to draw the canvas and load an image in it
var canvasOjo1 = document.getElementById('canvas'),
context1 = canvasOjo1.getContext('2d');
ojo1();
function ojo1()
{
base_image1 = new Image();
base_image1.src = 'https://www.admedicall.com.do/admedicall_v1//assets/img/patients-pictures/620236447.jpg';
base_image1.onload = function(){
context1.drawImage(base_image1, 0, 0);
}
}
2- Codes to draw the points
$("#canvas").click(function(e){
getPosition(e);
});
var pointSize = 3;
function getPosition(event){
var rect = canvas.getBoundingClientRect();
var x = event.clientX - rect.left;
var y = event.clientY - rect.top;
drawCoordinates(x,y);
}
function drawCoordinates(x,y){
var ctx = document.getElementById("canvas").getContext("2d");
ctx.fillStyle = "#ff2626"; // Red color
ctx.beginPath();
ctx.arc(x, y, pointSize, 0, Math.PI * 2, true);
ctx.fill();
}
My fiddle :http://jsfiddle.net/xpvt214o/834918/
By hover the mouse over the image we see a cross to create the point.
How can i remove a point if i want to after create it on double click ?
Thank you in advance.
Please read first this answer how to differentiate single click event and double click event because this is a tricky thing.
For the sake of clarity I've simplified your code by removing irrelevant things.
Also please read the comments of my code.
let pointSize = 3;
var points = [];
var timeout = 300;
var clicks = 0;
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let cw = (canvas.width = 160);
let ch = (canvas.height = 160);
function getPosition(event) {
var rect = canvas.getBoundingClientRect();
return {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};
}
function drawCoordinates(point, r) {
ctx.fillStyle = "#ff2626"; // Red color
ctx.beginPath();
ctx.arc(point.x, point.y, r, 0, Math.PI * 2, true);
ctx.fill();
}
canvas.addEventListener("click", function(e) {
clicks++;
var m = getPosition(e);
// this point won't be added to the points array
// it's here only to mark the point on click since otherwise it will appear with a delay equal to the timeout
drawCoordinates(m, pointSize);
if (clicks == 1) {
setTimeout(function() {
if (clicks == 1) {
// on click add a new point to the points array
points.push(m);
} else { // on double click
// 1. check if point in path
for (let i = 0; i < points.length; i++) {
ctx.beginPath();
ctx.arc(points[i].x, points[i].y, pointSize, 0, Math.PI * 2, true);
if (ctx.isPointInPath(m.x, m.y)) {
points.splice(i, 1); // remove the point from the array
break;// if a point is found and removed, break the loop. No need to check any further.
}
}
//clear the canvas
ctx.clearRect(0, 0, cw, ch);
}
points.map(p => {
drawCoordinates(p, pointSize);
});
clicks = 0;
}, timeout);
}
});
body {
background: #20262E;
padding: 20px;
}
<canvas id="canvas" style="cursor:crosshair;border:1px solid white"></canvas>