提交 3cf3ae3c 编写于 作者: 郭胜强

feat: 实现基础组件:canvas 和相关接口:uni.createCanvasContext

上级 371d6c75
import createCallbacks from 'uni-helpers/callbacks'
const canvasEventCallbacks = createCallbacks('canvasEvent')
UniServiceJSBridge.subscribe('onDrawCanvas', ({
reqId,
res
}) => {
const callback = canvasEventCallbacks.pop(reqId)
if (callback) {
callback(res)
}
})
function operateCanvas (canvasId, pageId, type, data) {
UniServiceJSBridge.publishHandler(pageId + '-canvas-' + canvasId, {
canvasId,
type,
data
}, pageId)
}
const predefinedColor = {
aliceblue: '#f0f8ff',
antiquewhite: '#faebd7',
aqua: '#00ffff',
aquamarine: '#7fffd4',
azure: '#f0ffff',
beige: '#f5f5dc',
bisque: '#ffe4c4',
black: '#000000',
blanchedalmond: '#ffebcd',
blue: '#0000ff',
blueviolet: '#8a2be2',
brown: '#a52a2a',
burlywood: '#deb887',
cadetblue: '#5f9ea0',
chartreuse: '#7fff00',
chocolate: '#d2691e',
coral: '#ff7f50',
cornflowerblue: '#6495ed',
cornsilk: '#fff8dc',
crimson: '#dc143c',
cyan: '#00ffff',
darkblue: '#00008b',
darkcyan: '#008b8b',
darkgoldenrod: '#b8860b',
darkgray: '#a9a9a9',
darkgrey: '#a9a9a9',
darkgreen: '#006400',
darkkhaki: '#bdb76b',
darkmagenta: '#8b008b',
darkolivegreen: '#556b2f',
darkorange: '#ff8c00',
darkorchid: '#9932cc',
darkred: '#8b0000',
darksalmon: '#e9967a',
darkseagreen: '#8fbc8f',
darkslateblue: '#483d8b',
darkslategray: '#2f4f4f',
darkslategrey: '#2f4f4f',
darkturquoise: '#00ced1',
darkviolet: '#9400d3',
deeppink: '#ff1493',
deepskyblue: '#00bfff',
dimgray: '#696969',
dimgrey: '#696969',
dodgerblue: '#1e90ff',
firebrick: '#b22222',
floralwhite: '#fffaf0',
forestgreen: '#228b22',
fuchsia: '#ff00ff',
gainsboro: '#dcdcdc',
ghostwhite: '#f8f8ff',
gold: '#ffd700',
goldenrod: '#daa520',
gray: '#808080',
grey: '#808080',
green: '#008000',
greenyellow: '#adff2f',
honeydew: '#f0fff0',
hotpink: '#ff69b4',
indianred: '#cd5c5c',
indigo: '#4b0082',
ivory: '#fffff0',
khaki: '#f0e68c',
lavender: '#e6e6fa',
lavenderblush: '#fff0f5',
lawngreen: '#7cfc00',
lemonchiffon: '#fffacd',
lightblue: '#add8e6',
lightcoral: '#f08080',
lightcyan: '#e0ffff',
lightgoldenrodyellow: '#fafad2',
lightgray: '#d3d3d3',
lightgrey: '#d3d3d3',
lightgreen: '#90ee90',
lightpink: '#ffb6c1',
lightsalmon: '#ffa07a',
lightseagreen: '#20b2aa',
lightskyblue: '#87cefa',
lightslategray: '#778899',
lightslategrey: '#778899',
lightsteelblue: '#b0c4de',
lightyellow: '#ffffe0',
lime: '#00ff00',
limegreen: '#32cd32',
linen: '#faf0e6',
magenta: '#ff00ff',
maroon: '#800000',
mediumaquamarine: '#66cdaa',
mediumblue: '#0000cd',
mediumorchid: '#ba55d3',
mediumpurple: '#9370db',
mediumseagreen: '#3cb371',
mediumslateblue: '#7b68ee',
mediumspringgreen: '#00fa9a',
mediumturquoise: '#48d1cc',
mediumvioletred: '#c71585',
midnightblue: '#191970',
mintcream: '#f5fffa',
mistyrose: '#ffe4e1',
moccasin: '#ffe4b5',
navajowhite: '#ffdead',
navy: '#000080',
oldlace: '#fdf5e6',
olive: '#808000',
olivedrab: '#6b8e23',
orange: '#ffa500',
orangered: '#ff4500',
orchid: '#da70d6',
palegoldenrod: '#eee8aa',
palegreen: '#98fb98',
paleturquoise: '#afeeee',
palevioletred: '#db7093',
papayawhip: '#ffefd5',
peachpuff: '#ffdab9',
peru: '#cd853f',
pink: '#ffc0cb',
plum: '#dda0dd',
powderblue: '#b0e0e6',
purple: '#800080',
rebeccapurple: '#663399',
red: '#ff0000',
rosybrown: '#bc8f8f',
royalblue: '#4169e1',
saddlebrown: '#8b4513',
salmon: '#fa8072',
sandybrown: '#f4a460',
seagreen: '#2e8b57',
seashell: '#fff5ee',
sienna: '#a0522d',
silver: '#c0c0c0',
skyblue: '#87ceeb',
slateblue: '#6a5acd',
slategray: '#708090',
slategrey: '#708090',
snow: '#fffafa',
springgreen: '#00ff7f',
steelblue: '#4682b4',
tan: '#d2b48c',
teal: '#008080',
thistle: '#d8bfd8',
tomato: '#ff6347',
turquoise: '#40e0d0',
violet: '#ee82ee',
wheat: '#f5deb3',
white: '#ffffff',
whitesmoke: '#f5f5f5',
yellow: '#ffff00',
yellowgreen: '#9acd32',
transparent: '#00000000'
}
function checkColor (e) {
var t = null
if ((t = /^#([0-9|A-F|a-f]{6})$/.exec(e)) != null) {
let n = parseInt(t[1].slice(0, 2), 16)
let o = parseInt(t[1].slice(2, 4), 16)
let r = parseInt(t[1].slice(4), 16)
return [n, o, r, 255]
}
if ((t = /^#([0-9|A-F|a-f]{3})$/.exec(e)) != null) {
let n = t[1].slice(0, 1)
let o = t[1].slice(1, 2)
let r = t[1].slice(2, 3)
n = parseInt(n + n, 16)
o = parseInt(o + o, 16)
r = parseInt(r + r, 16)
return [n, o, r, 255]
}
if ((t = /^rgb\((.+)\)$/.exec(e)) != null) {
return t[1].split(',').map(function (e) {
return Math.min(255, parseInt(e.trim()))
}).concat(255)
}
if ((t = /^rgba\((.+)\)$/.exec(e)) != null) {
return t[1].split(',').map(function (e, t) {
return t === 3 ? Math.floor(255 * parseFloat(e.trim())) : Math.min(255, parseInt(e.trim()))
})
}
var i = e.toLowerCase()
if (predefinedColor.hasOwnProperty(i)) {
t = /^#([0-9|A-F|a-f]{6,8})$/.exec(predefinedColor[i])
let n = parseInt(t[1].slice(0, 2), 16)
let o = parseInt(t[1].slice(2, 4), 16)
let r = parseInt(t[1].slice(4, 6), 16)
let a = parseInt(t[1].slice(6, 8), 16)
a = a >= 0 ? a : 255
return [n, o, r, a]
}
console.group('非法颜色: ' + e)
console.error('不支持颜色:' + e)
console.groupEnd()
return [0, 0, 0, 255]
}
function TextMetrics (width) {
this.width = width
}
function Pattern (image, repetition) {
this.image = image
this.repetition = repetition
}
class CanvasGradient {
constructor (type, data) {
this.type = type
this.data = data
this.colorStop = []
}
addColorStop (position, color) {
this.colorStop.push([position, checkColor(color)])
}
}
var methods1 = ['scale', 'rotate', 'translate', 'setTransform', 'transform']
var methods2 = ['drawImage', 'fillText', 'fill', 'stroke', 'fillRect', 'strokeRect', 'clearRect',
'strokeText'
]
var methods3 = ['setFillStyle', 'setTextAlign', 'setStrokeStyle', 'setGlobalAlpha', 'setShadow',
'setFontSize', 'setLineCap', 'setLineJoin', 'setLineWidth', 'setMiterLimit',
'setTextBaseline', 'setLineDash'
]
var c2d
class CanvasContext {
constructor (id, pageId) {
this.id = id
this.pageId = pageId
this.actions = []
this.currentTransform = []
this.currentStepAnimates = []
this.drawingState = []
this.state = {
lineDash: [0, 0],
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowBlur: 0,
shadowColor: [0, 0, 0, 0],
font: '10px sans-serif',
fontSize: 10,
fontWeight: 'normal',
fontStyle: 'normal',
fontFamily: 'sans-serif'
}
}
draw (reserve = false, callback) {
var actions = [...this.actions]
this.actions = []
this.path = []
var callbackId
if (typeof callback === 'function') {
callbackId = canvasEventCallbacks.push(callback)
}
operateCanvas(this.id, this.pageId, 'actionsChanged', {
actions,
reserve,
callbackId
})
}
createLinearGradient (x0, y0, x1, y1) {
return new CanvasGradient('linear', [x0, y0, x1, y1])
}
createCircularGradient (x, y, r) {
return new CanvasGradient('radial', [x, y, r])
}
createPattern (image, repetition) {
if (undefined === repetition) {
console.error("Failed to execute 'createPattern' on 'CanvasContext': 2 arguments required, but only 1 present.")
} else if (['repeat', 'repeat-x', 'repeat-y', 'no-repeat'].indexOf(repetition) < 0) {
console.error("Failed to execute 'createPattern' on 'CanvasContext': The provided type ('" + repetition + "') is not one of 'repeat', 'no-repeat', 'repeat-x', or 'repeat-y'.")
} else {
return new Pattern(image, repetition)
}
}
measureText (text) {
if (!c2d) {
let canvas = document.createElement('canvas')
c2d = canvas.getContext('2d')
}
c2d.font = this.state.font
return new TextMetrics(c2d.measureText(text).width || 0)
}
save () {
this.actions.push({
method: 'save',
data: []
})
this.drawingState.push(this.state)
}
restore () {
this.actions.push({
method: 'restore',
data: []
})
this.state = this.drawingState.pop() || {
lineDash: [0, 0],
shadowOffsetX: 0,
shadowOffsetY: 0,
shadowBlur: 0,
shadowColor: [0, 0, 0, 0],
font: '10px sans-serif',
fontSize: 10,
fontWeight: 'normal',
fontStyle: 'normal',
fontFamily: 'sans-serif'
}
}
beginPath () {
this.path = []
this.subpath = []
}
moveTo (x, y) {
this.path.push({
method: 'moveTo',
data: [x, y]
})
this.subpath = [
[x, y]
]
}
lineTo (x, y) {
if (this.path.length === 0 && this.subpath.length === 0) {
this.path.push({
method: 'moveTo',
data: [x, y]
})
} else {
this.path.push({
method: 'lineTo',
data: [x, y]
})
}
this.subpath.push([x, y])
}
quadraticCurveTo (cpx, cpy, x, y) {
this.path.push({
method: 'quadraticCurveTo',
data: [cpx, cpy, x, y]
})
this.subpath.push([x, y])
}
bezierCurveTo (cp1x, cp1y, cp2x, cp2y, x, y) {
this.path.push({
method: 'bezierCurveTo',
data: [cp1x, cp1y, cp2x, cp2y, x, y]
})
this.subpath.push([x, y])
}
arc (x, y, r, sAngle, eAngle, counterclockwise = false) {
this.path.push({
method: 'arc',
data: [x, y, r, sAngle, eAngle, counterclockwise]
})
this.subpath.push([x, y])
}
rect (x, y, width, height) {
this.path.push({
method: 'rect',
data: [x, y, width, height]
})
this.subpath = [
[x, y]
]
}
arcTo (x1, y1, x2, y2, radius) {
this.path.push({
method: 'arcTo',
data: [x1, y1, x2, y2, radius]
})
this.subpath.push([x2, y2])
}
clip () {
this.actions.push({
method: 'clip',
data: [...this.path]
})
}
closePath () {
this.path.push({
method: 'closePath',
data: []
})
if (this.subpath.length) {
this.subpath = [this.subpath.shift()]
}
}
}
[...methods1, ...methods2].forEach(function (method) {
function get (method) {
switch (method) {
case 'fill':
case 'stroke':
return function () {
this.actions.push({
method: method + 'Path',
data: [...this.path]
})
}
case 'fillRect':
return function (x, y, width, height) {
this.actions.push({
method: 'fillPath',
data: [{
method: 'rect',
data: [x, y, width, height]
}]
})
}
case 'strokeRect':
return function (x, y, width, height) {
this.actions.push({
method: 'strokePath',
data: [{
method: 'rect',
data: [x, y, width, height]
}]
})
}
case 'fillText':
case 'strokeText':
return function (text, x, y, maxWidth) {
var data = [text.toString(), x, y]
if (typeof maxWidth === 'number') {
data.push(maxWidth)
}
this.actions.push({
method,
data
})
}
case 'drawImage':
return function (imageResource, dx, dy, dWidth, dHeight, sx, sy, sWidth, sHeight) {
if (sHeight === undefined) {
sx = dx
sy = dy
sWidth = dWidth
sHeight = dHeight
dx = undefined
dy = undefined
dWidth = undefined
dHeight = undefined
}
var data
function isNumber (e) {
return typeof e === 'number'
}
data = isNumber(dx) && isNumber(dy) && isNumber(dWidth) && isNumber(dHeight) ? [imageResource, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight] : isNumber(sWidth) && isNumber(
sHeight) ? [imageResource, sx, sy, sWidth, sHeight] : [imageResource, sx, sy]
this.actions.push({
method,
data
})
}
default:
return function (...data) {
this.actions.push({
method,
data
})
}
}
}
CanvasContext.prototype[method] = get(method)
})
methods3.forEach(function (method) {
function get (method) {
switch (method) {
case 'setFillStyle':
case 'setStrokeStyle':
return function (color) {
this.actions.push({
method,
data: ['normal', checkColor(color)]
})
}
case 'setGlobalAlpha':
return function (alpha) {
alpha = Math.floor(255 * parseFloat(alpha))
this.actions.push({
method,
data: [alpha]
})
}
case 'setShadow':
return function (offsetX, offsetY, blur, color) {
color = checkColor(color)
this.actions.push({
method,
data: [offsetX, offsetY, blur, color]
})
this.state.shadowBlur = blur
this.state.shadowColor = color
this.state.shadowOffsetX = offsetX
this.state.shadowOffsetY = offsetY
}
case 'setLineDash':
return function (pattern, offset) {
pattern = pattern || [0, 0]
offset = offset || 0
this.actions.push({
method,
data: [pattern, offset]
})
this.state.lineDash = pattern
}
case 'setFontSize':
return function (fontSize) {
this.state.font = this.state.font.replace(/\d+\.?\d*px/, fontSize + 'px')
this.state.fontSize = fontSize
this.actions.push({
method,
data: [fontSize]
})
}
default:
return function (...data) {
this.actions.push({
method,
data
})
}
}
}
CanvasContext.prototype[method] = get(method)
})
export function createCanvasContext (id, context) {
if (context) {
return new CanvasContext(id, context.$page.id)
}
const app = getApp()
if (app.$route && app.$route.params.__id__) {
return new CanvasContext(id, app.$route.params.__id__)
} else {
UniServiceJSBridge.emit('onError', 'createCanvasContext:fail')
}
}
<template> <template>
<uni-canvas/> <uni-canvas>
<canvas
ref="canvas"
:canvas-id="canvasId"
:disable-scroll="disableScroll"
:width="width"
:height="height"
@touchmove="_touchmove"
/>
</uni-canvas>
</template> </template>
<script> <script>
import {
subscriber
} from 'uni-mixins'
function resolveColor (color) {
color = color.slice(0)
color[3] = color[3] / 255
return 'rgba(' + color.join(',') + ')'
}
export default { export default {
name: 'Canvas' name: 'Canvas',
mixins: [subscriber],
props: {
canvasId: {
type: String,
default: ''
},
disableScroll: {
type: [Boolean, String],
default: false
}
},
data () {
return {
width: 300,
height: 150,
actionsWaiting: false
}
},
computed: {
id () {
return this.canvasId
}
},
created () {
this._actionsDefer = []
this._images = {}
},
mounted () {
var canvas = this.$refs.canvas
this.width = canvas.offsetWidth
this.height = canvas.offsetHeight
},
methods: {
_handleSubscribe ({
type,
data = {}
}) {
var method = this[type]
if (type.indexOf('_') !== 0 && typeof method === 'function') {
method(data)
}
},
_touchmove (event) {
if (this.disableScroll) {
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++) {
let action = actions[index]
let method = action.method
let data = action.data
if (/^set/.test(method) && method !== 'setTransform') {
let 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') {
let LinearGradient = c2d.createLinearGradient(...data[1])
data[2].forEach(function (data2) {
let offset = data2[0]
let color = resolveColor(data2[1])
LinearGradient.addColorStop(offset, color)
})
} else if (data[0] === 'radial') {
let x = data[1][0]
let y = data[1][1]
let r = data[1][2]
let LinearGradient = c2d.createRadialGradient(x, y, 0, x, y, r)
data[2].forEach(function (data2) {
let offset = data2[0]
let color = resolveColor(data2[1])
LinearGradient.addColorStop(offset, color)
})
} else if (data[0] === 'pattern') {
let 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') {
c2d.font = c2d.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 {
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('onDrawCanvas', {
errMsg: 'drawCanvas:ok',
callbackId
}, this.$page.id)
}
},
preloadImage: function (actions) {
var sefl = this
actions.forEach(function (action) {
var method = action.method
var data = action.data
var src = ''
if (method === 'drawImage') {
src = data[0]
src = sefl.$getRealPath(src)
data[0] = src
} else if (method === 'setFillStyle' && data[0] === 'pattern') {
src = data[1]
src = sefl.$getRealPath(src)
data[1] = src
}
if (src && !sefl._images[src]) {
loadImage()
}
/**
* 加载图像
*/
function loadImage () {
sefl._images[src] = new Image()
sefl._images[src].onload = function () {
sefl._images[src].ready = true
}
/**
* 从Blob加载
* @param {Blob} blob
*/
function loadBlob (blob) {
sefl._images[src].src = window.URL.createObjectURL(blob)
}
/**
* 从本地文件加载
* @param {string} path 文件路径
*/
function loadFile (path) {
var bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
bitmap.load(path, function () {
sefl._images[src].src = bitmap.toBase64Data()
bitmap.clear()
}, function () {
bitmap.clear()
console.error('preloadImage error')
})
}
/**
* 从网络加载
* @param {string} url 文件地址
*/
function loadUrl (url) {
function plusDownload () {
plus.downloader.createDownload(url, {
filename: '_doc/uniapp_temp/download/'
}, function (d, status) {
if (status === 200) {
loadFile(d.filename)
}
}).start()
}
var xhr = new XMLHttpRequest()
xhr.open('GET', url, true)
xhr.responseType = 'blob'
xhr.onload = function () {
if (this.status === 200) {
loadBlob(this.response)
}
}
xhr.onerror = plusDownload
xhr.send()
}
// 解决 plus-app wkwebview 图像跨域问题
if (window.plus && window.webkit && window.webkit.messageHandlers) {
if (src.indexOf('http://') === 0 || src.indexOf('https://') === 0) {
loadUrl(src)
} else if (/^data:[a-z-]+\/[a-z-]+;base64,/.test(src)) {
sefl._images[src].src = src
} else {
loadFile(src)
}
} else {
sefl._images[src].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
}
}
}
} }
</script> </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>
...@@ -3,12 +3,13 @@ import { ...@@ -3,12 +3,13 @@ import {
} from 'uni-shared' } from 'uni-shared'
export default { export default {
props: { // 取消id的定义,某些组件(canvas)内不在props内定义id
id: { // props: {
type: String, // id: {
default: '' // type: String,
} // default: ''
}, // }
// },
mounted () { mounted () {
this._toggleListeners('subscribe', this.id) // 初始化监听 this._toggleListeners('subscribe', this.id) // 初始化监听
this.$watch('id', (newId, oldId) => { // watch id this.$watch('id', (newId, oldId) => { // watch id
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册