Draw tilemap only on visible canvas area - optimization - javascript

I just came up with idea on how to draw only the images that are within the canvas are (javascript tilemap game). however not sure if that is optimized enough as I though it would be. Any ideas on how to make it more optimized?
Currently I loop for Y and X using map array and then for every X in Y I use drawImage with position coordinates. I have put an if statement, right before it draws, to check if the current X and Y are within the canvas or not. If it is, it draws the image. Here is a bit of code that can show that and in a moment will give a link to test it.
var mapArray=[
[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,3],
[3,0,1,1,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,0,1,3],
[3,0,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,3],
[3,0,1,1,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,0,1,3],
[3,0,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,3],
[3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,3],
[3,0,0,0,1,1,1,0,0,0,0,0,0,0,1,1,1,0,0,0,0,3],
[3,0,1,1,1,1,0,0,0,0,1,0,1,1,1,1,0,0,0,0,1,3],
[3,0,1,0,0,1,1,1,0,0,0,0,1,0,0,1,1,1,0,0,0,3],
[3,0,0,0,0,1,1,0,0,0,0,0,0,0,0,1,1,0,0,0,0,3],
[3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3]
];
// x= 22
// y= 15
//-----------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------
// DRAW PLAYER
var player = new Object();
player.y = canvas.height/2-40; //player position - middle of canvas - 40
player.x = canvas.width/2-40; //player position - middle of canvas - 40
player.Width = 80;
player.Height = 80;
player_image = new Image();
player_image.src = 'http://sarahkerrigan.biz/wpmtest/1/images/horseright1.png';
function drawPlayer() { // drawing the player
context.beginPath();
context.drawImage(player_image, player.x, player.y, player.Width, player.Height);
context.closePath();
}
//-----------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------
var updateX=(player.x-210); // Starting point of canvas X
var updateY=(player.y-160); // Starting point of canvas Y
var posX=updateX;
var posY=updateY;
//------------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------
//-----------------------------------------------------------------------------------------------------
//DRAW THE MAP AND THE PLAYER
function drawMap() {
var posY = updateY; // new Y coordinates for the map after movement
var grass = new Image();
var stone = new Image();
var black = new Image();
grass.src= 'http://sarahkerrigan.biz/wpmtest/1/images/tile/grass.jpeg';
stone.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/sand.jpeg';
black.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/black.png';
//---------------------------------------------------------
// Draw the map loop
grass.onload = function (){
stone.onload = function (){
black.onload = function (){
for(var i=0; i < mapArray.length; i++){
for(var j=0; j < mapArray[i].length; j++){
//=======================================================================
//CHECK IF X AND Y POSITIONS OF THE TILE ARE WITHIN THE CANVAS
//=======================================================================
if(mapArray[i][j]==0){
if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX){
context.drawImage(grass,posX, posY, 64, 64); // Load image for grass "0"
}
}
if(mapArray[i][j]==1){
if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX){
context.drawImage(stone,posX,posY,64,64); // Load image for stone "1"
}
}
if(mapArray[i][j]==3){
if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX){
context.drawImage(black,posX,posY,64,64); // Load image for black "3"
}
}
//=======================================================================
posX+=64;
}
posY+=64;
posX=updateX; // new X coordinates for the map after movement
//---------------------------------------------------------
drawPlayer(); // Draw the player
}
}
}
}
}
//-----------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
//------------------------------------------------------------------------------------------------------
It looks like it's simple enough and that is why I would like to check if there is anything you can think off that can optimize it more.
Here is a link to the whole thing to test it out:
https://jsfiddle.net/todorpet/cast5aq2/
Also, though about adding clearRect to the non-visible part around the canvas as the images. Should I add this as well ?

