I am trying to make a circle follow the mouse in HTML Canvas which I am using in a game. I am trying to make the circle move 5px per iteration, but it goes slower when traveling horizontal and faster when it goes vertical. Here's the math that I used:
x=distance between mouse and circle on the x-axis
y=distance between mouse and circle on the y-axis
z=shortest distance between mouse and circle
a=number of units circle should move along the x-axis
b=number of units circle should move along the y axis
x^2 + y^2=z^2
Want the total distance traveled every iteration to be five pixels
a^2 + b^2 = 25
b/a=y/x
b=ay/x
a=sqrt(25-ay/x^2)
a^2+ay/x-25=0
Use Quadratic formula to find both answers
a=(-y/x+-sqrt(y/x)^2+100)/2
I replicated the problem in the code below
$(function(){
let canvas = $("canvas")[0];
let ctx = canvas.getContext("2d");
//Gets position of mouse and stores the value in variables mouseX and mouseY
let mouseX = mouseY = 0;
$("canvas").mousemove(function(e){
mouseX = e.pageX;
mouseY = e.pageY;
}).trigger("mousemove");
let circleX = 0;
let circleY = 0;
function loop(t){
//Background
ctx.fillStyle="blue";
ctx.fillRect(0, 0, canvas.width, canvas.height);
let xFromMouse = mouseX-circleX;
let yFromMouse = mouseY-circleY;
let yxRatio = yFromMouse/xFromMouse;
let xyRatio = xFromMouse/yFromMouse;
let speed = 25;
let possibleXValues = [(-yxRatio+Math.sqrt(Math.pow(yxRatio,2)+(4*speed)))/2,(-yxRatio-Math.sqrt(Math.pow(yxRatio,2)+(4*speed)))/2];
//I use this code as a temporary fix to stop the circle from completely disappearing
if(xFromMouse === 0 || isNaN(yxRatio) || isNaN(possibleXValues[0]) || isNaN(possibleXValues[1])){
possibleXValues = [0,0];
yxRatio = 0;
}
//Uses b=ay/x to calculate for y values
let possibleYValues = [possibleXValues[0]*yxRatio,possibleXValues[1]*yxRatio];
if(xFromMouse >= 0){
circleX += possibleXValues[0];
circleY += possibleYValues[0];
} else {
circleX += possibleXValues[1];
circleY += possibleYValues[1];
}
ctx.beginPath();
ctx.arc(circleX, circleY, 25, 0, 2 * Math.PI,false);
ctx.fillStyle = "red";
ctx.lineWidth = 0;
ctx.fill();
window.requestAnimationFrame(loop);
}
window.requestAnimationFrame(loop);
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<canvas width="450" height="250"></canvas>
I think you may be better using a cartesian to polar conversion. Here's an example from something I made previously. This will allow you to have a consistent step per iteration "speed".
//Canvas, context, mouse.
let c, a, m = { x:0, y:0};
//onload.
window.onload = function(){
let circle = {},
w, h,
speed = 5; //step speed = 5 "pixels" (this will be fractional in any one direction depending on direction of travel).
//setup
c = document.getElementById('canvas');
a = c.getContext('2d');
w = c.width = window.innerWidth;
h = c.height = window.innerHeight;
function move(){
//get distance and angle from mouse to circle.
let v1m = circle.x - m.x,
v2m = circle.y - m.y,
vDm = Math.sqrt(v1m*v1m + v2m*v2m),
vAm = Math.atan2(v2m, v1m);
//if distance is above some threshold, to stop jittering, move the circle by 'speed' towards mouse.
if(vDm > speed) {
circle.x -= Math.cos(vAm) * speed;
circle.y -= Math.sin(vAm) * speed;
}
}
function draw(){
//draw it all.
a.fillStyle = "blue";
a.fillRect(0,0,w,h);
a.fillStyle = "red";
a.beginPath();
a.arc(circle.x, circle.y, circle.r, Math.PI * 2, false);
a.closePath();
a.fill();
}
circle = {x:w/2, y:h/2, r:25};
function animate(){
requestAnimationFrame(animate);
move();
draw();
}
c.onmousemove = function(e){
m.x = e.pageX;
m.y = e.pageY;
};
animate();
}
<canvas id="canvas" width="450" height="250"></canvas>
I am visualising flight paths with D3 and Canvas. In short, I have data for each flight's origin and destination
as well as the airport coordinates. The ideal end state is to have an indiviudal circle representing a plane moving
along each flight path from origin to destination. The current state is that each circle gets visualised along the path,
yet the removal of the previous circle along the line does not work as clearRect gets called nearly constantly.
Current state:
Ideal state (achieved with SVG):
The Concept
Conceptually, an SVG path for each flight is produced in memory using D3's custom interpolation with path.getTotalLength() and path.getPointAtLength() to move the circle along the path.
The interpolator returns the points along the path at any given time of the transition. A simple drawing function takes these points and draws the circle.
Key functions
The visualisation gets kicked off with:
od_pairs.forEach(function(el, i) {
fly(el[0], el[1]); // for example: fly('LHR', 'JFK')
});
The fly() function creates the SVG path in memory and a D3 selection of a circle (the 'plane') - also in memory.
function fly(origin, destination) {
var pathElement = document.createElementNS(d3.namespaces.svg, 'path');
var routeInMemory = d3.select(pathElement)
.datum({
type: 'LineString',
coordinates: [airportMap[origin], airportMap[destination]]
})
.attr('d', path);
var plane = custom.append('plane');
transition(plane, routeInMemory.node());
}
The plane gets transitioned along the path by the custom interpolater in the delta() function:
function transition(plane, route) {
var l = route.getTotalLength();
plane.transition()
.duration(l * 50)
.attrTween('pointCoordinates', delta(plane, route))
// .on('end', function() { transition(plane, route); });
}
function delta(plane, path) {
var l = path.getTotalLength();
return function(i) {
return function(t) {
var p = path.getPointAtLength(t * l);
draw([p.x, p.y]);
};
};
}
... which calls the simple draw() function
function draw(coords) {
// contextPlane.clearRect(0, 0, width, height); << how to tame this?
contextPlane.beginPath();
contextPlane.arc(coords[0], coords[1], 1, 0, 2*Math.PI);
contextPlane.fillStyle = 'tomato';
contextPlane.fill();
}
This results in an extending 'path' of circles as the circles get drawn yet not removed as shown in the first gif above.
Full code here: http://blockbuilder.org/larsvers/8e25c39921ca746df0c8995cce20d1a6
My question is, how can I achieve to draw only a single, current circle while the previous circle gets removed without interrupting other circles being drawn on the same canvas?
Some failed attempts:
The natural answer is of course context.clearRect(), however, as there's a time delay (roughly a milisecond+) for each circle to be drawn as it needs to get through the function pipeline clearRect gets fired almost constantly.
I tried to tame the perpetual clearing of the canvas by calling clearRect only at certain intervals (Date.now() % 10 === 0 or the like) but that leads to no good either.
Another thought was to calculate the previous circle's position and remove the area specifically with a small and specific clearRect definition within each draw() function.
Any pointers very much appreciated.
Handling small dirty regions, especially if there is overlap between objects quickly becomes very computationally heavy.
As a general rule, a average Laptop/desktop can easily handle 800 animated objects if the computation to calculate position is simple.
This means that the simple way to animate is to clear the canvas and redraw every frame. Saves a lot of complex code that offers no advantage over the simple clear and redraw.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x += p.ax * p.speed;
p.y += p.ay * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(800,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
As pointed out in the comments some machines may be able to draw a circle if one colour and all as one path slightly quicker (not all machines). The point of rendering an image is that it is invariant to the image complexity. Image rendering is dependent on the image size but colour and alpha setting per pixel have no effect on rendering speed. Thus I have changed the circle to show the direction of each point via a little plane icon.
Path follow example
I have added a way point object to each plane that in the demo has a random set of way points added. I called it path (could have used a better name) and a unique path is created for each plane.
The demo is to just show how you can incorporate the D3.js interpolation into the plane update function. The plane.update now calls the path.getPos(time) which returns true if the plane has arrived. If so the plane is remove. Else the new plane coordinates are used (stored in the path object for that plane) to set the position and direction.
Warning the code for path does little to no vetting and thus can easily be made to throw an error. It is assumed that you write the path interface to the D3.js functionality you want.
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
function createIcon(drawFunc){
const icon = document.createElement("canvas");
icon.width = icon.height = 10;
drawFunc(icon.getContext("2d"));
return icon;
}
function drawPlane(ctx){
const cx = ctx.canvas.width / 2;
const cy = ctx.canvas.height / 2;
ctx.beginPath();
ctx.strokeStyle = ctx.fillStyle = "red";
ctx.lineWidth = cx / 2;
ctx.lineJoin = "round";
ctx.lineCap = "round";
ctx.moveTo(cx/2,cy)
ctx.lineTo(cx * 1.5,cy);
ctx.moveTo(cx,cy/2)
ctx.lineTo(cx,cy*1.5)
ctx.stroke();
ctx.lineWidth = cx / 4;
ctx.moveTo(cx * 1.7,cy * 0.6)
ctx.lineTo(cx * 1.7,cy*1.4)
ctx.stroke();
}
const path = {
wayPoints : null, // holds way points
nextTarget : null, // holds next target waypoint
current : null, // hold previously passed way point
x : 0, // current pos x
y : 0, // current pos y
addWayPoint(x,y,time){
this.wayPoints.push({x,y,time});
},
start(){
if(this.wayPoints.length > 1){
this.current = this.wayPoints.shift();
this.nextTarget = this.wayPoints.shift();
}
},
getNextTarget(){
this.current = this.nextTarget;
if(this.wayPoints.length === 0){ // no more way points
return;
}
this.nextTarget = this.wayPoints.shift(); // get the next target
},
getPos(time){
while(this.nextTarget.time < time && this.wayPoints.length > 0){
this.getNextTarget(); // get targets untill the next target is ahead in time
}
if(this.nextTarget.time < time){
return true; // has arrivecd at target
}
// get time normalised ove time between current and next
var timeN = (time - this.current.time) / (this.nextTarget.time - this.current.time);
this.x = timeN * (this.nextTarget.x - this.current.x) + this.current.x;
this.y = timeN * (this.nextTarget.y - this.current.y) + this.current.y;
return false; // has not arrived
}
}
const planes = {
items : [],
icon : createIcon(drawPlane),
clear(){
planes.items.length = 0;
},
add(x,y){
var p;
planes.items.push(p = {
x,y,
ax : 0, // the direction of the x axis of this plane
ay : 0,
path : Object.assign({},path,{wayPoints : []}),
})
return p; // return the plane
},
update(time){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
if(p.path.getPos(time)){ // target reached
planes.items.splice(i--,1); // remove
}else{
p.dir = Math.atan2(p.y - p.path.y, p.x - p.path.x) + Math.PI; // add 180 because i drew plane wrong way around.
p.ax = Math.cos(p.dir);
p.ay = Math.sin(p.dir);
p.x = p.path.x;
p.y = p.path.y;
}
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.setTransform(-p.ax,-p.ay,p.ay,-p.ax,x,y);
ctx.drawImage(planes.icon,-planes.icon.width / 2,-planes.icon.height / 2);
}
}
}
const ctx = canvas.getContext("2d");
function mainLoop(time){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(810,()=>{
var p = planes.add(Math.random() * canvas.width, Math.random() * canvas.height);
// now add random number of way points
var timeP = time;
// info to create a random path
var dir = Math.random() * Math.PI * 2;
var x = p.x;
var y = p.y;
doFor(Math.floor(Math.random() * 80 + 12),()=>{
var dist = Math.random() * 5 + 4;
x += Math.cos(dir) * dist;
y += Math.sin(dir) * dist;
dir += (Math.random()-0.5)*0.3;
timeP += Math.random() * 1000 + 500;
p.path.addWayPoint(x,y,timeP);
});
// last waypoin at center of canvas.
p.path.addWayPoint(canvas.width / 2,canvas.height / 2,timeP + 5000);
p.path.start();
})
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update(time);
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
}
<canvas id=canvas></canvas>
800 animated points
#Blindman67 is correct, clear and redraw everything, every frame.
I'm here just to say that when dealing with such primitive shapes as arc without too many color variations, it's actually better to use the arc method than drawImage().
The idea is to wrap all your shapes in a single path declaration, using
ctx.beginPath(); // start path declaration
for(i; i<shapes.length; i++){ // loop through our points
ctx.moveTo(pt.x + pt.radius, pt.y); // default is lineTo and we don't want it
// Note the '+ radius', arc starts at 3 o'clock
ctx.arc(pt.x, pt.y, pt.radius, 0, Math.PI*2);
}
ctx.fill(); // a single fill()
This is faster than drawImage, but the main caveat is that it works only for single-colored set of shapes.
I've made an complex plotting app, where I do draw a lot (20K+) of entities, with animated positions. So what I do, is to store two sets of points, one un-sorted (actually sorted by radius), and one
sorted by color. I then do use the sorted-by-color one in my animations loop, and when the animation is complete, I draw only the final frame with the sorted-by-radius (after I filtered the non visible entities). I achieve 60fps on most devices. When I tried with drawImage, I was stuck at about 10fps for 5K points.
Here is a modified version of Blindman67's good answer's snippet, using this single-path approach.
/* All credits to SO user Blindman67 */
const doFor = (count,callback) => {var i=0;while(i < count){callback(i++)}};
const planes = {
items : [],
clear(){
planes.items.length = 0;
},
add(x,y){
planes.items.push({
x,y,
rad: 2,
dir : Math.random() * Math.PI * 2,
speed : Math.random() * 0.2 + 0.1,
dirV : (Math.random() - 0.5) * 0.01, // change in direction
})
},
update(){
var i,p;
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
p.dir += p.dirV;
p.x += Math.cos(p.dir) * p.speed;
p.y += Math.sin(p.dir) * p.speed;
}
},
draw(){
var i,p;
const w = canvas.width;
const h = canvas.height;
ctx.beginPath();
ctx.fillStyle = 'red';
for(i = 0; i < planes.items.length; i ++){
p = planes.items[i];
var x = ((p.x % w) + w) % w;
var y = ((p.y % h) + h) % h;
ctx.moveTo(x + p.rad, y)
ctx.arc(x, y, p.rad, 0, Math.PI*2);
}
ctx.fill();
}
}
const ctx = canvas.getContext("2d");
function mainLoop(){
if(canvas.width !== innerWidth || canvas.height !== innerHeight){
canvas.width = innerWidth;
canvas.height = innerHeight;
planes.clear();
doFor(8000,()=>{ planes.add(Math.random() * canvas.width, Math.random() * canvas.height) })
}
ctx.setTransform(1,0,0,1,0,0);
// clear or render a background map
ctx.clearRect(0,0,canvas.width,canvas.height);
planes.update();
planes.draw();
requestAnimationFrame(mainLoop)
}
requestAnimationFrame(mainLoop)
canvas {
position : absolute;
top : 0px;
left : 0px;
z-index: -1;
}
<canvas id=canvas></canvas>
8000 animated points
Not directly related but in case you've got part of your drawings that don't update at the same rate as the rest (e.g if you want to highlight an area of your map...) then you might also consider separating your drawings in different layers, on offscreen canvases. This way you'd have one canvas for the planes, that you'd clear every frame, and other canvas for other layers that you would update at different rate. But that's an other story.
I am trying to create a game (at beginning stages). I am trying to create balls that bounce around a canvas, I have created balls, randomized them and have animated them.
But when trying to add a boundary I can only seem to get the balls to act as one object rather than separate ones. If NumShapes is changed to 1 it works perfectly.
if( shapes[i].x<0 || shapes[i].x>width) dx=-dx;
if( shapes[i].y<0 || shapes[i].y>height) dy=-dy;
For movement:
shapes[i].x+=dx;
shapes[i].y+=dy;
See this:
var ctx;
var numShapes;
var shapes;
var dx = 5; // speed on the X axis
var dy = 5; // speed on the Y axis
var ctx = canvas.getContext('2d');
var width = canvas.width;
var height = canvas.height;
function init() // draws on the Canvas in the HTML
// calling functions here would not run them in the setInterval
{
numShapes = 10;
shapes = [];
drawScreen();
ctx = canvas.getContext('2d');
setInterval(draw, 10); // Runs the Draw function with nestled functions
makeShapes();
}
function draw() {
clear();
drawShapes();
}
function clear() {
ctx.clearRect(0, 0, width, height); // clears the canvas by WIDTH and HEIGHT variables
}
function makeShapes() {
var i;
var tempRad;
var tempR;
var tempG;
var tempB;
var tempX;
var tempY;
var tempColor;
for (i = 0; i < numShapes; i++) { // runs while i is less than numShapes
tempRad = 10 + Math.floor(Math.random() * 25); // random radius number
tempX = Math.random() * (width - tempRad); // random X value
tempY = Math.random() * (height - tempRad); // random Y value
tempR = Math.floor(Math.random() * 255); // random red value
tempG = Math.floor(Math.random() * 255); // random green value
tempB = Math.floor(Math.random() * 255); // random blue value
tempColor = "rgb(" + tempR + "," + tempG + "," + tempB + ")"; // creates a random colour
tempShape = {
x: tempX,
y: tempY,
rad: tempRad,
color: tempColor
}; // creates a random shape based on X, Y and R
shapes.push(tempShape); // pushes the shape into the array
}
}
function drawShapes() {
var i;
for (i = 0; i < numShapes; i++) {
ctx.fillStyle = shapes[i].color;
ctx.beginPath();
ctx.arc(shapes[i].x, shapes[i].y, shapes[i].rad, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
shapes[i].x += dx; // increases the X value of Shape
shapes[i].y += dy; // increases the Y value of Shape
// Boundary, but applies to all shapes as one shape
if (shapes[i].x < 0 || shapes[i].x > width) dx = -dx;
if (shapes[i].y < 0 || shapes[i].y > height) dy = -dy;
}
}
function drawScreen() {
//bg
ctx.fillStyle = '#EEEEEE';
ctx.fillRect(0, 0, width, height);
//Box
ctx.strokeStyle = '#000000';
ctx.strokeRect(1, 1, width - 2, height - 2);
}
canvas {
border: 1px solid #333;
}
<body onLoad="init();">
<div class="container container-main">
<div class="container-canvas">
<canvas id="canvas" width="800" height="600">
This is my fallback content.
</canvas>
</div>
</div>
</body>
Your dx and dy are globals, they should be unique for each ball object that you are simulating. Either clear them to 0 in your rendering loop (draw) or actually implement a ball object/class to hold variables unique to that object.
When you do your collision detection you change dx and dy which then persists to the next ball object as they are global.
Your fiddle, edited to add local dx and dy per shape: https://jsfiddle.net/a9b3rm5u/3/
tempDx = Math.random()*5; // random DX value
tempDy = Math.random()*5; // random DY value
shapes[i].x+=shapes[i].dx;// increases the X value of Shape
shapes[i].y+=shapes[i].dy;// increases the Y value of Shape
if( shapes[i].x<0 || shapes[i].x>width) shapes[i].dx= - shapes[i].dx;
if( shapes[i].y<0 || shapes[i].y>height) shapes[i].dy= -shapes[i].dy;
I'm working on a canvas-based animation, and I'm trying to get a 3D effect in a 2D canvas.
So far, things are going well! I've got my "orbiting line of triangles" working very well:
var c = document.createElement('canvas');
c.width = c.height = 100;
document.body.appendChild(c);
var ctx = c.getContext("2d");
function Triangles() {
this.rotation = {
x: Math.random()*Math.PI*2,
y: Math.random()*Math.PI*2,
z: Math.random()*Math.PI*2
};
/* Uncomment this for testing perspective...
this.rotation = {
x: Math.PI/2,
y: 0,
z: 0
};
*/
}
Triangles.prototype.draw = function(t) {
this.rotation.z += t/1000;
var i, points;
for( i=0; i<15; i++) {
points = [
this.computeRotation(Math.cos(0.25*i),-Math.sin(0.25*i),0),
this.computeRotation(Math.cos(0.25*(i+1)),-Math.sin(0.25*(i+1)),-0.1),
this.computeRotation(Math.cos(0.25*(i+1)),-Math.sin(0.25*(i+1)),0.1)
];
ctx.fillStyle = "black";
ctx.beginPath();
ctx.moveTo(50+40*points[0][0],50+40*points[0][1]);
ctx.lineTo(50+40*points[1][0],50+40*points[1][1]);
ctx.lineTo(50+40*points[2][0],50+40*points[2][1]);
ctx.closePath();
ctx.fill();
}
};
Triangles.prototype.computeRotation = function(x,y,z) {
var rz, ry, rx;
rz = [
Math.cos(this.rotation.z) * x - Math.sin(this.rotation.z) * y,
Math.sin(this.rotation.z) * x + Math.cos(this.rotation.z) * y,
z
];
ry = [
Math.cos(this.rotation.y) * rz[0] + Math.sin(this.rotation.y) * rz[2],
rz[1],
-Math.sin(this.rotation.y) * rz[0] + Math.cos(this.rotation.y) * rz[2]
];
rx = [
ry[0],
Math.cos(this.rotation.x) * ry[1] - Math.sin(this.rotation.x) * ry[2],
Math.sin(this.rotation.x) * ry[1] + Math.cos(this.rotation.x) * ry[2]
];
return rx;
};
var tri = new Triangles();
requestAnimationFrame(function(start) {
function step(t) {
var delta = t-start;
ctx.clearRect(0,0,100,100)
tri.draw(delta);
start = t;
requestAnimationFrame(step);
}
step(start);
});
As you can see it's using rotation matrices for calculating the position of the points after their rotation, and I'm using this to draw the triangles using the output x and y coordinates.
I want to take this a step further by using the z coordinate and adding perspective to this animation, which will make the triangles slightly bigger when in the foreground, and smaller when in the background. However, I'm not sure how to go about doing this.
I guess this is more of a maths question than a programming one, sorry about that!
Define a focal length to control the amount of perspective. The greater the value the less the amount of perspective. Then
var fl = 200; // focal length;
var px = 100; // point in 3D space
var py = 200;
var pz = 500;
Then to get the screen X,Y
var sx = (px * fl) / pz;
var sy = (py * fl) / pz;
The resulting point is relative to the center of the veiw so you need to center it to the canvas.
sx += canvas.width/2;
sy += canvas.height/2;
That is a point.
It assumes that the point being viewed is in front of the view and further than the focal length from the focal point.
I've managed to figure out a basic solution, but I'm sure there's better ones, so if you have a more complete answer feel free to add it! But for now...
Since the coordinate system is already based around the origin with the viewpoint directly on the Z axis looking at the (x,y) plane, it's actually sufficient to just multiply the (x,y) coordinates by a value proportional to z. For example, x * (z+2)/2 will do just fine in this case
There's bound to be a more proper, general solution though!
I guess I'll just have to show it:
function drawSector(ctx, cxy, rInner, rOuter, radStart, radWidth, color) {
ctx.beginPath();
ctx.arc(cxy, cxy, rInner, radStart, radStart + radWidth);
ctx.lineTo(cxy + rOuter * Math.cos(radStart + radWidth), cxy + rOuter * Math.sin(radStart + radWidth));
ctx.arc(cxy, cxy, rOuter, radStart + radWidth, radStart, true);
ctx.lineTo(cxy + rInner * Math.cos(radStart), cxy + rInner * Math.sin(radStart));
ctx.fillStyle = color;
ctx.fill();
ctx.stroke();
}
function makecircle(diam) {
var canv = document.createElement("canvas");
canv.width = diam; canv.height = diam;
document.getElementById("c").appendChild(canv);
var ctx = canv.getContext("2d");
ctx.strokeStyle = "rgba(0,0,0,1)";
ctx.lineWidth = 5;
var centerXY = diam / 2;
var centerRadius = diam / 6;
var sectorHeight = (centerXY - centerRadius) / 5 - 2;
var sectorAngle = Math.PI * 2 / 15;
for (var r = 0; r < 15; r++) {
for (var h = 0; h < 5; h++) {
drawSector(ctx, centerXY, centerRadius + sectorHeight * h, centerRadius + sectorHeight * (h + 1), r * sectorAngle, sectorAngle, "rgba(255,0,0,0.5)");
}
}
}
makecircle(500);
body {
background-color:darkgreen;
}
<div id="c"></div>
So there is this circle divided into sectors. I want there to be a little space between each sector (currently illustrated by black stroke). Cutting the height/angle width of each sector works but since its a constant angle it widens outwards and I don't like the look.
I figured there must be a way to draw stroke ontop of the circle and have that area become transparent again, is there? (also I can't ditch filling each sector separately since I'm going to make them different colors).
You can "erase" existing pixels by setting globalCompositeOperation='destination-out'.
Then draw your radiating lines. Instead of being visible, the radiating lines will erase any existing pixels that the lines overlap.
When you're done erasing, be sure to set globalCompositeOperation back to its default of 'source-over'.