0

I have a network that I want to draw with Konva (and the react-konva bindings). When positions update I want to animate the nodes in the network to their new positions while also animating the start and end position of the link that connects them.

I started with the following simple example, but can't seem to get a Line to animate in the same way that the nodes do.

Is there a way to fix this, or am I approaching it in the wrong way?

import React from "react";
import { Stage, Layer, Rect, Line } from "react-konva";

class Node extends React.Component {
  componentDidUpdate() {
    this.rect.to({
      x: this.props.x,
      y: this.props.y,
    });
  }

  render() {
    const { id } = this.props;
    const color = id === "a" ? "blue" : "red";

    return (
      <Rect
        ref={node => {
          this.rect = node;
        }}
        width={5}
        height={5}
        fill={color}
      />
    );
  }
}

class Link extends React.Component {
  componentDidUpdate() {
    const x0 = 0;
    const y0 = 0;
    const x1 = 100;
    const y1 = 100;

    this.line.to({
      x: x0,
      y: y0,
      points: [x1, y1, x0, y0],
    });
  }

  render() {
    const color = "#ccc";

    return (
      <Line
        ref={node => {
          this.line = node;
        }}
        stroke={color}
      />
    );
  }
}

class Graph extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      nodes: [{ id: "a", x: 0, y: 0 }, { id: "b", x: 200, y: 200 }],
      links: [
        {
          source: "a",
          target: "b",
        },
      ],
    };
  }

  handleClick = () => {
    const nodes = this.state.nodes.map(node => {
      const position = node.x === 0 ? { x: 200, y: 200 } : { x: 0, y: 0 };

      return Object.assign({}, node, position);
    });

    this.setState({
      nodes,
    });
  };

  render() {
    const { links, nodes } = this.state;

    return (
      <React.Fragment>
        <Stage width={800} height={800}>
          <Layer>
            {nodes.map((node, index) => {
              return (
                <Node
                  key={`node-${index}`}
                  x={node.x}
                  y={node.y}
                  id={node.id}
                />
              );
            })}
          </Layer>
          <Layer>
            {links.map(link => {
              return (
                <Link
                  source={nodes.find(node => node.id === link.source)}
                  target={nodes.find(node => node.id === link.target)}
                />
              );
            })}
          </Layer>
        </Stage>
        <button onClick={this.handleClick}>Click me</button>
      </React.Fragment>
    );
  }
}

export default Graph;
Tom
  • 2,734
  • 2
  • 22
  • 39

1 Answers1

2

You may need to set initial values for points attribute for a better tween. Also, you are not using the source and target in the Link component. You should use that props for calculating animations.

import React from "react";
import { render } from "react-dom";
import { Stage, Layer, Rect, Line } from "react-konva";

class Node extends React.Component {
  componentDidMount() {
    this.rect.setAttrs({
      x: this.props.x,
      y: this.props.y
    });
  }
  componentDidUpdate() {
    this.rect.to({
      x: this.props.x,
      y: this.props.y
    });
  }

  render() {
    const { id } = this.props;
    const color = id === "a" ? "blue" : "red";

    return (
      <Rect
        ref={node => {
          this.rect = node;
        }}
        width={5}
        height={5}
        fill={color}
      />
    );
  }
}

class Link extends React.Component {
  componentDidMount() {
    // set initial value:
    const { source, target } = this.props;

    console.log(source, target);
    this.line.setAttrs({
      points: [source.x, source.y, target.x, target.y]
    });
  }
  componentDidUpdate() {
    this.animate();
  }

  animate() {
    const { source, target } = this.props;

    this.line.to({
      points: [source.x, source.y, target.x, target.y]
    });
  }

  render() {
    const color = "#ccc";

    return (
      <Line
        ref={node => {
          this.line = node;
        }}
        stroke={color}
      />
    );
  }
}

class Graph extends React.Component {
  constructor(props) {
    super(props);

    this.state = {
      nodes: [{ id: "a", x: 0, y: 0 }, { id: "b", x: 200, y: 200 }],
      links: [
        {
          source: "a",
          target: "b"
        }
      ]
    };
  }

  handleClick = () => {
    const nodes = this.state.nodes.map(node => {
      const position = node.x === 0 ? { x: 200, y: 200 } : { x: 0, y: 0 };

      return Object.assign({}, node, position);
    });

    this.setState({
      nodes
    });
  };

  render() {
    const { links, nodes } = this.state;

    return (
      <React.Fragment>
        <Stage width={800} height={300}>
          <Layer>
            {nodes.map((node, index) => {
              return (
                <Node
                  key={`node-${index}`}
                  x={node.x}
                  y={node.y}
                  id={node.id}
                />
              );
            })}
          </Layer>
          <Layer>
            {links.map(link => {
              return (
                <Link
                  source={nodes.find(node => node.id === link.source)}
                  target={nodes.find(node => node.id === link.target)}
                />
              );
            })}
          </Layer>
        </Stage>
        <button onClick={this.handleClick}>Click me</button>
      </React.Fragment>
    );
  }
}

render(<Graph />, document.getElementById("root"));

Demo: https://codesandbox.io/s/react-konva-animating-line-demo-erufn

lavrton
  • 18,973
  • 4
  • 30
  • 63
  • That's great, thanks @lavrton. As an added bonus you also fixed something else I was stuck on which was to use `setAttrs` in `componentDidMount`, I had initially tried setting the properties directly and it didn't seem to work. – Tom Aug 01 '19 at 09:27