マウスについてくるやつの実装
お絵かきアプリの実装などでも役立つ。
https://gyazo.com/d26041cfcd86d06e5200b5fff7568ea3
Vueのバインディングで動かしてるのでワンテンポ追従は遅れる。
Vue + mousemove版。
code: index-mouse.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
body {
}
svg {
background: white;
}
</style>
</head>
<body>
<div id="app">
<svg ref="canv" @mousemove="onMouseMove" width="600" height="400">
<circle :cx="p.x" :cy="p.y" r="5"></circle>
</svg>
</div>
<script>
new Vue({
el: "#app",
data: {
p: {
x: 0,
y: 0
}
},
methods: {
onMouseMove: function(e) {
//SVG要素への参照を取る
const svg = this.$refs.canv;
const pt = svg.createSVGPoint();
//スクリーン座標を取得
pt.x = e.clientX;
pt.y = e.clientY;
//SVG上の座標に変換
//getScreenCTMを利用することで、SVGが拡縮された場合でも
//位置ずれが起きない
this.p = pt.matrixTransform(svg.getScreenCTM().inverse());
}
}
});
</script>
</body>
</html>
しかし、iOSではかなり追従が遅く、クリックしたあとに少したって発火する。
これを回避するには、touchmoveとmousemove両対応のコードを書くことになる。
少し簡潔に書くためには、pointermoveを使うと良い。
ただしSafariでは未だに対応がないので、ポリフィルとしてPEPを利用する。
touchmove版に対するアドバンテージとしては、コードの短さの他に、
setPointCaptureが使えることがある。
Vue + pointermove版
code: index-pointer.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
body {
}
svg {
background: white;
}
</style>
</head>
<body>
<div id="app">
<svg
touch-action="none"
ref="canv"
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
width="600"
height="400"
<circle :cx="p.x" :cy="p.y" r="5"></circle>
</svg>
</div>
<script>
new Vue({
el: "#app",
data: {
p: {
x: 0,
y: 0
}
},
methods: {
onPointerDown: function(e) {
//setPointerCaptureを使うことで
//要素外でもpointermoveイベントを取得することができる
//(お絵かきアプリなどで便利)
const svg = this.$refs.canv;
svg.setPointerCapture(e.pointerId);
},
onPointerMove: function(e) {
//SVG要素への参照を取る
const svg = this.$refs.canv;
const pt = svg.createSVGPoint();
//スクリーン座標を取得
pt.x = e.clientX;
pt.y = e.clientY;
//SVG上の座標に変換
//getScreenCTMを利用することで、SVGが拡縮された場合でも
//位置ずれが起きない
this.p = pt.matrixTransform(svg.getScreenCTM().inverse());
}
}
});
</script>
</body>
</html>
PEPを利用する場合は、Safariネイティブのスクロールなどをキャンセルするため、要素にtouch-action="none"を付与する必要がある。
pointermoveを利用すると、ドラッガブルなオブジェクトを比較的楽に実装できる。
https://gyazo.com/a7a710c6d05a118bd746a5b0b8972f8a
code: index-drag.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<style>
body {
}
svg {
background: white;
}
</style>
</head>
<body>
<div id="app">
<svg touch-action="none" ref="canv" width="600" height="400">
<circle
@pointerdown="onPointerDown"
@pointermove="onPointerMove"
@pointerup="onPointerUp"
:cx="p.x"
:cy="p.y"
r="30"
</circle>
</svg>
</div>
<script>
new Vue({
el: "#app",
data: {
p: {
x: 100,
y: 100
},
dragging: false
},
methods: {
onPointerUp: function(e) {
this.dragging = false;
},
onPointerDown: function(e) {
const circle = e.target;
circle.setPointerCapture(e.pointerId);
this.dragging = true;
},
onPointerMove: function(e) {
if (this.dragging) {
const svg = this.$refs.canv;
const pt = svg.createSVGPoint();
pt.x = e.clientX;
pt.y = e.clientY;
this.p = pt.matrixTransform(svg.getScreenCTM().inverse());
}
}
}
});
</script>
</body>
</html>
これはSafariでもスムーズに動作する。
コードが簡潔になる理由
従来のtouchmoveでの実装だと、touchstartは操作したい要素に設定するが、touchendやtouchmoveは「操作したい要素の親要素」に設定する必要があった(ドラッグしているうちに、要素からカーソルが外れてしまうので)
一方、setPointCaptureがある環境では、その要素の領域外でもイベントが上がってくるため、すべてのイベントハンドラを操作対象要素にまとめることができる。