mina-painter
Version:
一个小程序图片生成库,轻松通过 json 方式绘制一张可以发到朋友圈的图片
638 lines (634 loc) • 22.3 kB
JavaScript
import Nerv from "nervjs";
import Taro, { getImageInfo as _getImageInfo, getSystemInfoSync as _getSystemInfoSync, canvasToTempFilePath as _canvasToTempFilePath, createSelectorQuery as _createSelectorQuery, createCanvasContext as _createCanvasContext } from "@tarojs/taro-h5";
import { View, Canvas, Block } from '@tarojs/components';
import Downloader from './lib/downloader';
import { isEmpty, isInView, isInDelete, isInScale, isNeedRefresh, getBox, getDeleteIcon, getScaleIcon } from './utils';
import WxCanvas from './lib/wx-canvas';
import * as Painter from 'painter-kernel';
const downloader = new Downloader();
const MAX_PAINT_COUNT = 5;
class Index extends Taro.Component {
render() {
const props = this.props;
const [state, setData] = Taro.useState({
picURL: '',
showCanvas: true,
painterStyle: ''
});
const refs = Taro.useRef({
imgSize: {},
needClear: false,
isDisabled: false,
currentPalette: {
width: '0rpx',
height: '0rpx',
background: '#ffffff',
views: []
},
findedIndex: -1,
startX: 0,
startY: 0,
startH: 0,
startW: 0,
isScale: false,
startTimeStamp: 0,
hasMove: false,
screenK: 0.5,
paintCount: 0,
canvasWidthInPx: 0,
canvasHeightInPx: 0,
outterDisabled: false,
canvasNode: null,
needScale: false
});
const frontContext = Taro.useRef();
const bottomContext = Taro.useRef();
const topContext = Taro.useRef();
const globalContext = Taro.useRef();
const photoContext = Taro.useRef();
const that = refs.current;
const scope = Taro.useScope();
function setState(params) {
setData(pre => {
return {
...pre,
...params
};
});
}
Taro.useEffect(() => {
Painter.initInjection({
loadImage: async url => {
return new Promise(resolve => {
if (!that.imgSize[url]) {
_getImageInfo({
src: url,
success: res => {
// 获得一下图片信息,供后续裁减使用
that.imgSize[url] = {
img: url,
width: res.width,
height: res.height
};
resolve(that.imgSize[url]);
},
fail: error => {
// 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了
resolve({
img: '',
width: 0,
height: 0
});
console.error(`getImageInfo ${url} failed, ${JSON.stringify(error)}`);
}
});
} else {
resolve(that.imgSize[url]);
}
});
}
});
}, []);
Taro.useEffect(() => {
if (isNeedRefresh(props.palette, that.oldPalette, props.dirty)) {
that.paintCount = 0;
Painter.clearPenCache();
that.oldPalette = JSON.parse(JSON.stringify(props.palette));
startPaint();
}
}, [props.palette]);
Taro.useEffect(() => {
if (!isEmpty(props.dancePalette)) {
Painter.clearPenCache();
initDancePalette();
}
}, [props.dancePalette]);
Taro.useEffect(() => {
if (props.action && !isEmpty(props.action)) {
doAction(props.action, true);
}
}, [props.action]);
Taro.useEffect(() => {
that.outterDisabled = props.disableAction || false;
that.isDisabled = props.disableAction || false;
}, [props.disableAction]);
Taro.useEffect(() => {
if (props.clearActionBox && !that.needClear) {
if (frontContext.current) {
setTimeout(() => {
frontContext.current.draw();
}, 100);
that.touchedView = {};
that.prevFindedIndex = that.findedIndex;
that.findedIndex = -1;
}
}
that.needClear = props.clearActionBox || false;
}, [props.clearActionBox]);
function doAction(action, overwrite) {
if (props.use2D) {
return;
}
const newVal = action ? action.view : undefined;
if (newVal && newVal.id && that.touchedView && that.touchedView.id !== newVal.id && that.currentPalette) {
// 带 id 的动作给撤回时使用,不带 id,表示对当前选中对象进行操作
const { views } = that.currentPalette;
for (let i = 0; i < views.length; i++) {
if (views[i].id === newVal.id) {
// 跨层回撤,需要重新构建三层关系
that.touchedView = views[i];
that.findedIndex = i;
sliceLayers();
break;
}
}
}
const doView = that.touchedView;
if (!doView || 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, props.LRU).then(path => {
if (newVal.url.startsWith('https')) {
doView.originUrl = newVal.url;
}
doView.url = path;
_getImageInfo({
src: path,
success: res => {
doView.sHeight = res.height;
doView.sWidth = res.width;
reDraw(doView);
},
fail: () => {
reDraw(doView);
}
});
}).catch(error => {
// 未下载成功,直接绘制
console.error(error);
reDraw(doView);
});
} 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);
reDraw(doView);
}
}
function reDraw(doView) {
const draw = {
width: that.currentPalette.width,
height: that.currentPalette.height,
views: isEmpty(doView) ? [] : [doView]
};
const pen = new Painter.Pen(globalContext.current, draw);
pen.paint(() => {
globalContext.current.draw();
props.onViewUpdate && props.onViewUpdate(that.touchedView);
});
const { rect, css, type } = doView;
that.block = {
width: that.currentPalette.width,
height: that.currentPalette.height,
views: isEmpty(doView) ? [] : [getBox(rect, doView.type, props.customActionStyle)]
};
if (css && css.scalable) {
that.block.views.push(getScaleIcon(rect, type, props.customActionStyle));
}
if (css && css.deletable) {
that.block.views.push(getDeleteIcon(rect, props.customActionStyle));
}
const topBlock = new Painter.Pen(frontContext.current, that.block);
topBlock.paint(() => {
frontContext.current.draw();
});
}
function onClick() {
const x = that.startX;
const y = that.startY;
const totalLayerCount = that.currentPalette.views.length;
let canBeTouched = [];
let isDelete = false;
let deleteIndex = -1;
for (let i = totalLayerCount - 1; i >= 0; i--) {
const view = that.currentPalette.views[i];
const { rect } = view;
if (that.touchedView && that.touchedView.id && that.touchedView.id === view.id && isInDelete(x, y, that.block)) {
canBeTouched.length = 0;
deleteIndex = i;
isDelete = true;
break;
}
if (isInView(x, y, rect)) {
canBeTouched.push({
view,
index: i
});
}
}
that.touchedView = {};
if (canBeTouched.length === 0) {
that.findedIndex = -1;
} else {
let i = 0;
const touchAble = canBeTouched.filter(item => Boolean(item.view.id));
if (touchAble.length === 0) {
that.findedIndex = canBeTouched[0].index;
} else {
for (i = 0; i < touchAble.length; i++) {
if (that.findedIndex === touchAble[i].index) {
i++;
break;
}
}
if (i === touchAble.length) {
i = 0;
}
that.touchedView = touchAble[i].view;
that.findedIndex = touchAble[i].index;
props.onViewClicked && props.onViewClicked(that.touchedView);
}
}
if (that.findedIndex < 0 || that.touchedView && !that.touchedView.id) {
// 证明点击了背景 或无法移动的view
frontContext.current && frontContext.current.draw();
if (isDelete) {
props.onTouchEnd && props.onTouchEnd({
view: that.currentPalette.views[deleteIndex],
index: deleteIndex,
type: 'delete'
});
doAction();
} else if (that.findedIndex < 0) {
props.onTouchEnd && props.onTouchEnd({});
}
that.findedIndex = -1;
that.prevFindedIndex = -1;
} else if (that.touchedView && that.touchedView.id) {
sliceLayers();
}
}
function sliceLayers() {
const bottomLayers = that.currentPalette.views.slice(0, that.findedIndex);
const topLayers = that.currentPalette.views.slice(that.findedIndex + 1);
const bottomDraw = {
width: that.currentPalette.width,
height: that.currentPalette.height,
background: that.currentPalette.background,
views: bottomLayers
};
const topDraw = {
width: that.currentPalette.width,
height: that.currentPalette.height,
views: topLayers
};
if (that.prevFindedIndex < that.findedIndex) {
new Painter.Pen(bottomContext.current, bottomDraw).paint(() => {
bottomContext.current.draw();
});
doAction();
new Painter.Pen(topContext.current, topDraw).paint(() => {
topContext.current.draw();
});
} else {
new Painter.Pen(topContext.current, topDraw).paint(() => {
topContext.current.draw();
});
doAction();
new Painter.Pen(bottomContext.current, bottomDraw).paint(() => {
bottomContext.current.draw();
});
}
that.prevFindedIndex = that.findedIndex;
}
function onTouchStart(event) {
if (that.isDisabled) {
return;
}
const { x, y } = event.touches[0];
that.startX = x;
that.startY = y;
that.startTimeStamp = Date.now();
if (that.touchedView && !isEmpty(that.touchedView)) {
const { rect } = that.touchedView;
if (isInScale(x, y, that.block) && rect) {
that.isScale = true;
that.startH = rect.bottom - rect.top;
that.startW = rect.right - rect.left;
} else {
that.isScale = false;
}
} else {
that.isScale = false;
}
}
function onTouchEnd() {
if (that.isDisabled) {
return;
}
const current = Date.now();
if (current - that.startTimeStamp <= 500 && !that.hasMove) {
!that.isScale && onClick();
} else if (that.touchedView && !isEmpty(that.touchedView)) {
props.onTouchEnd && props.onTouchEnd({
view: that.touchedView
});
}
that.hasMove = false;
}
function onTouchMove(event) {
if (that.isDisabled) {
return;
}
that.hasMove = true;
if (!that.touchedView || that.touchedView && !that.touchedView.id) {
return;
}
const { x, y } = event.touches[0];
const offsetX = x - that.startX;
const offsetY = y - that.startY;
const { rect, type } = that.touchedView;
let css = {};
if (that.isScale) {
Painter.clearPenCache(that.touchedView.id);
const newW = that.startW + offsetX > 1 ? that.startW + offsetX : 1;
if (that.touchedView.css && that.touchedView.css.minWidth) {
if (newW < Painter.toPx(that.touchedView.css.minWidth)) {
return;
}
}
if (that.touchedView.rect && that.touchedView.rect.minWidth) {
if (newW < that.touchedView.rect.minWidth) {
return;
}
}
const newH = that.startH + offsetY > 1 ? that.startH + offsetY : 1;
css = {
width: `${newW}px`
};
if (type !== 'text') {
if (type === 'image') {
css.height = `${newW * that.startH / that.startW}px`;
} else {
css.height = `${newH}px`;
}
}
} else {
that.startX = x;
that.startY = y;
css = {
left: `${rect.x + offsetX}px`,
top: `${rect.y + offsetY}px`,
right: undefined,
bottom: undefined
};
}
doAction({
view: {
css
}
});
}
function initScreenK() {
if (!(Taro.getApp() && Taro.getApp().systemInfo && Taro.getApp().systemInfo.screenWidth)) {
try {
Taro.getApp().systemInfo = _getSystemInfoSync();
} catch (e) {
console.error(`Painter get system info failed, ${JSON.stringify(e)}`);
return;
}
}
if (Taro.getApp().systemInfo && Taro.getApp().systemInfo.screenWidth) {
that.screenK = Taro.getApp().systemInfo.screenWidth / 750;
}
Painter.setStringPrototype(that.screenK, props.scaleRatio);
}
function initDancePalette() {
if (props.use2D) {
return;
}
that.isDisabled = true;
initScreenK();
downloadImages(props.dancePalette).then(async palette => {
that.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;
}
setState({
// @ts-ignore
painterStyle: `width:${Painter.toPx(width)}px;height:${Painter.toPx(height)}px;`
});
frontContext.current || (frontContext.current = await getCanvasContext(props.use2D, 'front'));
bottomContext.current || (bottomContext.current = await getCanvasContext(props.use2D, 'bottom'));
topContext.current || (topContext.current = await getCanvasContext(props.use2D, 'top'));
globalContext.current || (globalContext.current = await getCanvasContext(props.use2D, 'k-canvas'));
new Painter.Pen(bottomContext.current, palette).paint(() => {
that.isDisabled = that.outterDisabled;
bottomContext.current.draw();
props.onDidShow && props.onDidShow();
});
globalContext.current.draw();
frontContext.current.draw();
topContext.current.draw();
});
that.touchedView = {};
}
function startPaint() {
initScreenK();
const { width, height } = props.palette;
if (!width || !height) {
console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`);
return;
}
// 生成图片时,根据设置的像素值重新绘制
if (that.canvasWidthInPx !== Painter.toPx(width)) {
that.canvasWidthInPx = Painter.toPx(width);
that.needScale = !!props.use2D;
}
if (props.widthPixels) {
Painter.setStringPrototype(that.screenK, props.widthPixels / that.canvasWidthInPx);
that.canvasWidthInPx = props.widthPixels;
}
if (that.canvasHeightInPx !== Painter.toPx(height)) {
that.canvasHeightInPx = Painter.toPx(height);
that.needScale = that.needScale || !!props.use2D;
}
const newPhotoStyle = `width:${that.canvasWidthInPx}px;height:${that.canvasHeightInPx}px;`;
setState({
photoStyle: state.photoStyle === newPhotoStyle ? newPhotoStyle + ';' : newPhotoStyle
});
}
Taro.useEffect(() => {
downloadImages(props.palette).then(async palette => {
if (palette === null) {
return;
}
photoContext.current || (photoContext.current = await getCanvasContext(props.use2D, 'photo'));
if (that.needScale) {
const scale = Taro.getApp().systemInfo.pixelRatio;
// @ts-ignore
photoContext.current.width = that.canvasWidthInPx * scale;
// @ts-ignore
photoContext.current.height = that.canvasHeightInPx * scale;
photoContext.current.scale(scale, scale);
}
new Painter.Pen(photoContext.current, palette).paint(() => {
photoContext.current.draw();
saveImgToLocal();
});
Painter.setStringPrototype(that.screenK, props.scaleRatio);
});
}, [state.photoStyle]);
function downloadImages(palette) {
return new Promise(resolve => {
if (!palette) {
resolve(null);
return;
}
let preCount = 0;
let completeCount = 0;
const paletteCopy = JSON.parse(JSON.stringify(palette));
if (paletteCopy.background) {
preCount++;
downloader.download(paletteCopy.background, props.LRU).then(path => {
paletteCopy.background = path;
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
}, () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
});
}
if (paletteCopy.views) {
for (let i = 0; i < paletteCopy.views.length; i++) {
const view = paletteCopy.views[i];
if (view && view.type === 'image' && view.url) {
preCount++;
/* eslint-disable no-loop-func */
downloader.download(view.url, props.LRU).then(path => {
view.originUrl = view.url;
view.url = path;
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
}, () => {
completeCount++;
if (preCount === completeCount) {
resolve(paletteCopy);
}
});
}
}
}
if (preCount === 0) {
resolve(paletteCopy);
}
});
}
function saveImgToLocal() {
setTimeout(() => {
_canvasToTempFilePath({
canvasId: 'photo',
// @ts-ignore
canvas: !!props.use2D ? that.canvasNode : undefined,
destWidth: that.canvasWidthInPx,
destHeight: that.canvasHeightInPx
}, scope).then(res => {
getImageInfo(res.tempFilePath);
}).catch(error => {
console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`);
props.onImgErr && props.onImgErr({
error
});
});
}, 300);
}
function getCanvasContext(use2D, id) {
return new Promise(resolve => {
if (use2D) {
const query = _createSelectorQuery().in(scope);
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 = _createCanvasContext(id, scope);
resolve(new WxCanvas('mina', temp, id, true));
}
});
}
function getImageInfo(filePath) {
_getImageInfo({
src: filePath
}).then(infoRes => {
if (that.paintCount > MAX_PAINT_COUNT) {
const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`;
console.error(error);
props.onImgErr && props.onImgErr({
error
});
return;
}
// 比例相符时才证明绘制成功,否则进行强制重绘制
if (Math.abs((infoRes.width * that.canvasHeightInPx - that.canvasWidthInPx * infoRes.height) / (infoRes.height * that.canvasHeightInPx)) < 0.01) {
props.onImgOK && props.onImgOK(filePath);
} else {
startPaint();
}
that.paintCount++;
}).catch(error => {
console.error(`getImageInfo failed, ${JSON.stringify(error)}`);
props.onImgErr && props.onImgErr({
error
});
});
}
return <View style={`position: relative;${props.customStyle};${state.painterStyle};`}>
{props.use2D ? <Nerv.Fragment>
<Canvas type="2d" id="photo" style={`${state.photoStyle}`} />
</Nerv.Fragment> : <Nerv.Fragment>
<Canvas canvas-id="photo" style={`${state.photoStyle};position: absolute; left: -9999px; top: -9999rpx;`} />
<Canvas canvas-id="bottom" style={`${state.painterStyle};position: absolute;`} />
<Canvas canvas-id="k-canvas" style={`${state.painterStyle};position: absolute;`} />
<Canvas canvas-id="top" style={`${state.painterStyle};position: absolute;`} />
<Canvas canvas-id="front" style={`${state.painterStyle};position: absolute;`} onTouchStart={onTouchStart} onTouchMove={onTouchMove} onTouchEnd={onTouchEnd} onTouchCancel={onTouchEnd} disableScroll={true} />
</Nerv.Fragment>}
</View>;
}
}
Index.defaultProps = {
scaleRatio: 1,
widthPixels: 0,
dirty: false,
LRU: true
};
Index.options = {
addGlobalClass: true
};
export default Index;