Filling canvas shapes with multiple different images in a grid - javascript

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!

Related

Slitting HTML5 canvas (video) element into pieces

I am new with canvas and I've been Googling for a couple of hours, but I am stuck.
What I would like to do is to render a video on a canvas element, divide it and animate the pieces. I am halfway there (see: http://jsbin.com/riduxadazi/edit?html,css,js,console,output ) but I have a couple of questions:
Am I doing things right, or is this extremly inefficient?
I would like to use the video fullscreen. Whatever I try, the canvas grid + video don't seem to match size.
I would like to animate the pieces of the video, but I have no clue how I should address them. Can I get some sort of array and animate the pieces one by one?
My JS looks like this. I tried to add comments to the most important parts. At least what I think were the most important parts ;)
var video = document.getElementById('video'); // Get the video
var ctx = canvas.getContext('2d'),
columns = 6,
rows = 4,
w, h, tileWidth, tileHeight;
// Start video and add it to canvas
video.addEventListener('play', function() {
var $this = this; //cache
(function loop() {
if (!$this.paused && !$this.ended) {
ctx.drawImage($this, 0, 0,window.innerWidth,window.innerHeight);
calcSize(); // Divide video
setTimeout(loop, 1000 / 30); // drawing at 30fps
}
})();
}, 0);
function calcSize() {
video.width = w = window.innerWidth;
video.height = h = window.innerHeight;
tileWidth = w / columns;
tileHeight = h / rows;
ctx.strokeStyle = '#000';
render();
}
function render() {
for(var x = 0; x < columns; x++) {
ctx.moveTo(x * tileWidth, 0);
ctx.lineTo(x * tileWidth, h);
}
for(var y = 0; y < rows; y++) {
ctx.moveTo(0, y * tileHeight);
ctx.lineTo(w, y * tileHeight);
}
ctx.stroke();
}
You would perhaps consider:
Using requestAnimationFrame to update the loop. This allows for perfect synchronization with the monitor update rate as well as being more efficient than setTimeout/setInterval You could throttle it so you only update per 1/30 frame to match video rate by using a simple boolean flag that alternates.
The video element does not need to be inserted into DOM. Also, the actual video bitmap size is read through the properties videoWidth and videoHeight, though, in the provided code you should use canvas' properties width and height as this determine the destination size. To draw proportional you can for example use this answer.
Using drawImage() using the clipping parameters would be the more efficient way to draw video onto canvas if you want to split the content.
You could split your video using a mathematical approach (see this answer) or using objects which allows you to define source regions and have individual properties on it such as position, rotation, scale and so forth. In case you would have to consider destination position to adopt to the current size of canvas.

draw outer and inner border around any canvas shape

How to draw outer and inner border around any canvas shape?
I'm drawing several stroke-only shapes on an html canvas, and I would like to draw an inner and outer border around them.
draft example:
Is there a generic why to do it for any shape (assuming it's a closed stroke-only shape)?
Two methods
There is no inbuilt way to do this and there are two programmatic ways that I use. The first is complicated and involves expanding and contracting the path then drawing along that path. This works for most situations but will fail in complex situation, and the solution has many variables and options to account for these complications and how to handle them.
The better of the two
The second and easiest way that I present below is by using the ctx.globalCompositeOperation setting to mask out what you want drawn or not. As the stroke is drawn along the center and the fill fills up to the center you can draw the stroke at twice the desired width and then either mask in or mask out the inner or outer part.
This does become problematic when you start to create very complex images as the masking (Global Composite Operation) will interfere with what has already been drawn.
To simplify the process you can create a second canvas the same size as the original as a scratch space. You can then draw the shape on he scratch canvas do the masking and then draw the scratch canvas onto the working one.
Though this method is not as fast as computing the expanded or shrunk path, it does not suffer from the ambiguities faced by moving points in the path. Nor does this method create the lines with the correct line join or mitering for the inside or outside edges, for that you must use a the other method. For most purposes the masking it is a good solution.
Below is a demo of the masking method to draw an inner or outer path. If you modify the mask by including drawing a stroke along with the fill you can also set an offset so that the outline or inline will be offset by a number of pixels. I have left that for you. (hint add stroke and set the line width to twice the offset distance when drawing the mask).
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = ( function () {
canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage = function(w,h){
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d");
return image;
}
/** CreateImage.js end **/
// define a shape for demo
var shape = [0.1,0.1,0.9,0.1,0.5,0.5,0.8,0.9,0.1,0.9];
// draws the shape as a stroke
var strokeShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] *w, shape[1] *h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i] * w, shape[i + 1] * h);
}
ctx.closePath();
ctx.stroke();
}
// draws the shape as filled
var fillShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] * w,shape[1] * h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i]*w,shape[i+1]*h);
}
ctx.closePath();
ctx.fill();
}
var drawInOutStroke = function(width,style,where){
// clear the workspace
workCtx.ctx.globalCompositeOperation ="source-over";
workCtx.ctx.clearRect(0, 0, workCtx.width, workCtx.height);
// set the width to double
workCtx.ctx.lineWidth = width*2;
workCtx.ctx.strokeStyle = style;
// fill colour does not matter here as its not seen
workCtx.ctx.fillStyle = "white";
// can use any join type
workCtx.ctx.lineJoin = "round";
// draw the shape outline at double width
strokeShape(workCtx.ctx);
// set comp to in.
// in means leave only pixel that are both in the source and destination
if (where.toLowerCase() === "in") {
workCtx.ctx.globalCompositeOperation ="destination-in";
} else {
// out means only pixels on the destination that are not part of the source
workCtx.ctx.globalCompositeOperation ="destination-out";
}
fillShape(workCtx.ctx);
ctx.drawImage(workCtx, 0, 0);
}
// clear in case of resize
ctx.globalCompositeOperation ="source-over";
ctx.clearRect(0,0,canvas.width,canvas.height);
// create the workspace canvas
var workCtx = createImage(canvas.width, canvas.height);
// draw the outer stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "black", "out");
// draw the inner stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "red", "in");
// draw the shape outline just to highlight the effect
ctx.strokeStyle = "white";
ctx.lineJoin = "round";
ctx.lineWidth = (canvas.width + canvas.height) / 140;
strokeShape(ctx);
};
// run the demo
demo();
// incase fullscreen redraw it all
window.addEventListener("resize",demo)

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/

