Adding transition to d3.drag() behaviour

Viewed 143

I'm close to finish my first project with d3.js. It calculates the potential energy available to a skier and I want to calculate the expected speed of the skier if all this energy is converted to kinetic energy. The final thing I want is to add a transition to the skier when he is dropped. I want the skier to always transit to the black circle on the right (p2). The duration of the transition doesn't matter right now. Is there an easy way to accomplish this task?

example

const height = 500;
const width = 800;
const skierIconSvg = 'https://image.flaticon.com/icons/svg/94/94150.svg';
const gate =
  'https://docs.google.com/drawings/d/1k-mtzjW05U6KZKNt0kAI2pWqKsXFobptsa9kz6grYzA/edit?usp=sharing';

const [p1, p2, p3] = [
  [80, 130],
  [600, 170],
  [750, 190],
];

const svg = d3.select('svg');

// Store a reference to the span we're going to update
const skierHeight = d3.select('#skier-height');

const vek = document.querySelector('#vekt');
console.log(vek.value);

const line = svg.append('line').attr('stroke', 'black');

const connection = svg.append('line').attr('stroke', 'green');

const projection = svg
  .append('circle')
  .attr('r', 5)
  .attr('stroke', 'red')
  .attr('fill', 'none');

const g = svg
  .append('g')
  .attr('cursor', 'move')
  .attr('pointer-events', 'all')
  .attr('stroke', 'transparent')
  .attr('stroke-width', 30);

const point = g
  .selectAll('image')
  .data([p1, p2])
  .enter()
  .append('circle')
  .attr('r', 10)
  .call(
    d3
    .drag()
    .subject(([x, y]) => ({
      x,
      y,
    }))
    .on('drag', dragged)
  );

const skier = g
  .append('image')
  .attr('id', 'skier')
  .datum(p3)
  .attr('href', skierIconSvg)
  .attr('width', 100)
  .attr('height', 100)
  .attr('transform', 'translate(-50, -40)')
  .call(
    d3
    .drag()
    .subject(([x, y]) => ({
      x,
      y,
    }))
    .on('drag', dragged)
  );

update();

function dragged(d) {
  d[0] = d3.event.x;
  d[1] = d3.event.y;
  update();
  potensiellEnergi();
}

function update() {
  const t = (width + height) / distance(p1, p2);

  const l1 = interpolate(p1, p2, t);

  const l2 = interpolate(p2, p1, t);
  const p = interpolate(p1, p2, project(p1, p2, p3));

  connection.attr('x1', p3[0]).attr('y1', p3[1]);
  connection.attr('x2', p[0]).attr('y2', p[1]);
  projection.attr('cx', p[0]).attr('cy', p[1]);
  line.attr('x1', l1[0]).attr('y1', l1[1]);
  line.attr('x2', l2[0]).attr('y2', l2[1]);
  point.attr('cx', (d) => d[0]).attr('cy', (d) => d[1]);
  skier.attr('x', (d) => d[0]).attr('y', (d) => d[1]);

  skierHeight.text(`${getHeight(p, p1, p2).toFixed(2)} meter`);
}

function distance([x1, y1], [x2, y2]) {
  return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

function interpolate([x1, y1], [x2, y2], t) {
  return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];
}

function project([x1, y1], [x2, y2], [x3, y3]) {
  const x21 = x2 - x1,
    y21 = y2 - y1;
  const x31 = x3 - x1,
    y31 = y3 - y1;
  return (x31 * x21 + y31 * y21) / (x21 * x21 + y21 * y21);
}

function getHeight([xp, yp], [x1, y1], [x2, y2]) {
  // Note that y is counted from top to bottom, so higher y means
  // a point is actually lower.

  // First, the total height is 100 metres.
  const pxPerMeter = (y2 - y1) / 100;

  // Calculate the height diff in pixels
  const heightDiffPx = y2 - yp;

  // Now transform it to meters
  return heightDiffPx / pxPerMeter;
}

function vektVelger() {
  const vektVerdi = document.querySelector('#vekt');
  const vektDisplay = document.querySelector('#skier-vekt');

  vektDisplay.innerHTML = vektVerdi.value;
}

