Open11

Konva 学習の軌跡

syonsyon

#4 Draggable rects with cursor style

ソースコード・デモ: Konva study #4

ドラッグできる四角の配置。マウスポインターを重ねるとカーソルスタイルが変わる。
マウスカーソルの変更にはステージの参照が必要だが、ドラッグイベントから取得できる。

  group.on('mouseenter', (e) => {
    e.target.getStage().container().style.cursor = 'move'
  })

また、今回自作している CyanSquare について、座標の指定をグループオブジェクトに対して行っている。これに伴い、中にある Rect と Text それぞれの X, Y 座標は 0 を基準としている。グループをドラッグ可能にし、以下のアプローチで新しい座標を Text に反映している。

  group.on('dragmove', (arg) => {
    text.setAttr('text', `${arg.target.x()}, ${arg.target.y()}`)
  })
syonsyon

#5 Bezier connector

ソースコード・デモ: Konva study #5

2つのオブジェクトを繋げるベジェ曲線のコネクターをつくる。ベジェ曲線は Konva.Path で生成し、data プロパティにデータを指定する。具体例は以下のとおり。

  const bezier = new Konva.Path({
    x: 0,
    y: 0,
    data: 'M200,200 C350,150 400,500 600,340',
    stroke: 'orange',
  })

この書式は SVG と同じであり、以前私が作成したデモにて、ハンドルを動かしながらそのデータが変わる様子を確認できる。

コネクターの作成には、その引数に接続する 2 つのオブジェクトの参照を渡している。渡されたオブジェクトの dragmove イベントを通して bezier.setAttrs({ data: '...' }) で更新している。なお、オブジェクトのメソッド dragBoundFunc((pos) => {...}) を通したアプローチもあったが、こちらはステージのドラッグを伴った際に期待した座標を得られなかったため、使用していない。

syonsyon

#6 Connect by object id

stackblitz - Konva study #6

前回の #5 ではコネクター生成時の引数にオブジェクトの参照を渡していたが、将来的にはこれをID文字列で紐づけてそれぞれ別のタイミングで生成したい目論見がある。Konvaではオブジェクトに対しIDを付与することができる。また、それを見つけるためには親要素から find または findOne メソッドを使って jQuery のようにクエリ文字列を使って探すことができる。

IDを付与する
const group = new Konva.Group()
group.id('grp01')
IDで探す
const obj = stage.findOne('#grp01')
console.log(obj.id()) // grp01
syonsyon

#7 Generate and connect

ソースコード・デモ: Konva study #7

生成したオブジェクトを、自動的にコネクターで接続する。今回の例では緑色の丸をクリックすると新しくコネクターでつないだ丸を生成する。

特別なことはしておらず、レイヤーオブジェクトへの参照をうまく取り回してクリックイベントが発生した時に layer.add() しているだけ。ポイントは生成した●とコネクターを同時に追加している点。繋がっているように見せているが、座標が同じなだけでデータとしては繋がっていない。

  const spawner = createSpawner(core, {
    onSpawn: (eggObj) => {
      layer.add(createEgg(eggObj))
      layer.add(createConnector(layer, eggObj.id, core.id))
    },
  })
  layer.add(spawner)
syonsyon

#8 react-konva on Next.js

ソースコード・デモ: Konva study #8

データドリブンな描画を行いたいので React ベースで Konva を扱える react-konva を使う。React を使うのなら Next.js に載せたいので、まずは基礎から確認。

これまではステージやレイヤーのオブジェクトを用意したのち .add() で入れ子構造を記述していたが、タグで視覚的に表現できるようになってわかりやすくなった。

import { Stage, Layer, Text } from 'react-konva';

const StageComponent = () => {
  return (
    <Stage width={window.innerWidth} height={window.innerHeight}>
      <Layer>
        <Text x={20} y={15} text="Hello!" />
      </Layer>
    </Stage>
  );
};

export default StageComponent;
syonsyon

#10 Storybook

ソースコード・デモ: Konva study #10

基礎の検証と把握ができてきたので、いよいよ完成イメージに近づけていく。
パーツの作り込みに集中するため Storybook を導入する。Konva だからといって特に難しいことはなく動作した。もちろん描画には canvas タグが前提となるので、ステージとレイヤーを敷いておく必要がある。以下のようにして デコレーター を定義するとよい。

NodeBox.stories.jsx
export default {
  title: "Example/NodeBox",
  component: NodeBoxStory,
  decorators: [
    (Story) => (
      <Stage width={window.innerWidth} height={window.innerHeight} draggable>
        <Layer>
          <Story />
        </Layer>
      </Stage>
    ),
  ],
};

syonsyon

#11 Synchronized objects

ソースコード・デモ: Konva study #11

次に箱と線を繋いでグリグリ動かした上で線が追従するものを作る。こちらも Recoil を使いたかったがうまくできなかったので、まずは useState で。今のうちから非同期でデータを取得してくる想定で擬似的にこのように記述。注意点としては以下。

  • useEffect の中を async 関数にすると ESLint エラー eslintreact-hooks/exhaustive-deps が発生。内部で async 関数を定義したのち呼び出すか、then を使う。今回は後者にした。
  • useEffect で componentDidMount を実現するには、その第 2 引数に useState から取り出した更新関数を割り当てることで実現できる。(参考記事
  const [root, setRoot] = React.useState({});

  React.useEffect(() => {
    $blueprint.helper.getRemoteData().then((data) => {
      setRoot(data);
    });
  }, [setRoot]);