600,000fps not going to happen.
There is a major flaw in the code
Line 288
setInterval(gameLoop, 30); // 30 milisec to draw next frame
This creates an interval event that is called every 30 ms. Because this is inside the function gameLoop, you are just creating more and more interval timers.
On the first call game loop is called ~30 times a second, on the next loop you add another interval, so now you have ~60 calls to game loop per second, the next call and you have 90, and now the extra calls start firing and the number of calls to gameLoop start to grow exponentially.
If the gameLoop function ran perfectly then within 1000ms (1 second) you would have ~20000 intervals each creating ~30 calls a second, that's ~600000 frames per second.
Obviously that cant happen, The interval between any two timer events is throttled by the browser, and the time that the game loop function takes to run limits the rate as well.
To fix
You had it correct in the code 'requestAnimationFrame`
function gameLoop(){
playerMovement(); //Check for movements
drawMap(); //Draw the map and the player
/* NEVER use setInterval or setTimeout for animating anything!!! */
//setInterval(gameLoop, 30); // 30 milisec to draw next frame
// use this. It will automatically slow down the frame rate to 30frames
// 20, 15, 10 and so on, per second if your render code is slow.
requestAnimationFrame(gameLoop);
}
To the tile map
Your function as I found it.
function drawMap() {
var posY = updateY; // new Y coordinates for the map after movement
var grass = new Image();
var stone = new Image();
var black = new Image();
grass.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/grass.jpeg';
stone.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/sand.jpeg';
black.src = 'http://sarahkerrigan.biz/wpmtest/1/images/tile/black.png';
grass.onload = function () {
stone.onload = function () {
black.onload = function () {
for (var i = 0; i < mapArray.length; i++) {
for (var j = 0; j < mapArray[i].length; j++) {
if (mapArray[i][j] == 0) {
if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX) {
context.drawImage(grass, posX, posY, 64, 64); // Load image for grass "0"
}
}
if (mapArray[i][j] == 1) {
if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX) {
context.drawImage(stone, posX, posY, 64, 64); // Load image for stone "1"
}
}
if (mapArray[i][j] == 3) {
if (posY > canvasBegY && posY < canvasEndY && posX > canvasBegX && posX < canvasEndX) {
context.drawImage(black, posX, posY, 64, 64); // Load image for black "3"
}
}
posX += 64;
}
posY += 64;
posX = updateX; // new X coordinates for the map after movement
drawPlayer(); // Draw the player
}
}
}
}
}
This is very bad, each frame you create a new set of images, then you add the onload only to the first image, when that image loads you add the onload event to the next image, and then the same for the third.
Not only is this very very slow and resource hungry, it also may not work randomly. Images will not load and fire the onload event in the same order as you create them, If the second image loads before the first its onload event is not set and thus will never fire, same for the third image.
Load once.
To use resources for a game you load them all at the start of the game before any game plays (the loading screen)
As you are using a tile map to reference to images the best is to load the images into an array indexed to match the map.
const imageSrcDir = "http://sarahkerrigan.biz/wpmtest/1/images/tile/"
const tileImages = [];
function loadImages(images) {
images.forEach(image => {
const img = tileImages[image.mapIndex] = new Image();
img.src = imageSrcDir + image.name;
});
}
// load the images and add to the tileImage array
loadImages([
{ name : "grass.jpeg", mapIndex : 0 },
{ name : "stone.jpeg", mapIndex : 1 },
{ name : "black.png", mapIndex : 3 },
]);
Player
You were rendering the player inside the second loop of the map rendering loops. That means you were rendering the player 15 times. No good. You should separate the different parts of the game. Draw the map, then the player as separate functions.
See example of how I deal with the player.
Setup the map.
First flatten the map so you can access it quickly, and I have converted it to a string so it is easier to edit
const testMap = [
"3333333333333333333333",
"3000000000000000000003",
"3000111000000011100003",
"3011110000101111000013",
"3010011100001001110003",
"3000000000000000000003",
"3000111000000011100003",
"3011110000101111000013",
"3010011100001001110003",
"3000000000000000000003",
"3000111000000011100003",
"3011110000101111000013",
"3010011100001001110003",
"3000011000000001100003",
"3333333333333333333333",
];
Function to create a map from the above type map. It get the width and height as well, and converts from string to number. You could use any character to represent different map bits.
function createMap(map){
const newMap = {};
newMap.width = map[0].length;
newMap.height = map.length;
newMap.array = new Uint8Array(newMap.width * newMap.height);
var index = 0;
for(const row of map){
var i = 0;
while(i < row.length){
newMap.array[index++] = Number(row[i++]);
}
}
return newMap;
}
const currentMap = createMap(testMap);
The tiles
You need some info about the tiles
const tileWidth = 64;
const tileHeight = 64;
Position the map
With the flattened map data you can draw the map, You will need to have a map position (the top right corner) that represents the view. You can get that from the player position, which should be in map coordinates.
var playerX = 6; // in tile coordinates 6.5 would be halfway to till 7
var playerY = 2;
var mapX = 0; // the map position so that the player can be seen
var mapY = 0;
// get the map position
function getMapPosition(){
// convert player to pixel pos
var x = playerX * tileWidth;
var y = playerY * tileHeight;
x -= canvas.width / 2; // center on the canvas
y -= canvas.heigth / 2;
mapX = x;
mapY = y;
}
Draw the map
There are some important parts. When you see |0 that is the same as floor. eg x = Math.floor(x) is the same as x = x | 0 or x |= 0 This is much faster than floor.
The canvas 2D renderer has some flaws that you must avoid. When you draw tiles you need to make sure that they are aligned to the canvas pixels, if not you end up will flickering seams between tiles as the map moves.
This is fixed in the line that sets the transform.
ctx.setTransform(1, 0, 0, 1, -mapX | 0, -mapY | 0);
the mapX and mapY are negated and floored. The floor aligns the map to the pixels ensuring there are no seams.
Once this function is called the canvas transform is set to map coordinates. You then draw all other game object at their map coordinates not the canvas coordinates, making drawing objects in the game a lot easier.
function drawMap(map) {
const w = map.width; // get the width of the tile array
const mArray = map.array;
const tx = mapX / tileWidth | 0; // get the top left tile
const ty = mapY / tileHeight | 0;
const tW = (canvas.width / tileWidth | 0) + 2; // get the number of tiles to fit canvas
const tH = (canvas.height / tileHeight | 0) + 2;
// set the location via the transform
// From here on you draw all the game items relative to the map not the canvas
ctx.setTransform(1, 0, 0, 1, -mapX | 0, -mapY | 0);
// Draw the tiles if tile pos is off map draw black tile
for (var y = 0; y < tH; y += 1) {
for (var x = 0; x < tW; x += 1) {
const i = tx + x + (ty + y) * w;
const tileIndex = mArray[i] === undefined ? 3 : mArray[i]; // if outside map draw black tile
ctx.drawImage(tileImages[tileIndex], (tx + x) * tileWidth, (ty + y) * tileHeight);
}
}
}
So that is how to draw a tile map, well one way to draw a tile map.
Example
The snippet below shows it all put together with your character, use arrow keys to move.
There are a few minor changes from the above.
const ctx = canvas.getContext("2d");
const imageSrcDir = "http://sarahkerrigan.biz/wpmtest/1/images/tile/"
const tileImages = [];
requestAnimationFrame(mainLoop); // start it after all code below has run
function mainLoop(){
ctx.setTransform(1,0,0,1,0,0);
//control the player
if(keys.ArrowUp){
player.y -= 0.1;
}
if(keys.ArrowDown){
player.y += 0.1;
}
if(keys.ArrowLeft){
player.x -= 0.1;
}
if(keys.ArrowRight){
player.x += 0.1;
}
// Make sure the player stays on the mapo
if(player.x < 2){ player.x = 2 }
if(player.y < 2){ player.y = 2 }
if(player.x >= currentMap.width-2){ player.x = currentMap.width-2}
if(player.y >= currentMap.height-2){ player.y = currentMap.height-2}
getMapPosition();
drawMap(currentMap);
player.draw();
requestAnimationFrame(mainLoop);
}
function loadImages(images) {
images.forEach(image => {
const img = tileImages[image.mapIndex] = new Image();
img.src = imageSrcDir + image.name;
});
}
// load the images and add to the tileImage array
loadImages([{
name: "grass.jpeg",
mapIndex: 0
},
{
name: "sand.jpeg",
mapIndex: 1
},
{
name: "black.png",
mapIndex: 3
},
]);
const player = {
x: 6,
y: 2,
width: 80,
height: 80,
image: (() => {
const img = new Image();
img.src = "https://sarahkerrigan.biz/wpmtest/1/images/horseright1.png";
return img;
})(),
draw(){
ctx.drawImage(player.image,player.x * tileWidth - player.width / 2, player.y * tileHeight - player.height / 2);
},
};
const testMap = [
"3333333333333333333333",
"3000000000000000000003",
"3000111000000011100003",
"3011110000101111000013",
"3010011100001001110003",
"3000000000000000000003",
"3000111000000011100003",
"3011110000101111000013",
"3010011100001001110003",
"3000000000000000000003",
"3000111000000011100003",
"3011110000101111000013",
"3010011100001001110003",
"3000011000000001100003",
"3333333333333333333333",
];
// function to create a map from the above type map
function createMap(map) {
const newMap = {};
newMap.width = map[0].length;
newMap.height = map.length;
newMap.array = new Uint8Array(newMap.width * newMap.height);
var index = 0;
for (const row of map) {
var i = 0;
while (i < row.length) {
newMap.array[index++] = Number(row[i++]);
}
}
return newMap;
}
const currentMap = createMap(testMap);
const tileWidth = 64;
const tileHeight = 64;
var mapX = 0; // the map position so that the player can be seen
var mapY = 0;
// get the map position
function getMapPosition() {
// convert player to pixel pos
var x = player.x * tileWidth + player.width / 2;
var y = player.y * tileHeight + player.height / 2;
x -= canvas.width / 2; // center on the canvas
y -= canvas.height / 2;
mapX = x;
mapY = y;
}
function drawMap(map) {
const w = map.width; // get the width of the tile array
const mArray = map.array;
const tx = mapX / tileWidth | 0; // get the top left tile
const ty = mapY / tileHeight | 0;
const tW = (canvas.width / tileWidth | 0) + 2; // get the number of tiles to fit canvas
const tH = (canvas.height / tileHeight | 0) + 2;
// set the location via the transform
// From here on you draw all the game items relative to the map not the canvas
ctx.setTransform(1, 0, 0, 1, -mapX | 0, -mapY | 0);
// Draw the tiles if tile pos is off map draw black tile
for (var y = 0; y < tH; y += 1) {
for (var x = 0; x < tW; x += 1) {
const rx = tx + x; // get tile real pos
const ry = ty + y;
var tileIndex;
if(rx < 0 || rx >= w){
tileIndex = 3; // black if off map
}else{
const i = rx + ry * w;
tileIndex = mArray[i] === undefined ? 3 : mArray[i]; // if outside map draw black tile
}
ctx.drawImage(tileImages[tileIndex], rx * tileWidth, ry * tileHeight, tileWidth, tileHeight);
}
}
}
const keys = {
ArrowUp : false,
ArrowDown : false,
ArrowLeft : false,
ArrowRight : false,
};
function keyEvents(e){
if(keys[e.code] !== undefined){
keys[e.code] = e.type === "keydown";
e.preventDefault();
}
}
addEventListener("keyup", keyEvents);
addEventListener("keydown", keyEvents);
window.focus();
Arrow keys to move.
<canvas id="canvas" width="500" height="300"></canvas>

