canvas中的拖拽、縮放、旋轉 (下) —— 代碼實現

寫在前面

圖片描述

本文首發於公衆號:符合預期的CoyPan

demo體驗地址及代碼在這裏:請用手機或瀏覽器模擬手機訪問

上一篇文章介紹了canvas中的拖拽、縮放、旋轉中涉及到的數學知識。可以點擊下面的鏈接查看。

canvas中的拖拽、縮放、旋轉 (上) —— 數學知識準備。

代碼準備 - 如何在canvas中畫出一個帶旋轉角度的元素

canvas中,如果一個元素帶有一個旋轉角度,可以直接變化canvas的座標軸來畫出此元素。舉個例子,

clipboard.png

代碼整體思路

整個demo的實現思路如下:

  1. 用戶開始觸摸(touchstart)時,獲取用戶的觸摸對象,是Sprite的本體?刪除按鈕?縮放按鈕?旋轉按鈕?並且根據各種情況,對變化參數進行初始化。
  2. 用戶移動手指(touchmove)時,根據手指的座標,更新stage中的所有元素的位置、大小,記錄變化參數。修改對應sprite的屬性值。同時對canvas進行重繪。
  3. 用戶一旦停止觸摸(touchend)時,根據變化參數,更新sprite的座標,同時對變化參數進行重置。

需要注意的是,在touchmove的過程中,並不需要更新sprite的座標,只需要記錄變化的參數即可。在touchend過程中,再進行座標的更新。座標的唯一用處,就是判斷用戶點擊時,落點是否在指定區域內。

代碼細節

首先,聲明兩個類:StageSpriteStage表示整個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到StagespriteList屬性內。

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的詳細實現過程。代碼還有很大的優化空間。事實上,工作上的需求並沒有要求【旋轉】,只需要實現【拖拽】、【縮放】即可。在只實現【拖拽】和【縮放】的情況下,會容易很多,不需要用到四個頂點的座標以及之前的那些複雜的數學知識。而在自己實現【旋轉】的過程中,也學到了很多。符合預期。


圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章