How to improve javascript canvas pattern performance - javascript

I'm creating a 2D html5 game using canvas. I'm currently making the map who is very large (25000px x 25000px). I'm now trying to add textures to map shapes
Here is the code :
let snowPattern;
let sandPattern;
let junglePattern;
const jungleTexture = new Image();
jungleTexture.src = "./assets/game/map/jungleTexture.png";
jungleTexture.onload = function() {
junglePattern = ctx.createPattern(jungleTexture, "repeat");
};
const snowTexture = new Image();
snowTexture.src = "./assets/game/map/snowTexture.png";
snowTexture.onload = function() {
snowPattern = ctx.createPattern(snowTexture, "repeat");
};
const sandTexture = new Image();
sandTexture.src = "./assets/game/map/sandTexture.png";
sandTexture.onload = function() {
sandPattern = ctx.createPattern(sandTexture, "repeat");
};
//Function to draw map shapes
function animate() {
mapX = document.documentElement.clientWidth / 2 - camera.x * zoom;
mapY = document.documentElement.clientHeight / 2 - camera.y * zoom;
ctx.setTransform(1, 0, 0, 1, mapX, mapY);
//Arctic
if (document.getElementById("3").checked === true) {
ctx.fillStyle = snowPattern;
}
else {
ctx.fillStyle = "#EFF4F6";
}
ctx.fillRect(0, 0, 12500 * zoom, 12500 * zoom);
//Desert
if (document.getElementById("3").checked === true) {
ctx.fillStyle = sandPattern;
}
else {
ctx.fillStyle = "#E5C068";
}
ctx.fillRect(12499 * zoom, 0 * zoom, 12500 * zoom, 12500 * zoom);
//Jungle
if (document.getElementById("3").checked === true) {
ctx.fillStyle = junglePattern;
}
else {
ctx.fillStyle = "#0F7F2A";
}
ctx.fillRect(0, 12500 * zoom, 25000 * zoom, 12500 * zoom);
window.requestAnimationFrame(animate);
}
animate();
So when I only put colors on the background of the shapes, it's working perfectly (constent 144fps), but with patterns, my fps decrease to 20.
Does anyone have an idea about how can I improve the performances ?

You are trying to draw a massive rectangle and it creates an overhead which is expected. It depends on the browser but canvas has some limits. When you reach those limits, you will suffer performance or crash. You can see the limits here Also drawing a huge rectangle will always end with poor performance.
My suggestion would be: draw a smaller rectangle (probably a bit bigger than your game screen) and move it till end of the rectangle and just before pattern ends, move it back again.

Finally, the problem wasnt coming from the size, even with a 50px x 50px pixel rect, the performance was terrible. You need to use ctx.beginPath(), ctx.rect, ctx.closePath() and ctx.fill() to get normal performances.

Related

Change dimension of canvas depending on selected option