Draw multiple image's on canvas with each image rotated

I'm new into HTML5 programming and I wanted to know how to rotate each image when it is added into canvas. Should each of them be placed into a canvas and then rotated? If so how can i add multiple canvas into a single canvas context.
Fiddle : http://jsfiddle.net/G7ehG/
Code
function loadImages(sources, callback) {
var images = {};
var loadedImages = 0;
var numImages = 0;
// get num of sources
for(var src in sources) {
numImages++;
}
for(var src in sources) {
images[src] = new Image();
images[src].onload = function() {
if(++loadedImages >= numImages) {
callback(images);
}
};
images[src].src = sources[src];
}
}
var canvas = document.getElementById('myCanvas');
var context = canvas.getContext('2d');
var sources = {
image1: 'http://farm3.static.flickr.com/2666/3686946460_0acfa289fa_m.jpg',
image2: 'http://farm4.static.flickr.com/3611/3686140905_cbf9824a49_m.jpg'
};
loadImages(sources, function(images) {
context.drawImage(images.image1, 100, 30, 200, 137);
context.drawImage(images.image2, 350, 55, 93, 104);
});
In your comment you mentioned that you know about context.rotate, but you don't want the context to stay rotated. That's not a problem at all. First, calling context.rotate only affects things which are drawn afterwards. Anything drawn before will stay were it was. Second, it can be easily reversed after drawing.
use context.save() to create a snapshot of all current context settings, including current rotation.
use context.rotate(angle) and draw your image. The angle is in Radian. That means a full 360° circle is Math.PI * 2. The point the image will be is rotated around is the current origin of the canvas (0:0). When you want to rotate the image around its center, use context.translate(x, y) to set the origin to where you want the center of the image to be, then rotate, and then draw the image at the coordinates -img.width/ 2, -img.height / 2
use context.restore() to return to your snapshot. Rotation and translation will now be like they were before.
Here is an example function which draws an image rotated by 45° at the coordinates 100,100:
function drawRotated(image, context) {
context.save();
context.translate(100, 100);
context.rotate(Math.PI / 4);
context.drawImage(image, -image.width / 2, -image.height / 2);
context.restore();
}

javascript dissapearing object on canvas

I am writing a script to draw an image of the sun, and have an image of the earth orbit around it.
I have defined a planet class as so:
function planet(name, size, rotateRate, startx, starty, colour, scale, oRad, oAng, oSpd){//container class for planets
this.name = name;
this.rotateRate = rotateRate;
this.x = startx;
this.y = starty;
this.colour = colour;
this.scale = scale;
this.size = size;
this.orbitRadius= oRad;
this.orbitAngle = oAng;
this.orbitSpeed = oSpd;
this.drawByArc =
function drawArcCircle(){//draws circles using the arc method
context.save();
context.strokeStyle = '#000000';
context.fillStyle = this.colour;
context.lineWidth=3;
context.beginPath();
context.arc(this.x,this.y,this.size*this.scale,0,Math.PI * 2,false)
context.stroke();
context.fill();
context.closePath();
context.restore();
}
}
Now I have created two instances of the class in the program and drawn them fine using the following functions:
function gameLoop(){//The Game Loop
var thisTime = Date.now();//current time
var deltaTime = thisTime - lastTime;//find difference, not yet used
update();
draw();
lastTime = thisTime;
setTimeout(gameLoop, 1000/60);
}
function draw(){// Draws all the objects
drawBackground();
Sun.drawByArc();
Earth.drawByArc();
}
function update(){// Updates for animation
//var newRotation = this.getCurrantRotation() + (this.getRotationRate()*deltaTime);
var gSegments;
gScale = 4;
simSpeed = 10;
Sun.scale = gScale;
Earth.scale = gScale;
Earth.orbitSpeed = 360/simSpeed;
//Earth.x = Sun.x + Earth.orbitRadius * Math.cos(Earth.orbitAngle * Math.pi / 180);
//Earth.y = Sun.y - Earth.orbitRadius * Math.sin(Earth.orbitAngle * Math.pi / 180);
}
When i have the last two lines of the update method commented out, both circles draw fine, however when i add the final two lines in to attempt to update the earths position in orbit, when i attempt to run the code in chrome the Earth sphere vanishes!
Chrome debugger shows no errors so i'm at a loss as to why it occurs.
EDITED::
Well, I found that thanks to a small typing error (math.pi instead of Math.PI) my planet x and y values were becoming NaN.
however now my earth is stuck at at 90 degree point in its orbit and simply doesnt move, at least it draws, any ideas?
Solved it.
Most of the issues came from using math.pi instead of Math.PI
on top of that, i had not set a value to change the angle of orbit, meaning the orbit always remained at 90, making there be no orbit.
Chrome debug very much helped me in figuring all this out, so thanks a lot user1816548

Categories

Resources