Related
I need to draw multiples balls inside a rect. I have a rect and 4 informations. Width and height of the rect.. numbers of balls per line and numbers of lines. That's been said I have to draw, for example, 4 balls at the same line. starting by the corners(That I was able to do) but I can't figure out how to draw more than 2 balls, example: If I have 3 balls, I need to draw 2 in the corners and 1 in the middle, if I have 4 balls... 2 in the corners and 2 in the middle. I had the idea of think about the rect as a matrix but having no luck.. link to see what I mean
If you need to drawn for example n dragon balls on line then you can divide length with n + 1 to get spacing between center of balls, or if you want different offset on start and end then you would divide (width - 2*offset) / (n - 1).
<canvas id="canvas" width="300" height="100">
</canvas>
<script>
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
class Rect {
constructor(x, y, width, heght) {
this.x = x;
this.y = y;
this.width = width;
this.heght = heght;
}
}
class Circle {
constructor(x, y, radius) {
this.x = x;
this.y = y;
this.radius = radius;
}
}
class Scene
{
constructor() {
this.items = [];
}
clear() {
this.items = [];
}
add(item) {
this.items.push(item);
}
draw(ctx) {
for(let item of this.items) {
if (item instanceof Rect) {
ctx.beginPath();
ctx.rect(item.x, item.y, item.width, item.heght);
ctx.stroke();
} else if (item instanceof Circle) {
ctx.beginPath();
ctx.arc(item.x, item.y, item.radius, 0, 2 * Math.PI);
ctx.stroke();
}
}
}
}
const scene = new Scene();
scene.clear();
scene.add(new Rect(0, 0, 300, 100));
let n = 5;
let offset = 30;
let spacing = ((300 - 2 * offset ) / (n - 1));
for (let i = 0; i < n; i++) {
scene.add(new Circle(i * spacing + offset, 50, 25))
}
scene.draw(ctx);
</script>
I loved it and i'm using it now... although i'm having trouble trying to positioning the balls inside my draw... see what I got so far and if you have a little more time to give me a hand in this <3 (I need to put the balls inside the third rect only no matter what width or height the user enter)
function draw() {
context.clearRect(0, 0, canvas.width, canvas.height);
scene.clear();
context.beginPath();
context.strokeRect(zoomedX(0), zoomedY(0), zoomed(width), zoomed(height));
context.strokeRect(zoomedX(55), zoomedY(55), zoomed(width-10), zoomed(height-10));
context.strokeRect(zoomedX(60), zoomedY(60), zoomed(width-20), zoomed(height-20));
context.closePath();
let radius = 8;
let n = 3;
let lines = 3;
let offset = 68;
let offsetY = 68;
let spacing = ((width - 2 * offset ) / (n - 1));
let spacingY = ((height - 2 * offset ) / (lines - 1));
for (let i = 0; i < n; i++) {
for(let j = 0; j < lines ;j++){
scene.add(new Circle(i * spacing + offset, j * spacingY + offset, radius))
}
}
scene.draw(context);
}
I am trying to achieve a tracing effect where the lines have a faded trail. The way I am trying to do it is simply by drawing the solid background once, and then on further frames draw a transparent background before drawing the new lines, so that you can still see a little of the image before it.
The issue is that I do want the lines to fade out completely after some time, but they seem to leave a permanent after image, even after drawing over them repeatedly.
I've tried setting different globalCompositeOperation(s) and it seemed like I was barking up the wrong tree there.
This code is called once
//initiate trace bg
traceBuffer.getContext("2d").fillStyle = "rgba(0, 30, 50, 1)";
traceBuffer.getContext("2d").fillRect(0, 0, traceBuffer.width, traceBuffer.height);
then inside the setInterval function it calls
//draw transparent background
ctx.fillStyle = "rgba(0, 30, 50, 0.04)";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
//set drawing settings
ctx.strokeStyle = "#AAAAAA";
ctx.lineWidth = 4;
for (let i = 0; i < tracer.layer2.length; i++){
ctx.beginPath();
ctx.moveTo(newX, newY);
ctx.lineTo(oldX, oldY);
ctx.stroke();
}
Here's an example: https://i.imgur.com/QTkeIVf.png
On the left is what I am currently getting, and on the right is the edit of what I actually want to happen.
This is how I would do it. I would build a history of the particles moving on the track. The older the position the smaller the value of the alpha value for the fill. Also for a nicer effect I would reduce the size of the circle.
I hope this is what you need.
PS: I would have loved to have your curve. Since I don't have it I've drawn a different one.
const hypotrochoid = document.getElementById("hypotrochoid");
const ctx = hypotrochoid.getContext("2d");
const cw = (hypotrochoid.width = 300);
const ch = (hypotrochoid.height = 300);
const cx = cw / 2,
cy = ch / 2;
ctx.lineWidth = 1;
ctx.strokeStyle = "#d9d9d9";
// variables for the hypotrochoid
let a = 90;
let b = 15;
let h = 50;
// an array where to save the points used to draw the track
let track = [];
//add points to the track array. This will be used to draw the track for the particles
for (var t = 0; t < 2 * Math.PI; t += 0.01) {
let o = {};
o.x = cx + (a - b) * Math.cos(t) + h * Math.cos((a - b) / b * t);
o.y = cy + (a - b) * Math.sin(t) - h * Math.sin((a - b) / b * t);
track.push(o);
}
// a function to draw the track
function drawTrack(ry) {
ctx.beginPath();
ctx.moveTo(ry[0].x, ry[0].y);
for (let t = 1; t < ry.length; t++) {
ctx.lineTo(ry[t].x, ry[t].y);
}
ctx.closePath();
ctx.stroke();
}
// a class of points that are moving on the track
class Point {
constructor(pos) {
this.pos = pos;
this.r = 3;//the radius of the circle
this.history = [];
this.historyLength = 40;
}
update(newPos) {
let old_pos = {};
old_pos.x = this.pos.x;
old_pos.y = this.pos.y;
//save the old position in the history array
this.history.push(old_pos);
//if the length of the track is longer than the max length allowed remove the extra elements
if (this.history.length > this.historyLength) {
this.history.shift();
}
//gry the new position on the track
this.pos = newPos;
}
draw() {
for (let i = 0; i < this.history.length; i++) {
//calculate the alpha value for every element on the history array
let alp = i * 1 / this.history.length;
// set the fill style
ctx.fillStyle = `rgba(0,0,0,${alp})`;
//draw an arc
ctx.beginPath();
ctx.arc(
this.history[i].x,
this.history[i].y,
this.r * alp,
0,
2 * Math.PI
);
ctx.fill();
}
}
}
// 2 points on the track
let p = new Point(track[0]);
let p1 = new Point(track[~~(track.length / 2)]);
let frames = 0;
let n, n1;
function Draw() {
requestAnimationFrame(Draw);
ctx.clearRect(0, 0, cw, ch);
//indexes for the track position
n = frames % track.length;
n1 = (~~(track.length / 2) + frames) % track.length;
//draw the track
drawTrack(track);
// update and draw the first point
p.update(track[n]);
p.draw();
// update and draw the second point
p1.update(track[n1]);
p1.draw();
//increase the frames counter
frames++;
}
Draw();
canvas{border:1px solid}
<canvas id="hypotrochoid"></canvas>
I'm attempting to detect a canvas click and if the click coordinates match a shape. It works fine, the problem is that when the shapes overlap on the canvas (one shape might be smaller than the other) then both shapes are removed. Is there a way to avoid this and only remove one at a time?
addShape() {
const randomNum = (min, max) => Math.round(Math.random() * (max - min) + min),
randomRad = randomNum(10, 100),
randomX = randomNum(randomRad, this.canvas.width - randomRad);
let shape = new Shape(randomX, randomNum, randomRad);
shape.drawShape();
this.shapes.push(shape);
}
canvasClick() {
if(!this.paused) {
const canvasRect = event.target.getBoundingClientRect(),
clickedX = event.clientX - canvasRect.left,
clickedY = event.clientY - canvasRect.top;
for (let i = 0; i < this.shapes.length; i++) {
if(Math.pow(clickedX - this.shapes[i].x, 2) + Math.pow(clickedY - this.shapes[i].y, 2)
< Math.pow(this.shapes[i].rad,2)) {
this.shapes.splice(i, 1);
}
}
}
}
Thanks in advance for the help!
If I have understood what you what, the solution is pretty simple. Just break the loop after you remove one shape. Something like this:
for (let i = 0; i < this.shapes.length; i++) {
if (
Math.pow(clickedX - this.shapes[i].x, 2)
+ Math.pow(clickedY - this.shapes[i].y, 2)
< Math.pow(this.shapes[i].rad, 2)
) {
this.shapes.splice(i, 1);
break; // <--
}
}
You might also what to store the z position of shapes so that you can first sort them in the terms of z and then run this loop so that you don't remove shape below the clicked shape.
This is how I would do it. Please read the comments in the code. I am using the ctx.isPointInPath method but you may use the formula if you prefer.
The main idea is to exit the loop after finding and deleting the first circle.
I hope it helps.
let ctx = canvas.getContext("2d");
canvas.width = 300;
canvas.height = 300;
ctx.fillStyle = "rgba(0,0,0,.5)";
// the array of the circles
let circles = []
class Circle{
constructor(){
this.x = ~~(Math.random() * canvas.width);
this.y = ~~(Math.random() * canvas.height);
this.r = ~~(Math.random() * (40 - 10 + 1) + 10);
this.draw();
}
draw(){
ctx.beginPath();
ctx.arc(this.x,this.y, this.r, 0,2*Math.PI)
}
}
// create 20 circles
for(i=0;i<20;i++){
let c = new Circle();
ctx.fill()
circles.push(c)
}
canvas.addEventListener("click",(e)=>{
// detect the position of the mouse
let m = oMousePos(canvas, e)
for(let i=0;i<circles.length;i++){
// draw a circle but do not fill
circles[i].draw();
// check if the point is in path
if(ctx.isPointInPath(m.x,m.y)){
//remove the circle from the array
circles.splice(i, 1);
// clear the context
ctx.clearRect(0,0,canvas.width,canvas.height)
// redraw all the circles from the array
for(let j=0;j<circles.length;j++){
circles[j].draw();
ctx.fill()
}
//exit the loop
return;
}
}
})
// a function to detect the mouse position
function oMousePos(canvas, evt) {
var ClientRect = canvas.getBoundingClientRect();
return { //objeto
x: Math.round(evt.clientX - ClientRect.left),
y: Math.round(evt.clientY - ClientRect.top)
}
}
canvas{border:1px solid}
<canvas id="canvas"></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'm attempting to draw the rotating line in this canvas animation with trailing opacity but it's not working. I've seen this effect with rectangles and arcs but never with a line, so I'm not sure what I need to add.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
ctx.fillStyle = 'rgba(255, 255, 255, 0.5)';
ctx.fillRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
} else {
timer = 0;
}
requestAnimationFrame(sonar);
}
}
sonar();
jsbin example
Here are two ways to do this: with a gradient and by adding translucent lines.
Sidenote, you should try and only redraw what you need to redraw. I separated the canvases and put one on top of the other so that we don't redraw the grid all the time.
function radians(degrees) {
return degrees * (Math.PI / 180);
}
var timer = 0;
function trail() {
var canvas = document.getElementById('trail');
var ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, innerWidth, innerHeight);
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
if (timer <= 360) {
timer++;
ctx.beginPath();
ctx.fillstyle = 'limegreen';
ctx.moveTo(cx, cy);
ctx.arc(cx,cy,cy,radians(timer-30),radians(timer));
ctx.lineTo(cx + cy * Math.cos(radians(timer)), cy + cy * Math.sin(radians(timer)));
var gradient = ctx.createLinearGradient(
cx+cy*Math.cos(radians(timer)), cy+cy*Math.sin(radians(timer)),
cx+cy*0.9*Math.cos(radians(timer-30)), cy+cy*0.9*Math.sin(radians(timer-30)));
gradient.addColorStop(0,'limegreen');
gradient.addColorStop(1,'transparent');
ctx.strokeStyle='transparent';
ctx.fillStyle = gradient;
ctx.fill();
ctx.beginPath();
var fade = 10;
for(var i =0;i<fade;i++)
{
ctx.moveTo(cx, cy);
ctx.lineTo(cx+cy*Math.cos(radians(180+timer-i*1.3)),cy+cy*Math.sin(radians(180+timer-i*1.3)));
ctx.strokeStyle ="rgba(50,205,50,0.1)";
ctx.lineWidth=5;
ctx.closePath();
ctx.stroke();
}
} else {
timer = 0;
}
requestAnimationFrame(trail);
}
function sonar() {
var canvas = document.getElementById('sonar');
if (canvas) {
var ctx = canvas.getContext('2d');
var cx = innerWidth / 2,
cy = innerHeight / 2;
canvas.width = innerWidth;
canvas.height = innerHeight;
//ctx.clearRect(0, 0, innerWidth, innerHeight);
var radii = [cy, cy - 30, innerHeight / 3.33, innerHeight / 6.67];
for (var a = 0; a < 4; a++) {
ctx.beginPath();
ctx.arc(cx, cy, radii[a], radians(0), radians(360), false);
ctx.strokeStyle = 'limegreen';
ctx.stroke();
ctx.closePath();
}
// draw grid lines
for (var i = 0; i < 12; i++) {
var x = cx + cy * Math.cos(radians(i * 30));
var y = cy + cy * Math.sin(radians(i * 30));
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(x, y);
ctx.lineCap = 'round';
ctx.strokeStyle = 'rgba(50, 205, 50, 0.45)';
ctx.stroke();
ctx.closePath();
}
}
}
sonar();
trail();
canvas{
position: absolute;
}
<canvas id=sonar></canvas>
<canvas id=trail></canvas>
The problem is that to get this effect, you need to draw a triangle with a gradient along an arc, and you can't do that in a canvas. Gradients must be linear or radial.
The other option is to have an inner loop run each time you want to draw the sweeper, and go backwards from your sweeper line, drawing with slightly less opacity each time. But lets say you want your sweep to cover 15 degrees--obviously, if you have a 100% opacity line at d and a 5% opacity line at d - 15, that doesn't do the trick. So start filling in more lines, and more lines...you will have to draw so many lines to make it seem filled your performance would probably suffer.
My suggestion--you shouldn't have to redraw that on every frame. I would just make a PNG that looks like you want it to, and then place it and just rotate it around the center on each frame. No need to redraw it all the time then. That will be much faster than drawing a bunch of lines.
Canvas stack trails.
Below is a quick demo of how to use a stack of canvases to create a trailing effect.
You have a normal on screen canvas (this FX will not effect it) and then a stack of canvases for the trail FX. Each frame you move to the next canvas in the stack, first slightly clearing it then drawing to it what you want to trail. Then you render that canvas and the one just above it to the canvas.
A point to keep in mind is that the trails can also have a hugh range of FX, like blurring (just render each frame stack on itself slightly offset each time you render to it), zoom in and out trails. Trails on top or trails under. You can change the trail distance and much more.
It is overkill but over kill is fun.
The slider above the demo controls the trail length. Also the code need babel because I dont have time to write it for ES5.
Top slider is trail amount.One under that is trail distance. Trail dist does not transition well. Sorry about that.
//==============================================================================
// helper function
function $(query,q1){
if(q1 !== undefined){
if(typeof query === "string"){
var e = document.createElement(query);
if(typeof q1 !== "string"){
for(var i in q1){
e[i] = q1[i];
}
}else{
e.id = q1;
}
return e;
}
return [...query.querySelectorAll(q1)];
}
return [...document.querySelectorAll(query)];
}
function $$(element,e1){
if(e1 !== undefined){
if(typeof element === "string"){
$(element)[0].appendChild(e1);
return e1;
}
element.appendChild(e1);
return e1;
}
document.body.appendChild(element);
return element;
}
function $E(element,types,listener){
if(typeof types === "string"){
types = types.split(",");
}
element = $(element)[0];
types.forEach(t=>{
element.addEventListener(t,listener)
});
return element;
}
function R(I){
if(I === undefined){
return Math.random();
}
return Math.floor(Math.random()*I);
}
//==============================================================================
//==============================================================================
// answer code
// canvas size
const size = 512;
const trailDist = 10; // There is this many canvases so be careful
var trailDistCurrent = 10; // distance between trails
var clearAll = false;
// create a range slider for trail fade
$$($("input",{type:"range",width : size, min:0, max:100, step:0.1, value:50, id:"trail-amount",title:"Trail amount"}));
$("#trail-amount")[0].style.width = size + "px";
$E("#trail-amount","change,mousemove",function(e){fadeAmount = Math.pow(this.value / 100,2);});
// create a range slider trail distance
$$($("input",{type:"range",width : size, min:2, max:trailDist , step:1, value:trailDist , id:"trail-dist",title:"Trail seperation"}));
$("#trail-dist")[0].style.width = size + "px";
$E("#trail-dist","change,mousemove", function(e){
if(this.value !== trailDistCurrent){
trailDistCurrent= this.value;
clearAll = true;
}
});
$$($("br","")) // put canvas under the slider
// Main canvas
var canvas;
$$(canvas = $("canvas",{width:size,height:size})); // Not jquery. Just creates a canvas
// and adds canvas to the document
var ctx = canvas.getContext("2d");
// Trailing canvas
var trailCanvases=[];
var i =0; // create trail canvas
while(i++ < trailDist){trailCanvases.push($("canvas",{width:size,height:size}));}
var ctxT = trailCanvases.map(c=>c.getContext("2d")); // get context
var topCanvas = 0;
var fadeAmount = 0.5;
// Draw a shape
function drawShape(ctx,shape){
ctx.lineWidth = shape.width;
ctx.lineJoin = "round";
ctx.strokeStyle = shape.color;
ctx.setTransform(shape.scale,0,0,shape.scale,shape.x,shape.y);
ctx.rotate(shape.rot);
ctx.beginPath();
var i = 0;
ctx.moveTo(shape.shape[i++],shape.shape[i++]);
while(i < shape.shape.length){
ctx.lineTo(shape.shape[i++],shape.shape[i++]);
}
ctx.stroke();
}
// Create some random shapes
var shapes = (function(){
function createRandomShape(){
var s = [];
var len = Math.floor(Math.random()*5 +4)*2;
while(len--){
s[s.length] = (R() + R()) * 20 * (R() < 0.5 ? -1 : 1);
}
return s;
}
var ss = [];
var i = 10;
while(i--){
ss[ss.length] = createRandomShape();
}
ss[ss.length] = [0,0,300,0]; // create single line
return ss;
})();
// Create some random poits to move the shapes
var points = (function(){
function point(){
return {
color : "hsl("+R(360)+",100%,50%)",
shape : shapes[R(shapes.length)],
width : R(4)+1,
x : R(size),
y : R(size),
scaleMax : R()*0.2 + 1,
scale : 1,
s : 0,
rot : R()*Math.PI * 2,
dr : R()*0.2 -0.1,
dx : R()*2 - 1,
dy : R()*2 - 1,
ds : R() *0.02 + 0.01,
}
}
var line = shapes.pop();
var ss = [];
var i = 5;
while(i--){
ss[ss.length] = point();
}
var s = ss.pop();
s.color = "#0F0";
s.x = s.y = size /2;
s.dx = s.dy = s.ds = 0;
s.scaleMax = 0.5;
s.dr = 0.02;
s.shape = line;
s.width = 6;
ss.push(s);
return ss;
})();
var frameCount = 0; // used to do increamental fades for long trails
function update(){
// to fix the trail distance problem when fade is low and distance high
if(clearAll){
ctxT.forEach(c=>{
c.setTransform(1,0,0,1,0,0);
c.clearRect(0,0,size,size);
});
clearAll = false;
}
frameCount += 1;
// get the next canvas that the shapes are drawn to.
topCanvas += 1;
topCanvas %= trailDistCurrent;
var ctxTop = ctxT[topCanvas];
// clear the main canvas
ctx.setTransform(1,0,0,1,0,0); // reset transforms
// Fade the trail canvas
ctxTop.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,size,size); // clear main canvas
// slowly blendout trailing layer
if(fadeAmount < 0.1){ // fading much less than this leaves perminant trails
// so at low levels just reduce how often the fade is done
if(((Math.floor(frameCount/trailDistCurrent)+topCanvas) % Math.ceil(1 / (fadeAmount * 10))) === 0 ){
ctxTop.globalAlpha = 0.1;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
}else{
ctxTop.globalAlpha = fadeAmount;
ctxTop.globalCompositeOperation = "destination-out";
ctxTop.fillRect(0,0,size,size);
}
ctxTop.globalCompositeOperation = "source-over";
ctxTop.globalAlpha = 1;
// draw shapes
for(var i = 0; i < points.length; i ++){
var p = points[i];
p.x += p.dx; // move the point
p.y += p.dy;
p.rot += p.dr;
p.s += p.ds;
p.dr += Math.sin(p.s) * 0.001;
p.scale = Math.sin(p.s) * p.scaleMax+1;
p.x = ((p.x % size) + size) % size;
p.y = ((p.y % size) + size) % size;
drawShape(ctxTop,p); // draw trailing layer (middle)
}
// draw the trail the most distance from the current position
ctx.drawImage(trailCanvases[(topCanvas + 1)%trailDistCurrent],0,0);
// do it all again.
requestAnimationFrame(update);
}
update();