Konva 学習の軌跡
#3 Draggable stage
キャンバスをドラッグでスクロールする
const stage = new Konva.Stage({
container: 'app',
width: window.innerWidth,
height: window.innerHeight,
draggable: true,
})
#4 Draggable rects with cursor style
ドラッグできる四角の配置。マウスポインターを重ねるとカーソルスタイルが変わる。
マウスカーソルの変更にはステージの参照が必要だが、ドラッグイベントから取得できる。
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()}`)
})
#5 Bezier connector
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 と同じであり、以前私が作成したデモにて、ハンドルを動かしながらそのデータが変わる様子を確認できる。
- svg-study - Step1: ハンドルをドラッグしてベジェ曲線のデータ変化を見る
- SVGとVueでノードエディタGUIをつくる
コネクターの作成には、その引数に接続する 2 つのオブジェクトの参照を渡している。渡されたオブジェクトの dragmove
イベントを通して bezier.setAttrs({ data: '...' })
で更新している。なお、オブジェクトのメソッド dragBoundFunc((pos) => {...})
を通したアプローチもあったが、こちらはステージのドラッグを伴った際に期待した座標を得られなかったため、使用していない。
#6 Connect by object id
前回の #5 ではコネクター生成時の引数にオブジェクトの参照を渡していたが、将来的にはこれをID文字列で紐づけてそれぞれ別のタイミングで生成したい目論見がある。Konvaではオブジェクトに対しIDを付与することができる。また、それを見つけるためには親要素から find
または findOne
メソッドを使って jQuery のようにクエリ文字列を使って探すことができる。
const group = new Konva.Group()
group.id('grp01')
const obj = stage.findOne('#grp01')
console.log(obj.id()) // grp01
#7 Generate and connect
生成したオブジェクトを、自動的にコネクターで接続する。今回の例では緑色の丸をクリックすると新しくコネクターでつないだ丸を生成する。
特別なことはしておらず、レイヤーオブジェクトへの参照をうまく取り回してクリックイベントが発生した時に layer.add()
しているだけ。ポイントは生成した●とコネクターを同時に追加している点。繋がっているように見せているが、座標が同じなだけでデータとしては繋がっていない。
const spawner = createSpawner(core, {
onSpawn: (eggObj) => {
layer.add(createEgg(eggObj))
layer.add(createConnector(layer, eggObj.id, core.id))
},
})
layer.add(spawner)
#8 react-konva on Next.js
データドリブンな描画を行いたいので 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;
#9 react-konva + Next.js + recoil
Next.js でデータドリブン・リアクティブな描画をする土台ができたので、今度はデータの状態管理ライブラリ Recoil を導入してデータを書き換えることを試みた。Recoil の使い方については別途記事にまとめたので、こちらを参照。
#10 Storybook
基礎の検証と把握ができてきたので、いよいよ完成イメージに近づけていく。
パーツの作り込みに集中するため Storybook を導入する。Konva だからといって特に難しいことはなく動作した。もちろん描画には canvas タグが前提となるので、ステージとレイヤーを敷いておく必要がある。以下のようにして デコレーター を定義するとよい。
export default {
title: "Example/NodeBox",
component: NodeBoxStory,
decorators: [
(Story) => (
<Stage width={window.innerWidth} height={window.innerHeight} draggable>
<Layer>
<Story />
</Layer>
</Stage>
),
],
};
#11 Synchronized objects
次に箱と線を繋いでグリグリ動かした上で線が追従するものを作る。こちらも 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]);