Как анимировать D3 SVG линию Безье для эффекта тонкой волны

#javascript #d3.js

#javascript #d3.js

Вопрос:

Я могу анимировать изогнутую линию примерно так:

 const svg = d3.select("#line-svg");
const lineWidth = 6;

// Scale.
const scaleX = d3.scaleLinear()
  .domain([0, 300])
  .range([0, parseFloat(svg.style("width"))]);
const scaleY = d3.scaleLinear()
  .domain([0, 120])
  .range([0, parseFloat(svg.style("height")) - lineWidth]);

// Curved line interpolator.
const bezierLine = d3.line()
  .curve(d3.curveBasis)
  .x((d) => scaleX(d[0]))
  .y((d) => scaleY(d[1]));

// Draw line amp; animate.
svg
  .append("path")
  .attr(
    "d",
    bezierLine([
      [0, 40],
      [25, 70],
      [50, 100],
      [100, 50],
      [150, 20],
      [200, 130],
      [300, 120]
    ])
  )
  .attr("stroke", "url(#b1xGradient)")
  .attr("stroke-width", lineWidth)
  .attr("fill", "none")
  .transition()
  .duration(900)
  .attrTween("stroke-dasharray", function () {
    const len = this.getTotalLength();
    return (t) => d3.interpolateString("0,"   len, len   ",0")(t);
  });  
 body {
  background: black;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="line-svg" width="100%" height="150">
  <defs>
    <linearGradient id="b1xGradient">
      <stop offset="0%" style="stop-color: #18e589;" />
      <stop offset="100%" style="stop-color: #2870f0;" />
    </linearGradient>
  </defs>
</svg>  

Но как я могу взять эти точки линии или сгенерировать несколько точек линии, а затем постоянно слегка сдвигать их, чтобы создать анимированный эффект волны?

PS Линия также не обязательно должна выглядеть точно так же — ее можно сгенерировать математически, чтобы она была какой-то волнистой линией, похожей на пример (на самом деле это было бы аккуратно).

Что-то похожее на это, но более тонкий диапазон движения и медленнее — https://codesandbox.io/s/threejs-meshline-custom-spring-3-forked-og1f7?file=/src/index.js

Ответ №1:

Я не знаю точно, что вы подразумеваете под волной, но вы можете добавить немного случайного смещения к строке и перерисовывать ее бесконечно много раз. Вы можете объединить это с анимацией роста, указав именованные переходы — они могут существовать бок о бок.

Я назначил точки данных, используя .datum() , чтобы я мог получить к ним доступ внутри перехода.

 const svg = d3.select("#line-svg");
const lineWidth = 6;

// Scale.
const scaleX = d3.scaleLinear()
  .domain([0, 300])
  .range([0, parseFloat(svg.style("width"))]);
const scaleY = d3.scaleLinear()
  .domain([0, 120])
  .range([0, parseFloat(svg.style("height")) - lineWidth]);

// Curved line interpolator.
const bezierLine = d3.line()
  .curve(d3.curveBasis)
  .x((d) => scaleX(d[0]))
  .y((d) => scaleY(d[1]));

// Draw line amp; animate.
const line = svg
  .append("path")
  .datum([
    [0, 40],
    [25, 70],
    [50, 100],
    [100, 50],
    [150, 20],
    [200, 130],
    [300, 120]
  ])
  .attr("stroke", "url(#b1xGradient)")
  .attr("stroke-width", lineWidth)
  .attr("fill", "none")
  .attr("d", function(d) { return bezierLine(d); });

line
  .transition("grow")
  .duration(900)
  .attrTween("stroke-dasharray", function () {
    const len = this.getTotalLength();
    return (t) => d3.interpolateString("0,"   len, len   ",0")(t);
  })

function wave() {
  line
    .transition("wave")
    .duration(900)
    .ease(d3.easeLinear)
    .attr("d", function(d) { 
      // Add a little offset to each coordinate
      const offsetCoords = d.map(function(e) {
        return [
          e[0] - 3   Math.random() * 6,
          e[1] - 2   Math.random() * 2
        ];
      });
      return bezierLine(offsetCoords);
    })
    // Repeat
    .on("end", wave);
}

wave();  
 body {
  background: black;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="line-svg" width="100%" height="150">
  <defs>
    <linearGradient id="b1xGradient">
      <stop offset="0%" style="stop-color: #18e589;" />
      <stop offset="100%" style="stop-color: #2870f0;" />
    </linearGradient>
  </defs>
</svg>  


Редактировать чтобы линия действительно вела себя как волна, а не пульсировала, вы можете сделать что-то вроде этого. Он генерирует волны, используя синусоидальную функцию, со случайными амплитудами, периодами и случайным смещением от среднего, поэтому он выглядит более хаотичным. Вы можете настроить переменные самостоятельно, потому что во встроенном просмотрщике это выглядит совсем иначе, чем при редактировании, что, я думаю, как-то связано с соотношением ширины и высоты.

В отличие от вашего примера, в нем нет гашения волн, они только появляются и исчезают из поля зрения. Однако теперь, когда о генерации волны позаботились, это должно быть легко реализовать.

 const svg = d3.select("#line-svg");
const lineWidth = 6;

// Scale.
const scaleX = d3.scaleLinear()
  .domain([0, 300])
  .range([0, parseFloat(svg.style("width"))]);
const scaleY = d3.scaleLinear()
  .domain([0, 120])
  .range([0, parseFloat(svg.style("height")) - lineWidth]);

// Curved line interpolator.
const bezierLine = d3.line()
  .curve(d3.curveBasis)
  .x((d) => scaleX(d[0]))
  .y((d) => scaleY(d[1]));

// Create a sine wave. Each wave is completes a full number of periods
// before being replaced by another one
// if varyMean is true, add a little bit of noise to the mean of the function
function generateSine(y, step, mean, varyMean) {
  const sine = {
    amplitude: Math.random() * 5   20, // [5, 25]
    period: Math.random() * 0.25   0.05, // [0.05, 0.3]
    repeats: 1   Math.round(Math.random() * 3), // [1, 4]
    meanOffset: varyMean ? Math.random() * 50 - 25 : 0 // [-25, 25]
  };

  // Calculate a gradual decrease or increase the mean
  function offset(i) {
    return Math.min(i, 2 * Math.PI) * sine.meanOffset;
  }

  const offsetX = y.length * step;
  let runningX = 0;
  while (runningX < 2 * Math.PI * sine.repeats) {
    const m = mean   offset(runningX);
    y.push(m   sine.amplitude * Math.sin(runningX   offsetX));
    runningX  = 2 * Math.PI * step / sine.period;
  }
}

// Draw line amp; animate.
const line = svg
  .append("path")
  .datum(function() {
    const domain = scaleX.domain();
    const nPoints = 50;
    const points = d3.range(nPoints).map(function(v) {
      return v / (nPoints - 1);
    });
    const step = points[1] - points[0];

    const x = points.map(function(v) {
      return domain[0]   v * (domain[1] - domain[0]);
    });
    const xStep = x[1] - x[0];
    
    // Draw two points just before and just after the visible part of the wave
    // to make the lines run smoothly
    x.unshift(x[0] - xStep); x.push(x[x.length - 1]   xStep);

    const y = [];
    const mean = d3.sum(scaleY.domain()) / 2;
    while(y.length < x.length) {
      generateSine(y, step, mean, true);
    }

    return {
      x: x,
      y: y,
      mean: mean,
      step: step
    };
  })
  .attr("stroke", "url(#b1xGradient)")
  .attr("stroke-width", lineWidth)
  .attr("fill", "none");

line
  .transition("grow")
  .duration(900)
  .attrTween("stroke-dasharray", function() {
    const len = this.getTotalLength() * 2;
    return (t) => d3.interpolateString("0,"   len, len   ",0")(t);
  })

function wave() {
  line
    .attr("d", function(d) {
      return bezierLine(d.x.map(function(v, i) {
        // We store some additional variables at the end of y,
        // we don't want to show yet
        return [v, d.y[d.x.length - 1 - i]];
      }));
    })
    .datum(function(d) {
      const y = d.y;
      
      // Remove the y value that was just moved out of view
      y.shift();      
      // See if we still have enough y values left, otherwise, generate some
      while(y.length < d.x.length) {
        generateSine(y, d.step, d.mean);
      }

      return d;
    })
    .attr("transform", function(d) {
      const step = d.x[1] - d.x[0];
      return `translate(${-scaleX(step)})`
    })
    .transition("wave")
    .duration(function(d) { return 5000 / d.x.length; })
    .ease(d3.easeLinear)
    .attr("transform", "translate(0)")
    .on("end", function() {
      // Repeat
      wave();
    });
}

wave();  
 body {
  background: black;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.js"></script>

<svg id="line-svg" width="100%" height="150">
  <defs>
    <linearGradient id="b1xGradient">
      <stop offset="0%" style="stop-color: #18e589;" />
      <stop offset="100%" style="stop-color: #2870f0;" />
    </linearGradient>
  </defs>
</svg>  

Комментарии:

1. 1 Спасибо за исходный код, и это вроде как работает, но думаю, мне придется попробовать какое-то решение, которое перемещает точки данных, используя некоторую математику, чтобы создать скорее волнообразное движение, чем случайное

2. Если вы обновите свой вопрос, чтобы быть более конкретным, я был бы рад обновить свой ответ

3. Я не совсем уверен, как это сформулировать, но я больше жду, чтобы волна двигалась, как в океане, поэтому волна будет перемещаться по линии и, вероятно, уменьшаться в размерах, как это происходит. Думаю, это довольно сложный вопрос, поскольку он будет включать некоторую математическую логику типа синусоидальной волны / генератора

4. например codesandbox.io/s /… (но более тонко и медленно)

5. Значит, текущие точки линии не имеют значения?

Ответ №2:

Вы можете просто повторно инициализировать точки пути в цикле и переход между ними.

Я только что добавил изменение координат Y. Вы могли бы поиграть с циклическим циклом координат X, скажем, для сдвига на 10%, пока он не повторится 10 раз (и вы сбросите глобальную переменную счетчика обратно на 0 и т. Д.). Это сделало бы желаемую волну (например, кажется, что она перемещается вправо).

 const svg = d3.select("#line-svg");
const lineWidth = 6;

// Scale.
const scaleX = d3.scaleLinear()
  .domain([0, 300])
  .range([0, parseFloat(svg.style("width"))]);
const scaleY = d3.scaleLinear()
  .domain([0, 120])
  .range([0, parseFloat(svg.style("height")) - lineWidth]);

// Curved line interpolator.
const bezierLine = d3.line()
  .curve(d3.curveBasis)
  .x((d) => scaleX(d[0]))
  .y((d) => scaleY(d[1]));

const randBezierLine = d3.line()
  .curve(d3.curveBasis)
  .x((d) => scaleX(d[0]))
  .y((d) => scaleY(d[1]*(1-(Math.random() 0.3)/5)));

const points = [
  [0, 40],
  [25, 70],
  [50, 100],
  [100, 50],
  [150, 20],
  [200, 130],
  [300, 120]
];

var lenTotal = 1200;// just more than gathered length
// Draw line amp; animate.
svg
  .append("path")
  .attr("id", "animLine")
  .attr(
    "d",
    bezierLine([
      [0, 40],
      [25, 70],
      [50, 100],
      [100, 50],
      [150, 20],
      [200, 130],
      [300, 120]
    ])
  )
  .attr("stroke", "url(#b1xGradient)")
  .attr("stroke-width", lineWidth)
  .attr("fill", "none")
  .transition()
  .duration(900)
  .attrTween("stroke-dasharray", function () {
    const len = this.getTotalLength();
    return (t) => d3.interpolateString("0,"   len, len   ",0")(t);
  });

function Transition() {
  d3.select("#animLine")
    .transition()
    .duration(500)
    .ease(d3.easeLinear)
    .attr("d", randBezierLine(points))
    .attr("stroke-dasharray", lenTotal   ",0")
    .on("end", function() { Transition(); });
}

setTimeout(Transition, 2000);  
 body {
  background: black;
}  
 <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

<svg id="line-svg" width="100%" height="150">
  <defs>
    <linearGradient id="b1xGradient">
      <stop offset="0%" style="stop-color: #18e589;" />
      <stop offset="100%" style="stop-color: #2870f0;" />
    </linearGradient>
  </defs>
</svg>  

Комментарии:

1. Хорошая идея, думаю, я буду использовать и адаптировать это 👍