资讯专栏INFORMATION COLUMN

canvas中的拖拽、缩放、旋转 (下) —— 代码实现

pumpkin9 / 792人阅读

摘要:中的拖拽缩放旋转上数学知识准备。表示整个区域,表示中的元素。事实上,工作上的需求并没有要求旋转,只需要实现拖拽缩放即可。

写在前面

本文首发于公众号:符合预期的CoyPan

demo体验地址及代码在这里:请用手机或浏览器模拟手机访问

上一篇文章介绍了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的实现思路如下:

用户开始触摸(touchstart)时,获取用户的触摸对象,是Sprite的本体?删除按钮?缩放按钮?旋转按钮?并且根据各种情况,对变化参数进行初始化。

用户移动手指(touchmove)时,根据手指的坐标,更新stage中的所有元素的位置、大小,记录变化参数。修改对应sprite的属性值。同时对canvas进行重绘。

用户一旦停止触摸(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的详细实现过程。代码还有很大的优化空间。事实上,工作上的需求并没有要求【旋转】,只需要实现【拖拽】、【缩放】即可。在只实现【拖拽】和【缩放】的情况下,会容易很多,不需要用到四个顶点的坐标以及之前的那些复杂的数学知识。而在自己实现【旋转】的过程中,也学到了很多。符合预期。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/101699.html

相关文章

  • 基于javascript的拖拽类封装

    摘要:参考了很多别人写的代码,最后终于弄明白了其中的原理,自己也写了一个。效果图如下地址如下拖拽类封装代码使用方法引入和对应的。如果没有为的结构,就创建。鼠标移动时,记录再次计算鼠标位置距离中心位置的的反正切函数。 在公司做一个h5编辑平台,中间需要对元素进行拖拽、放大缩小、旋转等操作,且需要对文本、图片、音乐组件等不同元素都可以具备这些功能。参考了很多别人写的代码,最后终于弄明白了其中的原...

    afishhhhh 评论0 收藏0
  • 用typescript开发手势库 - (1)web开发常用手势有哪些?

    这只是个开头 说在最前面,本文是一个系列文章的开头, 这个系列里我会讲如何用typescript开发一款支持pc和手机端的手势库any-touch, 以及通过jest让你的代码测试覆盖率100%. showImg(https://segmentfault.com/img/bVbp3B0?w=936&h=246); 目录 用TypeScript开发手势库 - (2)tsconfig.json & r...

    raise_yang 评论0 收藏0
  • 多功能React影像组件(拖拽、水印、缩放、切换、旋转)

    摘要:移动的过程中可以通过拿到元素的坐标,记为。向上滚动放大,向下滚动缩小这里要注意控制最小缩放值。还要注意的是图片在边界的缩放,不然图片可能会移动在屏幕外。代码实现控制滚轮缩放计算缩放后的大小每一次滚轮限制最小不让由于缩小消失在视野中 cxj-react-image 用法如下: yarn add cxj-react-image // npm i cxj-react-image import...

    soasme 评论0 收藏0
  • jQuery 图片查看插件 Magnify 开发简介(仿 Windows 照片查看器)

    摘要:随后会陆续发布及相关版本的插件。这和图片查看器的操作方式是相同的。目前的调整大小存在一点,但不影响整体的使用。键盘控制和照片查看器的按键是一样的。除了照片查看器,的图片查看器也非常的高大上。 showImg(https://segmentfault.com/img/remote/1460000012565638?w=750&h=375); 前言 因为一些特殊的业务需求,经过一个多月的蛰...

    anyway 评论0 收藏0

发表评论

0条评论

pumpkin9

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<