初始化仓库
This commit is contained in:
54
components/lime-painter/changelog.md
Normal file
54
components/lime-painter/changelog.md
Normal file
@ -0,0 +1,54 @@
|
||||
## 1.8.5.5(2021-08-17)
|
||||
- chore: 更新文档,删除 replace
|
||||
- fix: 修复 text 值为 number时报错
|
||||
## 1.8.5.4(2021-08-16)
|
||||
- fix: 字节小程序兼容
|
||||
## 1.8.5.3(2021-08-15)
|
||||
- fix: 修复线性渐变与css现实效果不一致的问题
|
||||
- chore: 更新文档
|
||||
## 1.8.5.2(2021-08-13)
|
||||
- chore: 增加`background-image`、`background-repeat` 能力,主要用于背景纹理的绘制,并不是代替`image`。例如:大面积的重复平铺的水印
|
||||
- 注意:这个功能H5暂时无法使用,因为[官方的API有BUG](https://ask.dcloud.net.cn/question/128793),待官方修复!!!
|
||||
## 1.8.5.1(2021-08-10)
|
||||
- fix: 修复因`margin`报错问题
|
||||
## 1.8.5(2021-08-09)
|
||||
- chore: 增加margin支持`auto`,以达到居中效果
|
||||
## 1.8.4(2021-08-06)
|
||||
- chore: 增加判断缓存文件条件
|
||||
- fix: 修复css 多余空格报错问题
|
||||
## 1.8.3(2021-08-04)
|
||||
- tips: 1.6.x 以下的版本升级到1.8.x后要为每个元素都加上定位:position: 'absolute'
|
||||
- fix: 修复只有一个view子元素时不计算高度的问题
|
||||
## 1.8.2(2021-08-03)
|
||||
- fix: 修复 path-type 为 `url` 无效问题
|
||||
- fix: 修复 qrcode `text` 为空时报错问题
|
||||
- fix: 修复 image `src` 动态设置时不生效问题
|
||||
- feat: 增加 css 属性 `min-width` `max-width`
|
||||
## 1.8.1(2021-08-02)
|
||||
- fix: 修复无法加载本地图片
|
||||
## 1.8.0(2021-08-02)
|
||||
- chore 文档更新
|
||||
- 使用旧版的同学不要升级!
|
||||
## 1.8.0-beta(2021-07-30)
|
||||
- ## 全新布局方式 不兼容旧版!
|
||||
- chore: 布局方式变更
|
||||
- tips: 微信canvas 2d 不支持真机调试
|
||||
## 1.6.6(2021-07-09)
|
||||
- chore: 统一命名规范,无须主动引入组件
|
||||
## 1.6.5(2021-06-08)
|
||||
- chore: 去掉console
|
||||
## 1.6.4(2021-06-07)
|
||||
- fix: 修复 数字 为纯字符串时不转换的BUG
|
||||
## 1.6.3(2021-06-06)
|
||||
- fix: 修复 PC 端放大的BUG
|
||||
## 1.6.2(2021-05-31)
|
||||
- fix: 修复 报`adaptor is not a function`错误
|
||||
- fix: 修复 text 多行高度
|
||||
- fix: 优化 默认文字的基准线
|
||||
- feat: `@progress`事件,监听绘制进度
|
||||
## 1.6.1(2021-02-28)
|
||||
- 删除多余节点
|
||||
## 1.6.0(2021-02-26)
|
||||
- 调整为uni_modules目录规范
|
||||
- 修复:transform的rotate不能为负数问题
|
||||
- 新增:`pathType` 指定生成图片返回的路径类型,可选值有 `base64`、`url`
|
||||
141
components/lime-painter/components/common/relation.js
Normal file
141
components/lime-painter/components/common/relation.js
Normal file
@ -0,0 +1,141 @@
|
||||
import {base64ToPath} from '../l-painter/utils.js'
|
||||
|
||||
const styles = (v = '') => v.split(';').filter(v => v && !/^[\n\s]+$/.test(v)).map(v => {
|
||||
const item = v.split(':');
|
||||
return {
|
||||
[item[0]
|
||||
.replace(/-([a-z])/g, function() {
|
||||
return arguments[1].toUpperCase()
|
||||
})
|
||||
.replace(/\s+/g, '')
|
||||
]: item?. [1]?.replace(/^\s+/, '')?.replace(/\s+$/, '') || ''
|
||||
}
|
||||
})
|
||||
export function parent(parent) {
|
||||
return {
|
||||
provide() {
|
||||
return {
|
||||
[parent]: this
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
el: {
|
||||
css: {},
|
||||
views: []
|
||||
},
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
css: {
|
||||
handler(v) {
|
||||
if (this.canvasId) {
|
||||
this.el.css = typeof v == 'object' ? v : v && Object.assign(...styles(v)) || {}
|
||||
this.canvasWidth = this.el.css?.width || this.canvasWidth
|
||||
this.canvasHeight = this.el.css?.height || this.canvasHeight
|
||||
}
|
||||
},
|
||||
immediate: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
export function children(parent, options = {}) {
|
||||
const indexKey = options.indexKey || 'index'
|
||||
return {
|
||||
inject: {
|
||||
[parent]: {
|
||||
default: null
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
el: {
|
||||
handler(v, o) {
|
||||
if (JSON.stringify(v) != JSON.stringify(o))
|
||||
this.bindRelation()
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
src: {
|
||||
handler(v, o) {
|
||||
if (v != o)
|
||||
this.bindRelation()
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
text: {
|
||||
handler(v, o) {
|
||||
if (v != o) this.bindRelation()
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
css: {
|
||||
handler(v, o) {
|
||||
if (v != o)
|
||||
this.el.css = typeof v == 'object' ? v : v && Object.assign(...styles(v)) || {}
|
||||
},
|
||||
immediate: true
|
||||
},
|
||||
replace: {
|
||||
handler(v, o) {
|
||||
if (JSON.stringify(v) != JSON.stringify(o))
|
||||
this.bindRelation()
|
||||
},
|
||||
deep: true,
|
||||
immediate: true
|
||||
},
|
||||
},
|
||||
created() {
|
||||
Object.defineProperty(this, 'parent', {
|
||||
get: () => {
|
||||
return this[parent]
|
||||
},
|
||||
})
|
||||
Object.defineProperty(this, 'index', {
|
||||
get: () => {
|
||||
this.bindRelation();
|
||||
return this.parent?.el.views?.indexOf(this.el)
|
||||
},
|
||||
});
|
||||
this.el.type = this.type
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.parent) {
|
||||
this.parent.el.views = this.parent.el.views.filter(
|
||||
(item) => item._uid !== this._uid
|
||||
);
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
bindRelation() {
|
||||
if (!this.el._uid) {
|
||||
this.el._uid = this._uid
|
||||
}
|
||||
if (['text', 'qrcode'].includes(this.type)) {
|
||||
this.el.text = this.$slots?.default?. [0]?.text || this.text?.replace(/\\n/g, '\n')
|
||||
}
|
||||
if (this.type == 'text' && this.replace) {
|
||||
this.el.replace = this.replace
|
||||
}
|
||||
if (this.type == 'image') {
|
||||
this.el.src = this.src
|
||||
}
|
||||
// || this.parent.el.views.indexOf(this.el) !== -1
|
||||
if (!this.parent) {
|
||||
return;
|
||||
}
|
||||
|
||||
let views = this.parent.el.views || [];
|
||||
if (views.indexOf(this.el) !== -1) {
|
||||
this.parent.el.views = views.map(v => v._uid == this._uid ? this.el : v)
|
||||
} else {
|
||||
this.parent.el.views = [...views, this.el];
|
||||
}
|
||||
}
|
||||
},
|
||||
mounted() {
|
||||
this.bindRelation()
|
||||
},
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<view></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {parent, children} from '../common/relation';
|
||||
export default {
|
||||
name: 'lime-painter-image',
|
||||
mixins:[children('painter')],
|
||||
props: {
|
||||
css: [String, Object],
|
||||
src: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: 'image',
|
||||
el: {
|
||||
css: {},
|
||||
src: null
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@ -0,0 +1,27 @@
|
||||
<template>
|
||||
<view></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {parent, children} from '../common/relation';
|
||||
export default {
|
||||
name: 'lime-painter-qrcode',
|
||||
mixins:[children('painter')],
|
||||
props: {
|
||||
css: [String, Object],
|
||||
text: String
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: 'qrcode',
|
||||
el: {
|
||||
css: {},
|
||||
text: null
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<text style="opacity: 0;"><slot/></text>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {parent, children} from '../common/relation';
|
||||
export default {
|
||||
name: 'lime-painter-text',
|
||||
mixins:[children('painter')],
|
||||
props: {
|
||||
css: [String, Object],
|
||||
text: [String, Number],
|
||||
replace: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: 'text',
|
||||
el: {
|
||||
css: {},
|
||||
text: null
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<view><slot/></view>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import {parent, children} from '../common/relation';
|
||||
export default {
|
||||
name: 'lime-painter-view',
|
||||
mixins:[children('painter'), parent('painter')],
|
||||
props: {
|
||||
css: [String, Object],
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
type: 'view',
|
||||
el: {
|
||||
css: {},
|
||||
views:[]
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
</style>
|
||||
523
components/lime-painter/components/l-painter/l-painter.vue
Normal file
523
components/lime-painter/components/l-painter/l-painter.vue
Normal file
@ -0,0 +1,523 @@
|
||||
<template>
|
||||
<view v-if="canvasId && size" class="lime-painter" :style="size + customStyle">
|
||||
<!-- #ifndef APP-NVUE -->
|
||||
<canvas class="lime-painter__canvas" v-if="use2dCanvas" :id="canvasId" type="2d" :style="size"></canvas>
|
||||
<canvas class="lime-painter__canvas" v-else :canvas-id="canvasId" :style="size" :id="canvasId" :width="boardWidth * dpr" :height="boardHeight * dpr"></canvas>
|
||||
|
||||
<!-- #endif -->
|
||||
<!-- #ifdef APP-NVUE -->
|
||||
<web-view
|
||||
:style="size"
|
||||
ref="webview"
|
||||
class="lime-painter__canvas"
|
||||
@pagefinish="onPageFinish"
|
||||
@error="onError"
|
||||
@onPostMessage="onMessage"
|
||||
></web-view>
|
||||
<!-- #endif -->
|
||||
<slot/>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
import { toPx, compareVersion, sleep, base64ToPath, pathToBase64, getImageInfo, isBase64 } from './utils';
|
||||
import {parent} from '../common/relation'
|
||||
// #ifndef APP-NVUE
|
||||
import {Painter} from './painter'
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
import painterScript from './nvue'
|
||||
// #endif
|
||||
export default {
|
||||
name: 'lime-painter',
|
||||
mixins:[parent('painter')],
|
||||
props: {
|
||||
board: Object,
|
||||
pathType: {
|
||||
type: String,
|
||||
// default: 'url'
|
||||
// 'base64'、'url'
|
||||
},
|
||||
fileType: {
|
||||
type: String,
|
||||
default: 'png'
|
||||
},
|
||||
quality: {
|
||||
type: Number,
|
||||
default: 1
|
||||
},
|
||||
css: [String, Object],
|
||||
width: [Number, String],
|
||||
height: [Number, String],
|
||||
pixelRatio: Number,
|
||||
customStyle: String,
|
||||
isCanvasToTempFilePath: Boolean,
|
||||
sleep: {
|
||||
type: Number,
|
||||
default: 1000 / 30
|
||||
},
|
||||
beforeDelay: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
afterDelay: {
|
||||
type: Number,
|
||||
default: 100
|
||||
},
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO||MP-ALIPAY
|
||||
type: {
|
||||
type: String,
|
||||
default: '2d'
|
||||
},
|
||||
// #endif
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
|
||||
use2dCanvas: true,
|
||||
// #endif
|
||||
// #ifndef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
|
||||
use2dCanvas: false,
|
||||
// #endif
|
||||
canvasHeight: 150,
|
||||
canvasWidth: null,
|
||||
isDrawIng: false,
|
||||
isPC: false,
|
||||
inited: false,
|
||||
name: 'view',
|
||||
progress: 0,
|
||||
// #ifdef APP-NVUE
|
||||
tempFilePath: [],
|
||||
// #endif
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
canvasId() {
|
||||
return `l-painter${this._uid}`
|
||||
},
|
||||
size() {
|
||||
if(this.boardWidth && this.boardHeight) {
|
||||
return `width:${this.boardWidth}px; height: ${this.boardHeight}px;`;
|
||||
}
|
||||
},
|
||||
dpr() {
|
||||
return this.pixelRatio || uni.getSystemInfoSync().pixelRatio;
|
||||
},
|
||||
boardWidth() {
|
||||
const {width = 0, canvasWidth = 0} = this
|
||||
const {width: boardWidth = 0} = this.board?.css || this.board || {}
|
||||
return Math.max(toPx(width || boardWidth), toPx(canvasWidth));
|
||||
},
|
||||
boardHeight() {
|
||||
const {height= 0, canvasHeight =0} = this
|
||||
const {height: boardHeight = 0} = this.board?.css || this.board || {}
|
||||
return Math.max(toPx(height || boardHeight), toPx(canvasHeight));
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
canvasWidth(v) {
|
||||
if(this.el.css && !this.el.css?.width) {
|
||||
this.el.css.width = v
|
||||
}
|
||||
},
|
||||
size(v) {
|
||||
// #ifdef MP-WEIXIN
|
||||
if (this.use2dCanvas) {
|
||||
this.inited = false;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
this.inited = false;
|
||||
// #endif
|
||||
},
|
||||
},
|
||||
// #ifdef MP-WEIXIN || MP-TOUTIAO || MP-ALIPAY
|
||||
created() {
|
||||
const { SDKVersion, version, platform, environment } = uni.getSystemInfoSync();
|
||||
// #ifdef MP-WEIXIN
|
||||
// ios wx7.0.20 createImage bug
|
||||
this.isPC = /windows/i.test(platform)
|
||||
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.9.2') >= 0 && !((/ios/i.test(platform) && /7.0.20/.test(version)) || /wxwork/i.test(environment)) && !this.isPC;
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '1.78.0') >= 0;
|
||||
// #endif
|
||||
// #ifdef MP-TOUTIAO
|
||||
this.use2dCanvas = this.type === '2d' && compareVersion(SDKVersion, '2.8.0') >= 0;
|
||||
// #endif
|
||||
},
|
||||
// #endif
|
||||
mounted() {
|
||||
// #ifdef APP-NVUE
|
||||
this.webViewInit()
|
||||
// #endif
|
||||
this.$nextTick(() => {
|
||||
setTimeout(() => {
|
||||
if(this.board) {
|
||||
this.$watch('board', this.watchRender, {deep: true,immediate: true});
|
||||
} else if(this.el.views.length) {
|
||||
this.$watch('el', this.watchRender, {deep: true,immediate: true});
|
||||
}
|
||||
},30)
|
||||
})
|
||||
},
|
||||
methods: {
|
||||
async watchRender(val) {
|
||||
this.progress = 0
|
||||
if (JSON.stringify(val) === '{}' || !val) return;
|
||||
clearTimeout(this.rendertimer)
|
||||
this.rendertimer = setTimeout(() => {
|
||||
this.render(val);
|
||||
}, this.beforeDelay)
|
||||
},
|
||||
async setFilePath(path, isEmit) {
|
||||
let filePath = path
|
||||
const {pathType} = this
|
||||
if(pathType == 'base64' && !isBase64(path)) {
|
||||
filePath = await pathToBase64(path)
|
||||
} else if(pathType == 'url' && isBase64(path)) {
|
||||
filePath = await base64ToPath(path)
|
||||
}
|
||||
if(isEmit) {
|
||||
this.$emit('success', filePath);
|
||||
}
|
||||
return filePath
|
||||
},
|
||||
// #ifdef APP-NVUE
|
||||
onError(e) {
|
||||
console.log('onError', e)
|
||||
},
|
||||
// onPagestart() {
|
||||
// console.log('onPagestart')
|
||||
// },
|
||||
// onPageFinish() {
|
||||
// this.$refs.webview.evalJS(`init()`)
|
||||
// },
|
||||
// onReceivedTitle() {
|
||||
// console.log('onReceivedTitle')
|
||||
// },
|
||||
onMessage(e) {
|
||||
const res = e?.detail?.data[0] || null;
|
||||
if (res?.event) {
|
||||
if(res.event == 'inited') {
|
||||
this.inited = true
|
||||
}
|
||||
if(res.event == 'layoutChange') {
|
||||
const data = JSON.parse(res.data)
|
||||
this.canvasWidth = data.width;
|
||||
this.canvasHeight = data.height;
|
||||
}
|
||||
if(res.event == 'progressChange') {
|
||||
this.progress = res.data * 1
|
||||
}
|
||||
if(res.event == 'file') {
|
||||
this.tempFilePath.push(res.data)
|
||||
if(this.tempFilePath.length > 7) {
|
||||
this.tempFilePath.shift()
|
||||
}
|
||||
return
|
||||
}
|
||||
if(res.event == 'success') {
|
||||
if(res.data) {
|
||||
this.tempFilePath.push(res.data)
|
||||
if(this.tempFilePath.length > 8) {
|
||||
this.tempFilePath.shift()
|
||||
}
|
||||
if(this.isCanvasToTempFilePath) {
|
||||
this.setFilePath(this.tempFilePath.join(''), true)
|
||||
}
|
||||
} else {
|
||||
this.$emit('fail')
|
||||
}
|
||||
return
|
||||
}
|
||||
this.$emit(res.event, JSON.parse(res.data));
|
||||
} else if (res?.file) {
|
||||
this.file = res.data;
|
||||
} else {
|
||||
console.error(res);
|
||||
}
|
||||
},
|
||||
async webViewInit() {
|
||||
await sleep(30)
|
||||
// await this.getWebViewInited()
|
||||
const webview = this.$refs.webview;
|
||||
webview.evalJS(painterScript)
|
||||
},
|
||||
getWebViewInited() {
|
||||
if(this.inited) return Promise.resolve(this.inited);
|
||||
return new Promise((resolve) => {
|
||||
this.$watch(
|
||||
'inited',
|
||||
async val => {if(val) {resolve(val)}},
|
||||
{immediate: true}
|
||||
);
|
||||
})
|
||||
},
|
||||
getTempFilePath() {
|
||||
if(this.tempFilePath.length == 8) return Promise.resolve(this.tempFilePath)
|
||||
return new Promise((resolve) => {
|
||||
this.$watch(
|
||||
'tempFilePath',
|
||||
async val => {if(val.length == 8) {resolve(val.join(''))}}
|
||||
);
|
||||
})
|
||||
},
|
||||
getWebViewDone() {
|
||||
if(this.progress == 1) return Promise.resolve(this.progress);
|
||||
return new Promise((resolve) => {
|
||||
this.$watch(
|
||||
'progress',
|
||||
async val => {
|
||||
if(val == 1) {
|
||||
this.$emit('done')
|
||||
resolve(val)
|
||||
}},
|
||||
{immediate: true}
|
||||
);
|
||||
})
|
||||
},
|
||||
async render(args) {
|
||||
const newNode = await this.calcImage(args);
|
||||
await this.getWebViewInited()
|
||||
const webview = this.$refs.webview;
|
||||
webview.evalJS(`source(${JSON.stringify(newNode)})`)
|
||||
if(this.isCanvasToTempFilePath) {
|
||||
await this.getWebViewDone()
|
||||
await sleep(this.afterDelay)
|
||||
const params = {fileType: this.fileType, quality: this.quality}
|
||||
webview.evalJS(`save(${JSON.stringify(params)})`)
|
||||
}
|
||||
},
|
||||
async calcImage(args) {
|
||||
let node = JSON.parse(JSON.stringify(args))
|
||||
const url = node.url || node.src
|
||||
if(node.type === "image" && url && !isBase64(url)) {
|
||||
const suffix = url.match(/\.(\w+)$/)[1]
|
||||
const {width = 0, height = 0, path, naturalSrc} = await getImageInfo(url)
|
||||
const src = await pathToBase64(path)
|
||||
node.src = src.replace(/^data:application[\w\/]+;base64/,'data:image/'+suffix+';base64')
|
||||
} else if(node.views?.length) {
|
||||
for (let i = 0; i < node.views.length; i++) {
|
||||
node.views[i] = await this.calcImage(node.views[i])
|
||||
}
|
||||
}
|
||||
return node
|
||||
},
|
||||
async canvasToTempFilePath(args = {}){
|
||||
this.tempFilePath = []
|
||||
this.$refs.webview.evalJS(`save(${JSON.stringify(args)})`)
|
||||
try{
|
||||
let tempFilePath = await this.getTempFilePath()
|
||||
tempFilePath = await this.setFilePath(tempFilePath)
|
||||
args.success({errMsg: "canvasToTempFilePath:ok", tempFilePath})
|
||||
}catch(e){
|
||||
args.fail({error: e})
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
// #ifndef APP-NVUE
|
||||
getParentWeith() {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select(`.lime-painter`)
|
||||
.boundingClientRect()
|
||||
.exec(res => {
|
||||
this.canvasWidth = Math.ceil(res[0].width)
|
||||
this.canvasHeight = res[0].height
|
||||
})
|
||||
},
|
||||
async update(args, single) {
|
||||
this.painter = null;
|
||||
// #ifdef MP-WEIXIN
|
||||
if (this.use2dCanvas) {
|
||||
this.ctx = null;
|
||||
this.inited = false;
|
||||
}
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
this.inited = false;
|
||||
// #endif
|
||||
this.isDrawIng = false;
|
||||
await new Promise(resolve => this.$nextTick(resolve));
|
||||
await sleep(200);
|
||||
return this.render(args, single);
|
||||
},
|
||||
async render(args = {}, single = false) {
|
||||
if (this.isDrawIng) {
|
||||
return this.update(args, single);
|
||||
}
|
||||
this.isDrawIng = true;
|
||||
const isArg = JSON.stringify(args) != '{}';
|
||||
const ctx = await this.getContext();
|
||||
let { use2dCanvas, boardWidth, boardHeight, canvas, afterDelay } = this;
|
||||
if (use2dCanvas && !canvas) {
|
||||
return Promise.reject(new Error('render: fail canvas has not been created'));
|
||||
}
|
||||
this.boundary = {
|
||||
top: 0,
|
||||
left: 0,
|
||||
width: boardWidth,
|
||||
height: boardHeight
|
||||
};
|
||||
// if (!single) {
|
||||
// ctx.clearRect(0, 0, boardWidth, boardHeight);
|
||||
// }
|
||||
if(!this.painter) {
|
||||
this.painter = new Painter({context: ctx, canvas, width: boardWidth, height: boardHeight, pixelRatio: this.dpr}, this)
|
||||
}
|
||||
this.painter.listen('progressChange', (v) => {
|
||||
this.$emit('progress', v)
|
||||
})
|
||||
const {width, height} = await this.painter.source(args)
|
||||
this.canvasHeight = toPx(this.el.css?.height) || height
|
||||
this.canvasWidth = toPx(this.el.css?.width) || width
|
||||
this.boundary.height = this.canvasHeight
|
||||
this.boundary.width = this.canvasWidth
|
||||
await sleep(this.sleep);
|
||||
await this.painter.render()
|
||||
|
||||
await new Promise(resolve => this.$nextTick(resolve));
|
||||
if (!use2dCanvas && !single) {
|
||||
await this.canvasDraw();
|
||||
}
|
||||
if (afterDelay && use2dCanvas) {
|
||||
await sleep(afterDelay);
|
||||
}
|
||||
this.$emit('done');
|
||||
if (this.isCanvasToTempFilePath && !single && this.isDrawIng) {
|
||||
this.canvasToTempFilePath()
|
||||
.then(async res => {
|
||||
this.$emit('success', res.tempFilePath)
|
||||
})
|
||||
.catch(err => {
|
||||
this.$emit('fail', new Error(JSON.stringify(err)));
|
||||
});
|
||||
}
|
||||
this.isDrawIng = false;
|
||||
return Promise.resolve({ ctx, draw: this.painter, node: this.node });
|
||||
},
|
||||
async custom(cb) {
|
||||
const { ctx, draw } = await this.render({}, true);
|
||||
ctx.save();
|
||||
await cb(ctx, draw);
|
||||
ctx.restore();
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
async single(args = {}) {
|
||||
const res = await this.render(args, true);
|
||||
return Promise.resolve(res);
|
||||
},
|
||||
canvasDraw(flag = false) {
|
||||
return new Promise((resolve, reject) => this.ctx.draw(flag, () => setTimeout(() => resolve(), this.afterDelay)));
|
||||
},
|
||||
async getContext() {
|
||||
if (this.ctx && this.inited) {
|
||||
return Promise.resolve(this.ctx);
|
||||
}
|
||||
this.getParentWeith()
|
||||
const { type, use2dCanvas, dpr, boardWidth, boardHeight } = this;
|
||||
const _getContext = () => {
|
||||
return new Promise(resolve => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select(`#${this.canvasId}`)
|
||||
.boundingClientRect()
|
||||
.exec(res => {
|
||||
if (res) {
|
||||
const ctx = uni.createCanvasContext(this.canvasId, this);
|
||||
if (!this.inited) {
|
||||
this.inited = true;
|
||||
this.use2dCanvas = false;
|
||||
this.canvas = res;
|
||||
}
|
||||
if(this.isPC) {
|
||||
ctx.scale(1/dpr, 1/dpr);
|
||||
}
|
||||
// #ifdef MP-ALIPAY
|
||||
ctx.scale(dpr, dpr);
|
||||
// #endif
|
||||
this.ctx = ctx
|
||||
resolve(this.ctx);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
// #ifndef MP-WEIXIN
|
||||
return _getContext();
|
||||
// #endif
|
||||
|
||||
if (!use2dCanvas) {
|
||||
return _getContext();
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
uni.createSelectorQuery()
|
||||
.in(this)
|
||||
.select(`#${this.canvasId}`)
|
||||
.node()
|
||||
.exec(res => {
|
||||
let { node: canvas } = res[0];
|
||||
if (!canvas) {
|
||||
this.use2dCanvas = false;
|
||||
resolve(this.getContext());
|
||||
}
|
||||
const ctx = canvas.getContext(type);
|
||||
if (!this.inited) {
|
||||
this.inited = true;
|
||||
this.use2dCanvas = true;
|
||||
this.canvas = canvas;
|
||||
}
|
||||
this.ctx = ctx
|
||||
resolve(this.ctx);
|
||||
});
|
||||
});
|
||||
},
|
||||
canvasToTempFilePath(args = {}) {
|
||||
const { use2dCanvas, canvasId, dpr, fileType, quality } = this;
|
||||
return new Promise((resolve, reject) => {
|
||||
let { top: y = 0, left: x = 0, width, height } = this.boundary || this;
|
||||
let destWidth = width * dpr;
|
||||
let destHeight = height * dpr;
|
||||
// #ifdef MP-ALIPAY
|
||||
width = destWidth;
|
||||
height = destHeight;
|
||||
// #endif
|
||||
const success = async (res) => {
|
||||
const tempFilePath = await this.setFilePath(res.tempFilePath)
|
||||
resolve(Object.assign(res, {tempFilePath}))
|
||||
}
|
||||
const copyArgs = Object.assign({
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
destWidth,
|
||||
destHeight,
|
||||
canvasId,
|
||||
fileType,
|
||||
quality,
|
||||
success,
|
||||
fail: reject
|
||||
}, args);
|
||||
if (use2dCanvas) {
|
||||
delete copyArgs.canvasId;
|
||||
copyArgs.canvas = this.canvas;
|
||||
}
|
||||
uni.canvasToTempFilePath(copyArgs, this);
|
||||
});
|
||||
}
|
||||
// #endif
|
||||
}
|
||||
};
|
||||
</script>
|
||||
<style>
|
||||
.lime-painter, .lime-painter__canvas {
|
||||
// #ifndef APP-NVUE
|
||||
width: 100%;
|
||||
// #endif
|
||||
// #ifdef APP-NVUE
|
||||
flex: 1;
|
||||
// #endif
|
||||
}
|
||||
</style>
|
||||
103
components/lime-painter/components/l-painter/nvue.js
Normal file
103
components/lime-painter/components/l-painter/nvue.js
Normal file
@ -0,0 +1,103 @@
|
||||
const painterContent = `
|
||||
var cache = [];
|
||||
var painter = null;
|
||||
var canvas = null;
|
||||
var context = null;
|
||||
var timer = null;
|
||||
var pixelRatio = 1;
|
||||
console.log = function(...args) {
|
||||
postMessage(args);
|
||||
};
|
||||
function stringify(key, value) {
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
if (cache.indexOf(value) !== -1) {
|
||||
return;
|
||||
}
|
||||
cache.push(value);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
function emit(event, data) {
|
||||
let dataStr = typeof data !== 'object' && data !== null ? data : JSON.stringify(data, stringify);
|
||||
postMessage({
|
||||
event,
|
||||
data: dataStr
|
||||
});
|
||||
cache = [];
|
||||
};
|
||||
function postMessage(data) {
|
||||
uni.postMessage({
|
||||
data
|
||||
});
|
||||
};
|
||||
function init() {
|
||||
canvas = document.querySelector('#lime-painter');
|
||||
context = canvas.getContext('2d');
|
||||
pixelRatio = window.devicePixelRatio;
|
||||
painter = new Painter({
|
||||
id: 'lime-painter',
|
||||
context,
|
||||
canvas,
|
||||
pixelRatio,
|
||||
width: canvas.offsetWidth,
|
||||
height: canvas.offsetHeight
|
||||
});
|
||||
emit('inited', true);
|
||||
painter.listen('progressChange', (v) => {
|
||||
emit('progressChange', v);
|
||||
});
|
||||
};
|
||||
function save(args) {
|
||||
delete args.success;
|
||||
delete args.fail;
|
||||
clearTimeout(timer);
|
||||
timer = setTimeout(() => {
|
||||
const path = painter.save(args);
|
||||
if(typeof path == 'string') {
|
||||
const index = Math.ceil(path.length / 8);
|
||||
for (var i = 0; i < 8; i++) {
|
||||
if(i == 7) {
|
||||
emit('success', path.substr(i * index, index));
|
||||
} else {
|
||||
emit('file', path.substr(i * index, index));
|
||||
}
|
||||
};
|
||||
} else {
|
||||
emit('fail', '');
|
||||
};
|
||||
}, 30);
|
||||
};
|
||||
async function source(args) {
|
||||
let res = await painter.source(args);
|
||||
emit('layoutChange', res);
|
||||
await painter.render();
|
||||
};
|
||||
`
|
||||
export default `
|
||||
document.write("<canvas id='lime-painter'>不支持cavnas</canvas>");
|
||||
let meta = document.createElement('meta');
|
||||
meta.name = 'viewport';
|
||||
meta.content = 'width=device-width, initial-scale=1.0';
|
||||
document.head.appendChild(meta);
|
||||
let styleEl = document.createElement('style');
|
||||
styleEl.setAttribute('type', 'text/css');
|
||||
styleEl.textContent='html,body,#lime-painter{padding: 0; margin: 0; width:100%;height:100%}';
|
||||
document.head.appendChild(styleEl);
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.language = "javascript";
|
||||
script.src = "https://js.cdn.aliyun.dcloud.net.cn/dev/uni-app/uni.webview.1.5.2.js";
|
||||
script.onload = function() {
|
||||
var script = document.createElement("script");
|
||||
script.language = "javascript";
|
||||
script.src = "https://cdn.jsdelivr.net/gh/liangei/image@latest/lime-ui/lime-painter/painter.js";
|
||||
script.onload = function() {init()};
|
||||
document.head.appendChild(script);
|
||||
};
|
||||
document.head.appendChild(script);
|
||||
|
||||
var script = document.createElement("script");
|
||||
script.language = "javascript";
|
||||
script.text = "${painterContent}";
|
||||
document.body.appendChild(script);
|
||||
`
|
||||
15
components/lime-painter/components/l-painter/painter.js
Normal file
15
components/lime-painter/components/l-painter/painter.js
Normal file
File diff suppressed because one or more lines are too long
414
components/lime-painter/components/l-painter/utils.js
Normal file
414
components/lime-painter/components/l-painter/utils.js
Normal file
@ -0,0 +1,414 @@
|
||||
const networkReg = /^(http|\/\/)/;
|
||||
export const isBase64 = (path) => /^data:image\/(\w+);base64/.test(path);
|
||||
export function sleep(delay) {
|
||||
return new Promise(resolve => setTimeout(resolve, delay))
|
||||
}
|
||||
const isDev = ['devtools'].includes(uni.getSystemInfoSync().platform)
|
||||
// 缓存图片
|
||||
let cache = {}
|
||||
export function isNumber(value) {
|
||||
return /^-?\d+(\.\d+)?$/.test(value);
|
||||
}
|
||||
export function toPx(value, baseSize, isDecimal = false) {
|
||||
// 如果是数字
|
||||
if (typeof value === 'number') {
|
||||
return value
|
||||
}
|
||||
// 如果是字符串数字
|
||||
if (isNumber(value)) {
|
||||
return value * 1
|
||||
}
|
||||
// 如果有单位
|
||||
if (typeof value === 'string') {
|
||||
const reg = /^-?([0-9]+)?([.]{1}[0-9]+){0,1}(em|rpx|px|%)$/g
|
||||
const results = reg.exec(value);
|
||||
if (!value || !results) {
|
||||
return 0;
|
||||
}
|
||||
const unit = results[3];
|
||||
value = parseFloat(value);
|
||||
let res = 0;
|
||||
if (unit === 'rpx') {
|
||||
res = uni.upx2px(value);
|
||||
} else if (unit === 'px') {
|
||||
res = value * 1;
|
||||
} else if (unit === '%') {
|
||||
res = value * toPx(baseSize) / 100;
|
||||
} else if (unit === 'em') {
|
||||
res =value * toPx(baseSize || 14);
|
||||
}
|
||||
return isDecimal ? res.toFixed(2) * 1 : Math.round(res);
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
// 计算版本
|
||||
export function compareVersion(v1, v2) {
|
||||
v1 = v1.split('.')
|
||||
v2 = v2.split('.')
|
||||
const len = Math.max(v1.length, v2.length)
|
||||
while (v1.length < len) {
|
||||
v1.push('0')
|
||||
}
|
||||
while (v2.length < len) {
|
||||
v2.push('0')
|
||||
}
|
||||
for (let i = 0; i < len; i++) {
|
||||
const num1 = parseInt(v1[i], 10)
|
||||
const num2 = parseInt(v2[i], 10)
|
||||
|
||||
if (num1 > num2) {
|
||||
return 1
|
||||
} else if (num1 < num2) {
|
||||
return -1
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
// #ifdef MP
|
||||
export const prefix = () => {
|
||||
// #ifdef MP-TOUTIAO
|
||||
return tt
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN
|
||||
return wx
|
||||
// #endif
|
||||
// #ifdef MP-BAIDU
|
||||
return swan
|
||||
// #endif
|
||||
// #ifdef MP-ALIPAY
|
||||
return my
|
||||
// #endif
|
||||
// #ifdef MP-QQ
|
||||
return qq
|
||||
// #endif
|
||||
// #ifdef MP-360
|
||||
return qh
|
||||
// #endif
|
||||
}
|
||||
// #endif
|
||||
|
||||
const base64ToArrayBuffer = (data) => {
|
||||
// #ifndef MP-WEIXIN || APP-PLUS
|
||||
/**
|
||||
* Base64Binary.decode(base64_string);
|
||||
* Base64Binary.decodeArrayBuffer(base64_string);
|
||||
*/
|
||||
const Base64Binary = {
|
||||
_keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",
|
||||
/* will return a Uint8Array type */
|
||||
decodeArrayBuffer(input) {
|
||||
const bytes = (input.length/4) * 3;
|
||||
const ab = new ArrayBuffer(bytes);
|
||||
this.decode(input, ab);
|
||||
return ab;
|
||||
},
|
||||
removePaddingChars(input) {
|
||||
const lkey = this._keyStr.indexOf(input.charAt(input.length - 1));
|
||||
if(lkey == 64){
|
||||
return input.substring(0,input.length - 1);
|
||||
}
|
||||
return input;
|
||||
},
|
||||
decode(input, arrayBuffer) {
|
||||
//get last chars to see if are valid
|
||||
input = this.removePaddingChars(input);
|
||||
input = this.removePaddingChars(input);
|
||||
|
||||
const bytes = parseInt((input.length / 4) * 3, 10);
|
||||
|
||||
let uarray;
|
||||
let chr1, chr2, chr3;
|
||||
let enc1, enc2, enc3, enc4;
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
|
||||
if (arrayBuffer)
|
||||
uarray = new Uint8Array(arrayBuffer);
|
||||
else
|
||||
uarray = new Uint8Array(bytes);
|
||||
|
||||
input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");
|
||||
|
||||
for (i=0; i<bytes; i+=3) {
|
||||
//get the 3 octects in 4 ascii chars
|
||||
enc1 = this._keyStr.indexOf(input.charAt(j++));
|
||||
enc2 = this._keyStr.indexOf(input.charAt(j++));
|
||||
enc3 = this._keyStr.indexOf(input.charAt(j++));
|
||||
enc4 = this._keyStr.indexOf(input.charAt(j++));
|
||||
|
||||
chr1 = (enc1 << 2) | (enc2 >> 4);
|
||||
chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
|
||||
chr3 = ((enc3 & 3) << 6) | enc4;
|
||||
|
||||
uarray[i] = chr1;
|
||||
if (enc3 != 64) uarray[i+1] = chr2;
|
||||
if (enc4 != 64) uarray[i+2] = chr3;
|
||||
}
|
||||
return uarray;
|
||||
}
|
||||
}
|
||||
return Base64Binary.decodeArrayBuffer(data)
|
||||
// #endif
|
||||
// #ifdef MP-WEIXIN || APP-PLUS
|
||||
return uni.base64ToArrayBuffer(data)
|
||||
// #endif
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* base64转路径
|
||||
* @param {Object} base64
|
||||
*/
|
||||
export function base64ToPath(base64) {
|
||||
const [, format, bodyData] = /data:image\/(\w+);base64,(.*)/.exec(base64) || [];
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef MP
|
||||
const fs = uni.getFileSystemManager()
|
||||
//自定义文件名
|
||||
if (!format) {
|
||||
console.error('ERROR_BASE64SRC_PARSE')
|
||||
reject(new Error('ERROR_BASE64SRC_PARSE'))
|
||||
}
|
||||
const time = new Date().getTime();
|
||||
let pre = prefix()
|
||||
const filePath = `${pre.env.USER_DATA_PATH}/${time}.${format}`
|
||||
//let buffer = base64ToArrayBuffer(bodyData)
|
||||
console.log(filePath)
|
||||
fs.writeFile({
|
||||
filePath,
|
||||
data: base64.replace(/^data:\S+\/\S+;base64,/, ''),
|
||||
encoding: 'base64',
|
||||
// data: buffer,
|
||||
// encoding: 'binary',
|
||||
success() {
|
||||
resolve(filePath)
|
||||
},
|
||||
fail(err) {
|
||||
|
||||
console.log(base64,'!!!!!!')
|
||||
console.error('获取base64图片失败', JSON.stringify(err))
|
||||
reject(err)
|
||||
}
|
||||
})
|
||||
// #endif
|
||||
|
||||
// #ifdef H5
|
||||
// mime类型
|
||||
let mimeString = base64.split(',')[0].split(':')[1].split(';')[0];
|
||||
//base64 解码
|
||||
let byteString = atob(base64.split(',')[1]);
|
||||
//创建缓冲数组
|
||||
let arrayBuffer = new ArrayBuffer(byteString.length);
|
||||
//创建视图
|
||||
let intArray = new Uint8Array(arrayBuffer);
|
||||
for (let i = 0; i < byteString.length; i++) {
|
||||
intArray[i] = byteString.charCodeAt(i);
|
||||
}
|
||||
resolve(URL.createObjectURL(new Blob([intArray], { type: mimeString })))
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
const bitmap = new plus.nativeObj.Bitmap('bitmap' + Date.now())
|
||||
bitmap.loadBase64Data(base64, () => {
|
||||
if (!format) {
|
||||
reject(new Error('ERROR_BASE64SRC_PARSE'))
|
||||
}
|
||||
const time = new Date().getTime();
|
||||
const filePath = `_doc/uniapp_temp/${time}.${format}`
|
||||
bitmap.save(filePath, {},
|
||||
() => {
|
||||
bitmap.clear()
|
||||
resolve(filePath)
|
||||
},
|
||||
(error) => {
|
||||
bitmap.clear()
|
||||
console.error(`${JSON.stringify(error)}`)
|
||||
reject(error)
|
||||
})
|
||||
}, (error) => {
|
||||
bitmap.clear()
|
||||
console.error(`${JSON.stringify(error)}`)
|
||||
reject(error)
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 路径转base64
|
||||
* @param {Object} string
|
||||
*/
|
||||
export function pathToBase64(path) {
|
||||
if(/^data:/.test(path)) return path
|
||||
return new Promise((resolve, reject) => {
|
||||
// #ifdef H5
|
||||
const _canvas = ()=> {
|
||||
let image = new Image();
|
||||
image.setAttribute("crossOrigin",'Anonymous');
|
||||
image.onload = function() {
|
||||
let canvas = document.createElement('canvas');
|
||||
// 获取图片原始宽高
|
||||
canvas.width = this.naturalWidth;
|
||||
canvas.height = this.naturalHeight;
|
||||
// 将图片插入画布并开始绘制
|
||||
canvas.getContext('2d').drawImage(image, 0, 0);
|
||||
let result = canvas.toDataURL('image/png')
|
||||
resolve(result);
|
||||
canvas.height = canvas.width = 0
|
||||
}
|
||||
image.src = path
|
||||
image.onerror = (error) => {
|
||||
console.error(`urlToBase64 error: ${path}`, JSON.stringify(error))
|
||||
reject(new Error('urlToBase64 error'));
|
||||
};
|
||||
}
|
||||
const _fileReader = (blob) => {
|
||||
const fileReader = new FileReader();
|
||||
fileReader.onload = (e) => {
|
||||
resolve(e.target.result);
|
||||
};
|
||||
fileReader.readAsDataURL(blob);
|
||||
fileReader.onerror = (error) => {
|
||||
console.error('blobToBase64 error:', JSON.stringify(error))
|
||||
reject(new Error('blobToBase64 error'));
|
||||
};
|
||||
}
|
||||
const isFileReader = typeof FileReader === 'function'
|
||||
if(networkReg.test(path) && isFileReader ) {
|
||||
window.URL = window.URL || window.webkitURL;
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open("get", path, true);
|
||||
xhr.timeout = 2000;
|
||||
xhr.responseType = "blob";
|
||||
xhr.onload = function() {
|
||||
if(this.status == 200) {
|
||||
_fileReader(this.response)
|
||||
} else {
|
||||
_canvas()
|
||||
}
|
||||
}
|
||||
xhr.onreadystatechange = function() {
|
||||
if(this.status === 0) {
|
||||
console.error('图片跨域了,得后端处理咯')
|
||||
}
|
||||
}
|
||||
xhr.send();
|
||||
} else if(/^blob/.test(path) && isFileReader){
|
||||
_fileReader(path)
|
||||
} else {
|
||||
_canvas()
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef MP
|
||||
if(uni.canIUse('getFileSystemManager')) {
|
||||
uni.getFileSystemManager().readFile({
|
||||
filePath: path,
|
||||
encoding: 'base64',
|
||||
success: (res) => {
|
||||
resolve('data:image/png;base64,' + res.data)
|
||||
},
|
||||
fail: (error) => {
|
||||
console.error('urlToBase64 error:', JSON.stringify(error))
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
// #endif
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
plus.io.resolveLocalFileSystemURL(getLocalFilePath(path), (entry) => {
|
||||
entry.file((file) => {
|
||||
const fileReader = new plus.io.FileReader()
|
||||
fileReader.onload = (data) => { resolve(data.target.result)}
|
||||
fileReader.onerror = (error) => {
|
||||
console.error('pathToBase64 error:', JSON.stringify(error))
|
||||
reject(error)
|
||||
}
|
||||
fileReader.readAsDataURL(file)
|
||||
}, (error) => {
|
||||
console.error('pathToBase64 error:', JSON.stringify(error))
|
||||
reject(error)
|
||||
})
|
||||
}, (error) => {
|
||||
console.error('pathToBase64 error:', JSON.stringify(error))
|
||||
reject(error)
|
||||
})
|
||||
// #endif
|
||||
})
|
||||
}
|
||||
|
||||
// #ifdef APP-PLUS
|
||||
const getLocalFilePath = (path)=> {
|
||||
if (path.indexOf('_www') === 0 || path.indexOf('_doc') === 0 || path.indexOf('_documents') === 0 || path.indexOf('_downloads') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('file://') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('/storage/emulated/0/') === 0) {
|
||||
return path
|
||||
}
|
||||
if (path.indexOf('/') === 0) {
|
||||
const localFilePath = plus.io.convertAbsoluteFileSystem(path)
|
||||
if (localFilePath !== path) {
|
||||
return localFilePath
|
||||
} else {
|
||||
path = path.substr(1)
|
||||
}
|
||||
}
|
||||
return '_www/' + path
|
||||
}
|
||||
// #endif
|
||||
|
||||
export function getImageInfo(img, isH5PathToBase64 = false) {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
// const base64Reg = /^data:image\/(\w+);base64/
|
||||
const localReg = /^\.|^\/(?=[^\/])/;
|
||||
// #ifdef H5
|
||||
if(networkReg.test(img) && isH5PathToBase64) {
|
||||
img = await pathToBase64(img)
|
||||
}
|
||||
// #endif
|
||||
// #ifndef MP-ALIPAY
|
||||
if(isBase64(img)) {
|
||||
|
||||
if(isDev || !cache[img]) {
|
||||
const imgName = img
|
||||
img = await base64ToPath(img)
|
||||
cache[imgName] = img
|
||||
} else {
|
||||
img = cache[img]
|
||||
}
|
||||
}
|
||||
// #endif
|
||||
if(cache[img] && cache[img].errMsg) {
|
||||
resolve(cache[img])
|
||||
} else {
|
||||
uni.getImageInfo({
|
||||
src: img,
|
||||
success: (image) => {
|
||||
// #ifdef MP-WEIXIN || MP-BAIDU || MP-QQ || MP-TOUTIAO
|
||||
image.path = localReg.test(img) ? `/${image.path}` : image.path;
|
||||
// #endif
|
||||
// #ifdef H5
|
||||
image.path = image.path.replace(/^\./, window.location.origin)
|
||||
// #endif
|
||||
image.naturalSrc = img
|
||||
if(isDev) {
|
||||
resolve(image)
|
||||
} else {
|
||||
cache[img] = image
|
||||
resolve(cache[img])
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
resolve({path: img})
|
||||
console.error(`getImageInfo:fail ${img} failed ${JSON.stringify(err)}`);
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
756
components/lime-painter/readme.md
Normal file
756
components/lime-painter/readme.md
Normal file
@ -0,0 +1,756 @@
|
||||
# Painter 画板 测试版
|
||||
> uniapp 海报画板,更优雅的海报生成方案
|
||||
> [查看更多 站点1](https://limeui.qcoon.cn/#/painter)
|
||||
> [查看更多 站点2](http://liangei.gitee.io/limeui/#/painter)
|
||||
> Q群:806744170
|
||||
|
||||
## 平台兼容
|
||||
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
|
||||
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
|
||||
| √ | √ | √ | 未测 | √ | √ | √ |
|
||||
|
||||
|
||||
## 代码演示
|
||||
### 基本用法
|
||||
- 插件提供JSON及HTML的方式绘制海报
|
||||
- 插件参考了 css 块状流布局模拟css schema方式,放弃了之前使用的绝对定位布局。
|
||||
|
||||
#### 方式一 HTML
|
||||
- 插件提供了`l-painter-view`、`l-painter-text`、`l-painter-image`、`l-painter-qrcode`四种类型组件
|
||||
- 通过 `css` 属性绘制样式,与style使用方式保持一致。 因为style是保留字段,所以命名为`css`,如果有大佬知道如何破解请告之。
|
||||
|
||||
|
||||
```html
|
||||
<l-painter >
|
||||
<l-painter-view css="background: #07c160; height: 120rpx; width: 120rpx; display: inline-block"></l-painter-view>
|
||||
<l-painter-view css="background: #1989fa; height: 120rpx; width: 120rpx; border-top-right-radius: 60rpx; border-bottom-left-radius: 60rpx; display: inline-block; margin: 0 30rpx;"></l-painter-view>
|
||||
<l-painter-view css="background: #ff9d00; height: 120rpx; width: 120rpx; border-radius: 50%; display: inline-block"></l-painter-view>
|
||||
</l-painter>
|
||||
```
|
||||
|
||||
#### 方式二 JSON schema
|
||||
- 在json里四种类型组件的`type`为`view`、`text`、`image`、`qrcode`
|
||||
- 通过 `board` 设置海报所需的 JSON schema 数据进行绘制
|
||||
- 所有类型的schema都具有`css`字段,css的样式属性key值使用驼峰命名如:`lineHeight`
|
||||
|
||||
|
||||
```html
|
||||
<l-painter :board="poster" />
|
||||
```
|
||||
```js
|
||||
data() {
|
||||
return {
|
||||
poster: {
|
||||
css: {
|
||||
// json 方式务必填写画板宽度
|
||||
width: '750rpx'
|
||||
},
|
||||
views: [
|
||||
{
|
||||
css: {
|
||||
background: "#07c160",
|
||||
height: "120rpx",
|
||||
width: "120rpx",
|
||||
display: "inline-block"
|
||||
},
|
||||
type: "view"
|
||||
},
|
||||
{
|
||||
css: {
|
||||
background: "#1989fa",
|
||||
height: "120rpx",
|
||||
width: "120rpx",
|
||||
borderTopRightRadius: "60rpx",
|
||||
borderBottomLeftRadius: "60rpx",
|
||||
display: "inline-block",
|
||||
margin: "0 30rpx"
|
||||
},
|
||||
views: [],
|
||||
type: "view"
|
||||
},
|
||||
{
|
||||
css: {
|
||||
background: "#ff9d00",
|
||||
height: "120rpx",
|
||||
width: "120rpx",
|
||||
borderRadius: "50%",
|
||||
display: "inline-block"
|
||||
},
|
||||
views: [],
|
||||
type: "view"
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### View 容器
|
||||
- 类似于 `div` 可以嵌套承载更多的 view、text、image,qrcode共同构建一颗完整的节点树
|
||||
- 在JSON schema里具有 `views` 的数组字段,用于嵌套承载节点。
|
||||
|
||||
|
||||
#### 方式一 HTML
|
||||
|
||||
|
||||
```html
|
||||
<l-painter >
|
||||
<l-painter-view css="background: #f0f0f0; padding-top: 100rpx;">
|
||||
<l-painter-view css="background: #d9d9d9; width: 33.33%; height: 100rpx; display: inline-block"></l-painter-view>
|
||||
<l-painter-view css="background: #bfbfbf; width: 66.66%; height: 100rpx; display: inline-block"></l-painter-view>
|
||||
</l-painter-view>
|
||||
</l-painter>
|
||||
```
|
||||
#### 方式二 JSON schema
|
||||
|
||||
|
||||
```js
|
||||
{
|
||||
css: {
|
||||
width: '750rpx'
|
||||
},
|
||||
views: [
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#f0f0f0',
|
||||
paddingTop: '100rpx'
|
||||
},
|
||||
views: [
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#d9d9d9',
|
||||
width: '33.33%',
|
||||
height: '100rpx',
|
||||
display: 'inline-block'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#bfbfbf',
|
||||
width: '66.66%',
|
||||
height: '100rpx',
|
||||
display: 'inline-block'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Text 文本
|
||||
- 通过 `text` 属性填写文本内容。
|
||||
- 支持`\n`换行符
|
||||
- 支持省略号,使用css的`line-clamp`设置行数,当文字内容超过会显示省略号。
|
||||
- 支持`text-decoration`
|
||||
|
||||
#### 方式一 HTML
|
||||
```html
|
||||
<l-painter >
|
||||
<l-painter-view css="background: #e0e2db; padding: 30rpx; color: #222a29">
|
||||
<l-painter-text
|
||||
text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"/>
|
||||
<l-painter-text
|
||||
text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
|
||||
css="text-align:center; padding-top: 20rpx; text-decoration: line-through "
|
||||
/>
|
||||
<l-painter-text
|
||||
text="登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼"
|
||||
css="text-align:right; padding-top: 20rpx"
|
||||
/>
|
||||
<l-painter-text
|
||||
text="水调歌头\n明月几时有?把酒问青天。不知天上宫阙,今夕是何年。我欲乘风归去,又恐琼楼玉宇,高处不胜寒。起舞弄清影,何似在人间。"
|
||||
css="line-clamp: 3; padding-top: 20rpx"
|
||||
/>
|
||||
</l-painter-view>
|
||||
</l-painter>
|
||||
```
|
||||
#### 方式二 JSON schema
|
||||
```js
|
||||
// 基础用法
|
||||
{
|
||||
type: 'text',
|
||||
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
|
||||
css: {
|
||||
// 设置居中对齐
|
||||
textAlign: 'center',
|
||||
// 设置中划线
|
||||
textDecoration: 'line-through'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
|
||||
css: {
|
||||
// 设置右对齐
|
||||
textAlign: 'right',
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'text',
|
||||
text: '登鹳雀楼\n白日依山尽,黄河入海流\n欲穷千里目,更上一层楼',
|
||||
css: {
|
||||
// 设置行数,超出显示省略号
|
||||
lineClamp: 3,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Image 图片
|
||||
- 通过 `src` 属性填写图片路径。
|
||||
- 图片路径支持:网络图片,本地static里的图片路径,缓存路径
|
||||
- 通过 `css` 的 `object-fit`属性可以设置图片的填充方式,可选值见下方CSS表格。
|
||||
- 通过 `css` 的 `object-position`配合 `object-fit` 可以设置图片的对齐方式,类似于`background-position`,详情见下方CSS表格。
|
||||
- 使用网络图片时:小程序需要去公众平台配置 [downloadFile](https://mp.weixin.qq.com/) 域名
|
||||
- 使用网络图片时:**H5需要决跨域问题**
|
||||
|
||||
#### 方式一 HTML
|
||||
```html
|
||||
<l-painter >
|
||||
<!-- 基础用法 -->
|
||||
<l-painter-image
|
||||
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
|
||||
css="width: 200rpx; height: 200rpx"
|
||||
/>
|
||||
<!-- 填充方式 -->
|
||||
<!-- css object-fit 设置 填充方式 见下方表格-->
|
||||
<l-painter-image
|
||||
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
|
||||
css="width: 200rpx; height: 200rpx; object-fit: contain; background: #eee"
|
||||
/>
|
||||
<!-- css object-position 设置 图片的对齐方式-->
|
||||
<l-painter-image
|
||||
src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg"
|
||||
css="width: 200rpx; height: 200rpx; object-fit: contain; object-position: 50% 50%; background: #eee"
|
||||
/>
|
||||
|
||||
</l-painter>
|
||||
```
|
||||
#### 方式二 JSON schema
|
||||
```js
|
||||
// 基础用法
|
||||
{
|
||||
type: 'image',
|
||||
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
|
||||
css: {
|
||||
width: '200rpx',
|
||||
height: '200rpx'
|
||||
}
|
||||
},
|
||||
// 填充方式
|
||||
// css objectFit 设置 填充方式 见下方表格
|
||||
{
|
||||
type: 'image',
|
||||
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
|
||||
css: {
|
||||
width: '200rpx',
|
||||
height: '200rpx',
|
||||
objectFit: 'contain'
|
||||
}
|
||||
},
|
||||
// css objectPosition 设置 图片的对齐方式
|
||||
{
|
||||
type: 'image',
|
||||
src: 'https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg',
|
||||
css: {
|
||||
width: '200rpx',
|
||||
height: '200rpx',
|
||||
objectFit: 'contain',
|
||||
objectPosition: '50% 50%'
|
||||
}
|
||||
}
|
||||
```
|
||||
### Qrcode 二维码
|
||||
- 通过`text`属性填写需要生成二维码的文本。
|
||||
- 通过 `css` 里的 `color` 可设置生成码点的颜色。
|
||||
- 通过 `css` 里的 `background`可设置背景色。
|
||||
- 通过 `css `里的 `width`、`height`设置尺寸。
|
||||
|
||||
|
||||
#### 方式一 HTML
|
||||
```html
|
||||
<l-painter>
|
||||
<l-painter-qrcode
|
||||
text="limeui.qcoon.cn"
|
||||
css="width: 200rpx; height: 200rpx"
|
||||
/>
|
||||
</l-painter>
|
||||
```
|
||||
#### 方式二 JSON schema
|
||||
```js
|
||||
{
|
||||
type: 'qrcode',
|
||||
text: 'limeui.qcoon.cn',
|
||||
css: {
|
||||
width: '200rpx',
|
||||
height: '200rpx',
|
||||
}
|
||||
}
|
||||
```
|
||||
### 生成图片
|
||||
- 1、通过设置`isCanvasToTempFilePath`自动生成图片并在 `@success` 事件里接收海报临时路径
|
||||
- 2、通过调用内部方法生成图片:
|
||||
|
||||
|
||||
```html
|
||||
<l-painter ref="painter">...code</l-painter>
|
||||
```
|
||||
```js
|
||||
// 主动调用方式只能在绘制完成之后
|
||||
// @done 事件表示绘制完成
|
||||
this.$refs.painter.canvasToTempFilePath({
|
||||
// 在nvue里是jpeg
|
||||
fileType: 'jpg',
|
||||
quality: 1,
|
||||
success: (res) => {
|
||||
console.log(res.tempFilePath)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
|
||||
### 海报示例
|
||||
- 提供一份示例,只把插件当成生成图片的工具,弹窗之类的功能另外寻找。
|
||||
- 通过设置`isCanvasToTempFilePath`主动生成图片,再由 `@success` 事件接收海报临时路径
|
||||
- 使用`custom-style`样式把画板移到屏幕之外,因为可能canvas的层级比较高,无法覆盖。
|
||||
- **注意**:海报画板不能隐藏 否则无法生成图片。
|
||||
|
||||
#### 方式一 HTML
|
||||
```html
|
||||
<image :src="path" mode="widthFix"></image>
|
||||
<l-painter isCanvasToTempFilePath @success="path = $event"
|
||||
custom-style="position: fixed; left: 200%"
|
||||
css="width: 750rpx; padding-bottom: 40rpx; background: linear-gradient(,#ff971b 0%, #ff5000 100%)">
|
||||
<l-painter-image src="https://cdn.jsdelivr.net/gh/liangei/image@latest/avatar-1.jpeg" css="margin-left: 40rpx; margin-top: 40rpx; width: 84rpx; height: 84rpx; border-radius: 50%;"/>
|
||||
<l-painter-view css="margin-top: 40rpx; padding-left: 20rpx; display: inline-block">
|
||||
<l-painter-text text="隔壁老王" css="display: block; padding-bottom: 10rpx; color: #fff; font-size: 32rpx; fontWeight: bold"/>
|
||||
<l-painter-text text="为您挑选了一个好物" css="color: rgba(255,255,255,.7); font-size: 24rpx"/>
|
||||
</l-painter-view>
|
||||
<l-painter-view css="margin-left: 40rpx; margin-top: 30rpx; padding: 32rpx; box-sizing: border-box; background: #fff; border-radius: 16rpx; width: 670rpx; box-shadow: 0 20rpx 58rpx rgba(0,0,0,.15)">
|
||||
<l-painter-image src="https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg" css="object-fit: cover; object-position: 50% 50%; width: 606rpx; height: 606rpx; border-radius: 12rpx;"/>
|
||||
<l-painter-view css="margin-top: 32rpx; color: #FF0000; font-weight: bold; font-size: 28rpx; line-height: 1em;">
|
||||
<l-painter-text text="¥" css="vertical-align: bottom"/>
|
||||
<l-painter-text text="39" css="vertical-align: bottom; font-size: 58rpx"/>
|
||||
<l-painter-text text=".39" css="vertical-align: bottom"/>
|
||||
<l-painter-text text="¥59.99" css="vertical-align: bottom; padding-left: 10rpx; font-weight: normal; text-decoration: line-through; color: #999999"/>
|
||||
</l-painter-view>
|
||||
<l-painter-view css="margin-top: 32rpx; font-size: 26rpx; color: #8c5400">
|
||||
<l-painter-text text="自营" css="color: #212121; background: #ffb400;"/>
|
||||
<l-painter-text text="30天最低价" css="margin-left: 16rpx; background: #fff4d9; text-decoration: line-through;"/>
|
||||
<l-painter-text text="满减优惠" css="margin-left: 16rpx; background: #fff4d9"/>
|
||||
<l-painter-text text="超高好评" css="margin-left: 16rpx; background: #fff4d9"/>
|
||||
</l-painter-view>
|
||||
<l-painter-view css="margin-top: 30rpx">
|
||||
<l-painter-text css="line-clamp: 2; color: #333333; line-height: 1.8em; font-size: 36rpx; width: 478rpx; padding-right:32rpx; box-sizing: border-box" text="360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝" :replace="{word: ['儿童', '9X'], color: 'red'}"></l-painter-text>
|
||||
<l-painter-qrcode css="width: 128rpx; height: 128rpx;" text="limeui.qcoon.cn"></l-painter-qrcode>
|
||||
</l-painter-view>
|
||||
</l-painter-view>
|
||||
</l-painter>
|
||||
```
|
||||
|
||||
```js
|
||||
data() {
|
||||
return {
|
||||
path: ''
|
||||
}
|
||||
}
|
||||
```
|
||||
#### 方式二 JSON schema
|
||||
```html
|
||||
<image :src="path" mode="widthFix"></image>
|
||||
<l-painter
|
||||
:board="poster"
|
||||
isCanvasToTempFilePath
|
||||
@success="path = $event"
|
||||
custom-style="position: fixed; left: 200%"
|
||||
/>
|
||||
```
|
||||
```js
|
||||
data() {
|
||||
return {
|
||||
path: '',
|
||||
poster: {
|
||||
css: {
|
||||
width: "750rpx",
|
||||
paddingBottom: "40rpx",
|
||||
background: "linear-gradient(,#000 0%, #ff5000 100%)"
|
||||
},
|
||||
views: [
|
||||
{
|
||||
src: "https://cdn.jsdelivr.net/gh/liangei/image@latest/avatar-1.jpeg",
|
||||
type: "image",
|
||||
css: {
|
||||
background: "#fff",
|
||||
objectFit: "cover",
|
||||
marginLeft: "40rpx",
|
||||
marginTop: "40rpx",
|
||||
width: "84rpx",
|
||||
border: "2rpx solid #fff",
|
||||
boxSizing: "border-box",
|
||||
height: "84rpx",
|
||||
borderRadius: "50%"
|
||||
}
|
||||
},
|
||||
{
|
||||
type: "view",
|
||||
css: {
|
||||
marginTop: "40rpx",
|
||||
paddingLeft: "20rpx",
|
||||
display: "inline-block"
|
||||
},
|
||||
views: [
|
||||
{
|
||||
text: "隔壁老王",
|
||||
type: "text",
|
||||
css: {
|
||||
display: "block",
|
||||
paddingBottom: "10rpx",
|
||||
color: "#fff",
|
||||
fontSize: "32rpx",
|
||||
fontWeight: "bold"
|
||||
}
|
||||
},
|
||||
{
|
||||
text: "为您挑选了一个好物",
|
||||
type: "text",
|
||||
css: {
|
||||
color: "rgba(255,255,255,.7)",
|
||||
fontSize: "24rpx"
|
||||
},
|
||||
}
|
||||
],
|
||||
},
|
||||
{
|
||||
css: {
|
||||
marginLeft: "40rpx",
|
||||
marginTop: "30rpx",
|
||||
padding: "32rpx",
|
||||
boxSizing: "border-box",
|
||||
background: "#fff",
|
||||
borderRadius: "16rpx",
|
||||
width: "670rpx",
|
||||
boxShadow: "0 20rpx 58rpx rgba(0,0,0,.15)"
|
||||
},
|
||||
views: [
|
||||
{
|
||||
src: "https://m.360buyimg.com/babel/jfs/t1/196317/32/13733/288158/60f4ea39E6fb378ed/d69205b1a8ed3c97.jpg",
|
||||
type: "image",
|
||||
css: {
|
||||
objectFit: "cover",
|
||||
objectPosition: "50% 50%",
|
||||
width: "606rpx",
|
||||
height: "606rpx"
|
||||
},
|
||||
}, {
|
||||
css: {
|
||||
marginTop: "32rpx",
|
||||
color: "#FF0000",
|
||||
fontWeight: "bold",
|
||||
fontSize: "28rpx",
|
||||
lineHeight: "1em"
|
||||
},
|
||||
views: [{
|
||||
text: "¥",
|
||||
type: "text",
|
||||
css: {
|
||||
verticalAlign: "bottom"
|
||||
},
|
||||
}, {
|
||||
text: "39",
|
||||
type: "text",
|
||||
css: {
|
||||
verticalAlign: "bottom",
|
||||
fontSize: "58rpx"
|
||||
},
|
||||
}, {
|
||||
text: ".39",
|
||||
type: "text",
|
||||
css: {
|
||||
verticalAlign: "bottom"
|
||||
},
|
||||
}, {
|
||||
text: "¥59.99",
|
||||
type: "text",
|
||||
css: {
|
||||
verticalAlign: "bottom",
|
||||
paddingLeft: "10rpx",
|
||||
fontWeight: "normal",
|
||||
textDecoration: "line-through",
|
||||
color: "#999999"
|
||||
}
|
||||
}],
|
||||
|
||||
type: "view"
|
||||
}, {
|
||||
css: {
|
||||
marginTop: "32rpx",
|
||||
fontSize: "26rpx",
|
||||
color: "#8c5400"
|
||||
},
|
||||
views: [{
|
||||
text: "自营",
|
||||
type: "text",
|
||||
css: {
|
||||
color: "#212121",
|
||||
background: "#ffb400"
|
||||
},
|
||||
}, {
|
||||
text: "30天最低价",
|
||||
type: "text",
|
||||
css: {
|
||||
marginLeft: "16rpx",
|
||||
background: "#fff4d9",
|
||||
textDecoration: "line-through"
|
||||
},
|
||||
}, {
|
||||
text: "满减优惠",
|
||||
type: "text",
|
||||
css: {
|
||||
marginLeft: "16rpx",
|
||||
background: "#fff4d9"
|
||||
},
|
||||
}, {
|
||||
text: "超高好评",
|
||||
type: "text",
|
||||
css: {
|
||||
marginLeft: "16rpx",
|
||||
background: "#fff4d9"
|
||||
},
|
||||
|
||||
}],
|
||||
|
||||
type: "view"
|
||||
}, {
|
||||
css: {
|
||||
marginTop: "30rpx"
|
||||
},
|
||||
views: [
|
||||
{
|
||||
text: "360儿童电话手表9X 智能语音问答定位支付手表 4G全网通20米游泳级防水视频通话拍照手表男女孩星空蓝",
|
||||
type: "text",
|
||||
css: {
|
||||
paddingRight: "32rpx",
|
||||
boxSizing: "border-box",
|
||||
lineClamp: 2,
|
||||
color: "#333333",
|
||||
lineHeight: "1.8em",
|
||||
fontSize: "36rpx",
|
||||
width: "478rpx"
|
||||
},
|
||||
}, {
|
||||
text: "limeui.qcoon.cn",
|
||||
type: "qrcode",
|
||||
css: {
|
||||
width: "128rpx",
|
||||
height: "128rpx",
|
||||
},
|
||||
|
||||
}],
|
||||
type: "view"
|
||||
}],
|
||||
type: "view"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 原生小程序
|
||||
- 插件里的`painter.js`支持在原生小程序中使用
|
||||
- new Painter之后在`source`里传入JSON schema
|
||||
- 再调用`render`绘制海报
|
||||
- 如需生成图片,请查看微信小程序cavnas的[canvasToTempFilePath](https://developers.weixin.qq.com/miniprogram/dev/api/canvas/wx.canvasToTempFilePath.html)
|
||||
|
||||
```html
|
||||
<canvas type="2d" id="painter" style="width: 100%"></canvas>
|
||||
```
|
||||
```js
|
||||
import {Painter} from './painter'
|
||||
page({
|
||||
data: {
|
||||
poster: {
|
||||
css: {
|
||||
width: '750rpx'
|
||||
},
|
||||
views: [
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#d2d4c8',
|
||||
paddingTop: '100rpx'
|
||||
},
|
||||
views: [
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#5f7470',
|
||||
width: '33.33%',
|
||||
height: '100rpx',
|
||||
display: 'inline-block'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#889696',
|
||||
width: '33.33%',
|
||||
height: '100rpx',
|
||||
display: 'inline-block'
|
||||
}
|
||||
},
|
||||
{
|
||||
type: 'view',
|
||||
css: {
|
||||
background: '#b8bdb5',
|
||||
width: '33.33%',
|
||||
height: '100rpx',
|
||||
display: 'inline-block'
|
||||
}
|
||||
}
|
||||
],
|
||||
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
async onLoad() {
|
||||
const res = await this.getCentext()
|
||||
const painter = new Painter(res)
|
||||
// 返回计算布局后的整个内容尺寸
|
||||
const {width, height} = await painter.source(this.data.poster)
|
||||
// 得到计算后的尺寸后 可给canvas尺寸赋值,达到动态响应效果
|
||||
// 渲染
|
||||
await painter.render()
|
||||
},
|
||||
// 获取canvas 2d
|
||||
// 非2d也可以使用这里只是举个例子
|
||||
getCentext() {
|
||||
return new Promise(resolve => {
|
||||
wx.createSelectorQuery()
|
||||
.select(`#painter`)
|
||||
.node()
|
||||
.exec(res => {
|
||||
let { node: canvas } = res[0];
|
||||
resolve({
|
||||
canvas,
|
||||
context: canvas.getContext('2d'),
|
||||
width: canvas.width,
|
||||
height: canvas.height,
|
||||
pixelRatio: 2
|
||||
})
|
||||
})
|
||||
})
|
||||
},
|
||||
})
|
||||
```
|
||||
### Nvue
|
||||
- 插件是通过 `web-view` 支持 `app-nvue`
|
||||
- 默认是远端的文件,如果需要本地化,请按下方步骤:
|
||||
- 1、去码云把[hybrid](https://gitee.com/liangei/lime-painter/tree/master/examples/uni/hybrid)目录的文件,放到自己项目的根目录。
|
||||
- 2、给插件文件`l-painter.vue`里`web-view`的`src`加上根目录的`hybrid路径`并注释`this.webViewInit()`
|
||||
|
||||
|
||||
```html
|
||||
// 只加上路径 其它参数不要改
|
||||
<web-view src="/hybrid/html/lime-ui/lime-painter/index.html" />
|
||||
```
|
||||
```js
|
||||
// 153行 注释该方法
|
||||
// this.webViewInit()
|
||||
```
|
||||
|
||||
### 旧版更新(1.6.x)
|
||||
- 由于1.8.x版放弃了以定位的方式,所以1.6.x版更新之后要每个样式都加上`position: absolute`
|
||||
- 旧版的 `image` mode 模式被放弃,使用`object-fit`
|
||||
- 旧版的 `isRenderImage` 改成 `is-canvas-to-temp-filePath`
|
||||
- 旧版的 `maxLines` 改成 `line-clamp`
|
||||
|
||||
## API
|
||||
|
||||
### Props
|
||||
|
||||
| 参数 | 说明 | 类型 | 默认值 |
|
||||
| ------------- | ------------ | ---------------- | ------------ |
|
||||
| board | JSON schema方式的海报元素对象集 | <em>object</em> | - |
|
||||
| css | 海报最外层的样式,可以理解为`body` | <em>object</em> | 参数请向下看 |
|
||||
| custom-style | canvas自定义样式 | <em>string</em> | |
|
||||
| is-canvas-to-temp-filePath | 是否生成图片,在`@success`事件接收图片地址 | <em>boolean</em> | `false` |
|
||||
| after-delay | 生成图片错乱,可延时生成图片 | <em>number</em> | `100` |
|
||||
| type | canvas 类型,对微信头条支付宝小程序可有效,可选值:`2d`、`` | <em>string</em> | `2d` |
|
||||
| file-type | 生成图片的后缀类型, 可选值:`png`、`jpg`,在nvue里是`jpeg` | <em>string</em> | `png` |
|
||||
| path-type | 生成图片路径类型,可选值`url`、`base64` | <em>string</em> | `-` |
|
||||
| pixel-ratio | 生成图片的像素密度,默认为对应手机的像素密度 | <em>number</em> | `-` |
|
||||
|
||||
<!-- | width | 画板的宽度,一般只用于通过内部方法时加上 | <em>number</em> | `` |
|
||||
| height | 画板的高度 ,同上 | <em>number</em> | `` | -->
|
||||
|
||||
### css
|
||||
| 属性名 | 支持的值或类型 | 默认值 |
|
||||
| ------------- | ------------ | ---------------- |
|
||||
| (min\max)width | 支持`%`、`rpx`、`px` | - |
|
||||
| height | 同上 | - |
|
||||
| color | `string` | - |
|
||||
| position | 定位,可选值:`absolute`、`fixed` | - |
|
||||
| ↳ left、top、right、bottom | 配合`position`才生效,支持`%`、`rpx`、`px` | - |
|
||||
| margin | 可简写或各方向分别写,如:`margin-top`,支持`auto`、`rpx`、`px` | - |
|
||||
| padding | 可简写或各方向分别写,支持`rpx`、`px` | - |
|
||||
| border | 可简写或各个值分开写:`border-width`、`border-style` 、`border-color`,简写请按顺序写| - |
|
||||
| line-clamp | `number`,超过行数显示省略号 | - |
|
||||
| background | 支持渐变,但必须写百分比!如:`linear-gradient(,#ff971b 0%, #ff5000 100%)`、`radial-gradient(#0ff 15%, #f0f 60%)`,目前radial-gradient 渐变的圆心为元素中点,半径为最长边,不支持设置 | - |
|
||||
| vertical-align | 文字垂直对齐,可选值:`bottom`、`top`、`middle` | `middle` |
|
||||
| line-height | 文字行高,支持`rpx`、`px`、`em`| `1.4em` |
|
||||
| font-weight | 文字粗细,可选值:`normal`、`bold`| `normal` |
|
||||
| font-size | 文字大小,`string`,支持`rpx`、`px` | `14px` |
|
||||
| text-decoration | 文本修饰,可选值:`underline` 、`line-through`、`overline`| - |
|
||||
| text-align | 文本水平对齐,可选值:`right` 、`center`| - |
|
||||
| display | 框类型,可选值:`block`、`inline-block`、`none`,当为`none`时是不渲染该段 | - |
|
||||
| border-radius | 圆角边框,支持`%`、`rpx`、`px` | - |
|
||||
| box-sizing | 可选值:`border-box` | - |
|
||||
| box-shadow | 投影 | - |
|
||||
| background-image | view元素设置背景纹理,注意这里的背景纹理一般是用于纹理平铺,无法代替`image`元素。如:水印 | - |
|
||||
| background-repeat | 设置是否及如何重复背景纹理,可选值:`repeat`、`repeat-x`、`repeat-y`、`no-repeat` | `repeat` |
|
||||
| [object-fit](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-fit/) | 图片元素适应容器方式,类似于`mode`,可选值:`cover`、 `contain`、 `fill`、 `none` | - |
|
||||
| [object-position](https://developer.mozilla.org/zh-CN/docs/Web/CSS/object-position) | 图片的对齐方式,配合`object-fit`使用 | - |
|
||||
|
||||
### 图片填充模式 object-fit
|
||||
| 名称 | 含义 |
|
||||
| ------- | ------------ |
|
||||
| contain | 保持宽高缩放图片,使图片的长边能完全显示出来 |
|
||||
| cover | 保持宽高缩放图片,使图片的短边能完全显示出来,裁剪长边 |
|
||||
| fill | 拉伸图片,使图片填满元素 |
|
||||
| none | 保持图片原有尺寸 |
|
||||
|
||||
### 事件 Events
|
||||
|
||||
| 事件名 | 说明 | 回调 |
|
||||
| ------- | ------------ | -------------- |
|
||||
| success | 生成图片成功,若使用了`is-canvas-to-temp-filePath` 可以接收图片地址 | path |
|
||||
| fail | 生成图片失败 | {error: error} |
|
||||
| done | 绘制成功 | |
|
||||
| progress | 绘制进度 | number |
|
||||
|
||||
## 常见问题
|
||||
- 1、H5端使用网络图片需要解决跨域问题。
|
||||
- 2、小程序使用网络图片需要去公众平台增加下载白名单!二级域名也需要配!
|
||||
- 3、H5端生成图片是base64,有时显示只有一半可以使用原生标签`<IMG/>`
|
||||
- 4、发生保存图片倾斜变形或提示native buffer exceed size limit时,使用pixel-ratio="2"参数,降分辨率。
|
||||
- 5、h5保存图片不需要调接口,提示用户长按图片保存。
|
||||
- 6、IOS APP 请勿使用HBX2.9.3.20201014的版本!这个版本无法生成图片。
|
||||
- 7、组件不能隐藏,包含`v-if`,`v-show`、`display:none`、`opacity:0`
|
||||
- 8、微信小程序 canvas 2d不支持真机调试,请使用真机预览方式。
|
||||
- 9、华为手机APP上无法生成图片,请使用HBX2.9.11++
|
||||
- 10、苹果微信7.0.20存在闪退和图片无法onload为微信bug,请到码云上升级本插件
|
||||
|
||||
## 打赏
|
||||
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。
|
||||
|
||||

|
||||
Reference in New Issue
Block a user