PainterCore/painter.js
2019-11-18 14:08:07 +08:00

624 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Pen from './lib/pen';
import Downloader from './lib/downloader';
const util = require('./lib/util');
const downloader = new Downloader();
// 最大尝试的绘制次数
const MAX_PAINT_COUNT = 5;
const ACTION_POINT_RADIUS = 10;
Component({
canvasWidthInPx: 0,
canvasHeightInPx: 0,
paintCount: 0,
currentPalette: {},
movingCache: {},
/**
* 组件的属性列表
*/
properties: {
customStyle: {
type: String,
},
palette: {
type: Object,
observer: function(newVal, oldVal) {
if (this.isNeedRefresh(newVal, oldVal)) {
this.paintCount = 0;
this.startPaint();
}
},
},
dancePalette: {
type: Object,
observer: function(newVal, oldVal) {
if (!this.isEmpty(newVal)) {
this.initDancePalette(newVal);
}
},
},
widthPixels: {
type: Number,
value: 0
},
// 启用脏检查,默认 false
dirty: {
type: Boolean,
value: false,
},
action: {
type: Object,
observer: function(newVal, oldVal) {
if (newVal) {
this.doAction(newVal)
}
},
}
},
data: {
picURL: '',
showCanvas: true,
painterStyle: '',
},
methods: {
/**
* 判断一个 object 是否为 空
* @param {object} object
*/
isEmpty(object) {
for (const i in object) {
return false;
}
return true;
},
isNeedRefresh(newVal, oldVal) {
if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) {
return false;
}
return true;
},
doAction(newVal, callback, isMoving) {
if (newVal && newVal.id && this.touchedView.id !== newVal.id) {
// 带 id 的动作给撤回时使用,不带 id表示对当前选中对象进行操作
const {
views
} = this.currentPalette;
for (let i = 0; i < views.length; i++) {
if (views[i].id === newVal.id) {
// 跨层回撤,需要重新构建三层关系
this.touchedView = views[i];
this.findedIndex = i;
this.sliceLayers();
break
}
}
}
const doView = this.touchedView
if (!doView) {
return
}
if (newVal && newVal.css) {
if (Array.isArray(doView.css) && Array.isArray(newVal.css)) {
doView.css = Object.assign({}, ...doView.css, ...newVal.css)
} else if (Array.isArray(doView.css)) {
doView.css = Object.assign({}, ...doView.css, newVal.css)
} else if (Array.isArray(newVal.css)) {
doView.css = Object.assign({}, doView.css, ...newVal.css)
} else {
doView.css = Object.assign({}, doView.css, newVal.css)
}
}
const draw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: this.isEmpty(doView) ? [] : [doView]
}
const pen = new Pen(this.globalContext, draw);
if (isMoving && this.currentPalette.views[0].type === 'text') {
pen.paint(callback, true, this.movingCache);
} else {
pen.paint(callback)
}
const {
rect
} = doView
const block = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: this.isEmpty(this.touchedView) ? [] : [{
type: 'rect',
css: {
height: `${rect.bottom - rect.top}px`,
width: `${rect.right - rect.left}px`,
left: `${rect.left}px`,
top: `${rect.top}px`,
borderWidth: '2rpx',
borderColor: '#0000ff',
color: 'transparent'
}
}, {
type: 'rect',
css: {
height: `${2 * ACTION_POINT_RADIUS}px`,
width: `${2 * ACTION_POINT_RADIUS}px`,
borderRadius: `${ACTION_POINT_RADIUS}px`,
color: '#0000ff',
left: `${rect.right - ACTION_POINT_RADIUS}px`,
top: `${rect.bottom - ACTION_POINT_RADIUS}px`
}
}]
}
if (this.touchedView.type === 'text') {
block.views.push({
type: 'rect',
css: {
height: `${2 * ACTION_POINT_RADIUS}px`,
width: `${2 * ACTION_POINT_RADIUS}px`,
borderRadius: `${ACTION_POINT_RADIUS}px`,
color: '#0000ff',
left: `${rect.left - ACTION_POINT_RADIUS}px`,
top: `${rect.top - ACTION_POINT_RADIUS}px`
}
})
}
const topBlock = new Pen(this.frontContext, block)
topBlock.paint();
},
inArea(x, y, rect, hasTouchedView) {
return (hasTouchedView &&
((x > rect.left &&
y > rect.top &&
x < rect.right &&
y < rect.bottom) ||
(x > rect.right - ACTION_POINT_RADIUS &&
y > rect.bottom - ACTION_POINT_RADIUS &&
x < rect.right + ACTION_POINT_RADIUS &&
y < rect.bottom + ACTION_POINT_RADIUS))
) ||
(x > rect.left &&
y > rect.top &&
x < rect.right &&
y < rect.bottom
)
},
isDelete(x, y, rect) {
return (x > rect.left - ACTION_POINT_RADIUS &&
y > rect.top - ACTION_POINT_RADIUS &&
x < rect.left + ACTION_POINT_RADIUS &&
y < rect.top + ACTION_POINT_RADIUS)
},
touchedView: {},
findedIndex: -1,
onClick() {
const x = this.startX
const y = this.startY
const totalLayerCount = this.currentPalette.views.length
let canBeTouched = []
let isDelete = false
for (let i = totalLayerCount - 1; i >= 0; i--) {
const view = this.currentPalette.views[i]
const {
rect
} = view
if (this.touchedView && this.touchedView.id &&
this.touchedView.id === view.id &&
this.isDelete(x, y, rect)) {
canBeTouched.length = 0
this.currentPalette.views.splice(i, 1)
isDelete = true
break
}
if (this.inArea(x, y, rect, !this.isEmpty(this.touchedView))) {
canBeTouched.push({
view,
index: i
})
}
}
this.touchedView = {}
if (canBeTouched.length === 0) {
this.findedIndex = -1
} else {
let i = 0
const touchAble = canBeTouched.filter(item => Boolean(item.view.id))
if (touchAble.length === 0) {
this.findedIndex = canBeTouched[0].index
} else {
for (i = 0; i < touchAble.length; i++) {
if (this.findedIndex === touchAble[i].index) {
i++
break
}
}
if (i === touchAble.length) {
i = 0
}
this.touchedView = touchAble[i].view
this.findedIndex = touchAble[i].index
const {
id,
css
} = this.touchedView
this.triggerEvent('touchStart', {
id: id,
css: css,
})
}
}
if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
// 证明点击了背景 或无法移动的view
const block = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: []
}
const topBlock = new Pen(this.frontContext, block)
topBlock.paint();
if (isDelete) {
this.doAction()
} else if (this.findedIndex < 0) {
this.triggerEvent('touchStart', {})
}
this.findedIndex = -1
this.prevFindedIndex = -1
} else if (this.touchedView && this.touchedView.id) {
this.sliceLayers()
}
},
sliceLayers() {
const bottomLayers = this.currentPalette.views.slice(0, this.findedIndex)
const topLayers = this.currentPalette.views.slice(this.findedIndex + 1)
const bottomDraw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
background: this.currentPalette.background,
views: bottomLayers
}
const topDraw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: topLayers
}
if (this.prevFindedIndex < this.findedIndex) {
new Pen(this.bottomContext, bottomDraw).paint();
this.doAction(null, (callbackInfo) => {
this.movingCache = callbackInfo
})
new Pen(this.topContext, topDraw).paint();
} else {
new Pen(this.topContext, topDraw).paint();
this.doAction(null, (callbackInfo) => {
this.movingCache = callbackInfo
})
new Pen(this.bottomContext, bottomDraw).paint();
}
this.prevFindedIndex = this.findedIndex
},
startX: 0,
startY: 0,
startH: 0,
startW: 0,
isScale: false,
startTimeStamp: 0,
onTouchStart(event) {
const {
x,
y
} = event.touches[0]
this.startX = x
this.startY = y
this.startTimeStamp = new Date().getTime()
if (this.touchedView && !this.isEmpty(this.touchedView)) {
const {
rect
} = this.touchedView
if (rect.right - ACTION_POINT_RADIUS < x && x < rect.right + ACTION_POINT_RADIUS && rect.bottom - ACTION_POINT_RADIUS < y && y < rect.bottom + ACTION_POINT_RADIUS) {
this.isScale = true
this.movingCache = {}
this.startH = rect.bottom - rect.top
this.startW = rect.right - rect.left
} else {
this.isScale = false
}
}
},
onTouchEnd(e) {
const current = new Date().getTime()
if ((current - this.startTimeStamp) <= 500 && !this.hasMove) {
this.onClick(e)
} else if (this.touchedView && !this.isEmpty(this.touchedView)) {
this.triggerEvent('touchEnd', {
id: this.touchedView.id,
css: this.touchedView.css
})
}
this.hasMove = false
},
onTouchCancel(e) {
this.onTouchEnd(e)
},
hasMove: false,
onTouchMove(event) {
this.hasMove = true
if (!this.touchedView || (this.touchedView && !this.touchedView.id)) {
return
}
const {
x,
y
} = event.touches[0]
const offsetX = x - this.startX
const offsetY = y - this.startY
const {
rect,
type
} = this.touchedView
let css = {}
if (this.isScale) {
const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1
const newH = this.startH + offsetY > 1 ? this.startH + offsetY : 1
css = {
width: `${newW}px`,
}
if (type !== 'text') {
if (type === 'image') {
css.height = `${(newW) * this.startH / this.startW }px`
} else {
css.height = `${newH}px`
}
}
} else {
this.startX = x
this.startY = y
css = {
left: `${rect.x + offsetX}px`,
top: `${rect.y + offsetY}px`,
right: undefined,
bottom: undefined
}
}
this.doAction({
css
}, (callbackInfo) => {
if (this.isScale) {
this.movingCache = callbackInfo
}
}, !this.isScale)
},
initScreenK() {
if (!(getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth)) {
try {
getApp().systemInfo = wx.getSystemInfoSync();
} catch (e) {
console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
return;
}
}
this.screenK = 0.5;
if (getApp() && getApp().systemInfo && getApp().systemInfo.screenWidth) {
this.screenK = getApp().systemInfo.screenWidth / 750;
}
setStringPrototype(this.screenK, 1);
},
initDancePalette() {
this.initScreenK();
this.downloadImages(this.properties.dancePalette).then((palette) => {
this.currentPalette = palette
const {
width,
height
} = palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
this.setData({
painterStyle: `width:${width.toPx()}px;height:${height.toPx()}px;`,
});
this.frontContext || (this.frontContext = wx.createCanvasContext('front', this));
this.bottomContext || (this.bottomContext = wx.createCanvasContext('bottom', this));
this.topContext || (this.topContext = wx.createCanvasContext('top', this));
this.globalContext || (this.globalContext = wx.createCanvasContext('k-canvas', this));
new Pen(this.globalContext, palette).paint();
});
},
startPaint() {
this.initScreenK();
this.downloadImages(this.properties.palette).then((palette) => {
const {
width,
height
} = palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
// 生成图片时,根据设置的像素值重新绘制
this.canvasWidthInPx = width.toPx();
if (this.properties.widthPixels) {
setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx)
this.canvasWidthInPx = this.properties.widthPixels
}
this.canvasHeightInPx = height.toPx();
this.setData({
photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
});
this.photoContext || (this.photoContext = wx.createCanvasContext('photo', this));
new Pen(this.photoContext, palette).paint(() => {
this.saveImgToLocal();
});
setStringPrototype(this.screenK, 1);
});
},
downloadImages(palette) {
return new Promise((resolve, reject) => {
let preCount = 0;
let completeCount = 0;
const paletteCopy = JSON.parse(JSON.stringify(palette));
if (paletteCopy.background) {
preCount++;
downloader.download(paletteCopy.background).then((path) => {
paletteCopy.background = path;
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
}, () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
});
}
if (paletteCopy.views) {
for (const view of paletteCopy.views) {
if (view && view.type === 'image' && view.url) {
preCount++;
/* eslint-disable no-loop-func */
downloader.download(view.url).then((path) => {
view.url = path;
wx.getImageInfo({
src: view.url,
success: (res) => {
// 获得一下图片信息,供后续裁减使用
view.sWidth = res.width;
view.sHeight = res.height;
},
fail: (error) => {
// 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
view.url = "";
console.error(`getImageInfo ${view.url} failed, ${JSON.stringify(error)}`);
},
complete: () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
});
}, () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
});
}
}
}
if (preCount === 0) {
resolve(paletteCopy);
}
});
},
saveImgToLocal() {
const that = this;
setTimeout(() => {
wx.canvasToTempFilePath({
canvasId: 'photo',
success: function(res) {
that.getImageInfo(res.tempFilePath);
},
fail: function(error) {
console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
that.triggerEvent('imgErr', {
error: error
});
},
}, this);
}, 300);
},
getImageInfo(filePath) {
const that = this;
wx.getImageInfo({
src: filePath,
success: (infoRes) => {
if (that.paintCount > MAX_PAINT_COUNT) {
const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
console.error(error);
that.triggerEvent('imgErr', {
error: error
});
return;
}
// 比例相符时才证明绘制成功,否则进行强制重绘制
if (Math.abs((infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) / (infoRes.height * that.canvasHeightInPx)) < 0.01) {
that.triggerEvent('imgOK', {
path: filePath
});
} else {
that.startPaint();
}
that.paintCount++;
},
fail: (error) => {
console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
that.triggerEvent('imgErr', {
error: error
});
},
});
},
},
});
function setStringPrototype(screenK, scale) {
/* eslint-disable no-extend-native */
/**
* 是否支持负数
* @param {Boolean} minus 是否支持负数
*/
String.prototype.toPx = function toPx(minus) {
let reg;
if (minus) {
reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g;
} else {
reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g;
}
const results = reg.exec(this);
if (!this || !results) {
console.error(`The size: ${this} is illegal`);
return 0;
}
const unit = results[2];
const value = parseFloat(this);
let res = 0;
if (unit === 'rpx') {
res = Math.round(value * (screenK || 0.5) * (scale || 1));
} else if (unit === 'px') {
res = Math.round(value * (scale || 1));
}
return res;
};
}