EP-WachatApp/eaterplanet_ecommerce/components/painter/painter.js

870 lines
26 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, { penCache, clearPenCache } from './lib/pen';
import Downloader from './lib/downloader';
import WxCanvas from './lib/wx-canvas';
const util = require('./lib/util');
const calc = require('./lib/calc');
const downloader = new Downloader();
// 最大尝试的绘制次数
const MAX_PAINT_COUNT = 5;
const ACTION_DEFAULT_SIZE = 24;
const ACTION_OFFSET = '2rpx';
Component({
canvasWidthInPx: 0,
canvasHeightInPx: 0,
canvasNode: null,
paintCount: 0,
currentPalette: {},
outterDisabled: false,
isDisabled: false,
needClear: false,
/**
* 组件的属性列表
*/
properties: {
use2D: {
type: Boolean,
},
customStyle: {
type: String,
},
// 运行自定义选择框和删除缩放按钮
customActionStyle: {
type: Object,
},
palette: {
type: Object,
observer: function (newVal, oldVal) {
if (this.isNeedRefresh(newVal, oldVal)) {
this.paintCount = 0;
clearPenCache();
this.startPaint();
}
},
},
dancePalette: {
type: Object,
observer: function (newVal, oldVal) {
if (!this.isEmpty(newVal) && !this.properties.use2D) {
clearPenCache();
this.initDancePalette(newVal);
}
},
},
// 缩放比,会在传入的 palette 中统一乘以该缩放比
scaleRatio: {
type: Number,
value: 1,
},
widthPixels: {
type: Number,
value: 0,
},
// 启用脏检查,默认 false
dirty: {
type: Boolean,
value: false,
},
LRU: {
type: Boolean,
value: false,
},
action: {
type: Object,
observer: function (newVal, oldVal) {
if (newVal && !this.isEmpty(newVal) && !this.properties.use2D) {
this.doAction(newVal, null, false, true);
}
},
},
disableAction: {
type: Boolean,
observer: function (isDisabled) {
this.outterDisabled = isDisabled;
this.isDisabled = isDisabled;
},
},
clearActionBox: {
type: Boolean,
observer: function (needClear) {
if (needClear && !this.needClear) {
if (this.frontContext) {
setTimeout(() => {
this.frontContext.draw();
}, 100);
this.touchedView = {};
this.prevFindedIndex = this.findedIndex;
this.findedIndex = -1;
}
}
this.needClear = needClear;
},
},
},
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;
},
getBox(rect, type) {
const boxArea = {
type: 'rect',
css: {
height: `${rect.bottom - rect.top}px`,
width: `${rect.right - rect.left}px`,
left: `${rect.left}px`,
top: `${rect.top}px`,
borderWidth: '4rpx',
borderColor: '#1A7AF8',
color: 'transparent',
},
};
if (type === 'text') {
boxArea.css = Object.assign({}, boxArea.css, {
borderStyle: 'dashed',
});
}
if (this.properties.customActionStyle && this.properties.customActionStyle.border) {
boxArea.css = Object.assign({}, boxArea.css, this.properties.customActionStyle.border);
}
Object.assign(boxArea, {
id: 'box',
});
return boxArea;
},
getScaleIcon(rect, type) {
let scaleArea = {};
const { customActionStyle } = this.properties;
if (customActionStyle && customActionStyle.scale) {
scaleArea = {
type: 'image',
url: type === 'text' ? customActionStyle.scale.textIcon : customActionStyle.scale.imageIcon,
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
},
};
} else {
scaleArea = {
type: 'rect',
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
color: '#0000ff',
},
};
}
scaleArea.css = Object.assign({}, scaleArea.css, {
align: 'center',
left: `${rect.right + ACTION_OFFSET.toPx()}px`,
top:
type === 'text'
? `${rect.top - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`
: `${rect.bottom - ACTION_OFFSET.toPx() - scaleArea.css.height.toPx() / 2}px`,
});
Object.assign(scaleArea, {
id: 'scale',
});
return scaleArea;
},
getDeleteIcon(rect) {
let deleteArea = {};
const { customActionStyle } = this.properties;
if (customActionStyle && customActionStyle.scale) {
deleteArea = {
type: 'image',
url: customActionStyle.delete.icon,
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
},
};
} else {
deleteArea = {
type: 'rect',
css: {
height: `${2 * ACTION_DEFAULT_SIZE}rpx`,
width: `${2 * ACTION_DEFAULT_SIZE}rpx`,
borderRadius: `${ACTION_DEFAULT_SIZE}rpx`,
color: '#0000ff',
},
};
}
deleteArea.css = Object.assign({}, deleteArea.css, {
align: 'center',
left: `${rect.left - ACTION_OFFSET.toPx()}px`,
top: `${rect.top - ACTION_OFFSET.toPx() - deleteArea.css.height.toPx() / 2}px`,
});
Object.assign(deleteArea, {
id: 'delete',
});
return deleteArea;
},
doAction(action, callback, isMoving, overwrite) {
if (this.properties.use2D) {
return;
}
let newVal = null;
if (action) {
newVal = action.view;
}
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 || this.isEmpty(doView)) {
return;
}
if (newVal && newVal.css) {
if (overwrite) {
doView.css = newVal.css;
} else 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);
}
}
if (newVal && newVal.rect) {
doView.rect = newVal.rect;
}
if (newVal && newVal.url && doView.url && newVal.url !== doView.url) {
downloader
.download(newVal.url, this.properties.LRU)
.then(path => {
if (newVal.url.startsWith('https')) {
doView.originUrl = newVal.url;
}
doView.url = path;
wx.getImageInfo({
src: path,
success: res => {
doView.sHeight = res.height;
doView.sWidth = res.width;
this.reDraw(doView, callback, isMoving);
},
fail: () => {
this.reDraw(doView, callback, isMoving);
},
});
})
.catch(error => {
// 未下载成功,直接绘制
console.error(error);
this.reDraw(doView, callback, isMoving);
});
} else {
newVal && newVal.text && doView.text && newVal.text !== doView.text && (doView.text = newVal.text);
newVal &&
newVal.content &&
doView.content &&
newVal.content !== doView.content &&
(doView.content = newVal.content);
this.reDraw(doView, callback, isMoving);
}
},
reDraw(doView, callback, isMoving) {
const draw = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: this.isEmpty(doView) ? [] : [doView],
};
const pen = new Pen(this.globalContext, draw);
pen.paint(callbackInfo => {
callback && callback(callbackInfo);
this.triggerEvent('viewUpdate', {
view: this.touchedView,
});
});
const { rect, css, type } = doView;
this.block = {
width: this.currentPalette.width,
height: this.currentPalette.height,
views: this.isEmpty(doView) ? [] : [this.getBox(rect, doView.type)],
};
if (css && css.scalable) {
this.block.views.push(this.getScaleIcon(rect, type));
}
if (css && css.deletable) {
this.block.views.push(this.getDeleteIcon(rect));
}
const topBlock = new Pen(this.frontContext, this.block);
topBlock.paint();
},
isInView(x, y, rect) {
return x > rect.left && y > rect.top && x < rect.right && y < rect.bottom;
},
isInDelete(x, y) {
for (const view of this.block.views) {
if (view.id === 'delete') {
return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
}
}
return false;
},
isInScale(x, y) {
for (const view of this.block.views) {
if (view.id === 'scale') {
return x > view.rect.left && y > view.rect.top && x < view.rect.right && y < view.rect.bottom;
}
}
return false;
},
touchedView: {},
findedIndex: -1,
onClick() {
const x = this.startX;
const y = this.startY;
const totalLayerCount = this.currentPalette.views.length;
let canBeTouched = [];
let isDelete = false;
let deleteIndex = -1;
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.isInDelete(x, y, rect)) {
canBeTouched.length = 0;
deleteIndex = i;
isDelete = true;
break;
}
if (this.isInView(x, y, rect)) {
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;
this.triggerEvent('viewClicked', {
view: this.touchedView,
});
}
}
if (this.findedIndex < 0 || (this.touchedView && !this.touchedView.id)) {
// 证明点击了背景 或无法移动的view
this.frontContext.draw();
if (isDelete) {
this.triggerEvent('touchEnd', {
view: this.currentPalette.views[deleteIndex],
index: deleteIndex,
type: 'delete',
});
this.doAction();
} else if (this.findedIndex < 0) {
this.triggerEvent('viewClicked', {});
}
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();
new Pen(this.topContext, topDraw).paint();
} else {
new Pen(this.topContext, topDraw).paint();
this.doAction();
new Pen(this.bottomContext, bottomDraw).paint();
}
this.prevFindedIndex = this.findedIndex;
},
startX: 0,
startY: 0,
startH: 0,
startW: 0,
isScale: false,
startTimeStamp: 0,
onTouchStart(event) {
if (this.isDisabled) {
return;
}
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 (this.isInScale(x, y, rect)) {
this.isScale = true;
this.startH = rect.bottom - rect.top;
this.startW = rect.right - rect.left;
} else {
this.isScale = false;
}
} else {
this.isScale = false;
}
},
onTouchEnd(e) {
if (this.isDisabled) {
return;
}
const current = new Date().getTime();
if (current - this.startTimeStamp <= 500 && !this.hasMove) {
!this.isScale && this.onClick(e);
} else if (this.touchedView && !this.isEmpty(this.touchedView)) {
this.triggerEvent('touchEnd', {
view: this.touchedView,
});
}
this.hasMove = false;
},
onTouchCancel(e) {
if (this.isDisabled) {
return;
}
this.onTouchEnd(e);
},
hasMove: false,
onTouchMove(event) {
if (this.isDisabled) {
return;
}
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) {
clearPenCache(this.touchedView.id);
const newW = this.startW + offsetX > 1 ? this.startW + offsetX : 1;
if (this.touchedView.css && this.touchedView.css.minWidth) {
if (newW < this.touchedView.css.minWidth.toPx()) {
return;
}
}
if (this.touchedView.rect && this.touchedView.rect.minWidth) {
if (newW < this.touchedView.rect.minWidth) {
return;
}
}
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(
{
view: {
css,
},
},
null,
!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, this.properties.scaleRatio);
},
initDancePalette() {
if (this.properties.use2D) {
return;
}
this.isDisabled = true;
this.initScreenK();
this.downloadImages(this.properties.dancePalette).then(async 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 = await this.getCanvasContext(this.properties.use2D, 'front'));
this.bottomContext || (this.bottomContext = await this.getCanvasContext(this.properties.use2D, 'bottom'));
this.topContext || (this.topContext = await this.getCanvasContext(this.properties.use2D, 'top'));
this.globalContext || (this.globalContext = await this.getCanvasContext(this.properties.use2D, 'k-canvas'));
new Pen(this.bottomContext, palette, this.properties.use2D).paint(() => {
this.isDisabled = false;
this.isDisabled = this.outterDisabled;
this.triggerEvent('didShow');
});
this.globalContext.draw();
this.frontContext.draw();
this.topContext.draw();
});
this.touchedView = {};
},
startPaint() {
this.initScreenK();
const { width, height } = this.properties.palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
let needScale = false;
// 生成图片时,根据设置的像素值重新绘制
if (width.toPx() !== this.canvasWidthInPx) {
this.canvasWidthInPx = width.toPx();
needScale = this.properties.use2D;
}
if (this.properties.widthPixels) {
setStringPrototype(this.screenK, this.properties.widthPixels / this.canvasWidthInPx);
this.canvasWidthInPx = this.properties.widthPixels;
}
if (this.canvasHeightInPx !== height.toPx()) {
this.canvasHeightInPx = height.toPx();
needScale = needScale || this.properties.use2D;
}
this.setData(
{
photoStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`,
},
function () {
this.downloadImages(this.properties.palette).then(async palette => {
if (!this.photoContext) {
this.photoContext = await this.getCanvasContext(this.properties.use2D, 'photo');
}
if (needScale) {
const scale = getApp().systemInfo.pixelRatio;
this.photoContext.width = this.canvasWidthInPx * scale;
this.photoContext.height = this.canvasHeightInPx * scale;
this.photoContext.scale(scale, scale);
}
new Pen(this.photoContext, palette).paint(() => {
this.saveImgToLocal();
});
setStringPrototype(this.screenK, this.properties.scaleRatio);
});
},
);
},
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, this.properties.LRU).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, this.properties.LRU).then(
path => {
view.originUrl = view.url;
view.url = path;
wx.getImageInfo({
src: path,
success: res => {
// 获得一下图片信息,供后续裁减使用
view.sWidth = res.width;
view.sHeight = res.height;
},
fail: error => {
// 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
console.warn(`getImageInfo ${view.originUrl} failed, ${JSON.stringify(error)}`);
view.url = '';
},
complete: () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
});
},
() => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
},
);
}
}
}
if (preCount === 0) {
resolve(paletteCopy);
}
});
},
saveImgToLocal() {
const that = this;
const optionsOf2d = {
canvas: that.canvasNode,
}
const optionsOfOld = {
canvasId: 'photo',
destWidth: that.canvasWidthInPx,
destHeight: that.canvasHeightInPx,
}
setTimeout(() => {
wx.canvasToTempFilePath(
{
...(that.properties.use2D ? optionsOf2d : optionsOfOld),
success: function (res) {
that.getImageInfo(res.tempFilePath);
},
fail: function (error) {
console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
that.triggerEvent('imgErr', {
error: error,
});
},
},
this,
);
}, 300);
},
getCanvasContext(use2D, id) {
const that = this;
return new Promise(resolve => {
if (use2D) {
const query = wx.createSelectorQuery().in(that);
const selectId = `#${id}`;
query
.select(selectId)
.fields({ node: true, size: true })
.exec(res => {
that.canvasNode = res[0].node;
const ctx = that.canvasNode.getContext('2d');
const wxCanvas = new WxCanvas('2d', ctx, id, true, that.canvasNode);
resolve(wxCanvas);
});
} else {
const temp = wx.createCanvasContext(id, that);
resolve(new WxCanvas('mina', temp, id, true));
}
});
},
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 */
/**
* string 到对应的 px
* @param {Number} baseSize 当设置了 % 号时,设置的基准值
*/
String.prototype.toPx = function toPx(_, baseSize) {
if (this === '0') {
return 0;
}
const REG = /-?[0-9]+(\.[0-9]+)?(rpx|px|%)/;
const parsePx = origin => {
const results = new RegExp(REG).exec(origin);
if (!origin || !results) {
console.error(`The size: ${origin} is illegal`);
return 0;
}
const unit = results[2];
const value = parseFloat(origin);
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));
} else if (unit === '%') {
res = Math.round((value * baseSize) / 100);
}
return res;
};
const formula = /^calc\((.+)\)$/.exec(this);
if (formula && formula[1]) {
// 进行 calc 计算
const afterOne = formula[1].replace(/([^\s\(\+\-\*\/]+)\.(left|right|bottom|top|width|height)/g, word => {
const [id, attr] = word.split('.');
return penCache.viewRect[id][attr];
});
const afterTwo = afterOne.replace(new RegExp(REG, 'g'), parsePx);
return calc(afterTwo);
} else {
return parsePx(this);
}
};
}