繁体   English   中英

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

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

我用D3和Canvas可视化飞行路径。 简而言之,我有每个航班的起点和终点以及机场坐标的数据。 理想的最终状态是具有表示沿着从起点到目的地的每个飞行路径移动的平面的个体圆。 当前状态是每个圆圈沿着路径可视化,但沿着该线移除前一个圆圈不起作用,因为clearRect几乎不断被调用。

当前状态:

帆布

理想状态(通过SVG实现):

SVG

这个概念

从概念上讲,每个航班的SVG路径在内存中使用D3的自定义插值生成,其中path.getTotalLength()path.getPointAtLength()沿路径移动圆。

插值器在转换的任何给定时间返回沿路径的点。 一个简单的绘图功能可以获取这些点并绘制圆形。

主要功能

可视化开始于:

od_pairs.forEach(function(el, i) {
  fly(el[0], el[1]); // for example: fly('LHR', 'JFK')
});

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());

}

通过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]);
    };
  };

}

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

}

这导致圆圈的“路径”延伸,因为圆圈被绘制但未被移除,如上面第一个gif所示。

完整代码: http//blockbuilder.org/larsvers/8e25c39921ca746df0c8995cce20d1a6

我的问题是,如何在不中断在同一画布上绘制的其他圆圈的情况下删除前一个圆圈时,如何仅绘制一个当前圆圈?

一些失败的尝试:

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

任何指针非常赞赏。

处理小的脏区域,特别是如果对象之间存在重叠,很快就变得非常计算。

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

这意味着动画的简单方法是清除画布并重绘每一帧。 保存了许多复杂的代码,这些代码与简单的清晰和重绘相比没有任何优势。

 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 

正如评论中所指出的,如果一种颜色和一条路径稍微快一些机器(不是所有机器),一些机器可能能够绘制圆形。 渲染图像的关键是它对图像复杂性不变。 图像渲染取决于图像大小,但每个像素的颜色和alpha设置对渲染速度没有影响。 因此,我改变了圆圈,通过一个小平面图标显示每个点的方向。

路径遵循示例

我为每个平面添加了一个方向点对象,在演示中添加了一组随机路径点。 我称它为路径(可能使用了更好的名称),并为每个平面创建了一个唯一的路径。

该演示仅展示如何将D3.js插值合并到平面更新功能中。 plane.update现在调用path.getPos(time) ,如果飞机已到达,则返回true。 如果是这样,飞机将被移除。 否则,使用新的平面坐标(存储在该平面的路径对象中)来设置位置和方向。

警告路径的代码几乎没有审查,因此可以很容易地引发错误。 假设您将路径接口写入所需的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是正确的, 清晰的,重绘每一帧,每一帧

我在这里只是说当处理像arc这样的原始形状而没有太多的颜色变化时,使用arc方法实际上比使用drawImage()更好。

我们的想法是使用将所有形状包装在单个路径声明中

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

这比drawImage更快,但主要的警告是它只适用于单色的形状集。

我制作了一个复杂的绘图应用程序,在那里我绘制了很多(20K +)实体,带有动画位置。 所以我所做的是存储两组点,一组未排序(实际按半径排序),另一组按颜色排序。 然后我在动画循环中使用按颜色排序的那个,当动画完成时,我只绘制具有逐个排序的最终帧(在我过滤非可见实体之后)。 我在大多数设备上达到60fps。 当我尝试使用drawImage时,我被困在大约10fps的5K点。

这是使用这种单路径方法的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 

没有直接关联,但如果您的部分图纸没有以与其余部分相同的速率更新(例如,如果您想突出显示地图的某个区域......),那么您也可以考虑分离您的图纸在不同的层,在屏幕外的画布上。 这样你就可以有一个用于平面的画布,你可以清除每一帧,以及其他用于以不同速率更新的图层的画布。 但这是另一个故事。

暂无
暂无

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

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