简体   繁体   English

如何在不中断动画的情况下清除画布?

[英]How to clear the canvas without interrupting animations?

I am visualising flight paths with D3 and Canvas. 我用D3和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. 当前状态是每个圆圈沿着路径可视化,但沿着该线移除前一个圆圈不起作用,因为clearRect几乎不断被调用。

Current state: 当前状态:

帆布

Ideal state (achieved with SVG): 理想状态(通过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. 从概念上讲,每个航班的SVG路径在内存中使用D3的自定义插值生成,其中path.getTotalLength()path.getPointAtLength()沿路径移动圆。

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. fly()函数在内存中创建SVG路径,在圆圈中创建D3选择('plane') - 也在内存中。

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: 通过delta()函数中的自定义插值器,平面将沿路径转换:

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 ...调用简单的draw()函数

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. 这导致圆圈的“路径”延伸,因为圆圈被绘制但未被移除,如上面第一个gif所示。

Full code here: http://blockbuilder.org/larsvers/8e25c39921ca746df0c8995cce20d1a6 完整代码: 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. 然而,自然答案当然是context.clearRect() ,因为需要通过函数管道绘制每个圆的时间延迟(大约一个毫秒+), clearRect几乎不断被触发。
  • 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. 我试图通过仅以特定间隔( Date.now() % 10 === 0等)调用clearRect来驯服画布的永久清除,但这也导致没有好处。
  • 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. 另一个想法是计算前一个圆的位置,并在每个draw()函数中使用一个小的特定clearRect定义来删除该区域。

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. 作为一般规则,如果计算位置的计算很简单,平均笔记本电脑/台式机可以轻松处理800个动画对象。

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. 图像渲染取决于图像大小,但每个像素的颜色和alpha设置对渲染速度没有影响。 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. 该演示仅展示如何将D3.js插值合并到平面更新功能中。 The plane.update now calls the path.getPos(time) which returns true if the plane has arrived. plane.update现在调用path.getPos(time) ,如果飞机已到达,则返回true。 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. 假设您将路径接口写入所需的D3.js功能。

 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 . @ Blindman67是正确的, 清晰的,重绘每一帧,每一帧

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() . 我在这里只是说当处理像arc这样的原始形状而没有太多的颜色变化时,使用arc方法实际上比使用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. 这比drawImage更快,但主要的警告是它只适用于单色的形状集。

I've made an complex plotting app, where I do draw a lot (20K+) of entities, with animated positions. 我制作了一个复杂的绘图应用程序,在那里我绘制了很多(20K +)实体,带有动画位置。 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. 我在大多数设备上达到60fps。 When I tried with drawImage, I was stuck at about 10fps for 5K points. 当我尝试使用drawImage时,我被困在大约10fps的5K点。

Here is a modified version of Blindman67's good answer's snippet, using this single-path approach. 这是使用这种单路径方法的Blindman67的优秀答案片段的修改版本。

 /* 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. 但这是另一个故事。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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