Acceleration setInterval in Jquery - javascript

I am trying to create a bouncing ball (on a canvas). I would love if the ball could accelerate when going up and down. No idea how to do this with setInterval. Here is my code:
setInterval(function animate() {
ctx.clearRect( 0, 0, canvas.width, canvas.height);
if (movement1 === true) {
dotHeight += 1;
if (dotHeight >= 100) movement1 = false;
} else {
dotHeight -= 1;
if (dotHeight <= 0) movement1 = true;
}
ctx.beginPath();
ctx.arc(canvas.width / 2, (canvas.height / 2) + dotHeight, dotSize, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
}, 4);
This results in a linear movement. I would love to have a natural movement. Basically starting fast and getting slower when reaching the top and vice versa.

You should have both a speed and a gravity (or acceleration) variable:
speed tells you how many units (pixels here) is going to travel your object in the current update.
gravity tells you by how many units is speed increased on each update.
You want a constant gravity so that speed is increasing the same amount of pixels on each update. That will give you a variable speed, so that your object (dot here) is travelling longer or shorter distances on each update, depending on the direction it is travelling.
To make the dot bounce just change the direction of its speed once it reaches the floor. You just need to multiply it by -1 or, instead of that, you could multiply it by a bouncingFactor (-1 < bouncingFactor < 0) so that it loses energy on each bounce:
Here you can see a working example:
var canvas = document.getElementById("canvas");
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
var ctx = canvas.getContext("2d");
var frames = 0; // Frame counter just to make sure you don't crash your browser while editing code!
// DOT STUFF:
var dotSize = 20;
var dotMinY = 0 + dotSize; // Start position
var dotMaxY = canvas.height - dotSize; // Floor
var dotY = dotMinY;
var dotSpeed = 0;
var dotLastBounceSpeed = 0; // You can use this to determine whether the ball is still bouncing enough to be visible by the user.
var center = canvas.width / 2; // Try to take every operation you can out of the animate function.
var pi2 = 2 * Math.PI;
// WORLD STUFF:
var gravity = .5;
var bounceFactor = .8; // If < 1, bouncing absorbs energy so ball won't go as high as it was before.
// MAIN ANIMATION LOOP:
function animate() {
ctx.clearRect( 0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(center, dotY, dotSize, 0, pi2);
ctx.fillStyle = "red";
ctx.fill();
// First, dotSpeed += gravity is calculated and that returns the new value for dotSpeed
// then, that new value is added to dotY.
dotY += dotSpeed += gravity;
if(dotY >= dotMaxY ) {
dotY = dotMaxY;
dotSpeed *= -bounceFactor;
}
var dotCurrentBounceSpeed = Math.round(dotSpeed * 100); // Takes two decimal digits.
if(frames++ < 5000 && dotLastBounceSpeed != dotCurrentBounceSpeed) {
dotLastBounceSpeed = dotCurrentBounceSpeed;
setTimeout(animate, 16); // 1000/60 = 16.6666...
}
else alert("Animation end. Took " + frames + " frames.");
}
animate();
html, body, #canvas {
position:relative;
width: 100%;
height: 100%;
margin: 0;
overflow:hidden;
}
<canvas id="canvas"></canvas>
You should also consider using requestAnimationFrame insted of setTimeout. From the MDN doc:
The Window.requestAnimationFrame() method tells the browser that you
wish to perform an animation and requests that the browser call a
specified function to update an animation before the next repaint. The
method takes as an argument a callback to be invoked before the
repaint.
The same example with requestAnimationFrame:
var canvas = document.getElementById("canvas");
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
var ctx = canvas.getContext("2d");
var frames = 0; // Frame counter just to make sure you don't crash your browser while editing code!
// DOT STUFF:
var dotSize = 20;
var dotMinY = 0 + dotSize; // Start position
var dotMaxY = canvas.height - dotSize; // Floor
var dotY = dotMinY;
var dotSpeed = 0;
var dotLastBounceSpeed = 0; // You can use this to determine whether the ball is still bouncing enough to be visible by the user.
var center = canvas.width / 2; // Try to take every operation you can out of the animate function.
var pi2 = 2 * Math.PI;
// WORLD STUFF:
var gravity = .5;
var bounceFactor = .8; // If < 1, bouncing absorbs energy so ball won't go as high as it was before.
// MAIN ANIMATION LOOP:
function animate() {
ctx.clearRect( 0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(center, dotY, dotSize, 0, pi2);
ctx.fillStyle = "red";
ctx.fill();
// First, dotSpeed += gravity is calculated and that returns the new value for dotSpeed
// then, that new value is added to dotY.
dotY += dotSpeed += gravity;
if(dotY >= dotMaxY ) {
dotY = dotMaxY;
dotSpeed *= -bounceFactor;
}
var dotCurrentBounceSpeed = Math.round(dotSpeed * 100); // Takes two decimal digits.
if(frames++ < 5000 && dotLastBounceSpeed != dotCurrentBounceSpeed) {
dotLastBounceSpeed = dotCurrentBounceSpeed;
//setTimeout(animate, 10);
window.requestAnimationFrame(animate); // Better!!
}
else alert("Animation end. Took " + frames + " frames.");
}
animate();
html, body, #canvas {
position:relative;
width: 100%;
height: 100%;
margin: 0;
overflow:hidden;
}
<canvas id="canvas"></canvas>
As you can see, you only need to change one line of code! However, you may need a polyfill so that you fall back to setTimeout if the browser does not support requestAnimationFrame.
You can learn more about requestAnimationFrame in this post. It explains the basics and also how to set a custom frame rate.

The basic principle is to use a velocity variable as opposed to a constant height increment. So instead of dotHeight += 1 or dotHeight -= 1 you would do dotHeight += dotVelocity, where you define dotVelocity, and subtract it by a constant value (gravity) whenever the ball is in the air.
var dotHeight = 0;
var dotVelocity = 3; // start out moving up, like in your example
var gravity = .1; // you can adjust this constant for stronger/weaker gravity
setInterval(function animate() {
ctx.clearRect( 0, 0, canvas.width, canvas.height);
if (dotHeight > 0) { // if it hit the ground, stop movement
dotVelocity -= gravity;
dotHeight += dotVelocity;
}
ctx.beginPath();
ctx.arc(canvas.width / 2, (canvas.height / 2) + dotHeight, dotSize, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
}, 4);

You could use a speed variable instead of the constant 1 to determine how "far" to move the ball, like this:
var speed = 1;
setInterval(function animate() {
ctx.clearRect( 0, 0, canvas.width, canvas.height);
if (movement1 === true) {
dotHeight += speed;
if (dotHeight >= 100) movement1 = false;
// make it faster
speed += 1;
} else {
dotHeight -= speed;
if (dotHeight <= 0) movement1 = true;
// slow down
speed -= 1;
}
ctx.beginPath();
ctx.arc(canvas.width / 2, (canvas.height / 2) + dotHeight, dotSize, 0, 2 * Math.PI);
ctx.fillStyle = "white";
ctx.fill();
}, 4);

Related

How to Improve Html5 Canvas Performance

So I have this project I have been working on and the goal of it is to randomly generate terrain on a 2D plane, and put rain in the background, and I chose to use the html5 canvas element to accomplish this goal. After creating it I am happy with the result but I am having performance issues and could use some advice on how to fix it. So far I have tried to only clear the bit of the canvas that is needed, which is above the rectangles I drew under the terrain to fill it in, but because of this I have to redraw the circles. The rn(rain number) has already been lowered by about 2 times and it still lags, any suggestions?
Note - The code in the snippet does not lag due to it's small size, but if I was to run it in full screen with the actual rain number(800), it would lag. I have shrunk the values to fit the snippet.
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
for (i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
c.fillRect(x, y, 4, canvas.height - y);
}
}
function gameloop() {
//clearing canvas
for (i = 0; i < tn; i++) {
c.clearRect(tp[i].x - 2, 0, 2, tp[i].y);
}
for (i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain drawing
c.fillRect(rp[i].x, rp[i].y, rp[i].w, 6);
}
for (i = 0; i < tn; i++) {
//terrain drawing
c.beginPath();
c.arc(tp[i].x, tp[i].y, 6, 0, 7);
c.fill();
}
}
setup();
setInterval(gameloop, 1000 / 60);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<html>
<head>
<link rel="stylesheet" href="index.css">
<title>A Snowy Night</title>
</head>
<body id="body"> <canvas id="gamecanvas"></canvas>
<script src="index.js"></script>
</body>
</html>
Superimposing canvas
Like I suggested in my comment, the use of a second canvas point is to only have to draw the terrain once, and hence it could enhance the performance of your animation by saving a redraw on each new frame. This can be done with CSS by positioning one on the other (like layers).
#canvasBase {
position: relative;
}
#canvasLayer1 {
position: absolute;
top: 0;
left: 0;
}
#canvasLayer2 {
position: absolute;
top: 0;
left: 0;
}
// etc...
Also I advise you to use requestAnimationFrame over setinterval (see why).
requestAnimationFrame
However, by using requestAnimationFrame, we don't control the refresh rate, it's tied to the client hardware. So we need to handle it and for that, we will use the DOMHighResTimeStamp which is passed as an argument to our callback method.
The idea is to let it run at native speed and manage the fps by updating the logic (our calculs) only at desired time. For exemple, if we need a fps = 60; that means we need to update our logic every 1000 / 60 = ~16,67 ms. So we check if the deltaTime with the time of the last frame is equal or superior than ~16,67ms. If not enough time elapsed, we call a new frame & we return (important, otherwise the control we just did is useless as the code keeps going whatever the outcome of it).
let fps = 60;
/* Check if we need to update the logic */
/* if not request a new frame & return */
if(deltaLastUpdate <= 1000 / fps){ // 1000 / 60 = ~16,67ms
requestAnimationFrame(animate);
return;
}
Clearing canvas
As you need to erase all the past rain drops, the simplest & cheapest in ressources in to clear the whole context in one swoop.
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
Path2D
As your drawing use the same color for the rain drops, you can as well group all these in one path:
rainPath = new Path2D();
...
So you will need only one instruction to draw them (same ressources saving type as the clearRect):
ctxRain.fill(rainPath);
Result
/* CANVAS "Terrain" */
const terrainCanvas = document.getElementById('gameTerrain');
const ctxTerrain = terrainCanvas.getContext('2d');
terrainCanvas.height = window.innerHeight;
terrainCanvas.width = window.innerWidth;
/* CANVAS "Rain" */
const rainCanvas = document.getElementById('gameRain');
const ctxRain = rainCanvas.getContext('2d');
rainCanvas.height = window.innerHeight;
rainCanvas.width = window.innerWidth;
/* Game Constants */
const wind = 5;
const rainMaxParticules = 100;
const rain = [];
let rainPath;
const terrainMaxParticules = terrainCanvas.width + 20;
const terrain = [];
let terrainPath;
/* Maths help */
const ma = Math.random;
const mo = Math.round;
/* Clear */
function clearTerrain(){
ctxTerrain.clearRect(0, 0, terrainCanvas.width, terrainCanvas.height);
}
function clearRain(){
ctxRain.clearRect(0, 0, rainCanvas.width, rainCanvas.height);
}
/* Logic */
function initTerrain(){
terrain[0] = { x: -2, y: terrainCanvas.height - 50 };
for (let i = 1; i <= terrainMaxParticules; i++) {
let x = terrain[i - 1].x + 2;
let y = terrain[i - 1].y + (ma() * 20) - 10;
if (y > terrainCanvas.height - 50) {
y = terrain[i - 1].y -= 1;
}
if (y < terrainCanvas.height - 100) {
y = terrain[i - 1].y += 1;
}
terrain[i] = { x, y };
}
}
function initRain(){
for (let i = 0; i < rainMaxParticules; i++) {
let x = mo(ma() * rainCanvas.width);
let y = mo(ma() * rainCanvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rain[i] = { x, y, w, s };
}
}
function init(){
initTerrain();
initRain();
}
function updateTerrain(){
terrainPath = new Path2D();
for(let i = 0; i < terrain.length; i++){
terrainPath.arc(terrain[i].x, terrain[i].y, 6, Math.PI/2, 5*Math.PI/2);
}
terrainPath.lineTo(terrainCanvas.width, terrainCanvas.height);
terrainPath.lineTo(0, terrainCanvas.height);
}
function updateRain(){
rainPath = new Path2D();
for (let i = 0; i < rain.length; i++) {
// Rain looping
if (rain[i].y > rainCanvas.height + 5) {
rain[i].y = -5;
}
if (rain[i].x > rainCanvas.width + 5) {
rain[i].x = -5;
}
// Rain movement
rain[i].y += rain[i].s;
rain[i].x += wind;
// Path containing all the drops
rainPath.rect(rain[i].x, rain[i].y, rain[i].w, 6);
}
}
/* Drawing */
function drawTerrain(){
ctxTerrain.fillStyle = 'black';
ctxTerrain.fill(terrainPath);
}
function drawRain(){
ctxRain.fillStyle = 'black';
ctxRain.fill(rainPath);
}
/* Animation Constant */
const fps = 60;
let lastTimestampUpdate;
let terrainDrawn = false;
/* Game loop */
function animate(timestamp){
/* Initialize rain & terrain particules */
if(rain.length === 0 || terrain.length === 0){
init();
}
/* Define "lastTimestampUpdate" from the first call */
if (lastTimestampUpdate === undefined){
lastTimestampUpdate = timestamp;
}
/* Check if we need to update the logic & the drawing, if not, request a new frame & return */
if(timestamp - lastTimestampUpdate <= 1000 / fps){
requestAnimationFrame(animate);
return;
}
if(!terrainDrawn){
/* Terrain --------------------- */
/* Clear */
clearTerrain();
/* Logic */
updateTerrain();
/* Draw */
drawTerrain();
/* ----------------------------- */
terrainDrawn = true;
}
/* --- Rain -------------------- */
/* Clear */
clearRain();
/* Logic */
updateRain();
/* Draw */
drawRain();
/* ----------------------------- */
/* Request another frame */
lastTimestampUpdate = timestamp;
requestAnimationFrame(animate);
}
/* Start the animation */
requestAnimationFrame(animate);
body {
background-color: white;
overflow: hidden;
margin: 0;
}
#gameTerrain {
position: relative;
}
#gameRain {
position: absolute;
top: 0;
left: 0;
}
<body>
<canvas id="gameTerrain"></canvas>
<canvas id="gameRain"></canvas>
</body>
Aside
This won't affect performance, however I encourage you to use const & let over var (What's the difference between using “let” and “var”?).
Generally, having more paint instructions will be what costs the most, the complexity of these paint instructions only comes to play when it's really complex.
Here you are spamming the GPU with paint instructions:
(canvas.width) + 20 calls to clearRect(). clearRect() is a paint instruction, and not a cheap one. Use it sporadically, but actually, you should use it only to clear the whole context.
One fillRect() per rain drop.. They're all the same color, they can be merged in a single sub-path and drawn in a single draw call.
One fill per circle composing the terrain.
So instead of this huge number of draw calls, we could make it in only two draw calls:
One clearRect, one fill() of one big subpath containing both the drops and
the terrain.
However it's certainly more practical to keep the terrain and the rain separated, so let's make it three draw calls, by keeping the terrain in its own Path2D object, which is more friendly for the CPU:
var canvas = document.getElementById('gamecanvas');
var c = canvas.getContext('2d');
var ma = Math.random;
var mo = Math.round;
var wind = 5;
var rn = 100;
var rp = [];
// this will hold our Path2D object
// which will hold the full terrain drawing
// set a 'let' because we will set it again on resize
let terrain;
var tp = [];
var tn;
function setup() {
//fillstyle
c.fillStyle = 'black';
//canvas size
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
//rain setup
for (let i = 0; i < rn; i++) {
let x = mo(ma() * canvas.width);
let y = mo(ma() * canvas.width);
let w = mo(ma() * 1) + 1;
let s = mo(ma() * 5) + 10;
rp[i] = { x, y, w, s };
}
//terrain setup
tn = (canvas.width) + 20;
tp[0] = { x: -2, y: canvas.height - 50 };
terrain = new Path2D();
for (let i = 1; i <= tn; i++) {
let x = tp[i - 1].x + 2;
let y = tp[i - 1].y + (ma() * 20) - 10;
if (y > canvas.height - 50) {
y = tp[i - 1].y -= 1;
}
if (y < canvas.height - 100) {
y = tp[i - 1].y += 1;
}
tp[i] = { x, y };
terrain.rect(x, y, 4, canvas.height - y);
terrain.arc(x, y, 6, 0, Math.PI*2);
}
}
function gameloop() {
// clear the whole canvas
c.clearRect(0, 0, canvas.width, canvas.height);
// start a new sub-path for the rain
c.beginPath();
for (let i = 0; i < rn; i++) {
//rain looping
if (rp[i].y > canvas.height + 5) {
rp[i].y = -5;
}
if (rp[i].x > canvas.width + 5) {
rp[i].x = -5;
}
//rain movement
rp[i].y += rp[i].s;
rp[i].x += wind;
//rain tracing
c.rect(rp[i].x, rp[i].y, rp[i].w, 6);
}
// paint all the drops in a single op
c.fill();
// paint the whole terrain in a single op
c.fill(terrain);
// loop at screen refresh frequency
requestAnimationFrame(gameloop);
}
setup();
requestAnimationFrame(gameloop);
onresize = () => setup();
body {
background-color: white;
overflow: hidden;
margin: 0;
}
canvas {
background-color: white;
}
<canvas id="gamecanvas"></canvas>
Further possible improvements:
Instead of making our terrain path a set of rectangles, using only lineTo to trace the actual outline would probably help a bit, some more calculations at init, but it's done only once in a while.
If the terrain becomes more complex, with more details, or with various colors and shadows etc. then consider painting it only once, and then produce an ImageBitmap from the canvas. Then in gameLoop you'll just have to drawImage that ImageBitmap (drawing bitmaps is super fast, but storing it consumes memory, so remember to .close() the ImageBitmap when you don't need it anymore).