function potensiellEnergi() {
  const vektVerdi = parseInt(document.querySelector('#skier-vekt').textContent);
  const skierHeight = parseInt(
    document.querySelector('#skier-height').textContent
  );
  const potEDisplay = document.querySelector('#skier-potentialenergi');
  const potEnergi = `${vektVerdi * skierHeight * 9.8}`;
  return (potEDisplay.innerHTML = `${potEnergi}`);
}
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8" />
  <script src="https://d3js.org/d3.v5.js"></script>
  <script src="https://d3js.org/d3-path.v1.min.js"></script>
  <script src="https://d3js.org/d3-shape.v1.min.js"></script>
  <script src="https://d3js.org/d3-scale.v3.min.js"></script>
  <script src="https://d3js.org/d3-axis.v1.min.js"></script>
  <script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
  <script src="https://d3js.org/d3-selection.v1.min.js"></script>

  <link href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300&display=swap" rel="stylesheet" />
  <link href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@700&display=swap" rel="stylesheet" />
</head>

<body>
  <h1>Forsøk på å lage en tutorial i JavaScript og D3.js</h1>
  <h2>Høydeforskjell: <span id="skier-height"></span></h2>
  <h2>Vekt: <span id="skier-vekt">70</span> kg</h2>
  <h2>
    Gravitasjonskraft (fg): <span id="skier-gravitasjon">9.8</span> m/sek/sek
  </h2>
  <h2>Potential Energy: <span id="skier-potentialenergi">0</span> KJOULE</h2>

  <svg width="800" height="300"></svg>
  <form>
    Velg utøverens vekt:
    <input type="number" id="vekt" placeholder="Velg utøverens vekt" />
  </form>
  <input type="button" value="velg" onclick="vektVelger()" />

  <script src="skier.js"></script>
</body>

</html>

1 Answers

I added the transition below. The skier first drops to the ground, and when that transition is done, they slide to the end. You could even use one of these transition easing methods to make them gain speed along the way!

I ran into some problems with your packages. I don't see why you had all these lines in your HTML, but they broke d3.transition:

  <script src="https://d3js.org/d3-path.v1.min.js"></script>
  <script src="https://d3js.org/d3-shape.v1.min.js"></script>
  <script src="https://d3js.org/d3-scale.v3.min.js"></script>
  <script src="https://d3js.org/d3-axis.v1.min.js"></script>
  <script src="https://d3js.org/d3-dispatch.v1.min.js"></script>
  <script src="https://d3js.org/d3-selection.v1.min.js"></script>

Removing all of them and leaving only <script src="https://d3js.org/d3.v5.js"></script> was completely fine.

const height = 500;
const width = 800;
const skierIconSvg = 'https://image.flaticon.com/icons/svg/94/94150.svg';
const gate =
    'https://docs.google.com/drawings/d/1k-mtzjW05U6KZKNt0kAI2pWqKsXFobptsa9kz6grYzA/edit?usp=sharing';

const [p1, p2, p3] = [
    [80, 130],
    [600, 170],
    [750, 190],
];

const svg = d3.select('svg');

// Store a reference to the span we're going to update
const skierHeight = d3.select('#skier-height');

const vek = document.querySelector('#vekt');
console.log(vek.value);

const line = svg.append('line').attr('stroke', 'black');

const connection = svg.append('line').attr('stroke', 'green');

const projection = svg
    .append('circle')
    .attr('r', 5)
    .attr('stroke', 'red')
    .attr('fill', 'none');

const g = svg
    .append('g')
    .attr('cursor', 'move')
    .attr('pointer-events', 'all')
    .attr('stroke', 'transparent')
    .attr('stroke-width', 30);

const point = g
    .selectAll('image')
    .data([p1, p2])
    .enter()
    .append('circle')
    .attr('r', 10)
    .call(
        d3
            .drag()
            .subject(([x, y]) => ({
                x,
                y,
            }))
            .on('start', () => {
              // Interrupt all transitions
              skier.interrupt();
              connection.interrupt();

              // Update the value of p3 to wherever the skier happened to be
              // This will stop him dead in his tracks instead of snapping him back to the old position
              p3[0] = Number(skier.attr('x'));
              p3[1] = Number(skier.attr('y'));
            })
            .on('drag', dragged)
    );

const skier = g
    .append('image')
    .attr('id', 'skier')
    .datum(p3)
    .attr('href', skierIconSvg)
    .attr('width', 100)
    .attr('height', 100)
    .attr('transform', 'translate(-50, -40)')
    .call(
        d3
            .drag()
            .subject(() => ({
                // Use where the skier is, not where he's supposed to be
                x: Number(skier.attr('x')),
                y: Number(skier.attr('y')),
            }))
            .on('start', () => {
              // Interrupt all transitions
              skier.interrupt();
              connection.interrupt();
            })
            .on('drag', dragged)
            .on('end', dropSkier)
    );

