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.
Related
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.
Let's put some text on a HTML5 <canvas> with
var canvas = document.getElementById('myCanvas'),
ctx = canvas.getContext('2d'),
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.font = '14px sans-serif';
ctx.fillText('Bonjour', 10, 10);
When zooming the canvas on text, one can see pixelation.
Is there a way of zooming on a canvas without having pixelation on text ?
When you fillText on the canvas, it stops being letters and starts being a letter-shaped collection of pixels. When you zoom in on it, the pixels become bigger. That's how a canvas works.
When you want the text to scale as a vector-based font and not as pixels, don't draw them on the canvas. You could create <span> HTML elements instead and place them on top of the canvas using CSS positioning. That way the rendering engine will render the fonts in a higher resolution when you zoom in and they will stay sharp. But anything you draw on the canvas will zoom accordingly.
Alternatively, you could override the browsers zoom feature and create your own zooming algorithm, but this will be some work.
When the user zooms in or out of the window, the window.onresize event handler is triggered. You can use this trigger to adjust the width and the height of the canvas css styling accordingly (not the properties of the canvas. That's the internal rendering resolution. Change the width and height attributes of the style which is the resolution it is scaled to on the website).
Now you effectively disabled the users web browser from resizing the canvas, and also have a place where you can react on the scaling input events. You can use this to adjust the context.scale of your canvas to change the size of everything you draw, including fonts.
Here is an example:
<!DOCTYPE html>
<html>
<head>
<script type="application/javascript">
"use strict"
var canvas;
var context;
function redraw() {
// clears the canvas and draws a text label
context.clearRect(0, 0, context.canvas.width, context.canvas.height);
context.font = "60pt sans-serif";
context.fillText("Hello World!", 100, 100);
}
function adjustSize() {
var width = window.innerWidth;
var height = window.innerHeight;
// resize the canvas to fill the whole screen
var style = canvas.style;
style.width = width + "px";
style.height = height + "px";
// backup the old current scaling factor
context.save();
// change the scaling according to the new zoom factor
context.scale(1000 / width, 1000 / height);
// redraw the canvas
redraw();
// restore the original scaling (important because multiple calls to scale are relative to the current scale factor)
context.restore();
}
window.onload = function() {
canvas = document.getElementById("myCanvas");
context = canvas.getContext("2d");
adjustSize();
}
window.onresize = adjustSize;
</script>
</head>
<body>
<canvas id ="myCanvas" width = 1000 height = 1000 ></canvas>
</body>
</html>
If you only need to scale text you can simply scale the font size.
A couple of notes on that however: fonts, or typefaces, are not just straight forward to scale meaning you will not get a smooth progress. This is because fonts are often optimized for certain sizes so the sizes in between so to speak are a result of the previous and next size. This can make the font look like it's moving around a little when scaled up and is normal and expected.
The approach here uses a simply size scale. If you need an absolute smooth scale for animation purposes you will have to use a very different technique.
The simple way is:
ctx.font = (fontSize * scale).toFixed(0) + 'px sans-serif';
An online demo here.
For animation purposes you would need to do the following:
Render a bigger size to an off-screen canvas which is then used to draw the different sizes
When the difference is too big and you get problems with interpolation you will have to render several of these cached text images at key sizes so you can switch between them when scaling factor exceeds a certain threshold.
In this demo you can see that at small sizes the pixels gets a bit "clumpy" but otherwise is much smoother than a pure text approach.
This is because the browser uses bi-linear interpolation rather than bi-cubic with canvas (this may or may not change in the future) so it's not able to interpolate properly when the difference gets to big (see below for solution with this issue).
The opposite happens at big sizes as the text gets blurry also due to interpolation.
This is where we would have to switch to a smaller (or bigger) cached version which we then scale within a certain range before we again switch.
The demo is simplified to show only a single cached version. You can see halfway through that this works fine. The principle would be in a full solution (sizes being just examples):
(Update Here is a demo of a switched image during scale).
-- Cached image (100px)
-- Draw cached image above scaled based on zoom between 51-100 pixels
-- Cached image (50px) generated from 100px version / 2
-- Draw cached image above scaled based on zoom between 26-50 pixels
-- Cached image (25px) generated from 50px version / 2
-- Draw cached image above scaled based on zoom between 1-25 pixels
Then use a "sweet spot" (which you find by experiment a little) to toggle between the cached versions before drawing them to screen.
var ctx = canvas.getContext('2d'),
scale = 1, /// initial scale
initialFactor = 6, /// fixed reduction scale of cached image
sweetSpot = 1, /// threshold to switch the cached images
/// create two off-screen canvases
ocanvas = document.createElement('canvas'),
octx = ocanvas.getContext('2d'),
ocanvas2 = document.createElement('canvas'),
octx2 = ocanvas2.getContext('2d');
ocanvas.width = 800;
ocanvas.height = 150;
ocanvas2.width = 400; /// 50% here, but maybe 75% in your case
ocanvas2.height = 75; /// experiment to find ideal size..
/// draw a big version of text to first off-screen canvas
octx.textBaseline = 'top';
octx.font = '140px sans-serif';
octx.fillText('Cached text on canvas', 10, 10);
/// draw a reduced version of that to second (50%)
octx2.drawImage(ocanvas, 0, 0, 400, 75);
Now we only need to check the sweet spot value to find out when to switch between these versions:
function draw() {
/// calc dimensions
var w = ocanvas.width / initialFactor * scale,
h = ocanvas.height / initialFactor * scale;
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (scale >= sweetSpot) {
ctx.drawImage(ocanvas, 10, 10, w, h); /// use cached image 1
} else {
ctx.drawImage(ocanvas2, 10, 10, w, h); /// use cached image 2
}
}
So why not just draw the second cached image with a font? You can do that but then you are back to the issue with fonts being optimized for certain sizes and it would generate a small jump when scaling. If you can live with that then use this as it will provide a little better quality (specially at small sizes). If you need smooth animation you will have to reduce a larger cached version in order to keep the size 100% proportional.
You can see this answer on how to get a large image resized without interpolation problems.
Hope this helps.
When the canvas element is resized (via the style changing) I also want to scale the canvas' drawn image as well. I cannot just change the height/width as this causes the canvas to clear itself, so I do:
Create a temporary canvas element
Draw the current canvas' image onto that temporary canvas
Resize the current canvas
Draw the temp canvas' image back to the current canvas but scaled to the new size
This results in some blurring - very noticeable after many resizes (example: when dragging to resize). How would I do this without any blurring?
EDIT: Turning off image smoothing (context.webkitImageSmoothingEnabled = false;) does not fix the problem, it simply makes it redraw it more and more jagged until the image looks nothing like the original after a number of resizes.
Called on resize event:
var tmpCanvas = null;
//Make a temporary canvas
tmpCanvas = document.createElement( "canvas" );
//Set its size to be equal
tmpCanvas.height = originalCanvas.height;
tmpCanvas.width = originalCanvas.width;
//Draw our current canvas onto it
tmpCanvas.getContext( "2d" ).drawImage( originalCanvas, 0, 0 );
//Set new dimensions
originalCanvas.width = originalCanvas.offsetWidth;
originalCanvas.height = originalCanvas.offsetHeight;
var originalContext = originalCanvas.getContext( "2d" );
//Set background and colors
originalContext.fillStyle = "#ffffff";
originalContext.strokeStyle = "#000000";
//Set paintbrush
originalContext.lineWidth = 4;
originalContext.lineCap = "round";
//Fill background as white
originalContext.fillRect( 0, 0, originalCanvas.width, originalCanvas.height );
//We have a saved signature
if ( SignatureCanvas.hasSignature === true )
{
//Draw it back but scaled (results in blurred image)
originalContext.drawImage( tmpCanvas, 0, 0, tmpCanvas.width, tmpCanvas.height, 0, 0, originalCanvas.width, originalCanvas.height );
/**
* This results in a blurred image as well
//Draw it back but scaled
originalContext.scale( originalCanvas.width / tmpCanvas.width, originalCanvas.height / tmpCanvas.height );
originalContext.drawImage( tmpCanvas, 0, 0, tmpCanvas.width, tmpCanvas.height, 0, 0, tmpCanvas.width, tmpCanvas.height );
*/
}
Is there a way to get the strokes and "scale" all those points and redraw?
Instead of taking the rendered image from the original canvas, actually redraw the image. By that, I mean execute the same logic you executed against the original canvas, but with the points involved scaled to the new size.
If you can, think about using SVG instead. It scales well by its nature.
Edit: Another option I've thought of is to simply use a gigantic canvas to start with. Sizing down tends to look better than sizing up, especially with smoothing on.
Edit II: The original answer was irrelevant, though the comment I had made is relevant, and am now promoting it and editing it to be an answer...and the answer I had given was not all that great anyway **.
Of course if you scale up raster graphics, that is, from an image with a smaller pixel dimensions of pixels, create an image with higher pixel dimensions, you are going to get blurred images. By scaling up, you're making a low resolution picture high resolution, but without the high resolution details.
There's absolutely no way around that blurriness unless you make multiple additional assumptions about your raster image like the only gray you'd see is at an image edge, or corners can only occur at apparent inflection points where the angle between the tangents of the joined curves must be 100 degrees or less. Essentially, you'd have to give additional information so that your higher resolution image can have detail "filled in". It's not all that terribly different from reverse engineering an SVG from a raster.
So, you appear to want to emulate is scaling vector graphics, in which the only solution is to save the commands, draw a SVG, or draw to a bigger canvas like Stuart Branham suggested.
** I had originally proposed that invoking drawImage would distort the pixels even if it were not scaled, and that it would be better to work with the actual pixel data. If that's true, I can't find proof, but that's irrelevant, as he wanted his image scaled up, without blurring...which is impossible, as I just mentioned.
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
}
}
Say I drew a rectangle on the canvas. Surely there is some sort of built in method to get the XY coordinates, and dimensions of that rectangle? But after some googling I came up with nothing. And just to clarify, I am not talking about the coordinates of the canvas element itself, but rather a shape/image that is drawn unto the canvas.
Any help is appreciated.
If you're talking about a 2D canvas drawing, then the drawing maps 1:1 with screen coordinates, so it is just location of <canvas> + location of the drawing.
To clarify, drawing on a <canvas> basically just changes the pixels of the canvas - after you draw to it, you can't reference the drawn object the same way you can reference an html element.
Canvas is 2D table (Array) of numbers (= pixels = colors). When drawing into canvas, you are just editing this table. When you draw into canvas (= change numbers in table), what should be the coordinates of your adjustment?
If you are drawing rectangles only and you can define the coordinates for your rectangle, you must know your coordinates inside a program, because you have just drawn it.
If you want your image to be separated into some "objects" (shapes), you should use SVG.
Basically, you should be using canvas as a means to output graphics to the screen and the rest of your logic goes straight into the JavaScript that powers your game/application. The best way to go about making something like this is to create objects and assign properties to them; in its simplest form that can look like this:
function Player(x, y)
{
this.x = x;
this.y = y;
}
var examplePlayerObject = new Player(20, 20);
By extending this object via prototyping you can create multiple copies of an object that has the exact same functions; such as draw. Drawing the player in this instance could just be a red square that is 20px*20px.
Player.prototype.draw = function()
{
context.clearRect(0, 0, canvas.width, canvas.height);
context.fillStyle = 'red';
context.fillRect(this.x, this.y, 20, 20);
}
Then, you should have an update step with some means of clearing what is on the screen and redrawing the parts which have changed.
function animationStep()
{
examplePlayerObject.x++;
examplePlayerObject.y++;
examplePlayerObject.draw();
}
This animation step should run each frame; look into requestAnimationFrame for smooth animation. Paul Irish has a good shim for older browsers. Add in requestAnimationFrame(animationStep) at the end of that function and you will have a red square moving slowly across the screen. Good luck!