<template>
|
<uni-canvas
|
:canvas-id="canvasId"
|
:disable-scroll="disableScroll"
|
v-on="_listeners"
|
>
|
<canvas
|
ref="canvas"
|
width="300"
|
height="150"
|
/>
|
<div
|
style="
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
overflow: hidden;
|
"
|
>
|
<slot />
|
</div>
|
<v-uni-resize-sensor
|
ref="sensor"
|
@resize="_resize"
|
/>
|
</uni-canvas>
|
</template>
|
<script>
|
import {
|
subscriber
|
} from 'uni-mixins'
|
|
import {
|
pixelRatio,
|
wrapper
|
} from 'uni-helpers/hidpi'
|
|
import saveImage from 'uni-platform/helpers/save-image'
|
import { getSameOriginUrl } from 'uni-platform/helpers/file'
|
|
function resolveColor (color) {
|
color = color.slice(0)
|
color[3] = color[3] / 255
|
return 'rgba(' + color.join(',') + ')'
|
}
|
|
function processTouches (target, touches) {
|
return ([]).map.call(touches, (touch) => {
|
var boundingClientRect = target.getBoundingClientRect()
|
return {
|
identifier: touch.identifier,
|
x: touch.clientX - boundingClientRect.left,
|
y: touch.clientY - boundingClientRect.top
|
}
|
})
|
}
|
|
var tempCanvas
|
function getTempCanvas (width = 0, height = 0) {
|
if (!tempCanvas) {
|
tempCanvas = document.createElement('canvas')
|
}
|
tempCanvas.width = width
|
tempCanvas.height = height
|
return tempCanvas
|
}
|
|
export default {
|
name: 'Canvas',
|
mixins: [subscriber],
|
props: {
|
canvasId: {
|
type: String,
|
default: ''
|
},
|
disableScroll: {
|
type: [Boolean, String],
|
default: false
|
},
|
hidpi: {
|
type: Boolean,
|
default: true
|
}
|
},
|
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
|
},
|
pixelRatio () {
|
return this.hidpi ? pixelRatio : 1
|
}
|
},
|
created () {
|
this._actionsDefer = []
|
this._images = {}
|
},
|
mounted () {
|
this._resize()
|
},
|
beforeDestroy () {
|
const canvas = this.$refs.canvas
|
canvas.height = canvas.width = 0
|
},
|
methods: {
|
_handleSubscribe ({
|
type,
|
data = {}
|
}) {
|
var method = this[type]
|
if (type.indexOf('_') !== 0 && typeof method === 'function') {
|
method(data)
|
}
|
},
|
_resize (size) {
|
var canvas = this.$refs.canvas
|
var hasChanged = !size || (canvas.width !== Math.floor(size.width * this.pixelRatio) || canvas.height !== Math.floor(size.height * this.pixelRatio))
|
if (!hasChanged) return
|
if (canvas.width > 0 && canvas.height > 0) {
|
var context = canvas.getContext('2d')
|
var imageData = context.getImageData(0, 0, canvas.width, canvas.height)
|
wrapper(canvas, this.hidpi)
|
context.putImageData(imageData, 0, 0)
|
} else {
|
wrapper(canvas, this.hidpi)
|
}
|
},
|
_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++) {
|
const action = actions[index]
|
let method = action.method
|
const data = action.data
|
if (/^set/.test(method) && method !== 'setTransform') {
|
const method1 = method[3].toLowerCase() + method.slice(4)
|
let color
|
if (method1 === 'fillStyle' || method1 === 'strokeStyle') {
|
if (data[0] === 'normal') {
|
color = resolveColor(data[1])
|
} else if (data[0] === 'linear') {
|
const LinearGradient = c2d.createLinearGradient(...data[1])
|
data[2].forEach(function (data2) {
|
const offset = data2[0]
|
const color = resolveColor(data2[1])
|
LinearGradient.addColorStop(offset, color)
|
})
|
color = LinearGradient
|
} else if (data[0] === 'radial') {
|
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)
|
data[2].forEach(function (data2) {
|
const offset = data2[0]
|
const color = resolveColor(data2[1])
|
LinearGradient.addColorStop(offset, color)
|
})
|
color = LinearGradient
|
} else if (data[0] === 'pattern') {
|
const loaded = this.checkImageLoaded(data[1], actions.slice(index + 1), callbackId,
|
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_
|
})
|
} 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'
|
}
|
c2d[method1] = data[0]
|
} else if (method1 === 'font') {
|
c2d.__font__ = c2d.font = data[0]
|
} else {
|
c2d[method1] = data[0]
|
}
|
} 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) {
|
UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
|
callbackId,
|
data: {
|
errMsg: 'drawCanvas:ok'
|
}
|
}, this.$page.id)
|
}
|
},
|
preloadImage: function (actions) {
|
var self = this
|
actions.forEach(function (action) {
|
var method = action.method
|
var data = action.data
|
var src = ''
|
if (method === 'drawImage') {
|
src = data[0]
|
src = self.$getRealPath(src)
|
data[0] = src
|
} else if (method === 'setFillStyle' && data[0] === 'pattern') {
|
src = data[1]
|
src = self.$getRealPath(src)
|
data[1] = src
|
}
|
if (src && !self._images[src]) {
|
loadImage()
|
}
|
/**
|
* 加载图像
|
*/
|
function loadImage () {
|
const image = self._images[src] = new Image()
|
image.onload = function () {
|
image.ready = true
|
}
|
|
// 安卓 WebView 除本地路径无跨域问题
|
if (__PLATFORM__ === 'app-plus' && navigator.vendor === 'Google Inc.') {
|
if (src.indexOf('file://') === 0) {
|
image.crossOrigin = 'anonymous'
|
}
|
image.src = src
|
return
|
}
|
getSameOriginUrl(src).then(src => {
|
image.src = src
|
}).catch(() => {
|
image.src = src
|
})
|
}
|
})
|
},
|
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 ({
|
x = 0,
|
y = 0,
|
width,
|
height,
|
destWidth,
|
destHeight,
|
hidpi = true,
|
dataType,
|
quality = 1,
|
type = 'png',
|
callbackId
|
}) {
|
const canvas = this.$refs.canvas
|
let data
|
const maxWidth = canvas.offsetWidth - x
|
width = width ? Math.min(width, maxWidth) : maxWidth
|
const maxHeight = canvas.offsetHeight - y
|
height = height ? Math.min(height, maxHeight) : maxHeight
|
if (!hidpi) {
|
if (!destWidth && !destHeight) {
|
destWidth = Math.round(width * this.pixelRatio)
|
destHeight = Math.round(height * this.pixelRatio)
|
} else if (!destWidth) {
|
destWidth = Math.round(width / height * destHeight)
|
} else if (!destHeight) {
|
destHeight = Math.round(height / width * destWidth)
|
}
|
} 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 {
|
let compressed
|
if (dataType === 'base64') {
|
data = newCanvas.toDataURL(`image/${type}`, quality)
|
} else {
|
const imgData = context.getImageData(0, 0, destWidth, destHeight)
|
if (__PLATFORM__ === 'app-plus') {
|
const pako = require('pako')
|
data = pako.deflateRaw(imgData.data, { to: 'string' })
|
compressed = true
|
} else {
|
// fix [...]展开TypedArray在低版本手机报错的问题,使用Array.prototype.slice
|
data = Array.prototype.slice.call(imgData.data)
|
}
|
}
|
result = {
|
errMsg: 'canvasGetImageData:ok',
|
data,
|
compressed,
|
width: destWidth,
|
height: destHeight
|
}
|
} catch (error) {
|
result = {
|
errMsg: `canvasGetImageData:fail ${error}`
|
}
|
}
|
newCanvas.height = newCanvas.width = 0
|
context.__hidpi__ = false
|
if (!callbackId) {
|
return result
|
} else {
|
UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
|
callbackId,
|
data: result
|
}, this.$page.id)
|
}
|
},
|
putImageData ({
|
data,
|
x,
|
y,
|
width,
|
height,
|
compressed,
|
callbackId
|
}) {
|
try {
|
if (__PLATFORM__ === 'app-plus' && compressed) {
|
const pako = require('pako')
|
data = pako.inflateRaw(data)
|
}
|
if (!height) {
|
height = Math.round(data.length / 4 / width)
|
}
|
const canvas = getTempCanvas(width, height)
|
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)
|
canvas.height = canvas.width = 0
|
} 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)
|
},
|
toTempFilePath ({
|
x = 0,
|
y = 0,
|
width,
|
height,
|
destWidth,
|
destHeight,
|
fileType,
|
quality,
|
dirname,
|
callbackId
|
}) {
|
const res = this.getImageData({
|
x,
|
y,
|
width,
|
height,
|
destWidth,
|
destHeight,
|
hidpi: false,
|
dataType: 'base64',
|
type: fileType,
|
quality
|
})
|
if (!res.data || !res.data.length) {
|
UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
|
callbackId,
|
data: {
|
errMsg: res.errMsg.replace('canvasPutImageData', 'toTempFilePath')
|
}
|
}, this.$page.id)
|
return
|
}
|
saveImage(res.data, dirname, (error, tempFilePath) => {
|
let errMsg = `toTempFilePath:${error ? 'fail' : 'ok'}`
|
if (error) {
|
errMsg += ` ${error.message}`
|
}
|
UniViewJSBridge.publishHandler('onCanvasMethodCallback', {
|
callbackId,
|
data: {
|
errMsg,
|
tempFilePath: tempFilePath
|
}
|
}, this.$page.id)
|
})
|
}
|
}
|
}
|
</script>
|
<style>
|
uni-canvas {
|
width: 300px;
|
height: 150px;
|
display: block;
|
position: relative;
|
}
|
|
uni-canvas > canvas {
|
position: absolute;
|
top: 0;
|
left: 0;
|
width: 100%;
|
height: 100%;
|
}
|
</style>
|