最近项目上搞一个图片缩放的功能, 闲来分享出来, 感兴趣的可以直接用;
支持对图片自动居中;增加标记;
支持缩放, 拖动;
图片来源可以是网络图片 , 也可以是本地选取的图片(--通过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 }
./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 }
./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 }
调用:
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