block by renecnielsen 4e8f83b1bea1feebe146

Animate SVG path on scroll

Full Screen

Make sure to pop out to a new window, and then scroll.

An experiment to see if I can get svg paths to animate only to a certain horizontal line while a user scrolls. Used in my unreleased wongfu side project.

Built with blockbuilder.org

forked from sxywu‘s block: Animate SVG path on scroll

index.html

<html>
  <head>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.10.1/lodash.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.6/d3.js" charset="utf-8"></script>
    <style>
    * {
      font-family: Helvetica, Verdana, sans-serif;
    }

    .marker {
      position: fixed;
      top: 0;
      left: 0;
      width: 100%;
      height: 400px;
      border: 1px solid #ccc;
    }
    </style>
  </head>

  <body>
    <svg width="1000" height="20000"></svg>
    <div class="marker"></div>

    <script>
    var data = {categoryX: 300, points: [{"x":300,"y":161.16},{"x":300,"y":232.58},{"x":200,"y":359.51},{"x":300,"y":360.53},{"x":300,"y":566.63},{"x":300,"y":594.08},{"x":300,"y":650.37},{"x":300,"y":723.46},{"x":300,"y":864.01},{"x":300,"y":879.88},{"x":300,"y":1138.35},{"x":300,"y":1158.8},{"x":300,"y":1220.25},{"x":300,"y":1489.94},{"x":300,"y":1583.51},{"x":300,"y":1627.55},{"x":300,"y":1739.97},{"x":300,"y":1800.63},{"x":300,"y":1885.21},{"x":300,"y":1979.47},{"x":300,"y":2057.81},{"x":150,"y":2059.57},{"x":300,"y":2097.48},{"x":300,"y":2152.41},{"x":300,"y":2152.46},{"x":300,"y":2157.54},{"x":300,"y":2157.56},{"x":300,"y":2174.22},{"x":300,"y":2188.61},{"x":300,"y":2213.54},{"x":300,"y":2231.43},{"x":300,"y":2252},{"x":200,"y":2275.33},{"x":300,"y":2291.35},{"x":300,"y":2319.71},{"x":300,"y":2328.22},{"x":300,"y":2345.74},{"x":250,"y":2345.91},{"x":300,"y":2367.61},{"x":300,"y":2409.25},{"x":300,"y":2429.13},{"x":300,"y":2447.6},{"x":300,"y":2467.67},{"x":300,"y":2473.07},{"x":200,"y":2473.24},{"x":300,"y":2485.47},{"x":300,"y":2528.83},{"x":300,"y":2664.08},{"x":300,"y":2664.16},{"x":250,"y":2665.75},{"x":300,"y":2681.58},{"x":250,"y":2682.98},{"x":250,"y":2683.03},{"x":300,"y":2703.05},{"x":250,"y":2707.75},{"x":300,"y":2719.75},{"x":300,"y":2725.5},{"x":250,"y":2727.59},{"x":300,"y":2741.92},{"x":250,"y":2758.02},{"x":300,"y":2758.88},{"x":300,"y":2766},{"x":250,"y":2768.52},{"x":300,"y":2781.85},{"x":250,"y":2783.5},{"x":300,"y":2790.62},{"x":300,"y":2797.6},{"x":300,"y":2803.05},{"x":300,"y":2822.51},{"x":300,"y":2842.45},{"x":300,"y":2859.41},{"x":250,"y":2861.83},{"x":250,"y":2861.9},{"x":300,"y":2878.11},{"x":300,"y":2899.3},{"x":300,"y":2899.39},{"x":150,"y":2900.16},{"x":300,"y":2915.11},{"x":300,"y":2954.28},{"x":300,"y":2963.22},{"x":300,"y":2993.16},{"x":300,"y":3024.93},{"x":300,"y":3034.73},{"x":300,"y":3053.71},{"x":100,"y":3108.65},{"x":300,"y":3108.78},{"x":100,"y":3170.02},{"x":300,"y":3307.02},{"x":300,"y":3347},{"x":100,"y":3386.84},{"x":300,"y":3388.35},{"x":300,"y":3392.8},{"x":300,"y":3409.08},{"x":200,"y":3482.29},{"x":300,"y":3490.06},{"x":100,"y":3490.08},{"x":300,"y":3521.3},{"x":300,"y":3525.8},{"x":300,"y":3527.47},{"x":100,"y":3527.71},{"x":350,"y":3785.85},{"x":300,"y":3817.04},{"x":100,"y":3879.62},{"x":300,"y":3990.06},{"x":100,"y":3994.39},{"x":300,"y":4033.28},{"x":150,"y":4040},{"x":300,"y":4168.39},{"x":300,"y":4203.66},{"x":300,"y":4309.47},{"x":300,"y":4355.49},{"x":300,"y":4438.63},{"x":300,"y":4501.4},{"x":300,"y":4527.18},{"x":300,"y":4527.24},{"x":300,"y":4527.32},{"x":300,"y":4705.31},{"x":200,"y":4790.07},{"x":300,"y":4790.17},{"x":300,"y":4831.58},{"x":300,"y":4909.79},{"x":300,"y":4978.06},{"x":300,"y":5024.68},{"x":300,"y":5341.95},{"x":300,"y":5404.04},{"x":300,"y":5707.09},{"x":300,"y":5846.84},{"x":300,"y":5918.93},{"x":300,"y":5956.66},{"x":300,"y":6008.86},{"x":300,"y":6032.41},{"x":300,"y":6074.86},{"x":300,"y":6160.47},{"x":300,"y":6494.77},{"x":300,"y":6544.9},{"x":300,"y":6554.59},{"x":300,"y":6572.89},{"x":300,"y":6620.47},{"x":300,"y":6628.18},{"x":300,"y":6651.49},{"x":300,"y":6690.64},{"x":300,"y":6734.86},{"x":300,"y":6784.42},{"x":300,"y":6897.47},{"x":50,"y":6978.9},{"x":300,"y":7053.46},{"x":300,"y":7064.33},{"x":300,"y":7081.25},{"x":200,"y":7169.69},{"x":300,"y":7169.81},{"x":300,"y":7209.83},{"x":300,"y":7258.2},{"x":100,"y":7352.41},{"x":300,"y":7365.26},{"x":300,"y":7437.33},{"x":300,"y":7556.63},{"x":300,"y":7589.71},{"x":300,"y":7626.93},{"x":300,"y":7649.84},{"x":300,"y":7666.02},{"x":300,"y":7689.28},{"x":300,"y":7705.31},{"x":300,"y":7727.3},{"x":300,"y":7744.38},{"x":300,"y":7766.83},{"x":300,"y":7817.81},{"x":300,"y":7862.84},{"x":400,"y":7882.63},{"x":300,"y":8036.55},{"x":300,"y":8192.58},{"x":300,"y":8419.56},{"x":300,"y":8566.87},{"x":300,"y":8667.14},{"x":300,"y":8780.15},{"x":300,"y":8784.1},{"x":300,"y":8827.76},{"x":300,"y":8855.53},{"x":300,"y":8890.78},{"x":300,"y":8939.6},{"x":300,"y":8997.51},{"x":300,"y":9169.01},{"x":300,"y":9314.37},{"x":300,"y":9399.17},{"x":300,"y":9594.91},{"x":500,"y":9756.56},{"x":200,"y":10109.02}]};

    var svg = d3.select('svg');
    var distancePath = svg.append('path')
      .attr('fill', 'none')
      .attr('stroke', 'none').node();

    var source;
    var target;
    var gap = 100;
    var totalDistance = 0;
    _.each(data.points, function(target) {
      if (!source) {
        target.d = 'M' + target.x + ',' + target.y;
      } else {
        if (source.y > target.y - gap) {
          // if they're sufficiently close to each other
          if (source.x === target.x) {
            target.d = drawLine(target.x, target.y);
            setDistance(source, target);
          } else {
            target.d = drawCurve(source.x, source.y, target.x, target.y);

            setDistance(source, target);
            target.y1 = target.y - source.y;
            target.interpolate1 = d3.interpolate(0, target.distance);
          }
        } else {
          target.d = '';
          if (source.x !== data.categoryX) {
            // if the source repo owner isn't the same as the contributor, move the line back
            target.d += drawCurve(source.x, source.y, data.categoryX, source.y + gap / 3);

            setDistance(source, target);
            target.y1 = gap / 3;
            target.interpolate1 = d3.interpolate(0, target.distance);
          }

          x = target.x;
          y = target.y;
          if (target.x !== data.categoryX) {
            x = data.categoryX;
            y = target.y - gap / 3;
          }
          
          target.d += drawLine(x, y);
          target.y2 = y - (target.y1 || 0);
          setDistance(source, target);

          if (target.x !== data.categoryX) {
            target.d += drawCurve(x, y, target.x, target.y);
            var currentDistance = target.distance;
            setDistance(source, target);
            target.y3 = gap / 3;
            target.interpolate2 = d3.interpolate(0, target.distance - currentDistance);
          }
        }
        target.totalDistance = (totalDistance += target.distance);
        
      }
      source = target;
    });

    var path = svg.append('path')
      .attr('d', _.pluck(data.points, 'd').join(' '))
      .attr('fill-opacity', 0)
      .attr('stroke', '#3FB8AF')
      .attr('stroke-width', 4)
      .attr('stroke-linecap', 'round')
      .attr('stroke-dasharray', totalDistance)
      .attr('stroke-dashoffset', totalDistance);
    var circle = svg.selectAll('circle')
      .data(data.points).enter().append('circle')
      .attr('fill', '#fff')
      .attr('stroke', '#3FB8AF')
      .attr('stroke-width', 2)
      .attr('cx', function(d) {return d.x})
      .attr('cy', function(d) {return d.y})
      .attr('r', 4);

    window.addEventListener('scroll', _.throttle(windowScroll, 200));

    function windowScroll() {
      var top = scrollY + 400;
      var source;
      var target = _.find(data.points, function(point) {
        if (point.y >= top) {
          return true;
        }
        source = point;
        return false;
      });
      if (source && target) {
        var distance = 0;
        var distanceFromSource = top - source.y;
        if (!target.interpolate1) {
          // if there's no interpolate1
          if (!target.interpolate2) {
            // and there's no interpolate2, must mean it's a straight line
            distance = distanceFromSource + source.totalDistance;
          } else {
            // if there's a interpolate2, must mean there's a straight line
            // and then a curve at the end, so figure out if we're in straight line or curve part
            if (distanceFromSource <= target.y2) {
              // it's in straight line part
              distance = distanceFromSource + source.totalDistance;
            } else {
              // if it's in last curve part, first interpolate the curve
              // and then add that back to the straight part and the previous total distance
              var partialDistance = (distanceFromSource - target.y2) / target.y3;
              distance = target.interpolate2(partialDistance) + target.y2 + source.totalDistance;
            }
          }
        } else {
          // if there's interpolate1, must mean there's a first curve
          if (distanceFromSource <= target.y1) {
            // so if it's within the first curve, interpolate that and add it to total distance
            var partialDistance = distanceFromSource / target.y1;
            distance = target.interpolate1(partialDistance) + source.totalDistance;
          } else if (distanceFromSource <= (target.y1 + target.y2)) {
            // if we're in line part, add curve to it
            distance = target.interpolate1(1) + (distanceFromSource - target.y1) + source.totalDistance;
          } else if (interpolate2) {
            var partialDistance = (distanceFromSource - target.y2 - target.y1) / target.y3;
            distance = target.interpolate1(1) + target.y2 + target.interpolate2(partialDistance);
          }
        }
        // var partialDistance = (top - source.y) / (target.y - source.y);
        // var distance = target.interpolater(partialDistance) + source.totalDistance;
        path.transition().duration(200)
          .attr('stroke-dashoffset', totalDistance - (distance || 0));
      }
      
    };

    function drawLine(x, y) {
      return 'L' + x + ',' + y;
    }

    function drawCurve(x1, y1, x2, y2) {
      var cy = (y1 + y2) / 2;
      return 'C' + x1 + ',' + cy + ' ' + x2 + ',' + cy + ' ' + x2 + ',' + y2;
    }

    function setDistance(source, target) {
      var distancePathD = 'M' + source.x + ',' + source.y + ' ' + target.d;
      distancePath.setAttribute('d', distancePathD);
      target.distance = parseFloat(distancePath.getTotalLength().toFixed(2));
    }

    </script>
  </body>
</html>