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

时间:2017-05-29 19:48:16

标签: javascript html5 d3.js canvas

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

现状:

canvas

理想状态(通过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选择(“平面”) - 也在内存中。

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被触发的每个圆圈都有一个时间延迟(大约一个毫秒+)不断。
  • 我试图通过仅以特定间隔(clearRect或类似物)调用Date.now() % 10 === 0来驯服画布的永久清除,但这也导致无益。
  • 另一个想法是计算前一个圆的位置,并在每个clearRect函数中使用一个小而具体的draw()定义删除该区域。

任何指针都非常赞赏。

2 个答案:

答案 0 :(得分:4)

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

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

正如评论中所指出的,如果一种颜色和一条路径稍微快一些机器(不是所有机器),一些机器可能能够绘制圆形。渲染图像的关键是它对图像复杂性不变。图像渲染取决于图像大小,但每个像素的颜色和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(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

答案 1 :(得分:3)

@ Blindman67是正确的,清除并重绘所有内容,每一帧

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

我们的想法是使用

将所有形状包装在单个路径声明中
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() 更快,但主要的警告是它仅适用于单色的形状组。

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

以下是使用此单路径方法的Blindman67优秀答案摘录的修改版本。

&#13;
&#13;
drawImage
&#13;
/* 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)
&#13;
canvas {
   position : absolute;
   top : 0px;
   left : 0px;
   z-index: -1;
}
&#13;
&#13;
&#13;

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

相关问题