index.vue 16.5 KB
Newer Older
fxy060608's avatar
fxy060608 已提交
1
<template>
雪洛's avatar
雪洛 已提交
2 3 4 5
  <uni-canvas
    :canvas-id="canvasId"
    :disable-scroll="disableScroll"
    v-on="_listeners"
fxy060608's avatar
fxy060608 已提交
6
  >
雪洛's avatar
雪洛 已提交
7 8 9 10
    <canvas
      ref="canvas"
      width="300"
      height="150"
fxy060608's avatar
fxy060608 已提交
11
    />
fxy060608's avatar
fxy060608 已提交
12 13 14
    <div style="position: absolute; top: 0; left: 0; width: 100%; height: 100%; overflow: hidden;">
      <slot />
    </div>
雪洛's avatar
雪洛 已提交
15 16 17
    <v-uni-resize-sensor
      ref="sensor"
      @resize="_resize"
fxy060608's avatar
fxy060608 已提交
18
    />
fxy060608's avatar
fxy060608 已提交
19 20 21 22 23 24 25 26
  </uni-canvas>
</template>
<script>
import {
  subscriber
} from 'uni-mixins'

import {
27
  pixelRatio,
fxy060608's avatar
fxy060608 已提交
28
  wrapper
29
} from 'uni-helpers/hidpi'
fxy060608's avatar
fxy060608 已提交
30

31 32
import saveImage from 'uni-platform/helpers/save-image'

fxy060608's avatar
fxy060608 已提交
33 34 35 36 37 38 39
function resolveColor (color) {
  color = color.slice(0)
  color[3] = color[3] / 255
  return 'rgba(' + color.join(',') + ')'
}

function processTouches (target, touches) {
40
  return ([]).map.call(touches, (touch) => {
fxy060608's avatar
fxy060608 已提交
41 42
    var boundingClientRect = target.getBoundingClientRect()
    return {
43 44 45
      identifier: touch.identifier,
      x: touch.clientX - boundingClientRect.left,
      y: touch.clientY - boundingClientRect.top
fxy060608's avatar
fxy060608 已提交
46 47 48 49
    }
  })
}

Q
qiang 已提交
50 51 52 53 54 55 56 57 58 59
var tempCanvas
function getTempCanvas (width = 0, height = 0) {
  if (!tempCanvas) {
    tempCanvas = document.createElement('canvas')
  }
  tempCanvas.width = width
  tempCanvas.height = height
  return tempCanvas
}

