I have a canvas with some irregular shape drawings in it, and I would like to have a feedback when someone clicks on a specific one?
I've been looking everywhere for this and have only found solutions for rectangle.
I think it may have to do with isPointInPath(), but I've yet to find a concise explanation on how to use it.
Any help welcome.
I made a tutorial that uses a second invisible canvas to do object picking/hit testing. Draw all your shapes, one by one, onto the second canvas until one of them has a black pixel where the mouse location is. Then you've found your object!
Here's a bit from the tutorial I wrote on selecting objects with canvas:
// gctx is ghost context, made from the second canvas
// clear(gctx)
// ...
// run through all the boxes
var l = boxes.length;
for (var i = l-1; i >= 0; i--) {
// draw shape onto ghost context
drawshape(gctx, boxes[i], 'black', 'black');
// get image data at the mouse x,y pixel
var imageData = gctx.getImageData(mx, my, 1, 1);
var index = (mx + my * imageData.width) * 4;
// if the mouse pixel exists, select and break
if (imageData.data[3] > 0) {
mySel = boxes[i];
offsetx = mx - mySel.x;
offsety = my - mySel.y;
mySel.x = mx - offsetx;
mySel.y = my - offsety;
isDrag = true;
canvas.onmousemove = myMove;
invalidate();
clear(gctx);
return;
}
}
My full demo only uses rectangles but in a later version I will use circles/paths/text.
If you want to see the demo and my full code it is here.
You can use a pathiterator, that turns all shapes into approximated polygons.
Then use a 'point in polygon' algorithm to check if the point is in the shape.
This can be achived by using Path2D.
var div = document.getElementById("result");
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var path1 = new Path2D();
path1.rect(10, 10, 100, 100);
path1.closePath();
ctx.stroke(path1);
var path2 = new Path2D();
path2.moveTo(220, 60);
path2.arc(170, 60, 50, 0, 2 * Math.PI);
path2.closePath();
ctx.stroke(path2);
var path3 = new Path2D("M230 10 h 80 v 80 h -80 Z");
ctx.fill(path3);
path3.closePath();
$('canvas').click( function(event)
{
div.innerHTML = "";
var x = event.pageX;
var y = event.pageY;
if (ctx.isPointInPath(path1, x, y))
div.innerHTML = "Path1 clicked";
if (ctx.isPointInPath(path2, x, y))
div.innerHTML = "Path2 clicked";
if (ctx.isPointInPath(path3, x, y))
div.innerHTML = "Path3 clicked";
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<body>
<canvas id="canvas"></canvas>
<div id="result"></div>
</body>
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 made a Canvas with 32x32 tiles. I wanted to stroke the tile the mouse is on, this is what I made (Thanks to some tutorials and adjustments)
First, I create cPush and cUndo. cPush is called when I draw something, to memorize it. For cUndo, you'll see later. I also call my ctx var and I call my canvas var.
var canvas = document.getElementById('canvas');
var cPushArray = new Array();
var cStep = -1;
var ctx = canvas.getContext('2d');
function cPush() {
cStep++;
if (cStep < cPushArray.length) { cPushArray.length = cStep; }
cPushArray.push(document.getElementById('canvas').toDataURL());
}
function cUndo() {
if (cStep > 0) {
cStep--;
var canvasPic = new Image();
canvasPic.src = cPushArray[cStep];
canvasPic.onload = function () { ctx.drawImage(canvasPic, 0, 0); }
}
}
I will skip some parts because they are not necessary.
Here, I'm calling the onmousemove function :
canvas.onmousemove = function(e) {
let rects = [], i = 0, r;
let jk = 0;
for(var nb = 0, l = map.terrain.length ; nb < l ; nb++) {
var ligne = map.terrain[nb];
var y2 = nb * 32;
for(var j = 0, k = ligne.length ; j < k ; j++, jk++) {
rects[jk] = {lignej: ligne[j], x: j * 32, y: y2, w: 32, h: 32};
}
}
This first part is made to get every tiles of the map and store it into rects[x] This will be useful for me later, when I will need specific tiles.
Now, I call a function :
map.test = function(linewidth) {
this.tileset.dessinerTile(r.lignej, context, r.x, r.y);
ctx.lineWidth = linewidth;
ctx.strokeRect(r.x, r.y, 32, 32);
}
This is made to redraw the Tile i'm on with a stroke. (r. = rects var, you'll see later)
Here's the rest :
var rect = this.getBoundingClientRect(),
x = e.clientX - rect.left,
y = e.clientY - rect.top;
while (r = rects[i++]) {
ctx.beginPath();
ctx.rect(r.x, r.y, 32, 32);
if (ctx.isPointInPath(x, y) == true) {
cPush();
map.test(2);
} else {
cUndo();
}
}
};
The var Rect is made to check the mouse's position.
Now, there's a while. For each tiles we'll check if isPointinPath is true. (x and y are the mouse's positions).
So, if that's true, the function map.test will draw the tile with a 2 lineWidth stroke as mentioned.
If not, it will Undo the last stroke.
What I wanted to do here :
If the mouse is on a tile, it stroke the tile. If the mouse move, it undo the last stroke and stroke the new one.
What it does :
If the mouse is on a tile, it stroke the tile before doing an undo of the stroke because it check the other ones. So the stroke only last 0.5s instead of being permanent until I move the mouse.
I think the problem is that I'm using a "onmousemove" function ? Do I need to change to a "onmousehover" or something like that ? I'm quite lost...
Thanks for the help !
RECORD OF THE PROBLEM (Sorry for the low FPS it's a gif)
http://recordit.co/GokcSAoMv6
I have drawn a circle with canvas using following code.
<div class="abc"><canvas id="myCanvas">HTML5 canvas tag.</canvas></div>
<script>
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.beginPath();
ctx.arc(100,75,50,-0.1*Math.PI,1.7*Math.PI);
ctx.lineWidth = 15;
ctx.strokeStyle = "#c32020";
ctx.stroke();
var c2 = document.getElementById("myCanvas");
var ctx2 = c2.getContext("2d");
ctx2.beginPath();
ctx2.arc(100,75,50,1.7*Math.PI,-0.1*Math.PI);
ctx2.lineWidth = 15;
ctx2.strokeStyle = "#a9a9a9";
ctx2.stroke();
</script>
Here is the result on the web browser.
Now I need to call two different javascript function when the user clicks on red part and gray part of the circle.
On click of the red part it should call function1 and on click of gray part, it should call function2.
I've tried a lot but didn't succeed. I need some expert's help to implement it.
Letting the browser do (most of) the work
On click of the red part it should call function1 and on click of gray part, it should call function2.
You can use reusable path objects to store the different paths you want to test against and then use isPointInPath() with path and coordinates as arguments.
By checking each path for a hit you can assign a different call based on for example index.
And there is no need to do this:
var c2 = document.getElementById("myCanvas");
var ctx2 = c2.getContext("2d");
This will simply reference the same context as a canvas can only have one - if requested more than once the same will be given (or null if different type).
How to use multiple click event on a canvas
You can share the click handler to do what you want as shown below -
For modern browsers you can use both Path2D objects to store path information you want to use later (I'll address older browsers in the second example).
Example
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var p1 = new Path2D();
var p2 = new Path2D();
var paths = [p1, p2]; // store paths in array for check later
// store arc parts to respective path objects
p1.arc(100, 75, 50, -0.1 * Math.PI, 1.7 * Math.PI); // red part
p2.arc(100, 75, 50, 1.7 * Math.PI, -0.1 * Math.PI); // grey part
// render the two path objects using a common context, but different style
ctx.lineWidth = 15;
ctx.strokeStyle = "#c32020";
ctx.stroke(p1);
ctx.strokeStyle = "#a9a9a9";
ctx.stroke(p2);
// check for clicks on common canvas
c.onclick = function(e) {
var rect = this.getBoundingClientRect(), // adjust click coordinates
x = e.clientX - rect.left,
y = e.clientY - rect.top;
// iterate through path array to test each path for hits
for(var i = 0; i < paths.length; i++) {
if (ctx.isPointInStroke(paths[i], x, y)) { // check which path
console.log("Path " + (i+1) + " clicked");
break;
}
}
};
<canvas id="myCanvas"></canvas>
Browser compatibility
Older browsers however, won't support the isPointInStroke(), such as IE11.
For this scenario you can use simple math to figure out if the click is within the radius of the arcs:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
ctx.lineWidth = 15;
ctx.arc(100, 75, 50, -0.1 * Math.PI, 1.7 * Math.PI); // red part
ctx.strokeStyle = "#c32020";
ctx.stroke();
ctx.beginPath();
ctx.arc(100, 75, 50, 1.7 * Math.PI, -0.1 * Math.PI); // grey part
ctx.strokeStyle = "#a9a9a9";
ctx.stroke();
// check for clicks on common canvas
c.onclick = function(e) {
var rect = this.getBoundingClientRect(), // adjust click coordinates
x = e.clientX - rect.left,
y = e.clientY - rect.top,
diffX = 100 - x,
diffY = 75 - y,
len = diffX*diffX + diffY*diffY, // we don't need to square-root it:
r1 = 43*43, // it's faster to scale up radius (50-50% of linewidth)
r2 = 57*57; // radius + 50% of linewidth
// are we on the edge of the circle?
if (len >= r1 && len <= r2) {
// now find angle to see if we're in red or grey area
var angle = Math.atan2(diffY, diffX);
if (angle > 0.7 * Math.PI && angle < 0.9 * Math.PI) {
console.log("Grey part");
}
else {
console.log("Red part");
}
}
};
<canvas id="myCanvas"></canvas>
Note that a special case in the latter example, i.e. when the arc crosses the 0/360° point, you will want to split the checks [0, angle> and [angle, 360>.
You will need to add a click event to the canvas element and calculate if the red or gray is clicked.
Here's some code to get you started, but you'll still have to figure out what coordinates your circle area has.
var elem = document.getElementById('myCanvas'),
elemLeft = elem.offsetLeft,
elemTop = elem.offsetTop;
// Add event listener for `click` events.
elem.addEventListener('click', function(event) {
var x = event.pageX,
y = event.pageY;
// use x and y to determine if the colored regions are clicked and execute code accordingly
}, false);
Code gotten from here: How do I add a simple onClick event handler to a canvas element?
I created circles using canvas but can't delete the same on double click.`
var canvas,
context, shapes,
dragging = false, draggingtoMove = false,
dragStartLocation,dragEndLocation,
snapshot;
var numShapes;
function initiate() {
numShapes = 100;
shapes = [];
canvas = document.getElementById('canvas');
context = canvas.getContext('2d');
canvas.addEventListener('mousedown', dragStart, false);
canvas.addEventListener('mousemove', drag, false);
canvas.addEventListener('mouseup', dragStop, false);
canvas.addEventListener('dblclick', dblclickerase);
}
function dblclickerase(evt)
{
var i;
//We are going to pay attention to the layering order of the objects so that if a mouse down occurs over more than object,
//only the topmost one will be dragged.
var highestIndex = -1;
//getting mouse position correctly, being mindful of resizing that may have occured in the browser:
var bRect = canvas.getBoundingClientRect();
mouseX = (evt.clientX - bRect.left)*(canvas.width/bRect.width);
mouseY = (evt.clientY - bRect.top)*(canvas.height/bRect.height);
//find which shape was clicked
for (i=0; i < shapes.length; i++) {
if (hitTest(shapes[i], mouseX, mouseY)) {
//draggingtoMove = true;
if (i > highestIndex) {
// here i want to delete the circle on double click but not getting logic, I can get mouse location but how to select the circle and delete it
}
}
}
}
function getCanvasCoordinates(event) {
var x = event.clientX - canvas.getBoundingClientRect().left,
y = event.clientY - canvas.getBoundingClientRect().top;
return {
x: x,
y: y
};
}
function takeSnapshot() {
snapshot = context.getImageData(0, 0, canvas.width, canvas.height);
}
function restoreSnapshot() {
context.putImageData(snapshot, 0, 0);
}
function draw(position) {
var radius = Math.sqrt(Math.pow((dragStartLocation.x - position.x), 2) + Math.pow((dragStartLocation.y - position.y), 2));
var i=0;
var tempX;
var tempY;
var tempRad;
var tempR;
var tempG;
var tempB;
var tempColor;
tempRad = radius;
tempX = dragStartLocation.x;
tempY = dragStartLocation.y;
tempColor = getRndColor();
tempShape = {x:tempX, y:tempY, rad:tempRad, color:tempColor};
shapes.push(tempShape);
context.beginPath();
context.arc(tempX, tempY, tempRad, 0, 2*Math.PI, false);
//context.closePath();
context.fillStyle = tempColor;
context.fill();
i++;
}
function dragStart(evt) {
dragging = true;
//if (dragging == true) {
dragStartLocation = getCanvasCoordinates(evt);
takeSnapshot();
//}
}
function hitTest(shape,mx,my) {
var dx;
var dy;
dx = mx - shape.x;
dy = my - shape.y;
//a "hit" will be registered if the distance away from the center is less than the radius of the circular object
return (dx*dx + dy*dy < shape.rad*shape.rad);
}
function drag(event) {
var position;
if (dragging === true) {
restoreSnapshot();
position = getCanvasCoordinates(event);
draw(position);
}
}
function dragStop(event) {
dragging = false;
restoreSnapshot();
var position = getCanvasCoordinates(event);
dragEndLocation = getCanvasCoordinates(event);
draw(position);
}
function getRndColor() {
var r = 255 * Math.random() | 0,
g = 255 * Math.random() | 0,
b = 255 * Math.random() | 0;
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
function eraseCanvas() {
context.clearRect(0, 0, canvas.width, canvas.height);
}
addEventListener("load",initiate);
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title></title>
</head>
<body>
<canvas id="canvas" width="1020" height="640"></canvas>
<button onclick="eraseCanvas()" style="float: right;">Reset</button>
</body>
</html>
My question is how to delete the circle when double click on same, I
added 'dblClick' eventListener but still I am only able to perform the 'clearRect'
which will only clear the rectangle from start and end location which is little odd. Another thing I can't change the color to white which will not be valid.point as if my circle overlaps another will look odd.
You can't delete what you draw on the canvas like that. Once it's drawn on the canvas, it stays there and you have no way to access it except to read the pixel data - but that won't solve your problem because you can have overlapping circles of the same color.
To solve the issue, you must keep track of drawn circles, and redraw the full canvas every time it's needed (when adding a new circle, removing an old one, etc.). That way, when you want to delete a circle, you'd simply remove it from the list of circles (a simple array would work). But the important thing is that you need to clear and redraw the full canvas.
Summary: while having your canvas constantly redrawn (either on every tick or when a user interaction happens), your click'n'drag function should only be adding the circle to the circle list (specifying data like x, y, radius, color), while double-clicking a circle would look up that circle in the list, and remove it.
I drawn a circle in canvas and put an image near the border. Now I have absolutely no idea..I want to drag the image around the circle but the top of the arrow image should always be on the border.
For Example: I drag the arrow from the top to the left at nine o'clock. Now the arrow image needs to be rotated 90 degrees.
http://jsfiddle.net/L5twk3ak/1/
canvas = document.getElementById('test');
var context = canvas.getContext("2d");
var points = [];
var radius = 55;
imageBG = new Image();
imageBG.onload = function() {context.drawImage(imageBG, 148, 100, 15, 15);};
imageBG.src = 'https://www.nanamee.com/upload/images/5945/5945_p.jpg';
for(var degree = 0; degree < 360; degree++)
{
var radians = degree * Math.PI / 179;
var x = 150 + radius * Math.cos(radians);
var y = 150 + radius * Math.sin(radians);
points.push({x : x, y : y});
}
context.beginPath();
context.moveTo(points[0].x + 4, points[0].y + 4)
for(var i = 1; i < points.length; i++)
{
var pt = points[i];
context.lineTo(pt.x + 4, pt.y + 4);
}
context.strokeStyle = "black";
context.lineWidth = 1;
context.stroke();
context.closePath();
<canvas id="test" width="400" height="400">Your browser does not support the HTML5 canvas tag.</canvas>
You need to :
Draw your Arc as we're supposed to (unless you have better plans with lineTo() )
calculate the mouse position inside the canvas - on mousemove.
calculate the resultant degree depending on Mouse Position vs. the Arc center.
cache your image for reuse
create draw functions (one for the Arc, the other for drawing the Image after translating the canvas context). That way on (click+drag) mousemove you can simply reuse them to draw your objects into Canvas.
I'll not show you how to implement the click+drag cause it's pretty trivial: you simply need to apply your draw functions if both CLICK+MOUSEMOVE are registered.
Here's the interesting calculations part:
var canvas = document.getElementById('test'); // Store in variable!
var context = canvas.getContext("2d");
var circle = {rad: 55, x:100, y:100}; // Object for ease of use
var img = {src:'//placehold.it/13x13/000', x:0 ,y:0, w:13, h:13};
var arrowImg; // Store for later Image reference
function drawArc(){
context.beginPath();
context.arc(circle.x, circle.y, circle.rad, 0, Math.PI*2, true);
context.strokeStyle = "#000";
context.lineWidth = 1;
context.stroke();
context.closePath();
}
function drawImg( deg ){
context.save(); // save before we mess with ctx translations
context.translate(circle.y, circle.x); // temporarily translate the ctx
// to the Arc center coordinates.
context.rotate(deg*Math.PI/180); // we need Radians so deg*Math.PI/180
context.drawImage(arrowImg, circle.rad-img.w, -img.h/2);
context.restore(); // restore to default
}
function calcDeg(e){ // Retrieve degree from mouse position vs. arc center
var mPos = {
x : e.pageX-canvas.offsetLeft-circle.x,
y : e.pageY-canvas.offsetTop-circle.y
};
var getAtan = Math.atan2(mPos.y, mPos.x);
return getAtan*180/Math.PI;
}
drawArc(); // Draw the ARc
arrowImg = new Image(); // Create Image Obj
arrowImg.onload = function(){ drawImg(-90) }; // onload draw the Image
arrowImg.src = img.src;
canvas.addEventListener("mousemove", function(evt){
canvas.width = canvas.width; // clear the canvas
drawArc(); // Draw Arc
drawImg( calcDeg(evt) ); // Draw Image at the calculated degree
}, false);
canvas{background:#eee;}
<canvas id="test" width="400" height="400">Your browser does not support the HTML5 canvas tag.</canvas>
Not clear? Goog, than ask