Creating trail over an already made canvas

So I am trying to implement a concept of shooting star over an already drawn canvas of slowly moving stars. But I haven't found a way to do so. I tried implementing an array to make it look so but the trail isn't as efficient.
This code is as follows:
var canvas = document.querySelector('canvas');
var c = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var mouse = {
x : innerWidth/2,
y : innerHeight/2
};
var colors = [
'#3399CC',
'#67B8DE',
'#91C9E8',
'#B4DCED',
'#E8F8FF'
];
addEventListener('resize', function () {
canvas.width = innerWidth;
canvas.height = innerHeight;
init();
});
var isClicked = false;
addEventListener('click', function () {
mouse.x = event.clientX;
mouse.y = event.clientY;
isClicked = true;
});
function randomIntFromRange (min, max) {
return Math.floor(Math.random() * (max - min + 1) + min);
}
function randomColor (colors) {
return colors[Math.floor(Math.random() * colors.length)];
}
function Stars (x, y, radius, dy, color) {
this.x = x;
this.y = y;
this.radius = radius;
this.dy = dy;
this.color = color;
this.draw = function () {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
c.shadowColor = this.color;
c.shadowBlur = 15;
c.shadowOffsetX = 0;
c.shadowOffsetY = 0;
c.fillStyle = this.color;
c.fill();
c.closePath();
}
this.update = function () {
if (this.y < -10) {
this.y = canvas.height + 10;
this.x = randomIntFromRange(this.radius, canvas.width);
}
this.y -= this.dy;
this.draw();
}
}
function ShootingStar (x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
this.draw = function () {
c.beginPath();
c.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, false);
c.shadowColor = "red";
c.shadowBlur = 15;
c.shadowOffsetX = 0;
c.shadowOffsetY = 0;
c.fillStyle = "red";
c.fill();
c.closePath();
}
this.update = function () {
this.x += 10;
this.y += 10;
this.draw();
}
}
let stars = [];
let shooting_star = [];
function init () {
stars = [];
for (var i = 0; i < 300; i++) {
var stars_radius = randomIntFromRange(2, 3);
var stars_x = randomIntFromRange(stars_radius, canvas.width);
var stars_y = randomIntFromRange(stars_radius, canvas.height);
var stars_dy = Math.random() / 6;
var color = randomColor(colors);
stars.push(new Stars(stars_x, stars_y, stars_radius, stars_dy, color));
}
}
function Explode () {
shooting_star = [];
var shooting_star_radius = 3;
var shooting_star_x = mouse.x;
var shooting_star_y = mouse.y;
for (var i = 0; i < 50; i++) {
shooting_star.push(new ShootingStar(shooting_star_x, shooting_star_y, shooting_star_radius));
if (shooting_star_radius > 0.2) {
shooting_star_radius -= .2;
}
var initiator = randomIntFromRange(-1, 1);
console.log(initiator);
shooting_star_x -= 3;
shooting_star_y -= 3;
}
}
function animate () {
requestAnimationFrame(animate);
c.clearRect(0, 0, canvas.width, canvas.height);
for (var i = 0; i < stars.length; i++)
stars[i].update();
for (var i = 0; i < shooting_star.length; i++)
shooting_star[i].update();
if (isClicked == true) {
Explode();
isClicked = false;
}
}
init();
animate();
Here is the jsfiddle to it
https://jsfiddle.net/qjug4qdz/
I basically want the shooting star to come from a random location to the point where my mouse is clicked, but the trail is difficult to work with using an array.
Basic particles
For the particular effect you are looking for you can use a basic particle system.
As the shooting star moves you will drop a particle that starts with the star velocity and then slows and fades out.
The particle
You first start with the particle. I like to use Object.assign when creating objects but you can use any method you like, class, new, factory...
// This defines a particle and is copied to create new particles
const starDust = {
x : 0, // the current position
y : 0,
dx : 0, // delta x,y (velocity)
dy : 0,
drag : 0, // the rate that the particle slows down.
life : 0, // count down till particle is removed
age : 0, // the starting value of life
draw(){ // function to update and draw the particle
this.x += this.dx; // move it
this.y += this.dy;
this.dx *= this.drag; // slow it down
this.dy *= this.drag;
const unitLife = (this.life / this.age); // get the life left as a value
// from 0 to 1 where 0 is end
ctx.globalAlpha = unitLife; // set the alpha
ctx.beginPath();
ctx.arc(this.x,this.y,4,0,Math.PI); // draw the particle
this.life -= 1; // count down
return this.life > 0; // return true if still alive
}
Be memory aware.
A common mistake when creating particle systems is that people forget that creating and destroying objects will add a lot of work to javascripts memory management. The worst of which is GC (Garbage Collection). GC is a major source of lag and if you are wasteful with memory it will impact the quality of the animation. For simple particles it may not be noticeable, but you may want hundreds of complex particles spawning each frame. This is when GC realy hurts the animation.
Most Game engines reduce the GC impact by reusing objects rather than dereferencing and recreating. A common method is an object pool, where a second array holds objects that are no longer used. When a new object is needed then the pool is first checked, if there is an unused object, it is used, else a new object is created.
This way you never delete any particles, greatly reducing the GC workload, and preventing your animation from dropping frames (if you use a lot of particles)
Particle needs initializer
But you need to provide a way to re-initialize the object. Thus add the function init to the particle that will set it up to be used again
init(x,y,vx,vy){ // where x,y and velocity vx,vy of shooting star
this.x = x;
this.y = y;
this.dx = vx;
this.dy = vy;
// give a random age
this.age = (Math.random() * 100 + 60) | 0; // in frames and | 0 floors the value
this.life = this.age; // and set the life countdown
this.drag = Math.random() * 0.01 + 0.99; // the drag that slows the particle down
}
} // end of starDust object.
The arrays
To manage all the particles we create object that has arrays and methods for adding, creating and rendering the particles. In this case I will call it dust
const dust = {
particles : [], // array of active particles
pool : [], // array of unused particels
createParticle(particleDesc){ // creates a new particle from particleDesc
return Object.assign({},particleDesc);
},
add(x,y,vx,vy){ // where x,y and velocity vx,vy
var dust;
if(this.pool.length){ // are there any particles in the pool
dust = this.pool.pop(); // get one
}else{ // else there are no spare particles so create a new one
dust = this.createParticle(starDust);
}
dust.init(x,y,vx,vy); // init the particle
this.items.push(dust); // put it in the active particle array
return dust; // return it (sometimes you want to do something with it)
},
draw(){ // updates and draws all active particles
var i = 0;
while(i < this.items.length){ // iterate each particle in items
if(this.items[i].draw() === false){ // is it dead??
this.pool.push(this.items.splice(i,1)[0]); // if dead put in the pool for later
}else{ i ++ } // if not dead get index of next particle
}
}
}//end of dust object
Using the particle system
The simplest way to create a particle is to use a random number and set the chance of a particle being created every frame.
In your main loop
// assuming that the falling star is called star and has an x,y and dx,dy (delta)
if(star) { // only if there is a start to spawn from
// add a particle once every 10 frame (on average
if(Math.random() < 0.1) {
dust.add(star.x, star.y, star.dx, star.dy); // add some dust at the shooting starts position and speed
}
}
dust.draw(); // draw all particles
And that is it.