Related

How to avoid repeated?

Good day,
I am generating some circles with colors, sizes and positions. All of this things randomly.
But, my problem is that I do not want them to collide, so that no circle is inside another, not even a little bit.
The logic explained in detail within the code, I would like to know why the failure and why the infinite loop.
The important functions are:
checkSeparation and setPositions
window.addEventListener("load", draw);
function draw() {
var canvas = document.getElementById("balls"), // Get canvas
ctx = canvas.getContext("2d"); // Context
canvas.width = document.body.clientWidth; // Set canvas width
canvas.height = document.documentElement.scrollHeight; // Height
var cW = canvas.width, cH = canvas.height; // Save in vars
ctx.fillStyle = "#fff022"; // Paint background
ctx.fillRect(0, 0, cW, cH); // Coordinates to paint
var arrayOfBalls = createBalls(); // create all balls
setPositions(arrayOfBalls, cW, cH);
arrayOfBalls.forEach(ball => { // iterate balls to draw
ctx.beginPath(); // start the paint
ctx.fillStyle = ball.color;
ctx.arc(ball.x, ball.y, ball.radius, 0, (Math.PI/180) * 360, false); // draw the circle
ctx.fill(); // fill
ctx.closePath(); // end the paint
});
}
function Ball() {
this.x = 0; // x position of Ball
this.y = 0; // y position of Ball
this.radius = Math.floor(Math.random() * ( 30 - 10 + 1) + 10);
this.color = "";
}
Ball.prototype.setColor = function(){
for(var j = 0, hex = "0123456789ABCDEF", max = hex.length,
random, str = ""; j <= 6; j++, random = Math.floor(Math.random() * max), str += hex[random])
this.color = "#" + str;
};
function random(val, min) {
return Math.floor(Math.random() * val + min); // Random number
}
function checkSeparation(value, radius, toCompare) {
var min = value - radius, // Min border of circle
max = value + radius; // Max border of circle
// Why ? e.g => x position of circle + this radius it will be its right edge
for(; min <= max; min++) {
if(toCompare.includes(min)) return false;
/*
Since all the positions previously obtained, I add them to the array, in order to have a reference when verifying the other positions and that they do NOT collide.
Here I check if they collide.
In the range of:
[pos x - its radius, pos x + its radius]
*/
}
return true; // If they never collided, it returns true
}
function createBalls() {
var maxBalls = 50, // number of balls
balls = []; // array of balls
for(var j = 0; j < maxBalls; j++) { // create 50 balls
var newBall = new Ball(); // create ball
newBall.setColor(); // set the ball color
balls.push(newBall); //push the ball to the array of balls
}
return balls; // return all balls to draw later
}
function setPositions(balls, canvasW, canvasH) {
var savedPosX = [], // to save x pos of balls
savedPosY = []; // to save y pos of balls
for(var start = 0, max = balls.length; start < max; start++) {
var current = balls[start], // current ball
randomX = random(canvasW, current.radius), // get random value for x pos
randomY = random(canvasH, current.radius); // get random value for y pos
if(checkSeparation(randomX, current.radius, savedPosX)) {
current.x = randomX; // If it position, along with your radio does not touch another circle, I add the position
} else {
// start--; continue;
console.log("X: The above code causes an infinite loop");
}
if(checkSeparation(randomY, current.radius, savedPosY)) {
current.y = randomY;
} else {
// start--; continue;
console.log("Y: The above code causes an infinite loop");
}
}
}
body,html {
margin: 0; border: 0; padding: 0; overflow: hidden;
}
<canvas id="balls"></canvas>
In your code, you test possible collisions by means of arrays of already used x and y positions, but you never add new positions to these arrays. You also check the x and y coordinates separately, which means you are really testing a collision of a bounding box.
Two circles collide when the distance between their centres is smaller than the sum of their radii, so you could use:
function collides(balls, n, x, y, r) {
for (let i = 0; i < n; i++) {
let ball = balls[i];
let dx = ball.x - x;
let dy = ball.y - y;
let dd = dx*dx + dy*dy;
let rr = r + ball.radius;
if (dd < rr * rr) return true;
}
return false;
}
function setPositions(balls, canvasW, canvasH) {
for (let i = 0, max = balls.length; i < max; i++) {
let ball = balls[i],
r = ball.radius,
maxTries = 20;
ball.x = -canvasW;
ball.y = -canvasH;
for (let tries = 0; tries = maxTries; tries++) {
let x = random(canvasW - 2*r, r),
y = random(canvasH - 2*r, r);
if (!collides(balls, i, x, y, r)) {
ball.x = x;
ball.y = y;
break;
}
}
}
}
This is reasonably fast for 50 balls, but will be slow if you have more balls. In that case, some spatial data structures can speed up the collision search.
You must also guard against the case that no good place can be found. The code above gives up after 20 tries and moves the ball outside the visible canvas. You can improve the chances of placing balls by sorting the balls by radius and plaing the large balls first.
Finally, you add one hex digit too many to your random colour. (That for loop, where everything happens in the loop control is horrible, by the way.)

