注意:因为非2D的Canvas,微信不在维护wx.createCanvasContext这个API,所有苹果端裁剪会有问题,有时间改造一版2D的吧
从别人 那看过来的,原项目是一个uniapp,因为需要在小程序里直接用,为了需求,所以编译后修改了部分内容。
目前还有点缺陷就是海报不能放大,可能会不清晰
MDCanvas.js代码
class MDCanvas {
/**
* 构造函数
* @param {Object} ctx canvas的实例
* @param {String} canvasId canvas组件的canvasId
* @param {Array} canvasData 要画canvas的数组
*/
constructor(ctx, canvasId, canvasData) {
this._canvasData = canvasData; // 要画的canvas数组
this._ctx = ctx; // canvas 实例
this._canvasId = canvasId; // canvasId
this._pmImgTask = []; // promise下载图片任务
}
/**
* 画canvas
*/
drawCanvas() {
wx.showToast({
title: '加载素材..',
icon: 'loading',
mask: true,
duration: 10 * 1000
});
const ctx = this._ctx;
const canvasId = this._canvasId;
this.asyncImage();
return new Promise((resolve, reject) => {
Promise.all(this._pmImgTask).then(() => {
this._canvasData.forEach(item => {
switch (item.type) {
case "bg": //背景
const radius = item.radius ? item.radius : 0;
this.roundRect(ctx, 0, 0, upx2px(item.width), upx2px(item.height), radius);
break;
case 'lg': // 线性渐变
{
const lrd = ctx.createLinearGradient(upx2px(item.x0), upx2px(item.y0),
upx2px(item.x1), upx2px(item.y1));
item.colors.forEach(x => {
lrd.addColorStop(x.scope, x.value);
});
ctx.setFillStyle(lrd);
ctx.fillRect(upx2px(item.x), upx2px(item.y), upx2px(item.width),
upx2px(item.height));
}
break;
case 'img': // 图片
ctx.save();
ctx.beginPath();
if (item.border === 'circle') { // 圆角
const radius = item.width <= item.height ? item.width : item.height;
if (item.width >= item.height) {
item.x = item.x - (item.width - item.height) / 2;
} else {
item.y = item.y - (item.height - item.width) / 2;
}
ctx.arc(upx2px(item.x + item.width / 2), upx2px(item.y + item.height / 2),
upx2px(radius / 2), 0, 2 * Math.PI);
ctx.setFillStyle('#fff');
ctx.fill();
ctx.clip();
}
ctx.drawImage(
item.content,
upx2px(item.x),
upx2px(item.y),
upx2px(item.width),
upx2px(item.height)
);
ctx.restore();
break;
case 'text': // 文字
{
ctx.setFillStyle(item.color);
if (item.textAlign) ctx.setTextAlign(item.textAlign);
if (item.baseLine) ctx.setTextBaseline(item.baseLine);
const bold = item.bold ? item.bold : 'normal'; // 加粗
const family = item.family ? item.family : 'Microsoft YaHei'; // 字体
ctx.font = `${bold} ${Math.floor(upx2px(item.fontSize))}px ${family}`;
const arr = this.newLine(item.content, upx2px(item.width), item.row); // 换行
const lineHeight = item.lineHeight ? item.lineHeight : Number(item.fontSize || 30) * 1.2; // 字体大小默认30
var fontSize = Number(item.fontSize || 30);
arr.forEach((itemText, key) => {
var x = upx2px(item.x);
if(item.textAlign){
var len1 = this.getZhongWenLen(itemText);
var len2 = itemText.length - len1;
x = upx2px((item.width - len1 * fontSize - len2 * (fontSize/2)) / 2);
}
ctx.fillText(itemText, x, upx2px(item.y + lineHeight * key));
});
}
break;
case 'shape': // 形状
ctx.save();
ctx.beginPath();
if (item.linearGradient) { // 渐变
const grad = ctx.createLinearGradient(item.linearGradient.x1, item.linearGradient.y1,
item.linearGradient
.x2, item.linearGradient.y2); // 创建一个渐变色线性对象
grad.addColorStop(0, item.linearGradient.color1); // 定义渐变色颜色
grad.addColorStop(1, item.linearGradient.color2);
ctx.setFillStyle(grad);
} else {
ctx.setFillStyle(item.background);
}
switch (item.shape) {
case 'circle': // 圆圈
ctx.arc(upx2px(item.x), upx2px(item.y), upx2px(item.radius), 0, 2 * Math.PI);
ctx.fill();
break;
case 'rect': // 四变形
ctx.fillRect(upx2px(item.x), upx2px(item.y), upx2px(item.width), upx2px(
item.height));
break;
case 'round': // 带圆角的形状
{
const radius = item.radius ? upx2px(item.radius) : 4;
ctx.arc(upx2px(item.x) + radius, upx2px(item.y) + radius, radius, Math.PI, (
Math.PI *
3) / 2);
ctx.lineTo(upx2px(item.width) - radius + upx2px(item.x), upx2px(item.y));
ctx.arc(upx2px(item.width) - radius + upx2px(item.x), radius + upx2px(
item.y),
radius, (Math.PI * 3) / 2, Math.PI * 2);
ctx.lineTo(upx2px(item.width) + upx2px(item.x), upx2px(item.height) + uni
.upx2px(
item.y) - radius);
ctx.arc(
upx2px(item.width) - radius + upx2px(item.x),
upx2px(item.height) - radius + upx2px(item.y),
radius,
0,
(Math.PI * 1) / 2
);
ctx.lineTo(radius + upx2px(item.x), upx2px(item.height) + upx2px(item.y));
ctx.arc(radius + upx2px(item.x), upx2px(item.height) - radius + upx2px(
item.y),
radius, (Math.PI * 1) / 2, Math.PI);
ctx.closePath();
ctx.fill();
ctx.strokeStyle = item.background;
ctx.stroke();
}
break;
}
ctx.restore();
break;
}
});
ctx.save();
wx.showToast({
title: '正在生成..',
icon: 'loading',
mask: true,
duration: 2 * 1000
});
ctx.draw(false);
setTimeout(() => {
try {
wx.canvasToTempFilePath({
canvasId: canvasId,
success: res => {
resolve(res.tempFilePath);
},
fail: err => {
reject(err);
},
complete() {
wx.hideToast();
}
});
} catch (error) {
console.log(error)
}
}, 300);
});
});
}
/**
* 计算换行
* @param {String} str 文字
* @param {Number} width 宽度
* @param {Number} row 行数
*/
newLine(str, width, row) {
const arr = [];
let str1 = '';
let newArr = [];
const ctx = this._ctx;
if (width) {
for (let i = 0; i < str.length; i++) {
if (i === str.length - 1) {
const str2 = str1 + str[i];
if (this.measureText(ctx.state.fontSize, str2)) {
arr.push(str2);
} else {
arr.push(str1);
str1 = str[i];
arr.push(str1);
}
} else {
const str2 = str1 + str[i];
if (this.measureText(ctx.state.fontSize, str2) > width) {
arr.push(str1);
str1 = str[i];
} else {
str1 = str2;
}
}
}
} else {
arr.push(str);
}
newArr = row ? arr.slice(0, row) : arr;
if (row && arr.length > row) {
const len = newArr[row - 1].length;
const lastStr = newArr[row - 1][len - 1];
const pattern = new RegExp('[\u4E00-\u9FA5]+');
newArr[row - 1] = pattern.test(lastStr) ?
newArr[row - 1].substring(0, newArr[row - 1].length - 1) + '...' :
newArr[row - 1].substring(0, newArr[row - 1].length - 2) + '...';
}
return newArr;
}
/**
* 计算文字宽度
* @param {Number} fontSize
* @param {String} str
*/
measureText(fontSize, str) {
if (!str) return 0;
if (typeof str !== 'string') {
str += '';
}
return (str.replace(/[^\x00-\xff]/g, 'ab').length / 2) * fontSize;
}
/**
* 处理图片
*/
asyncImage() {
this._canvasData.forEach(x => {
if (x.type === 'img') {
const p = this.downLoadFile(x.content, x.useGet).then(res => {
x.content = res;
});
this._pmImgTask.push(p);
}
});
}
/**
* 下载文件
* @param {String} url 下载文件地址
* @param {Boolean} useGet 是否需要从服务器下载(小程序未配置下载域名、跨域时需要服务器支持)
*/
downLoadFile(url, useGet) {
if (url.indexOf('base64') > -1) { // base64文件直接返回
return new Promise(resolve => {
resolve(url);
});
}
//url += `?r=${+new Date()}`; // 加上随机数防止微信缓存导致canvas画图时出错
if (url.indexOf("?") > -1) {
if (url.endsWith("&")) {
url += `r=${+new Date()}`; // 加上随机数防止微信缓存导致canvas画图时出错
} else {
url += `&r=${+new Date()}`; // 加上随机数防止微信缓存导致canvas画图时出错
}
} else {
url += `?r=${+new Date()}`; // 加上随机数防止微信缓存导致canvas画图时出错
}
return new Promise((resolve, reject) => {
wx.downloadFile({
url,
success: res => {
resolve(res.tempFilePath);
},
fail: err => {
console.error('下载文件出错', err, url);
reject(err);
wx.showModal({
title: '提示',
showCancel: false,
content: err.errMsg
});
}
});
});
}
/**
* 切割圆角
* @param {CanvasContext} ctx canvas上下文
* @param {number} x 圆角矩形选区的左上角 x坐标
* @param {number} y 圆角矩形选区的左上角 y坐标
* @param {number} w 圆角矩形选区的宽度
* @param {number} h 圆角矩形选区的高度
* @param {number} r 圆角的半径
*/
roundRect(ctx, x, y, w, h, r) {
ctx.beginPath();
ctx.fillStyle = "transparent";
ctx.arc(x + r, y + r, r, Math.PI, Math.PI * 1.5);
ctx.moveTo(x + r, y);
ctx.lineTo(x + w - r, y);
ctx.lineTo(x + w, y + r);
ctx.arc(x + w - r, y + r, r, Math.PI * 1.5, Math.PI * 2);
ctx.lineTo(x + w, y + h - r);
ctx.lineTo(x + w - r, y + h);
ctx.arc(x + w - r, y + h - r, r, 0, Math.PI * 0.5);
ctx.lineTo(x + r, y + h);
ctx.lineTo(x, y + h - r);
ctx.arc(x + r, y + h - r, r, Math.PI * 0.5, Math.PI);
ctx.lineTo(x, y + r);
ctx.lineTo(x + r, y);
ctx.fillStyle = "#fff";
ctx.fill();
ctx.clip();
}
/**
* 中文数量
* @param {*} val
*/
getZhongWenLen(val) {
var re = /[\u4E00-\u9FA5]/g;
if (re.test(val)) {
return val.match(re).length
}
return 0;
}
}
const EPS = 1e-4;
const BASE_DEVICE_WIDTH = 750;
function upx2px(number) {
let isIOS = false;
let deviceWidth = 0;
let deviceDPR = 0;
const {
platform,
pixelRatio,
windowWidth
} = wx.getSystemInfoSync();
deviceWidth = windowWidth;
deviceDPR = pixelRatio;
isIOS = platform === "ios";
number = Number(number);
if (number === 0) {
return 0;
}
let width = deviceWidth;
let result = number / BASE_DEVICE_WIDTH * width;
if (result < 0) {
result = -result;
}
result = Math.floor(result + EPS);
if (result === 0) {
if (deviceDPR === 1 || !isIOS) {
result = 1;
} else {
result = 0.5;
}
}
return number < 0 ? -result : result;
}
exports.MDCanvas = MDCanvas;
支持功能:
画线性渐变(支持多颜色)
画图片(支持自动裁切圆角)
画文字(支持自动换行、省略)
画圆形
画矩形(支持圆角)
使用:
1.页面,css是让canvas移除可是范围内
<view class="container">
<canvas class="canvas2" canvas-id="canvasId" id="canvasId" style="width: {{canvasWidth}}rpx; height:{{canvasHeight}}rpx;"></canvas>
</view>
//样式
.container {
height: 1000rpx;
width: 750rpx;
position: fixed;
left: 2000px;
top: 2000px;
}
2.js导入
import {
MDCanvas
} from "./md-canvas";
3.其他参数
data:{
canvasWidth: 468, //canvas宽
canvasHeight: 832, //canvas高
}
4.初始化canvas
onLoad(options) {
const ctx = wx.createCanvasContext("canvasId", this);
this.ctx = ctx;
}
5.代码使用
var canvasData=[{...}]
const mdCtx = new MDCanvas(ctx, "canvasId", canvasData);
mdCtx.drawCanvas().then((res) => {
console.log("临时图片地址", res);
}).catch((err) => {
console.error(err);
});
备注:多海报生成 ,需要特殊处理一下
//封装
drawCanvas(canvasData) {
var ctx = this.ctx;
const mdCtx = new MDCanvas(ctx, "canvasId", canvasData);
return new Promise((resolve) => {
mdCtx.drawCanvas().then((res) => {
console.log("临时图片地址", res);
resolve(res);
}).catch((err) => {
console.error(err);
});
});
}
//调用
var imgArray = [];
for (let element of canvasDataArray) {
var res = await this.drawCanvas(element);
imgArray.push(res)
}
参数
常规参数
参数名 | 类型 | 参考值 | 说明 |
type | String | bg、img、shape、text、lg | 绘画内容,如下: bg:背景 img:图片 shape:形状 lg:画线性渐变 text:文字 |
x | Number | 坐标轴X | |
y | Number | 坐标轴Y | |
width | Number | 内容宽度 | |
height | Number | 内容高度 |
1.当type为bg:
参数名 | 类型 | 参考值 | 说明 |
x | Number | 坐标轴X | |
y | Number | 坐标轴Y | |
width | Number | 内容宽度 | |
height | Number | 内容高度 | |
radius | Number | 半径 |
2.当type为img:
参数名 | 类型 | 参考值 | 说明 |
x | Number | 坐标轴X | |
y | Number | 坐标轴Y | |
width | Number | 内容宽度 | |
height | Number | 内容高度 | |
border | String | circle | border的值为circle时 自动裁切成圆形 |
radius | Number | 半径 border的值为circle时起效,不填写则剪切成圆 | |
content | String | 图片Url地址 |
3.当type为shape:
参数名 | 类型 | 参考值 | 说明 |
x | Number | 坐标轴X | |
y | Number | 坐标轴Y | |
width | Number | 内容宽度 | |
height | Number | 内容高度 | |
border | String | circle | border的值为circle时 自动裁切成圆形 |
radius | Number | 半径 border的值为circle时起效,不填写则剪切成圆 |
4.当type为text:
参数名 | 类型 | 默认值 | 说明 |
x | Number | 坐标轴X | |
y | Number | 坐标轴Y | |
width | Number | 内容宽度 | |
row | Number | 字体占用行数 | |
color | String | 文字颜色 | |
fontSize | Number | 30 | 字体大小 |
textAlign | String | 设置文字的对齐 | |
content | String | 文字内容 | |
bold | String | normal | 是否加粗,默认normal |
baseLine | String | normal | 设置文字的竖直对齐 |
family | String | Microsoft YaHei | 设置字体,默认Microsoft YaHei |
5.当type为lg:
参数名 | 类型 | 参考值 | 说明 |
x | Number | 坐标轴X | |
y | Number | 坐标轴Y | |
x0 | Number | 渐变起点的 x 坐标 | |
y0 | Number | 渐变起点的 y 坐标 | |
x1 | Number | 渐变终点的 x 坐标 | |
y1 | Number | 渐变终点的 y 坐标 | |
colors | Array | 渐变颜色 |
colors参数详情:
参数名 | 类型 | 参考值 | 说明 |
scope | Number | 表示渐变中开始与结束之间的位置,范围 0-1 | |
value | String | 色值,如#fff |