canvas中的拖拽、缩放、扭转 (下) —— 代码完成

写在前面

《canvas中的拖拽、缩放、扭转 (下) —— 代码完成》

本文首发于民众号:相符预期的CoyPan

demo体验地点及代码在这里:请用手机或浏览器模拟手机接见

上一篇文章引见了canvas中的拖拽、缩放、扭转中涉及到的数学知识。能够点击下面的链接检察。

canvas中的拖拽、缩放、扭转 (上) —— 数学知识预备。

代码预备 – 如安在canvas中画出一个带扭转角度的元素

canvas中,假如一个元素带有一个扭转角度,能够直接变化canvas的坐标轴来画出此元素。举个例子,

《canvas中的拖拽、缩放、扭转 (下) —— 代码完成》

ctx.save(); // 保留旧的坐标系状况
ctx.translate(x0 + w / 2, y0 + h / 2); // 坐标原点挪动到扭转中间
ctx.rotate(angle); // 扭转坐标系
ctx.translate(-(x0 + w / 2),  -(y0 + h / 2)); // 坐标原点复原
ctx.rect(x0, y0, w, h); // 以新坐标系为参照,画出矩形。
ctx.restore(); // 复原之前的坐标系状况

代码团体思绪

全部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的细致完成历程。代码另有很大的优化空间。事实上,工作上的需求并没有请求【扭转】,只须要完成【拖拽】、【缩放】即可。在只完成【拖拽】和【缩放】的情况下,会轻易许多,不须要用到四个极点的坐标以及之前的那些庞杂的数学知识。而在本身完成【扭转】的历程当中,也学到了许多。相符预期。

《canvas中的拖拽、缩放、扭转 (下) —— 代码完成》

    原文作者:CoyPan
    原文地址: https://segmentfault.com/a/1190000018163152
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