I want to move a object (circle in this case) through array of coordinates (for example: {(300,400), (200,300), (300,200),(400,400)})on HTML5 Canvas. I could move the object to one coordinate as follows. The following code draws a circle at (100,100) and moves it to (300,400). I am stuck when trying to extend this so that circle moves through set of coordinates one after the other.
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
//circle object
let circle ={
x:100,
y:100,
radius:10,
dx:1,
dy:1,
color:'blue'
}
//function to draw above circle on canvas
function drawCircle(){
ctx.beginPath();
ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2);
ctx.fillStyle=circle.color;
ctx.fill();
ctx.closePath();
}
//Moving to a target coordinate (targetX,targetY)
function goTo(targetX,targetY){
if(Math.abs(circel.x-targetX)<circle.dx && Math.abs(circel.y-targetY)<circle.dy){
circle.dx=0;
circle.dy=0;
circel.x = targetX;
circle.y = targetY;
}
else{
const opp = targetY - circle.y;
const adj = targetX - circle.x;
const angle = Math.atan2(opp,adj)
circel.x += Math.cos(angle)*circle.dx
circle.y += Math.sin(angle)*circle.dy
}
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawCircle()
goTo(300,400)
requestAnimationFrame(update);
}
update()
Random access key frames
For the best control of animations you need to create way points (key frames) that can be accessed randomly by time. This means you can get any position in the animation just by setting the time.
You can then play and pause, set speed, reverse and seek to any position in the animation.
Example
The example below uses a set of points and adds data required to quickly locate the key frames at the requested time and interpolate the position.
The blue dot will move at a constant speed over the path in a time set by pathTime in this case 4 seconds.
The red dot's position is set by the slider. This is to illustrate the random access of the animation position.
const ctx = canvas.getContext('2d');
const pathTime = 4; // Total time to travel path from start to end in seconds
var startTime, animTime = 0, paused = false;
requestAnimationFrame(update);
const P2 = (x, y) => ({x, y, dx: 0,dy: 0,dist: 0, start: 0, end: 0});
const pathCoords = [
P2(20, 20), P2(100, 50),P2(180, 20), P2(150, 100), P2(180, 180),
P2(100, 150), P2(20, 180), P2(50, 100), P2(20, 20),
];
createAnimationPath(pathCoords);
const circle ={
draw(rad = 10, color = "blue") {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(this.x, this.y, rad, 0, Math.PI * 2);
ctx.fill();
}
};
function createAnimationPath(points) { // Set up path for easy random position lookup
const segment = (prev, next) => {
[prev.dx, prev.dy] = [next.x - prev.x, next.y - prev.y];
prev.dist = Math.hypot(prev.dx, prev.dy);
next.end = next.start = prev.end = prev.start + prev.dist;
}
var i = 1;
while (i < points.length) { segment(points[i - 1], points[i++]) }
}
function getPos(path, pos, res = {}) {
pos = (pos % 1) * path[path.length - 1].end; // loop & scale to total length
const pathSeg = path.find(p => pos >= p.start && pos <= p.end);
const unit = (pos - pathSeg.start) / pathSeg.dist; // unit distance on segment
res.x = pathSeg.x + pathSeg.dx * unit; // x, y position on segment
res.y = pathSeg.y + pathSeg.dy * unit;
return res;
}
function update(time){
// startTime ??= time; // Throws syntax on iOS
startTime = startTime ?? time; // Fix for above
ctx.clearRect(0, 0, canvas.width, canvas.height);
if (paused) { startTime = time - animTime }
else { animTime = time - startTime }
getPos(pathCoords, (animTime / 1000) / pathTime, circle).draw();
getPos(pathCoords, timeSlide.value, circle).draw(5, "red");
requestAnimationFrame(update);
}
pause.addEventListener("click", ()=> { paused = true; pause.classList.add("pause") });
play.addEventListener("click", ()=> { paused = false; pause.classList.remove("pause") });
rewind.addEventListener("click", ()=> { startTime = undefined; animTime = 0 });
div {
position:absolute;
top: 5px;
left: 20px;
}
#timeSlide {width: 360px}
.pause {color:blue}
button {height: 30px}
<div><input id="timeSlide" type="range" min="0" max="1" step="0.001" value="0" width= "200"><button id="rewind">Start</button><button id="pause">Pause</button><button id="play">Play</button></div>
<canvas id="canvas" width="200" height="200"></canvas>
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// array of path coords
const pathCoords = [
[200,100],
[300, 150],
[200,190],
[400,100],
[50,10],
[150,10],
[0, 50],
[500,90],
[20,190],
[10,180],
];
// current point
let currentTarget = pathCoords.shift();
//circle object
const circle ={
x:10,
y:10,
radius:10,
dx:2,
dy:2,
color:'blue'
}
//function to draw above circle on canvas
function drawCircle(){
ctx.beginPath();
ctx.arc(circle.x,circle.y,circle.radius,0,Math.PI*2);
ctx.fillStyle=circle.color;
ctx.fill();
ctx.closePath();
}
//Moving to a target coordinate (targetX,targetY)
function goTo(targetX, targetY){
if(Math.abs(circle.x-targetX)<circle.dx && Math.abs(circle.y-targetY)<circle.dy){
// dont stop...
//circle.dx = 0;
//circle.dy = 0;
circle.x = targetX;
circle.y = targetY;
// go to next point
if (pathCoords.length) {
currentTarget = pathCoords.shift();
} else {
console.log('Path end');
}
} else {
const opp = targetY - circle.y;
const adj = targetX - circle.x;
const angle = Math.atan2(opp,adj)
circle.x += Math.cos(angle)*circle.dx
circle.y += Math.sin(angle)*circle.dy
}
}
function update(){
ctx.clearRect(0,0,canvas.width,canvas.height);
drawCircle();
goTo(...currentTarget);
requestAnimationFrame(update);
}
update();
<canvas id=canvas width = 500 height = 200></canvas>
Related
I would like to "zoom" (mouse wheel) a grid I have, around the mouse position in canvas (like Desmos does).
By zooming, I mean redrawing the lines inside the canvas to look like a zoom effect, NOT performing an actual zoom.
And I would like to only use vanilla javascript and no libraries (so that I can learn).
At this point, I set up a very basic magnification effect that only multiplies the distance between the gridlines, based on the mouse wheel values.
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(-this.width/2, -this.height/2, this.width, this.height);
// Horizontal lines
this.xSubdivs = Math.floor(this.height / this.cellSize);
for (let i = 0; i <= this.xSubdivs; i++) {this.setHorizontalLines(i);}
// Vertical lines
this.ySubdivs = Math.floor(this.width / this.cellSize);
for (let i = 0; i <= this.ySubdivs; i++) {this.setVerticalLines(i) ;}
// Axis
this.setAxis();
}
setHorizontalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % this.cellBlock == 0) {
// light lines
context.strokeStyle = gridCellColor;
}
else{
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * i) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * i) - this.height/2);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
context.lineWidth = 0.5;
if (i % cellBlock == 0) {
// Light lines
context.strokeStyle = gridCellColor;
}
else {
// Dark lines
context.strokeStyle = gridBlockColor;
}
//Draw lines
context.beginPath();
context.moveTo((this.cellSize * i) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * i) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
// Axis are separated from the line loops so that they remain on
// top of them (cosmetic measure)
setAxis(){
// Style x Axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw x Axis
context.beginPath();
context.moveTo(-this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.lineTo( this.width/2, (this.cellSize * yAxisOffset) - this.height/2);
context.stroke();
context.closePath();
// Style y axis
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
// Draw y axis
context.beginPath();
context.moveTo((this.cellSize * xAxisOffset ) - this.width/2, -this.height/2);
context.lineTo((this.cellSize * xAxisOffset ) - this.width/2, this.height/2);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Initialize coordinates in the middle of the canvas
context.translate(canvasWidth/2,canvasHeight/2)
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom(){
grid.cellSize = grid.cellSize + zoom;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<span>Use mouse wheel to zoom</span>
<canvas id="canvas"></canvas>
This works fine but, as expected, it magnifies the grid from the top left corner not from the mouse position.
So, I thought about detecting the mouse position and then modifying all the "moveTo" and "lineTo" parts. The goal would be to offset the magnified grid so that everything is displaced except the 2 lines intersecting the current mouse coordinates.
For instance, it feels to me that instead of this:
context.moveTo(
(this.cellSize * i) - this.width/2,
-this.height/2
);
I should have something like this:
context.moveTo(
(this.cellSize * i) - this.width/2 + OFFSET BASED ON MOUSE COORDS,
-this.height/2
);
But after hours of trials and errors, I'm stuck. So, any help would be appreciated.
FYI: I already coded a functional panning system that took me days to achieve (that I stripped from the code here for clarity), so I would like to keep most of the logic I have so far, if possible.
Unless my code is total nonsense or has performance issues, of course.
Thank you.
You're already tracking the mouse position in pixels. If you transform the mouse position to the coordinate system of your grid, you can define which grid cell it's hovering.
After zooming in, you can again calculate the mouse's coordinate. When zooming in around another center point, you'll see the coordinate shift.
You can undo that shift by translating the grid's center in the opposite direction.
Here's the main part of it:
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
// Make the zoom happen: update the cellSize
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
To make this easier to work with, I introduced a grid.centerX and .centerY. I updated your draw method to take the center in to account (and kind of butchered it along the way, but I think you'll manage to improve that a bit).
Here's the complete example:
////////////////////////////////////////////////////////////////////////////////
// User contants
const canvasWidth = 400;
const canvasHeight = 200;
const canvasBackground = '#282c34';
const gridCellColor = "#777";
const gridBlockColor = "#505050";
const axisColor = "white";
// Internal constants
const canvas = document.getElementById('canvas');
const context = canvas.getContext('2d', { alpha: false });
const bodyToCanvas = 8;
////////////////////////////////////////////////////////////////////////////////
// User variables
let cellSize = 10;
let cellBlock = 5;
let xSubdivs = 40
let ySubdivs = 20
// Internal variables
let grid = '';
let zoom = 0;
let xAxisOffset = xSubdivs/2;
let yAxisOffset = ySubdivs/2;
let mousePosX = 0;
let mousePosY = 0;
////////////////////////////////////////////////////////////////////////////////
// Classes
class Grid{
constructor() {
this.width = canvasWidth,
this.height = canvasHeight,
this.cellSize = cellSize,
this.cellBlock = cellBlock,
this.xSubdivs = xSubdivs,
this.ySubdivs = ySubdivs,
this.centerX = canvasWidth / 2,
this.centerY = canvasHeight / 2
}
draw(){
// Show canvas
context.fillStyle = canvasBackground;
context.fillRect(0, 0, this.width, this.height);
// Horizontal lines
const minIY = -Math.ceil(this.centerY / this.cellSize);
const maxIY = Math.ceil((this.height - this.centerY) / this.cellSize);
for (let i = minIY; i <= maxIY; i++) {this.setHorizontalLines(i);}
// Vertical lines
const minIX = -Math.ceil(this.centerX / this.cellSize);
const maxIX = Math.ceil((this.width - this.centerX) / this.cellSize);
for (let i = minIX; i <= maxIX; i++) {this.setVerticalLines(i) ;}
this.setVerticalLines(0);
this.setHorizontalLines(0);
}
setLineStyle(i) {
if (i === 0) {
context.lineWidth = 1.5;
context.strokeStyle = axisColor;
} else if (i % cellBlock == 0) {
// Light lines
context.lineWidth = 0.5;
context.strokeStyle = gridCellColor;
} else {
// Dark lines
context.lineWidth = 0.5;
context.strokeStyle = gridBlockColor;
}
}
setHorizontalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const y = this.centerY + this.cellSize * i;
context.beginPath();
context.moveTo(0, y);
context.lineTo(this.width, y);
context.stroke();
context.closePath();
}
setVerticalLines(i) {
// Style
this.setLineStyle(i);
//Draw lines
const x = this.centerX + this.cellSize * i;
context.beginPath();
context.moveTo(x, 0);
context.lineTo(x, this.height);
context.stroke();
context.closePath();
}
}
////////////////////////////////////////////////////////////////////////////////
// Functions
function init() {
// Set up canvas
if (window.devicePixelRatio > 1) {
canvas.width = canvasWidth * window.devicePixelRatio;
canvas.height = canvasHeight * window.devicePixelRatio;
context.scale(window.devicePixelRatio, window.devicePixelRatio);
}
else {
canvas.width = canvasWidth;
canvas.height = canvasHeight;
}
canvas.style.width = canvasWidth + "px";
canvas.style.height = canvasHeight + "px";
// Setup the grid
grid = new Grid();
// Display the grid
grid.draw();
}
function setZoom() {
// Calculate the mouse position before applying the zoom
// in the coordinate system of the grid
const x1 = (mousePosX - grid.centerX) / grid.cellSize;
const y1 = (mousePosY - grid.centerY) / grid.cellSize;
grid.cellSize = grid.cellSize + zoom;
// After zoom, you'll see the coordinates changed
const x2 = (mousePosX - grid.centerX) / grid.cellSize;
const y2 = (mousePosY - grid.centerY) / grid.cellSize;
// Calculate the shift
const dx = x2 - x1;
const dy = y2 - y1;
// "Undo" the shift by shifting the coordinate system's center
grid.centerX += dx * grid.cellSize;
grid.centerY += dy * grid.cellSize;
grid.draw();
}
////////////////////////////////////////////////////////////////////////////////
//Launch the page
init();
////////////////////////////////////////////////////////////////////////////////
// Update the page on resize
window.addEventListener("resize", init);
// Zoom the canvas with mouse wheel
canvas.addEventListener('mousewheel', function (e) {
e.preventDefault();
e.stopPropagation();
zoom = e.wheelDelta/120;
requestAnimationFrame(setZoom);
})
// Get mouse coordinates on mouse move.
canvas.addEventListener('mousemove', function (e) {
e.preventDefault();
e.stopPropagation();
mousePosX = parseInt(e.clientX)-bodyToCanvas ;
mousePosY = parseInt(e.clientY)-bodyToCanvas ;
})
////////////////////////////////////////////////////////////////////////////////
html, body
{
background:#21252b;
width:100%;
height:100%;
margin:0px;
padding:0px;
overflow: hidden;
}
span{
color:white;
font-family: arial;
font-size: 12px;
margin:8px;
}
#canvas{
margin:8px;
border: 1px solid white;
}
<canvas id="canvas"></canvas>
Can somebody fix it script to make it works properly?
What I expects:
Run script
Click at the canvas to set target (circle)
Object (triangle) starts to rotate and move towards to target (circle)
Change target at any time
How it works:
Sometimes object rotates correctly, sometimes isn't
Looks like one half sphere works well, another isn't
Thanks!
// prepare 2d context
const c = window.document.body.appendChild(window.document.createElement('canvas'))
.getContext('2d');
c.canvas.addEventListener('click', e => tgt = { x: e.offsetX, y: e.offsetY });
rate = 75 // updates delay
w = c.canvas.width;
h = c.canvas.height;
pi2 = Math.PI * 2;
// object that moves towards the target
obj = {
x: 20,
y: 20,
a: 0, // angle
};
// target
tgt = undefined;
// main loop
setInterval(() => {
c.fillStyle = 'black';
c.fillRect(0, 0, w, h);
// update object state
if (tgt) {
// draw target
c.beginPath();
c.arc(tgt.x, tgt.y, 2, 0, pi2);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
// update object position
// vector from obj to tgt
dx = tgt.x - obj.x;
dy = tgt.y - obj.y;
// normalize
l = Math.sqrt(dx*dx + dy*dy);
dnx = (dx / l);// * 0.2;
dny = (dy / l);// * 0.2;
// update object position
obj.x += dnx;
obj.y += dny;
// angle between +x and tgt
a = Math.atan2(0 * dx - 1 * dy, 1 * dx + 0 * dy);
// update object angle
obj.a += -a * 0.04;
}
// draw object
c.translate(obj.x, obj.y);
c.rotate(obj.a);
c.beginPath();
c.moveTo(5, 0);
c.lineTo(-5, 4);
c.lineTo(-5, -4);
//c.lineTo(3, 0);
c.closePath();
c.strokeStyle = 'red';
c.stroke();
c.rotate(-obj.a);
c.translate(-obj.x, -obj.y);
}, rate);
This turned out to be a bit more challenging than I first thought and I ended up just re-writing the code.
The challenges:
Ensure the ship only rotated to the exact point of target. This required me to compare the two angle from the ship current position to where we want it to go.
Ensure the target did not rotate past the target and the ship did not translate past the target. This required some buffer space for each because when animating having this.x === this.x when an object is moving is very rare to happen so we need some room for the logic to work.
Ensure the ship turned in the shortest direction to the target.
I have added notes in the code to better explain. Hopefully you can implement this into yours or use it as is. Oh and you can change the movement speed and rotation speed as you see fit.
let canvas = document.getElementById("canvas");
let ctx = canvas.getContext("2d");
canvas.width = 400;
canvas.height = 400;
let mouse = { x: 20, y: 20 };
let canvasBounds = canvas.getBoundingClientRect();
let target;
canvas.addEventListener("mousedown", (e) => {
mouse.x = e.x - canvasBounds.x;
mouse.y = e.y - canvasBounds.y;
target = new Target();
});
class Ship {
constructor() {
this.x = 20;
this.y = 20;
this.ptA = { x: 15, y: 0 };
this.ptB = { x: -15, y: 10 };
this.ptC = { x: -15, y: -10 };
this.color = "red";
this.angle1 = 0;
this.angle2 = 0;
this.dir = 1;
}
draw() {
ctx.save();
//use translate to move the ship
ctx.translate(this.x, this.y);
//angle1 is the angle from the ship to the target point
//angle2 is the ships current rotation angle. Once they equal each other then the rotation stops. When you click somewhere else they are no longer equal and the ship will rotate again.
if (!this.direction(this.angle1, this.angle2)) {
//see direction() method for more info on this
if (this.dir == 1) {
this.angle2 += 0.05; //change rotation speed here
} else if (this.dir == 0) {
this.angle2 -= 0.05; //change rotation speed here
}
} else {
this.angle2 = this.angle1;
}
ctx.rotate(this.angle2);
ctx.beginPath();
ctx.strokeStyle = this.color;
ctx.moveTo(this.ptA.x, this.ptA.y);
ctx.lineTo(this.ptB.x, this.ptB.y);
ctx.lineTo(this.ptC.x, this.ptC.y);
ctx.closePath();
ctx.stroke();
ctx.restore();
}
driveToTarget() {
//get angle to mouse click
this.angle1 = Math.atan2(mouse.y - this.y, mouse.x - this.x);
//normalize vector
let vecX = mouse.x - this.x;
let vecY = mouse.y - this.y;
let dist = Math.hypot(vecX, vecY);
vecX /= dist;
vecY /= dist;
//Prevent continuous x and y increment by checking if either vec == 0
if (vecX != 0 || vecY != 0) {
//then also give the ship a little buffer incase it passes the given point it doesn't turn back around. This allows time for it to stop if you increase the speed.
if (
this.x >= mouse.x + 3 ||
this.x <= mouse.x - 3 ||
this.y >= mouse.y + 3 ||
this.y <= mouse.y - 3
) {
this.x += vecX; //multiple VecX by n to increase speed (vecX*2)
this.y += vecY; //multiple VecY by n to increase speed (vecY*2)
}
}
}
direction(ang1, ang2) {
//converts rads to degrees and ensures we get numbers from 0-359
let a1 = ang1 * (180 / Math.PI);
if (a1 < 0) {
a1 += 360;
}
let a2 = ang2 * (180 / Math.PI);
if (a2 < 0) {
a2 += 360;
}
//checks whether the target is on the right or left side of the ship.
//We use then to ensure it turns in the shortest direction
if ((360 + a1 - a2) % 360 > 180) {
this.dir = 0;
} else {
this.dir = 1;
}
//Because of animation timeframes there is a chance the ship could turn past the target if rotating too fast. This gives the ship a 1 degree buffer to either side of the target to determine if it is pointed in the right direction.
//We then correct it to the exact degrees in the draw() method above once the if statment defaults to 'else'
if (
Math.trunc(a2) <= Math.trunc(a1) + 1 &&
Math.trunc(a2) >= Math.trunc(a1) - 1
) {
return true;
}
return false;
}
}
let ship = new Ship();
class Target {
constructor() {
this.x = mouse.x;
this.y = mouse.y;
this.r = 3;
this.color = "red";
}
draw() {
ctx.strokeStyle = this.color;
ctx.beginPath();
ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2, false);
ctx.stroke();
}
}
function animate() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
ship.draw();
ship.driveToTarget();
if (target) {
target.draw();
}
requestAnimationFrame(animate);
}
animate();
<canvas id="canvas"></canvas>
I'm trying to make an animation inside a canvas: here, a numbered circle must be drawn and move from left to right one single time, disappearing as soon as it reaches the end of animation.
For now I managed to animate it in loop, but I need to animate at the same time (or with a set delay) multiple numbered circles, strating in different rows (changing y position) so they wont overlap.
Any idea how can I manage this? my JS code is the following:
// Single Animated Circle - Get Canvas element by Id
var canvas = document.getElementById("canvas");
// Set Canvas dimensions
canvas.width = 300;
canvas.height = 900;
// Get drawing context
var ctx = canvas.getContext("2d");
// Radius
var radius = 13;
// Starting Position
var x = radius;
var y = radius;
// Speed in x and y direction
var dx = 1;
var dy = 0;
// Generate random number
var randomNumber = Math.floor(Math.random() * 60) + 1;
if (randomNumber > 0 && randomNumber <= 10) {
ctx.strokeStyle = "#0b0bf1";
} else if (randomNumber > 10 && randomNumber <= 20) {
ctx.strokeStyle = "#f10b0b";
} else if (randomNumber > 20 && randomNumber <= 30) {
ctx.strokeStyle = "#0bf163";
} else if (randomNumber > 30 && randomNumber <= 40) {
ctx.strokeStyle = "#f1da0b";
} else if (randomNumber > 40 && randomNumber <= 50) {
ctx.strokeStyle = "#950bf1";
} else if (randomNumber > 50 && randomNumber <= 60) {
ctx.strokeStyle = "#0bf1e5";
}
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, 300, 900);
if (x + radius > 300 || x - radius < 0) {
x = radius;
}
x += dx;
ctx.beginPath();
ctx.arc(x, y, 12, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(randomNumber, x - 5, y + 3);
}
// Animate the Circle
animate3();
<canvas id="canvas"></canvas>
Here is a solution which doesn't use classes as such and separates the animation logic from the updating - which can be useful if you want more precise control over timing.
// Some helper functions
const clamp = (number, min, max) => Math.min(Math.max(number, min), max);
// Choose and remove random member of arr with equal probability
const takeRandom = arr => arr.splice(parseInt(Math.random() * arr.length), 1)[0]
// Call a function at an interval, passing the amount of time that has passed since the last call
function update(callBack, interval) {
let now = performance.now();
let last;
setInterval(function() {
last = now;
now = performance.now();
callBack((now - last) / 1000);
})
}
const settings = {
width: 300,
height: 150,
radius: 13,
gap: 5,
circles: 5,
maxSpeed: 100,
colors: ["#0b0bf1", "#f10b0b", "#0bf163", "#f1da0b", "#950bf1", "#0bf1e5"]
};
const canvas = document.getElementById("canvas");
canvas.width = settings.width;
canvas.height = settings.height;
const ctx = canvas.getContext("2d");
// Set circle properties
const circles = [...Array(settings.circles).keys()].map(i => ({
number: i + 1,
x: settings.radius,
y: settings.radius + (settings.radius * 2 + settings.gap) * i,
radius: settings.radius,
dx: settings.maxSpeed * Math.random(), // This is the speed in pixels per second
dy: 0,
color: takeRandom(settings.colors)
}));
function drawCircle(circle) {
ctx.strokeStyle = circle.color;
ctx.beginPath();
ctx.arc(circle.x, circle.y, circle.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(circle.number, circle.x - 5, circle.y + 3);
}
function updateCircle(circle, dt) {
// Update a circle's position after dt seconds
circle.x = clamp(circle.x + circle.dx * dt, circle.radius + 1, settings.width - circle.radius - 1);
circle.y = clamp(circle.y + circle.dy * dt, circle.radius + 1, settings.height - circle.radius - 1);
}
function animate() {
ctx.clearRect(0, 0, settings.width, settings.height);
circles.forEach(drawCircle);
requestAnimationFrame(animate);
}
update(dt => circles.forEach(circle => updateCircle(circle, dt)), 50);
animate();
<canvas id="canvas" style="border: solid 1px black"></canvas>
Here I transformed your sample code into a class ...
We pass all the data as a parameter, you can see that in the constructor, I simplified a lot of your code to keep it really short, but all the same drawing you did is there in the draw function
Then all we need to do is create instances of this class and call the draw function inside that animate3 loop you already have.
You had a hardcoded value on the radius:
ctx.arc(x, y, 12, 0, Math.PI * 2, false)
I assume that was a mistake and fix it on my code
var canvas = document.getElementById("canvas");
canvas.width = canvas.height = 300;
var ctx = canvas.getContext("2d");
class Circle {
constructor(data) {
this.data = data
}
draw() {
if (this.data.x + this.data.radius > 300 || this.data.x - this.data.radius < 0) {
this.data.x = this.data.radius;
}
this.data.x += this.data.dx;
ctx.beginPath();
ctx.arc(this.data.x, this.data.y, this.data.radius, 0, Math.PI * 2, false);
ctx.stroke();
ctx.fillText(this.data.number, this.data.x - 5, this.data.y + 3);
}
}
circles = []
circles.push(new Circle({radius:13, x: 10, y: 15, dx: 1, dy: 0, number: "1"}))
circles.push(new Circle({radius:10, x: 10, y: 50, dx: 2, dy: 0, number: "2"}))
function animate3() {
requestAnimationFrame(animate3);
ctx.clearRect(0, 0, canvas.width, canvas.height);
circles.forEach(item => item.draw());
}
animate3();
<canvas id="canvas"></canvas>
Code should be easy to follow let me know if you have any questions
In the Below code link HTML5 canvas spin wheel game. I want to stop this canvas at a user-defined position as if the user wants to stop always at 200 texts or 100 texts like that.
Currently, it is stopping at random points I want to control where to stop as in if I want to stop circle at 100 or 200 or 0 whenever I want.
How can we achieve that??? Can anyone Help!!!!!
Attached Codepen link also.
Html file
<div>
<canvas class="spin-wheel" id="canvas" width="300" height="300"></canvas>
</div>
JS file
var color = ['#ca7','#7ac','#77c','#aac','#a7c','#ac7', "#caa"];
var label = ['10', '200','50','100','5','500',"0"];
var slices = color.length;
var sliceDeg = 360/slices;
var deg = 270;
var speed = 5;
var slowDownRand = 0;
var ctx = canvas.getContext('2d');
var width = canvas.width; // size
var center = width/2; // center
var isStopped = false;
var lock = false;
function rand(min, max) {
return Math.random() * (max - min) + min;
}
function deg2rad(deg){ return deg * Math.PI/180; }
function drawSlice(deg, color){
ctx.beginPath();
ctx.fillStyle = color;
ctx.moveTo(center, center);
ctx.arc(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg));
console.log(center, center, width/2, deg2rad(deg), deg2rad(deg+sliceDeg))
ctx.lineTo(center, center);
ctx.fill();
}
function drawText(deg, text) {
ctx.save();
ctx.translate(center, center);
ctx.rotate(deg2rad(deg));
ctx.textAlign = "right";
ctx.fillStyle = "#fff";
ctx.font = 'bold 30px sans-serif';
ctx.fillText(text, 130, 10);
ctx.restore();
}
function drawImg() {
ctx.clearRect(0, 0, width, width);
for(var i=0; i<slices; i++){
drawSlice(deg, color[i]);
drawText(deg+sliceDeg/2, label[i]);
deg += sliceDeg;
}
}
// ctx.rotate(360);
function anim() {
isStopped = true;
deg += speed;
deg %= 360;
// Increment speed
if(!isStopped && speed<3){
speed = speed+1 * 0.1;
}
// Decrement Speed
if(isStopped){
if(!lock){
lock = true;
slowDownRand = rand(0.994, 0.998);
}
speed = speed>0.2 ? speed*=slowDownRand : 0;
}
// Stopped!
if(lock && !speed){
var ai = Math.floor(((360 - deg - 90) % 360) / sliceDeg); // deg 2 Array Index
console.log(slices)
ai = (slices+ai)%slices; // Fix negative index
return alert("You got:\n"+ label[ai] ); // Get Array Item from end Degree
// ctx.arc(150,150,150,8.302780584487312,9.200378485512967);
// ctx.fill();
}
drawImg();
window.requestAnimationFrame(anim);
}
function start() {
anim()
}
drawImg();
Spin wheel codepen
Ease curves
If you where to plot the wheel position over time as it slows to a stop you would see a curve, a curve that looks like half a parabola.
You can get the very same curve if you plot the value of x squared in the range 0 to 1 as in the next snippet, the red line shows the plot of f(x) => x * x where 0 <= x <= 1
Unfortunately the plot is the wrong way round and needs to be mirrored in x and y. That is simple by changing the function to f(x) => 1 - (1 - x) ** 2 (Click the canvas to get the yellow line)
const size = 200;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size / 2}).getContext("2d");
document.body.appendChild(ctx.canvas);
ctx.canvas.style.border = "2px solid black";
plot(getData());
plot(unitCurve(x => x * x), "#F00");
ctx.canvas.addEventListener("click",()=>plot(unitCurve(x => 1 - (1 - x) ** 2), "#FF0"), {once: true});
function getData(chart = []) {
var pos = 0, speed = 9, deceleration = 0.1;
while(speed > 0) {
chart.push(pos);
pos += speed;
speed -= deceleration;
}
return chart;
}
function unitCurve(f,chart = []) {
const step = 1 / 100;
var x = 0;
while(x <= 1) {
chart.push(f(x));
x += step
}
return chart;
}
function plot(chart, col = "#000") {
const xScale = size / chart.length, yScale = size / 2 / Math.max(...chart);
ctx.setTransform(xScale, 0, 0, yScale, 0, 0);
ctx.strokeStyle = col;
ctx.beginPath();
chart.forEach((y,x) => ctx.lineTo(x,y));
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.stroke();
}
In animation this curve is an ease in.
We can create function that uses the ease function, takes the time and returns the position of the wheel. We can provide some additional values that controls how long the wheel will take to stop, the starting position and the all important stop position.
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
// first scale the current time to a value from 0 to 1
const x = (currentTime - startTime) / (endTime - startTime);
// rather than the square, we will use the square root (this flips the curve)
const xx = x ** (1 / 2);
// convert the value to a wheel position
return xx * (endPos - startPos) + startPos;
}
Demo
The demo puts it in action. Rather than using the square root the function in the demo defines the root as the constant slowDownRate = 2.6. The smaller this value the greater start speed and the slower the end speed. A value of 1 means it will move at a constant speed and then stop. The value must be > 0 and < 1
requestAnimationFrame(mainLoop);
Math.TAU = Math.PI * 2;
const size = 160;
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
document.body.appendChild(ctx.canvas);
const stopAt = document.createElement("div")
document.body.appendChild(stopAt);
ctx.canvas.style.border = "2px solid black";
var gTime; // global time
const colors = ["#F00","#F80","#FF0","#0C0","#08F","#00F","#F0F"];
const wheelSteps = 12;
const minSpins = 3 * Math.TAU; // min number of spins before stopping
const spinTime = 6000; // in ms
const slowDownRate = 1 / 1.8; // smaller this value the greater the ease in.
// Must be > 0
var startSpin = false;
var readyTime = 0;
ctx.canvas.addEventListener("click",() => { startSpin = !wheel.spinning });
stopAt.textContent = "Click wheel to spin";
const wheel = { // hold wheel related variables
img: createWheel(wheelSteps),
endTime: performance.now() - 2000,
startPos: 0,
endPos: 0,
speed: 0,
pos: 0,
spinning: false,
set currentPos(val) {
this.speed = (val - this.pos) / 2; // for the wobble at stop
this.pos = val;
},
set endAt(pos) {
this.endPos = (Math.TAU - (pos / wheelSteps) * Math.TAU) + minSpins;
this.endTime = gTime + spinTime;
this.startTime = gTime;
stopAt.textContent = "Spin to: "+(pos + 1);
}
};
function wheelPos(currentTime, startTime, endTime, startPos, endPos) {
const x = ((currentTime - startTime) / (endTime - startTime)) ** slowDownRate;
return x * (endPos - startPos) + startPos;
}
function mainLoop(time) {
gTime = time;
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0, 0, size, size);
if (startSpin && !wheel.spinning) {
startSpin = false;
wheel.spinning = true;
wheel.startPos = (wheel.pos % Math.TAU + Math.TAU) % Math.TAU;
wheel.endAt = Math.random() * wheelSteps | 0;
} else if (gTime <= wheel.endTime) { // wheel is spinning get pos
wheel.currentPos = wheelPos(gTime, wheel.startTime, wheel.endTime, wheel.startPos, wheel.endPos);
readyTime = gTime + 1500;
} else { // wobble at stop
wheel.speed += (wheel.endPos - wheel.pos) * 0.0125;
wheel.speed *= 0.95;
wheel.pos += wheel.speed;
if (wheel.spinning && gTime > readyTime) {
wheel.spinning = false;
stopAt.textContent = "Click wheel to spin";
}
}
// draw wheel
ctx.setTransform(1,0,0,1,size / 2, size / 2);
ctx.rotate(wheel.pos);
ctx.drawImage(wheel.img, -size / 2 , - size / 2);
// draw marker shadow
ctx.setTransform(1,0,0,1,1,4);
ctx.fillStyle = "#0004";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
// draw marker
ctx.setTransform(1,0,0,1,0,0);
ctx.fillStyle = "#F00";
ctx.beginPath();
ctx.lineTo(size - 13, size / 2);
ctx.lineTo(size, size / 2 - 7);
ctx.lineTo(size, size / 2 + 7);
ctx.fill();
requestAnimationFrame(mainLoop);
}
function createWheel(steps) {
const ctx = Object.assign(document.createElement("canvas"),{width: size, height: size}).getContext("2d");
const s = size, s2 = s / 2, r = s2 - 4;
var colIdx = 0;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.fillStyle = colors[colIdx++ % colors.length];
ctx.beginPath();
ctx.moveTo(s2, s2);
ctx.arc(s2, s2, r, aa, aa + Math.TAU / steps);
ctx.fill();
}
ctx.fillStyle = "#FFF";
ctx.beginPath();
ctx.arc(s2, s2, 12, 0, Math.TAU);
ctx.fill();
ctx.beginPath();
ctx.lineWidth = 2;
ctx.arc(s2, s2, r, 0, Math.TAU);
ctx.moveTo(s2 + 12, s2);
ctx.arc(s2, s2, 12, 0, Math.TAU);
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const aa = a - Math.PI / steps;
ctx.moveTo(Math.cos(aa) * 12 + s2, Math.sin(aa) * 12 + s2);
ctx.lineTo(Math.cos(aa) * r + s2, Math.sin(aa) * r + s2);
}
//ctx.fill("evenodd");
ctx.stroke();
ctx.fillStyle = "#000";
ctx.font = "13px arial black";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
const tr = r - 8;
var idx = 1;
for (let a = 0; a < Math.TAU; a += Math.TAU / steps) {
const dx = Math.cos(a);
const dy = Math.sin(a);
ctx.setTransform(dy, -dx, dx, dy, dx * (tr - 4) + s2, dy * (tr - 4) + s2);
ctx.fillText(""+ (idx ++), 0, 0);
}
return ctx.canvas;
}
body { font-family: arial }
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>