Set allowed drawing area in JavaScript canvas API - javascript

I'm using the JavaScript canvas API for free drawing. I'm stuck at masking the area that is allowed to be drawn on - in my example it should only be the speechbubble area.
I'm using this Vue component: https://github.com/sametaylak/vue-draw/blob/master/src/components/CanvasDraw.vue
draw(event) {
this.drawCursor(event);
if (!this.isDrawing) return;
if (this.tools[this.selectedToolIdx].name === 'Eraser') {
this.canvasContext.globalCompositeOperation = 'destination-out';
} else {
this.canvasContext.globalCompositeOperation = 'source-over';
this.canvasContext.strokeStyle = this.tools[this.selectedToolIdx].color;
}
this.canvasContext.beginPath();
this.canvasContext.moveTo(this.lastX, this.lastY);
this.canvasContext.lineTo(event.offsetX, event.offsetY);
this.canvasContext.stroke();
[this.lastX, this.lastY] = [event.offsetX, event.offsetY];
},
drawCursor(event) {
this.cursorContext.beginPath();
this.cursorContext.ellipse(
event.offsetX, event.offsetY,
this.brushSize, this.brushSize,
Math.PI / 4, 0, 2 * Math.PI
);
this.cursorContext.stroke();
setTimeout(() => {
this.cursorContext.clearRect(0, 0, this.width, this.height);
}, 100);
},

There is a built-in clip() method which sets a path as the clipping region.
var ctx=document.getElementById("cnv").getContext("2d");
ctx.lineWidth=2;
ctx.strokeStyle="red";
ctx.moveTo(0,0);
ctx.lineTo(100,100);
ctx.stroke(); // 1.
ctx.strokeStyle="black";
ctx.beginPath();
ctx.moveTo(10,10);
ctx.lineTo(100,10);
ctx.lineTo(100,60);
ctx.lineTo(30,60);
ctx.lineTo(10,80);
ctx.closePath();
ctx.stroke(); // 2.
ctx.clip(); // 3.
ctx.strokeStyle="green";
ctx.beginPath();
ctx.moveTo(0,100);
ctx.lineTo(100,0);
ctx.stroke(); // 4.
<canvas id="cnv"></canvas>
red line is drawn between 0,0 and 100,100, without clipping
bubble is drawn in black
bubble is set as clipping region
green line is drawn between 0,100 and 100,0, and correctly clipped into the bubble.
In practice you may want to have the clipping region one pixel inside the bubble, so a separate path (which is not stroke()-d, just clip()-ped), so drawing can not modify the bubble itself. If you zoom in now as it is, you will see that the green line actually overdraws the inner pixels of the bubble (linewidth is 2 pixels, and the outer one is "unharmed").

