zoukankan      html  css  js  c++  java
  • 图片放大, 拖动的例子

    最近项目上搞一个图片缩放的功能, 闲来分享出来, 感兴趣的可以直接用;

    支持对图片自动居中;增加标记;

    支持缩放, 拖动; 

    图片来源可以是网络图片 , 也可以是本地选取的图片(--通过URL.createObjectURL(file) 方法创建一个链接即可)

    以下是效果图:

    相关源码:

      1 import {
      2   getDevicePixelRatio,
      3   returnFalse,
      4   normalize,
      5   rotate,
      6   isPointInBlock,
      7   image2Base64Data
      8 } from './utils'
      9 import { addEventListener, removeEventListener, defaultWheelDelta, isDomLevel2 } from './eventProxy'
     10 import EXIF from 'exif-js'
     11 
     12 /**
     13  * 一个支持拖动, 缩放的简易图片预览插件, 注册事件部分(on, off, dispatchEvent) 暂无此类需求不做实现;
     14  * 总体思路:
     15  *  把所有要绘制的物体如图片, 标记当作是一个个绘制对象, 将这一个个对象添加到一个场景中, 最后绘制这个场景;
     16  *    (扩展: 抽象来说, 这个场景可以设计一个独立的坐标系, 场景中的所有物体相对于这个坐标系定位, 设置尺寸;
     17  *          而如果有多个场景 场景之间互相独立, 可以对某一场景整体设置缩放, 平移; 又可以对另一场景整体设置旋转;
     18  *          svg的 g标签便是这样的一个例子;
     19  *      )
     20  *
     21  *  首先需要计算出图片和标记在场景中的定位和尺寸, 图片需要铺满并自动上下左右居中, 因此第一步先算出图片具体的缩放系数和尺寸, 再用这个缩放系数计算出标记的位置;
     22  *  由上, 完成了往场景中添加物体操作;
     23  *  再则在绘图时由于标记是位于图片之上的, 所以要先绘制图片, 再绘制标记(为了区分顺序, 在添加物体时增加了 zIndex属性);
     24  *  由上, 完成了绘制场景的操作;
     25  *
     26  *  在缩放和拖动时, 需要更新场景中物体的位置和大小, 再执行重绘
     27  *    在缩放操作时, 对场景中的所有物体应用缩放和偏移;
     28  *    在拖动操作时, 对场景中每一个物体应用偏移;
     29  *
     30  *  往场景中增加标记时, 根据标记对应的图片, 获取到图片对应的缩放系数和即时位置, 计算出标记的即时位置, 然后重绘;
     31  */
     32 export default class ImagePreview {
     33   constructor(selector, options = { images: [] }) {
     34     let char = 'abcdefghijklmnopqrstuvwxyz'
     35     this.selector = selector
     36     this.container = document.querySelector(selector)
     37     this.domId = char[(Math.random() * char.length) | 0] + Math.random() * 1409
     38     this.canvas = null
     39     this.canvas2dContext = null
     40     this.dpr = getDevicePixelRatio()
     41     this.width = 0
     42     this.height = 0
     43 
     44     this.scene = []
     45     this.images = options.images || []
     46     this.marks = []
     47     this.defaultScaleExtent = options.scaleExtent || [1, 10]
     48     this.scaleExtent = [].concat(this.defaultScaleExtent)
     49     this.markStyle = options.markStyle || { stroke: '#0D9EFB', fill: 'transparent', strokeWidth: 1 }
     50     this.state = 0
     51     this.lockState = 0
     52     this.uuid = 10000
     53     this.transform = {
     54       k: 1
     55     }
     56     // this.eventProxy = new EventProxy(selector)
     57     this.drag = {}
     58     this.scrollHandler = this.scrollHandler.bind(this)
     59     this.moveStart = this.moveStart.bind(this)
     60     this.moveEnd = this.moveEnd.bind(this)
     61     this.moving = this.moving.bind(this)
     62 
     63     if (options.images.length) {
     64       this.init(options.images)
     65     }
     66   }
     67 
     68   init(images) {
     69     var domRect = this.container.getBoundingClientRect()
     70     var canvas = document.createElement('canvas')
     71 
     72     canvas.width = domRect.width * this.dpr
     73     canvas.height = domRect.height * this.dpr
     74     canvas.id = this.domId
     75 
     76     var canvasStyle = canvas.style
     77     if (canvasStyle) {
     78       canvasStyle.onselectStart = returnFalse // 避免页面选中的尴尬
     79       canvasStyle['-webkit-user-select'] = 'none'
     80       canvasStyle['user-select'] = 'none'
     81       canvasStyle['-webkit-touch-callout'] = 'none'
     82       canvasStyle['-webkit-tap-highlight-color'] = 'rgba(0,0,0,0)'
     83       canvasStyle['margin'] = 0
     84       canvasStyle['padding'] = 0
     85       canvasStyle['border-width'] = 0
     86       canvasStyle['transform'] = `scale(${1 / this.dpr})`
     87       canvasStyle['transform-origin'] = `0px 0px`
     88     }
     89 
     90     this.canvas = canvas
     91     this.width = canvas.width
     92     this.height = canvas.height
     93     this.canvas2dContext = canvas.getContext('2d')
     94     this.container.appendChild(canvas)
     95 
     96     if (images && images.length) {
     97       this.initScene(images)
     98       this.initEvent()
     99     }
    100   }
    101 
    102   initScene(newImages) {
    103     var allElements
    104     if (newImages) {
    105       allElements = newImages || []
    106 
    107       this.scaleExtent = this.defaultScaleExtent
    108       // this.eventProxy = null
    109       this.drag = {}
    110       this.scene = []
    111       this.images = allElements
    112       this.resetTransform()
    113     } else {
    114       allElements = this.images || []
    115     }
    116 
    117     this.state = 0
    118     this.lockState = allElements.length
    119     this.marks = []
    120 
    121     var current
    122     for (var i = 0, len = allElements.length; i < len; i++) {
    123       current = allElements[i]
    124       if (current && current['url']) {
    125         //
    126         current['scaleFactor'] = {}
    127         current['pid'] = this.getUUID()
    128 
    129         // 将图片添加到场景中
    130         this.addImage(current, current['pid'], current.rotate || 0)
    131         // 如果有标记, 将标记也添加待绘制任务中
    132         if (current['shapes']) {
    133           var _this = this
    134           current.shapes.forEach(function(shape) {
    135             _this.marks.push({
    136               shape: shape,
    137               pid: current['pid'],
    138               scaleFactor: current['scaleFactor'],
    139               angle: current.rotate || 0
    140             })
    141           })
    142         }
    143       }
    144     }
    145   }
    146 
    147   getUUID() {
    148     return ++this.uuid
    149   }
    150 
    151   // 计算图片缩放系数, 并将图片上下左右居中;
    152   calcScaleFactor(
    153     output = {},
    154     targetRect = {  1, height: 1 },
    155     sourceRect = {  1, height: 1 }
    156   ) {
    157     var t_width = targetRect.width
    158     var t_height = targetRect.height
    159     var s_width = sourceRect.width
    160     var s_height = sourceRect.height
    161     var t_x, t_y, k_x, k_y, k_f
    162 
    163     // 如果图片相对较小, 不放大, 只居中;如果图片相对较大, 先缩小再居中
    164     if (s_width > t_width || s_height > t_height) {
    165       k_x = t_width / s_width
    166       k_y = t_height / s_height
    167       k_f = k_x > k_y ? k_y : k_x
    168 
    169       t_x = (t_width - s_width * k_f) / 2
    170       t_y = (t_height - s_height * k_f) / 2
    171 
    172       output.k = k_f
    173       output.width = s_width * k_f
    174       output.height = s_height * k_f
    175       output.rawWidth = s_width
    176       output.rawHeight = s_height
    177       output.tx = t_x
    178       output.ty = t_y
    179     } else {
    180       k_f = 1
    181       t_x = (t_width - s_width) / 2
    182       t_y = (t_height - s_height) / 2
    183 
    184       output.k = k_f
    185       output.width = s_width
    186       output.height = s_height
    187       output.rawWidth = s_width
    188       output.rawHeight = s_height
    189       output.tx = t_x
    190       output.ty = t_y
    191     }
    192   }
    193 
    194   // 辅助方法: 纠正到原图;
    195   getExifOrientation(img) {
    196     return new Promise(function(resolve, reject) {
    197       EXIF.getData(img, function() {
    198         try {
    199           // 借助 EXIF 读取图片元信息, 获取 Orientation:图片方向
    200           var orient = EXIF.getTag(this, 'Orientation')
    201           // console.log(EXIF.getAllTags(this))
    202           switch (orient) {
    203             // 不旋转
    204             case 1:
    205               resolve(0)
    206               break
    207             // 旋转 -90度
    208             case 6:
    209               resolve(90)
    210               break
    211             // 旋转 90度
    212             case 8:
    213               resolve(-90)
    214               break
    215             // 旋转 180度
    216             case 3:
    217               resolve(-180)
    218               break
    219             default:
    220               resolve(0)
    221               break
    222           }
    223         } catch (e) {
    224           resolve(0)
    225         }
    226       })
    227     })
    228   }
    229 
    230   /**
    231    * imageInfo: {
    232    *              url: 图片地址,
    233    *            }
    234    */
    235   addImage(imageInfo = {}, pid, angle) {
    236     // width, height 自定义的下载图片尺寸;
    237     // url 图片地址
    238     // image Image() 对象的实例; 可以直接拿来绘制不需要下载
    239     var { url, image, width, height, doNotRotate } = imageInfo
    240     var _this = this
    241     var img
    242 
    243     if (!image) {
    244       img = new Image()
    245       img.crossOrigin = 'anonymous'
    246       img.src = url
    247       img.onload = function(e) {
    248         width = width || this.width
    249         height = height || this.height
    250 
    251         if (doNotRotate) {
    252           _this.addImageToScene(img, imageInfo)
    253           return
    254         }
    255         // 由于 .jpg格式的图片会有自动旋转的 bug, 需要对图片纠正到原图的方向, PNG格式的图片暂未验证
    256         // 然后再依据 angle(也即 Ocr给出的旋转角度) 对图片再次旋转;
    257         _this.getExifOrientation(img).then(function(rotate) {
    258           // 无需旋转, 图片正常
    259           if (rotate === 0) {
    260             if (angle) {
    261               // 存储 base64格式的图片数据;
    262               var base64Switch = image2Base64Data(img, {
    263                 width,
    264                 height,
    265                 rotate: angle
    266               })
    267               var newImg = new Image()
    268               newImg.src = base64Switch.result
    269               newImg.width = base64Switch.width
    270               newImg.height = base64Switch.height
    271               newImg.onload = function() {
    272                 _this.addImageToScene(newImg, imageInfo)
    273               }
    274             } else {
    275               _this.addImageToScene(img, imageInfo)
    276             }
    277           } else {
    278             // 将图片旋转至原图, 并存储 base64格式的图片数据;
    279             var base64Switch = image2Base64Data(img, {
    280               width,
    281               height,
    282               rotate: rotate
    283             })
    284 
    285             // 得到纠正到正确角度的原图
    286             var newImg = new Image()
    287             newImg.src = base64Switch.result
    288             newImg.width = base64Switch.width
    289             newImg.height = base64Switch.height
    290             newImg.onload = function() {
    291               // 如果 ocr 识别结果需要对图片应用旋转
    292               if (angle) {
    293                 var finalBase64Image = image2Base64Data(newImg, {
    294                    newImg.width,
    295                   height: newImg.height,
    296                   rotate: angle
    297                 })
    298 
    299                 var finalImg = new Image()
    300                 finalImg.src = finalBase64Image.result
    301                 finalImg.width = finalBase64Image.width
    302                 finalImg.height = finalBase64Image.height
    303                 finalImg.onload = function() {
    304                   _this.addImageToScene(finalImg, imageInfo)
    305                 }
    306               } else {
    307                 _this.addImageToScene(newImg, imageInfo)
    308               }
    309             }
    310           }
    311         })
    312       }
    313 
    314       img.onerror = function(e) {
    315         _this.updateState()
    316       }
    317     } else {
    318       // 取消对 Image实例的支持;
    319     }
    320   }
    321 
    322   rotateImage(pid, rotate) {
    323     var sourceImg, imageInfo, current, cur
    324     for (var i = 0; i < this.scene.length; i++) {
    325       cur = this.scene[i]
    326       if ((cur.type === 'image' && cur.pid === pid) || cur.url === pid) {
    327         sourceImg = cur.image
    328         break
    329       }
    330     }
    331 
    332     for (var k = 0; k < this.images.length; i++) {
    333       current = this.images[k]
    334       if (current.url === pid || current.pid === pid) {
    335         imageInfo = current
    336         break
    337       }
    338     }
    339 
    340     // 对图片进行旋转, 并重新计算图片的缩放系数: this.image[i].scaleFactor
    341     // 并且清除场景中的图片和标记, 将图片添加到场景中
    342     if (sourceImg && imageInfo && rotate % 360 !== 0) {
    343       var base64Switch = image2Base64Data(sourceImg, {
    344          sourceImg.width,
    345         height: sourceImg.height,
    346         rotate: rotate
    347       })
    348 
    349       var newImg = new Image()
    350       var _this = this
    351       newImg.src = base64Switch.result
    352       newImg.width = base64Switch.width
    353       newImg.height = base64Switch.height
    354       newImg.onload = function() {
    355         //
    356         _this.state = 0
    357         _this.resetTransform()
    358         _this.clearScene()
    359         // 重绘:::
    360         _this.addImageToScene(newImg, imageInfo)
    361       }
    362     }
    363   }
    364 
    365   //
    366   addImageToScene(img, imageInfo) {
    367     // 初始化缩放系数, 使图片自动铺满画布并上下左右居中
    368     this.calcScaleFactor(
    369       imageInfo['scaleFactor'],
    370       {  this.width, height: this.height },
    371       {  img.width, height: img.height }
    372     )
    373     // console.log('图片下载完毕:::', width, height)
    374     // console.log('准备计算缩放系数::', _this.width, _this.height)
    375 
    376     // 获取到缩放系数
    377     var scaleFactor = imageInfo.scaleFactor
    378     var pid = imageInfo.pid
    379 
    380     this.scene.push({
    381       type: 'image',
    382       actions: null,
    383       image: img,
    384       pid: pid,
    385       rotate: rotate,
    386       shape: {
    387         x: scaleFactor.tx,
    388         y: scaleFactor.ty,
    389          scaleFactor.width,
    390         height: scaleFactor.height
    391       },
    392       rawShape: {
    393         x: scaleFactor.tx,
    394         y: scaleFactor.ty,
    395          scaleFactor.width,
    396         height: scaleFactor.height
    397       },
    398       zIndex: 1,
    399       uid: this.getUUID()
    400     })
    401 
    402     this.updateState()
    403   }
    404 
    405   /**
    406    * shape: {
    407    *    type: "rect", 形状,矩形;
    408    *    x: 100, 形状相对原图 x方向坐标
    409    *    y: 100, 形状相对原图 y方向坐标
    410    *     100, 形状本身的宽度
    411    *    height: 100 形状本身的高度
    412    * },
    413    * scaleFactor: {
    414    *    k: 缩放系数,
    415    *     缩放后的宽度,
    416    *    height: 缩放后的高度,
    417    *    rawWidth: 原始宽度,
    418    *    rawHeight: 原始高度
    419    *    tx: x方向的偏移,
    420    *    ty: y方向的偏移
    421    * }
    422    */
    423   addShape(shape, scaleFactor, pid) {
    424     var k = scaleFactor.k
    425     var x = scaleFactor.tx + shape.x * k
    426     var y = scaleFactor.ty + shape.y * k
    427     var width = Math.round(shape.width * k)
    428     var height = Math.round(shape.height * k)
    429 
    430     this.scene.push({
    431       type: 'rect',
    432       actions: null,
    433       pid: pid,
    434       origin: {
    435         ...shape
    436       },
    437       shape: {
    438         x: x,
    439         y: y,
    440          width,
    441         height: height
    442       },
    443       rawShape: {
    444         x: x,
    445         y: y,
    446          width,
    447         height: height
    448       },
    449       zIndex: 2,
    450       uid: this.getUUID()
    451     })
    452   }
    453 
    454   updateState() {
    455     this.state++
    456     // 等待所有图片下载完成, 将标记也添加到场景
    457     if (this.state === this.lockState) {
    458       // 将标记添加到场景中
    459       var allElements = this.marks,
    460         current
    461 
    462       for (var i = 0, len = allElements.length; i < len; i++) {
    463         current = allElements[i]
    464         this.addShape(current.shape, current.scaleFactor, current['pid'])
    465       }
    466 
    467       this.renderPass()
    468     }
    469   }
    470 
    471   /** 由于 canvas是有状态的, 设置状态后后面的绘制会一直生效, 可以利用这一点对做性能优化;
    472    *    在绘图时, 要有效利用这一机制, 避免频繁的状态切换; 在具体绘制图形时, 对图形进行分组, 比如:
    473    *    一个堆叠柱状图, 有N多的柱形(分3类: 从深蓝->浅蓝, 从棕色->浅黄, 从亮蓝->天蓝, 并分别有不同线框), 如果一个个绘制会频繁的设置渐变和描边属性, 可以对其先分类成
    474    *      深蓝 -> 浅蓝, 框 1
    475    *      棕色 -> 浅黄, 框 2
    476    *      亮蓝 -> 天蓝, 框 3
    477    *    这三组图形, 然后只需设置三次描边属性, 创建三个渐变即可;
    478    */
    479   renderPass() {
    480     this.scene.sort(function(pre, next) {
    481       return pre.zIndex > next.zIndex ? 1 : -1
    482     })
    483 
    484     // var group = { index: 0 }
    485     // var initZindex = 0
    486     // this.scene.forEach(function(shape) {
    487     //   if (shape.zIndex > initZindex) {
    488     //     initZindex = shape.zIndex
    489 
    490     //     group.index++
    491     //     group[group.index] = {}
    492     //   }
    493 
    494     //   if (group[group.index][shape.type]) {
    495     //     group[group.index][shape.type].push(shape)
    496     //   } else {
    497     //     group[group.index][shape.type] = [shape]
    498     //   }
    499     // })
    500     this.clearRect(0, 0, this.width, this.height)
    501 
    502     if (this.scene.length) {
    503       var style = this.markStyle
    504       this.canvas2dContext.strokeStyle = style.stroke
    505       this.canvas2dContext.lineWidth = style.strokeWidth
    506       this.canvas2dContext.fillStyle = style.fill
    507     }
    508 
    509     var current
    510     for (var i = 0, len = this.scene.length; i < len; i++) {
    511       current = this.scene[i]
    512       switch (current.type) {
    513         case 'rect':
    514           current = current.shape
    515           this.canvas2dContext.beginPath()
    516           this.canvas2dContext.rect(current.x, current.y, current.width, current.height)
    517           this.canvas2dContext.stroke()
    518           this.canvas2dContext.fill()
    519           this.canvas2dContext.closePath()
    520           break
    521         case 'image':
    522           this.canvas2dContext.drawImage(
    523             current.image,
    524             current.shape.x,
    525             current.shape.y,
    526             current.shape.width,
    527             current.shape.height
    528           )
    529           break
    530         default:
    531           break
    532       }
    533     }
    534   }
    535 
    536   // 事件初始化
    537   initEvent() {
    538     var support =
    539       'onwheel' in document.createElement('div')
    540         ? 'wheel' // 各个厂商的高版本浏览器都支持"wheel"
    541         : document.onmousewheel !== undefined
    542         ? 'mousewheel' // Webkit 和 IE一定支持"mousewheel"
    543         : 'DOMMouseScroll' // 低版本firefox
    544     addEventListener(this.canvas, support, this.scrollHandler)
    545     addEventListener(this.canvas, 'mousedown', this.moveStart)
    546   }
    547 
    548   // 设置光标样式: 预留代码,暂无此需求;
    549   setCursorStyle(style, isDocument) {
    550     if (style) {
    551       if (isDocument) {
    552         document.body.style.cursor = style
    553       } else {
    554         this.canvas.style.cursor = style
    555       }
    556     }
    557   }
    558 
    559   moveStart(e) {
    560     this.drag.lastX = e.x
    561     this.drag.lastY = e.y
    562     this.drag.isDrag = true
    563     this.setCursorStyle('move')
    564     addEventListener(document.body, 'mousemove', this.moving)
    565     addEventListener(this.canvas, 'mouseup', this.moveEnd)
    566     addEventListener(document.body, 'mouseup', this.moveEnd)
    567   }
    568   moveEnd() {
    569     this.drag.isDrag = false
    570     this.setCursorStyle('default')
    571     removeEventListener(document.body, 'mousemove', this.moving)
    572     removeEventListener(this.canvas, 'mouseup', this.moveEnd)
    573     removeEventListener(document.body, 'mouseup', this.moveEnd)
    574   }
    575 
    576   // 拖动
    577   moving(e) {
    578     var _this = this
    579     requestAnimationFrame(function() {
    580       var moveX = e.x - _this.drag.lastX
    581       var moveY = e.y - _this.drag.lastY
    582       var speed = _this.dpr
    583       // 对场景中的所有物体进行平移
    584       _this.scene.forEach(function(item) {
    585         _this.translateRect(item.shape, item.shape, { x: moveX * speed, y: moveY * speed })
    586       })
    587 
    588       _this.renderPass()
    589 
    590       _this.drag.lastX = e.x
    591       _this.drag.lastY = e.y
    592 
    593       _this = null
    594     })
    595   }
    596 
    597   // 对整个画布缩放
    598   zoom(isZoomMax, zoomStep = 0.5) {
    599     var _this = this
    600     var step = isZoomMax ? zoomStep : -zoomStep
    601     var lastK = _this.transform.k
    602     var k, offsetDis
    603 
    604     _this.transform.k += step
    605     _this.transform.k = normalize(_this.transform.k, _this.scaleExtent)
    606 
    607     // 简化实现, 直接应用画布中心点进行缩放
    608     var imgViewCenterToCanvas = { x: this.width / 2, y: this.height / 2 }
    609 
    610     k = _this.transform.k / lastK
    611     offsetDis = _this.calcOffset(imgViewCenterToCanvas, k)
    612 
    613     // 先缩放, 再平移
    614     _this.scene.forEach(function(item) {
    615       _this.scaleRect(item.shape, item.shape, k)
    616       _this.translateRect(item.shape, item.shape, {
    617         x: -offsetDis.x,
    618         y: -offsetDis.y
    619       })
    620     })
    621 
    622     _this.renderPass()
    623   }
    624 
    625   // 计算多个矩形的上下边界
    626   calcBounding(rects) {
    627     var i = 0,
    628       len = rects.length,
    629       left,
    630       right,
    631       top,
    632       bottom,
    633       cur
    634     for (; i < len; i++) {
    635       cur = rects[i]
    636       left = cur.x > left ? left : cur.x
    637       right = cur.x + cur.width < right ? right : cur.x + cur.width
    638       top = cur.y > top ? top : cur.y
    639       bottom = cur.y + cur.height < bottom ? bottom : cur.y + cur.height
    640     }
    641     return len ? { x: left, y: top,  right - left, height: bottom - top } : null
    642   }
    643 
    644   // 将图片局部位置缩放至中心
    645   zoomToCenter(block) {
    646     var halfW = this.width * 0.5,
    647       halfH = this.height * 0.5,
    648       k,
    649       kw,
    650       kh,
    651       cur,
    652       lastK,
    653       targetK
    654 
    655     // 块的中心点坐标
    656     var pCenter = { x: block.x + block.width / 2, y: block.y + block.height / 2 }
    657     lastK = this.transform.k
    658 
    659     // (block.width / lastK) 块的实际大小, (this.width * 0.6) 缩放到的位置; 以此计算出块放大的倍率
    660     kw = (this.width * 0.4) / (block.width / lastK)
    661     kh = (this.height * 0.4) / (block.height / lastK)
    662 
    663     targetK = Math.min(kw, kh)
    664     targetK = normalize(targetK, this.scaleExtent)
    665     // 计算出实时放大倍率
    666     k = targetK / lastK
    667 
    668     // 方式一: 应用缩放和平移
    669     this.transform.k = targetK
    670     for (var i = 0; i < this.scene.length; i++) {
    671       cur = this.scene[i]
    672 
    673       this.scaleRect(cur.shape, cur.shape, k)
    674       this.translateRect(cur.shape, cur.shape, {
    675         x: -pCenter.x * k + halfW,
    676         y: -pCenter.y * k + halfH
    677       })
    678     }
    679 
    680     // 方式二: 仅应用平移
    681     // for (var i = 0; i < this.scene.length; i++) {
    682     //   cur = this.scene[i]
    683     //   this.translateRect(cur.shape, cur.shape, {
    684     //     x: -pCenter.x + halfW,
    685     //     y: -pCenter.y + halfH
    686     //   })
    687     // }
    688 
    689     this.renderPass()
    690   }
    691 
    692   // 缩放
    693   scrollHandler(e) {
    694     var _this = this
    695     requestAnimationFrame(function() {
    696       var eventToCanvas, canvasBoundingRect
    697       // 如果支持 offsetX, 且为有效的值;
    698       if (e.offsetX) {
    699         eventToCanvas = { x: e.offsetX, y: e.offsetY }
    700       } else {
    701         canvasBoundingRect = _this.canvas.getBoundingClientRect()
    702         eventToCanvas = { x: e.x - canvasBoundingRect.x, y: e.y - canvasBoundingRect.y }
    703       }
    704       var offsetDis, k
    705       var lastK = _this.transform.k
    706 
    707       _this.transform.k += defaultWheelDelta(e)
    708       _this.transform.k = normalize(_this.transform.k, _this.scaleExtent)
    709 
    710       k = _this.transform.k / lastK
    711       offsetDis = _this.calcOffset(eventToCanvas, k)
    712 
    713       // 对场景中所有物体进行缩放及平移
    714       _this.scene.forEach(function(item) {
    715         // 备选: 只对鼠标"选中"的物体进行缩放及平移
    716         // if (isPointInBlock(item.shape, eventToCanvas)) {
    717         // 先缩放, 再平移
    718         _this.scaleRect(item.shape, item.shape, k)
    719         _this.translateRect(item.shape, item.shape, {
    720           x: -offsetDis.x,
    721           y: -offsetDis.y
    722         })
    723       })
    724 
    725       _this.renderPass()
    726       _this = null
    727     })
    728 
    729     if (isDomLevel2()) {
    730       e.preventDefault()
    731       e.stopPropagation()
    732     } else {
    733       e.returnValue = false
    734       e.cancelBubble = true
    735     }
    736 
    737     return false
    738   }
    739 
    740   // 计算缩放偏移的距离
    741   calcOffset(point, scale) {
    742     var x = point.x * scale - point.x
    743     var y = point.y * scale - point.y
    744     return {
    745       x: x,
    746       y: y
    747     }
    748   }
    749 
    750   subVector2(output, vector1, vector2) {
    751     output.x = vector1.x - vector2.x
    752     output.y = vector1.y - vector2.y
    753   }
    754 
    755   translateRect(output, block, translate) {
    756     output.x = block.x + translate.x
    757     output.y = block.y + translate.y
    758   }
    759 
    760   scaleRect(output, block, scale) {
    761     output.width = block.width * scale // Math.round(block.width * scale)
    762     output.height = block.height * scale // Math.round(block.height * scale)
    763     output.x = block.x * scale // Math.round(block.x * scale)
    764     output.y = block.y * scale // Math.round(block.y * scale)
    765   }
    766 
    767   /**
    768    * 一个矩形绕点旋转后的形状
    769    */
    770   rotateRect(output, block, rotatePoint, angel) {
    771     var ltp = { x: block.x, y: block.y }
    772     var rtp = { x: block.x + block.width, y: block.y }
    773     var lbp = { x: block.x, y: block.y + block.height }
    774     var rbp = { x: block.x + block.width, y: block.y + block.height }
    775 
    776     output.lt = rotate(ltp, rotatePoint, angel)
    777     output.rt = rotate(rtp, rotatePoint, angel)
    778     output.lb = rotate(lbp, rotatePoint, angel)
    779     output.rb = rotate(rbp, rotatePoint, angel)
    780   }
    781 
    782   clearRect(x, y, width, height) {
    783     this.canvas2dContext.clearRect(x, y, width, height)
    784   }
    785 
    786   clearAllEvent() {
    787     // 清除挂载的DOM事件
    788     // from: https://developer.mozilla.org/zh-CN/docs/Web/API/Element/wheel_event
    789     var support =
    790       'onwheel' in document.createElement('div')
    791         ? 'wheel' // 各个厂商的高版本浏览器都支持"wheel"
    792         : document.onmousewheel !== undefined
    793         ? 'mousewheel' // Webkit 和 IE一定支持"mousewheel"
    794         : 'DOMMouseScroll' // 低版本firefox
    795     removeEventListener(this.canvas, support, this.scrollHandler)
    796     removeEventListener(this.canvas, 'mousedown', this.moveStart)
    797 
    798     removeEventListener(document.body, 'mousemove', this.moving)
    799     removeEventListener(this.canvas, 'mouseup', this.moveEnd)
    800     removeEventListener(document.body, 'mouseup', this.moveEnd)
    801 
    802     // 清除挂载的自定义事件
    803     // this.eventProxy.disposeAll()
    804   }
    805 
    806   destroy() {
    807     // 清除事件
    808     this.clearAllEvent()
    809     // 清除DOM
    810     this.container.removeChild(this.canvas)
    811 
    812     // 清除属性
    813     for (var key in this) {
    814       delete this[key]
    815     }
    816   }
    817 
    818   on(eventName, callbacl) {}
    819   off(eventName, callback) {}
    820   dispatchEvent(eventName) {}
    821 
    822   // 重置所有形状回到初始位置, 重置缩放
    823   reset() {
    824     this.scene.forEach(function(item) {
    825       item.shape.x = item.rawShape.x
    826       item.shape.y = item.rawShape.y
    827       item.shape.width = item.rawShape.width
    828       item.shape.height = item.rawShape.height
    829     })
    830     this.resetTransform()
    831     this.renderPass()
    832   }
    833 
    834   resetTransform() {
    835     this.transform.k = 1
    836   }
    837 
    838   clear() {
    839     this.scene.length = 0
    840     this.images = []
    841     this.scaleExtent = this.defaultScaleExtent
    842     this.state = 0
    843     this.lockState = 0
    844     // this.eventProxy = null
    845     this.drag = {}
    846 
    847     this.resetTransform()
    848     this.clearAllEvent()
    849     this.clearRect(0, 0, this.width, this.height)
    850   }
    851   clearScene() {
    852     this.scene.length = 0
    853     this.marks.length = []
    854   }
    855   relayout() {
    856     this.resetTransform()
    857     var domRect = this.container.getBoundingClientRect()
    858 
    859     this.width = domRect.width * this.dpr
    860     this.height = domRect.height * this.dpr
    861     this.canvas.width = domRect.width * this.dpr
    862     this.canvas.height = domRect.height * this.dpr
    863 
    864     if (this.images && this.images.length) {
    865       // 偷了个懒... 这里重新计算图片的位置, 标记的位置; 然后重新渲染场景即可
    866       this.clearScene()
    867       this.initScene()
    868     }
    869   }
    870 
    871   removeImage(imgId) {
    872     if (!imgId) {
    873       this.images.length = 0
    874       this.scene.length = 0
    875 
    876       this.renderPass()
    877       return
    878     }
    879 
    880     var uid, current
    881 
    882     for (var i = this.images.length - 1; i >= 0; i--) {
    883       current = this.images[i]
    884       if (current.pid === imgId || current.url === imgId) {
    885         uid = current.uid
    886         this.images.splice(i, 1)
    887       }
    888     }
    889 
    890     if (uid) {
    891       for (var k = this.scene.length - 1; k >= 0; k--) {
    892         if (this.scene[k].pid === uid) {
    893           this.scene.splice(k, 1)
    894         }
    895       }
    896     }
    897 
    898     this.renderPass()
    899   }
    900 
    901   removeShape(imgId) {
    902     if (!imgId) {
    903       for (var i = 0; i < this.images.length; i++) {
    904         this.images[i].shapes = []
    905       }
    906 
    907       for (var i = this.scene.length - 1; i >= 0; i--) {
    908         if (this.scene[i].type === 'rect') {
    909           this.scene.splice(i, 1)
    910         }
    911       }
    912 
    913       this.renderPass()
    914       return
    915     }
    916 
    917     var uid, current
    918     for (var i = 0; i < this.images.length; i++) {
    919       current = this.images[i]
    920       if (current.pid === imgId || current.url === imgId) {
    921         uid = current.uid
    922         current.shapes = []
    923       }
    924     }
    925 
    926     for (var k = this.scene.length - 1; k >= 0; k--) {
    927       if (this.scene[k].type === 'rect' && this.scene[k].pid === uid) {
    928         this.scene.splice(k, 1)
    929       }
    930     }
    931 
    932     this.renderPass()
    933   }
    934 
    935   // 往场景中添加块并重绘
    936   addShapesToScene(imgId, shapes, autoFocus) {
    937     var allElements = this.images,
    938       addedShapes = [],
    939       target,
    940       _this,
    941       k,
    942       pid,
    943       scaleFactor,
    944       img,
    945       bounding
    946 
    947     for (var i = 0, len = allElements.length; i < len; i++) {
    948       if (allElements[i].pid === imgId || allElements[i].url === imgId) {
    949         target = allElements[i]
    950         break
    951       }
    952     }
    953     if (target) {
    954       target.shapes = target.shapes || []
    955       target.shapes = target.shapes.concat(shapes)
    956       pid = target.pid
    957       scaleFactor = target.scaleFactor
    958 
    959       _this = this
    960       k = _this.transform.k
    961       img = _this.scene.find(function(v) {
    962         return v.type === 'image' || v.pid === pid
    963       })
    964 
    965       shapes.forEach(function(shape) {
    966         // 将块添加到场景中
    967         _this.addShape(shape, target['scaleFactor'], target['pid'])
    968         // 获取刚刚加入的块
    969         var _lastShape = _this.scene[_this.scene.length - 1]
    970 
    971         // 计算物体绝对位置, 因为图可能会有缩放, 拖动等;
    972         _lastShape.shape.x = img.shape.x + _lastShape.origin.x * scaleFactor.k * k
    973         _lastShape.shape.y = img.shape.y + _lastShape.origin.y * scaleFactor.k * k
    974         _lastShape.shape.width *= k
    975         _lastShape.shape.height *= k
    976 
    977         if (autoFocus) {
    978           addedShapes.push(_lastShape.shape)
    979         }
    980       })
    981 
    982       _this.renderPass()
    983       // 对选中的位置进行缩放并居中;
    984       if (autoFocus) {
    985         // 如果是一次加入了多个块, 计算出多个块组成的区块边界; 以此为放大区域
    986         bounding = this.calcBounding(addedShapes)
    987 
    988         // 对块放大至画布中心
    989         this.zoomToCenter(bounding)
    990       }
    991     }
    992   }
    993 }
    View Code

      

     ./utils.js

      1 function getDevicePixelRatio() {
      2   var divicePixelRatio = 1
      3   if (typeof window !== 'undefined') {
      4     divicePixelRatio = window.devicePixelRatio || 1
      5     divicePixelRatio = Math.max(divicePixelRatio, 1)
      6   }
      7   return divicePixelRatio
      8 }
      9 
     10 function returnFalse() {
     11   return false
     12 }
     13 
     14 // 以 cornerX, cornerY 为 x,y; 输出一个 宽度为 width, 高度为 height, 圆角大小为 cornerRadius 的圆角矩形路径
     15 function roundedRect(canvas2dContext, cornerX, cornerY, width, height, cornerRadius) {
     16   if (width > 0) {
     17     canvas2dContext.moveTo(cornerX + cornerRadius, cornerY)
     18   } else {
     19     canvas2dContext.moveTo(cornerX - cornerRadius, cornerY)
     20   }
     21 
     22   canvas2dContext.arcTo(cornerX + width, cornerY, cornerX + width, cornerY + height, cornerRadius)
     23   canvas2dContext.arcTo(cornerX + width, cornerY + height, cornerX, cornerY + height, cornerRadius)
     24   canvas2dContext.arcTo(cornerX, cornerY + height, cornerX, cornerY, cornerRadius)
     25   if (width > 0) {
     26     canvas2dContext.arcTo(cornerX, cornerY, cornerX + cornerRadius, cornerY, cornerRadius)
     27   } else {
     28     canvas2dContext.arcTo(cornerX, cornerY, cornerX - cornerRadius, cornerY, cornerRadius)
     29   }
     30 }
     31 
     32 /**
     33  * 绘制一个支持渐变, 支持圆角的矩形; 仅支持 canvas.2d 绘图对象
     34  *
     35  */
     36 function fillRect(
     37   canvas2dContext,
     38   x,
     39   y,
     40   width,
     41   height,
     42   background,
     43   border,
     44   borderRadius,
     45   boxShadow,
     46   isSave
     47 ) {
     48   if (isSave) {
     49     canvas2dContext.save()
     50   }
     51   canvas2dContext.beginPath()
     52   var gradient = null
     53   // 渐变
     54   if (background && background.indexOf(':') !== -1) {
     55     var gradientText = background.split(':'),
     56       colors = [],
     57       direction = null
     58 
     59     try {
     60       gradientText.forEach((t, index) => {
     61         var tx = ''
     62         if (index === 0) {
     63           direction = t.split(' ')[1].trim()
     64         } else {
     65           tx = t.trim()
     66           var start = tx.match(/^[0-9.%]+/)[0]
     67           var color = tx.slice(start.length)
     68 
     69           start = start[start.length - 1] === '%' ? parseFloat(start) / 100 : +start
     70           colors.push({
     71             rate: start,
     72             color: color.trim()
     73           })
     74         }
     75       })
     76     } catch (e) {
     77       console.warn('绘制矩形错误 ', e)
     78     }
     79 
     80     switch (direction) {
     81       case 'top':
     82         gradient = canvas2dContext.createLinearGradient(x, y + height, x, y)
     83         break
     84       case 'bottom':
     85         gradient = canvas2dContext.createLinearGradient(x, y, x, y + height)
     86         break
     87       case 'left':
     88         gradient = canvas2dContext.createLinearGradient(x + width, y, x, y)
     89         break
     90       case 'right':
     91         gradient = canvas2dContext.createLinearGradient(x, y, x + width, y)
     92         break
     93       case 'topLeft':
     94         gradient = canvas2dContext.createLinearGradient(x + width, y + height, x, y)
     95         break
     96       case 'topRight':
     97         gradient = canvas2dContext.createLinearGradient(x, y + height, x + width, y)
     98         break
     99       case 'bottomLeft':
    100         gradient = canvas2dContext.createLinearGradient(x + width, x, y, y + height)
    101         break
    102       case 'bottomRight':
    103         gradient = canvas2dContext.createLinearGradient(x, y, x + width, y + height)
    104         break
    105       default:
    106         break
    107     }
    108 
    109     if (direction && gradient) {
    110       colors.forEach(color => {
    111         gradient.addColorStop(+color.rate, color.color)
    112       })
    113 
    114       canvas2dContext.fillStyle = gradient
    115     }
    116   }
    117   // 纯色
    118   else {
    119     canvas2dContext.fillStyle = background || '#000'
    120   }
    121 
    122   // 圆角
    123   if (borderRadius) {
    124     roundedRect(canvas2dContext, x, y, width, height, borderRadius)
    125   } else {
    126     canvas2dContext.rect(x, y, width, height)
    127   }
    128 
    129   if (boxShadow) {
    130     boxShadow = boxShadow.split(':')
    131     canvas2dContext.shadowColor = boxShadow[0]
    132     canvas2dContext.shadowBlur = boxShadow[1] || 0
    133     canvas2dContext.shadowOffsetX = boxShadow[2] || 0
    134     canvas2dContext.shadowOffsetY = boxShadow[3] || 0
    135   }
    136   canvas2dContext.fill()
    137   if (border && border.borderWidth) {
    138     canvas2dContext.strokeWidth = border.borderWidth
    139     canvas2dContext.strokeStyle = border.color
    140     canvas2dContext.stroke()
    141   }
    142 
    143   canvas2dContext.closePath()
    144   if (isSave) {
    145     canvas2dContext.restore()
    146   }
    147 }
    148 
    149 function normalize(value, range) {
    150   return value >= range[1] ? range[1] : value <= range[0] ? range[0] : value
    151 }
    152 
    153 // 判断点是否在某一个矩形方块内:
    154 // block: x, y, width, height; point: x, y;
    155 function isPointInBlock(block, point) {
    156   return (
    157     block.x <= point.x &&
    158     block.x + block.width >= point.x &&
    159     block.y <= point.y &&
    160     block.y + block.height >= point.y
    161   )
    162 }
    163 
    164 // 公式:
    165 // x0= (x - rx0)*cos(a) - (y - ry0)*sin(a) + rx0 ;
    166 // y0= (x - rx0)*sin(a) + (y - ry0)*cos(a) + ry0 ;
    167 // 点 p绕着 p0旋转 angle度; angle为正时, 表示逆时针旋转, angle为负时, 表示顺时针旋转
    168 function rotate(p, p0, angle) {
    169   var rad = (Math.PI / 180) * angle
    170   var sinR = Math.sin(rad)
    171   var cosR = Math.cos(rad)
    172   return {
    173     x: p0.x + (p.x - p0.x) * cosR - (p.y - p0.y) * sinR,
    174     y: p0.y + (p.x - p0.x) * sinR + (p.y - p0.y) * cosR
    175   }
    176 }
    177 
    178 function image2Base64Data(img, option = {}) {
    179   // width 图片本身的原始宽度
    180   // height 图片本身的原始高度
    181   var { width = 0, height = 0, rotate = 0 } = option
    182   var canvas = document.createElement('canvas')
    183   var context = canvas.getContext('2d')
    184   var dataURL, rotatedWidth, rotatedHeight, sinReg, cosReg
    185 
    186   canvas.width = width
    187   canvas.height = height
    188 
    189   if (rotate) {
    190     // 任意角度的旋转
    191     // _w = w * cos? + h * sin?
    192     // _h = w * sin? + h * cos?
    193 
    194     sinReg = Math.sin((rotate * Math.PI) / 180)
    195     cosReg = Math.cos((rotate * Math.PI) / 180)
    196     canvas.width = Math.abs(width * cosReg) + Math.abs(height * sinReg)
    197     canvas.height = Math.abs(width * sinReg) + Math.abs(height * cosReg)
    198     context.translate(canvas.width / 2, canvas.height / 2)
    199     context.rotate((rotate * Math.PI) / 180)
    200 
    201     context.drawImage(img, -width / 2, -height / 2, width, height)
    202   } else {
    203     context.drawImage(img, 0, 0, width, height)
    204   }
    205 
    206   rotatedWidth = canvas.width
    207   rotatedHeight = canvas.height
    208   dataURL = canvas.toDataURL()
    209   canvas = null
    210 
    211   return {
    212     result: dataURL,
    213      rotatedWidth,
    214     height: rotatedHeight
    215   }
    216 }
    217 
    218 export {
    219   getDevicePixelRatio,
    220   returnFalse,
    221   roundedRect,
    222   fillRect,
    223   normalize,
    224   isPointInBlock,
    225   rotate,
    226   image2Base64Data
    227 }
    View Code

      

    ./eventProxy.js

      1 function addEventListener(el, name, handler) {
      2   if (el.addEventListener) {
      3     el.addEventListener(name, handler)
      4   } else if (el.attachEvent) {
      5     el.attachEvent('on' + name, handler)
      6   } else {
      7     el['on' + name] = handler
      8   }
      9 }
     10 
     11 function removeEventListener(el, name, handler) {
     12   if (el.removeEventListener) {
     13     el.removeEventListener(name, handler)
     14   } else if (el.detachEvent) {
     15     el.detachEvent('on' + name, handler)
     16   } else {
     17     el['on' + name] = null
     18   }
     19 }
     20 
     21 function isDomLevel2() {
     22   return typeof window !== 'undefined' && !!window.addEventListener
     23 }
     24 
     25 function getBoundingClientRect(el) {
     26   return el.getBoundingClientRect
     27     ? el.getBoundingClientRect()
     28     : {
     29         left: 0,
     30         top: 0
     31       }
     32 }
     33 function canvasSupported() {
     34   return !!document.createElement('canvas').getContext
     35 }
     36 
     37 function defaultWheelDelta(e) {
     38   // 一般浏览器
     39   if (e.deltaY) {
     40     return -e.deltaY * (e.deltaMode === 1 ? 0.05 : e.deltaMode ? 1 : 0.002)
     41   }
     42   // IE浏览器
     43   else if (e.wheelDelta) {
     44     // e.wheelDelta 正, 向上滚动-放大, 负, 向下滚动-缩小
     45     return e.wheelDelta > 0 ? e.wheelDelta * 0.002 : e.wheelDelta * 0.002
     46   }
     47 }
     48 
     49 // 返回一个 dom相对于屏幕距离的对象
     50 function defaultGetZrXY(el, e, out) {
     51   // This well-known method below does not support css transform.
     52   var box = getBoundingClientRect(el)
     53   out.zrX = e.clientX - box.left
     54   out.zrY = e.clientY - box.top
     55 }
     56 
     57 // 输出 鼠标点与某一个 dom的相对 x,y 距离
     58 // from: https://github.com/ecomfe/zrender/tree/master/src/core/event.js
     59 function clientToLocal(el, e, out, calculate) {
     60   out = out || {} // According to the W3C Working Draft, offsetX and offsetY should be relative
     61   // to the padding edge of the target element. The only browser using this convention
     62   // is IE. Webkit uses the border edge, Opera uses the content edge, and FireFox does
     63   // not support the properties.
     64   // (see http://www.jacklmoore.com/notes/mouse-position/)
     65   // In zr painter.dom, padding edge equals to border edge.
     66   // FIXME
     67   // When mousemove event triggered on ec tooltip, target is not zr painter.dom, and
     68   // offsetX/Y is relative to e.target, where the calculation of zrX/Y via offsetX/Y
     69   // is too complex. So css-transfrom dont support in this case temporarily.
     70 
     71   if (calculate || canvasSupported()) {
     72     defaultGetZrXY(el, e, out)
     73   } // Caution: In FireFox, layerX/layerY Mouse position relative to the closest positioned
     74   // ancestor element, so we should make sure el is positioned (e.g., not position:static).
     75   // BTW1, Webkit don't return the same results as FF in non-simple cases (like add
     76   // zoom-factor, overflow / opacity layers, transforms ...)
     77   // BTW2, (ev.offsetY || ev.pageY - $(ev.target).offset().top) is not correct in preserve-3d.
     78   // <https://bugs.jquery.com/ticket/8523#comment:14>
     79   // BTW3, In ff, offsetX/offsetY is always 0.
     80   // else if (env.browser.firefox && e.layerX != null && e.layerX !== e.offsetX) {
     81   // out.zrX = e.layerX;
     82   // out.zrY = e.layerY;
     83   //   } // For IE6+, chrome, safari, opera. (When will ff support offsetX?)
     84   else if (e.offsetX != null) {
     85     out.zrX = e.offsetX
     86     out.zrY = e.offsetY
     87   } // For some other device, e.g., IOS safari.
     88   else {
     89     defaultGetZrXY(el, e, out)
     90   }
     91 
     92   return out
     93 }
     94 
     95 // 为某个 dom, 输出一个规格化的事件对象:
     96 // 该对象会在原本的 MouseEvent 对象基础上, 添加三个属性: zrX, zrY, zrDelta; 分别表示 MouseEvent对象相对于 dom的 x, y距离, MouseEvent对象 缩放的系数
     97 // from: https://github.com/ecomfe/zrender/tree/master/src/core/event.js
     98 function normalizeEvent(el, e, calculate) {
     99   e = e || window.event
    100 
    101   if (e.zrX != null) {
    102     return e
    103   }
    104 
    105   var eventType = e.type
    106   var isTouch = eventType && eventType.indexOf('touch') >= 0
    107 
    108   if (!isTouch) {
    109     clientToLocal(el, e, e, calculate)
    110     e.zrDelta = e.wheelDelta ? e.wheelDelta / 120 : -(e.detail || 0) / 3
    111   } else {
    112     // var touch = eventType !== 'touchend' ? e.targetTouches[0] : e.changedTouches[0];
    113     // touch && clientToLocal(el, touch, e, calculate);
    114   }
    115 
    116   return e
    117 }
    118 
    119 /**
    120  * 支持 DOM事件, 自定义事件, 制定的一个事件代理函数;
    121  * 思路: 对于一个 canvas 元素而言, 事件代理主要围绕 坐标体系/路径/点 之类来为不同位置 注册不同的事件类型;
    122  *       因此在实现上, 考虑了两种添加事件方式:
    123  *          1. 如果声明了坐标区域, 那么仅仅在坐标区域才 触发坐标区域内的监听函数队列: 这种只支持 MouseEvent事件类型, 不支持 keyboardEvent 类型事件(因为 keyboardEvent 类型事件没有坐标信息, 只有键盘按下信息):
    124  *          2. 如果没有声明坐标区域, 那么当同类型事件发生时, 则直接触发同类型的函数队列(排除有声明区域的)
    125  *
    126  *       这个事件对象必须包含的基本功能有:
    127  *          1. 注册事件: 为某一个坐标位置, 注册不同类型事件
    128  *                  坐标位置: { type: 'block', data: {参数}, area: {x, y, width, height} } area 表示注册事件的区域,
    129  *                      注意为 area区域注册事件会存在覆盖的情况, 也即同一区域注册有多个事件后, 会同时触发所有同类型的注册事件, 这可能并不是我们所希望看到的;
    130  *                      比如一个块内的某一点分别注册有点击事件, 点击块内的点并不希望触发块的点击事件
    131  *
    132  *                      处理方式目前支持两种:
    133  *                          1. 设置函数的 silence属性;
    134  *                                  有时事件不应该被注销, 但也不应该被触发, 所以增加了一个额外的 silence属性来控制
    135  *                                  只有当 silence为 false时, 才会触发事件; 这里简化了实现, 直接为注册事件挂载一个 silence属性即可:
    136  *                          2. 为注册事件增加权重 zIndex(默认为 0), 并为事件返回 true, 即可停止事件派发
    137  *                                  权重较高的, 会被放置在队列前面, 派发事件时从前往后, 如果其中有一个事件返回结构判断为 true, 则会停止事件派发;
    138  *
    139  *                  支持的事件类型: 在MouseEvent, keyboardEvent 中选择部分来实现, 出于浏览器兼容性考虑, 部分事件需要特殊处理
    140  *
    141  *          2. 解绑事件: 清空某一个类型中的某个事件; 清空所有同类型的事件回调; 清空注册区块的所有事件回调等
    142  *          3. 派发事件: 判断鼠标所在位置, 判断是否在形状中/坐标位置内; 然后找出位置中所有注册的回调函数, 然后调用:
    143  *                          并在调用时传入 该事件对象, 传入当前 dom 等必要信息, 传入注册时的 data信息
    144  *
    145  */
    146 
    147 class EventProxy {
    148   constructor(selector) {
    149     var support =
    150       'onwheel' in document.createElement('div')
    151         ? 'wheel' // 各个厂商的高版本浏览器都支持"wheel"
    152         : document.onmousewheel !== undefined
    153         ? 'mousewheel' // Webkit 和 IE一定支持"mousewheel"
    154         : 'DOMMouseScroll' // 低版本firefox
    155 
    156     // 支持的所有类型事件;
    157     var supportEventAliasNames = {
    158       mouseenter: 'mouseover',
    159       mouseleave: 'mouseout',
    160       contextmenu: 'contextmenu',
    161       click: 'click',
    162       dblclick: 'dblclick',
    163       mousewheel: support == 'DOMMouseScroll' ? 'MozMousePixelScroll' : support,
    164       mousedown: 'mousedown',
    165       mouseup: 'mouseup',
    166       mousemove: 'mousemove',
    167       //  ['mousedown', 'mouseup'],     // 对于 brush事件, 尚未实现;
    168       setPointStyle: 'mousemove'
    169     }
    170 
    171     this.dom = document.querySelector(selector)
    172     this.uuid = 10000
    173     this.eventListener = {}
    174     this.eventDispatcher = {}
    175     this.supportEventNames = supportEventAliasNames
    176     this.registerBlock = []
    177   }
    178   getUUid() {
    179     return ++this.uuid
    180   }
    181   createFunc(aliasName) {
    182     var _this = this
    183     return function(e) {
    184       normalizeEvent(_this.dom, e)
    185       _this.dispatchEventToElement(e, _this.dom, aliasName)
    186     }
    187   }
    188   // 添加事件: 可能是 DOM类型事件, 也可能是自定义事件;
    189   // {@param.eventAliasName} 事件别名: 必要参数
    190   // {@param.data} 要传递的参数: 必要参数
    191   // {@param.cb} 要执行的回调: 可选参数
    192   // {@param.context} 回调函数执行时的上下文对象: 可选参数
    193   addEvent(eventAliasName, data, cb, context) {
    194     var domRawEvent
    195     var method = 'push'
    196     var args = [].slice.call(arguments, 1)
    197     console.log('数据:::', data)
    198     // 如果除事件类型外, 只有一个参数, 那么判断哪一个参数是否为函数;
    199     if (args.length === 1) {
    200       cb = typeof args[0] === 'function' ? args[0] : undefined
    201       data = undefined
    202       context = undefined
    203     }
    204     // 如果是两个参数, 可能是 data + cb, 也可能是 cb + context 的组合
    205     // 如果第一个参数是函数, 那么认为传入的是 回调 + 上下文的组合
    206     // 如果第二个参数是函数, 那么认为传入的是 数据 + 回调的组合
    207     else if (args.length === 2) {
    208       if (typeof args[0] === 'function') {
    209         cb = args[0]
    210         context = args[1]
    211         data = null
    212       } else if (typeof args[1] === 'function') {
    213         data = args[0]
    214         cb = args[1]
    215         context = null
    216       }
    217     } else if (args.length === 3) {
    218       data = args[0]
    219       cb = typeof args[1] === 'function' ? args[1] : undefined
    220       context = args[2]
    221     }
    222 
    223     if (!cb) {
    224       return
    225     }
    226     cb.uid = cb.uid || this.getUUid()
    227 
    228     // 如果是绑定的 DOM事件, 给 DOM添加同类型事件
    229     if ((domRawEvent = this.supportEventNames[eventAliasName])) {
    230       if (!this.eventDispatcher[eventAliasName]) {
    231         this.eventDispatcher[eventAliasName] = this.createFunc(eventAliasName)
    232         addEventListener(this.dom, domRawEvent, this.eventDispatcher[eventAliasName])
    233       }
    234     }
    235     // 如果已经注册过同类型事件
    236     if (this.eventListener[eventAliasName]) {
    237       // 默认阻止一个事件的重复挂载 ; 原则上不应该阻止, 应该遵守调用者的一切行为;
    238       if (
    239         this.eventListener[eventAliasName].findIndex(function(ev) {
    240           return ev.uid === cb.uid
    241         }) === -1
    242       ) {
    243         this.eventListener[eventAliasName][method]({
    244           uid: cb.uid,
    245           data: data,
    246           silence: !!cb.silence,
    247           callback: cb,
    248           zIndex: cb.zIndex || 0,
    249           block: (data && data.block) || cb.block,
    250           context: context
    251         })
    252       }
    253       // 如果函数声明了 $repeat属性, 表示允许重复挂载
    254       else if (cb.$repeat) {
    255         this.eventListener[eventAliasName][method]({
    256           uid: cb.uid,
    257           data: data,
    258           silence: !!cb.silence,
    259           callback: cb,
    260           zIndex: cb.zIndex || 0,
    261           block: (data && data.block) || cb.block,
    262           context: context
    263         })
    264       }
    265     }
    266     // 注册自定义类型事件的处理
    267     else {
    268       // 注册事件时, 传入的一个规格化的事件描述对象;
    269       //  uid 事件函数的唯一 id
    270       //  data 事件执行时, 暂存的参数值: 可能是一个原始值(primitive value), 也可能是一个引用
    271       //  silence 事件函数是否执行的控制条件
    272       //  callback 要执行的函数
    273       //  zIndex 事件的权重
    274       //  block 事件注册的位置: 这个值目前实现的是 {x, y, width, height};
    275       this.eventListener[eventAliasName] = [
    276         {
    277           uid: cb.uid,
    278           data: data,
    279           silence: !!cb.silence,
    280           callback: cb,
    281           zIndex: cb.zIndex || 0,
    282           block: (data && data.block) || cb.block,
    283           context: context
    284         }
    285       ]
    286     }
    287   }
    288   // 往 回调队列中, 删除某一个事件对象
    289   // {@param.eventAliasName } 要删除的事件类型别名
    290   // {@param.block } 要删除某一个声明块内注册的事件
    291   // {@param.cb } 要删除的事件
    292   removeEvent(eventAliasName, block, cb) {
    293     var uid, cur, condition
    294 
    295     if (!cb && block) {
    296       cb = block
    297       block = null
    298     }
    299 
    300     uid = cb.uid
    301     if (!uid) {
    302       // console.warn('未注册的回调或不是一个有效的回调, 无法清除');
    303       return
    304     }
    305 
    306     if (this.eventListener[eventAliasName]) {
    307       for (var i = 0; i < this.eventListener[eventAliasName].length; i++) {
    308         cur = this.eventListener[eventAliasName][i]
    309         condition = block
    310           ? this.isBlockContainBlock(block, cur.block) && cur.uid === uid
    311           : cur.uid === uid
    312 
    313         if (condition) {
    314           this.eventListener[eventAliasName].splice(i, 1)
    315         }
    316       }
    317     }
    318   }
    319 
    320   // 判断点是否在某一个矩形方块内:
    321   // block: x, y, width, height; point: x, y;
    322   isPointInBlock(block, point) {
    323     return (
    324       block.x <= point.x &&
    325       block.x + block.width >= point.x &&
    326       block.y <= point.y &&
    327       block.y + block.height >= point.y
    328     )
    329   }
    330 
    331   // 判断一个块是否全包含另一个块
    332   // {@param pBlock} Object{x, y, width, height}
    333   // {@param sBlock} Object{x1, y1, width2, height2}
    334   isBlockContainBlock(pBlock, sBlock) {
    335     return (
    336       pBlock.x <= sBlock.x &&
    337       pBlock.x + pBlock.width >= sBlock.x + sBlock.width &&
    338       pBlock.y <= sBlock.y &&
    339       pBlock.y + pBlock.height >= sBlock.y + sBlock.height
    340     )
    341   }
    342 
    343   // 分发 DOM事件:
    344   dispatchEventToElement(event, dom, eventAliasName) {
    345     var listen = this.eventListener[eventAliasName]
    346     var length = listen && listen.length
    347     var _this = this
    348     var tobreak = false,
    349       curFunc,
    350       inBlockListen,
    351       stop,
    352       stopImmediatePropagation,
    353       locationToDom
    354     if (length) {
    355       listen.sort(function(fn1, fn2) {
    356         return fn1.zIndex > fn2.zIndex ? -1 : 1
    357       })
    358 
    359       locationToDom = { x: event.zrX, y: event.zrY }
    360       inBlockListen = listen.filter(function(fn) {
    361         if (fn.block) {
    362           return _this.isPointInBlock(fn.block, locationToDom)
    363         } else {
    364           return true
    365         }
    366       })
    367       length = inBlockListen.length
    368       for (var i = 0; i < length; i++) {
    369         curFunc = inBlockListen[i]
    370         if (!curFunc.silence && typeof curFunc.callback === 'function') {
    371           stop = curFunc.callback.call(curFunc.context, event, dom, curFunc.data)
    372           if (stop) {
    373             if (typeof stop === 'object') {
    374               tobreak = !!stop.stopPropagation
    375               stopImmediatePropagation = !!stop.stopImmediatePropagation
    376             } else {
    377               tobreak = true
    378             }
    379           } else {
    380             tobreak = false
    381           }
    382         }
    383         if (tobreak) {
    384           break
    385         }
    386       }
    387       // 鼠标右键时, 如果要阻止浏览器右键默认行为, 那么应该在 contextmenu的回调中返回一个有效值;
    388       if (stopImmediatePropagation) {
    389         if (eventAliasName === 'contextmenu') {
    390           document.oncontextmenu = stopImmediatePropagation
    391             ? function(e) {
    392                 e.stopImmediatePropagation()
    393                 return false
    394               }
    395             : null
    396         } else {
    397           document.oncontextmenu = null
    398           event.stopImmediatePropagation()
    399         }
    400       } else {
    401         document.oncontextmenu = null
    402       }
    403     }
    404   }
    405   // 分发自定义事件:
    406   dispatchEvent(eventType, params) {
    407     var isDomEvent = this.supportEventNames[eventType]
    408     var params = [].slice.call(arguments, 1)
    409     if (isDomEvent) {
    410       //
    411     } else {
    412       var listen = this.eventListener[eventType]
    413       var length = listen && listen.length
    414       var tobreak = true,
    415         curFunc
    416       if (length) {
    417         listen.sort(function(fn1, fn2) {
    418           return fn1.zIndex > fn2.zIndex ? -1 : 1
    419         })
    420         for (var i = 0; i < length; i++) {
    421           curFunc = this.eventListener[eventType][i]
    422           if (!curFunc.silence && typeof curFunc.callback === 'function') {
    423             tobreak =
    424               tobreak & !curFunc.callback.apply(curFunc.context, [curFunc.data].concat(params))
    425           }
    426           if (!tobreak) {
    427             break
    428           }
    429         }
    430       }
    431     }
    432   }
    433 
    434   // 清除所有注册事件;
    435   disposeAll(eventAliasName) {
    436     var supportEventNames = this.supportEventNames
    437     // 没传递类型, 则删除所有事件回调
    438     if (!eventAliasName) {
    439       // 清除 dom类型事件
    440       Object.keys(supportEventNames).forEach(aliasName => {
    441         var evName = supportEventNames[aliasName]
    442         removeEventListener(this.dom, evName, this.eventDispatcher[aliasName])
    443       })
    444       // 清除自定义类型事件
    445       Object.keys(this.eventListener).forEach(aliasName => {
    446         this.eventListener[aliasName].length = 0
    447         delete this.eventListener[aliasName]
    448       })
    449 
    450       document.oncontextmenu = null
    451     }
    452     // 如果传了事件类型, 只清除监听函数队列
    453     else {
    454       if (this.eventListener[eventAliasName]) {
    455         this.eventListener[eventAliasName].length = 0
    456       }
    457     }
    458   }
    459 }
    460 
    461 export { addEventListener, defaultWheelDelta, removeEventListener, isDomLevel2, EventProxy }
    View Code

      

    调用: 

    function setChart(imageUrl) {
      if (chartInstance) {
        chartInstance.clear();
        chartInstance.initEvent();
        chartInstance.initScene([{ url: imageUrl }])
      } else {
        chartInstance = new Chart(DOMselector, {
          images: [{ url: url }]
        })
      }
    }
    
    setChart(imageUrl)

    整体实现比较粗糙,错误之处欢迎纠正;  

    :::纠正一个 bug, 某些图片会自动旋转,后来百度发现是图片拍摄时保留的元信息中旋转角度 orientation导致。--代码已更新

    在 img标签引用一个旋转了角度的照片时, 图片被浏览器自动转正. 如果浏览器兼容可以使用 css属性 image-orientation: from-image 来校正角度(css4工作草案), 考虑到兼容, 得想其他办法读取到 EXIF旋转角度然后再转正了;

    推荐使用 exif-js 来读取图片信息, 用法示例:

    {

    参考: https://blog.csdn.net/weixin_39660922/article/details/111250912

             https://www.jianshu.com/p/a20033e33810

    }

    import EXIF from 'exif-js';
    
    var img = document.getElementById("img");
    EXIF.getData(img, function() {
      var orientation = EXIF.getTag(this, 'Orientation');
      switch(orientation ) {
        // 正常
        // 矫正方式: 不做操作
        case 1: , 
          break;
        // 正常镜像
        // 矫正方式: 水平翻转
        case 2: ,
          break;
        // 顺时针旋转 180°
        // 矫正方式: 逆时针旋转 180°, 向上、右移动复原位置
        case 3: 
          break;
        // 顺时针旋转 180°镜像
        // 矫正方式: 水平翻转, 逆时针旋转 90°, 向上、右移动复原位置
        case 4: 
          break;
        // 顺时针旋转 270°镜像
        // 矫正方式: 垂直翻转、顺时针旋转 90°、向上平移复原位置
        case 5: 
          break;
        // 顺时针旋转270°‍
        // 矫正方式: 顺时针旋转 90°‍、向上平移复原位置
        case 6:
          break;
        // 顺时针旋转90°镜像
        // 矫正方式: 垂直翻转、逆时针旋转 90、向右平移复原位置
        case 7:
          break;
        // 顺时针旋转90°
        // 矫正方式: 逆时针旋转 90、向右平移复原位置
        case 8:
          break;
        // 读取失败
        default:
          break;
      }
    })

    附: canvas 图片旋转算法

    // 旋转公式与canvas绘图
    // _w = w * cos? + h * sin?
    // _h = w * sin? + h * cos?
    
    canvas.width = Math.abs(width * cosReg) + Math.abs(height * sinReg)
    canvas.height = Math.abs(width * sinReg) + Math.abs(height * cosReg)
    
    context.translate(canvas.width / 2, canvas.height / 2)
    context.rotate((rotate * Math.PI) / 180)
    
    // 回到原点
    context.setTransform(1, 0, 0, 1, 0, 0)

     图片翻转算法: 原理应该是类似于一个镜像, 想象一下: 照镜子时里面的自己, 和现实中的自己正是一个水平方向的翻转;

    // 伪代码
    // 水平翻转
    var imgData = canvas.getImageData(0, 0, width, height);
    var startPoint, endPoint, r, g, b, a
    for (var i = 0; i < height; i++) {
      for (var j = 0; j < width; j++) {
        // 起点
        startPoint = (i * width + j) * 4
        // 对称点
        endPoint = (i * width + (width - j)) * 4
        
        r = imgData[startPoint]
        g = imgData[startPoint + 1]
        b = imgData[startPoint + 2]
        a = imgData[startPoint + 3]
        imgData[startPoint] = imgData[endPoint]
        imgData[startPoint + 1] = imgData[endPoint + 1]
        imgData[startPoint + 2] = imgData[endPoint + 2]
        imgData[startPoint + 3] = imgData[endPoint + 3]
        imgData[endPoint] = r
        imgData[endPoint + 1] = g
        imgData[endPoint + 2] = b
        imgData[endPoint + 3] = a
      }
    }
    
    // 垂直翻转方法类似

          

    附: 

    canvas 的优化: https://developer.mozilla.org/zh-CN/docs/Web/API/Canvas_API/Tutorial/Optimizing_canvas

  • 相关阅读:
    Python的time模块随笔。
    生成器递归解决八皇后问题(勉强理解)
    Python历史「解密」Python底层逻辑 及Python 字节码介绍(转帖)
    可迭代(Interable),迭代器(Iterator),生成器(generator)的手记(11月26日再次修改)
    __getattr__,__setattr__,__delattr__,__getattribute__,记录
    关于property的一些记录,以及描述符(descriptor)中__get__,__set__,__delete__的属性使用。
    Python魔法方法之容器部方法(__len__,__getitem__,__setitem__,__delitem__,__missing__)(更新版本)
    Mac下PyCharm快捷键大全!(每天记住几个)
    开笔了,就写一下,hasattr,getattr,setattr。
    GUI
  • 原文地址:https://www.cnblogs.com/liuyingde/p/14453278.html
Copyright © 2011-2022 走看看