fxy060608's avatar
fxy060608 已提交
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109
export default {
  name: 'Canvas',
  mixins: [subscriber],
  props: {
    canvasId: {
      type: String,
      default: ''
    },
    disableScroll: {
      type: [Boolean, String],
      default: false
    }
  },
  data () {
    return {
      actionsWaiting: false
    }
  },
  computed: {
    id () {
      return this.canvasId
    },
    _listeners () {
      var $listeners = Object.assign({}, this.$listeners)
      var events = ['touchstart', 'touchmove', 'touchend']
      events.forEach(event => {
        var existing = $listeners[event]
        var eventHandler = []
        if (existing) {
          eventHandler.push(($event) => {
            this.$trigger(event, Object.assign({}, $event, {
              touches: processTouches($event.currentTarget, $event.touches),
              changedTouches: processTouches($event.currentTarget, $event
                .changedTouches)
            }))
          })
        }
        if (this.disableScroll && event === 'touchmove') {
          eventHandler.push(this._touchmove)
        }
        $listeners[event] = eventHandler
      })
      return $listeners
    }
  },
  created () {
    this._actionsDefer = []
    this._images = {}
  },
  mounted () {
110
    this._resize()
fxy060608's avatar
fxy060608 已提交
111
  },
Q
qiang 已提交
112 113 114 115
  beforeDestroy () {
    const canvas = this.$refs.canvas
    canvas.height = canvas.width = 0
  },
fxy060608's avatar
fxy060608 已提交
116 117 118 119 120 121 122 123 124 125
  methods: {
    _handleSubscribe ({
      type,
      data = {}
    }) {
      var method = this[type]
      if (type.indexOf('_') !== 0 && typeof method === 'function') {
        method(data)
      }
    },
126 127 128 129
    _resize () {
      var canvas = this.$refs.canvas
      if (canvas.width > 0 && canvas.height > 0) {
        var context = canvas.getContext('2d')
130
        var imageData = context.getImageData(0, 0, canvas.width, canvas.height)
131
        wrapper(canvas)
132 133
        context.putImageData(imageData, 0, 0)
      } else {
134
        wrapper(canvas)
135
      }
fxy060608's avatar
fxy060608 已提交
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166
    },
    _touchmove (event) {
      event.preventDefault()
    },
    actionsChanged ({
      actions,
      reserve,
      callbackId
    }) {
      var self = this
      if (!actions) {
        return
      }
      if (this.actionsWaiting) {
        this._actionsDefer.push([actions, reserve, callbackId])
        return
      }
      var canvas = this.$refs.canvas
      var c2d = canvas.getContext('2d')
      if (!reserve) {
        c2d.fillStyle = '#000000'
        c2d.strokeStyle = '#000000'
        c2d.shadowColor = '#000000'
        c2d.shadowBlur = 0
        c2d.shadowOffsetX = 0
        c2d.shadowOffsetY = 0
        c2d.setTransform(1, 0, 0, 1, 0, 0)
        c2d.clearRect(0, 0, canvas.width, canvas.height)
      }
      this.preloadImage(actions)
      for (let index = 0; index < actions.length; index++) {
fxy060608's avatar
fxy060608 已提交
167
        const action = actions[index]
fxy060608's avatar
fxy060608 已提交
168
        let method = action.method
fxy060608's avatar
fxy060608 已提交
169
        const data = action.data
fxy060608's avatar
fxy060608 已提交
170
        if (/^set/.test(method) && method !== 'setTransform') {
fxy060608's avatar
fxy060608 已提交
171
          const method1 = method[3].toLowerCase() + method.slice(4)
fxy060608's avatar
fxy060608 已提交
172 173 174 175 176
          let color
          if (method1 === 'fillStyle' || method1 === 'strokeStyle') {
            if (data[0] === 'normal') {
              color = resolveColor(data[1])
            } else if (data[0] === 'linear') {
fxy060608's avatar
fxy060608 已提交
177
              const LinearGradient = c2d.createLinearGradient(...data[1])
fxy060608's avatar
fxy060608 已提交
178
              data[2].forEach(function (data2) {
fxy060608's avatar
fxy060608 已提交
179 180
                const offset = data2[0]
                const color = resolveColor(data2[1])
fxy060608's avatar
fxy060608 已提交
181 182
                LinearGradient.addColorStop(offset, color)
              })
183
              color = LinearGradient
fxy060608's avatar
fxy060608 已提交
184
            } else if (data[0] === 'radial') {
fxy060608's avatar
fxy060608 已提交
185 186 187 188
              const x = data[1][0]
              const y = data[1][1]
              const r = data[1][2]
              const LinearGradient = c2d.createRadialGradient(x, y, 0, x, y, r)
fxy060608's avatar
fxy060608 已提交
189
              data[2].forEach(function (data2) {
fxy060608's avatar
fxy060608 已提交
190 191
                const offset = data2[0]
                const color = resolveColor(data2[1])
fxy060608's avatar
fxy060608 已提交
192 193
                LinearGradient.addColorStop(offset, color)
              })
194
              color = LinearGradient
fxy060608's avatar
fxy060608 已提交
195
            } else if (data[0] === 'pattern') {
fxy060608's avatar
fxy060608 已提交
196
              const loaded = this.checkImageLoaded(data[1], actions.slice(index + 1), callbackId,
fxy060608's avatar
fxy060608 已提交
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
                function (image) {
                  if (image) {
                    c2d[method1] = c2d.createPattern(image, data[2])
                  }
                })
              if (!loaded) {
                break
              }
              continue
            }
            c2d[method1] = color
          } else if (method1 === 'globalAlpha') {
            c2d[method1] = data[0] / 255
          } else if (method1 === 'shadow') {
            var _ = ['shadowOffsetX', 'shadowOffsetY', 'shadowBlur', 'shadowColor']
            data.forEach(function (color_, method_) {
              c2d[_[method_]] = _[method_] === 'shadowColor' ? resolveColor(color_) : color_
            })
215 216 217 218 219 220 221 222 223
          } else if (method1 === 'fontSize') {
            const font = c2d.__font__ || c2d.font
            c2d.__font__ = c2d.font = font.replace(/\d+\.?\d*px/, data[0] + 'px')
          } else if (method1 === 'lineDash') {
            c2d.setLineDash(data[0])
            c2d.lineDashOffset = data[1] || 0
          } else if (method1 === 'textBaseline') {
            if (data[0] === 'normal') {
              data[0] = 'alphabetic'
fxy060608's avatar
fxy060608 已提交
224
            }
225 226 227 228 229
            c2d[method1] = data[0]
          } else if (method1 === 'font') {
            c2d.__font__ = c2d.font = data[0]
          } else {
            c2d[method1] = data[0]
fxy060608's avatar
fxy060608 已提交
230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268
          }
        } else if (method === 'fillPath' || method === 'strokePath') {
          method = method.replace(/Path/, '')
          c2d.beginPath()
          data.forEach(function (data_) {
            c2d[data_.method].apply(c2d, data_.data)
          })
          c2d[method]()
        } else if (method === 'fillText') {
          c2d.fillText.apply(c2d, data)
        } else if (method === 'drawImage') {
          var A = (function () {
            var dataArray = [...data]
            var url = dataArray[0]
            var otherData = dataArray.slice(1)
            self._images = self._images || {}
            if (!self.checkImageLoaded(url, actions.slice(index + 1), callbackId, function (
              image) {
              if (image) {
                c2d.drawImage.apply(c2d, [image].concat([...otherData.slice(4, 8)],
                  [...otherData.slice(0, 4)]))
              }
            })) return 'break'
          }())
          if (A === 'break') {
            break
          }
        } else {
          if (method === 'clip') {
            data.forEach(function (data_) {
              c2d[data_.method].apply(c2d, data_.data)
            })
            c2d.clip()
          } else {
            c2d[method].apply(c2d, data)
          }
        }
      }
      if (!this.actionsWaiting && callbackId) {
269
        UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
fxy060608's avatar
fxy060608 已提交
270 271 272 273 274 275 276 277
          callbackId,
          data: {
            errMsg: 'drawCanvas:ok'
          }
        }, this.$page.id)
      }
    },
    preloadImage: function (actions) {
雪洛's avatar
雪洛 已提交
278
      var self = this
fxy060608's avatar
fxy060608 已提交
279 280 281 282 283 284
      actions.forEach(function (action) {
        var method = action.method
        var data = action.data
        var src = ''
        if (method === 'drawImage') {
          src = data[0]
雪洛's avatar
雪洛 已提交
285
          src = self.$getRealPath(src)
fxy060608's avatar
fxy060608 已提交
286 287 288
          data[0] = src
        } else if (method === 'setFillStyle' && data[0] === 'pattern') {
          src = data[1]
雪洛's avatar
雪洛 已提交
289
          src = self.$getRealPath(src)
fxy060608's avatar
fxy060608 已提交
290 291
          data[1] = src
        }
雪洛's avatar
雪洛 已提交
292
        if (src && !self._images[src]) {
fxy060608's avatar
fxy060608 已提交
293 294 295
          loadImage()
        }
        /**
Q
qiang 已提交
296 297
         * 加载图像
         */
fxy060608's avatar
fxy060608 已提交
298
        function loadImage () {
299 300 301
          const image = self._images[src] = new Image()
          image.onload = function () {
            image.ready = true
fxy060608's avatar
fxy060608 已提交
302 303
          }
          /**
Q
qiang 已提交
304 305 306
           * 从本地文件加载
           * @param {string} path 文件路径
           */
fxy060608's avatar
fxy060608 已提交
307
          function loadFile (path) {
308
            function onError () {
309 310 311 312 313 314 315 316
              const bitmap = new plus.nativeObj.Bitmap(`bitmap_${Date.now()}_${Math.random()}}`)
              bitmap.load(path, function () {
                image.src = bitmap.toBase64Data()
                bitmap.clear()
              }, function () {
                bitmap.clear()
                image.src = src
              })
317 318 319 320 321 322 323 324 325 326 327
            }
            plus.io.resolveLocalFileSystemURL(path, function (entry) {
              entry.file(function (file) {
                var fileReader = new plus.io.FileReader()
                fileReader.onload = function (data) {
                  image.src = data.target.result
                }
                fileReader.onerror = onError
                fileReader.readAsDataURL(file)
              }, onError)
            }, onError)
fxy060608's avatar
fxy060608 已提交
328 329
          }
          /**
Q
qiang 已提交
330 331 332
           * 从网络加载
           * @param {string} url 文件地址
           */
fxy060608's avatar
fxy060608 已提交
333
          function loadUrl (url) {
334 335 336 337 338 339 340
            plus.downloader.createDownload(url, {
              filename: '_doc/uniapp_temp/download/'
            }, function (d, status) {
              if (status === 200) {
                loadFile(d.filename)
              } else {
                image.src = src
fxy060608's avatar
fxy060608 已提交
341
              }
342
            }).start()
fxy060608's avatar
fxy060608 已提交
343 344
          }

345 346 347 348 349 350 351 352 353 354 355
          if (__PLATFORM__ === 'app-plus') {
            // WKWebView
            if (window.webkit && window.webkit.messageHandlers) {
              if (src.indexOf('file://') === 0) {
                loadFile(src)
                return
              } else if (src.indexOf('http://') === 0 || src.indexOf('https://') === 0) {
                loadUrl(src)
                return
              }
            }
356 357
            // 安卓 WebView 本地路径
            if (src.indexOf('file://') === 0 && navigator.vendor === 'Google Inc.') {
358
              image.crossOrigin = 'anonymous'
fxy060608's avatar
fxy060608 已提交
359 360
            }
          }
361 362

          image.src = src
fxy060608's avatar
fxy060608 已提交
363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393
        }
      })
    },
    checkImageLoaded: function (src, actions, callbackId, fn) {
      var self = this
      var image = this._images[src]
      if (image.ready) {
        fn(image)
        return true
      } else {
        this._actionsDefer.unshift([actions, true])
        this.actionsWaiting = true
        image.onload = function () {
          image.ready = true
          fn(image)
          self.actionsWaiting = false
          var actions = self._actionsDefer.slice(0)
          self._actionsDefer = []
          for (var action = actions.shift(); action;) {
            self.actionsChanged({
              actions: action[0],
              reserve: action[1],
              callbackId
            })
            action = actions.shift()
          }
        }
        return false
      }
    },
    getImageData ({
394 395
      x = 0,
      y = 0,
fxy060608's avatar
fxy060608 已提交
396 397
      width,
      height,
398 399 400
      destWidth,
      destHeight,
      hidpi = true,
401 402 403
      dataType,
      qualit = 1,
      type = 'png',
fxy060608's avatar
fxy060608 已提交
404 405
      callbackId
    }) {
406 407
      const canvas = this.$refs.canvas
      let data
fxy060608's avatar
fxy060608 已提交
408
      if (!width) {
409
        width = canvas.offsetWidth - x
fxy060608's avatar
fxy060608 已提交
410 411
      }
      if (!height) {
412
        height = canvas.offsetHeight - y
fxy060608's avatar
fxy060608 已提交
413
      }
414 415 416 417 418 419 420 421
      if (!hidpi) {
        if (!destWidth && !destHeight) {
          destWidth = Math.round(width * pixelRatio)
          destHeight = Math.round(height * pixelRatio)
        } else if (!destWidth) {
          destWidth = Math.round(width / height * destHeight)
        } else if (!destHeight) {
          destHeight = Math.round(height / width * destWidth)
422
        }
423 424 425 426 427 428 429 430 431 432 433 434 435 436 437
      } else {
        destWidth = width
        destHeight = height
      }
      const newCanvas = getTempCanvas(destWidth, destHeight)
      const context = newCanvas.getContext('2d')
      if (type === 'jpeg' || type === 'jpg') {
        type = 'jpeg'
        context.fillStyle = '#fff'
        context.fillRect(0, 0, destWidth, destHeight)
      }
      context.__hidpi__ = true
      context.drawImageByCanvas(canvas, x, y, width, height, 0, 0, destWidth, destHeight, false)
      let result
      try {
438 439 440 441 442 443 444
        if (dataType === 'base64') {
          data = newCanvas.toDataURL(`image/${type}`, qualit)
        } else {
          const imgData = context.getImageData(0, 0, destWidth, destHeight)
          // fix [...]展开TypedArray在低版本手机报错的问题,使用Array.prototype.slice
          data = Array.prototype.slice.call(imgData.data)
        }
445 446
        result = {
          errMsg: 'canvasGetImageData:ok',
447
          data,
Q
qiang 已提交
448 449 450
          width: destWidth,
          height: destHeight
        }
451 452 453 454 455 456 457 458 459
      } catch (error) {
        result = {
          errMsg: `canvasGetImageData:fail ${error}`
        }
      }
      newCanvas.height = newCanvas.width = 0
      context.__hidpi__ = false
      if (!callbackId) {
        return result
雪洛's avatar
雪洛 已提交
460 461 462
      } else {
        UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
          callbackId,
463
          data: result
Q
qiang 已提交
464
        }, this.$page.id)