I was working on canvas and came across the Idea of changing dimensions of the cube. So, by using HTML5 Canvas I made up this cube which has two squares joined by the lines to make it look like a cube.
What I want is when I select a cube type from select the cube should automatically change itself depending on the length and width of the selected option. The height remains constant. Like if the I select the cube of 5x5 which is by default a cube but when the I select the option of 5x10 the width(front) should not be changed but the length(side) of the cube should expand, and vice versa if I select 10x5 my max option is 25x15. As you can see the canvas I created below is in pixels, first I need to convert these pixels into centimeters(cm) then centimeters to cubic meters.
The whole cube should be aligned in the fixed canvas area specified.
Here is fiddle
var canvas = document.querySelector('canvas');
canvas.width = 500;
canvas.height = 300;
var contxt = canvas.getContext('2d');
//squares
/*
contxt.fillRect(x, y, widht, height);
*/
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(80, 80, 100, 100);
contxt.rect(120, 40, 100, 100);
if (fillRect) {
contxt.fill();
}
contxt.stroke();
/*Lines
contxt.beginPath();
contxt.moveTo(x, y);
contxt.lineTo(300, 100);
*/
contxt.beginPath();
contxt.moveTo(80, 80);
contxt.lineTo(120, 40);
contxt.moveTo(180, 80);
contxt.lineTo(220, 40);
contxt.moveTo(80, 180);
contxt.lineTo(120, 140);
contxt.moveTo(180, 180);
contxt.lineTo(220, 140);
contxt.stroke();
canvas {
border: 1px solid #000;
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<select>
<option>5x5</option>
<option>5x10</option>
<option>10x5</option>
</select>
<canvas></canvas>
Drawing the cube:
To generate a dynamic cube you would have to listen to an onChange event on the <select> element. Every time the selected option changes you would want to redraw your cube.
To redraw the cube you need to create a renderCube function which should take the new dimensions of the cube and as specified an offset for positioning. In this function you have to clear the previously drawn cube and redraw the new one with the given dimensions and offset.
Adding a transition effect:
As you can not apply css transitions to canvas elements you have to implement the transition yourself. You would have to create an animation function which would calculate the dimensions of the cube in the transition phase and rerender it to the screen on each frame.
An implementation of the resizable cube with a transition effect would be:
(if you prefer here is a fiddle too)
(if you do not need the transition effect check the fiddle before it has been implemented)
var canvas = document.querySelector('canvas');
canvas.width = 320;
canvas.height = 150;
var contxt = canvas.getContext('2d');
var currentHeight = 0, currentWidth = 0, currentDepth = 0, animationId = 0;
function renderCube(height, width, depth, offsetX, offsetY) {
currentHeight = height;
currentWidth = width;
currentDepth = depth;
// Clear possible existing cube
contxt.clearRect(0, 0, canvas.width, canvas.height);
contxt.beginPath();
// Calculate depth, width and height based on given input
depth = (depth * 10 * 0.8) / 2;
width = width * 10;
height = height * 10;
// Draw 2 squares to the canvas
contxt.strokeStyle = 'grey';
var fillRect = false;
contxt.fillStyle = 'rgba(0, 0, 0, 0.2)';
contxt.rect(offsetX, offsetY, width, height);
contxt.rect(offsetX + depth, offsetY - depth, width, height);
if (fillRect) {
contxt.fill();
}
contxt.stroke();
// An array which specifies where to draw the depth lines between the 2 rects
// The offset will be applied while drawing the lines
var depthLineCoordinates = [
// posX, posY, posX2, posY2
[0, 0, depth, -depth],
[width, 0, width + depth, -depth],
[0, height, depth, height - depth],
[width, height, width + depth, height - depth]
];
// Draw the depth lines to the canvas
depthLineCoordinates.forEach(function(element) {
contxt.moveTo(offsetX + element[0], offsetY + element[1]);
contxt.lineTo(offsetX + element[2], offsetY + element[3]);
});
contxt.stroke();
}
// As requested by OP an example of a transition to the cube
// The transitionDuration may be a double which specifies the transition duration in seconds
function renderCubeWithTransistion(height, width, depth, offsetX, offsetY, transitionDuration) {
var fps = 60;
var then = Date.now();
var startTime = then;
var finished = false;
var heightDifference = (height - currentHeight);
var widthDifference = (width - currentWidth);
var depthDifference = (depth - currentDepth);
// Get an "id" for the current animation to prevent multiple animations from running at the same time.
// Only the last recently started animation will be executed.
// If a new one should be run, the last one will get aborted.
var transitionStartMillis = (new Date()).getMilliseconds();
animationId = transitionStartMillis;
function animate() {
// Do not continue rendering the current animation if a new one has been started
if (transitionStartMillis != animationId) return;
// request another frame if animation has not been finished
if (!finished) requestAnimationFrame(animate);
// Control FPS
now = Date.now();
elapsed = now - then;
if (elapsed > (1000 / fps)) {
then = now - (elapsed % (1000 / fps));
// Calculate a linear transition effect
if (parseInt(currentHeight, 0) != parseInt(height, 0)) currentHeight += heightDifference / (transitionDuration * fps);
if (parseInt(currentWidth, 0) != parseInt(width, 0)) currentWidth += widthDifference / (transitionDuration * fps);
if (parseInt(currentDepth, 0) != parseInt(depth, 0)) currentDepth += depthDifference / (transitionDuration * fps);
// Render the cube
renderCube(currentHeight, currentWidth, currentDepth, offsetX, offsetY);
// Check if the current dimensions of the cube are equal to the specified dimensions of the cube
// If they are the same, finish the transition
if (parseInt(currentHeight, 0) === parseInt(height, 0) && parseInt(currentWidth, 0) === parseInt(width, 0) && parseInt(currentDepth, 0) === parseInt(depth, 0)) {
finished = true;
}
}
}
// Start the animation process
animate();
return true;
}
// Draw the cube initially with 5x5
renderCube(5, 5, 5, 80, 70);
// Add the onChange event listener to the select element
var cubeSizeSelector = document.getElementById('cubeSizeSelector');
cubeSizeSelector.onchange = function(e) {
var cubeSize = e.target.value.split('x');
renderCubeWithTransistion(5, parseInt(cubeSize[0], 0), parseInt(cubeSize[1], 0), 80, 70, 0.3);
}
canvas {
border: 1px solid #000;
}
select {
display: block;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"> </script>
<select id="cubeSizeSelector">
<option>5x5</option>
<option>5x10</option>
<option>10x5</option>
</select>
<canvas></canvas>
Drawing an extruded outline. Axonometric
Ideally you would create a generic axonometric renderer that given a floor plan renders the object to the canvas as needed.
You can then link the plan to a selection box and update the view when the selection has changed.
Best as a code example
The example below uses the object renderIsoPlan to render the shape.
Shapes are set via a plan. eg a box has a floor plan [[-1,-1],[1,-1],[1,1],[-1,1]] representing the 4 bottom corners.
The renderIsoPlan has the following properties
canvas The canvas that the shape is rendered to. Will not draw until this is set. renderIsoPlan will create a 2D context which will be the same if you have one already
height How far up the outline is projected.
style Canvas context style object eg {stokeStyle : "red", lineWidth : 2} draws 2 pixel with red lines.
plan Set of points for the floor. Points are moved to center automatically. eg [[0,-1],[1,1],[-1,1]] draws a triangle
scale Scale say no more
rotate Amount to rotate. If not 0 then projection is dimetric else it is trimetric.
centerY in unit size of canvas. ie 0.5 is center
centerX same as centerY
Call renderIsoPlan.refresh to draw
Note that you can not rotate the projection in the question as it visually appears to warp (change shape) thus if rotate is not 0 then a different projection is used.
Note the object is automatically centered around 0,0 use centerX, centerY to center in the view
setTimeout(start,0); // wait till Javascript parsed and executed
requestAnimationFrame(animate); // Animate checked at start so start anim
// named list of shapes
const boxes = {
box1By1 : {
plan : [[-1,-1],[1,-1],[1,1],[-1,1]],
scale : 35,
centerY : 0.75,
},
box1By2 : {
plan : [[-1,-2],[1,-2],[1,2],[-1,2]],
scale : 30,
centerY : 0.7,
},
box2By2 : {
plan : [[-2,-2],[2,-2],[2,2],[-2,2]],
scale : 25,
centerY : 0.7,
},
box2By1 : {
plan : [[-2,-1],[2,-1],[2,1],[-2,1]],
scale : 30,
centerY : 0.7,
},
box1By3 : {
plan : [[-1,-3],[1,-3],[1,3],[-1,3]],
scale : 22,
centerY : 0.67,
},
box1By4 :{
plan : [[-1,-4],[1,-4],[1,4],[-1,4]],
scale : 20,
centerY : 0.63,
},
lShape : {
plan : [[-2,-4],[0,-4],[0,2],[2,2],[2,4],[-2,4]],
scale : 20,
centerY : 0.65,
},
current : null,
}
// Sets the renderIsoPlan object to the current selection
function setShape(){
boxes.current = boxes[boxShape.value];
Object.assign(renderIsoPlan, boxes.current);
if (!animateCheckBox.checked) { renderIsoPlan.refresh() }
}
// When ready this is called
function start(){
renderIsoPlan.canvas = canvas;
renderIsoPlan.height = 2;
setShape();
renderIsoPlan.refresh();
}
// Add event listeners for checkbox and box selection
boxShape.addEventListener("change", setShape );
animateCheckBox.addEventListener("change",()=>{
if (animateCheckBox.checked) {
requestAnimationFrame(animate);
} else {
renderIsoPlan.rotate = 0;
setShape();
}
});
// Renders animated object
function animate(time){
if (animateCheckBox.checked) {
renderIsoPlan.rotate = time / 1000;
renderIsoPlan.refresh();
requestAnimationFrame(animate);
}
}
// Encasulate Axonometric render.
const renderIsoPlan = (() => {
var ctx,canvas,plan,cx,cy,w,h,scale,height, rotate;
height = 50;
scale = 10;
rotate = 0;
const style = {
strokeStyle : "#000",
lineWidth : 1,
lineJoin : "round",
lineCap : "round",
};
const depthScale = (2/3);
// Transforms then projects the point to 2D
function transProjPoint(p) {
const project = rotate !== 0 ? 0 : depthScale;
const xdx = Math.cos(rotate);
const xdy = Math.sin(rotate);
const y = p[0] * xdy + p[1] * xdx;
const x = p[0] * xdx - p[1] * xdy - y * project;
return [x,y * depthScale];
}
// draws the plan
function draw() {
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0,0,w,h);
ctx.setTransform(scale, 0, 0, scale, cx, cy);
var i = plan.length;
ctx.beginPath();
while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
ctx.closePath();
i = plan.length;
ctx.translate(0,-height);
ctx.moveTo(...transProjPoint(plan[--i]))
while(i--){ ctx.lineTo(...transProjPoint(plan[i])) }
ctx.closePath();
i = plan.length;
while(i--){
const [x,y] = transProjPoint(plan[i]);
ctx.moveTo(x,y);
ctx.lineTo(x,y + height);
}
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
// centers the plan view on coordinate 0,0
function centerPlan(plan){
var x = 0, y = 0;
for(const point of plan){
x += point[0];
y += point[1];
}
x /= plan.length;
y /= plan.length;
for(const point of plan){
point[0] -= x;
point[1] -= y;
}
return plan;
}
// Sets the style of the rendering
function setStyle(){
for(const key of Object.keys(style)){
if(ctx[key] !== undefined){
ctx[key] = style[key];
}
}
}
// define the interface
const API = {
// setters allow the use of Object.apply
set canvas(c) {
canvas = c;
ctx = canvas.getContext("2d");
w = canvas.width; // set width and height
h = canvas.height;
cx = w / 2 | 0; // get center
cy = h / 2 | 0; // move center down because plan is extruded up
},
set height(hh) { height = hh },
set style(s) { Object.assign(style,s) },
set plan(points) { plan = centerPlan([...points]) },
set scale(s) { scale = s },
set rotate(r) { rotate = r },
set centerY(c) { cy = c * h },
set centerX(c) { cx = c * w },
// getters not used in the demo
get height() { return height },
get style() { return style },
get plan() { return plan },
get scale() { return scale },
get rotate() { return r },
get centerY() { return cy / h },
get centerX() { return cx / w },
// Call this to refresh the view
refresh(){
if(ctx && plan){
ctx.save();
if(style){ setStyle() }
draw();
ctx.restore();
}
}
}
// return the interface
return API;
})();
canvas { border : 2px solid black; }
<select id="boxShape">
<option value = "box1By1">1 by 1</option>
<option value = "box1By2">1 by 2</option>
<option value = "box2By2">2 by 2</option>
<option value = "box2By1">2 by 1</option>
<option value = "box1By3">1 by 3</option>
<option value = "box1By4">1 by 4</option>
<option value = "lShape">L shape</option>
</select>
<input type="checkBox" id="animateCheckBox" checked=true>Animate</input><br>
<canvas id="canvas"></canvas>

Efficient HTML5 Canvas Glow Effect without shape

I am creating a game using the HTML5 Canvas element, and as one of the visual effects I would like to create a glow (like a light) effect. Previously for glow effects I found solutions involving creating shadows of shapes, but these require a solid shape or object to cast the shadow. What I am looking for is a way to create something like an ambient light glow with a source location but no object at the position.
Something I have thought of was to define a centerpoint x and y and create hundreds of concentric circles, each 1px larger than the last and each with a very low opacity, so that together they create a solid center and a transparent edge. However, this is very computationally heavy and does not seem elegant at all, as the resulting glow looks awkward.
While this is all that I am asking of and I would be more than happy to stop here, bonus points if your solution is A) computationally light, B) modifiable to create a focused direction of light, or even better, C) if there was a way to create an "inverted" light system in which the entire screen is darkened by a mask and the shade is lifted where there is light.
I have done several searches, but none have turned up any particularly illuminating results.
So I'm not quite sure what you want, but I hope the following snippet will help.
Instead of creating a lot of concentric circles, create one radialGradient.
Then you can combine this radial gradient with some blending, and even filters to modify the effect as you wish.
var img = new Image();
img.onload = init;
img.src = "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/car.svg";
var ctx = c.getContext('2d');
var gradCtx = c.cloneNode().getContext('2d');
var w, h;
var ratio;
function init() {
w = c.width = gradCtx.canvas.width = img.width;
h = c.height = gradCtx.canvas.height = img.height;
draw(w / 2, h / 2)
updateGradient();
c.onmousemove = throttle(handleMouseMove);
}
function updateGradient() {
var grad = gradCtx.createRadialGradient(w / 2, h / 2, w / 8, w / 2, h / 2, 0);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, 'white');
gradCtx.fillStyle = grad;
gradCtx.filter = "blur(5px)";
gradCtx.fillRect(0, 0, w, h);
}
function handleMouseMove(evt) {
var rect = c.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
draw(x, y);
}
function draw(x, y) {
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(gradCtx.canvas, x - w / 2, y - h / 2);
ctx.globalCompositeOperation = 'lighten';
ctx.fillRect(0, 0, w, h);
}
function throttle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function() { // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
<canvas id="c"></canvas>

Filling canvas shapes with multiple different images in a grid

I am creating a hexagonal grid using canvas, and I'm trying to fill each tile with a specific pattern from an image. The following code is what I'm working with.
The end result is a hexagonal grid that has tiles patterned all with the same image... and it shouldn't be. I think what is happening is that it's creating an overlay for the pattern for every tile, but that image basically covers all tiles... and I only ever end up seeing that last image called.
I was under the impression that my fill() was only filling that small hexagon shape... and not all of them. How can I make this so each individual hex shape can have it's own image?
This code is run in a for loop to create the grid. This is my drawHex() method. I can't imagine that I need a whole new canvas for each tile to make this happen.
var numberOfSides = 6,
size = hex.properties.radius,
Xcenter = hexObj.x + (hex.properties.width / 2),
Ycenter = hexObj.y + (hex.properties.height / 2);
var img = new Image();
if (hexObj.t == "grassland"){
img.src = "/static/grass.jpg";
}else{
img.src = "/static/mountain.jpg";
}
var pattern = context.createPattern(img, "repeat");
context.fillStyle = pattern;
context.beginPath();
context.moveTo (Xcenter + size * Math.cos(0), Ycenter + size * Math.sin(0));
for (var i = 1; i <= numberOfSides;i += 1) {
context.lineTo (Xcenter + size * Math.cos(i * 2 * Math.PI / numberOfSides), Ycenter + size * Math.sin(i * 2 * Math.PI / numberOfSides));
}
context.fill();
context.closePath();
context.stroke();
First important thing is to understand how image loading is working. This is an asynchronous operation which means that you have to wait for the image to load before continue.
This sort of will dictate the strategy as setting image source each time you want to change an image will also force you to handle the asynchronous aspect of it will the delays and so forth. Also, if the image(s) hasn't been loading at the time of creating a pattern the style will fail to render.
So a better approach is to preload all the images (or a single sprite-sheet) that you will use. Then hold on to them in memory (this is usually not a problem today if the images aren't gigantic).
Bundling tile
You can then store several CanvasPattern objects referencing each image. One way is to build a tile object holding all information about that tile incl. its pattern.
For example:
function Tile(ctx, x, y, radius, img) {
this.pattern = ctx.createPattern(img, "repeat");
// store other properties here like x, y, radius etc.
}
Tile.prototype.render = function(ctx) {
ctx.beginPath();
// create hex shape here
ctx.fillStyle = this.pattern;
ctx.fill();
}
Now you can create a Tile object and hold it in an array (or a mother object):
var tiles = [];
tiles.push(new Tile(ctx, x1, y1, radius, img1); // img1 has loaded (onload)
tiles.push(new Tile(ctx, x2, y2, radius, img2); // img2 has also loaded (onload)
// etc.
Then simply render them at once like:
tiles.forEach(function(tile) { tile.render(ctx) });
Loading images
You will need an image loader to load all images. The drawback is that the user has to wait for the images to load unless you have a front that can occupy the user meanwhile the images are loading in the background.
A loader doesn't have to be complicated, but for production you would want to handle errors (onerror/onabort). This example will call the function start() when all images has loaded:
var images = [];
var urls = ["//image1.jpg", "//image2.jpg", ...];
var count = urls.length;
function handler() {
if (!--count) start();
}
urls.forEach(function(url) {
var img = new Image;
images.push(img);
img.onload = handler;
//img.onerror = ... // handler for error and abort here
//img.onabort = ...
img.src = url;
})
A small demo
var images = [], urls = ["//i.imgur.com/DAg71N5.jpg?1", "//i.imgur.com/ZO3XQpj.jpg?1"],
tiles = [], count = urls.length, ctx = c.getContext("2d");
function handler() {if (!--count) start()}
function Tile(ctx, x, y, radius, img) {
this.pattern = ctx.createPattern(img, "repeat");
this.x = x;
this.y = y;
this.radius = radius;
}
Tile.prototype.render = function(ctx) {
ctx.beginPath();
for(var i = 0; i < Math.PI*2; i += Math.PI/3)
ctx.lineTo(this.x + Math.cos(i) * this.radius, this.y + Math.sin(i) * this.radius);
ctx.fillStyle = this.pattern;
ctx.fill();
}
urls.forEach(function(url) {
var img = new Image;
images.push(img);
img.onload = handler;
img.src = url;
});
function start() {
tiles.push(new Tile(ctx, 50, 50, 50, images[0]));
tiles.push(new Tile(ctx, 130, 95, 50, images[1]));
tiles.forEach(function(tile) { tile.render(ctx) });
}
<canvas id=c></canvas>
Tips
Hex shape can be drawn this way:
for(var i = 0; i < Math.PI*2; i += Math.PI/3)
ctx.lineTo(this.x + Math.cos(i) * this.radius, this.y + Math.sin(i) * this.radius);
Note that this requires beginPath(). This allows us to pass on the moveTo() as the first lineTo() on the new path will move the path-cursor to its start point.
To keep patterns relative to the shape you can use translate() on the context before drawing them relative to (0,0) which also simplify the hex drawing:
ctx.translate(this.x, this.y);
ctx.moveTo(this.radius, 0);
for(var i = 0; i < Math.PI*2; i += Math.PI/3)
ctx.lineTo(Math.cos(i) * this.radius, Math.sin(i) * this.radius);
// cancel transforms here if needed
In newer browsers you can use setTransform() on the pattern itself. This is not supported in all browsers so be careful..
Mini-update For reuse purposes you can consider creating a pattern outside the object as part of the loading process, so that you only use a reference to the pattern for each tile.
The specs are a little unclear on what patterns do to images copy-wise. The only requirement is that changes to the image source must not affect the pattern after it has been created, which may or may not mean the image is copied internally, always or just when the condition for it demands it:
Modifying the image used when creating a CanvasPattern object after
calling the createPattern() method must not affect the pattern(s)
rendered by the CanvasPattern object.
In any case, there should be enough meat in the examples above to give you a basis and understanding to how to attack the problem. Modify as needed!

Black resized canvas not completely fading drawings to black over time

I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.
Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?
Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 150),
2, 2);
context.fillStyle = 'rgba(0,0,0,.02)';
context.fillRect(0, 0, 300, 150);
setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);
The problem is two-parted:
There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.
Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.
To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.
Update:
The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.
Online demo here
A simple spec object can look like this:
function Spec(ctx, speed) {
var me = this;
reset(); /// initialize object
this.update = function() {
ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing
this.alpha -= speed; /// update alpha
if (this.alpha <= 0) reset(); /// if black then reset again
/// draw the spec
ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
ctx.fillRect(me.x, me.y, 1, 1);
}
function reset() {
me.x = (ctx.canvas.width * Math.random())|0; /// random x rounded to int
me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int
if (me.alpha) { /// reset alpha
me.alpha = 1.0; /// set to 1 if existed
} else {
me.alpha = Math.random(); /// use random if not
}
}
}
Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.
The next step then is to generate a number of points:
/// create 100 specs with random speed
var i = 100, specs = [];
while(i--) {
specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}
Instead of messing with FPS you simply use the speed which can be set individually per spec.
Now it's simply a matter of updating each object in a loop:
function loop() {
/// iterate each object
var i = specs.length - 1;
while(i--) {
specs[i].update(); /// update each object
}
requestAnimationFrame(loop); /// loop synced to monitor
}
As you can see performance is not an issue and there is no residue left. Hope this helps.
I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:
EDIT
You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";
var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');
canvas2.width=canvas2.height=canvas.width;
window.draw = function(){
var W = canvas2.width;
var H = canvas2.height;
context2.fillStyle='rgba(255,255,255,1)';
context2.fillRect(
Math.floor(Math.random()*W),
Math.floor(Math.random()*H),
2,2);
context2.fillStyle='rgba(0,0,0,.02)';
context2.fillRect(0,0,W,H);
context.fillStyle='rgba(0,0,0,1)';
context.fillRect(0,0,300,150);
context.drawImage(canvas2,0,0,300,150);
setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);
window.thresholdFilter = function () {
var W = canvas2.width;
var H = canvas2.height;
var i, j, threshold = 30, rgb = []
, imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;
for (i = 0; i < Npixels; i += 4) {
rgb[0] = imgData.data[i];
rgb[1] = imgData.data[i+1];
rgb[2] = imgData.data[i+2];
if ( rgb[0] < threshold &&
rgb[1] < threshold &&
rgb[2] < threshold
) {
imgData.data[i] = 0;
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
}
}
context2.putImageData(imgData,0,0);
};
setInterval("thresholdFilter()", 1000);
Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/
To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 300),
2, 2);
setTimeout('draw()', 1000 / 20);
}
window.fadeToBlack = function () {
context.fillStyle = 'rgba(0,0,0,.1)';
context.fillRect(0, 0, 300, 300);
setTimeout('fadeToBlack()', 1000 / 4);
}
draw();
fadeToBlack();
Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/

