关于javascript:Mini-Canvas-Lib-核心交互实现原理

32次阅读

共计 3317 个字符,预计需要花费 9 分钟才能阅读完成。

背景:
须要应用 Canvas 实现增加图片、文字、摄像头画面,并且反对拖拽、缩放、旋转等性能。
但成熟 Canvas 库(比方 Sprite.js Fabric.js)个别都比拟宏大(300kb+),所以本人实现精简版本,缩小体积。

基本功能:

  • 拖拽挪动元素
  • 缩放元素(变形、等比例缩放)
  • 旋转元素
  • 元素内容能够是:图片、文字、摄像头

画布中的元素(也可称为 Sprite),每个元素的的地位信息由 IRect 形容,交互可体验 Fabric.js。

interface IRect {
  x: number;
  y: number;
  w: number;
  h: number;
}

class Sprite {
  // 地位,尺寸数据
  rect: IRect
  // 旋转角度
  angle: number
}

canvas 绘制元素

因为需绘制元素(图片、文字、视频)占用的地位都可用矩形示意,所以能够:

// 将 canvas 坐标系原点挪动到 rect 中心点
ctx.setTransform(1, 0, 0, 1, x + w / 2, y + h / 2)
// 这样任意 rect 的绘制参数都是一样的
ctx.drwaImage(img, -w / 2, -h / 2, w, h)
// 围绕中心点旋转
ctx.rotate(angle)

挪动元素

这一步非常简单,跟拖拽 dom 元素一样。

// 1. 监听 mousemove 事件,由鼠标地位减去初始地位,能够失去挪动的坐标差值。// startX startY 是鼠标按下时的地位
incX = offsetX - startX
incY = offsetY - startY
// 2. 将挪动间隔映射为 Canvas 坐标系中的间隔(因为 Canvas clientWith 跟 width 很可能不一样)。incX = incX / (canvas.clientWith / canvas.width) // incY 同理
// 3. 将新的坐标设置到元素的地位上。sprite.setRect({x: x + incX, y: y + incY})

翻转元素

个别通过右键菜单来触发翻转成果,接管到事件后抉择以下某个办法

两个办法

  1. 可通过 ctx.setTransform 实现。(倡议,据说性能好些,不须要 ctx.save ctx.restore)

    // https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/setTransform
    // 程度翻转
    ctx.setTransform(-1, 0, 0, 1, 0, 0)
    // 垂直翻转
    ctx.setTransform(1, 0, 0, -1, 0, 0)
  2. 可通过 ctx.scale 实现。

    // https://developer.mozilla.org/zh-CN/docs/Web/API/CanvasRenderingContext2D/scale
    // 程度翻转
    ctx.scale(-1, 1)
    // 垂直翻转
    ctx.scale(1, -1)

旋转元素

  1. 获取旋转的中心点,通过 rect 计算失去。{x: x + w / 2, y: y + h / 2}
  2. 监听 mousemove 事件,获取鼠标坐标,由鼠标坐标失去旋转角度angle = Math.atan2(y, x)

    function rotateSprite (centerPos, onChange) {const onMove = ({ clientX, clientY}: MouseEvent): void => {
     // 映射为 绝对中心点的坐标
     const x = clientX - centerPos.x
     const y = clientY - centerPos.y
     // 旋转控制点在正上方,绝对 x 轴是 -90°,所以须要加上 Math.PI / 2
     const angle = Math.atan2(y, x) + Math.PI / 2
     onChange(angle)
      }
      const clear = (): void => {window.removeEventListener('mousemove', onMove)
     window.removeEventListener('mouseup', clear)
      }
      window.addEventListener('mousemove', onMove)
      window.addEventListener('mouseup', clear)
    }

缩放元素

自身 rect 缩放非常简单,加上旋转后,缩放成果就简单了很多。
比方:

  1. 拖拽 rect 右侧的控制点,只能扭转矩形的宽度,在旋转状况下,rect 左侧的边是放弃不动的。
  2. 拖拽 rect 右下角的控制点,同时扭转宽高,但须要地位等比,rect 左上角的点放弃不动。

所以这个题能够形象为

  1. 给出的参数:rect(坐标、宽高)数据,旋转角度、鼠标坐标。
  2. 须要解出的答案:新的 IRect(坐标、宽高)。

解题步骤

以拖拽右下角控制点(RB)为例,因为是等比缩放,所以中心点会始终处于对角线(LT-RB)上。

  1. 监听 mousemove 事件,获取鼠标坐标,减去中心点坐标,失去绝对于中心点的坐标。
  2. 依据坐标 O、元素旋转角度、IRect,可计算出 RB 被 挪动的长度。
  3. 挪动长度乘以对应的三角函数,即为减少的宽、高。

    // 对角线角度(LB-RT 对角线,角度是正数,须要乘以 -1)const diagonalAngle = Math.atan2(rect.h, rect.w)
    // 坐标系旋转角度,lb->rt 的对角线的初始角度为正数,所以须要乘以 -1
    const rotateAngle = diagonalAngle + angle
    // startPos 及 RB 的坐标(拖拽起始地位),ClientXY 是 mousemove 事件中获取的坐标
    const ox = clientX - startPos.x
    const oy = clientY - startPos.y
    // 坐标系旋转变动公式,让 x 轴与【对角线重合】,鼠标地位的 x 值即为减少的长度(RB’位于鼠标与对角线的垂直交点)const incS = ox * Math.cos(rotateAngle) + oy * Math.sin(rotateAngle)
    // 等比例缩放,减少宽低等于长度乘以对应的角度函数
    // 因为等比例缩放,核心及被拖拽的点,肯定在对角线上
    const incW = incS * Math.cos(diagonalAngle)
    const incH = incS * Math.sin(diagonalAngle)
    
    // 新中心点坐标, 原核心坐标 (x + w / 2, y + h / 2)
    const newCntX = incS / 2 * Math.cos(rotateAngle) + x + w / 2
    const newCntY = incS / 2 * Math.sin(rotateAngle) + y + h / 2
    // rect 新坐标
    const newX = newCntX - newW / 2
    const newY = newCntY - newH / 2
    
    // 新的 rect: {x: newX, y: newY, w: newW, h: newH}

    下面代码用到了 坐标系旋转变动公式,其推导如下图所示:

其余缩放场景注意事项

  1. 右边三个点的缩放,坐标值变小(正数),也是在放大,所以计算出来值要乘以 -1。
  2. 利用好坐标系旋转变动公式,拖拽某些点时,留神其角度与默认 X 轴之间的差值。

判断鼠标是否点击了元素

  1. 将鼠标坐标,rect xy 值转换为绝对于中心点的坐标
  2. 再求出鼠标坐标旋转 rect 角度后的坐标(坐标系旋转公式)
  3. 判断鼠标点击的坐标是否超出 rect 边界

    // pos 鼠标坐标,rect center 中心点
    // 鼠标点击坐标映射成 canvas 坐标, 而后转换为以中点为原点的坐标
    const cvsOX = pos.x / ratio.w - center.x
    const cvsOY = pos.y / ratio.h - center.y
    
    // 如果有旋转,映射成绝对原点,旋转前的坐标
    let mx = cvsOX
    let my = cvsOY
    
    let {x, y, w, h} = rect
    // 映射成中心点坐标
    x = x - center.x
    y = y - center.y
    // 坐标系旋转变动公式 
    mx = cvsOX * Math.cos(angle) + cvsOY * Math.sin(angle)
    my = cvsOY * Math.cos(angle) - cvsOX * Math.sin(angle)
    
    if (mx < x || mx > x + w || my < y || my > y + h) return false
    
    return true

正文完
 0