Animating a section of a path using canvas

I'm trying to animate a section of a path using canvas. Basically I have a straight line that runs through three links. When you hover over a link the section of the path behind the link should animate into a sine wave.
I have no problem animating the whole path into the shape of a sine wave but when it comes to animating a section of a path, I'm at a loss :(
I've attached a reference image below to give an idea of what it is I'm trying to achieve.
Below is jsfiddle of the code I'm currently using to animate the path. I'm a canvas noob so forgive me if it awful...
https://jsfiddle.net/9mu8xo0L/
and here is the code:
class App {
constructor() {
this.drawLine();
}
drawLine() {
this.canvas = document.getElementById('sine-wave');
this.canvas.width = 1000;
this.ctx = this.canvas.getContext("2d");
this.cpY = 0;
this.movement = 1;
this.fps = 60;
this.ctx.moveTo(0, 180);
this.ctx.lineTo(1000, 180);
this.ctx.stroke();
this.canvas.addEventListener('mouseover', this.draw.bind(this));
}
draw() {
setTimeout(() => {
if (this.cpY >= 6) return;
requestAnimationFrame(this.draw.bind(this));
// animate the control point
this.cpY += this.movement;
const increase = (90 / 180) * (Math.PI / 2);
let counter = 0;
let x = 0;
let y = 180;
this.ctx.beginPath();
this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
for(let i = 0; i <= this.canvas.width; i += 6) {
this.ctx.moveTo(x,y);
x = i;
y = 180 - Math.sin(counter) * this.cpY;
counter += increase;
this.ctx.lineTo(x, y);
this.ctx.stroke();
}
}, 1000 / this.fps);
}
}
Simply split the drawing of the line in three parts, drawing 1/3 as straight line, animate the mid section and add the last 1/3.
I'll demonstrate the first 1/3 + animation and leave the last 1/3 as an exercise (also moved the stroke() outside the loop so it doesn't overdraw per segment) - there is room for refactoring and optimizations here but I have not addressed that in this example -
let x = this.canvas.width / 3; // start of part 2 (animation)
let y = 180;
this.ctx.beginPath();
this.ctx.clearRect(0, x, this.canvas.width, this.canvas.height);
// draw first 1/3
this.ctx.moveTo(0, y);
this.ctx.lineTo(x, y);
// continue with part 2
for(let i = x; i <= this.canvas.width; i += 6) {
x = i;
y = 180 - Math.sin(counter) * this.cpY;
counter += increase;
// add to 2. segment
this.ctx.lineTo(x, y);
}
// stroke line
this.ctx.stroke();
Modified fiddle

