简体   繁体   中英

How to clear the canvas without interrupting animations?

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):

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); px += p.ax * p.speed; py += 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 = ((px % w) + w) % w; var y = ((py % 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(py - p.path.y, px - 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); px = p.path.x; py = 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 = ((px % w) + w) % w; var y = ((py % 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 = px; var y = py; 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; px += Math.cos(p.dir) * p.speed; py += 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 = ((px % w) + w) % w; var y = ((py % 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 (eg 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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM