So I am trying to get the clearInterval to work. This is from: https://codepen.io/whqet/pen/Auzch (I modified it a bit to make it more towards what I needed. Essentially, I want the animation to cease after 10000 ms. Excuse the messy coding, I threw in a timer at the bottom so I could see whether or not it would work.. Any assistance would be appreciated. Thanks!
var canvas = document.getElementById( 'canvas' ),
ctx = canvas.getContext( '2d' ),
// full screen dimensions
cw = window.innerWidth,
ch = window.innerHeight,
// firework collection
fireworks = [],
// particle collection
particles = [],
// starting hue
hue = 120,
// when launching fireworks with a click, too many get launched at once without a limiter, one launch per 5 loop ticks
limiterTotal = 5,
limiterTick = 0,
// this will time the auto launches of fireworks, one launch per 60 loop ticks
timerTotal = 60,
timerTick = 0,
mousedown = false,
// mouse x coordinate,
mx,
// mouse y coordinate
my;
// set canvas dimensions
canvas.width = cw;
canvas.height = ch;
// now we are going to setup our function placeholders for the entire demo
// get a random number within a range
function random( min, max ) {
return Math.random() * ( max - min ) + min;
}
// calculate the distance between two points
function calculateDistance( p1x, p1y, p2x, p2y ) {
var xDistance = p1x - p2x,
yDistance = p1y - p2y;
return Math.sqrt( Math.pow( xDistance, 2 ) + Math.pow( yDistance, 2 ) );
}
// create firework
function Firework( sx, sy, tx, ty ) {
// actual coordinates
this.x = sx;
this.y = sy;
// starting coordinates
this.sx = sx;
this.sy = sy;
// target coordinates
this.tx = tx;
this.ty = ty;
// distance from starting point to target
this.distanceToTarget = calculateDistance( sx, sy, tx, ty );
this.distanceTraveled = 0;
// track the past coordinates of each firework to create a trail effect, increase the coordinate count to create more prominent trails
this.coordinates = [];
this.coordinateCount = 3;
// populate initial coordinate collection with the current coordinates
while( this.coordinateCount-- ) {
this.coordinates.push( [ this.x, this.y ] );
}
this.angle = Math.atan2( ty - sy, tx - sx );
this.speed = 2;
this.acceleration = 1.05;
this.brightness = random( 50, 70 );
// circle target indicator radius
this.targetRadius = 1;
}
// update firework
Firework.prototype.update = function( index ) {
// remove last item in coordinates array
this.coordinates.pop();
// add current coordinates to the start of the array
this.coordinates.unshift( [ this.x, this.y ] );
// cycle the circle target indicator radius
if( this.targetRadius < 8 ) {
this.targetRadius += 0.3;
} else {
this.targetRadius = 1;
}
// speed up the firework
this.speed *= this.acceleration;
// get the current velocities based on angle and speed
var vx = Math.cos( this.angle ) * this.speed,
vy = Math.sin( this.angle ) * this.speed;
// how far will the firework have traveled with velocities applied?
this.distanceTraveled = calculateDistance( this.sx, this.sy, this.x + vx, this.y + vy );
// if the distance traveled, including velocities, is greater than the initial distance to the target, then the target has been reached
if( this.distanceTraveled >= this.distanceToTarget ) {
createParticles( this.tx, this.ty );
// remove the firework, use the index passed into the update function to determine which to remove
fireworks.splice( index, 1 );
} else {
// target not reached, keep traveling
this.x += vx;
this.y += vy;
}
}
// draw firework
Firework.prototype.draw = function() {
ctx.beginPath();
// move to the last tracked coordinate in the set, then draw a line to the current x and y
ctx.moveTo( this.coordinates[ this.coordinates.length - 1][ 0 ], this.coordinates[ this.coordinates.length - 1][ 1 ] );
ctx.lineTo( this.x, this.y );
ctx.strokeStyle = 'hsl(' + hue + ', 100%, ' + this.brightness + '%)';
ctx.stroke();
ctx.beginPath();
// draw the target for this firework with a pulsing circle
ctx.arc( this.tx, this.ty, this.targetRadius, 0, Math.PI * 2 );
ctx.stroke();
}
// create particle
function Particle( x, y ) {
this.x = x;
this.y = y;
// track the past coordinates of each particle to create a trail effect, increase the coordinate count to create more prominent trails
this.coordinates = [];
this.coordinateCount = 5;
while( this.coordinateCount-- ) {
this.coordinates.push( [ this.x, this.y ] );
}
// set a random angle in all possible directions, in radians
this.angle = random( 0, Math.PI * 2 );
this.speed = random( 1, 10 );
// friction will slow the particle down
this.friction = 0.95;
// gravity will be applied and pull the particle down
this.gravity = 1;
// set the hue to a random number +-50 of the overall hue variable
this.hue = random( hue - 50, hue + 50 );
this.brightness = random( 50, 80 );
this.alpha = 1;
// set how fast the particle fades out
this.decay = random( 0.015, 0.03 );
}
// update particle
Particle.prototype.update = function( index ) {
// remove last item in coordinates array
this.coordinates.pop();
// add current coordinates to the start of the array
this.coordinates.unshift( [ this.x, this.y ] );
// slow down the particle
this.speed *= this.friction;
// apply velocity
this.x += Math.cos( this.angle ) * this.speed;
this.y += Math.sin( this.angle ) * this.speed + this.gravity;
// fade out the particle
this.alpha -= this.decay;
// remove the particle once the alpha is low enough, based on the passed in index
if( this.alpha <= this.decay ) {
particles.splice( index, 1 );
}
}
// draw particle
Particle.prototype.draw = function() {
ctx. beginPath();
// move to the last tracked coordinates in the set, then draw a line to the current x and y
ctx.moveTo( this.coordinates[ this.coordinates.length - 1 ][ 0 ], this.coordinates[ this.coordinates.length - 1 ][ 1 ] );
ctx.lineTo( this.x, this.y );
ctx.strokeStyle = 'hsla(' + this.hue + ', 100%, ' + this.brightness + '%, ' + this.alpha + ')';
ctx.stroke();
}
// create particle group/explosion
function createParticles( x, y ) {
// increase the particle count for a bigger explosion, beware of the canvas performance hit with the increased particles though
var particleCount = 300;
while( particleCount-- ) {
particles.push( new Particle( x, y ) );
}
}
// main demo loop
function loop() {
// this function will run endlessly with requestAnimationFrame
requestAnimFrame( loop );
// increase the hue to get different colored fireworks over time
//hue += 0.5;
// create random color
hue= random(0, 360 );
// normally, clearRect() would be used to clear the canvas
// we want to create a trailing effect though
// setting the composite operation to destination-out will allow us to clear the canvas at a specific opacity, rather than wiping it entirely
ctx.globalCompositeOperation = 'destination-out';
// decrease the alpha property to create more prominent trails
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect( 0, 0, cw, ch );
// change the composite operation back to our main mode
// lighter creates bright highlight points as the fireworks and particles overlap each other
ctx.globalCompositeOperation = 'lighter';
// loop over each firework, draw it, update it
var i = fireworks.length;
while( i-- ) {
fireworks[ i ].draw();
fireworks[ i ].update( i );
}
// loop over each particle, draw it, update it
var i = particles.length;
while( i-- ) {
particles[ i ].draw();
particles[ i ].update( i );
}
// launch fireworks automatically to random coordinates, when the mouse isn't down
if( timerTick >= timerTotal ) {
if( !mousedown ) {
// start the firework at the bottom middle of the screen, then set the random target coordinates, the random y coordinates will be set within the range of the top half of the screen
fireworks.push( new Firework( cw / 2, ch, random( 0, cw ), random( 0, ch / 2 ) ) );
timerTick = 0;
}
} else {
timerTick++;
}
// limit the rate at which fireworks get launched when mouse is down
if( limiterTick >= limiterTotal ) {
if( mousedown ) {
// start the firework at the bottom middle of the screen, then set the current mouse coordinates as the target
fireworks.push( new Firework( cw / 2, ch, mx, my ) );
limiterTick = 0;
}
} else {
limiterTick++;
}
}
// mouse event bindings
// update the mouse coordinates on mousemove
canvas.addEventListener( 'mousemove', function( e ) {
mx = e.pageX - canvas.offsetLeft;
my = e.pageY - canvas.offsetTop;
});
// toggle mousedown state and prevent canvas from being selected
canvas.addEventListener( 'mousedown', function( e ) {
e.preventDefault();
mousedown = true;
});
canvas.addEventListener( 'mouseup', function( e ) {
e.preventDefault();
mousedown = false;
});
// once the window loads, we are ready for some fireworks!
window.onload = loop;
// when animating on canvas, it is best to use requestAnimationFrame instead of setTimeout or setInterval
window.requestAnimFrame = ( function() {
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( ) {
window.setTimeout( timePeriodms );
};
function stopFireworks () {
var timePeriodms = 10000;
window.clearTimeout(timePeriodms);
};
})();
// now we will setup our basic variables for the demo
var minute = 0;
var sec = 00;
var zeroPholder = 0;
var counterIdv2 = setInterval(function(){
countUp2();
}, 1000);
function countUp2 () {
sec++;
if(sec == 60){
sec = 00;
minute = minute + 1;
}
if (minute == 0 && sec == 1) {
document.getElementById('count-up-2').style.color = 'red';
}
if (minute == 0 && sec == 59) {
document.getElementById('count-up-2').style.color = 'blue';
}
if (minute == 10 && sec == 00) {
document.getElementById('count-up-2').style.color = 'red';
}
if(sec == 10){
zeroPholder = '';
}else
if(sec == 00){
zeroPholder = 0;
}
document.getElementById("count-up-2").innerText = minute+':'+zeroPholder+sec;
}
body {
background: #000;
margin: 0;
}
canvas {
display: block;
}
<canvas id="canvas">Canvas is not supported in your browser.</canvas>
<table border="1" style="border-color:black;">
<tbody>
<tr>
<td style="background-color: #fff; padding: 5px;"><span>Time spent on page: </span><span id="count-up-2">0:00</span>
</td>
</tr>
</tbody>
</table>
requestAnimationFrame callback's argument
When requestAnimationFrame calls the callback function it supplies a high precision time in ms. You can use this time to control the timing of your animations.
For example the following animation loop will stop 10 seconds after the first frame is called.
requestAnimationFrame(mainLoop);
var startTime;
const endTime = 10000; // in ms
function mainLoop(time) {
if (startTime === undefined) { startTime = time }
if (time - startTime > endTime) {
console.log("Animation end.");
return;
}
// draw animated content
requestAnimationFrame(mainLoop);
}
This gives you better timing control in animations than using setTimeout especially if you have more than one thing to control the timing of (see demo)
Demo
The demo uses the same method to count down the time and control the fireworks. A few seconds before the end the fireworks are held back and then a second before a group are fired to explode in time with zero (Well there about!!!)
// Code based loosely on OPs code in so far as it animates fireworks.
// There is no control of fireworks via mouse.
// Uses reusable buffers to avoid GC overhead in low end devices.
const ctx = canvas.getContext( '2d' );
const GRAVITY = 0.2; // per frame squared
var cw = canvas.width = innerWidth - 40;
var ch = canvas.height = 500;
const iH = innerHeight;
var startTime;
const DISPLAY_TIME = 10000; // in ms. Time to countdown
const SHELL_X = cw / 2; // Location that shells are fired from in px
const SHELL_Y = ch;
const SHELL_TIME = 100; // in frames
const MAX_SHELLS = 8;
const MAX_PARTICLES = 1000; // Approx max particles.
const SHELL_RANDOM_RATE = 0.01; // cof of shell random fire control
const SHELL_FIRE_CURVE = 3; // Highest power of fire control exponents
var randomFire = 0; // holds the odds of a random shell being fired
Math.TAU = Math.PI * 2;
Math.PI90 = Math.PI / 2;
Math.PI270 = Math.PI + Math.PI90;
Math.rand = (m, M) => Math.random() * (M - m) + m;
Math.distance = (x1, y1, x2, y2) => ((x1 - x2) ** 2 + (y1 - y2) ** 2) ** 0.5;
requestAnimationFrame(mainLoop);
function mainLoop(time) {
if (startTime === undefined) { startTime = time }
const timer = DISPLAY_TIME - (time - startTime);
const displayTime = timer / 1000 | 0;
if (timer <= 0) {
countdown.textContent = "HAPPY NEW YEAR 2020";
if(particles.size === 0 && shells.size === 0) {
ctx.clearRect(0,0,cw,ch);
shells.clear();
startTime = undefined;
countdown.textContent = "Click to restart";
canvas.addEventListener("click",() => requestAnimationFrame(mainLoop), {once: true});
return; // ends update
} else {
randomFire = 0; // pervent shells firing after zero time
}
} else {
countdown.textContent !== displayTime && (countdown.textContent = displayTime);
}
ctx.lineCap = "round";
ctx.globalCompositeOperation = 'destination-out';
ctx.globalAlpha = 0.2;
ctx.fillStyle = "#000"
ctx.fillRect( 0, 0, cw, ch );
ctx.globalCompositeOperation = 'lighter';
shells.update();
particles.update();
ctx.lineWidth = 2;
shells.draw();
ctx.lineWidth = 3;
particles.draw();
if (timer < 2500 && timer > 1000) { randomFire = 0 }
else if(timer <= 1000 && timer > 0) { randomFire = 1 }
if(shells.size < MAX_SHELLS && particles.size < MAX_PARTICLES) {
if(Math.random() < randomFire ** SHELL_FIRE_CURVE) {
randomFire = 0;
shells.fire(
Math.rand(cw * (1/3), cw *(2/3)),
Math.rand(iH * (3/4), iH *(4/4)),
SHELL_TIME
);
}
randomFire += SHELL_RANDOM_RATE;
}
requestAnimationFrame(mainLoop);
}
function Trail() {}
function Particle() { }
function Shell( sx, sy, tx, ty ) {
this.trail = new Trail();
this.init(sx, sy,tx,sy);
}
Trail.prototype = {
init(x, y) {
this.x1 = this.x2 = this.x3 = x;
this.y1 = this.y2 = this.y3 = y;
},
update(x, y) {
this.x3 = this.x2
this.y3 = this.y2
this.x2 = this.x1
this.y2 = this.y1
this.x1 = x;
this.y1 = y;
},
draw() {
ctx.moveTo(this.x1, this.y1);
ctx.lineTo(this.x2, this.y2);
ctx.lineTo(this.x3, this.y3);
}
};
Shell.prototype = {
init(x, y, time) {
this.x = SHELL_X;
this.y = SHELL_Y;
this.sx = (x - this.x) / (time / 2);
this.sy = ((y - this.y) * (GRAVITY / ((time) ** 0.5)))* 2;
this.power = (-this.sy * 10) | 0;
this.hue = Math.rand(360, 720) % 360 | 0;
this.active = true;
this.trail.init(this.x, this.y);
this.time = time / 2;
this.life = time / 2;
},
explode() {
this.active = false;
particles.explode(this, this.power);
},
update() {
this.time -= 1;
if (this.time <= 0) { this.explode() }
this.sy += GRAVITY;
this.x += this.sx;
this.y += this.sy;
this.trail.update(this.x, this.y);
return this.active;
},
draw() {
ctx.strokeStyle = `hsl(${this.hue},100%,${(this.time / this.life) * 100}%)`;
ctx.beginPath();
this.trail.draw();
ctx.stroke();
},
};
Particle.prototype = {
init(shell) {
this.x2 = this.x1 = this.x = shell.x;
this.y2 = this.y1 = this.y = shell.y;
this.dx = shell.sx;
this.dy = shell.sy;
this.angle = Math.rand(0, Math.TAU);
const zAng = Math.cos(Math.random() ** 2 * Math.PI)
this.speed = zAng * shell.power / 30;
this.friction = 0.95;
this.gravity = GRAVITY;
this.hue = (Math.rand(shell.hue - 5, shell.hue + 5) + 360) % 360;
this.brightness = Math.rand( 25, 50 );
this.alpha = shell.power / 10;
this.decay = Math.rand( 0.2, 0.5);
this.active = true;
},
update() {
const dx = Math.cos(this.angle);
const dy = Math.sin(this.angle);
this.x2 = this.x1;
this.y2 = this.y1;
this.x1 = this.x - dx;
this.y1 = this.y + dy;
this.speed *= this.friction;
this.x += (this.dx *= 0.9);
this.y += (this.dy *= 0.9);
this.dy += GRAVITY / 100;
this.x += dx * this.speed;
this.y += dy * this.speed;
this.alpha -= this.decay;
if( this.alpha <= 0 || this.x < 0 || this.y < 0 || this.x > cw) {
this.active = false;
}
return this.active;
},
draw() {
const alpha = this.alpha / 5 > 1 ? 1 : this.alpha / 5;
const lum = this.brightness + this.alpha
ctx.strokeStyle = `hsla(${this.hue},100%,${lum<100 ? lum : 100}%,${alpha})`;
ctx. beginPath();
ctx.moveTo( this.x2, this.y2);
ctx.lineTo( this.x, this.y );
ctx.stroke();
}
};
function BubbleArray(extension) {
return Object.assign([], {
size: 0,
update() {
var read = 0, write = 0;
while (read < this.size) {
const item = this[read];
if(read !== write) {
const temp = this[write]
this[write] = item;
this[read] = temp;
}
item.update() === true && (write ++);
read++;
}
this.size = write;
},
draw() {
var i = 0,len = this.size;
while(i < len) { this[i++].draw() }
},
add(item) {
this.size ++;
this.push(item);
},
clear() { this.length = this.size = 0 },
getInactive() { return this.size < this.length ? this[this.size++] : undefined },
},
extension,
);
}
const particles = BubbleArray({
explode(shell, count) {
var item;
while(count-- > 0) {
!(item = this.getInactive()) && this.add(item = new Particle());
item.init(shell);
}
},
});
const shells = BubbleArray({
fire(tx = mx, ty = my) {
var item;
!(item = this.getInactive()) && this.add(item = new Shell());
item.init(tx, ty, 100);
}
});
body {
padding: 0px;
}
canvas {
background: #000;
position: absolute;
top: 0px;
left: 0px;
}
#countdown {
position: absolute;
top: 20px;
left: 20px;
font-family: arial;
font-size: xx-large;
color: white;
}
<canvas id="canvas"></canvas>
<div id="countdown"></div>
I have an object I want to make orbit a star. I've managed to make the object move towards the star, but now I need to set up lateral movement as well.
Obviously this isn't as easy as just adjusting X, as when it moves round to the side of the star I'll have to adjust Y as well. I'm wondering how I could use some math to figure out how much I need to adjust X and Y by as the object moves around the star.
Here's my code so far:
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
c.width = window.innerWidth;
c.height = window.innerHeight;
var star = {
x: c.width / 2,
y: c.height / 2,
r: 100,
g: 2,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'orange';
ctx.fill();
ctx.closePath();
}
};
var node = {
x: c.width / 2,
y: 100,
r: 20,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
}
};
//GAME LOOP
function gameLoop()
{
update();
render();
window.requestAnimationFrame(gameLoop);
}
function update()
{
//Move towards star
var dx = star.x - node.x;
var dy = star.y - node.y;
var angle = Math.atan2(dy, dx);
node.x += Math.cos(angle);
node.y += Math.sin(angle);
//Lateral movement
node.x += 2;
}
function render()
{
ctx.clearRect(0, 0, c.width, c.height);
star.draw();
node.draw();
}
window.requestAnimationFrame(gameLoop);
<html>
<head>
<style>
body
{
margin: 0;
padding: 0;
overflow: hidden;
}
#canvas
{
background-color: #001319;
}
</style>
</head>
<body>
<canvas id="canvas">
</canvas>
<script src="Orbit.js"></script>
</body>
</html>
Newton's and Kepler's clockwork universe
When Newton worked out the maths for calculating orbits he noted something that prompted him to coin the term "clockwork universe". In a two body simulation the orbital paths of both objects repeat precisely. This mean that both objects will at the same time be in the exact same position at the exact same speed they were in the last orbit, there will be no precession.
Gravity, force, mass, and distance.
For a more accurate model of gravity you can use the laws of gravity as discovered by Newton. F = G * (m1*m2)/(r*r) where F is force, G is the Gravitational constant (and for simulations it is just a scaling factor) m1,m2 are the mass of each body and r is the distance between the bodies.
Mass of a sphere
We give both the star and planet some mass. Let's say that in the computer 1 pixel cubed is equal to 1 unit mass. Thus the mass of a sphere of radius R is 4/3 * R3 * PI.
Force, mass, and acceleration
The force is always applied along the line between the bodies and is called acceleration.
When a force is applied to an object we use another of Newton's discovered laws, F=ma where a is acceleration. We have the F (force) and m (mass) so now all we need is a. Rearrange F=ma to get a = f/m.
If we look at both formulas in terms of a (acceleration) a = (G * (m1*m2)/(r*r)) / m1 we can see that the mass of the object we are apply force to is cancelled out a = G * (m2)/(r*r). Now we can calculate the acceleration due to gravity. Acceleration is just change in velocity over time, and we know that that change is in the direction of the other body. So we get the vector between the bodies (o1,o2 for object 1 and 2) dx = o2.x-o1.x, dy = o2.y-o1.y Then find the length of that vector (which is the r in the gravity formula) dist = Math.sqrt(dx* dx + dy * dy). Then we normalise the vector (make its length = one) by dividing by its length. dx /= dist, dy /= dist. Calculate the a (acceleration) and multiply the normalised vector between the object by a then add that to the object's velocity and that is it. Perfect Newtonian clockwork orbits (for two bodies that is).
Clean up with Kepler.
All that math is good but it does not make for a nice simulation. When the math is done both objects start moving and if the starting velocities are not in balance then the whole system will slowly drift of the canvas.
We could just make the display relative to one of the bodies, this would eliminate any drift in the system, but we still have the problem of getting an orbit. If one object is moving to fast it will fly off and never come back. If it is going too slow then it will fall in very close to the centerpoint of the other object. If this happens the change in velocity will approch infinity, something computers are not that good at handling.
So to get nice circular orbits we need one last bit of math.
Using Kepler's second law modified to fit into Newton's math we get a formula that will give the approximate (It is an approximate as the actual calculations involve an infinite series and I can not be bothered writing that out.) orbital velocity v = sqrt(G*(m1 + m2)/r). It looks similar to Newton's gravity law but in this the masses are summed not multiplied, and the distance is not squared.
So we use this to calculate the tangential speed of both bodies to give them near circular orbits. It is important that each object go in the opposite direction to each other.
I created a setup function that sets up the correct orbits for both the sun and the planet. But the value of G (Gravitational constant) is likely way to large. To get a better value I scale G (via kludge math) so that the sun's orbit speed is close to a desired ideal sunV (pixels per frame) To make the whole sim run quicker increase this value
As I have set up the code to have more than two bodies the calculation of starting velocity will only work if each object is significantly more massive than the next. I have added a moon (you need to un-comment to see) to the planet, but it is too big and it's starting velocity is a little too low. It gets pulled (gravity sling shot) by the Earth into a higher orbit,. but this also pulls the earth into a lower orbit making its orbit more eccentric
NOTE After all that I find that something is not quite right and there is still a tiny bit of drift in the system. As I am out of time I have just fixed the sun position to keep the system on the canvas.
var c = document.getElementById('canvas');
c.width = innerWidth;
c.height = innerHeight;
var ctx = c.getContext('2d');
const STAR_RADIUS = 100;
const PLANET_RADIUS = 10;
const MOON_RADIUS = 4.5;
var G = 1; // gravitational constant is not so constant as need to
// scale it to find best value for the system.
// for that I will scale it so that the suns orbital speed around the
// planet is approx 0.1 pixels per frame
const sunV = 0.1; // the sun's orbital desired speed. THis is used to tune G
const DRAW = function () {
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = this.col;
ctx.fill();
ctx.closePath();
}
var star = {
x: c.width / 2,
y: c.height / 2,
vx : 0,
vy : 0,
r: STAR_RADIUS,
mass : (4/3) * Math.pow(STAR_RADIUS,3) * Math.PI,
col : 'orange',
draw : DRAW,
};
// kludge to fix drift
const sunStartX = star.x;
const sunStartY = star.y;
var node = {
x: c.width / 2 - STAR_RADIUS - PLANET_RADIUS * 5,
y: c.height / 2,
r: PLANET_RADIUS,
mass : (4/3) * Math.pow(PLANET_RADIUS,3) * Math.PI,
col : "blue",
draw : DRAW,
vx: -1,
vy: 0,
};
var moon = {
x: c.width / 2- STAR_RADIUS - PLANET_RADIUS * 7 ,
y: c.height / 2,
r: MOON_RADIUS,
mass : (4/3) * Math.pow(PLANET_RADIUS,3) * Math.PI,
col : "#888",
draw : DRAW,
vx: -1,
vy: 0,
};
const objects = [star, node];//, moon];
function setup(){
var dist,dx,dy,o1,o2,v,c,dv;
o1 = objects[0];
o1.vx = 0;
o1.vy = 0;
for(var j = 0; j < objects.length; j ++){
if(j !== 0){ // object can not apply force to them selves
o2 = objects[j];
dx = o2.x - o1.x;
dy = o2.y - o1.y;
dist = Math.sqrt(dx * dx + dy * dy);
dx /= dist;
dy /= dist;
// Find value og G
if(j === 1){ // is this not sun
v = Math.sqrt(G * ( o2.mass ) / dist);
dv = sunV - v;
while(Math.abs(dv) > sunV * sunV){
if(dv < 0){ // sun too fast
G *= 0.75;
}else{
G += G * 0.1;
}
v = Math.sqrt(G * ( o2.mass ) / dist);
dv = sunV - v;
}
}
v = Math.sqrt(G * ( o2.mass ) / dist);
o1.vx -= v * dy; // along the tangent
o1.vy += v * dx;
}
}
for(var i = 1; i < objects.length; i ++){
o1 = objects[i];
o1.vx = 0;
o1.vy = 0;
for(var j = 0; j <objects.length; j ++){
if(j !== i){
o2 = objects[j];
dx = o2.x - o1.x;
dy = o2.y - o1.y;
dist = Math.sqrt(dx * dx + dy * dy);
dx /= dist;
dy /= dist;
v = Math.sqrt(G * ( o2.mass ) / dist);
o1.vx += v * dy; // along the tangent
o1.vy -= v * dx;
}
}
}
}
//GAME LOOP
function gameLoop(){
update();
render();
requestAnimationFrame(gameLoop);
}
// every object exerts a force on every other object
function update(){
var dist,dx,dy,o1,o2,a;
// find force of acceleration each object applies to each object
for(var i = 0; i < objects.length; i ++){
o1 = objects[i];
for(var j = 0; j < objects.length; j ++){
if(i !== j){ // object can not apply force to them selves
o2 = objects[j];
dx = o2.x - o1.x;
dy = o2.y - o1.y;
dist = Math.sqrt(dx * dx + dy * dy);
dx /= dist; // normalise the line between the objects (makes the vector 1 unit long)
dy /= dist;
// get force
a = (G * o2.mass ) / (dist * dist);
o1.vx += a * dx;
o1.vy += a * dy;
}
}
}
// once all the forces have been found update objects positions
for(var i = 0; i < objects.length; i ++){
o1 = objects[i];
o1.x += o1.vx;
o1.y += o1.vy;
}
}
function render(){
ctx.clearRect(0, 0, c.width, c.height);
// kludge to fix drift
var offsetX = objects[0].x - sunStartX;
var offsetY = objects[0].y - sunStartY;
ctx.setTransform(1,0,0,1,-offsetX,-offsetY);
for(var i = 0; i < objects.length; i ++){
objects[i].draw();
}
ctx.setTransform(1,0,0,1,0,0);
}
setup();
requestAnimationFrame(gameLoop);
<canvas id='canvas'></canvas>
Okay, I've answered my own question.
Rather than making the star's gravity directly affect the x and y coordinates, I have a vx and vy of the object, and I cause the gravity to affect that value, and then just adjust x and y by vx and vy on each update.
Here's the code:
var c = document.getElementById('canvas');
var ctx = c.getContext('2d');
c.width = window.innerWidth;
c.height = window.innerHeight;
var star = {
x: c.width / 2,
y: c.height / 2,
r: 100,
g: 0.5,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'orange';
ctx.fill();
ctx.closePath();
}
};
var node = {
x: c.width / 2,
y: 50,
r: 20,
vx: 15,
vy: 0,
draw: function()
{
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, 2*Math.PI);
ctx.fillStyle = 'blue';
ctx.fill();
ctx.closePath();
}
};
//GAME LOOP
function gameLoop()
{
update();
render();
window.requestAnimationFrame(gameLoop);
}
function update()
{
node.x += node.vx;
node.y += node.vy;
//Move towards star
var dx = star.x - node.x;
var dy = star.y - node.y;
var angle = Math.atan2(dy, dx);
node.vx += (Math.cos(angle) * star.g);
node.vy += (Math.sin(angle) * star.g);
}
function render()
{
ctx.clearRect(0, 0, c.width, c.height);
star.draw();
node.draw();
}
window.requestAnimationFrame(gameLoop);
<canvas id='canvas'></canvas>
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]"); }