JavaScript用戶交互:移動物體
交互動畫的一個主要目標是創建出流暢的用戶體驗,其中大多數的用戶交互都是通過鼠標和觸摸屏實現的。
在這篇博文中,我想分享一些JS對于物體移動的常見用法,包括拖拽和投擲效果。
一. 使用鼠標事件
可以將一個鼠標單擊事件分解成兩個事件:鼠標按下事件和按鍵彈起事件。通常情況下這兩個事件是同時發生的。不過,有時鼠標按下后,鼠標還會移動一段時間才彈起,這種操作稱為拖曳,即按下、移動、在釋放。
在canvas動畫中,鼠標事件只能被HTML DOM樹上的canvas元素所捕獲,因此,我們需要手動計算出鼠標事件在canvas上的發生位置,并判斷出它是否發生在哪個繪制到canvas的物體 上。需要關注的鼠標事件有:mousedown、mousemove和mouseup。具體細節可參考我的相關博文《JavaScript動畫詳解(一) —— 循環與事件監聽》。
二. 使用觸摸事件
隨著觸摸屏設備的流行,我們很可能需要在動畫中捕捉用戶的觸摸事件。雖然觸摸屏與鼠標是不同的設備,但幸運的是,在DOM樹上捕捉觸摸事件與捕捉鼠標事件的差別不大。
與鼠標事件mousedown、mousemove和mouseup相對應的觸摸事件分別是touchstart、touchend與touchmove。
使用手指與鼠標的一個比較大的區別在于,鼠標始終出現在屏幕上,而手指卻不是一直處于觸摸狀態。常見的做法是,引入自定義屬性isPressed,用來告訴我們屏幕上是否有手指在觸摸。具體細節可參考我的相關博文《JavaScript動畫詳解(一) —— 循環與事件監聽》。
三. 拖拽事件
拖拽事件包含了三個子事件:鼠標按下、移動、釋放。通過不斷更新物體的坐標位置使其追隨鼠標指針的位置,就可以實現在canvas元素上拖拽物體。 另外還需要一個自定義屬性isPressed來標示當前鼠標是否按下,默認為false表示鼠標為彈起狀態。實現代碼包含以下過程:
1 . 捕捉mousedown事件,判斷當前鼠標是否在物體內。當鼠標在物體內按下時,設置isPressed = true;
2 . 捕捉mousemove事件,在處理程序內判斷當isPressed = true時,通過不斷更新物體的坐標位置使其追隨鼠標指針的位置來模擬出鼠標拖拽效果;
3 . 捕捉mouseup事件,將isPressed設置為false;
HTML代碼如下:
<canvas id="canvas" width="400" height="400"></canvas>
JavaScript代碼如下:
// 創建畫球函數
function Ball() {
this.x = 0;
this.y = 0;
this.radius = 20;
this.fillStyle = "#f85455";
this.draw = function(cxt) {
cxt.fillStyle = this.fillStyle;
cxt.beginPath();
cxt.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, true);
cxt.closePath();
cxt.fill();
}
}
// 獲得當前鼠標位置
function getMouse(ev) {
var mouse = {
x: 0,
y: 0
};
var event = ev || window.event;
if(event.pageX || event.pageY) {
x = event.x;
y = event.y;
}else {
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
x = event.clientX + scrollLeft;
y = event.clientY + scrollTop;
}
mouse.x = x;
mouse.y = y;
return mouse;
}
var canvas = document.getElementById("canvas"),
context = canvas.getContext("2d"),
ball = new Ball(),
mouse = {x: 0, y: 0},
isPressed = false;
ball.x = 20;
ball.y = 20;
// 渲染小球
ball.draw(context);
// 小球拖拽事件
canvas.addEventListener("mousedown", mouseDown, false);
canvas.addEventListener("mousemove", mouseMove, false);
canvas.addEventListener("mouseup", mouseUp, false);
function mouseDown(ev) {
// 判斷當前鼠標是否在小球內
mouse = getMouse(ev);
if(Math.pow(mouse.x - ball.x, 2) + Math.pow(mouse.y - ball.y, 2) <= Math.pow(ball.radius, 2)) {
isPressed = true;
}
}
function mouseMove(ev) {
if(isPressed) {
mouse = getMouse(ev);
ball.x = mouse.x;
ball.y = mouse.y;
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
}
function mouseUp(ev) {
// 標示鼠標彈起事件
isPressed = false;
}
但是,這個例子是有bug的!很快你就能發現,在拖拽的時候,小球的中心位置都在鼠標位置上,特別是當鼠標單擊小球邊緣時,會看見小球的中心點突然就跳動到了鼠標光標的位置上了。顯然,這顯得有點唐突。
我們可以稍作改良:
在鼠標按下的時候記錄當前鼠標位置與小球中心點位置的偏移量;
// 記錄鼠標按下時,鼠標與小球圓心的偏移量
dx = mouse.x - ball.x;
dy = mouse.y - ball.y;
在鼠標移動時,用鼠標的當前位置減去鼠標按下時記錄的偏移量
ball.x = mouse.x - dx;
ball.y = mouse.y - dy;
四. 投擲事件
在動畫中如何表現投擲呢?用鼠標選中一個物體,拖拽著它向某個方向移動,松開鼠標后,物體沿著拖拽的方向繼續移動。
在投擲物體時,必須在拖拽物體的過程中計算物體的速度向量,并在釋放物體時將這個速度向量賦給物體。實際上,計算拖拽時物體的速度向量的過程,恰好 與對物體應用速度向量的過程相反。在對物體應用速度向量時,將速度追加到物體原來所在的位置上,從而計算出物體的新位置,這個公式可以寫成:舊的位置 + 速度向量 = 新的位置,即速度向量 = 新的位置 – 舊的位置。
為了實現投擲行為,需要對前面的代碼做一些改動。首先,檢查鼠標是否按下,如果按下,用oldX和oldY變量保存小球舊的x、y坐標位置,并更新小球的拖拽速度。
具體JavaScript代碼實現如下:
// 創建畫球函數
function Ball() {
this.x = 0;
this.y = 0;
this.radius = 20;
this.fillStyle = "#f85455";
this.draw = function(cxt) {
cxt.fillStyle = this.fillStyle;
cxt.beginPath();
cxt.arc(this.x, this.y, this.radius, 0, 2 * Math.PI, true);
cxt.closePath();
cxt.fill();
}
}
// 獲得當前鼠標位置
function getMouse(ev) {
var mouse = {
x: 0,
y: 0
};
var event = ev || window.event;
if(event.pageX || event.pageY) {
x = event.x;
y = event.y;
}else {
var scrollLeft = document.documentElement.scrollLeft || document.body.scrollLeft;
var scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
x = event.clientX + scrollLeft;
y = event.clientY + scrollTop;
}
mouse.x = x;
mouse.y = y;
return mouse;
}
var canvas = document.getElementById("canvas"),
context = canvas.getContext("2d"),
ball = new Ball(),
mouse = {x: 0, y: 0},
isPressed = false,
oldX = 0,
oldY = 0,
currentX = 0,
currentY = 0,
vx = 0,
vy = 0;
ball.x = 200;
ball.y = 200;
// 聲明鼠標按下時,鼠標與小球圓心的距離
var dx = 0,
dy = 0;
// 渲染小球
ball.draw(context);
// 小球拖拽事件
canvas.addEventListener("mousedown", mouseDown, false);
canvas.addEventListener("mousemove", mouseMove, false);
canvas.addEventListener("mouseup", mouseUp, false);
function mouseDown(ev) {
// 判斷當前鼠標是否在小球內
mouse = getMouse(ev);
if(Math.pow(mouse.x - ball.x, 2) + Math.pow(mouse.y - ball.y, 2) <= Math.pow(ball.radius, 2)) {
isPressed = true;
// 記錄鼠標按下時,鼠標與小球圓心的距離
dx = mouse.x - ball.x;
dy = mouse.y - ball.y;
// 獲得小球拖拽前的位置
mouse = getMouse(ev);
oldX = mouse.x;
oldY = mouse.y;
}
}
function mouseMove(ev) {
if(isPressed) {
mouse = getMouse(ev);
ball.x = mouse.x - dx;
ball.y = mouse.y - dy;
context.clearRect(0, 0, canvas.width, canvas.height);
ball.draw(context);
}
}
function mouseUp(ev) {
// 標示鼠標彈起事件
isPressed = false;
// 把鼠標與圓心的距離位置恢復初始值
dx = 0;
dy = 0;
// 獲得小球拖拽后的位置
mouse = getMouse(ev);
currentX = mouse.x;
currentY = mouse.y;
// 更新速度向量:速度向量 = 新的位置 - 舊的位置
vx = (currentX - oldX) * 0.05;
vy = (currentY - oldY) * 0.05;
drawFrame();
}
// 緩動動畫
function drawFrame() {
animRequest = window.requestAnimationFrame(drawFrame, canvas);
context.clearRect(0, 0, canvas.width, canvas.height);
if(ball.x >= canvas.width - 30 || ball.x <= 30 || ball.y >= canvas.height - 30 || ball.y <= 30) {
window.cancelAnimationFrame(animRequest);
}
ball.x += vx;
ball.y += vy;
ball.draw(context);
}
這個Demo的邊界判斷還有一些bug,過些日子再修復。好累哇今天~
五. 總結
物體移動事件可以有很多總運動形式,但是都可以分解為三個單獨的事件來控制:按下、移動、釋放,在鼠標事件中分別對應的是mousedown、 mousemove和mouseup,在觸摸事件中分別對應的是touchstart、touchmove和touchend。通過不斷更新物體的坐標位 置使其追隨鼠標指針的位置,就可以實現在canvas元素上拖拽和投擲的效果。
來自:http://www.dengzhr.com/frontend/html/511