使用D3.js更好地重新绘制或“移动”对象?

时间:2016-12-15 02:37:19

标签: javascript canvas d3.js

我一直在尝试动画。

通过清除整个画布并在每个帧(或“tick”)中将其重新绘制到新位置来对画布中的对象进行动画制作非常简单:

// Inside requestAnimationFrame(...) callback

// Clear canvas
canvas.selectAll('*').remove();

// ... calculate position of x and y
// x, y = ...

// Add object in new position
canvas.append('circle')
    .attr('cx', x)
    .attr('cy', y)
    .attr('r', 10)
    .attr('fill', '#ffffff');

这是一种不好的做法还是我做得对?

例如,如果您制作的屏幕上到处都是物体移动,那么通过更新每个帧中的属性(例如x,y坐标)来设置动画是否更好呢?

或者,也许还有一些我完全没有意识到的其他方法,不是吗?

注意:我的动画一次可能包含100-200个对象。

2 个答案:

答案 0 :(得分:5)

最好移动它们,因为这是您可以无错误地制作动画的唯一方法。

在d3.js中,想法是对象是数据绑定的。清除并重新绘制'画布'不是正确的方法。首先它不是画布,它是一个网页,任何清除和重绘都由浏览器本身处理。你的工作就是将数据绑定到SVG,基本上就是这样。

您需要使用d3事件enterexitupdate来处理修改数据绑定基础数据时SVG的行为,并让d3处理动画。

最简单的例子是:https://bost.ocks.org/mike/circles/

  1. 选择您的元素,并将选择存储在变量
  2. var svg= d3.select("svg");

    var circles = svg.selectAll('circle');

    1. 现在我们需要将某些内容数据化为圆圈。
    2. var databoundCircles = circles.data([12,13,14,15,66]);

      这些数据可以是任何东西。通常我会期待一个对象列表,但这些都是简单的数字。

      1. 处理事物的制作方式'何时出现数据
      2. databoundCircles.enter().append('circle');;

        1. 处理删除数据时发生的事情
        2. databoundCircles.exit().remove()

          1. 处理数据更新时会发生什么
          2. databoundCircles.attr('r', function(d, i) { return d * 2; })

            这将在数据更改时更改半径。

            回顾那个教程:

            1. 输入 - 传入元素,进入舞台。

            2. 更新 - 持久性元素,留在舞台上。

            3. 退出 - 退出元素,退出舞台。

            4. 总而言之:不要像你一样。确保您专门使用这些事件来处理元素的生命周期。

              PRO TIP :如果您正在使用对象列表,请确保按id或某些唯一标识符绑定数据,否则动画可能会随着时间的推移异常。请记住,将数据绑定到SVG ,您不仅要擦除并重新绘制画布!

              d3.selectAll('circle').data([{id:1},{id:2}], function(d) { return d.id; });

              记下可选的第二个参数,告诉我们如何绑定数据!非常重要!

              
              
              var svg = d3.select("svg");
              
              //the data looks like this.
              var data = [{
                  id: 1,
                  r: 3,
                  x: 35,
                  y: 30
              }, {
                  id: 2,
                  r: 5,
                  x: 30,
                  y: 35
              }];
              
              
              //data generator makes the list above
              function newList() {
                  //just make a simple array full of the number 1
                  var items = new Array(randoNum(1, 10)).fill(1)
                  //make the pieces of data. ID is important!
                  return items.map(function(val, i) {
                    
                      var r = randoNum(1, 16)
                    
                      return {
                          id: i,
                          r: r,
                          x: randoNum(1, 200) + r,
                          y: randoNum(1, 100) + r
                      }
                  });
              }
              
              //im just making rando numbers with this.
              function randoNum(from, to) {
                  return Math.floor(Math.random() * (to - from) + from);
              }
              
              function update(data) {
                
                  //1. get circles (there are none in the first pass!)
                  var circles = svg.selectAll('circle');
                  
                  //2. bind data
                  var databoundCircles = circles.data(data, function(d) {
                      return d.id;
                  });
              
                  //3. enter
                  var enter = databoundCircles.enter()
                    .append('circle')
                    .attr('r', 0)
              
                  //4. exit
                  databoundCircles.exit()
                    .transition()
                    .attr('r', 0)
                    .remove();
              
                  //5. update
                  //(everything after transition is tweened)
                  databoundCircles
                      .attr('fill', function(d, i){
                        var h =  parseInt(i.toString(16));   
                        return '#' + [h,h,h].join('');
                      })
              
                      .transition()
                      .duration(1000)
                      .attr('r', function(d, i) {
                          return d.r * 4
                      })
                      .attr('cx', function(d, i) {
                          return d.x * 2;
                      })
                      .attr('cy', function(d, i){
                          return d.y * 2
                      })
               ;
              }
              
              //first time I run, I use my example data above
              update(data);
              
              //now i update every few seconds
              //watch how d3 'keeps track' of each circle
              setInterval(function() {
                  update(newList());
              }, 2000);
              
              <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>
              <svg width="500" height="300">
              
              </svg>
              &#13;
              &#13;
              &#13;

答案 1 :(得分:3)

  

这是一种不好的做法还是我做得对?

是的,这是一种不好的做法。在正常情况下,我喜欢称之为延迟编码:清除SVG(或其他)并再次绘制dataviz。

但是,在你的情况下,更糟糕的是:你最终会编写巨大的数量的代码(不完全是 laziness ),忽略{{1} },可以很容易地做你想要的。这就把我们带到了你的第二个问题:

  

或者,也许还有一些我完全没有意识到的其他方法,不是吗?

是的,正如我刚才所说,它被称为d3.transition()https://github.com/d3/d3-transition

然后,最后,你说:

  

注意:我的动画一次可能包含100-200个对象。

首先,现代浏览器可以很好地处理它。其次,您仍然需要手动删除并重新绘制所有元素。如果您对这两种方法进行基准测试,可能情况会更糟。

因此,只需使用transition()

您可以随时更改元素的数据(或属性),并将它们“移动”(或转换)为调用转换的新值。例如,要移动这个圆圈,我不必将其移除并再次绘制它:

d3.transition()
var circle = d3.select("circle")
setInterval(() => {
    circle.transition()
        .duration(900)
        .attr("cx", Math.random() * 300)
        .attr("cy", Math.random() * 150)
        .ease(d3.easeElastic);
}, 1000)