Moving a sprite to click location and stop there PIXIJS

This simple game makes a sprite move around to the position a user clicks. I got it working that the sprite moves to the location, but I need to make it stop at the click location. This code makes the sprite only stop at the click location when the sprite moves towards the bottom right corner. How do I fix this to make it always stop at the click location?
var Container = PIXI.Container,
autoDetectRenderer = PIXI.autoDetectRenderer,
loader = PIXI.loader,
resources = PIXI.loader.resources,
Sprite = PIXI.Sprite;
var stage = new PIXI.Container(),
renderer = PIXI.autoDetectRenderer(1000, 1000);
document.body.appendChild(renderer.view);
PIXI.loader
.add("animal.png")
.load(setup);
var rocket, state;
function setup() {
//Create the `tileset` sprite from the texture
var texture = PIXI.utils.TextureCache["animal.png"];
//Create a rectangle object that defines the position and
//size of the sub-image you want to extract from the texture
var rectangle = new PIXI.Rectangle(192, 128, 32, 32);
//Tell the texture to use that rectangular section
texture.frame = rectangle;
//Create the sprite from the texture
rocket = new Sprite(texture);
rocket.anchor.x = 0.5;
rocket.anchor.y = 0.5;
rocket.x = 50;
rocket.y = 50;
rocket.vx = 0;
rocket.vy = 0;
//Add the rocket to the stage
stage.addChild(rocket);
document.addEventListener("click", function(){
rocket.clickx = event.clientX;
rocket.clicky = event.clientY;
var x = event.clientX - rocket.x;
var y = event.clientY - rocket.y;
rocket.vmax = 5;
var total = Math.sqrt(x * x + y * y);
var tx = x/total;
var ty = y/total;
rocket.vx = tx*rocket.vmax;
rocket.vy = ty*rocket.vmax;
});
state = play;
gameLoop();
}
function gameLoop() {
//Loop this function at 60 frames per second
requestAnimationFrame(gameLoop);
state();
//Render the stage to see the animation
renderer.render(stage);
}
function play(){
rocket.x += rocket.vx;
rocket.y += rocket.vy;
if(rocket.x >= rocket.clickx){
if(rocket.y >= rocket.clicky){
rocket.x = rocket.clickx;
rocket.y = rocket.clicky;
}
}
}
So your sprite has the velocity 5. Then let's just check out the distance between the sprite and the stop position. Whenever it's less than 5, make it stop at the position.
function play(){
var dx = rocket.x - rocket.clickx;
var dy = rocket.y - rocket.clicky;
if (Math.sqrt(dx * dx + dy * dy) <= 5) {
rocket.x = rocket.clickx;
rocket.y = rocket.clicky;
}
else {
rocket.x += rocket.vx;
rocket.y += rocket.vy;
}
}
You can modify the if statement like below to avoid Math.srqt call.
if ((dx * dx + dy * dy) <= (5 * 5)) {

HTML5 Canvas: Bouncing Balls with Image Overlay

I'm really struggling with a couple problems in the HTML5 canvas.
I've posted the project to GitHub pages (https://swedy13.github.io/) and added an image (the circles are in motion) so you can see the issue. Basically, if you scroll down you'll find several green circles bouncing around on the page. I'd like to replace those with my client logos.
I'm calling requestAnimation from three files based on different actions, all of which can be found in https://github.com/swedy13/swedy13.github.io/tree/master/assets/js
Filenames:
- filters.js (calls requestAnimation when you use the filters)
- main.js (on load and resize)
- portfolio.js (this is where the canvas code is)
Update: I've added the "portfolio.js" code below so the answer can be self-contained.
function runAnimation(width, height, type){
var canvas = document.getElementsByTagName('canvas')[0];
var c = canvas.getContext('2d');
// ---- DIMENSIONS ---- //
// Container
var x = width;
var y = height - 65;
canvas.width = x;
canvas.height = y;
var container = {x: 0 ,y: 0 ,width: x, height: y};
// Portrait Variables
var cPos = 200;
var cMargin = 70;
var cSpeed = 3;
var r = x*.075;
if (y > x && x >= 500) {
cPos = x * (x / y) - 150;
cMargin = 150;
}
// Landscape Variables
if (x > y) {
cPos = y * (y / x) - 50;
cMargin = 150;
cSpeed = 3;
r = x*.05;
}
// ---- CIRCLES ---- //
// Circles
var circles = [];
var img = new Image();
// Gets active post ids and count
var activeName = [];
var activeLogo = [];
var activePosts = $('.active').map(function() {
activeName.push($(this).text().replace(/\s+/g, '-').toLowerCase());
// Returns the image source
/*activeLogo.push($(this).find('img').prop('src'));*/
// Returns an image node
var elem = document.getElementsByClassName($(this).text().replace(/\s+/g, '-').toLowerCase())
activeLogo.push(elem[0].childNodes[0]);
});
// Populates circle data
for (var i = 0; i < $('.active').length; i++) {
circles.push({
id:activeName[i],
r:r,
color: 100,
/*image: activeLogo[i],*/
x:Math.random() * cPos + cMargin,
y:Math.random() * cPos + cMargin,
vx:Math.random() * cSpeed + .25,
vy:Math.random() * cSpeed + .25
});
}
// ---- DRAW ---- //
requestAnimationFrame(draw);
function draw(){
c.fillStyle = 'white';
c.fillRect(container.x, container.y, container.width, container.height);
for (var i = 0; i < circles.length; i++){
/*var img = new Image();
var path = circles[i].image;*/
/*var size = circles[i].r * 2;*/
/*img.src = circles[4].image;*/
var img = activeLogo[i];
img.onload = function (circles) {
/*c.drawImage(img, 0, 0, size, size);*/
var pattern = c.createPattern(this, "repeat");
c.fillStyle = pattern;
c.fill();
};
c.fillStyle = 'hsl(' + circles[i].color + ', 100%, 50%)';
c.beginPath();
c.arc(circles[i].x, circles[i].y, circles[i].r, 0, 2*Math.PI, false);
c.fill();
// If the circle size/position is greater than the canvas width, bounce x
if ((circles[i].x + circles[i].vx + circles[i].r > container.width) || (circles[i].x - circles[i].r + circles[i].vx < container.x)) {
circles[i].vx = -circles[i].vx;
}
// If the circle size/position is greater than the canvas width, bounce y
if ((circles[i].y + circles[i].vy + circles[i].r > container.height) || (circles[i].y - circles[i].r + circles[i].vy < container.y)){
circles[i].vy = -circles[i].vy;
}
// Generates circle motion by adding position and velocity each frame
circles[i].x += circles[i].vx;
circles[i].y += circles[i].vy;
}
requestAnimationFrame(draw);
}
}
The way it works right now is:
1. I have my portfolio content set to "display: none" (eventually it will be a pop-up when they click on one of the circles).
2. The canvas is getting the portfolio objects from the DOM, including the image that I can't get to work.
3. If I use the "onload()" function, I can get the images to show up and repeat in the background. But it's just a static background - the circles are moving above it and revealing the background. That isn't what I want.
So basically, I'm trying to figure out how to attach the background image to the circle (based on the circle ID).
----------------- UPDATE -----------------
I can now clip the image to a circle and get the circle to move in the background. But it isn't visible on the page (I can tell it's moving by console logging it's position). The only time I see anything is when the circle lines up with the images position, then it shows.
function runAnimation(width, height, type){
var canvas = document.getElementsByTagName('canvas')[0];
var c = canvas.getContext("2d");
canvas.width = width;
canvas.height = height;
// Collects portfolio information from the DOM
var activeName = [];
var activeLogo = [];
$('.active').map(function() {
var text = $(this).text().replace(/\s+/g, '-').toLowerCase();
var elem = document.getElementsByClassName(text);
activeName.push(text);
activeLogo.push(elem[0].childNodes[0]);
});
var img = new Image();
img.onload = start;
var circles = [];
var cPos = 200;
var cMargin = 70;
var cSpeed = 3;
for (var i = 0; i < 1; i++) {
circles.push({
id: activeName[i],
img: activeLogo[i],
size: 50,
xPos: Math.random() * cPos + cMargin,
yPos: Math.random() * cPos + cMargin,
xVel: Math.random() * cSpeed + .25,
yVel: Math.random() * cSpeed + .25,
});
img.src = circles[i].img;
}
requestAnimationFrame(start);
function start(){
for (var i = 0; i < circles.length; i++) {
var circle = createImageInCircle(circles[i].img, circles[i].size, circles[i].xPos, circles[i].yPos);
c.drawImage(circle, circles[i].size, circles[i].size);
animateCircle(circles[i]);
}
requestAnimationFrame(start);
}
function createImageInCircle(img, radius, x, y){
var canvas2 = document.createElement('canvas');
var c2 = canvas2.getContext('2d');
canvas2.width = canvas2.height = radius*2;
c2.fillStyle = 'white';
c2.beginPath();
c2.arc(x, y, radius, 0, Math.PI*2);
c2.fill();
c2.globalCompositeOperation = 'source-atop';
c2.drawImage(img, 0, 0, 100, 100);
return(canvas2);
}
function animateCircle(circle) {
// If the circle size/position is greater than the canvas width, bounce x
if ((circle.xPos + circle.xVel + circle.size > canvas.width) || (circle.xPos - circle.size + circle.xVel < 0)) {
console.log('Bounce X');
circle.xVel = -circle.xVel;
}
// If the circle size/position is greater than the canvas width, bounce y
if ((circle.yPos + circle.yVel + circle.size > canvas.height) || (circle.yPos + circle.yVel - circle.size < 0)) {
console.log('Bounce Y');
circle.yVel = -circle.yVel;
}
// Generates circle motion by adding position and velocity each frame
circle.xPos += circle.xVel;
circle.yPos += circle.yVel;
}
}
I'm not sure if I'm animating the correct thing. I've tried animating canvas2, but that didn't make sense to me.
PS - Sorry for the GitHub formatting, not sure why it looks like that.
PPS - Apologies for any junk code I didn't clean up. I've tried a lot of stuff and probably lost track of some of the changes.
PPPS - And forgive me for not making the answer self-contained. I thought linking to GitHub would be more useful, but I've updated the question to contain all the necessary info. Thanks for the feedback.
To get you started...
Here's how to clip an image into a circle using compositing.
The example code creates a single canvas logo-ball that you can reuse for each of your bouncing balls.
var logoball1=dreateImageInCircle(logoImg1,50);
var logoball2=dreateImageInCircle(logoImg2,50);
Then you can draw each logo-ball onto your main canvas like this:
ctx.drawImage(logoball1,35,40);
ctx.drawImage(logoball2,100,75);
There are many examples here on Stackoverflow of how to animate the balls around the canvas so I leave that part to you.
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/m%26m600x455.jpg";
function start(){
var copy=createImageInCircle(img,50);
ctx.drawImage(copy,20,75);
ctx.drawImage(copy,150,120);
ctx.drawImage(copy,280,75);
}
function createImageInCircle(img,radius){
var c=document.createElement('canvas');
var cctx=c.getContext('2d');
c.width=c.height=radius*2;
cctx.beginPath();
cctx.arc(radius,radius,radius,0,Math.PI*2);
cctx.fill();
cctx.globalCompositeOperation='source-atop';
cctx.drawImage(img,radius-img.width/2,radius-img.height/2);
return(c);
}
body{ background-color:white; }
#canvas{border:1px solid red; }
<canvas id="canvas" width=512 height=512></canvas>

Canvas - Draw only when hovering over new tile instead of whole canvas

Let's say I have a canvas that is split into a 15x10 32-pixel checkboard. Thus, I have this:
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
var tileSize = 32;
var xCoord
var yCoord
var tilesX = 15; // tiles across
var tilesY = 10; // tiles up and down
var counted = 1; // for drawing purpose for checkerboard for visual guidance
var mouseSel = new Image()
mouseSel.src = 'http://i.imgur.com/vAA03NB.png' // mouse selection
mouseSel.width = 32
mouseSel.height = 32
function isOdd(num) {
return num % 2;
}
function getMousePos(canvas, evt) {
// super simple stuff here
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
drawCanvas(); // upon intilization... draw
function drawCanvas() {
for (var y = 0; y <= 10; y++) {
for (var x = 0; x <= 15; x++) {
if (isOdd(counted)) {
context.fillStyle = '#dedede'
context.fillRect(x * 32, y * 32, 32, 32);
// checkboard drawn complete.
}
counted++;
} // end first foor loop
counted++;
} // end last for loop
if (counted >= 176) counted = 1 // once all tiles (16x11) are drawn... reset counter for next instance
}
canvas.addEventListener('mousemove', function (evt) {
context.clearRect(0, 0, canvas.width, canvas.height); // clear canvas so mouse isn't stuck
drawCanvas(); // draw checkboard
// get the actual x,y position of 15x10 32-pixel checkboard
var mousePos = getMousePos(canvas, evt);
mousePos.xCoord = Math.floor(mousePos.x / tileSize)
mousePos.yCoord = Math.floor(mousePos.y / tileSize)
// draw the mouse selection
context.drawImage(mouseSel, (mousePos.xCoord * 32), (mousePos.yCoord * 32), 32, 32) // draw mouse selection
// debug
var message = ' (' + mousePos.xCoord + ',' + mousePos.yCoord + ') | (' + mousePos.x + ',' + mousePos.y + ')';
var textarea = document.getElementById('debug');
textarea.scrollTop = textarea.scrollHeight;
$('#debug').append(message + '\n');
}, false);
canvas#canvas {
background: #ABABAB;
position: relative;
z-index: 1;
float: left;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<canvas id="canvas" height="352" width="512" tabindex="0"></canvas>
<textarea name="" id="debug" cols="30" rows="35"></textarea>
**NOTE: ** Make sure to scroll down in that preview pane so you can see the debug textarea.
As you can see, the event of "drawing" fires EVERY single time it moves. That means every pixel.
I am trying to figure out how to make the drawing fire ONLY when a new x,y coord has changed. Because it'd be useless to redraw the mouse selection when it's only moved 5 pixels across and it's still going to be drawn at the same place.
My suggestion
Upon entering, have a temporary value and when that is passed, to redraw again?
Make a temporary value and update that if it was different from before. Then put the code in an if statement where either have changed.
var tempX, tempY;
var newX = 100;
var newY = 100;
tempX = mousePos.xCoord;
tempY = mousePos.yCoord;
if (newX !== tempX || newY !== tempY) {
// code here
}
if (tempX !== newX) newX = mousePos.xCoord;
if (tempY !== newY) newY = mousePos.yCoord;
JSFiddle: http://jsfiddle.net/weka/bvnma354/8/

how to move an image on canvas using keyboard arrow key in html5

I m Drawing the image on canvas with this code
and it successfully draw the image on canvas now i want to move the image on canvas for that i write the code i check that if the right key of my keyboard is pressed i will increment the x coordinate of an image if left key is pressed i will decrement the x coordinate but image is not moving on the canvas
player = new Image();
player.src = "game_character.png";
context.drawImage(player,player.x * wallDim + wallDim ,player.y * wallDim + wallDim ,50,50);
how to move an image on canvas
var handleInput = function(event, keyState) {
switch(event.which) {
case 37: { // Left Arrow
keyDown.arrowLeft = keyState;
break;
}
case 38: { // Up Arrow
keyDown.arrowUp = keyState;
break;
}
case 39: { // Right Arrow
keyDown.arrowRight = keyState;
break;
}
case 40: { // Down Arrow
keyDown.arrowDown = keyState;
break;
}
}
}
/**
* physics
*
* This function contains the basic logic for the maze.
*/
var physics = function() {
console.log("physics ");
console.log("first condition "+keyDown.arrowRight +player.x+1);
if(keyDown.arrowLeft && player.x-1 >= 0 && map[player.y][player.x-1] != 1) {
player.x--;
redraw = true;
}
if(keyDown.arrowUp && player.y-1 >= 0 && map[player.y-1][player.x] != 1) {
player.y--;
redraw = true;
}
if(keyDown.arrowRight && player.x+1 < map[0].length && map[player.y][player.x+1] != 1) {
console.log("arrow right");
player.x++;
redraw = true;
}
if(keyDown.arrowDown && player.y+1 < map.length && map[player.y+1][player.x] != 1) {
player.y++;
redraw = true;
}
if(keyDown.arrowRight && player.x+1 >= map[0].length)
{
player.x++;
document.getElementById("canvas_div").style.display="none";
document.getElementById("end_screen_div").style.display="block";
//alert("completed");
}
}
/**
* draw
*
* This function simply draws the current state of the game.
*/
var draw = function() {
// Don't redraw if nothing has changed
if(!redraw)
return;
context.clearRect(0, 0, cols, rows);
context.beginPath();
// Draw the maze
for(var a = 0; a < rows; a++) {
for(var b = 0; b < cols; b++) {
switch(map[a][b]) {
case C.EMPTY: context.fillStyle = colors.empty; break;
case C.WALL: context.fillStyle = colors.wall; break;
}
context.fillRect(b * wallDim, a * wallDim, wallDim, wallDim); // x, y, width, height
}
}
// Draw the player
/* context.fillStyle = colors.player;
context.arc(
player.x * wallDim + wallDim / 2, // x position
player.y * wallDim + wallDim / 2, // y position
wallDim / 2, // Radius
0, // Starting angle
Math.PI * 2, // Ending angle
true // antiClockwise
);*/
player = new Image();
player.src = "game_character.png";
context.drawImage(player,player.x * wallDim + wallDim ,player.y * wallDim + wallDim ,50,50);
var firstplayer=new Image();
firstplayer.src="top_character01.png";
context.drawImage(firstplayer,680,0,60,60);
var secondplayer= new Image();
secondplayer.src="top_character02.png";
context.drawImage(secondplayer,750,0,60,60);
context.fill();
context.closePath();
redraw = false;
}
In your draw method, you reinitialize the player each time :
player = new Image();
player.src = "game_character.png";
So you erase the player.x modified by your event handler.
You should initialize the player only once, outside the draw function. You can move the initialization like this :
var player = new Image();
player.src = "game_character.png";
var draw = function() {
There is absolutely no need to call player.src = "game_character.png"; inside the draw function.
As a general rule, when dealing with animation, try to remove all what you can from the draw function, which should be as fast as possible.
You will need to redraw the canvas each time. Something like this:
function init()
{
canvas = document.getElementById("canvas");
context = canvas.getContext("2d");
x = canvas.width / 2; //align to centre of the screen
y = canvas.height / 2; //same as above
speed = 5; //speed for the player to move at
width = 50; //width of the player
height = 50; //height of the player
playerimage = new Image();
playerimage.src = "path/to/image/for/player"; //path to the image to use for the player
canvas.addEventListener("keypress", update);
}
function update(event)
{
if (event.keyCode == 38)
{
y -= speed; //going up
}
if (event.keyCode == 40)
{
y += speed; //going down
}
if (event.keyCode == 37)
{
x -= speed; //going left
}
if (event.keyCode == 39)
{
x += speed; //going right
}
render();
}
function render()
{
context.clearRect(0, 0, canvas.width, canvas.height);
context.drawImage(playerimage, x, y, width, height);
}
I haven't tested it, so I don't know whether it works and there may be some mistakes here and there. It should work though! If nothing else, it will (hopefully) give you an idea of one way in which you can go about doing it...

Categories

Resources