Painter 的优势
- 功能全,支持文本、图片、矩形、qrcode 类型的 view 绘制
- 布局全,支持多种布局方式,如 align(对齐方式)、rotate(旋转)
- 支持圆角,其中图片,矩形,和整个画布支持 borderRadius 来设置圆角
- 支持边框,同时支持 solid、dashed、dotted 三种类型
- 支持渐变色,包括线性渐变与径向渐变。
- 支持 box-shadow 和 text-shadow,统一使用 shadow 表示。
- 支持文字背景、获取宽度、主动换行
- 支持图片 mode
- 支持元素的相对定位方法
- 杠杠的性能优化,我们对网络素材图片加载实现了一套 LRU 存储机制,不用重复下载素材图片。
- 杠杠的容错,因为某些特殊情况会导致 Canvas 绘图不完整。我们对此加入了对结果图片进行检测机制,如果绘图出错会进行重绘。
- 生成的图片支持分辨率调节
- 支持使用拖动等操作动态编辑绘制内容
How To Use
运行例子
git clone https://github.com/Kujiale-Mobile/Painter.git
代码下载后,用小程序 IDE 打开后即可使用。
注:请选择小程序项目,非小游戏,例子中无 appid,所以无法在手机上运行,如果需要真机调试,请在打开例子时,填上自己的小程序 id
以下是我在uniapp中使用
下载库到项目中,注册组件,使用组件
<painter @imgOK="onImgOk" @imgErr="onImgErr" :widthPixels="windowWidth" :palette="template" />
// 微信海报模块
export default {
"width": "750px",
"height": "1334px",
"background": "#FFFFFF",
"views": [{
"type": "image",
"url": "",
"css": {
"width": "750px",
"height": "1334px",
"top": "0px",
"left": "0px",
"rotate": "0",
"borderRadius": "",
"borderWidth": "",
"borderColor": "#000000",
"shadow": "",
"mode": "scaleToFill"
}
}, {
"type": "image",
"url": "",
"css": {
"color": "#000000",
"background": "#ffffff",
"width": "334px",
"height": "146px",
"top": "186px",
"left": "208px",
"rotate": "0"
}
}, {
"type": "image",
"url": "",
"css": {
"color": "#000000",
"background": "#ffffff",
"width": "610px",
"height": "826px",
"top": "435px",
"left": "70px",
"rotate": "0"
}
}, {
"type": "image",
"url": "",
"css": {
"color": "#000000",
"background": "#ffffff",
"width": "124px",
"height": "124px",
"top": "1078px",
"left": "91px",
"rotate": "0",
"borderRadius": "62px"
}
}, {
"type": "image",
"url": "",
"css": {
"color": "#000000",
"background": "#ffffff",
"width": "609px",
"height": "408px",
"top": "435px",
"left": "71px",
"rotate": "0",
"borderRadius": "20px"
}
}, {
"type": "image",
"url": "",
"css": {
"color": "#000000",
"background": "#ffffff",
"width": "160px",
"height": "160px",
"top": "1057px",
"left": "489px",
"rotate": "0",
"borderRadius": "20px"
}
},
{
"type": "text",
"text": "",
"css": {
"color": "#222222",
"background": "rgba(0,0,0,0)",
"width": "216px",
"height": "42px",
"top": "1076px",
"left": "238px",
"rotate": "0",
"padding": "0px",
"fontSize": "30px",
"fontWeight": "normal",
"maxLines": "1",
"lineHeight": "42px",
"textStyle": "fill",
"textDecoration": "none",
"fontFamily": "",
"textAlign": "left"
}
},
{
"type": "text",
"text": "精品推荐",
"css": {
"color": "#ffffff",
"background": "rgba(0,0,0,0)",
"width": "144px",
"height": "120px",
"top": "360px",
"left": "304px",
"rotate": "0",
"padding": "0px",
"fontSize": "36px",
"fontWeight": "normal",
"maxLines": "2",
"lineHeight": "60px",
"textStyle": "fill",
"textDecoration": "none",
"fontFamily": "",
"textAlign": "center"
}
},
{
"type": "text",
"text": "长按识别,立享优惠",
"css": {
"color": "#999999",
"background": "rgba(0,0,0,0)",
"width": "216px",
"height": "33px",
"top": "1172px",
"left": "238px",
"rotate": "0",
"padding": "0px",
"fontSize": "24px",
"fontWeight": "normal",
"maxLines": "2",
"lineHeight": "33px",
"textStyle": "fill",
"textDecoration": "none",
"fontFamily": "",
"textAlign": "left"
}
},
{
"type": "text",
"text": "约你一起买好货",
"css": {
"color": "#222222",
"background": "rgba(0,0,0,0)",
"width": "216px",
"height": "42px",
"top": "1120px",
"left": "238px",
"rotate": "0",
"padding": "0px",
"fontSize": "30px",
"fontWeight": "normal",
"maxLines": "2",
"lineHeight": "42px",
"textStyle": "fill",
"textDecoration": "none",
"fontFamily": "",
"textAlign": "left"
}
},
// 商品标题
{
"type": "text",
"text": "无",
"css": {
"color": "#222222",
"background": "rgba(0,0,0,0)",
"width": "550px",
"height": "40px",
"top": "870px",
"left": "100px",
"rotate": "0",
"padding": "0px",
"fontSize": "36px",
"fontWeight": "normal",
"maxLines": "1",
"lineHeight": "40px",
"textStyle": "fill",
"textDecoration": "none",
"fontFamily": "",
"textAlign": "left"
}
},
// 商品价格
{
"type": "text",
"text": "¥139.00/KG",
"css": {
"color": "#FF6060",
"background": "rgba(0,0,0,0)",
"width": "550px",
"height": "40px",
"top": "940px",
"left": "100px",
"rotate": "0",
"padding": "0px",
"fontSize": "36px",
"fontWeight": "normal",
"maxLines": "2",
"lineHeight": "40px",
"textStyle": "fill",
"textDecoration": "none",
"fontFamily": "",
"textAlign": "left"
}
}
]
}
<template>
<view>
<view class="">
<image class="poster-img" :src="posterUrl" mode="widthFix"></image>
</view>
<!-- #ifdef MP-WEIXIN -->
<view class="canvas-box">
<painter @imgOK="onImgOk" @imgErr="onImgErr" :widthPixels="windowWidth" :palette="template" />
</view>
<!-- #endif -->
<!-- #ifdef H5 -->
<a style="display: none;" id="saveImg" href="">下载</a>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<button @click="savePicture" class="poster-button">保存图片</button>
<!-- #endif -->
<!-- #ifdef H5 -->
<button >长按保存图片</button>
<!-- #endif -->
</view>
</template>
<script>
// #ifdef H5
import MC from "mcanvas";
import qs from "@/utils/qs.js"
import QR_CODE from "qrious";
// #endif
import $mConfig from '@/config/index.config.js';
import WechatCode from "@/api/base/WechatCode"
import tpl from "./mpPosterData.js"
import {
mapState,
mapActions
} from "vuex"
export default {
data() {
return {
windowWidth: 750,
show: false,
posterUrl: "",
template: tpl,
poster_bg: ""
}
},
onLoad() {
this.getUserInfo()
this.getCode()
// #ifdef H5
this.createPoster()
// #endif
},
computed: {
...mapState(["userInfo", "goodsDetail"])
},
methods: {
init() {
this.poster_bg = $mConfig.assetsPath + "poster-bg.png";
this.template.views[0].url = $mConfig.assetsPath + "poster-bg.png";
this.template.views[1].url = $mConfig.assetsPath + "poster-logo.png";
this.template.views[2].url = $mConfig.assetsPath + "poster-bg2.png";
this.template.views[3].url = this.userInfo.avatar.url;
this.template.views[4].url = this.goodsDetail.list_cover.url;
this.template.views[5].url = this.wechatCode;
this.template.views[6].text = this.userInfo.nickname;
this.template.views[10].text = this.goodsDetail.title;
this.template.views[11].text = `¥${this.goodsDetail.price}/${this.goodsDetail.company}`
setTimeout(() => {
this.show = true;
})
},
// H5生成海报
createPoster() {
let qr_url = window.location.origin;
let parmas = {
id: this.goodsDetail.id,
parent_id: this.userInfo.id,
is_share: 1
}
qr_url = qr_url + "/#/pages/goods_detail/goods_detail?" + qs.stringify(parmas, {
encode: false
})
console.log(qr_url)
const qr_code = new QR_CODE({
value: qr_url,
});
const qr_img = new Image();
qr_img.src = qr_code.toDataURL("image/jpeg");
const mc = new MC({
"width": 750,
"height": 1334,
backgroundColor: "white",
});
let nickname = this.userInfo.nickname.length > 8 ? this.userInfo.nickname.slice(0, 8) : this.userInfo
.nickname;
mc.add($mConfig.assetsPath + "poster-bg.png", {
"width": "750px",
"height": "1334px",
pos: {
y: 0,
x: 0,
},
})
.add($mConfig.assetsPath + "poster-logo.png", {
"width": "334px",
"height": "146px",
pos: {
y: "186px",
x: "208px",
},
})
.add($mConfig.assetsPath + "poster-bg2.png", {
"width": "610px",
"height": "826px",
pos: {
y: "435px",
x: "70px",
},
})
.add(this.userInfo.avatar.url, {
"width": "124px",
"height": "124px",
"align": "center",
pos: {
y: "1078px",
x: "91px",
},
crop: {
radius: 120,
},
})
.add(this.goodsDetail.list_cover.url, {
"width": "609px",
"height": "408px",
"align": "center",
crop: {
"y": "0",
"x": "0",
"width": "100%",
"height": "66.99%",
"radius": "20px"
},
pos: {
y: "435px",
x: "71px",
},
})
.add(qr_img, {
"width": "160px",
"height": "160px",
pos: {
y: "1057px",
x: "489px",
},
})
.text(this.userInfo.nickname, {
width: "350px",
align: "left",
normalStyle: {
// 字体颜色;
color: "#222222",
font: "30px arial sans-serif",
},
pos: {
y: "1076px",
x: "238px",
},
})
.text(this.goodsDetail.title, {
width: "550px",
align: "left",
normalStyle: {
// 字体颜色;
color: "#222222",
font: "36px arial sans-serif",
},
pos: {
y: "870px",
x: "100px",
},
})
.text(`¥${this.goodsDetail.price}/${this.goodsDetail.company}`, {
width: "550px",
align: "left",
normalStyle: {
// 字体颜色;
color: "#FF6060",
font: "36px arial sans-serif",
},
pos: {
y: "950px",
x: "100px",
},
})
.text("精品推荐", {
width: "160px",
align: "center",
normalStyle: {
// 字体颜色;
color: "#ffffff",
font: "36px arial sans-serif",
},
pos: {
y: "360px",
x: "304px",
},
})
.text("长按识别,立享优惠", {
width: "216px",
align: "left",
normalStyle: {
// 字体颜色;
color: "#999999",
font: "24px arial sans-serif",
},
pos: {
y: "1172px",
x: "238px",
},
})
.text("约你一起买好货", {
width: "216px",
align: "left",
normalStyle: {
// 字体颜色;
color: "#222222",
font: "30px arial sans-serif",
},
pos: {
y: "1120px",
x: "238px",
},
})
.draw((b64) => {
// window.console.log(b64);
this.posterUrl = b64;
});
},
convertImageToCanvas() {
const qr_img = new Image();
qr_img.src = this.posterUrl;
var canvas = document.createElement("canvas");
canvas.width = qr_img.width;
canvas.height = qr_img.height;
canvas.getContext("2d").drawImage(qr_img, 0, 0);
return canvas;
},
// 点击保存图片
savePicture: function(e) {
if (!this.posterUrl) {
uni.showToast({
title: "海报生成失败"
})
return
}
// #ifdef H5
// var sampleImage = document.querySelector(".erweima");
var canvas = this.convertImageToCanvas()
var url = canvas.toDataURL("image/png"); //生成下载的url
var triggerDownload = document.querySelector("#saveImg")
// .attr("href", url).attr("download", "ewm.png");
triggerDownload.setAttribute("href", url)
triggerDownload.setAttribute("download", `${new Date().getTime()}.png`)
triggerDownload.click();
return
// #endif
// let {
// poster
// } = e.currentTarget.dataset;
var $this = this;
uni.getSetting({
success: function(res) {
var accredit = res.authSetting['scope.writePhotosAlbum']
if (accredit) {
uni.showLoading({
title: '图片下载中...',
})
uni.saveImageToPhotosAlbum({
filePath: $this.posterUrl,
success: function(ret) {
// core.toast("保存图片成功");
uni.showToast({
title: "保存图片成功"
})
},
fail: function(ret) {
console.log(ret)
// core.toast("保存图片失败");
uni.showToast({
title: "保存图片失败"
})
}
})
} else {
uni.authorize({
scope: 'scope.writePhotosAlbum',
fail: function() {
/*获取权限时点击了拒绝以后的弹窗*/
uni.showModal({
title: '警告',
content: '您点击了拒绝授权,将无法正常使用保存图片或视频的功能体验,请删除小程序重新进入。'
})
}
})
}
}
})
},
getCode() {
WechatCode(`id=${this.goodsDetail.id}&parent_id=${this.userInfo.id || ""}&is_share=1`,
"pages/goods_detail/goods_detail")
.then(res => {
console.log(res)
this.wechatCode = res.data.wechatCode
this.init()
})
},
onImgOk(res) {
console.log(res)
this.posterUrl = res.detail.path;
},
onImgErr(res) {
console.log(res)
},
...mapActions(["getUserInfo"])
}
}
</script>
<style>
.canvas-box {
position: fixed;
top: -99999px;
}
.poster-img {
width: 100vw;
padding-bottom: 100rpx;
}
.poster-button {
position: fixed;
bottom: 0;
width: 100%;
padding: 20rpx;
background: #EFEDF8;
color: #715DC2;
}
</style>