簡體   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