寫在前面
本文首發於公衆號:符合預期的CoyPan
demo體驗地址及代碼在這裏:請用手機或瀏覽器模擬手機訪問
上一篇文章介紹了canvas中的拖拽、縮放、旋轉中涉及到的數學知識。可以點擊下面的鏈接查看。
canvas中的拖拽、縮放、旋轉 (上) —— 數學知識準備。
代碼準備 - 如何在canvas中畫出一個帶旋轉角度的元素
在canvas
中,如果一個元素帶有一個旋轉角度,可以直接變化canvas
的座標軸來畫出此元素。舉個例子,
代碼整體思路
整個demo的實現思路如下:
- 用戶開始觸摸(
touchstart
)時,獲取用戶的觸摸對象,是Sprite
的本體?刪除按鈕?縮放按鈕?旋轉按鈕?並且根據各種情況,對變化參數進行初始化。 - 用戶移動手指(
touchmove
)時,根據手指的座標,更新stage
中的所有元素的位置、大小,記錄變化參數。修改對應sprite
的屬性值。同時對canvas
進行重繪。 - 用戶一旦停止觸摸(
touchend
)時,根據變化參數,更新sprite
的座標,同時對變化參數進行重置。
需要注意的是,在touchmove
的過程中,並不需要更新sprite
的座標,只需要記錄變化的參數即可。在touchend
過程中,再進行座標的更新。座標的唯一用處,就是判斷用戶點擊時,落點是否在指定區域內。
代碼細節
首先,聲明兩個類:Stage
和Sprite
。Stage
表示整個canvas區域,Sprite
表示canvas中的元素。我們可以在Stage
中添加多個Sprite
,刪除Sprite
。這兩個類的屬性如下。
class Stage {
constructor(props) {
this.canvas = props.canvas;
this.ctx = this.canvas.getContext('2d');
// 用一個數組來保存canvas中的元素。每一個元素都是一個Sprite類的實例。
this.spriteList = [];
// 獲取canvas在視窗中的位置,以便計算用戶touch時,相對與canvas內部的座標。
const pos = this.canvas.getBoundingClientRect();
this.canvasOffsetLeft = pos.left;
this.canvasOffsetTop = pos.top;
this.dragSpriteTarget = null; // 拖拽的對象
this.scaleSpriteTarget = null; // 縮放的對象
this.rotateSpriteTarget = null; // 旋轉的對象
this.dragStartX = undefined;
this.dragStartY = undefined;
this.scaleStartX = undefined;
this.scaleStartY = undefined;
this.rotateStartX = undefined;
this.rotateStartY = undefined;
}
}
class Sprite {
constructor(props) {
// 每一個sprite都有一個唯一的id
this.id = Date.now() + Math.floor(Math.random() * 10);
this.pos = props.pos; // 在canvas中的位置
this.size = props.size; // sprite的當前大小
this.baseSize = props.size; // sprite的初始化大小
this.minSize = props.minSize; // sprite縮放時允許的最小size
this.maxSize = props.maxSize; // sprite縮放時允許的最大size
// 中心點座標
this.center = [
props.pos[0] + props.size[0] / 2,
props.pos[1] + props.size[1] / 2
];
this.delIcon = null;
this.scaleIcon = null;
this.rotateIcon = null;
// 四個頂點的座標,順序爲:左上,右上,左下,右下
this.coordinate = this.setCoordinate(this.pos, this.size);
this.rotateAngle = 0; // 累計旋轉的角度
this.rotateAngleDir = 0; // 每次旋轉角度
this.scalePercent = 1; // 縮放比例
}
}
demo中,點擊canvas下方的紅色方塊時,會實例化一個sprite
,調用stage.append
時,會將實例化的sprite
直接push到Stage
的spriteList
屬性內。
window.onload = function () {
const stage = new Stage({
canvas: document.querySelector('canvas')
});
document.querySelector('.red-box').addEventListener('click', function () {
const randomX = Math.floor(Math.random() * 200);
const randomY = Math.floor(Math.random() * 200);
const sprite = new Sprite({
pos: [randomX, randomY],
size: [120, 60],
minSize: [40, 20],
maxSize: [240, 120]
});
stage.append(sprite);
});
}
下面是Stage
的方法:
class Stage {
constructor(props) {}
// 將sprite添加到stage內
append(sprite) {}
// 監聽事件
initEvent() {}
// 處理touchstart
handleTouchStart(e) {}
// 處理touchmove
handleTouchMove(e) {}
// 處理touchend
handleTouchEnd() {}
// 初始化sprite的拖拽事件
initDragEvent(sprite, { touchX, touchY }) {}
// 初始化sprite的縮放事件
initScaleEvent(sprite, { touchX, touchY }) {}
// 初始化sprite的旋轉事件
initRotateEvent(sprite, { touchX, touchY }) {}
// 通過觸摸的座標重新計算sprite的座標
reCalSpritePos(sprite, touchX, touchY) {}
// 通過觸摸的【橫】座標重新計算sprite的大小
reCalSpriteSize(sprite, touchX, touchY) {}
// 重新計算sprite的角度
reCalSpriteRotate(sprite, touchX, touchY) {}
// 返回當前touch的sprite
getTouchSpriteTarget({ touchX, touchY }) {}
// 判斷是否touch在了sprite中的某一部分上,返回這個sprite
getTouchTargetOfSprite({ touchX, touchY }, part) {}
// 返回觸摸點相對於canvas的座標
normalizeTouchEvent(e) {}
// 判斷是否在在某個sprite中移動。當前默認所有的sprite都是長方形的。
checkIfTouchIn({ touchX, touchY }, sprite) {}
// 從場景中刪除
remove(sprite) {}
// 畫出stage中的所有sprite
drawSprite() {}
// 清空畫布
clearStage() {}
}
Sprite
的方法:
class Sprite {
constructor(props) {}
// 設置四個頂點的初始化座標
setCoordinate(pos, size) {}
// 根據旋轉角度更新sprite的所有部分的頂點座標
updateCoordinateByRotate() {}
// 根據旋轉角度更新頂點座標
updateItemCoordinateByRotate(target, center, angle){}
// 根據縮放比例更新頂點座標
updateItemCoordinateByScale(sprite, center, scale) {}
// 根據按鈕icon的頂點座標獲取icon中心點座標
getIconCenter(iconCoordinate) {}
// 根據按鈕icon的中心點座標獲取icon的頂點座標
getIconCoordinateByIconCenter(center) {}
// 根據縮放比更新頂點座標
updateCoordinateByScale() {}
// 畫出該sprite
draw(ctx) {}
// 畫出該sprite對應的按鈕icon
drawIcon(ctx, icon) {}
// 對sprite進行初始化
init() {}
// 初始化刪除按鈕,左下角
initDelIcon() {}
// 初始化縮放按鈕,右上角
initScaleIcon() {}
// 初始化旋轉按鈕,左上角
initRotateIcon() {}
// 重置icon的位置與大小
resetIconPos() {}
// 根據移動的距離重置sprite所有部分的位置
resetPos(dirX, dirY) {}
// 根據觸摸點移動的距離計算縮放比,並重置sprite的尺寸
resetSize(dir) {}
// 設置sprite的旋轉角度
setRotateAngle(angleDir) {}
}
Stage
的方法主要是處理和用戶交互的邏輯,得到用戶操作的交互參數,然後根據交互參數調用Sprite
的方法來進行變化。
代碼在這裏:https://coypan.info/demo/canvas-drag-scale-rotate.html
寫在後面
本文介紹了文章開頭給出的demo的詳細實現過程。代碼還有很大的優化空間。事實上,工作上的需求並沒有要求【旋轉】,只需要實現【拖拽】、【縮放】即可。在只實現【拖拽】和【縮放】的情況下,會容易很多,不需要用到四個頂點的座標以及之前的那些複雜的數學知識。而在自己實現【旋轉】的過程中,也學到了很多。符合預期。