fxy060608's avatar
fxy060608 已提交
465 466 467 468 469 470 471 472 473 474 475 476 477 478
      }
    },
    putImageData ({
      data,
      x,
      y,
      width,
      height,
      callbackId
    }) {
      try {
        if (!height) {
          height = Math.round(data.length / 4 / width)
        }
Q
qiang 已提交
479
        const canvas = getTempCanvas(width, height)
480 481 482
        const context = canvas.getContext('2d')
        context.putImageData(new ImageData(new Uint8ClampedArray(data), width, height), 0, 0)
        this.$refs.canvas.getContext('2d').drawImage(canvas, x, y, width, height)
Q
qiang 已提交
483
        canvas.height = canvas.width = 0
fxy060608's avatar
fxy060608 已提交
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498
      } catch (error) {
        UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
          callbackId,
          data: {
            errMsg: 'canvasPutImageData:fail'
          }
        }, this.$page.id)
        return
      }
      UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
        callbackId,
        data: {
          errMsg: 'canvasPutImageData:ok'
        }
      }, this.$page.id)
Q
qiang 已提交
499
    },
500
    toTempFilePath ({
Q
qiang 已提交
501 502 503 504 505 506 507 508
      x = 0,
      y = 0,
      width,
      height,
      destWidth,
      destHeight,
      fileType,
      qualit,
509
      dirname,
Q
qiang 已提交
510 511
      callbackId
    }) {
fxy060608's avatar
fxy060608 已提交
512
      const res = this.getImageData({
Q
qiang 已提交
513 514 515 516 517 518
        x,
        y,
        width,
        height,
        destWidth,
        destHeight,
519 520 521 522
        hidpi: false,
        dataType: 'base64',
        type: fileType,
        qualit
Q
qiang 已提交
523 524 525 526 527
      })
      if (!res.data || !res.data.length) {
        UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
          callbackId,
          data: {
528
            errMsg: res.errMsg.replace('canvasPutImageData', 'toTempFilePath')
Q
qiang 已提交
529 530 531 532
          }
        }, this.$page.id)
        return
      }
533 534 535 536
      saveImage(res.data, dirname, (error, tempFilePath) => {
        let errMsg = `toTempFilePath:${error ? 'fail' : 'ok'}`
        if (error) {
          errMsg += ` ${error.message}`
Q
qiang 已提交
537 538 539 540
        }
        UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
          callbackId,
          data: {
541 542
            errMsg,
            tempFilePath: tempFilePath
Q
qiang 已提交
543 544
          }
        }, this.$page.id)
545
      })
fxy060608's avatar
fxy060608 已提交
546 547 548 549 550
    }
  }
}
</script>
<style>
Q
qiang 已提交
551 552 553 554 555 556
uni-canvas {
  width: 300px;
  height: 150px;
  display: block;
  position: relative;
}
fxy060608's avatar
fxy060608 已提交
557

Q
qiang 已提交
558 559 560 561 562 563 564
uni-canvas > canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
565
</style>