How to flip images horizontally with HTML5

In IE, I can use:
<img src="http://example.com/image.png" style="filter:FlipH">
to implement an image flip horizontally.
Is there any way to flip horizontally in HTML5? (maybe by using canvas?)
thanks all :)
canvas = document.createElement('canvas');
canvasContext = canvas.getContext('2d');
canvasContext.translate(width, 0);
canvasContext.scale(-1, 1);
canvasContext.drawImage(image, 0, 0);
Here's a snippet from a sprite object being used for testing and it produces the results you seem to expect.
Here's another site with more details. http://andrew.hedges.name/widgets/dev/
You don't need HTML5, it can be done with CSS same as in IE:
-moz-transform: scale(-1, 1);
-webkit-transform: scale(-1, 1);
-o-transform: scale(-1, 1);
transform: scale(-1, 1);
filter: FlipH;
I like Eschers function above. I have made it a little neater and better. I have added flop (vertically) besides flip. Also a possibility to draw/rotate around the center of the image instead of top left. Finally, the function does not require all arguments. img, x and y are required but the rest are not.
If you were using something like context.drawImage(...), you can now just use drawImage(...) and add the rotate/flip/flop functionality explained here:
function drawImage(img, x, y, width, height, deg, flip, flop, center) {
context.save();
if(typeof width === "undefined") width = img.width;
if(typeof height === "undefined") height = img.height;
if(typeof center === "undefined") center = false;
// Set rotation point to center of image, instead of top/left
if(center) {
x -= width/2;
y -= height/2;
}
// Set the origin to the center of the image
context.translate(x + width/2, y + height/2);
// Rotate the canvas around the origin
var rad = 2 * Math.PI - deg * Math.PI / 180;
context.rotate(rad);
// Flip/flop the canvas
if(flip) flipScale = -1; else flipScale = 1;
if(flop) flopScale = -1; else flopScale = 1;
context.scale(flipScale, flopScale);
// Draw the image
context.drawImage(img, -width/2, -height/2, width, height);
context.restore();
}
Examples:
var myCanvas = document.getElementById("myCanvas");
var context = myCanvas.getContext("2d"); // i use context instead of ctx
var img = document.getElementById("myImage"); // your img reference here!
drawImage(img, 100, 100); // just draw it
drawImage(img, 100, 100, 200, 50); // draw it with width/height specified
drawImage(img, 100, 100, 200, 50, 45); // draw it at 45 degrees
drawImage(img, 100, 100, 200, 50, 0, true); // draw it flipped
drawImage(img, 100, 100, 200, 50, 0, false, true); // draw it flopped
drawImage(img, 100, 100, 200, 50, 0, true, true); // draw it flipflopped
drawImage(img, 100, 100, 200, 50, 45, true, true, true); // draw it flipflopped and 45 degrees rotated around the center of the image :-)
Mirror an image or rendering using the canvas.
Note. This can be done via CSS as well.
Mirroring
Here is a simple utility function that will mirror an image horizontally, vertically or both.
function mirrorImage(ctx, image, x = 0, y = 0, horizontal = false, vertical = false){
ctx.save(); // save the current canvas state
ctx.setTransform(
horizontal ? -1 : 1, 0, // set the direction of x axis
0, vertical ? -1 : 1, // set the direction of y axis
x + (horizontal ? image.width : 0), // set the x origin
y + (vertical ? image.height : 0) // set the y origin
);
ctx.drawImage(image,0,0);
ctx.restore(); // restore the state as it was when this function was called
}
Usage
mirrorImage(ctx, image, 0, 0, true, false); // horizontal mirror
mirrorImage(ctx, image, 0, 0, false, true); // vertical mirror
mirrorImage(ctx, image, 0, 0, true, true); // horizontal and vertical mirror
Drawable image.
Many times you will want to draw on images. I like to call them drawable images. To make an image drawable you convert it to a canvas
To convert an image to canvas.
function makeImageDrawable(image){
if(image.complete){ // ensure the image has loaded
var dImage = document.createElement("canvas"); // create a drawable image
dImage.width = image.naturalWidth; // set the resolution
dImage.height = image.naturalHeight;
dImage.style.width = image.style.width; // set the display size
dImage.style.height = image.style.height;
dImage.ctx = dImage.getContext("2d"); // get drawing API
// and add to image
// for possible later use
dImage.ctx.drawImage(image,0,0);
return dImage;
}
throw new ReferenceError("Image is not complete.");
}
Putting it all together
var dImage = makeImageDrawable(image); // convert DOM img to canvas
mirrorImage(dImage.ctx, dImage, 0, 0, false, true); // vertical flip
image.replaceWith(dImage); // replace the DOM image with the flipped image
More mirrors
If you wish to be able to mirror along an arbitrary line see the answer Mirror along line
One option is to horizontally flip the pixels of images stored in ImageData objects directly, e.g.
function flip_image (canvas) {
var context = canvas.getContext ('2d') ;
var imageData = context.getImageData (0, 0, canvas.width, canvas.height) ;
var imageFlip = new ImageData (canvas.width, canvas.height) ;
var Npel = imageData.data.length / 4 ;
for ( var kPel = 0 ; kPel < Npel ; kPel++ ) {
var kFlip = flip_index (kPel, canvas.width, canvas.height) ;
var offset = 4 * kPel ;
var offsetFlip = 4 * kFlip ;
imageFlip.data[offsetFlip + 0] = imageData.data[offset + 0] ;
imageFlip.data[offsetFlip + 1] = imageData.data[offset + 1] ;
imageFlip.data[offsetFlip + 2] = imageData.data[offset + 2] ;
imageFlip.data[offsetFlip + 3] = imageData.data[offset + 3] ;
}
var canvasFlip = document.createElement('canvas') ;
canvasFlip.setAttribute('width', width) ;
canvasFlip.setAttribute('height', height) ;
canvasFlip.getContext('2d').putImageData(imageFlip, 0, 0) ;
return canvasFlip ;
}
function flip_index (kPel, width, height) {
var i = Math.floor (kPel / width) ;
var j = kPel % width ;
var jFlip = width - j - 1 ;
var kFlip = i * width + jFlip ;
return kFlip ;
}
For anyone stumbling upon this.
If you want to do more complex drawing, the other scale-based answers don't all work. By 'complex' i mean situations where things are more dynamic, like for games.
The problem being that the location is also flipped. So if you want to draw a small image in the top left corner of the canvas and then flip it horizontally, it will relocate to the top right.
The fix is to translate to the center of where you want to draw the image, then scale, then translate back. Like so:
if (flipped) {
ctx.translate(x + width/2, y + width/2);
ctx.scale(-1, 1);
ctx.translate(-(x + width/2), -(y + width/2));
}
ctx.drawImage(img, x, y, width, height);
Here x and y are the location you want to draw the image, and width and height are the width and height you want to draw the image.
I came across this page, and no-one had quite written a function to do what I wanted, so here's mine. It draws scaled, rotated, and flipped images (I used this for rending DOM elements to canvas that have these such transforms applied).
var myCanvas = document.getElementById("myCanvas");
var ctx = myCanvas.getContext("2d");
var img = document.getElementById("myimage.jpg"); //or whatever
var deg = 13; //13 degrees rotation, for example
var flip = "true";
function drawImage(img, x, y, width, height, deg, flip){
//save current context before applying transformations
ctx.save();
//convert degrees to radians
if(flip == "true"){
var rad = deg * Math.PI / 180;
}else{
var rad = 2*Math.PI - deg * Math.PI / 180;
}
//set the origin to the center of the image
ctx.translate(x + width/2, y + height/2);
//rotate the canvas around the origin
ctx.rotate(rad);
if(flip == "true"){
//flip the canvas
ctx.scale(-1,1);
}
//draw the image
ctx.drawImage(img, -width/2, -height/2, width, height);
//restore the canvas
ctx.restore();
}

Categories

Resources