admin管理员组

文章数量:1435859

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:

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.

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/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.

Share Improve this question edited May 29, 2017 at 21:09 lve asked May 29, 2017 at 19:48 lvelve 4781 gold badge6 silver badges16 bronze badges 5
  • The standard way to handle this type of animation is to clear the whole canvas and redraw all for every frame. If this is to much of a load you can layer the canvas with the map and other details on one canvas and the other canvas for the flights and bine them for every frame. – Blindman67 Commented May 29, 2017 at 20:40
  • A example for point along path (moving point) here bl.ocks/mbostock/1705868 I believe that only the initial points should be created and then moved across the route – PiLHA Commented May 29, 2017 at 20:44
  • @PiLHA That example is using SVG whereas the question explicitly asks for canvas... – altocumulus Commented May 29, 2017 at 21:00
  • Thanks @Blindman67. The flight paths are indeed on their own canvas layered on top of the map/airport layer. My problem is when to clear the last frame and draw a new frame as the x and y positions of the ~150 flights e in at slightly different times. – lve Commented May 29, 2017 at 21:06
  • 1 @Ive I have added an answer that animates 800 points. The refresh rate will depend on the machine but most will do it in 60FPS. If you have different positions at different times the frame rate will make that inconsequential. – Blindman67 Commented May 29, 2017 at 21:18
Add a ment  | 

2 Answers 2

Reset to default 5

Handling small dirty regions, especially if there is overlap between objects quickly bees very putationally heavy.

As a general rule, a average Laptop/desktop can easily handle 800 animated objects if the putation 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 plex 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 ments 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 plexity. 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 plex 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 plete, 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.

本文标签: javascriptHow to clear the canvas without interrupting animationsStack Overflow