I'm taking photos from user's web camera and drawing it on Canvas. I have built video controls using CSS such as zooming video, rotating it, moving left/right. It's applying on live stream, but When I take photos and draw on Canvas these features (rotate, zoom) don't apply.
I know, because I haven't altered Canvas that's why it isn't applying.
So, any idea how can I draw [rotated, zoomed, moved left/right] on canvas using that same CSS code. (or may be code specific to Canvas context).
Transformation made easy.
Unfortunately the given answer fails to describe the correct usage of scale, translate, and rotate functions. These function multiply the existing transformation and thus the results are relative rather than absolute. For example from the default transformation
ctx.setTransform(1, 0, 0, 1, 0, 0); // set the transformation to the default identity matrix
ctx.scale(2, 2); // scale the transform. Objects are now drawn 2 time larger
ctx.scale(2, 2); // This is applied to the existing scale
// objects are now draw 4 times as large not 2 times
ctx.rotate(Math.PI / 2); // rotate the transformation 90 deg clockwise
// objects are drawn with the x axis down the screen
ctx.rotate(Math.PI / 2); // Rotate a further 90 deg clockwise
// objects are drawn with the x axis from right to left
// and the y axis moves up
The ctx.translate(x, y) function is also relative and is more complicated to work out as the coordinates that you provide are transformed by the existing transformation. So if applied after the above code giving a translation of 100 by 100 would first scale and rotate by 4 and 180 degrees. The resulted position would be at the canvas coordinates x:-400 and y:-400. To translate to the desired coordinates (100, 100) would require first applying the inverse transformation, that would result in ctx.translate(-25, -25)
Because there is no way to know with certainty the current transformation it is very difficult to compute the inverse transformation and apply that so you can work in canvas coordinates.
setTransform
Not all is lost the canvas 2D API provides a function ctx.setTransform() which replaces the current transformation with a new one. It is not relative but absolute. This allows you to know with certainty the current transformation and greatly simplifies the process of transforming an image (or anything being drawn)
A general purpose function.
To rotate zoom and position an image here is a general purpose function to do that for you.
The Arguments
ctx: The 2D canvas context to draw to.
image: The Image to be drawn.
x, y: The absolute canvas coordinates to place the center
coordinates at, measured in pixels from the top left of the canvas.
centerX, centerY: The coordinates of the image origin in pixels relative to the image coordinated. If the image is 100 by 100 then setting centerX, centerY to 50,50 will scale and rotate around the center of the image and draw that center at the coordinates given by the arguments x and y. if centerX, centerY are given as 0,0 then the image is rotated and scaled around the top left corner.
scale: The scale to draw the image. A value of 1 is no scale, 2 is
twice as big 0.5 is half the size. Negative numbers revers the image
in the x and y directions
rotate: The amount of rotation given in radians with 0 being no
rotation and then in 90Deg steps clockwise are Math.PI / 2,
Math.PI, Math.PI * 1.5, and back to the start Math.PI * 2
What it does
The function sets the absolute transformation with the desired scale and translation. The applies the rotation to that transform, then draws the image offset to place the centerX, centerY at the desired coordinates. Finally the function set the transformation back to the default. This is strictly not needed if you use the function or setTransform for all transformations but I have added it to not mess up 80% of existing code that would rely on the default transform being current.
The Function source code.
function drawImage(ctx, image, x, y, centerX, centerY, scale, rotate){
ctx.setTransform(scale, 0, 0, scale, x, y); // resets transform and
// set scale and position
ctx.rotate(rotate); // apply the rotation to the above transformation
ctx.drawImage(image, -centerX, -centerY); // draw the image offset to its center
ctx.setTransform(1, 0, 0, 1, 0, 0); // restore the transformation to default.
}
Or the simpler version that does not do the unneeded reset to the default transform
function drawImage(ctx, image, x, y, centerX, centerY, scale, rotate){
ctx.setTransform(scale, 0, 0, scale, x, y); // resets transform and
// set scale and position
ctx.rotate(rotate); // apply the rotation to the above transformation
ctx.drawImage(image, -centerX, -centerY); // draw the image offset to its center
}
Or this that assumes you always use the image center
function drawImageCentered(ctx, image, x, y, scale, rotate){
ctx.setTransform(scale, 0, 0, scale, x, y); // resets transform and
// set scale and position
ctx.rotate(rotate); // apply the rotation to the above transformation
ctx.drawImage(image, -image.width / 2, -image.height / 2); // draw the image offset to its center
}
Usage
// image; is a 200 by 200 pixel image
// ctx; is the canvas 2D context
// canvas; is the canvas element
// call the function
drawImage(
ctx, // the context
image, // the image to draw
canvas.width / 2, canvas.height / 2, //draw it at the center of the canvas
image.width / 2, image.height / 2, // at the image center
2, // scale to twice its size
Math.PI / 4 // and rotated clockwise 45 deg
);
You can't do it via CSS, but with some javascript when drawing to the canvas. For zoom, you'll need to handle it semi-manually, using the parameters to drawImage on the context from canvas.getContext(). Basically parameters 2-5 are the (in order), x, y, width and height of area from which you are extracting the image and parameters 6-9 are the same for how it gets put into the image.
So for example, if your base image is 2000x2000 and you are imaging it into a canvas which is 1000x1000:
The following will just draw the whole image into the smaller image.
var ctx = canvas.getContext("2d");
ctx.drawImage(img,0,0,2000,2000,0,0,1000,1000);
While this will draw the middle 1000x1000 of the base image into the canvas (i.e. 2x zoom):
var ctx = canvas.getContext("2d");
ctx.drawImage(img,500,500,1000,1000,0,0,1000,1000);
In the above, it is saying, extract the area from (500,500) to (1500,1500) and "print" it into the canvas.
Rotation is a little more involved, because if you just rotate the image and draw from the origin (0,0) of the canvas, the image will end up off-canvas. You will need to perform a translate on the canvas as well.
var ctx = canvas.getContext("2d");
ctx.rotate(90 * Math.PI/180); //rotate 90 degrees
ctx.translate(0, -canvas.height); //translate down the height of the canvas
OR
var ctx = canvas.getContext("2d");
ctx.rotate(180 * Math.PI/180); //rotate 180 degrees
ctx.translate(-canvas.width,-canvas.height); // translate down and over
OR
var ctx = canvas.getContext("2d");
ctx.rotate(270 * Math.PI/180); //rotate 270 degrees
ctx.translate(-canvas.width,0); // translate down and over
Note that the rotation occurs around the upper-left corner. It all gets more complex if you want to support free rotation.
Edit: As Blindman67 notes correctly in his comment, the transforms apply to the transformation state of the context based on any prior transformation applied. I updated the code examples to clarify that that is the effect of the transformation when applied to a clean context with no transformation previously applied to it.
Related
I have the following crop coordinates relative to this rotated image:
The coordinates are relative to the rotated image's height and width. The rotation origin is the centre of the crop rectangle. The rotation of the crop is known.
My goal here is to use the drawImage call and the crop information above and render the following image:
My approach so far is:
Rotate the image first.
Translate the coordinates shown above to be relative to the container.
Call drawImage with the canvas containing the rotated image as the source and the translated coordinates.
drawImage(rotatedCanvas, rotatedX, rotatedY, cropWidth, cropHeight, 0, 0, width, height)
Issues:
I'm not sure how to find the centre point of the crop rectangle given the
crop information above. Without this, I cannot set the correct
origin for the rotation of the image
Without finding this centre point, I cannot use the following code to rotate the coordinates correctly.
// cx origin x, cy origin y
const rotate = (cx: number, cy: number, x: number, y: number, angle: number) => {
const radians = (Math.PI / 180) * angle;
const cos = Math.cos(radians);
const sin = Math.sin(radians);
const nx = cos * (x - cx) + sin * (y - cy) + cx;
const ny = cos * (y - cy) - sin * (x - cx) + cy;
return { nx, ny };
};
To be honest, I'm not sure if I'm going about this the wrong way completely.
Any advice would be greatly appreciated! Many thanks.
Posting this as an answer so we can make at least some progress here.
I made another image of the situation and assuming that those crop area "coordinates" are percentages of the underlying image width and height, we can use those to calculate the top-left and bottom-right coordinates of the crop rectangle.
If we further assume that the crop region is a square, finding the midpoint is just finding the line between the top-left and bottom-right coordinates of the crop area, and then finding the midpoint of that line.
Looking at the question though, the crop area doesn't seem to be square. In that case, we have couple options:
Option 1
Temporarily rotate the crop area (or the image) so that the "relative rotation" between them is 0. Now we can construct the top-right and bottom-left coordinates of the crop area directly, by using the top-left and bottom-right coordinates we already have, and then just rotate everything back
Option 2
Use the same, whatever method was used to acquire the top-left and bottom-right percentages to acquire the top-right and bottom-left coordinates, find the line between those coordinates as well and then find the intersection point of the two lines you now have to get the crop area midpoint
My image with some calculations
I'm working on a project in which i need to draw an image on a canvas and rectangles also to mention the objects present on that image. Rectangles are drawing correct positions if the image is not scaling to fit to the canvas. But if i scaled the image, Rectangles are not getting scaled to meet the correct positions. I have already gone through similar questions/answers present on stackoverflow, but none of them are working in my case or may be i'm not much educated to understand and implement them in a proper way.
Following are the 2 different snippets:
Without Image Scale (Drawing Rectangles in correct positions) and With Image Scale(Drawing rectangles on incorrect position)
Difference is just with the commented lines.
ctx.drawImage(img, x, y, img.width * scale, img.height * scale);
//ctx.drawImage(img, 0, 0); // not fitting image to canvas fill
and
ctx.strokeRect(c[0]* scale, c[1]* scale, c[2] * scale , c[3] * scale);
//ctx.strokeRect(c[0], c[1], c[2] , c[3]); //drawing rects on correct position
I've looked at many of the rotate canvas/scaling canvas resolved issues, but the solutions didn't really solve this issue, so still unsure how to get it to work correctly with canvas.
There is a vertical fixed dimensions rectangle 100w × 150h, shown as the red border below. When an image (vertical/horizontal/square) is added, and rotated, it should rotate and be scaled correctly within the vertical fixed dimensions rectangle, as shown in the example below.
In the first example, we'll go with a vertical image (Eiffel tower original image at 240w × 400h), this is what it should look like at all four rotation angles:
In the second example, we'll go with a horizontal image (Dog original image at 1280w × 720h), this is what it should look like at all four rotation angles:
What would be the most efficient way to accomplish this using canvas?
(I know css can be used transform: rotate(90deg)and play around with the background size/position properties, but I'm trying to learn how to accomplish the example above using canvas for vertical/horizontal/square images).
Here is a fiddle.
We don't need any of the canvas.width/2-image.width/2 code, so change your onload to simply by using ctx.drawImage(image,0,0, canvas.width, canvas.height). Along with this you can define a global ratio variable that will be used for scaling correctly when you rotate sideways and need to scale upwards:
var ratio = 1;
image.onload=function(){
canvas.width = 100;
canvas.height = 150;
ratio = canvas.height/canvas.width; // We will use this for scaling the image to fit
ctx.drawImage(image,0,0, canvas.width, canvas.height);
}
Now the best way to rotate a image by it's center is to translate the image center to the (0,0) point of the canvas. Then you can rotate and move it back to where it was. This is because when a rotation is applied the canvas (0,0) point is the point of rotation.
function drawRotated(degrees){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.translate(canvas.width/2,canvas.height/2); // Move image center to 0,0
ctx.rotate(degrees*Math.PI/180); // Rotate will go from center
ctx.translate(-canvas.width/2,-canvas.height/2); // Move image back to normal spot
ctx.drawImage(image,0,0, canvas.width, canvas.height)
ctx.restore();
}
With the code so far the normal and 180 degree images look fine. But the sideways ones need to be scaled upwards, to do that add in some logic to detect if the image is flipped to the left or right and then scale by the ratio variable (1.5 in this case).
function drawRotated(degrees){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.translate(canvas.width/2,canvas.height/2);
ctx.rotate(degrees*Math.PI/180);
if((degrees - 90) % 180 == 0) // Is the image sideways?
ctx.scale(ratio, ratio); // Scale it up to fill the canvas
ctx.translate(-canvas.width/2,-canvas.height/2);
ctx.drawImage(image,0,0, canvas.width, canvas.height)
ctx.restore();
}
Updated Fiddle
Update:
The reason that horizontal images look odd is due to two things. Currently the scaling assumes the image needs to be zoomed in when it's sideways, in the event of horizontal images that logic is flipped. Instead we want to zoom in when we are flipped normally or upside-down:
function drawRotated(degrees) {
ctx.clearRect(0,0,canvas.width,canvas.height);
...
if(imgRatio < 1) angleToScale += 90
if(angleToScale % 180 == 0)
ctx.scale(ratio, ratio);
ctx.translate(-canvas.width/2,-canvas.height/2);
...
}
Here we are determining based on if imgRatio < 1 we will claim the image is horizontal. Otherwise it will be vertical. While this is a bit broad of a stroke on claiming vertical vs horizontal, it will work for the purposes assuming we just have vertical or horizontal images.
Although even after these changes something is still off (see this fiddle). This is because when we draw the image we are fitting it to the canvas which is vertical, causing the image to stretch when it's drawn to the canvas.
This can be fixed by changing the location of where we draw the image destination. For horizontal images we want to draw it horizontally:
One note is some changes to the onload method:
var ratio = 0;
var xImgOffset = 0;
var yImgOffset = 0;
image.onload=function(){
canvas.width = 100;
canvas.height = 150;
ratio = canvas.height/canvas.width;
var imgRatio = image.height/image.width;
if(imgRatio < 1) { // Horizonal images set Height then proportionally scale width
var dimDiff = image.height/canvas.width;
image.height = canvas.width; // This keeps in mind that the image
image.width = image.width / dimDiff; // is rotated, which is why width is used
} else { // Verticle images set Height then proportionally scale height
var dimDiff = image.width/canvas.width;
image.width = canvas.width;
image.height = image.height / dimDiff;
}
xImgOffset = -(image.width - canvas.width) / 2;
yImgOffset = -(image.height - canvas.height) / 2;
drawRotated(0);
}
The drawRotated method is called right away to apply scaling changes. Along with that xImgOffset and yImgOffset are the difference in positions between the starting location of a horizontal and vertical canvas size in proportion to the original image dimensions.
Visually this looks something like this:
In the image above we are going to need to draw a horizontal image as the green horizontal rectangle when we draw it in our canvas. For vertical images the image is drawn with the width set to the canvas width and the height scaled proportionally with a offset so the image is centered. Likewise this is the same for horizontal images, we just need to keep in mind that we are drawing this as if the canvas is horizontal initially (See the first figure).
Finally the method as a whole looks like this:
function drawRotated(degrees){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.save();
ctx.translate(canvas.width/2,canvas.height/2);
ctx.rotate(degrees*Math.PI/180);
var angleToScale = degrees - 90;
var imgRatio = image.height/image.width;
if(imgRatio < 1) angleToScale += 90
if(angleToScale % 180 == 0)
ctx.scale(ratio, ratio);
ctx.translate(-canvas.width/2,-canvas.height/2);
ctx.drawImage(image, xImgOffset, yImgOffset, image.width, image.height);
ctx.restore();
}
Updated Fiddle For both horizontal and vertical images with original image ratio and cropping
This is setup to work with any canvas dimension and size.
I currently have a bunch of balls(circles) that are bouncing and colliding with eachother inside a box.
Right now they are currently plain green balls. But I want to use a image for this circles.
How can I do this? Here is my render function.
function render() {
var ball;
context.fillStyle = "#008800";
for (var i = 0; i <balls.length; i++) {
ball = balls[i];
ball.x = ball.nextx;
ball.y = ball.nexty;
context.beginPath();
context.arc(ball.x,ball.y,ball.radius,0,Math.PI*2,true);
context.closePath();
context.fill();
}
Any ideas? Is it possible? If not, is there any other methods to achieve bouncing and colliding balls with images?
You can do this three ways:
Method 1 - use pattern to fill the ball
Define the image you want to use as a pattern:
/// create a pattern
var pattern = context.createPattern(img, 'repeat');
Now you can use the pattern as a fill style instead of the green color:
context.fillStyle = pattern;
However, as patterns are always drawn with basis at the coordinate's origo (default 0, 0) you will need to to translate for each move. Luckily not that much extra code:
/// to move pattern where the ball is
context.translate(x, y);
context.beginPath();
/// and we can utilize that for the ball as well so we draw at 0,0
context.arc(0, 0, radius, 0, Math.PI * 2);
context.closePath();
context.fillStyle = pattern;
context.fill();
Now, depending on how you want the move the balls around you may or may not need to translate back for each time.
Here's an online demo showing this approach.
Method 2 - Clip the image to fit the ball with Path clipping
Instead of pattern we can use drawImage to draw the image. However, the problem is that this will draw a square so unless you are creating a ball shaped image which fits on top of your ball with transparent pixels.
You can therefor use clipping to achieve the same as with the pattern method:
Here only a few more lines are needed:
/// define the ball (we will use its path for clipping)
context.beginPath();
context.arc(x, y, radius, 0, Math.PI * 2);
context.closePath();
/// as we need to remove clip we need to save current satte
context.save();
/// uses current Path to clip the canvas
context.clip();
/// draw the ball which now will be clipped
context.drawImage(img, x - radius, y - radius);
/// remove clipping
context.restore();
Here's an online demo of this approach.
Method 3 - Pre-clip the ball as an image
Make a ball in Photoshop or some similar programs and just draw this as an image instead of drawing an arc which you then fill.
You simply draw the ball instead of creating a Path with arc:
drawImage(ballImage, x - radius, y -radius);
If you need to draw in different sizes simply extend the call to:
drawImage(ballImage, x - radius, y - radius, radius, radius);
If performance is critical this is the way to go as this has better performance than the other two approaches but isn't as flexible as them.
If you need a balance between flexibility and performance the clipping approach appear to be the optimal one (this may vary from browser to browser so test).
Here's an online demo with drawImage
Checkout the drawImage function. This allows you to draw an image at a coordinate point on the canvas. It takes an Image instance as it's first parameter and various other position and cropping values. To quote from the MDN page linked above:
drawImage(image, dx, dy, dw, dh, sx, sy, sw, sh)
image
An element to draw into the context; the specification permits any image element (that is, <img>, <canvas>, and <video>). Some browsers, including Firefox, let you use any arbitrary element.
dx The X coordinate in the destination canvas at which to place the top-left corner of the source image.
dy The Y coordinate in the destination canvas at which to place the top-left corner of the source image.
dw The width to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not
scaled in width when drawn.
dh The height to draw the image in the destination canvas. This allows scaling of the drawn image. If not specified, the image is not
scaled in height when drawn.
sx The X coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
sy The Y coordinate of the top left corner of the sub-rectangle of the source image to draw into the destination context.
sw The width of the sub-rectangle of the source image to draw into the destination context. If not specified, the entire rectangle from
the coordinates specified by sx and sy to the bottom-right corner of
the image is used. If you specify a negative value, the image is
flipped horizontally when drawn.
sh The height of the sub-rectangle of the source image to draw into the destination context. If you specify a negative value, the
image is flipped vertically when drawn.
In your case, you would replace the path drawing function with drawImage.
var img = new Image;
img.onload = function() {
//You have to make sure the image is loaded first
//Begin rendering!
render();
};
img.src = "path/to/your/ball/img.png"
function render() {
var ball;
context.fillStyle = "#008800";
for (var i = 0; i <balls.length; i++) {
ball = balls[i];
ball.x = ball.nextx;
ball.y = ball.nexty;
context.drawImage(img, ball.x - (img.width/2), ball.y - (img.height/2)); //Make x, y the centerpoint
}
}
I've tried to build a scrollable tilemap with the canvas object which automatically calculates it's size relative to the screen resolution and renders the tiles relative to a position on a (bigger) map.
I planned to move the generated tiles and only draw the tiles that need to be drawn new.
So that means I scroll the map, draw the new tiles at the top (for example on moving upwards) and deleting the last tiles on the botton which are now out of the visible canvas.
My problem is:
I don't think that it's very good for the performance to change the position of every tile in the canvas, I think this could be solved using getImageData() and putImageData() but there is still one problem left:
If i just move these tiles and draw new tiles it will always "hop" for 30px (1 tile = 30x30), so is there a simple / performance technically good way to make this with a smooth, linear scroll effect?
Just fill the board wither using drawImage or pattern (the latter require you to use translate to get the tiles in the right position).
drawImage takes new destination size as argument so you can zoom the tiles too.
Here is a pattern implementation I made for some other question, but I assume you should be able to see what goes on in the code:
function fillPattern(img, w, h) {
//draw the tile once
ctx.drawImage(img, 0, 0, w, h);
/// draw horizontal line by cloning
/// already drawn tiles before it
while (w < canvas.width) {
ctx.drawImage(canvas, w, 0);
w *= 2;
}
/// clone vertically, double steps each time
while (h < canvas.height) {
ctx.drawImage(canvas, 0, h);
h *= 2;
}
}
The performance is good as you can see in the implementation this was used for (video wall with live scaling and tiling).
To project this more to what you have - instead of drawing each tile as above you can simply draw the canvas to a new position and fill in the new "gap":
ctx.drawImage(myCanvas, offsetX, offseY);
fillGap();
You could have used clipping with drawImage but the canvas will clip this for new internally so there is no gain in clipping the image in JavaScript as you move part of it outside the canvas.