I have created lines using canvas quadraticCurveTo connecting with each other. My goal is to animate this lines.
I have an example with lineTo method, how to change it for quadraticCurveTo method?
(function () {
var lastTime = 0;
var vendors = ['ms', 'moz', 'webkit', 'o'];
for (var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
window.requestAnimationFrame = window[vendors[x] + 'RequestAnimationFrame'];
window.cancelAnimationFrame = window[vendors[x] + 'CancelAnimationFrame'] || window[vendors[x] + 'CancelRequestAnimationFrame'];
}
if (!window.requestAnimationFrame) window.requestAnimationFrame = function (callback, element) {
var currTime = new Date().getTime();
var timeToCall = Math.max(0, 16 - (currTime - lastTime));
var id = window.setTimeout(function () {
callback(currTime + timeToCall);
},
timeToCall);
lastTime = currTime + timeToCall;
return id;
};
Example link: http://jsfiddle.net/m1erickson/7faRQ/
What I expected: example image
Animation along a path
To do as the fiddle but with any of the path functions you can use line dash to set the amount of the path that is drawn. This will give you a animation along the path that is at a constant speed.
The only problem with this method is that you do not know how long the path is. To know that involves some rather complicated math (warning some bezier length solutions are approximations).
In this example I used my eye to work out the length.
requestAnimationFrame(loop);
const ctx = canvas.getContext("2d");
ctx.lineCap = "round";
const lines = [[10, 10], [300, 10, 250, 200], [100, 300, 20, 120, 10, 10]];
const render = {
"2"(p) { ctx.lineTo(...p) },
"4"(p) { ctx.quadraticCurveTo(...p) },
"6"(p) { ctx.bezierCurveTo(...p) },
start(width, col) { ctx.lineWidth = width; ctx.strokeStyle = col; ctx.beginPath() }
}
var startTime;
const len = 760;
function loop(time) {
if(startTime === undefined){ startTime = time }
const animTime = time - startTime;
ctx.clearRect(0, 0, 300, 300);
ctx.setLineDash([1, 0]);
render.start(1,"blue")
lines.forEach(l => render[l.length](l));
ctx.stroke();
ctx.setLineDash([(animTime / 10) % len, 10000]);
render.start(8, "red")
lines.forEach(l => render[l.length](l));
ctx.stroke();
requestAnimationFrame(loop);
}
canvas { border : 2px solid black; }
<canvas id="canvas" width = 300 height = 300></canvas>
I know this question is a little old, but SO is filled with bad answers to variations on this question, and until I found this one I found no good solutions. #Blindman67 pointed me in the right direction with setLineDash, and the following works beautifully for me. rnd2() returns a random integer in the range (but you can always just use a constant or a parameter), and curves() calculates a quick rough approximate length of its curve (the average of chord and total segments length), which I multiply by 1.2 to be sure I don't go over. My setLineDash call works with alpha < 1.0 because it doesn't repeatedly overdraw the curve, and it doesn't spend extra time calculating a long invisible blank. raf() is requestAnimationFrame.
var lineLen = 0, delta = rnd2(20, 80);
var approxCurveLen = curves(c0.width, c0.height, ctx, false) * 1.2;
var step = () =>
{
if (lineLen < approxCurveLen)
{
ctx.setLineDash([0, lineLen, delta, approxCurveLen-lineLen]);
ctx.stroke();
lineLen += delta;
raf(step);
}
else
{
ctx.setLineDash([]);
}
};
raf(step);
Related
I'm trying to have a simple rectangle move from current location to the clicked location on the canvas. When I provide a constant speed, the rectangle appears and moves fine. But when I multiply it by deltatime the rectangle doesn't appear anymore. I'm using requestAnimationFrame as the draw loop.
canvas.js
window.addEventListener('DOMContentLoaded', (event) => {
let canvas = document.getElementById("gamecanvas");
let ctx = canvas.getContext('2d');
var oldframetime = 0;
var x = 0;
var y = 0;
var dstx = 0;
var dsty = 0;
var deltatime = 0;
var speed = 5;
function getmousepos(evt){
var rect = canvas.getBoundingClientRect();
return {
x: evt.clientX - rect.left,
y: evt.clientY - rect.top
};
}
canvas.addEventListener("mousedown",e=>{
let coord = getmousepos(e);
dstx = coord.x;
dsty = coord.y;
});
function movetowards(current,target,maxdistancedelta){
if(Math.abs(target - current)<=maxdistancedelta){
return target;
}
return current+Math.sign(target - current) * maxdistancedelta;
}
function tick(timestamp){
deltatime = (timestamp - oldframetime)/1000;
var step = deltatime*speed;
var newlocx = movetowards(x,dstx-50,5);
var newlocy = movetowards(y,dsty-50,5);
ctx.clearRect(0,0,canvas.clientWidth,canvas.clientHeight);
ctx.fillStyle = 'green';
ctx.fillRect(newlocx,newlocy,100,100);
x = newlocx;
y = newlocy;
console.log(newlocx+":"+newlocy);
oldframetime = timestamp;
requestAnimationFrame(tick);
}
requestAnimationFrame(function(){
tick();
});
});
In that example, newlocx and newlocy both print NaN:NaN
But if I choose not to use step and give it a constant speed of like 5, then it works fine.
function tick(timestamp){
deltatime = (timestamp - oldframetime)/1000;
var step = deltatime*speed;
var newlocx = movetowards(x,dstx-50,5);
var newlocy = movetowards(y,dsty-50,5);
ctx.clearRect(0,0,canvas.clientWidth,canvas.clientHeight);
ctx.fillStyle = 'green';
ctx.fillRect(newlocx,newlocy,100,100);
x = newlocx;
y = newlocy;
console.log(newlocx+":"+newlocy);
oldframetime = timestamp;
requestAnimationFrame(tick);
}
The print is also accurate now. Why does multiplyng step by deltatime prevent the rectangle from moving? Or even appearing?
Here's the HTML if anyone's interested.
index.html
<html>
<head>
<script src="canvas.js"></script>
</head>
<body>
<canvas id="gamecanvas" width="2000" height="1000"></canvas>
</body>
</html>
I have checked and found out the issue is in the first statement of tick function. When program starts then value of parameter is undefined. And thats why first statement of tick function result in NaN.
Just use some default value for parameter "timestamp". Its working now but speed is low and i hope you are aware about it.
Line to check:
function tick(timestamp=10)
function tick(timestamp=10) {
deltatime = (timestamp - oldframetime) / 1000;
var step = deltatime * speed;
var newlocx = movetowards(x, dstx - 50, 5);
var newlocy = movetowards(y, dsty - 50, 5);
ctx.clearRect(0, 0, canvas.clientWidth, canvas.clientHeight);
ctx.fillStyle = 'green';
ctx.fillRect(newlocx, newlocy, 100, 100);
x = newlocx;
y = newlocy;
console.log(newlocx + ":" + newlocy);
oldframetime = timestamp;
requestAnimationFrame(tick);
}
You should be starting the animation with requestAnimationFrame rather than indirectly as you have done.
Replace
requestAnimationFrame(function(){
tick();
});
with
requestAnimationFrame(tick)
Then in the function tick you check if oldframetime has been set, if not set deltaTime to 0, as no time has passed, and thus starts the animation at the start. If you set the value of deltaTime to any other value you end up not rendering the animation from the start.
function tick(timestamp){
if (!oldframetime) {
deltaTime = 0;
} else {
deltatime = (timestamp - oldframetime)/1000;
}
// .. animation code
oldframetime = timestamp;
requestAnimationFrame(tick);
}
I tried to create a canvas effect with fireworks, but the more you click, the faster it gets and it seems to accumulate on itself. When I listed the speed it was similar and did not correspond to what was happening there. I also tried to cancel the draw if it got out of the canvas but it didnĀ“t help.
Here is link https://dybcmwd8icxxdxiym4xkaw-on.drv.tw/canvasTest.html
var fireAr = [];
var expAr = [];
function Firework(x, y, maxY, maxX, cn, s, w, en) {
this.x = x;
this.y = y;
this.maxY = maxY;
this.maxX = maxX;
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
this.explosion = function() {
for (; this.i < this.en; this.i++) {
var ey = this.maxY;
var ex = this.maxX;
var ecn = Math.floor(Math.random() * color.length);
var esX = (Math.random() - 0.5) * 3;
var esY = (Math.random() - 0.5) * 3;
var ew = Math.random() * 10;
var t = true;
expAr.push(new Exp(ew, esX, esY, ex, ey, ecn, t));
}
for (var e = 0; e < expAr.length; e++) {
expAr[e].draw();
}
}
this.draw = function() {
if (this.y < this.maxY) {
this.explosion();
} else {
this.track();
this.y -= this.s;
}
}
}
function Exp(ew, esX, esY, ex, ey, ecn, t) {
this.ew = ew;
this.esX = esX;
this.esY = esY;
this.ex = ex;
this.ey = ey;
this.ecn = ecn;
this.t = t;
this.draw = function() {
if (this.t == true) {
c.beginPath();
c.shadowBlur = 20;
c.shadowColor = color[this.ecn];
c.rect(this.ex, this.ey, this.ew, this.ew);
c.fillStyle = color[this.ecn];
c.fill();
c.closePath();
this.ex += this.esX;
this.ey += this.esY;
}
}
}
window.addEventListener('click', function(event) {
var x = event.clientX;
var y = canvas.height;
mouse.clickX = event.clientX;
mouse.clickY = event.clientY;
var maxY = event.clientY;
var maxX = event.clientX;
var cn = Math.floor(Math.random() * color.length);
var s = Math.random() * 5 + 5;
var w = Math.random() * 20 + 2;
var en = Math.random() * 50 + 5;
fireAr.push(new Firework(x, y, maxY, maxX, cn, s, w, en));
});
function ani() {
requestAnimationFrame(ani);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < fireAr.length; i++) {
fireAr[i].draw();
}
}
ani();
I deleted some unnecessary parts in my opinion but if I'm wrong and I missed something I'll try to fix it
Here are a few simple ways you can improve performance:
Commenting out shadowBlur gives a noticeable boost. If you need shadows, see this answer which illustrates pre-rendering.
Try using fillRect and ctx.rotate() instead of drawing a path. Saving/rotating/restoring the canvas might be prohibitive, so you could use non-rotated rectangles.
Consider using a smaller canvas which is quicker to repaint than one that may fill the entire window.
Another issue is more subtle: Fireworks and Exps are being created (making objects is expensive!) and pushed onto arrays. But these arrays are never trimmed and objects are never reused after they've left the visible canvas. Eventually, the rendering loop gets bogged down by all of the computation for updating and rendering every object in the fireAr and expAr arrays.
A naive solution is to check for objects exiting the canvas and splice them from the expAr. Here's pseudocode:
for (let i = expAr.length - 1; i >= 0; i--) {
if (!inBounds(expAr[i], canvas)) {
expAr.splice(i, 1);
}
}
Iterate backwards since this mutates the array's length. inBounds is a function that checks an Exp object's x and y properties along with its size and the canvas width and height to determine if it has passed an edge. More pseudocode:
function inBounds(obj, canvas) {
return obj.x >= 0 && obj.x <= canvas.width &&
obj.y >= 0 && obj.y <= canvas.height;
}
This check isn't exactly correct since the rectangles are rotated. You could check each corner of the rectangle with a pointInRect function to ensure that at least one is inside the canvas.
Fireworks can be spliced out when they "explode".
splice is an expensive function that walks up to the entire array to shift items forward to fill in the vacated element. Splicing multiple items in a loop gives quadratic performance. This can be made linear by putting surviving fireworks in a new list and replacing the previous generation on each frame. Dead firework objects can be saved in a pool for reuse.
Beyond that, I strongly recommend using clear variable names.
this.cn = cn;
this.s = s;
this.w = w;
this.en = en;
this.i = 0;
These names have little or no meaning to an outside reader and are unlikely to mean much to you if you take a couple months away from the code. Use full words like "size", "width", etc.
Another side point is that it's a good idea to debounce your window resize listener.
Here's a quick proof of concept that illustrates the impact of shadowBlur and pruning dead elements.
const rnd = n => ~~(Math.random() * n);
const mouse = {pressed: false, x: 0, y: 0};
let fireworks = [];
let shouldSplice = false;
const canvas = document.createElement("canvas");
const ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
document.body.style.margin = 0;
canvas.style.background = "#111";
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
ctx.shadowBlur = 0;
const fireworksAmt = document.querySelector("#fireworks-amt");
document.querySelector("input[type=range]").addEventListener("change", e => {
ctx.shadowBlur = e.target.value;
document.querySelector("#shadow-amt").textContent = ctx.shadowBlur;
});
document.querySelector("input[type=checkbox]").addEventListener("change", e => {
shouldSplice = !shouldSplice;
});
const createFireworks = (x, y) => {
const color = `hsl(${rnd(360)}, 100%, 60%)`;
return Array(rnd(20) + 1).fill().map(_ => ({
x: x,
y: y,
vx: Math.random() * 6 - 3,
vy: Math.random() * 6 - 3,
size: rnd(4) + 2,
color: color
}));
}
(function render() {
if (mouse.pressed) {
fireworks.push(...createFireworks(mouse.x, mouse.y));
}
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (const e of fireworks) {
e.x += e.vx;
e.y += e.vy;
e.vy += 0.03;
ctx.beginPath();
ctx.fillStyle = ctx.shadowColor = e.color;
ctx.arc(e.x, e.y, e.size, 0, Math.PI * 2);
ctx.fill();
if (shouldSplice) {
e.size -= 0.03;
if (e.size < 1) {
e.dead = true;
}
}
}
fireworks = fireworks.filter(e => !e.dead);
fireworksAmt.textContent = "fireworks: " + fireworks.length;
requestAnimationFrame(render);
})();
let debounce;
addEventListener("resize", e => {
clearTimeout(debounce);
debounce = setTimeout(() => {
canvas.width = document.body.scrollWidth;
canvas.height = document.body.clientHeight;
}, 100);
});
canvas.addEventListener("mousedown", e => {
mouse.pressed = true;
});
canvas.addEventListener("mouseup", e => {
mouse.pressed = false;
});
canvas.addEventListener("mousemove", e => {
mouse.x = e.offsetX;
mouse.y = e.offsetY;
});
* {
font-family: monospace;
user-select: none;
}
div > span, body > div {padding: 0.5em;}
canvas {display: block;}
<div>
<div id="fireworks-amt">fireworks: 0</div>
<div>
<label>splice? </label>
<input type="checkbox">
</div>
<div>
<label>shadowBlur (<span id="shadow-amt">0</span>): </label>
<input type="range" value=0>
</div>
</div>
I have a canvas animation that I wish to slow down, it's pretty simple noise effect that moves around pixels. demo can be found here: https://codepen.io/anon/pen/eyyjqm - I want to slow down the pixels movement/jitter.
const canvas = document.querySelector('canvas'),
ctx = canvas.getContext('2d')
canvas.width = canvas.height = 128
resize();
window.onresize = resize;
function resize() {
canvas.width = window.innerWidth * window.devicePixelRatio / 1
canvas.height = window.innerHeight * window.devicePixelRatio / 1
canvas.style.width = window.innerWidth + 'px'
canvas.style.height = window.innerHeight + 'px'
}
function noise(ctx) {
const w = ctx.canvas.width,
h = ctx.canvas.height,
iData = ctx.createImageData(w, h),
buffer32 = new Uint32Array(iData.data.buffer),
len = buffer32.length
let i = 1
for(; i < len;i++)
if (Math.random() < 0.5) buffer32[i] = 0xffffffff;
ctx.putImageData(iData, 0, 0);
}
(function loop() {
noise(ctx);
requestAnimationFrame(loop);
})();
I've tried;
window.setInterval('noise(ctx)',10);
but this looks so jittery and not very smooth because im setting an interval of 10 frames. What would be a better way to slow down the animation?
Appreciate any ideas! Thanks, John
Here is an approach maybe can help you.
The requestAnimationFrame pass as parameter the currentTime which is executing, so you can get some delta currentTime - oldTime time in each call and if is very short not execute the noise function again, on the other hand if it has passed a considerable time execute it again, this deltaTime can de set:
something like this:
delta = 200;
oldTime = 0;
function loop(currentTime) {
if(oldTime === 0) {
oldTime = currentTime;
}
if((currentTime - oldTime) >= delta){
noise(ctx);
oldTime = currentTime;
}
requestAnimationFrame(loop);
};
requestAnimationFrame(loop);
here is working: https://codepen.io/anon/pen/GyyYKa
I rewrote some of the code to make it slightly more efficient and to hopefully get a similar effect you were looking for:
const w = ctx.canvas.width;
h = ctx.canvas.height;
const iData = ctx.createImageData(w, h);
buffer32 = new Uint32Array(iData.data.buffer);
len = buffer32.length;
window.setInterval('noise(ctx)',38);
function noise(ctx) {
let i = 1
for(; i < len;i += 4)
if (Math.random() < 0.4)
{
buffer32[i] = 0xffffffff;
} else {
buffer32[i] = 0x00000000;
}
ctx.putImageData(iData, 0, 0);
}
//(function loop() {
//noise(ctx);
// requestAnimationFrame(loop);
//})();
I would however recommend applying a more conventional noise algorithm such as the simplex algorithm. See example here: http://haptic-data.com/toxiclibsjs/examples/simplex-noise-canvas. It will most likely be much smoother.
I've used this method in the past to limit how frequently the animation is updated.
let frame = 0
let frameLimit = 3
draw() {
// animation code here...
}
animate() {
frame++
if (frame % frameLimit === 0) {
// make some changes then redraw animation
draw()
}
requestAnimationFrame(animate)
}
Now your animation will only update every third frame. you can then easily adjust the value to speed up or slow down your animation. I don't want to claim that this is the best approach but its an alternative that I didn't see listed.
I'm trying to create a game in canvas with javascript where you control a spaceship and have it so that the canvas will translate and rotate to make it appear like the spaceship is staying stationary and not rotating.
Any help would be greatly appreciated.
window.addEventListener("load",eventWindowLoaded, false);
function eventWindowLoaded() {
canvasApp();
}
function canvasSupport() {
return Modernizr.canvas;
}
function canvasApp() {
if (!canvasSupport()) {
return;
}
var theCanvas = document.getElementById("myCanvas");
var height = theCanvas.height; //get the heigth of the canvas
var width = theCanvas.width; //get the width of the canvas
var context = theCanvas.getContext("2d"); //get the context
var then = Date.now();
var bgImage = new Image();
var stars = new Array;
bgImage.onload = function() {
context.translate(width/2,height/2);
main();
}
var rocket = {
xLoc: 0,
yLoc: 0,
score : 0,
damage : 0,
speed : 20,
angle : 0,
rotSpeed : 1,
rotChange: 0,
pointX: 0,
pointY: 0,
setScore : function(newScore){
this.score = newScore;
}
}
function Star(){
var dLoc = 100;
this.xLoc = rocket.pointX+ dLoc - Math.random()*2*dLoc;
this.yLoc = rocket.pointY + dLoc - Math.random()*2*dLoc;
//console.log(rocket.xLoc+" "+rocket.yLoc);
this.draw = function(){
drawStar(this.xLoc,this.yLoc,20,5,.5);
}
}
//var stars = new Array;
var drawStars = function(){
context.fillStyle = "yellow";
if (typeof stars !== 'undefined'){
//console.log("working");
for(var i=0;i< stars.length ;i++){
stars[i].draw();
}
}
}
var getDistance = function(x1,y1,x2,y2){
var distance = Math.sqrt(Math.pow((x2-x1),2)+Math.pow((y2-y1),2));
return distance;
}
var updateStars = function(){
var numStars = 10;
while(stars.length<numStars){
stars[stars.length] = new Star();
}
for(var i=0; i<stars.length; i++){
var tempDist = getDistance(rocket.pointX,rocket.pointY,stars[i].xLoc,stars[i].yLoc);
if(i == 0){
//console.log(tempDist);
}
if(tempDist > 100){
stars[i] = new Star();
}
}
}
function drawRocket(xLoc,yLoc, rWidth, rHeight){
var angle = rocket.angle;
var xVals = [xLoc,xLoc+(rWidth/2),xLoc+(rWidth/2),xLoc-(rWidth/2),xLoc-(rWidth/2),xLoc];
var yVals = [yLoc,yLoc+(rHeight/3),yLoc+rHeight,yLoc+rHeight,yLoc+(rHeight/3),yLoc];
for(var i = 0; i < xVals.length; i++){
xVals[i] -= xLoc;
yVals[i] -= yLoc+rHeight;
if(i == 0){
console.log(yVals[i]);
}
var tempXVal = xVals[i]*Math.cos(angle) - yVals[i]*Math.sin(angle);
var tempYVal = xVals[i]*Math.sin(angle) + yVals[i]*Math.cos(angle);
xVals[i] = tempXVal + xLoc;
yVals[i] = tempYVal+(yLoc+rHeight);
}
rocket.pointX = xVals[0];
rocket.pointY = yVals[0];
//rocket.yLoc = yVals[0];
//next rotate
context.beginPath();
context.moveTo(xVals[0],yVals[0])
for(var i = 1; i < xVals.length; i++){
context.lineTo(xVals[i],yVals[i]);
}
context.closePath();
context.lineWidth = 5;
context.strokeStyle = 'blue';
context.stroke();
}
var world = {
//pixels per second
startTime: Date.now(),
speed: 50,
startX:width/2,
startY:height/2,
originX: 0,
originY: 0,
xDist: 0,
yDist: 0,
rotationSpeed: 20,
angle: 0,
distance: 0,
calcOrigins : function(){
world.originX = -world.distance*Math.sin(world.angle*Math.PI/180);
world.originY = -world.distance*Math.cos(world.angle*Math.PI/180);
}
};
var keysDown = {};
addEventListener("keydown", function (e) {
keysDown[e.keyCode] = true;
}, false);
addEventListener("keyup", function (e) {
delete keysDown[e.keyCode];
}, false);
var update = function(modifier) {
if (37 in keysDown) { // Player holding left
rocket.angle -= rocket.rotSpeed* modifier;
rocket.rotChange = - rocket.rotSpeed* modifier;
//console.log("left");
}
if (39 in keysDown) { // Player holding right
rocket.angle += rocket.rotSpeed* modifier;
rocket.rotChange = rocket.rotSpeed* modifier;
//console.log("right");
}
};
var render = function (modifier) {
context.clearRect(-width*10,-height*10,width*20,height*20);
var dX = (rocket.speed*modifier)*Math.sin(rocket.angle);
var dY = (rocket.speed*modifier)*Math.cos(rocket.angle);
rocket.xLoc += dX;
rocket.yLoc -= dY;
updateStars();
drawStars();
context.translate(-dX,dY);
context.save();
context.translate(-rocket.pointX,-rocket.pointY);
context.translate(rocket.pointX,rocket.pointY);
drawRocket(rocket.xLoc,rocket.yLoc,50,200);
context.fillStyle = "red";
context.fillRect(rocket.pointX,rocket.pointY,15,5);
//context.restore(); // restores the coordinate system back to (0,0)
context.fillStyle = "green";
context.fillRect(0,0,10,10);
context.rotate(rocket.angle);
context.restore();
};
function drawStar(x, y, r, p, m)
{
context.save();
context.beginPath();
context.translate(x, y);
context.moveTo(0,0-r);
for (var i = 0; i < p; i++)
{
context.rotate(Math.PI / p);
context.lineTo(0, 0 - (r*m));
context.rotate(Math.PI / p);
context.lineTo(0, 0 - r);
}
context.fill();
context.restore();
}
// the game loop
function main(){
requestAnimationFrame(main);
var now = Date.now();
var delta = now - then;
update(delta / 1000);
//now = Date.now();
//delta = now - then;
render(delta / 1000);
then = now;
// Request to do this again ASAP
}
var w = window;
var requestAnimationFrame = w.requestAnimationFrame || w.webkitRequestAnimationFrame || w.msRequestAnimationFrame || w.mozRequestAnimationFrame;
//start the game loop
//gameLoop();
//event listenters
bgImage.src = "images/background.jpg";
} //canvasApp()
Origin
When you need to rotate something in canvas it will always rotate around origin, or center for the grid if you like where the x and y axis crosses.
You may find my answer here useful as well
By default the origin is in the top left corner at (0, 0) in the bitmap.
So in order to rotate content around a (x,y) point the origin must first be translated to that point, then rotated and finally (and usually) translated back. Now things can be drawn in the normal order and they will all be drawn rotated relative to that rotation point:
ctx.translate(rotateCenterX, rotateCenterY);
ctx.rotate(angleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
Absolute angles and positions
Sometimes it's easier to keep track if an absolute angle is used rather than using an angle that you accumulate over time.
translate(), transform(), rotate() etc. are accumulative methods; they add to the previous transform. We can set absolute transforms using setTransform() (the last two arguments are for translation):
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY); // absolute
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
The rotateCenterX/Y will represent the position of the ship which is drawn untransformed. Also here absolute transforms can be a better choice as you can do the rotation using absolute angles, draw background, reset transformations and then draw in the ship at rotateCenterX/Y:
ctx.setTransform(1, 0, 0, 1, rotateCenterX, rotateCenterY);
ctx.rotate(absoluteAngleInRadians);
ctx.translate(-rotateCenterX, -rotateCenterY);
// update scene/background etc.
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(ship, rotateCenterX, rotateCenterY);
(Depending on orders of things you could replace the first line here with just translate() as the transforms are reset later, see demo for example).
This allows you to move the ship around without worrying about current transforms, when a rotation is needed use the ship's current position as center for translation and rotation.
And a final note: the angle you would use for rotation would of course be the counter-angle that should be represented (ie. ctx.rotate(-angle);).
Space demo ("random" movements and rotations)
The red "meteors" are dropping in one direction (from top), but as the ship "navigates" around they will change direction relative to our top view angle. Camera will be fixed on the ship's position.
(ignore the messy part - it's just for the demo setup, and I hate scrollbars... focus on the center part :) )
var img = new Image();
img.onload = function() {
var ctx = document.querySelector("canvas").getContext("2d"),
w = 600, h = 400, meteors = [], count = 35, i = 0, x = w * 0.5, y, a = 0, a2 = 0;
ctx.canvas.width = w; ctx.canvas.height = h; ctx.fillStyle = "#555";
while(i++ < count) meteors.push(new Meteor());
(function loop() {
ctx.clearRect(0, 0, w, h);
y = h * 0.5 + 30 + Math.sin((a+=0.01) % Math.PI*2) * 60; // ship's y and origin's y
// translate to center of ship, rotate, translate back, render bg, reset, draw ship
ctx.translate(x, y); // translate to origin
ctx.rotate(Math.sin((a2+=0.005) % Math.PI) - Math.PI*0.25); // rotate some angle
ctx.translate(-x, -y); // translate back
ctx.beginPath(); // render some moving meteors for the demo
for(var i = 0; i < count; i++) meteors[i].update(ctx); ctx.fill();
ctx.setTransform(1, 0, 0, 1, 0, 0); // reset transforms
ctx.drawImage(img, x - 32, y); // draw ship as normal
requestAnimationFrame(loop); // loop animation
})();
};
function Meteor() { // just some moving object..
var size = 5 + 35 * Math.random(), x = Math.random() * 600, y = -200;
this.update = function(ctx) {
ctx.moveTo(x + size, y); ctx.arc(x, y, size, 0, 6.28);
y += size * 0.5; if (y > 600) y = -200;
};
}
img.src = "http://i.imgur.com/67KQykW.png?1";
body {background:#333} canvas {background:#000}
<canvas></canvas>
I'm trying to make a game using HTML5's canvas and javascript. I believe that I have been doing everything "okay" up until the point where I need to reset the game on death. I'm not really sure what is causing the unexpected behavior but i have thoughts.
Pretty much, the game operates at 'x' speed and then when it resets everything is moving much faster. I have not changed any of the variables for any speed of anything and when debugging and inspecting my code the speeds are the same on the newly created objects as the first time the game is started but they are moving much faster. If you die again it resets again and operates even faster. Die, rinse, repeat and it keeps speeding up until the page would probably hit "not responding" state.
I don't know for sure what I'm doing wrong that is causing the speed difference or at least the illusion of the speed difference. I have an idea that it has something to do with my game loop running multiple times into forever every time it's reset but I don't understand how/why it would do that. any feedback is appreciated.
Also, you can visit my testing page to see all of the code if you need more than I posted below: game test playground
var windowx,
windowy,
canvas,
player,
quadTree,
ctx;
var drawObjects;
var keyState;
var mainloop;
var animFrame;
var ONE_FRAME_TIME;
var reset = function () {
//get canvas and set height and width
canvas = document.getElementById('canvas');
canvas.setAttribute('width', windowx / 2);
canvas.setAttribute('height', windowy / 2);
ctx = canvas.getContext("2d");
drawObjects = [];
keyState = {};
quadTree = new Quadtree(quadTreeBounds);
//make the friendly square
player = new Rectangle(20, 20, 40, 40, 0, 0, XPhysicsBehaviorNormal, YPhysicsBehaviorNormal, XBoundaryBehaviorInCanvas, YBoundaryBehaviorInCanvas, playerObjectType, '#580000', null);
drawObjects.push(player);
drawObjects.push(new Rectangle(40, 100, canvas.width + (distanceOutsideCanvasBeforeDie / 2), canvas.clientHeight - 100, defaultEnemyRectangleVelocity, 0, null, YPhysicsBehaviorNormal, null, YBoundaryBehaviorInCanvas, enemyObjectType, null, OutOfCanvasDieBehavior));
backgroundMusicAudio.play();
//define main loop
mainloop = function () {
buildQuadTree();
updateGame();
drawGame();
};
//define the windowanimationframeobject
animFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
null;
if (animFrame !== null) {
var recursiveAnim = function () {
mainloop();
animFrame(recursiveAnim, canvas);
};
// start the mainloop
animFrame(recursiveAnim, canvas);
} else {
// fallback to setInterval if the browser doesn't support requestAnimationFrame
ONE_FRAME_TIME = 1000.0 / 60.0;
setInterval(mainloop, ONE_FRAME_TIME);
}
}
$(function () {
//get window width and height;
windowx = window.innerWidth;
windowy = window.innerHeight;
reset();
$(document).on('change', '#sound-enabled-toggle', function() {
var isChecked = $(this).is(':checked');
$('#sound-enabled-toggle').blur();
if (isChecked) {
backgroundMusicAudio.play();
playerJumpAudio = playerJumpMusicAudioSetup();
playerBlinkAudio = playerBlinkMusicAudioSetup();
} else {
backgroundMusicAudio.pause();
playerJumpAudio = new Audio('');
playerBlinkAudio = new Audio('');
}
});
});
//left the function here in case I need to do anything else but for now it's just clearing.
function buildQuadTree() {
quadTree.clear();
}
function updateGame() {
//determine if there are any keys pushed at the current point
keyPressActions();
//loop for calculating and updating all objects positions/values.
for (var i = 0; i < drawObjects.length; i++) {
var object = drawObjects[i];
quadTree.insert(new SimpleRectangle(object.x, object.y, object.width || (object.radius * 2), object.height || (object.radius * 2), object.name));
object.update();
//roundFloatingPoints Numbers to 2 decimal places
roundObjectVelocitiesAndPoints(object);
}
PlayerDeathTrigger(player);
}
function drawGame() {
//clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = "20px Verdana";
ctx.fillText("100,000", (canvas.width * .8), (canvas.clientHeight * .1));
ctx.font = "15px Verdana";
ctx.fillText("Temp Score", (canvas.width * .8), (canvas.clientHeight * .05));
//draw all objects in drawObjects
for (var i = 0; i < drawObjects.length; i++) {
var object = drawObjects[i];
object.draw();
}
}
Overall I would recommend re-factoring this code so your reset function doesn't re-create so many objects. Basically the reset function is just re-creating a bulk of the game logic.
The specific issue you identified appears to be coming from the fact that you call setInterval on each reset without clearing the previous interval. See the code below with the minimal changes to prevent this issue.
var windowx,
windowy,
canvas,
player,
quadTree,
ctx,
gameLoop;
var drawObjects;
var keyState;
var mainloop;
var animFrame;
var ONE_FRAME_TIME;
var reset = function() {
// Remove our old game loop
clearInterval(gameLoop);
//get canvas and set height and width
canvas = document.getElementById('canvas');
canvas.setAttribute('width', windowx / 2);
canvas.setAttribute('height', windowy / 2);
ctx = canvas.getContext("2d");
drawObjects = [];
keyState = {};
quadTree = new Quadtree(quadTreeBounds);
//make the friendly square
player = new Rectangle(20, 20, 40, 40, 0, 0, XPhysicsBehaviorNormal, YPhysicsBehaviorNormal, XBoundaryBehaviorInCanvas, YBoundaryBehaviorInCanvas, playerObjectType, '#580000', null);
drawObjects.push(player);
drawObjects.push(new Rectangle(40, 100, canvas.width + (distanceOutsideCanvasBeforeDie / 2), canvas.clientHeight - 100, defaultEnemyRectangleVelocity, 0, null, YPhysicsBehaviorNormal, null, YBoundaryBehaviorInCanvas, enemyObjectType, null, OutOfCanvasDieBehavior));
backgroundMusicAudio.play();
//define main loop
mainloop = function() {
buildQuadTree();
updateGame();
drawGame();
};
//define the windowanimationframeobject
animFrame = window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
null;
if (animFrame !== null) {
var recursiveAnim = function() {
mainloop();
animFrame(recursiveAnim, canvas);
};
// start the mainloop
animFrame(recursiveAnim, canvas);
} else {
// fallback to setInterval if the browser doesn't support requestAnimationFrame
ONE_FRAME_TIME = 1000.0 / 60.0;
gameLoop = setInterval(mainloop, ONE_FRAME_TIME);
}
}
$(function() {
//get window width and height;
windowx = window.innerWidth;
windowy = window.innerHeight;
reset();
$(document).on('change', '#sound-enabled-toggle', function() {
var isChecked = $(this).is(':checked');
$('#sound-enabled-toggle').blur();
if (isChecked) {
backgroundMusicAudio.play();
playerJumpAudio = playerJumpMusicAudioSetup();
playerBlinkAudio = playerBlinkMusicAudioSetup();
} else {
backgroundMusicAudio.pause();
playerJumpAudio = new Audio('');
playerBlinkAudio = new Audio('');
}
});
});
//left the function here in case I need to do anything else but for now it's just clearing.
function buildQuadTree() {
quadTree.clear();
}
function updateGame() {
//determine if there are any keys pushed at the current point
keyPressActions();
//loop for calculating and updating all objects positions/values.
for (var i = 0; i < drawObjects.length; i++) {
var object = drawObjects[i];
quadTree.insert(new SimpleRectangle(object.x, object.y, object.width || (object.radius * 2), object.height || (object.radius * 2), object.name));
object.update();
//roundFloatingPoints Numbers to 2 decimal places
roundObjectVelocitiesAndPoints(object);
}
PlayerDeathTrigger(player);
}
function drawGame() {
//clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.font = "20px Verdana";
ctx.fillText("100,000", (canvas.width * .8), (canvas.clientHeight * .1));
ctx.font = "15px Verdana";
ctx.fillText("Temp Score", (canvas.width * .8), (canvas.clientHeight * .05));
//draw all objects in drawObjects
for (var i = 0; i < drawObjects.length; i++) {
var object = drawObjects[i];
object.draw();
}
}