图片放大, 拖动的例子

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

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

支持缩放, 拖动; 

图片来源可以是网络图片 , 也可以是本地选取的图片(--通过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

原文地址:https://www.cnblogs.com/liuyingde/p/14453278.html