最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 使用canvas 如何绘制形状并支持拖拽、缩放功能

    正文概述 掘金(rudy_zhou)   2021-01-04   1197

    引言

       之前遇到过一个面试的机试题,就是用画布绘制形状,并且支持缩放、拖拽功能。现在有点时间就分享一下我是如何一步一步完成这个功能的。看这篇信息之前最好先去看一下canvasapi,canvas API 穿梭机。

    开始编写

    先写出容器Dom,和样式 html

    <div id="chart-wrap" class="chart-wrap"></div>
    

    css

    html,body {
      margin: 0;
      height: 100%;
      overflow: hidden;
    }
    .chart-wrap {
      height: calc(100% - 40px);
      margin: 20px;
      box-shadow: 0 0 3px orange;
    }
    

    首先绘制一个形状

    这里写一个 名叫 chart 的类,在 构造器 constructor 里初始化画布,写好绘制形状的函数、以及画布渲染。代码如下:

    class chart {
      // 初始构造器
      constructor(params) {
        var wrapDomStyle = getComputedStyle(params.el);
        this.width = parseInt(wrapDomStyle.width, 10);
        this.height = parseInt(wrapDomStyle.height, 10);
    
        // 创建canvas画布
        this.El = document.createElement('canvas');
        this.El.height = this.height;
        this.El.width = this.width;
        this.ctx = this.El.getContext('2d');
    
        params.el.appendChild(this.El);
      }
    
      // 绘制圆形
      drawCircle(data) {
        this.ctx.beginPath();
        this.ctx.fillStyle = data.fillStyle;
        this.ctx.arc(data.x, data.y, data.r, 0, 2 * Math.PI);
        this.ctx.fill();
      }
    
      // 添加形状
      push(data) {
        this.drawCircle(data);
      }
    }
    
    // 构建图表对象
    var chartObj = new chart( { el: document.getElementById('chart-wrap') } );
    
    // 绘制圆形
    chartObj.push({
      fillStyle: 'pink',
      x: 400,
      y: 300,
      r: 50
    });
    

    上面代码结构很简单,new 一个对象,传入容器Dom,在constructor 中初始化一个画布放入 div#chart-wrap 这个 dom 中,再把创建好的实例赋值给 chartObj 这个变量。

    通过调用类的 push 方法,绘制一个圆形。

    代码效果点击此处观看

    绘制多个、多种类型形状

    如果想绘制其他图形就需要加 type 判断,以上代码改造完成后如下:

    class chart {
      // 初始构造器
      constructor(params) {
        var wrapDomStyle = getComputedStyle(params.el);
        this.width = parseInt(wrapDomStyle.width, 10);
        this.height = parseInt(wrapDomStyle.height, 10);
    
        // 创建canvas画布
        this.El = document.createElement('canvas');
        this.El.height = this.height;
        this.El.width = this.width;
        this.ctx = this.El.getContext('2d');
    
        params.el.appendChild(this.El);
      }
    
      // 绘制圆形
      drawCircle(data) {
        this.ctx.beginPath();
        this.ctx.fillStyle = data.fillStyle;
        this.ctx.arc(data.x, data.y, data.r, 0, 2 * Math.PI);
        this.ctx.fill();
      }
      
      // _____________ 添加绘制线条方法 ____________
      drawLine(data) {
        var arr = data.data.concat()
        var ctx = ctx || this.ctx;  
    
        ctx.beginPath()
        ctx.moveTo(arr.shift(), arr.shift())
        ctx.lineWidth = data.lineWidth || 1
        do{
          ctx.lineTo(arr.shift(), arr.shift());
        } while (arr.length)
    
        ctx.stroke();
      }
      
      // ___________ 添加绘制矩形方法 ______________
      drawRect(data) {
        this.ctx.beginPath();
        this.ctx.fillStyle = data.fillStyle;
        this.ctx.fillRect(...data.data);
      }
    
      // ___________ 添加一个判断类型绘制的方法 _____________
      draw(item) {
        switch(item.type){
          case 'line':
            this.drawLine(item)
            break;
          case 'rect':
            this.drawRect(item)
            break;
          case 'circle':
            this.drawCircle(item)
            break;
        }
      }
      
      // 添加形状
      push(data) {
        this.draw(data); // ____________ 修改调用绘制方法 ____________
      }
    }
    
    // 构建图表对象
    var chartObj = new chart( { el: document.getElementById('chart-wrap') } );
    
    // 绘制圆形
    chartObj.push({
      type: 'circle', // ____________ 这里添加了一个类型 __________________
      fillStyle: 'pink',
      x: 400,
      y: 300,
      r: 50
    });
    
    // ___________ 添加绘制线条 __________
    chartObj.push({
      type: 'line',
      lineWidth: 4,
      data: [100, 90, 200, 90, 250, 200, 400, 200]
    })
    
    // ___________ 添加绘制矩形 __________
    chartObj.push({
      type: 'rect',
      fillStyle: "#0f00ff",
      data: [350, 400, 100, 100]
    })
    

    对比前面这里添加了一个绘制矩形(drawRect)、绘制线条(drawLine)的方法 和 数据,并且添加了判断渲染类型的函数(draw)。

    代码效果点击此处观看

    添加缩放功能

    添加缩放需要先理清一些东西。

    缩放 canvas 提供了两个类型方法可以实现,一个是在当前缩放基础上缩放,一个是在基础画布上缩放。

    矩阵变化不只有缩放,但是可以其他参数不变只更改缩放值

    当前缩放基础上缩放:scale()缩放当前绘图至更大或更小,transform()替换绘图的当前转换矩阵;   意思就是原本画布大小是 1,第一次放大 2倍,就变成2,第二次放大2倍就变成4

    在基础画布上缩放: setTransform()将当前转换重置为单位矩阵。然后运行 transform()。   意思就是原本画布大小是 1,第一次放大 2倍,就变成2,第二次放大2倍还是2,因为重置回原来的1后再放大的

    这里我使用 setTransform() 缩放画布

    第一步骤:.因为要缩放所以必须保存好当前的缩放值,就在constructor 加以下参数,以及在 push() 方法下保存数据、render() 重绘所有数据

    constructor() {
      // 因为canvas是基于状态绘制的,也就是设置了缩放值,再绘制的元素才会根据缩放倍数绘制,因此需要把每个绘制的对象保存起来。
      this.data = []; 
      this.scale = 1; // 默认缩放值是 1
    }
    
    // 添加形状
    push(data) {
      // push 方法中添加保存数据操作
      this.data.push(data);
    }
    
    // 渲染整个 图形画布
    render() {
      this.El.width = this.width
    
      this.data.forEach(item => {
        this.draw(item)
      })
    }
    

    第二步骤:.因为缩放时鼠标滚轮控制,所以加上监听滚轮事件,而且是在鼠标移入画布中时才添加,不在画布中就不需要监听滚轮事件。

    constructor() {
      // 添加滚轮判断事件
      this.addScaleFunc();
    }
     
    // 添加缩放功能,判断时机注册移除MouseWhell事件
    addScaleFunc() {
      this.El.addEventListener('mouseenter', this.addMouseWhell);
      this.El.addEventListener('mouseleave', this.removeMouseWhell);
    }
    
    // 添加 mousewhell 事件
    addMouseWhell = () => {
      document.addEventListener('mousewheel', this.scrollFunc, {passive: false});
    }
    
    // 移除mousewhell 事件
    removeMouseWhell = () => {
      document.removeEventListener('mousewheel', this.scrollFunc, {passive: false});
    }
    

    第三步骤:滚轮事件监听完成后,就是调用具体的缩放实现代码了

    constructor() {
      // 缩放具体实现会用到的数据
      this.maxScale = 3; // 最大缩放值
      this.minScale = 1; // 最小缩放值
      this.step = 0.1;   // 缩放率
      this.offsetX = 0;  // 画布X轴偏移值
      this.offsetY = 0;  // 画布Y轴偏移值
    }
    
    // 缩放 具体计算
    scrollFunc = (e) => {
      // 阻止默认事件 (缩放时外部容器禁止滚动)
      e.preventDefault();
    
      if(e.wheelDelta){
        var x = e.offsetX - this.offsetX
        var y = e.offsetY - this.offsetY
    
        var offsetX = (x / this.scale) * this.step
        var offsetY = (y / this.scale) * this.step
    
        if(e.wheelDelta > 0){
          this.offsetX -= this.scale >= this.maxScale ? 0 : offsetX
          this.offsetY -= this.scale >= this.maxScale ? 0 : offsetY
    
          this.scale += this.step
        } else {
          this.offsetX += this.scale <= this.minScale ? 0 : offsetX
          this.offsetY += this.scale <= this.minScale ? 0 : offsetY
    
          this.scale -= this.step
        }
    
        this.scale = Math.min(this.maxScale, Math.max(this.scale, this.minScale))
        
        this.render()
      }
    }
    
    // 在类型判断渲染方法内添加设置缩放
    draw() {
      this.ctx.setTransform(this.scale,0, 0, this.scale, this.offsetX, this.offsetY);
    }
    

    以上代码效果预览

    解释:    第一步骤第二步骤理解起来很容易,比较麻烦的是第三步骤,下面就来详细解释一下第三部具体缩放实现。

    缩减一下代码

    scrollFunc = (e) => {
      // 阻止默认事件 (缩放时外部容器禁止滚动)
      e.preventDefault();
    
      if(e.wheelDelta){
      
        e.wheelDelta > 0 ? this.scale += this.step : this.scale -= this.step
        
        this.render()
      }
    }
    

    只需要上述几行就实现了缩放。判断 e.wheelDelta 是向上滚动还是向下,从而增加或减少 this.scale 的大小,最后调用 render() 重新绘制当前画布。

    e.preventDefault() 就不多解释了,大家都知道是解决默认行为的。但是有一点要解释一下 在调用 scrollFunc() 这个函数的事件监听器的第三个参数 {passive: false} 是必须加的(默认就是 {passive: true}),不然无法阻止默认的滚动事件。

    大家可以在演示例子中注释掉 scrollFunc 中的其它代码查看效果,发现缩放是可以了,但是,却没有根据鼠标位置进行缩放,而是始终以画布(0,0) 的位置缩放。所以画布放大后会向右下偏移,因此需要向左和上偏移校正,使缩放看起来就像在鼠标位置缩放。

    在上方代码上改造一下 代码如下:

    scrollFunc = (e) => {
      // 阻止默认事件 (缩放时外部容器禁止滚动)
      e.preventDefault();
    
      if(e.wheelDelta){
      
        var x = e.offsetX - this.offsetX
        var y = e.offsetY - this.offsetY
    
        var offsetX = (x / this.scale) * this.step
        var offsetY = (y / this.scale) * this.step
    
        if(e.wheelDelta > 0){
          this.offsetX -= offsetX
          this.offsetY -= offsetY
    
          this.scale += this.step
        } else {
          this.offsetX += offsetX
          this.offsetY += offsetY
    
          this.scale -= this.step
        }
        
        this.render()
      }
    }
    

    x,y 是鼠标距离画布原始原点的距离,offsetX,offsetY 是本次缩放的偏移量,然后判断放大或者缩小从而增减整体画布的偏移量。

    本次偏移量计算方式:鼠标距原始点距离(x,y) 除以 缩放值 this.scale 再乘以 缩放率 this.step  解释:因为是使用setTransform(),所以每次放大或者缩小都是在原始画布大小的基础上缩放,所以需要除以缩放值,找到在原始缩放基础上鼠标距离原始点的距离。   解释:如果使用scale(),就不需要除以缩放值,直接当前缩放值乘以缩放率就能等于现在实际缩放值

    最后再把缩放功能完善,添加最大缩放值this.maxScale 和 最小缩放值 this.minScale 限制,完成代码如下:

    // 缩放 具体计算
    scrollFunc = (e) => {
      // 阻止默认事件 (缩放时外部容器禁止滚动)
      e.preventDefault();
    
      if(e.wheelDelta){
        var x = e.offsetX - this.offsetX
        var y = e.offsetY - this.offsetY
    
        var offsetX = (x / this.scale) * this.step
        var offsetY = (y / this.scale) * this.step
    
        if(e.wheelDelta > 0){
          this.offsetX -= this.scale >= this.maxScale ? 0 : offsetX
          this.offsetY -= this.scale >= this.maxScale ? 0 : offsetY
    
          this.scale += this.step
        } else {
          this.offsetX += this.scale <= this.minScale ? 0 : offsetX
          this.offsetY += this.scale <= this.minScale ? 0 : offsetY
    
          this.scale -= this.step
        }
    
        this.scale = Math.min(this.maxScale, Math.max(this.scale, this.minScale))
        
        this.render()
      }
    }
    

    以上缩放值计算就完成了,最后只需调用 this.render(),在this.render 中会调用 this.draw 函数,这个函数里调用setTransform 方法,这里会将更改后的缩放值,以及偏移值设置到画布中。

    this.ctx.setTransform(this.scale,0, 0, this.scale, this.offsetX, this.offsetY);
    

    添加拖拽画布的效果

    首先理清一下拖拽的步骤 鼠标按下 => 鼠标移动 => 鼠标放开

    鼠标按下:我们用 mousedown 事件,然后在按下事件中注册 鼠标移动 事件 鼠标移动:我们用 mousemove 事件,在鼠标移动事件中 具体实现画布移动 鼠标放开:我们用 mouseup 事件,在鼠标放开事件中 删除 鼠标移动 事件

    具体代码如下:

    constructor(params) {
      this.wrapDom = params.el;
      this.addDragFunc();
    }
    
    // 添加拖拽功能,判断时机注册移除 拖拽 功能
    addDragFunc() {
      this.El.addEventListener('mousedown', this.addMouseMove);
      document.addEventListener('mouseup', this.removeMouseMove);
    }
    
    // 添加鼠标移动 功能,获取保存当前点击坐标
    addMouseMove = (e) => {
      this.targetX = e.offsetX
      this.targetY = e.offsetY
    
      this.mousedownOriginX = this.offsetX;
      this.mousedownOriginY = this.offsetY;
      
      this.wrapDom.style.cursor = 'grabbing'
      this.El.addEventListener('mousemove', this.moveCanvasFunc, false)
    }
    // 移除鼠标移动事件
    removeMouseMove = () => {
      this.wrapDom.style.cursor = ''
      this.El.removeEventListener('mousemove', this.moveCanvasFunc, false)
      this.El.removeEventListener('mousemove', this.moveShapeFunc, false)
    }
    
    // 移动画布
    moveCanvasFunc = (e) => {
      // 获取 最大可移动宽
      var maxMoveX = this.El.width / 2;
      var maxMoveY = this.El.height / 2;
    
      var offsetX = this.mousedownOriginX + (e.offsetX - this.targetX);
      var offsetY = this.mousedownOriginY + (e.offsetY - this.targetY);
    
      this.offsetX = Math.abs(offsetX) > maxMoveX ? this.offsetX : offsetX
      this.offsetY = Math.abs(offsetY) > maxMoveY ? this.offsetY : offsetY
      
      this.render()
    }
    
    

    以上代码效果演示

    其它代码都很简单,这里就详细解释一下 addMouseMove()moveCanvasFunc() 做了哪些操作。

    addMouseMove 函数中 使用 targetX,targetY 保存了鼠标点击时的坐标,mousedownOriginX ,mousedownOriginX 保存了鼠标点击时 画布的整体偏移量。

    再在 moveCanvasFunc 函数中 计算出移动后的整体偏移量,moveCanvasFunc 函数中的代码可以简化成这样:

    moveCanvasFunc = (e) => {
      var offsetX = this.mousedownOriginX + (e.offsetX - this.targetX);
      var offsetY = this.mousedownOriginY + (e.offsetY - this.targetY);
      
      this.render()
    }
    

    其他代码是为了限制偏移量的最大值,最后调用this.render()

    整体来讲,拖拽画布功能比缩放稍微简单一些,同样这里最后会调用 this.render(),在this.render 中会调用 this.draw 函数,这个函数里调用了setTransform 方法,这里会将更改后的缩放值,以及偏移值设置到画布中。

    this.ctx.setTransform(this.scale,0, 0, this.scale, this.offsetX, this.offsetY);
    

    拖拽画布中的形状

    如果要拖拽画布中的形状,需要判断鼠标点击的位置是否处于形状中,而且因为层级关系,只能控制顶层的形状。

    因此需要写鼠标按下时是否处于形状内部的判断方法,这里我们只写了矩形、圆形、线段的判断方法。

    因为之前已经在实现画布拖拽的时候,实现了拖拽功能,现在只需要要改造 addMouseMove 函数 和添加 形状移动 函数,以及三个判断方法。

    整体代码如下:

    // 添加鼠标移动 功能,获取保存当前点击坐标
    addMouseMove = (e) => {
    
      this.targetX = e.offsetX
      this.targetY = e.offsetY
    
      this.mousedownOriginX = this.offsetX;
      this.mousedownOriginY = this.offsetY;
    
      var x = (this.targetX - this.offsetX) / this.scale;
      var y = (this.targetY - this.offsetY) / this.scale;
    
      this.activeShape = null
    
      this.data.forEach(item => {
        switch(item.type){
          case 'rect':
            this.isInnerRect(...item.data, x, y) && (this.activeShape = item)
            break;
          case 'circle':
            this.isInnerCircle(item.x, item.y, item.r, x, y) && (this.activeShape = item)
            break;
          case 'line':
            var lineNumber = item.data.length / 2 - 1
            var flag = false
            for(let i = 0; i < lineNumber; i++){
              let index = i*2;
              flag = this.isInnerPath(item.data[index], item.data[index+1], item.data[index+2], item.data[index+3], x, y, item.lineWidth || 1)
              if(flag){
                this.activeShape = item
                break;
              }
            }
        }
      })
    
      if(!this.activeShape){
        this.wrapDom.style.cursor = 'grabbing'
        this.El.addEventListener('mousemove', this.moveCanvasFunc, false)
      } else {
        this.wrapDom.style.cursor = 'all-scroll'
        this.shapedOldX = null
        this.shapedOldY = null
        this.El.addEventListener('mousemove', this.moveShapeFunc, false)
      }
    }
    
    // 移动形状
    moveShapeFunc = (e) => {
      var moveX = e.offsetX - (this.shapedOldX || this.targetX);
      var moveY = e.offsetY - (this.shapedOldY || this.targetY);
      
      moveX /= this.scale
      moveY /= this.scale
    
      switch(this.activeShape.type){
        case 'rect':
          let x = this.activeShape.data[0]
          let y = this.activeShape.data[1]
          let width = this.activeShape.data[2]
          let height = this.activeShape.data[3]
          this.activeShape.data = [x + moveX, y + moveY, width, height]
          break;
        case 'circle':
          this.activeShape.x += moveX
          this.activeShape.y += moveY
          break;
        case 'line':
          var item = this.activeShape;
          var lineNumber = item.data.length / 2
          for(let i = 0; i < lineNumber; i++){
            let index = i*2;
            item.data[index] += moveX
            item.data[index + 1] += moveY
          }
      }
      this.shapedOldX = e.offsetX
      this.shapedOldY = e.offsetY
    
      this.render()
    }
    
    // 判断是否在矩形框内
    isInnerRect(x0, y0, width, height, x, y) {
      return x0 <= x && y0 <= y && (x0 + width) >= x && (y0 + height) >= y
    }
    
    // 判断是否在圆形内
    isInnerCircle(x0, y0, r, x, y) {
      return Math.pow(x0 - x, 2) + Math.pow(y0 - y, 2) <= Math.pow(r, 2)
    }
    
    // 判断是否在路径上
    isInnerPath(x0, y0, x1, y1, x, y, lineWidth) {
      var a1pow = Math.pow(x0 - x, 2) + Math.pow(y0 - y, 2);
      var a1 = Math.sqrt(a1pow, 2)
      var a2pow = Math.pow(x1 - x, 2) + Math.pow(y1 - y, 2)
      var a2 = Math.sqrt(a2pow, 2)
    
      var a3pow = Math.pow(x1 - x0, 2) + Math.pow(y1 - y0, 2)
      var a3 = Math.sqrt(a3pow, 2)
    
      var r = lineWidth / 2
      var ab = (a1pow - a2pow + a3pow) / (2 * a3)var ab = (a1pow - a2pow + a3pow) / (2 * a3)
      var h = Math.sqrt(a1pow - Math.pow(ab, 2), 2)
    
      var ad = Math.sqrt(Math.pow(a3, 2) + Math.pow(r, 2))
    
      return h <= r && a1 <= ad && a2 <= ad
    }
    

    以上代码效果演示

    以上代码在 addMouseMove 中加入了判断是否处于形状内部的操作。

    var x = (this.targetX - this.offsetX) / this.scale;
    var y = (this.targetY - this.offsetY) / this.scale;
    
    this.activeShape = null
    
    this.data.forEach(item => {
      switch(item.type){
        case 'rect':
          this.isInnerRect(...item.data, x, y) && (this.activeShape = item)
          break;
        case 'circle':
          this.isInnerCircle(item.x, item.y, item.r, x, y) && (this.activeShape = item)
          break;
        case 'line':
          var lineNumber = item.data.length / 2 - 1
          var flag = false
          for(let i = 0; i < lineNumber; i++){
            let index = i*2;
            flag = this.isInnerPath(item.data[index], item.data[index+1], item.data[index+2], item.data[index+3], x, y, item.lineWidth || 1)
            if(flag){
              this.activeShape = item
              break;
            }
          }
      }
    })
    

    根据鼠标位置获取到基于原始缩放状态下距离画布原点的x,y 坐标,根据不同 type 调用不同方法判断是否处于当前形状中。

    然后根据是否处于形状内部判断注册 拖拽画布 还是 拖拽形状 的事件

    if(!this.activeShape){
      this.wrapDom.style.cursor = 'grabbing'
      this.El.addEventListener('mousemove', this.moveCanvasFunc, false)
    } else {
      this.wrapDom.style.cursor = 'all-scroll'
      this.shapedOldX = null
      this.shapedOldY = null
      this.El.addEventListener('mousemove', this.moveShapeFunc, false)
    }
    

    如果处于形状内部,就修改形状位置参数,并调用 this.render(),重新渲染画布

    // 移动形状
    moveShapeFunc = (e) => {
      var moveX = e.offsetX - (this.shapedOldX || this.targetX);
      var moveY = e.offsetY - (this.shapedOldY || this.targetY);
      
      moveX /= this.scale
      moveY /= this.scale
    
      switch(this.activeShape.type){
        case 'rect':
          let x = this.activeShape.data[0]
          let y = this.activeShape.data[1]
          let width = this.activeShape.data[2]
          let height = this.activeShape.data[3]
          this.activeShape.data = [x + moveX, y + moveY, width, height]
          break;
        case 'circle':
          this.activeShape.x += moveX
          this.activeShape.y += moveY
          break;
        case 'line':
          var item = this.activeShape;
          var lineNumber = item.data.length / 2
          for(let i = 0; i < lineNumber; i++){
            let index = i*2;
            item.data[index] += moveX
            item.data[index + 1] += moveY
          }
      }
      this.shapedOldX = e.offsetX
      this.shapedOldY = e.offsetY
    
      this.render()
    }
    

    移动形状同样也是要获取到基于原始缩放大小(可以看到上方除了this.scale)的画布的移动量 moveX,moveY,再将移动量增加至 选中形状的位置坐标中。

    保存好当前偏移量 this.shapedOldX,this.shapedOldY,供下次事件触发使用。

    判断是否处于形状内部方法解释

    1.判断是否处于矩形框内 根据当前计算出的 x,y 坐标,判断是否小于 矩形的x,y 坐标,并且判断是否大于矩形 (x + width)(y + height) 的右下角坐标。

    2.判断是否处于圆形内 根据当前计算出的 x,y 坐标,计算出距离圆心 坐标的距离,如果小于等于圆的半径,就说明处于圆形内部。

    3.判断是否处于线段中 假设线段 AB(线段粗为90),鼠标点击点为C,判断AC 或 BC 是否大于 AD,如果大于C,肯定不处于线段内,并且C与AB 的垂直距离CH必须小于等于 线段宽度的一半。

    使用canvas 如何绘制形状并支持拖拽、缩放功能

    这里只支持单个线段判断,多个连接线段判断不精确,连接处会有多余部分无法判断。 如下图:

    使用canvas 如何绘制形状并支持拖拽、缩放功能

    这是宽度为90的线段,红色区域上述方法能判断,箭头指向部分无法判断。

    这里暂时不考虑也是因为如果 线段之间的夹角小于 90deg,默认形状会是:

    使用canvas 如何绘制形状并支持拖拽、缩放功能

    可以看 miterLimit 属性 和 lineJoin 属性 以及 lineCap 属性,这些属性对线段影响较大,这里只做默认状态下单条线段判断演示。

    总结

    OK,以上就已经把最开始讲的需求做完了,有兴趣的朋友可以更改Demo 中的例子修改参数看看效果。

    以上如有问题或疏漏,欢迎指正,谢谢。


    起源地下载网 » 使用canvas 如何绘制形状并支持拖拽、缩放功能

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元