Establishing if a given point belongs to polygon's area is a quite tricky and solved problem in Computer Science.
In this concrete scenario, where you have canvas with above image set as background and 500x300 dimension you don't really need to use ray casting algorithm.
You can for example divide speech bubble area to a rectangle and a triangle, and then using event.offsetX, event.offsetY check if any given point lies inside of any of these two figures.
Code example:
isPointInArea(event) {
const x = event.offsetX;
const y = event.offsetY;
// For rectangle it is straightforward
if (x >= 60 && x <= 325 && y >= 60 && y <= 215) {
return true;
}
/* Since two sides of this triangle are parallel to canvas
It is enough to check y coordinate with one linear function of a third one
in form of y = ax + b */
if(x >= 60 && x <= 120 && y >= 215) {
const boundaryY = -0.81818181818 * x + 313.181818182;
if (y <= boundaryY) {
return true;
}
}
return false;
}
In draw function of CanvasDraw.vue
draw(event) {
if(!this.isPointInArea(event)) {
return;
}
this.drawCursor(event);
if (!this.isDrawing) return;
...
Working example on codesandbox
Result:
Edit:
As pointed out by #tevemadar you can also use simply clip() method of Canvas API. If there is nothing else you want to render (And since it is not a game, so probably that is the case), then you can just execute clip() once and you are all set. Otherwise remember to use save() method (and then of course restore(), so that you can render stuff also outside of speech bubble clipping region.

Related

Issues with circle task in Canvas

I have been given the following task, but I am getting errors that can be seen when the code snippet is run. I would like some help figuring out what exactly I am doing wrong.
Basically, I need to draw a circle, make it so that it moves and changes the direction/color when touching the walls of the screen.
Task: create a Circle class with the following properties:
x - the initial value of the coordinate x
y is the initial value of the y coordinate
radius - values โ€‹โ€‹of width and height
color - fill color Describe the methods:
draw () - marks off on the screen an element that is described by the given properties
setColor (newColor) - Changes the fill color to newColor
move ({x = 0, y = 0}) - moves the captured object by the vector (x, y) - each time period (for example, 100 ms) changes (adds \ subtracts)
to the values โ€‹โ€‹x and y, respectively. When a circle collides with any
edge of the screen it is necessary to realize its mirror reflection
(change the value of the corresponding coordinate of the vector on the
opposite of the value of the sign, and call this method with the new
vector) and generate the collision event, collision, which is captured
at the document level.Hang on this event a handler that will change
the color of the pouring of the circle into another (random) value.
Movement occurs until the stop method is called.
stop () - stops the circle movement
If the Escape button on the keyboard was pressed, the movement should stop.
I created a canvas and set the frame to move. I drew a circle and tried to move it using setInterval(), but it seems like I'm losing the context.
let c = document.getElementById("mycanvas");
let ctx = c.getContext("2d");
let xinc = 1;
let yinc = 1;
class Circle {
constructor(xpos, ypos, radius, color) {
this.xpos = xpos;
this.ypos = ypos;
this.radius = radius;
this.color = color;
}
draw() {
ctx.beginPath();
ctx.arc(this.xpos, this.ypos, this.radius, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
}
move(xpos, ypos) {
ctx.clearRect(0, 0, c.width, c.height);
ctx.beginPath();
this.draw();
xpos += xinc;
ypos += yinc;
console.log(xpos, ypos);
if ((this.xpos > c.width - this.radius) || (this.xpos < 0 + this.radius)) {
xinc = -xinc;
}
if ((this.ypos > c.height - this.radius) || (this.ypos < 0 + this.radius)) {
yinc = -yinc;
}
setInterval(this.move, 10);
//this.draw();
}
}
let circle = new Circle(200, 300, 50, "red");
circle.draw();
circle.move(200, 300);
<canvas id="mycanvas" width="1335" height="650" style="border: 1px solid"> </canvas>
I am just starting to learn events and DOMs, please help me correctly implement this task
You are passing this.move to setInterval with no context - just a function, with no this to call it in. You can pass in this.move.bind(this) to create a bound function. You can also do it once in the constructor: this.move = this.move.bind(this).
Also, the call to beginPath in move seems unnecessary.

JavaScript draw using mouse Canvas with 3 axis

How is it / is it possible to draw using the mouse a canvas using 3 axis(x,y,z).
I know that one can draw a canvas on 2 axis and I have done that successfully.
But I have no idea of how I shall draw it on 3 axis (for example a cube).
Following shows some 2d canvas drawing functionallity
$(canvas).on('mousemove', function(e) {
mousex = parseInt(e.clientX-canvasx);
mousey = parseInt(e.clientY-canvasy);
if(mousedown) {
ctx.beginPath();
if(tooltype=='draw') {
ctx.globalCompositeOperation = 'source-over';
ctx.strokeStyle = 'black';
ctx.lineWidth = 3;
} else {
ctx.globalCompositeOperation = 'destination-out';
ctx.lineWidth = 10;
}
ctx.moveTo(last_mousex,last_mousey);
ctx.lineTo(mousex,mousey);
ctx.lineJoin = ctx.lineCap = 'round';
ctx.stroke();
}
last_mousex = mousex;
last_mousey = mousey;
//Output
$('#output').html('current: '+mousex+', '+mousey+'<br/>last: '+last_mousex+', '+last_mousey+'<br/>mousedown: '+mousedown);
});
The full code https://jsfiddle.net/ArtBIT/kneDX/.
But how can I add a z axis and draw a 3d canvas for instance a cube.
With 2D it is simple, you have the X and Y coordinate of the mouse, and when a mouse button is clicked you can change pixels at that location in the canvas.
3D on the other hand is quite hard. Because of the extra dimension that does not exist on the 2D surface, you need to know how to control the 3D positions. And to make matters worse, with that third dimension comes all kinds of extra's that everyone likes to have: lightning and shadows, effects, focus, etc.
Simple drawing
In its most basic form, (set aside some arithmic) you can flatten the Z axis on the 2D surface with a single division. Suppose that you have a point in 3D which consists of three points on three axis (x3d, y3d, z3d) then you can do:
var x2d = x3d / z3d;
var y2d = y3d / z3d;
If you're new to 3D, you will want to play with this first. Here is a tutorial.
Advanced drawing
For just particles and lines this is rather straightforward, although you might want to use another perspective. But it gets more complicated soon when you use objects and want to rotate them in 3D space. This is why most people rely on an engine like three.js to do the 3D drawing for them.
Control 3D space
When drawing with the mouse, you need to map the 2D mouse movement to 3D for control. For examples, have a look a these 3D GUI's: Microsoft's Paint 3D, Google's Sketchup, and Blender. Note that the more kinds of mappings needs to be implemented (like scaling and other transformations) the more math is required.
Using three.js would help you out. See here: https://jsfiddle.net/bn890dtc/
The core code for drawing the line as your click and drag:
function onMouseMove(evt) {
if (renderer) {
var x = (event.clientX / window.innerWidth) * 2 - 1;
var y = -(event.clientY / window.innerHeight) * 2 + 1;
var z = 0
var vNow = new THREE.Vector3(x, y, z);
vNow.unproject(camera);
splineArray.push(vNow);
}
}
The line
vNow.unproject(camera);
will project your drawing into 3D space.
This function will update the line in 3D space:
function updatePositions() {
var positions = line.geometry.attributes.position.array;
var index = 0;
for ( var i = 0; i < splineArray.length; i ++ ) {
positions[ index ++ ] = splineArray[i].x;
positions[ index ++ ] = splineArray[i].y;
positions[ index ++ ] = splineArray[i].z;
}
}

Can I optimize this canvas draw call that takes up 30% of my memory profiling?

My game has many Laser objects. mx & my represent velocity. I use the following code to draw a line from behind the Laser 2 pixels to ahead of the Laser in the direction it's going 2 pixels.
Removing the first line of the function adjusted the % of the Profiling by ~1% but I don't like the way it looks. I think I could optimize the drawing by sorting by Linewidth but that doesn't appear to get me much.
How else could I optimize this?
Laser.prototype.draw = function(client, context) {
context.lineWidth = Laser.lineWidth;
context.beginPath();
context.moveTo(this.x - this.mx * 2, this.y - this.my * 2);
context.lineTo(this.x + this.mx * 2, this.y + this.my * 2);
context.strokeStyle = this.teamColor;
context.closePath();
context.stroke();
}
Instead of multiplying things by two, why not add them?
E.g.
context.moveTo(this.x - this.mx - this.mx, this.y - this.my - this.my);
context.lineTo(this.x + this.mx + this.mx, this.y + this.my - this.my);
Testing shows that addition is an order of magnitude faster on an imac over multiplication
https://jsfiddle.net/1c85r2pq/
Dont use moveTo or lineTo as they do not use the hardware to render and are very slow. Also your code is drawing the line twice
ctx.beginPath(); // starts a new path
ctx.moveTo(x,y); // sets the start point of a line
ctx.lineTo(xx,yy); // add a line from x,y to xx,yy
// Not needed
ctx.closePath(); // This is not like beginPath
// it is like lineTo and tells the context
// to add a line from the last point xx,yy
// back to the last moveTo which is x,y
This would half the already slow render time.
A quick way to draw lines using bitmaps.
First at the start create an image to hold the bitmap used to draw the line
function createLineSprite(col,width){
var lineSprite = document.createElement("canvas");
var lineSprite.width = 2;
var lineSprite.height = width;
lineSprite.ctx = lineSprite.getContext("2d");
lineSprite.ctx.fillStyle = col;
lineSprite.ctx.fillRect(0,0,2,width);
return lineSprite;
}
var line = createLineSprite("red",4); // create a 4 pixel wide red line sprite
Or you can use an image that you load.
To draw a line you just need to create a transform that points in the direction of the line, and draw that sprite the length of the line.
// draw a line with sprite from x,y,xx,yy
var drawLineSprite = function(sprite,x,y,xx,yy){
var nx = xx-x; // get the vector between the points
var ny = yy-y;
if(nx === 0 && ny === 0){ // nothing to draw
return;
}
var d = Math.hypot(nx,ny); // get the distance. Note IE does not have hypot Edge does
// normalise the vector
nx /= d;
ny /= d;
ctx.setTransform(nx,ny,-ny,nx,x,y); // create the transform with x axis
// along the line and origin at line start x,y
ctx.drawImage(sprite, 0, 0, sprite.width, sprite.height, 0, -sprite.height / 2, d, sprite.height);
}
To draw the line
drawSpriteLine(line,0,0,100,100);
When you are done drawing all the lines you can get the default transform back with
ctx.setTransform(1,0,0,1,0,0);
The sprite can be anything, this allows for very detailed lines and great for game lasers and the like.
If you have many different colours to draw then create one sprite (image) that has many colour on it, then in the line draw function simply draw only the part of the sprite that has the colour you want. You can stretch out a single pixel to any size so you can get many colours on a small bitmap.

Get coordinates of two different places on HTML canvas

Aim of my code:
Draw a small rectangle on a HTML canvas whenever a user clicks the canvas. The rectangle should have a small number representing the number of rectangles made by the user.
The user should be able to connect any two rectangles using a straight line. (Preferably by just pressing left mouse button, and taking the mouse from first rectangle to second rectangle)
Approach and my attempt
As you can see in this jsFiddle , I have been able to achieve the first part of above very well. On clicking on the canvas, a rectangle with a number inside of it is made. But I am really clueless about the second part.
How do I make the user connect any two made rectangles? I want the connection to be made only if a rectangle is there ( So I would need to store coordinates of every rectangle that has been made, that's okay as I can use an array for that ).
Basically, I just want to check if the mousedown was at one place and mouseup at the other.
How do I get these two different coordinates ( one of mousedown and other of mouseup ) , and draw a line between them?
I have given the Fiddle above but still here's my jquery:
$(function () {
var x, y;
var globalCounter = 0;
$('#mycanvas').on("click", function (event) {
x = event.clientX + document.body.scrollLeft + document.documentElement.scrollLeft;
x -= mycanvas.offsetLeft;
y = event.clientY + document.body.scrollTop + document.documentElement.scrollTop;
y -= mycanvas.offsetLeft;
// alert("x:"+x+"y: "+y);
drawRectangle(x, y);
});
function drawRectangle(x, y) {
var acanvas = document.getElementById("mycanvas");
var context = acanvas.getContext("2d");
context.strokeRect(x, y, 25, 25);
globalCounter++;
writeNo(x, y, globalCounter);
}
function writeNo(x, y, n) {
var acanvas = document.getElementById("mycanvas");
var context = acanvas.getContext("2d");
context.font = "bold 14px sans-serif";
context.fillText(n, x + 8, y + 12.5);
}
});
The main question is therefore: connecting the two made rectangles by mousedrag
How do I achieve this?
Thank You.
How about this: http://jsfiddle.net/4jqptynt/4/
Ok, first I did a little refactoring for your code to make things easier. Just stuff like putting the code that gets the canvas coordinates into it's own function, and caching some variables (like the canvas context) in the outer function's scope. Oh, and defining your rectangle dimensions as constants because we'll be using the same numbers in a couple of different places.
As you said, the first thing we need is to keep track of the existing rectangles using an array rects (easy enough to do within drawRectangle). Then we need a function to check if a particular pair of coordinates are within some rectangle:
function inRectangle(x, y) {
for (var i = 0, l = rects.length; i < l; i++) {
if ((x - rects[i].x) <= RECT_X && (y - rects[i].y) <= RECT_Y &&
(x - rects[i].x) >= 0 && (y - rects[i].y) >= 0) {
return i;
}
}
}
where RECT_X & RECT_Y define the sides of the rectangle. If the coordinates do exist within some rectangle then this will return the index of that rectangle within the rects array.
Then it's a case of checking whether or not a mousedown occurred within a rectangle, noting that inRectangle will only return a number if the mousedown event was within a rectangle:
$acanvas.on("mousedown", function (event) {
var coords = getCoords(event),
rect = inRectangle(coords.x, coords.y);
if (typeof rect === "number") {
dragStart = rect + 1;
} else {
drawRectangle(coords.x, coords.y);
}
});
if so, make a note of which rectangle using dragStart, if not draw a rectangle as before.
Then to complete the drag, we need to attach a handler to mouseup:
$acanvas.on("mouseup", function (event) {
if (!dragStart) { return; }
var coords = getCoords(event),
rect = inRectangle(coords.x, coords.y);
if (typeof rect === "number") {
drawConnection(dragStart - 1, rect);
}
dragStart = 0;
});
If no drag was started, then it does nothing. If it's coordinates aren't within a rectangle, then it does nothing but reset dragStart. If however, it is within a rectangle, then it draws a connecting line:
function drawConnection(rect1, rect2) {
context.strokeStyle = "black";
context.lineWidth = 1;
context.beginPath();
context.moveTo(rects[rect1].x + RECT_X/2, rects[rect1].y + RECT_Y/2);
context.lineTo(rects[rect2].x + RECT_X/2, rects[rect2].y + RECT_Y/2);
context.stroke();
context.closePath();
}

How can I interpolate between 2 points when drawing with canvas?

I have a paint-style application that works with touch events.
The JavaScript code is...
var RADIUS = 10;
var ctx = document.querySelector("canvas").getContext("2d");
var getRandomColorFragment = function () {
return Math.floor(Math.random() * 255);
};
document.body.addEventListener("touchstart", function (event) {
ctx.fillStyle = "rgb(" + [getRandomColorFragment(), getRandomColorFragment(), getRandomColorFragment()].join() + ")";
});
document.body.addEventListener("touchmove", function (event) {
// Prevent default behavior of scrolling elements.
event.preventDefault();
// Get a reference to the first touch placed.
var touch = event.touches[0];
// Draw a circle at the location of the finger.
ctx.beginPath();
ctx.arc(touch.pageX - RADIUS, touch.pageY - RADIUS, RADIUS, 0, Math.PI * 2);
ctx.closePath();
ctx.fill();
});
jsFiddle.
You can test this on a platform that doesn't support touch events by using Chrome and opening the Web Inspector's settings and choosing Emulate touch events.
When the finger/pointer moves very fast, it fails to paint the canvas continually like which would be expected (see screenshot above). It seems I can only get the touches' coordinates only as fast as the touchmove event is triggered.
I thought that I could determine if there is a big enough gap between arcs using the Distance Formula (c2 = a2 + b2) and determining if it's larger than the RADIUS constant. This part worked well. The next thing I needed to figure out is how to interpolate between the two points: the previous registered touch's coordinates and the newly registered coordinates'.
So, essentially, I'd like to know how I could interpolate between the two points I have to determine how to fill-in the gap.
You want to draw a line from last mouse position to the new mouse postition. But you are drawing a circle at the new position instead. Why?
Why don't you do it this way?
onTouchStart: ctx.moveTo(x,y);
onTouchMove: ctx.lineTo(x,y);
All interpolation, antialiasing etc. is already included in "lineTo" function. Use ctx.lineCap = "round"; to have round corners.
If you still want to interpolate numbers, here is the function:
function interpolate(a, b, frac) // points A and B, frac between 0 and 1
{
var nx = a.x+(b.x-a.x)*frac;
var ny = a.y+(b.y-a.y)*frac;
return {x:nx, y:ny};
}
interpolate({x:2, y:2}, {x:4,y:4}, 0.5); // returns {x:3, y:3}

Categories

Resources