update();

function dragged(d) {
    d[0] = d3.event.x;
    d[1] = d3.event.y;
    update();
    potensiellEnergi();
}

function update() {
    const t = (width + height) / distance(p1, p2);

    const l1 = interpolate(p1, p2, t);

    const l2 = interpolate(p2, p1, t);
    const p = interpolate(p1, p2, project(p1, p2, p3));

    connection.attr('x1', p3[0]).attr('y1', p3[1]);
    connection.attr('x2', p[0]).attr('y2', p[1]);
    projection.attr('cx', p[0]).attr('cy', p[1]);
    line.attr('x1', l1[0]).attr('y1', l1[1]);
    line.attr('x2', l2[0]).attr('y2', l2[1]);
    point.attr('cx', (d) => d[0]).attr('cy', (d) => d[1]);
    skier.attr('x', (d) => d[0]).attr('y', (d) => d[1]);

    skierHeight.text(`${getHeight(p, p1, p2).toFixed(2)} meter`);
}
function distance([x1, y1], [x2, y2]) {
    return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}

function interpolate([x1, y1], [x2, y2], t) {
    return [x1 + (x2 - x1) * t, y1 + (y2 - y1) * t];
}

function project([x1, y1], [x2, y2], [x3, y3]) {
    const x21 = x2 - x1,
        y21 = y2 - y1;
    const x31 = x3 - x1,
        y31 = y3 - y1;
    return (x31 * x21 + y31 * y21) / (x21 * x21 + y21 * y21);
}

function getHeight([xp, yp], [x1, y1], [x2, y2]) {
    // Note that y is counted from top to bottom, so higher y means
    // a point is actually lower.

    // First, the total height is 100 metres.
    const pxPerMeter = (y2 - y1) / 100;

    // Calculate the height diff in pixels
    const heightDiffPx = y2 - yp;

    // Now transform it to meters
    return heightDiffPx / pxPerMeter;
}

function vektVelger() {
    const vektVerdi = document.querySelector('#vekt');
    const vektDisplay = document.querySelector('#skier-vekt');

    vektDisplay.innerHTML = vektVerdi.value;
}

function potensiellEnergi() {
    const vektVerdi = parseInt(document.querySelector('#skier-vekt').textContent);
    const skierHeight = parseInt(
        document.querySelector('#skier-height').textContent
    );
    const potEDisplay = document.querySelector('#skier-potentialenergi');
    const potEnergi = `${vektVerdi * skierHeight * 9.8}`;
    return (potEDisplay.innerHTML = `${potEnergi}`);
}

function dropSkier(d) {
  const projection = interpolate(p1, p2, project(p1, p2, p3));
  skier
    .transition()
    .duration(500)
    // First down to the ground
    .attr('x', projection[0])
    .attr('y', projection[1])
    .on("end", () => {
      skier
        .transition()
        .duration(2500)
        // First down to the ground
        .attr('x', p2[0])
        .attr('y', p2[1]);
    });
    
    // Remove the line together with the skier
    connection
      .transition()
      .duration(500)
      // First down to the ground
      .attr('x1', projection[0])
      .attr('y1', projection[1]);
}
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8" />
        <script src="https://d3js.org/d3.v5.js"></script>

        <link
            href="https://fonts.googleapis.com/css2?family=Inconsolata:wght@300&display=swap"
            rel="stylesheet"
        />
        <link
            href="https://fonts.googleapis.com/css2?family=Amatic+SC:wght@700&display=swap"
            rel="stylesheet"
        />
    </head>

    <body>
        <h1>Forsøk på å lage en tutorial i JavaScript og D3.js</h1>
        <h2>Høydeforskjell: <span id="skier-height"></span></h2>
        <h2>Vekt: <span id="skier-vekt">70</span> kg</h2>
        <h2>
            Gravitasjonskraft (fg): <span id="skier-gravitasjon">9.8</span> m/sek/sek
        </h2>
        <h2>Potential Energy: <span id="skier-potentialenergi">0</span> KJOULE</h2>

        <svg width="800" height="300"></svg>
        <form>
            Velg utøverens vekt:
            <input type="number" id="vekt" placeholder="Velg utøverens vekt" />
        </form>
        <input type="button" value="velg" onclick="vektVelger()" />

        <script src="skier.js"></script>
    </body>
</html>

Related