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); } }; }