How to create a camera view in canvas that will follow a players rotation and rotation?

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>

HTML5 Canvas camera/viewport - how to actually do it?

I'm sure this was solven 1000 times before: I got a canvas in the size of 960*560 and a room in the size of 5000*3000 of which always only 960*560 should be drawn, depending on where the player is. The player should be always in the middle, but when near to borders - then the best view should be calculated). The player can move entirely free with WASD or the arrow keys. And all objects should move themselves - instead of that i move everything else but the player to create the illusion that the player moves.
I now found those two quesitons:
HTML5 - Creating a viewport for canvas works, but only for this type of game, i can't reproduce the code for mine.
Changing the view "center" of an html5 canvas seems to be more promising and also perfomant, but i only understand it for drawing all other objects correctly relative to the player and not how to scroll the canvas viewport relative to the player, which i want to achieve first of course.
My code (simplified - the game logic is seperately):
var canvas = document.getElementById("game");
canvas.tabIndex = 0;
canvas.focus();
var cc = canvas.getContext("2d");
// Define viewports for scrolling inside the canvas
/* Viewport x position */ view_xview = 0;
/* Viewport y position */ view_yview = 0;
/* Viewport width */ view_wview = 960;
/* Viewport height */ view_hview = 560;
/* Sector width */ room_width = 5000;
/* Sector height */ room_height = 3000;
canvas.width = view_wview;
canvas.height = view_hview;
function draw()
{
clear();
requestAnimFrame(draw);
// World's end and viewport
if (player.x < 20) player.x = 20;
if (player.y < 20) player.y = 20;
if (player.x > room_width-20) player.x = room_width-20;
if (player.y > room_height-20) player.y = room_height-20;
if (player.x > view_wview/2) ... ?
if (player.y > view_hview/2) ... ?
}
The way i am trying to get it working feels totally wrong and i don't even know how i am trying it... Any ideas? What do you think about the context.transform-thing?
I hope you understand my description and that someone has an idea. Kind regards
LIVE DEMO at jsfiddle.net
This demo illustrates the viewport usage in a real game scenario. Use arrows keys to move the player over the room. The large room is generated on the fly using rectangles and the result is saved into an image.
Notice that the player is always in the middle except when near to borders (as you desire).
Now I'll try to explain the main portions of the code, at least the parts that are more difficult to understand just looking at it.
Using drawImage to draw large images according to viewport position
A variant of the drawImage method has eight new parameters. We can use this method to slice parts of a source image and draw them to the canvas.
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
The first parameter image, just as with the other variants, is either a reference to an image object or a reference to a different canvas element. For the other eight parameters it's best to look at the image below. The first four parameters define the location and size of the slice on the source image. The last four parameters define the position and size on the destination canvas.
Font: https://developer.mozilla.org/en-US/docs/Web/Guide/HTML/Canvas_tutorial/Using_images
How it works in demo:
We have a large image that represents the room and we want to show on canvas only the part within the viewport. The crop position (sx, sy) is the same position of the camera (xView, yView) and the crop dimensions are the same as the viewport(canvas) so sWidth=canvas.width and sHeight=canvas.height.
We need to take care about the crop dimensions because drawImage draws nothing on canvas if the crop position or crop dimensions based on position are invalid. That's why we need the if sections bellow.
var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;
// offset point to crop the image
sx = xView;
sy = yView;
// dimensions of cropped image
sWidth = context.canvas.width;
sHeight = context.canvas.height;
// if cropped image is smaller than canvas we need to change the source dimensions
if(image.width - sx < sWidth){
sWidth = image.width - sx;
}
if(image.height - sy < sHeight){
sHeight = image.height - sy;
}
// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;
// draw the cropped image
context.drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
Drawing game objects related to viewport
When writing a game it's a good practice separate the logic and the rendering for each object in game. So in demo we have update and draw functions. The update method changes object status like position on the "game world", apply physics, animation state, etc. The draw method actually render the object and to render it properly considering the viewport, the object need to know the render context and the viewport properties.
Notice that game objects are updated considering the game world's position. That means the (x,y) position of the object is the position in world. Despite of that, since the viewport is changing, objects need to be rendered properly and the render position will be different than world's position.
The conversion is simple:
object position in world(room): (x, y)
viewport position: (xView, yView)
render position: (x-xView, y-yView)
This works for all kind of coordinates, even the negative ones.
Game Camera
Our game objects have a separated update method. In Demo implementation, the camera is treated as a game object and also have a separated update method.
The camera object holds the left top position of viewport (xView, yView), an object to be followed, a rectangle representing the viewport, a rectangle that represents the game world's boundary and the minimal distance of each border that player could be before camera starts move (xDeadZone, yDeadZone). Also we defined the camera's degrees of freedom (axis). For top view style games, like RPG, the camera is allowed to move in both x(horizontal) and y(vertical) axis.
To keep player in the middle of viewport we set the deadZone of each axis to converge with the center of canvas. Look at the follow function in the code:
camera.follow(player, canvas.width/2, canvas.height/2)
Note: See the UPDATE section below as this will not produce the expected behavior when any dimension of the map (room) is smaller than canvas.
World's limits
Since each object, including camera, have its own update function, its easy to check the game world's boundary. Only remember to put the code that block the movement at the final of the update function.
Demonstration
See the full code and try it yourself. Most parts of the code have comments that guide you through. I'll assume that you know the basics of Javascript and how to work with prototypes (sometimes I use the term "class" for a prototype object just because it have a similar behavior of a Class in languages like Java).
LIVE DEMO
Full code:
<!DOCTYPE HTML>
<html>
<body>
<canvas id="gameCanvas" width=400 height=400 />
<script>
// wrapper for our game "classes", "methods" and "objects"
window.Game = {};
// wrapper for "class" Rectangle
(function() {
function Rectangle(left, top, width, height) {
this.left = left || 0;
this.top = top || 0;
this.width = width || 0;
this.height = height || 0;
this.right = this.left + this.width;
this.bottom = this.top + this.height;
}
Rectangle.prototype.set = function(left, top, /*optional*/ width, /*optional*/ height) {
this.left = left;
this.top = top;
this.width = width || this.width;
this.height = height || this.height
this.right = (this.left + this.width);
this.bottom = (this.top + this.height);
}
Rectangle.prototype.within = function(r) {
return (r.left <= this.left &&
r.right >= this.right &&
r.top <= this.top &&
r.bottom >= this.bottom);
}
Rectangle.prototype.overlaps = function(r) {
return (this.left < r.right &&
r.left < this.right &&
this.top < r.bottom &&
r.top < this.bottom);
}
// add "class" Rectangle to our Game object
Game.Rectangle = Rectangle;
})();
// wrapper for "class" Camera (avoid global objects)
(function() {
// possibles axis to move the camera
var AXIS = {
NONE: 1,
HORIZONTAL: 2,
VERTICAL: 3,
BOTH: 4
};
// Camera constructor
function Camera(xView, yView, viewportWidth, viewportHeight, worldWidth, worldHeight) {
// position of camera (left-top coordinate)
this.xView = xView || 0;
this.yView = yView || 0;
// distance from followed object to border before camera starts move
this.xDeadZone = 0; // min distance to horizontal borders
this.yDeadZone = 0; // min distance to vertical borders
// viewport dimensions
this.wView = viewportWidth;
this.hView = viewportHeight;
// allow camera to move in vertical and horizontal axis
this.axis = AXIS.BOTH;
// object that should be followed
this.followed = null;
// rectangle that represents the viewport
this.viewportRect = new Game.Rectangle(this.xView, this.yView, this.wView, this.hView);
// rectangle that represents the world's boundary (room's boundary)
this.worldRect = new Game.Rectangle(0, 0, worldWidth, worldHeight);
}
// gameObject needs to have "x" and "y" properties (as world(or room) position)
Camera.prototype.follow = function(gameObject, xDeadZone, yDeadZone) {
this.followed = gameObject;
this.xDeadZone = xDeadZone;
this.yDeadZone = yDeadZone;
}
Camera.prototype.update = function() {
// keep following the player (or other desired object)
if (this.followed != null) {
if (this.axis == AXIS.HORIZONTAL || this.axis == AXIS.BOTH) {
// moves camera on horizontal axis based on followed object position
if (this.followed.x - this.xView + this.xDeadZone > this.wView)
this.xView = this.followed.x - (this.wView - this.xDeadZone);
else if (this.followed.x - this.xDeadZone < this.xView)
this.xView = this.followed.x - this.xDeadZone;
}
if (this.axis == AXIS.VERTICAL || this.axis == AXIS.BOTH) {
// moves camera on vertical axis based on followed object position
if (this.followed.y - this.yView + this.yDeadZone > this.hView)
this.yView = this.followed.y - (this.hView - this.yDeadZone);
else if (this.followed.y - this.yDeadZone < this.yView)
this.yView = this.followed.y - this.yDeadZone;
}
}
// update viewportRect
this.viewportRect.set(this.xView, this.yView);
// don't let camera leaves the world's boundary
if (!this.viewportRect.within(this.worldRect)) {
if (this.viewportRect.left < this.worldRect.left)
this.xView = this.worldRect.left;
if (this.viewportRect.top < this.worldRect.top)
this.yView = this.worldRect.top;
if (this.viewportRect.right > this.worldRect.right)
this.xView = this.worldRect.right - this.wView;
if (this.viewportRect.bottom > this.worldRect.bottom)
this.yView = this.worldRect.bottom - this.hView;
}
}
// add "class" Camera to our Game object
Game.Camera = Camera;
})();
// wrapper for "class" Player
(function() {
function Player(x, y) {
// (x, y) = center of object
// ATTENTION:
// it represents the player position on the world(room), not the canvas position
this.x = x;
this.y = y;
// move speed in pixels per second
this.speed = 200;
// render properties
this.width = 50;
this.height = 50;
}
Player.prototype.update = function(step, worldWidth, worldHeight) {
// parameter step is the time between frames ( in seconds )
// check controls and move the player accordingly
if (Game.controls.left)
this.x -= this.speed * step;
if (Game.controls.up)
this.y -= this.speed * step;
if (Game.controls.right)
this.x += this.speed * step;
if (Game.controls.down)
this.y += this.speed * step;
// don't let player leaves the world's boundary
if (this.x - this.width / 2 < 0) {
this.x = this.width / 2;
}
if (this.y - this.height / 2 < 0) {
this.y = this.height / 2;
}
if (this.x + this.width / 2 > worldWidth) {
this.x = worldWidth - this.width / 2;
}
if (this.y + this.height / 2 > worldHeight) {
this.y = worldHeight - this.height / 2;
}
}
Player.prototype.draw = function(context, xView, yView) {
// draw a simple rectangle shape as our player model
context.save();
context.fillStyle = "black";
// before draw we need to convert player world's position to canvas position
context.fillRect((this.x - this.width / 2) - xView, (this.y - this.height / 2) - yView, this.width, this.height);
context.restore();
}
// add "class" Player to our Game object
Game.Player = Player;
})();
// wrapper for "class" Map
(function() {
function Map(width, height) {
// map dimensions
this.width = width;
this.height = height;
// map texture
this.image = null;
}
// creates a prodedural generated map (you can use an image instead)
Map.prototype.generate = function() {
var ctx = document.createElement("canvas").getContext("2d");
ctx.canvas.width = this.width;
ctx.canvas.height = this.height;
var rows = ~~(this.width / 44) + 1;
var columns = ~~(this.height / 44) + 1;
var color = "red";
ctx.save();
ctx.fillStyle = "red";
for (var x = 0, i = 0; i < rows; x += 44, i++) {
ctx.beginPath();
for (var y = 0, j = 0; j < columns; y += 44, j++) {
ctx.rect(x, y, 40, 40);
}
color = (color == "red" ? "blue" : "red");
ctx.fillStyle = color;
ctx.fill();
ctx.closePath();
}
ctx.restore();
// store the generate map as this image texture
this.image = new Image();
this.image.src = ctx.canvas.toDataURL("image/png");
// clear context
ctx = null;
}
// draw the map adjusted to camera
Map.prototype.draw = function(context, xView, yView) {
// easiest way: draw the entire map changing only the destination coordinate in canvas
// canvas will cull the image by itself (no performance gaps -> in hardware accelerated environments, at least)
/*context.drawImage(this.image, 0, 0, this.image.width, this.image.height, -xView, -yView, this.image.width, this.image.height);*/
// didactic way ( "s" is for "source" and "d" is for "destination" in the variable names):
var sx, sy, dx, dy;
var sWidth, sHeight, dWidth, dHeight;
// offset point to crop the image
sx = xView;
sy = yView;
// dimensions of cropped image
sWidth = context.canvas.width;
sHeight = context.canvas.height;
// if cropped image is smaller than canvas we need to change the source dimensions
if (this.image.width - sx < sWidth) {
sWidth = this.image.width - sx;
}
if (this.image.height - sy < sHeight) {
sHeight = this.image.height - sy;
}
// location on canvas to draw the croped image
dx = 0;
dy = 0;
// match destination with source to not scale the image
dWidth = sWidth;
dHeight = sHeight;
context.drawImage(this.image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight);
}
// add "class" Map to our Game object
Game.Map = Map;
})();
// Game Script
(function() {
// prepaire our game canvas
var canvas = document.getElementById("gameCanvas");
var context = canvas.getContext("2d");
// game settings:
var FPS = 30;
var INTERVAL = 1000 / FPS; // milliseconds
var STEP = INTERVAL / 1000 // seconds
// setup an object that represents the room
var room = {
width: 500,
height: 300,
map: new Game.Map(500, 300)
};
// generate a large image texture for the room
room.map.generate();
// setup player
var player = new Game.Player(50, 50);
// Old camera setup. It not works with maps smaller than canvas. Keeping the code deactivated here as reference.
/* var camera = new Game.Camera(0, 0, canvas.width, canvas.height, room.width, room.height);*/
/* camera.follow(player, canvas.width / 2, canvas.height / 2); */
// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);
// Setup the camera
var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);
// Game update function
var update = function() {
player.update(STEP, room.width, room.height);
camera.update();
}
// Game draw function
var draw = function() {
// clear the entire canvas
context.clearRect(0, 0, canvas.width, canvas.height);
// redraw all objects
room.map.draw(context, camera.xView, camera.yView);
player.draw(context, camera.xView, camera.yView);
}
// Game Loop
var gameLoop = function() {
update();
draw();
}
// <-- configure play/pause capabilities:
// Using setInterval instead of requestAnimationFrame for better cross browser support,
// but it's easy to change to a requestAnimationFrame polyfill.
var runningId = -1;
Game.play = function() {
if (runningId == -1) {
runningId = setInterval(function() {
gameLoop();
}, INTERVAL);
console.log("play");
}
}
Game.togglePause = function() {
if (runningId == -1) {
Game.play();
} else {
clearInterval(runningId);
runningId = -1;
console.log("paused");
}
}
// -->
})();
// <-- configure Game controls:
Game.controls = {
left: false,
up: false,
right: false,
down: false,
};
window.addEventListener("keydown", function(e) {
switch (e.keyCode) {
case 37: // left arrow
Game.controls.left = true;
break;
case 38: // up arrow
Game.controls.up = true;
break;
case 39: // right arrow
Game.controls.right = true;
break;
case 40: // down arrow
Game.controls.down = true;
break;
}
}, false);
window.addEventListener("keyup", function(e) {
switch (e.keyCode) {
case 37: // left arrow
Game.controls.left = false;
break;
case 38: // up arrow
Game.controls.up = false;
break;
case 39: // right arrow
Game.controls.right = false;
break;
case 40: // down arrow
Game.controls.down = false;
break;
case 80: // key P pauses the game
Game.togglePause();
break;
}
}, false);
// -->
// start the game when page is loaded
window.onload = function() {
Game.play();
}
</script>
</body>
</html>
UPDATE
If width and/or height of the map (room) is smaller than canvas the previous code will not work properly. To resolve this, in the Game Script make the setup of the camera as followed:
// Set the right viewport size for the camera
var vWidth = Math.min(room.width, canvas.width);
var vHeight = Math.min(room.height, canvas.height);
var camera = new Game.Camera(0, 0, vWidth, vHeight, room.width, room.height);
camera.follow(player, vWidth / 2, vHeight / 2);
You just need to tell the camera constructor that viewport will be the smallest value between map (room) or canvas. And since we want the player centered and bonded to that viewport, the camera.follow function must be update as well.
Feel free to report any errors or to add suggestions.
Here is a simple example of this where we clamp the camera position to the bounds of the game world. This allows the camera to move through the game world and will never display any void space outside of the bounds you specify.
const worldBounds = {minX:-100,maxX:100,minY:-100,maxY:100};
function draw() {
ctx.setTransform(1,0,0,1,0,0);//reset the transform matrix as it is cumulative
ctx.clearRect(0, 0, canvas.width, canvas.height);//clear the viewport AFTER the matrix is reset
// update the player position
movePlayer();
// player is clamped to the world boundaries - don't let the player leave
player.x = clamp(player.x, worldBounds.minX, worldBounds.maxX);
player.y = clamp(player.y, worldBounds.minY, worldBounds.maxY);
// center the camera around the player,
// but clamp the edges of the camera view to the world bounds.
const camX = clamp(player.x - canvas.width/2, worldBounds.minX, worldBounds.maxX - canvas.width);
const camY = clamp(player.y - canvas.height/2, worldBounds.minY, worldBounds.maxY - canvas.height);
ctx.translate(-camX, -camY);
//Draw everything
}
And clamp just ensures that the value given is always between the specified min/max range :
// clamp(10, 20, 30) - output: 20
// clamp(40, 20, 30) - output: 30
// clamp(25, 20, 30) - output: 25
function clamp(value, min, max){
if(value < min) return min;
else if(value > max) return max;
return value;
}
Building on #dKorosec's example - use the arrow keys to move:
Fiddle
Here’s how to use canvas to be a viewport on another larger-than-canvas image
A viewport is really just a cropped portion of a larger image that is displayed to the user.
In this case, the viewport will be displayed to the user on a canvas (the canvas is the viewport).
First, code a move function that pans the viewport around the larger image.
This function moves the top/left corner of the viewport by 5px in the specified direction:
function move(direction){
switch (direction){
case "left":
left-=5;
break;
case "up":
top-=5;
break;
case "right":
left+=5;
break;
case "down":
top+=5
break;
}
draw(top,left);
}
The move function calls the draw function.
In draw(), the drawImage function will crop a specified portion of a larger image.
drawImage will also display that “cropped background” to the user on the canvas.
context.clearRect(0,0,game.width,game.height);
context.drawImage(background,cropLeft,cropTop,cropWidth,cropHeight,
0,0,viewWidth,viewHeight);
In this example,
Background is the full background image (usually not displayed but is rather a source for cropping)
cropLeft & cropTop define where on the background image the cropping will begin.
cropWidth & cropHeight define how large a rectangle will be cropped from the background image.
0,0 say that the sub-image that has been cropped from the background will be drawn at 0,0 on the viewport canvas.
viewWidth & viewHeight are the width and height of the viewport canvas
So here is an example of drawImage using numbers.
Let’s say our viewport (= our display canvas) is 150 pixels wide and 100 pixels high.
context.drawImage(background,75,50,150,100,0,0,150,100);
The 75 & 50 say that cropping will start at position x=75/y=50 on the background image.
The 150,100 say that the rectangle to be cropped will be 150 wide and 100 high.
The 0,0,150,100 say that the cropped rectangle image will be displayed using the full size of the viewport canvas.
That’s it for the mechanics of drawing a viewport…just add key-controls!
Here is code and a Fiddle: http://jsfiddle.net/m1erickson/vXqyc/
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
body{ background-color: ivory; }
canvas{border:1px solid red;}
</style>
<script>
$(function(){
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var game=document.getElementById("game");
var gameCtx=game.getContext("2d");
var left=20;
var top=20;
var background=new Image();
background.onload=function(){
canvas.width=background.width/2;
canvas.height=background.height/2;
gameCtx.fillStyle="red";
gameCtx.strokeStyle="blue";
gameCtx.lineWidth=3;
ctx.fillStyle="red";
ctx.strokeStyle="blue";
ctx.lineWidth=3;
move(top,left);
}
background.src="https://dl.dropboxusercontent.com/u/139992952/stackoverflow/game.jpg";
function move(direction){
switch (direction){
case "left":
left-=5;
break;
case "up":
top-=5;
break;
case "right":
left+=5;
break;
case "down":
top+=5
break;
}
draw(top,left);
}
function draw(top,left){
ctx.clearRect(0,0,canvas.width,canvas.height);
ctx.drawImage(background,0,0,background.width,background.height,0,0,canvas.width,canvas.height);
gameCtx.clearRect(0,0,game.width,game.height);
gameCtx.drawImage(background,left,top,250,150,0,0,250,150);
gameCtx.beginPath();
gameCtx.arc(125,75,10,0,Math.PI*2,false);
gameCtx.closePath();
gameCtx.fill();
gameCtx.stroke();
ctx.beginPath();
ctx.rect(left/2,top/2,125,75);
ctx.stroke();
ctx.beginPath();
ctx.arc(left/2+125/2,top/2+75/2,5,0,Math.PI*2,false);
ctx.stroke();
ctx.fill();
}
$("#moveLeft").click(function(){move("left");});
$("#moveRight").click(function(){move("right");});
$("#moveUp").click(function(){move("up");});
$("#moveDown").click(function(){move("down");});
}); // end $(function(){});
</script>
</head>
<body>
<canvas id="game" width=250 height=150></canvas><br>
<canvas id="canvas" width=500 height=300></canvas><br>
<button id="moveLeft">Left</button>
<button id="moveRight">Right</button>
<button id="moveUp">Up</button>
<button id="moveDown">Down</button>
</body>
</html>
#gustavo-carvalho's solution is phenomenal, but it involves extensive calculations and cognitive overhead. #Colton's approach is a step in the right direction; too bad it wasn't elaborated enough in his answer. I took his idea and ran with it to create this CodePen. It achieves exactly what #user2337969 is asking for using context.translate. The beauty is that this doesn't require offsetting any map or player coordinates so drawing them is as easy as using their x and y directly, which is much more straightforward.
Think of the 2D camera as a rectangle that pans inside a larger map. Its top-left corner is at (x, y) coordinates in the map, and its size is that of the canvas, i.e. canvas.width and canvas.height. That means that x can range from 0 to map.width - canvas.width, and y from 0 to map.height - canvas.height (inclusive). These are min and max that we feed into #Colton's clamp method.
To make it work however, I had to flip the sign on x and y since with context.translate, positive values shift the canvas to the right (making an illusion as if the camera pans to the left) and negative - to the left (as if the camera pans to the right).
This is a simple matter of setting the viewport to the target's x and y coordinates, as Colton states, on each frame. Transforms are not necessary but can be used as desired. The basic formula without translation is:
function update() {
// Assign the viewport to follow a target for this frame
var viewportX = canvas.width / 2 - target.x;
var viewportY = canvas.height / 2 - target.y;
// Draw each entity, including the target, relative to the viewport
ctx.fillRect(
entity.x + viewportX,
entity.y + viewportY,
entity.size,
entity.size
);
}
Clamping to the map is an optional second step to keep the viewport within world bounds:
function update() {
// Assign the viewport to follow a target for this frame
var viewportX = canvas.width / 2 - target.x;
var viewportY = canvas.height / 2 - target.y;
// Keep viewport in map bounds
viewportX = clamp(viewportX, canvas.width - map.width, 0);
viewportY = clamp(viewportY, canvas.height - map.height, 0);
// Draw each entity, including the target, relative to the viewport
ctx.fillRect(
entity.x + viewportX,
entity.y + viewportY,
entity.size,
entity.size
);
}
// Restrict n to a range between lo and hi
function clamp(n, lo, hi) {
return n < lo ? lo : n > hi ? hi : n;
}
Below are a few examples of this in action.
Without viewport translation, clamped:
const clamp = (n, lo, hi) => n < lo ? lo : n > hi ? hi : n;
const Ship = function (x, y, angle, size, color) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.ax = 0;
this.ay = 0;
this.rv = 0;
this.angle = angle;
this.accelerationAmount = 0.05;
this.decelerationAmount = 0.02;
this.friction = 0.9;
this.rotationSpd = 0.01;
this.size = size;
this.radius = size;
this.color = color;
};
Ship.prototype = {
accelerate: function () {
this.ax += this.accelerationAmount;
this.ay += this.accelerationAmount;
},
decelerate: function () {
this.ax -= this.decelerationAmount;
this.ay -= this.decelerationAmount;
},
rotateLeft: function () {
this.rv -= this.rotationSpd;
},
rotateRight: function () {
this.rv += this.rotationSpd;
},
move: function () {
this.angle += this.rv;
this.vx += this.ax;
this.vy += this.ay;
this.x += this.vx * Math.cos(this.angle);
this.y += this.vy * Math.sin(this.angle);
this.ax *= this.friction;
this.ay *= this.friction;
this.vx *= this.friction;
this.vy *= this.friction;
this.rv *= this.friction;
},
draw: function (ctx, viewportX, viewportY) {
ctx.save();
ctx.translate(this.x + viewportX, this.y + viewportY);
ctx.rotate(this.angle);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(this.size / 1.2, 0);
ctx.stroke();
ctx.fillStyle = this.color;
ctx.fillRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.strokeRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.restore();
}
};
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 180;
const map = {
height: canvas.height * 5,
width: canvas.width * 5
};
const ship = new Ship(
canvas.width / 2,
canvas.height / 2,
0,
canvas.width / 10 | 0,
"#fff"
);
const keyCodesToActions = {
38: () => ship.accelerate(),
37: () => ship.rotateLeft(),
39: () => ship.rotateRight(),
40: () => ship.decelerate(),
};
const validKeyCodes = new Set(
Object.keys(keyCodesToActions).map(e => +e)
);
const keysPressed = new Set();
document.addEventListener("keydown", e => {
if (validKeyCodes.has(e.keyCode)) {
e.preventDefault();
keysPressed.add(e.keyCode);
}
});
document.addEventListener("keyup", e => {
if (validKeyCodes.has(e.keyCode)) {
e.preventDefault();
keysPressed.delete(e.keyCode);
}
});
(function update() {
requestAnimationFrame(update);
keysPressed.forEach(k => {
if (k in keyCodesToActions) {
keyCodesToActions[k]();
}
});
ship.move();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
const viewportX = clamp(canvas.width / 2 - ship.x, canvas.width - map.width, 0);
const viewportY = clamp(canvas.height / 2 - ship.y, canvas.height - map.height, 0);
/* draw everything offset by viewportX/Y */
const tileSize = canvas.width / 5;
for (let x = 0; x < map.width; x += tileSize) {
for (let y = 0; y < map.height; y += tileSize) {
const xx = x + viewportX;
const yy = y + viewportY;
// simple culling
if (xx > canvas.width || yy > canvas.height ||
xx < -tileSize || yy < -tileSize) {
continue;
}
const light = (~~(x / tileSize + y / tileSize) & 1) * 5 + 70;
ctx.fillStyle = `hsl(${360 - (x + y) / 10}, 50%, ${light}%)`;
ctx.fillRect(xx, yy, tileSize + 1, tileSize + 1);
}
}
ship.draw(ctx, viewportX, viewportY);
ctx.restore();
})();
body {
margin: 0;
font-family: monospace;
display: flex;
flex-flow: row nowrap;
align-items: center;
}
html, body {
height: 100%;
}
canvas {
background: #eee;
border: 4px solid #222;
}
div {
transform: rotate(-90deg);
background: #222;
color: #fff;
padding: 2px;
}
<div>arrow keys to move</div>
With viewport translation, unclamped:
const Ship = function (x, y, angle, size, color) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.ax = 0;
this.ay = 0;
this.rv = 0;
this.angle = angle;
this.accelerationAmount = 0.05;
this.decelerationAmount = 0.02;
this.friction = 0.9;
this.rotationSpd = 0.01;
this.size = size;
this.radius = size;
this.color = color;
};
Ship.prototype = {
accelerate: function () {
this.ax += this.accelerationAmount;
this.ay += this.accelerationAmount;
},
decelerate: function () {
this.ax -= this.decelerationAmount;
this.ay -= this.decelerationAmount;
},
rotateLeft: function () {
this.rv -= this.rotationSpd;
},
rotateRight: function () {
this.rv += this.rotationSpd;
},
move: function () {
this.angle += this.rv;
this.vx += this.ax;
this.vy += this.ay;
this.x += this.vx * Math.cos(this.angle);
this.y += this.vy * Math.sin(this.angle);
this.ax *= this.friction;
this.ay *= this.friction;
this.vx *= this.friction;
this.vy *= this.friction;
this.rv *= this.friction;
},
draw: function (ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(this.size / 1.2, 0);
ctx.stroke();
ctx.fillStyle = this.color;
ctx.fillRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.strokeRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.restore();
}
};
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 180;
const map = {
height: canvas.height * 5,
width: canvas.width * 5
};
const ship = new Ship(
canvas.width / 2,
canvas.height / 2,
0,
canvas.width / 10 | 0,
"#fff"
);
const keyCodesToActions = {
38: () => ship.accelerate(),
37: () => ship.rotateLeft(),
39: () => ship.rotateRight(),
40: () => ship.decelerate(),
};
const validKeyCodes = new Set(
Object.keys(keyCodesToActions).map(e => +e)
);
const keysPressed = new Set();
document.addEventListener("keydown", e => {
if (validKeyCodes.has(e.keyCode)) {
e.preventDefault();
keysPressed.add(e.keyCode);
}
});
document.addEventListener("keyup", e => {
if (validKeyCodes.has(e.keyCode)) {
e.preventDefault();
keysPressed.delete(e.keyCode);
}
});
(function update() {
requestAnimationFrame(update);
keysPressed.forEach(k => {
if (k in keyCodesToActions) {
keyCodesToActions[k]();
}
});
ship.move();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2 - ship.x, canvas.height / 2 - ship.y);
/* draw everything as normal */
const tileSize = canvas.width / 5;
for (let x = 0; x < map.width; x += tileSize) {
for (let y = 0; y < map.height; y += tileSize) {
// simple culling
if (x > ship.x + canvas.width || y > ship.y + canvas.height ||
x < ship.x - canvas.width || y < ship.y - canvas.height) {
continue;
}
const light = ((x / tileSize + y / tileSize) & 1) * 5 + 70;
ctx.fillStyle = `hsl(${360 - (x + y) / 10}, 50%, ${light}%)`;
ctx.fillRect(x, y, tileSize + 1, tileSize + 1);
}
}
ship.draw(ctx);
ctx.restore();
})();
body {
margin: 0;
font-family: monospace;
display: flex;
flex-flow: row nowrap;
align-items: center;
}
html, body {
height: 100%;
}
canvas {
background: #eee;
border: 4px solid #222;
}
div {
transform: rotate(-90deg);
background: #222;
color: #fff;
padding: 2px;
}
<div>arrow keys to move</div>
If you want to keep the target always facing in one direction and rotate the world, make a few adjustments:
ctx.translate(canvas.width / 2, canvas.height / 2);
ctx.rotate(target.angle); // adjust to match your world
ctx.translate(-target.x, -target.y);
/* draw everything as normal */
Here's an example of this variant:
const Ship = function (x, y, angle, size, color) {
this.x = x;
this.y = y;
this.vx = 0;
this.vy = 0;
this.ax = 0;
this.ay = 0;
this.rv = 0;
this.angle = angle;
this.accelerationAmount = 0.05;
this.decelerationAmount = 0.02;
this.friction = 0.9;
this.rotationSpd = 0.01;
this.size = size;
this.radius = size;
this.color = color;
};
Ship.prototype = {
accelerate: function () {
this.ax += this.accelerationAmount;
this.ay += this.accelerationAmount;
},
decelerate: function () {
this.ax -= this.decelerationAmount;
this.ay -= this.decelerationAmount;
},
rotateLeft: function () {
this.rv -= this.rotationSpd;
},
rotateRight: function () {
this.rv += this.rotationSpd;
},
move: function () {
this.angle += this.rv;
this.vx += this.ax;
this.vy += this.ay;
this.x += this.vx * Math.cos(this.angle);
this.y += this.vy * Math.sin(this.angle);
this.ax *= this.friction;
this.ay *= this.friction;
this.vx *= this.friction;
this.vy *= this.friction;
this.rv *= this.friction;
},
draw: function (ctx) {
ctx.save();
ctx.translate(this.x, this.y);
ctx.rotate(this.angle);
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(0, 0);
ctx.lineTo(this.size / 1.2, 0);
ctx.stroke();
ctx.fillStyle = this.color;
ctx.fillRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.strokeRect(
this.size / -2,
this.size / -2,
this.size,
this.size
);
ctx.restore();
}
};
const canvas = document.createElement("canvas");
document.body.appendChild(canvas);
const ctx = canvas.getContext("2d");
canvas.height = canvas.width = 180;
const map = {
height: canvas.height * 5,
width: canvas.width * 5
};
const ship = new Ship(
canvas.width / 2,
canvas.height / 2,
0,
canvas.width / 10 | 0,
"#fff"
);
const keyCodesToActions = {
38: () => ship.accelerate(),
37: () => ship.rotateLeft(),
39: () => ship.rotateRight(),
40: () => ship.decelerate(),
};
const keysPressed = new Set();
document.addEventListener("keydown", e => {
e.preventDefault();
keysPressed.add(e.keyCode);
});
document.addEventListener("keyup", e => {
e.preventDefault();
keysPressed.delete(e.keyCode);
});
(function update() {
requestAnimationFrame(update);
keysPressed.forEach(k => {
if (k in keyCodesToActions) {
keyCodesToActions[k]();
}
});
ship.move();
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.save();
ctx.translate(canvas.width / 2, canvas.height / 1.4);
// ^^^ optionally offset y a bit
// so the player can see better
ctx.rotate(-90 * Math.PI / 180 - ship.angle);
ctx.translate(-ship.x, -ship.y);
/* draw everything as normal */
const tileSize = ~~(canvas.width / 5);
for (let x = 0; x < map.width; x += tileSize) {
for (let y = 0; y < map.height; y += tileSize) {
// simple culling
if (x > ship.x + canvas.width || y > ship.y + canvas.height ||
x < ship.x - canvas.width || y < ship.y - canvas.height) {
continue;
}
const light = ((x / tileSize + y / tileSize) & 1) * 5 + 70;
ctx.fillStyle = `hsl(${360 - (x + y) / 10}, 50%, ${light}%)`;
ctx.fillRect(x, y, tileSize + 1, tileSize + 1);
}
}
ship.draw(ctx);
ctx.restore();
})();
body {
margin: 0;
font-family: monospace;
display: flex;
flex-flow: row nowrap;
align-items: center;
}
html, body {
height: 100%;
}
canvas {
background: #eee;
border: 4px solid #222;
}
div {
transform: rotate(-90deg);
background: #222;
color: #fff;
padding: 2px;
}
<div>arrow keys to move</div>
See this related answer for an example of the player-perspective viewport with a physics engine.
The way you're going about it right now seems correct to me. I would change the "20" bounds to a variable though, so you can easily change the bounds of a level or the entire game if you ever require so.
You could abstract this logic into a specific "Viewport" method, that would simply handle the calculations required to determine where your "Camera" needs to be on the map, and then make sure the X and Y coordinates of your character match the center of your camera.
You could also flip that method and determine the location of your camera based on the characters position (e.g.: (position.x - (desired_camera_size.width / 2))) and draw the camera from there on out.
When you have your camera position figured out, you can start worrying about drawing the room itself as the first layer of your canvas.
Save the code below as a .HTM (.html) file and open in your browser.
The result should match this screen shot EXACTLY.
Here is some example code that maps viewports of different sizes onto each other.
Though this implementation uses pixels, you could expand upon this logic to render
tiles. I actually store my tilemaps as .PNG files. Depending on the color of the
pixel, it can represent a different tile type. The code here is designed to sample
from viewports 1,2, or 3 and paste results into viewport 0.
Youtube Video Playlist For The Screenshot and Code Directly Below : REC_MAP
EDIT: REC_MAP.HTM CODE MOVED TO PASTEBIN:
https://pastebin.com/9hWs8Bag
Part #2: BUF_VEW.HTM (Sampling from off screen buffer)
We are going to refactor the code from the previous demo so that
our source viewport samples a bitmap that is off screen. Eventually
we will interpret each pixel color on the bitmap as a unique tile value.
We don't go that far in this code, this is just a refactor to get one
of our viewports off-screen. I recorded the entire process here.
No edits. Entire process including me taking way too long to think
up variable names.
Youtube Video Playlist For The Screenshot and Code Directly Below : BUF_VEW
As before, you can take this source code, save it as a .HTM (.html) file, and run it in your browser.
EDIT: BUF_VEW.HTM CODE MOVED TO PASTEBIN:
https://pastebin.com/zedhD60u
Part #3: UIN_ADA.HTM ( User Input Adapter & Snapping Camera )
We are now going to edit the previous BUF_VEW.HTM file from
part #2 and add 2 new pieces of functionality.
1: User input handling
2: A camera that can zoom in and out and be moved.
This camera will move in increments of it's own viewport
selection area width and height, meaning the motion will
be very "snappy". This camera is designed for level editing,
not really in-game play. We are focusing on a level editor
camera first. The long-term end goal is to make the editor-code
and the in-game-play code the same code. The only difference
should be that when in game-play mode the camera will behave
differently and tile-map editing will be disabled.
Youtube Video Playlist For The Screenshot And Code Directly Below: UIN_ADA
Copy code below, save as: "UIN_ADA.HTM" and run in browser.
Controls: Arrows & "+" "-" for camera zoom-in, zoom-out.
EDIT: UIN_ADA.HTM MOVED TO PASTEBIN:
https://pastebin.com/ntmWihra
Part #4: DAS_BOR.HTM ( DAShed_BOaRders )
We are going to do some calculations to draw a 1 pixel
thin boarder around each tile. The result won't be fancy,
but it will help us verify that we are able to get the
local coordinates of each tile and do something useful with
them. These tile-local coordinates will be necessary for
mapping a bitmap image onto the tile in later installments.
Youtube_Playlist: DAS_BOR.HTM
Source_Code: DAS_BOR.HTM
Part #5: Zoom + Pan over WebGL Canvas fragment shader code:
This is the math required for zooming and panning over a
shader written in GLSL. Rather than taking a sub-sample of off-screen
data, we take a sub-sample of the gl_FragCoord values. The math here
allows for an inset on-screen viewport and a camera that can
zoom and pan over your shader. If you have done a shader tutorial
by "Lewis Lepton" and you would like to zoom and pan over it,
you can filter his input coordinates through this logic and that
should do it.
JavaScript Code
Quick Video Explanation Of Code
Part #6: ICOG.JS : WebGL2 port of DAS_BOR.HTM
To run this you'll need to include the script in an otherwise
empty .HTM file. It replicates the same behavior found in DAS_BOR.HTM,
except all of the rendering is done with GLSL shader code.
There is also the makings of a full game framework in the code as well.
Usage:
1: Press "~" to tell the master editor to read input.
2: Press "2" to enter editor #2 which is the tile editor.
3: WASD to move over 512x512 memory sub sections.
4: Arrow Keys to move camera over by exactly 1 camera.
5: "+" and "-" keys to change the "zoom level" of the camera.
Though this code simply renders each tile value as a gradient square,
it demonstrates the ability to get the correct tile value and internal
coordinates of current tile being draw. Armed with the local coordinates
of a tile in your shader code, you have the ground-work math in place
for mapping images onto these tiles.
Full JavaScript Webgl2 Code
Youtube playlist documenting creation of ICOG.JS
//|StackOverflow Says:
//|Links to pastebin.com must be accompanied by code. Please |//
//|indent all code by 4 spaces using the code toolbar button |//
//|or the CTRL+K keyboard shortcut. For more editing help, |//
//|click the [?] toolbar icon. |//
//| |//
//|StackOverflow Also Says (when I include the code here) |//
//|You are over you 30,000 character limit for posts. |//
function(){ console.log("[FixingStackOverflowComplaint]"); }

Categories

Resources