604 lines
12 KiB
TypeScript
604 lines
12 KiB
TypeScript
import { getCurrentInstance } from "vue";
|
|
import { isEmpty, isString, cloneDeep, isObject } from "lodash-es";
|
|
|
|
// 渲染参数
|
|
declare interface RenderOptions {
|
|
x: number;
|
|
y: number;
|
|
height?: number;
|
|
width?: number;
|
|
[key: string]: any;
|
|
}
|
|
|
|
// 文本渲染参数
|
|
declare interface TextRenderOptions extends RenderOptions {
|
|
text: string;
|
|
color?: string;
|
|
fontSize?: number;
|
|
textAlign?: "left" | "right" | "center";
|
|
overflow?: "ellipsis";
|
|
lineClamp?: number;
|
|
letterSpace?: number;
|
|
lineHeight?: number;
|
|
}
|
|
|
|
// 图片渲染参数
|
|
declare interface ImageRenderOptions extends RenderOptions {
|
|
url: string;
|
|
mode?: "aspectFill" | "aspectFit";
|
|
radius?: number;
|
|
}
|
|
|
|
// 块渲染参数
|
|
declare interface DivRenderOptions extends RenderOptions {
|
|
radius?: number;
|
|
backgroundColor?: string;
|
|
border?: {
|
|
width: number;
|
|
color: string;
|
|
};
|
|
}
|
|
|
|
// 导出图片参数
|
|
declare interface CreateImageOptins {
|
|
x?: number;
|
|
y?: number;
|
|
width?: number;
|
|
height?: number;
|
|
destWidth?: number;
|
|
destHeight?: number;
|
|
fileType?: "jpg" | "png";
|
|
quality?: number;
|
|
}
|
|
|
|
class Canvas {
|
|
ctx: any;
|
|
canvasId: any;
|
|
scope: any;
|
|
renderQuene: any;
|
|
imageQueue: any;
|
|
|
|
constructor(canvasId: string) {
|
|
// 绘图上下文
|
|
this.ctx = null;
|
|
|
|
// canvas id
|
|
this.canvasId = canvasId;
|
|
|
|
// 当前页面作用域
|
|
const { proxy }: any = getCurrentInstance();
|
|
this.scope = proxy;
|
|
|
|
// 渲染队列
|
|
this.renderQuene = [];
|
|
|
|
// 图片队列
|
|
this.imageQueue = [];
|
|
|
|
// 创建画布
|
|
this.create();
|
|
}
|
|
|
|
// 创建画布
|
|
create() {
|
|
this.ctx = uni.createCanvasContext(this.canvasId, this.scope);
|
|
return this;
|
|
}
|
|
|
|
// 块
|
|
div(options: DivRenderOptions) {
|
|
let render = () => {
|
|
this.divRender(options);
|
|
};
|
|
this.renderQuene.push(render);
|
|
return this;
|
|
}
|
|
|
|
// 文本
|
|
text(options: TextRenderOptions) {
|
|
let render = () => {
|
|
this.textRender(options);
|
|
};
|
|
this.renderQuene.push(render);
|
|
return this;
|
|
}
|
|
|
|
// 图片
|
|
image(options: ImageRenderOptions) {
|
|
let render = () => {
|
|
this.imageRender(options);
|
|
};
|
|
this.imageQueue.push(options);
|
|
this.renderQuene.push(render);
|
|
return this;
|
|
}
|
|
|
|
// 绘画
|
|
draw(save = false) {
|
|
return new Promise((resolve) => {
|
|
let next = () => {
|
|
this.render();
|
|
this.ctx.draw(save, () => {
|
|
resolve(true);
|
|
});
|
|
};
|
|
|
|
if (!isEmpty(this.imageQueue)) {
|
|
this.preLoadImage().then(next);
|
|
} else {
|
|
next();
|
|
}
|
|
});
|
|
}
|
|
|
|
// 生成图片
|
|
createImage(options?: CreateImageOptins): Promise<string> {
|
|
return new Promise((resolve, reject) => {
|
|
let data = {
|
|
canvasId: this.canvasId,
|
|
...options,
|
|
success: (res: any) => {
|
|
// #ifdef MP-ALIPAY
|
|
resolve(res.apFilePath);
|
|
// #endif
|
|
|
|
// #ifndef MP-ALIPAY
|
|
resolve(res.tempFilePath);
|
|
// #endif
|
|
},
|
|
fail: reject,
|
|
};
|
|
|
|
// #ifdef MP-ALIPAY
|
|
this.ctx.toTempFilePath(data);
|
|
// #endif
|
|
|
|
// #ifndef MP-ALIPAY
|
|
uni.canvasToTempFilePath(data, this.scope);
|
|
// #endif
|
|
});
|
|
}
|
|
|
|
// 保存图片
|
|
saveImage(options?: CreateImageOptins) {
|
|
uni.showLoading({
|
|
title: "图片下载中...",
|
|
});
|
|
this.createImage(options).then((path: any) => {
|
|
return new Promise((resolve) => {
|
|
uni.hideLoading();
|
|
uni.saveImageToPhotosAlbum({
|
|
filePath: path,
|
|
success: () => {
|
|
uni.showToast({
|
|
title: "保存图片成功",
|
|
});
|
|
resolve(path);
|
|
},
|
|
fail: (err) => {
|
|
// #ifdef MP-ALIPAY
|
|
uni.showToast({
|
|
title: "保存图片成功",
|
|
});
|
|
// #endif
|
|
|
|
// #ifndef MP-ALIPAY
|
|
uni.showToast({
|
|
title: "保存图片失败",
|
|
icon: "none",
|
|
});
|
|
// #endif
|
|
},
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
// 预览图片
|
|
previewImage(options?: CreateImageOptins) {
|
|
this.createImage(options).then((url: string | any) => {
|
|
uni.previewImage({
|
|
urls: [url],
|
|
});
|
|
});
|
|
}
|
|
|
|
// 下载图片
|
|
downLoadImage(item: any) {
|
|
return new Promise((resolve, reject) => {
|
|
if (!item.url) {
|
|
return reject("url 不能为空");
|
|
}
|
|
|
|
// 处理base64
|
|
// #ifdef MP
|
|
if (item.url.indexOf("data:image") >= 0) {
|
|
let extName = item.url.match(/data\:\S+\/(\S+);/);
|
|
if (extName) {
|
|
extName = extName[1];
|
|
}
|
|
const fs = uni.getFileSystemManager();
|
|
const fileName = Date.now() + "." + extName;
|
|
// @ts-ignore
|
|
const filePath = wx.env.USER_DATA_PATH + "/" + fileName;
|
|
|
|
return fs.writeFile({
|
|
filePath,
|
|
data: item.url.replace(/^data:\S+\/\S+;base64,/, ""),
|
|
encoding: "base64",
|
|
success: () => {
|
|
item.url = filePath;
|
|
resolve(filePath);
|
|
},
|
|
});
|
|
}
|
|
// #endif
|
|
|
|
// 是否网络图片
|
|
const isHttp = item.url.includes("http");
|
|
|
|
uni.getImageInfo({
|
|
src: item.url,
|
|
success: (result) => {
|
|
item.sheight = result.height;
|
|
item.swidth = result.width;
|
|
|
|
if (isHttp) {
|
|
item.url = result.path;
|
|
}
|
|
|
|
resolve(item.url);
|
|
},
|
|
fail: (err) => {
|
|
console.log(err, item.url);
|
|
reject(err);
|
|
},
|
|
});
|
|
|
|
return 1;
|
|
});
|
|
}
|
|
|
|
// 预加载图片
|
|
async preLoadImage() {
|
|
await Promise.all(this.imageQueue.map(this.downLoadImage));
|
|
}
|
|
|
|
// 设置背景颜色
|
|
setBackground(options: any) {
|
|
if (!options) return null;
|
|
|
|
let backgroundColor;
|
|
|
|
if (!isString(options)) {
|
|
backgroundColor = options;
|
|
}
|
|
|
|
if (isString(options.backgroundColor)) {
|
|
backgroundColor = options.backgroundColor;
|
|
}
|
|
|
|
if (isObject(options.backgroundColor)) {
|
|
let { startX, startY, endX, endY, gradient } = options.backgroundColor;
|
|
const rgb = this.ctx.createLinearGradient(startX, startY, endX, endY);
|
|
for (let i = 0, l = gradient.length; i < l; i++) {
|
|
rgb.addColorStop(gradient[i].step, gradient[i].color);
|
|
}
|
|
backgroundColor = rgb;
|
|
}
|
|
|
|
this.ctx.setFillStyle(backgroundColor);
|
|
|
|
return this;
|
|
}
|
|
|
|
// 设置边框
|
|
setBorder(options: any) {
|
|
if (!options.border) return this;
|
|
|
|
let { x, y, width: w, height: h, border, radius: r } = options;
|
|
|
|
if (border.width) {
|
|
this.ctx.setLineWidth(border.width);
|
|
}
|
|
|
|
if (border.color) {
|
|
this.ctx.setStrokeStyle(border.color);
|
|
}
|
|
|
|
// 偏移距离
|
|
let p = border.width / 2;
|
|
|
|
// 是否有圆角
|
|
if (r) {
|
|
this.drawRadiusRoute(x - p, y - p, w + 2 * p, h + 2 * p, r + p);
|
|
this.ctx.stroke();
|
|
} else {
|
|
this.ctx.strokeRect(x - p, y - p, w + 2 * p, h + 2 * p);
|
|
}
|
|
|
|
return this;
|
|
}
|
|
|
|
// 设置缩放,旋转
|
|
setTransform(options: any) {
|
|
if (options.scale) {
|
|
}
|
|
if (options.rotate) {
|
|
}
|
|
}
|
|
|
|
// 带有圆角的路径绘制
|
|
drawRadiusRoute(x: number, y: number, w: number, h: number, r: number) {
|
|
this.ctx.beginPath();
|
|
this.ctx.moveTo(x + r, y, y);
|
|
this.ctx.lineTo(x + w - r, y);
|
|
this.ctx.arc(x + w - r, y + r, r, 1.5 * Math.PI, 0);
|
|
this.ctx.lineTo(x + w, y + h - r);
|
|
this.ctx.arc(x + w - r, y + h - r, r, 0, 0.5 * Math.PI);
|
|
this.ctx.lineTo(x + r, y + h);
|
|
this.ctx.arc(x + r, y + h - r, r, 0.5 * Math.PI, Math.PI);
|
|
this.ctx.lineTo(x, y + r);
|
|
this.ctx.arc(x + r, y + r, r, Math.PI, 1.5 * Math.PI);
|
|
this.ctx.closePath();
|
|
}
|
|
|
|
// 裁剪图片
|
|
cropImage(
|
|
mode: "aspectFill" | "aspectFit",
|
|
width: number,
|
|
height: number,
|
|
sWidth: number,
|
|
sHeight: number,
|
|
x: number,
|
|
y: number
|
|
) {
|
|
let cx, cy, cw, ch, sx, sy, sw, sh;
|
|
switch (mode) {
|
|
case "aspectFill":
|
|
if (width <= height) {
|
|
let p = width / sWidth;
|
|
cw = width;
|
|
ch = sHeight * p;
|
|
cx = 0;
|
|
cy = (height - ch) / 2;
|
|
} else {
|
|
let p = height / sHeight;
|
|
cw = sWidth * p;
|
|
ch = height;
|
|
cx = (width - cw) / 2;
|
|
cy = 0;
|
|
}
|
|
break;
|
|
case "aspectFit":
|
|
if (width <= height) {
|
|
let p = height / sHeight;
|
|
sw = width / p;
|
|
sh = sHeight;
|
|
sx = x + (sWidth - sw) / 2;
|
|
sy = y;
|
|
} else {
|
|
let p = width / sWidth;
|
|
sw = sWidth;
|
|
sh = height / p;
|
|
sx = x;
|
|
sy = y + (sHeight - sh) / 2;
|
|
}
|
|
break;
|
|
}
|
|
return { cx, cy, cw, ch, sx, sy, sw, sh };
|
|
}
|
|
|
|
// 获取文本内容
|
|
getTextRows({
|
|
text,
|
|
fontSize = 14,
|
|
width = 100,
|
|
lineClamp = 1,
|
|
overflow,
|
|
letterSpace = 0,
|
|
}: any) {
|
|
let arr: any[] = [[]];
|
|
let a = 0;
|
|
|
|
for (let i = 0; i < text.length; i++) {
|
|
let b = this.getFontPx(text[i], { fontSize, letterSpace });
|
|
|
|
if (a + b > width) {
|
|
a = b;
|
|
arr.push(text[i]);
|
|
} else {
|
|
// 最后一行且设置超出省略号
|
|
if (
|
|
overflow == "ellipsis" &&
|
|
arr.length == lineClamp &&
|
|
a + 3 * this.getFontPx(".", { fontSize, letterSpace }) > width - 5
|
|
) {
|
|
arr[arr.length - 1] += "...";
|
|
break;
|
|
} else {
|
|
a += b;
|
|
arr[arr.length - 1] += text[i];
|
|
}
|
|
}
|
|
}
|
|
|
|
return arr;
|
|
}
|
|
|
|
// 获取单个字体像素大小
|
|
getFontPx(text: string, { fontSize = 14, letterSpace }: any) {
|
|
if (!text) {
|
|
return fontSize / 2 + fontSize / 14 + letterSpace;
|
|
}
|
|
|
|
let ch = text.charCodeAt(0);
|
|
|
|
if ((ch >= 0x0001 && ch <= 0x007e) || (0xff60 <= ch && ch <= 0xff9f)) {
|
|
return fontSize / 2 + fontSize / 14 + letterSpace;
|
|
} else {
|
|
return fontSize + letterSpace;
|
|
}
|
|
}
|
|
|
|
// 渲染块
|
|
divRender(options: DivRenderOptions) {
|
|
this.ctx.save();
|
|
this.setBackground(options);
|
|
this.setBorder(options);
|
|
this.setTransform(options);
|
|
|
|
// 区分是否有圆角采用不同模式渲染
|
|
if (options.radius) {
|
|
let { x, y } = options;
|
|
let w = options.width || 0;
|
|
let h = options.height || 0;
|
|
let r = options.radius || 0;
|
|
// 画路径
|
|
this.drawRadiusRoute(x, y, w, h, r);
|
|
// 填充
|
|
this.ctx.fill();
|
|
} else {
|
|
this.ctx.fillRect(options.x, options.y, options.width, options.height);
|
|
}
|
|
this.ctx.restore();
|
|
}
|
|
|
|
// 渲染文本
|
|
textRender(options: TextRenderOptions) {
|
|
let {
|
|
fontSize = 14,
|
|
textAlign,
|
|
width,
|
|
color = "#000000",
|
|
x,
|
|
y,
|
|
letterSpace,
|
|
lineHeight = 14,
|
|
} = options || {};
|
|
|
|
this.ctx.save();
|
|
|
|
// 设置字体大小
|
|
this.ctx.setFontSize(fontSize);
|
|
|
|
// 设置字体颜色
|
|
this.ctx.setFillStyle(color);
|
|
|
|
// 获取文本内容
|
|
let rows = this.getTextRows(options);
|
|
|
|
// 获取文本行高
|
|
let lh = lineHeight - fontSize;
|
|
|
|
// 左偏移
|
|
let offsetLeft = 0;
|
|
|
|
// 字体对齐
|
|
if (textAlign && width) {
|
|
this.ctx.textAlign = textAlign;
|
|
|
|
switch (textAlign) {
|
|
case "left":
|
|
break;
|
|
case "center":
|
|
offsetLeft = width / 2;
|
|
break;
|
|
case "right":
|
|
offsetLeft = width;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// 逐行写入
|
|
for (let i = 0; i < rows.length; i++) {
|
|
let d = offsetLeft;
|
|
|
|
if (letterSpace) {
|
|
for (let j = 0; j < rows[i].length; j++) {
|
|
// 写入文字
|
|
this.ctx.fillText(rows[i][j], x + d, (i + 1) * fontSize + y + lh * i);
|
|
|
|
// 设置偏移
|
|
d += this.getFontPx(rows[i][j], options);
|
|
}
|
|
} else {
|
|
// 写入文字
|
|
this.ctx.fillText(rows[i], x + offsetLeft, (i + 1) * fontSize + y + lh * i);
|
|
}
|
|
}
|
|
|
|
this.ctx.restore();
|
|
}
|
|
|
|
// 渲染图片
|
|
imageRender(options: ImageRenderOptions) {
|
|
this.ctx.save();
|
|
|
|
if (options.radius) {
|
|
// 画路径
|
|
this.drawRadiusRoute(
|
|
options.x,
|
|
options.y,
|
|
options.width || options.swidth,
|
|
options.height || options.sHeight,
|
|
options.radius
|
|
);
|
|
// 填充
|
|
this.ctx.fill();
|
|
// 裁剪
|
|
this.ctx.clip();
|
|
}
|
|
let temp = cloneDeep(this.imageQueue[0]);
|
|
|
|
if (options.mode) {
|
|
let { cx, cy, cw, ch, sx, sy, sw, sh } = this.cropImage(
|
|
options.mode,
|
|
temp.swidth,
|
|
temp.sheight,
|
|
temp.width,
|
|
temp.height,
|
|
temp.x,
|
|
temp.y
|
|
);
|
|
switch (options.mode) {
|
|
case "aspectFit":
|
|
this.ctx.drawImage(temp.url, sx, sy, sw, sh);
|
|
break;
|
|
case "aspectFill":
|
|
this.ctx.drawImage(
|
|
temp.url,
|
|
cx,
|
|
cy,
|
|
cw,
|
|
ch,
|
|
temp.x,
|
|
temp.y,
|
|
temp.width,
|
|
temp.height
|
|
);
|
|
break;
|
|
}
|
|
} else {
|
|
this.ctx.drawImage(
|
|
temp.url,
|
|
temp.x,
|
|
temp.y,
|
|
temp.width || temp.swidth,
|
|
temp.height || temp.sheight
|
|
);
|
|
}
|
|
this.imageQueue.shift();
|
|
this.ctx.restore();
|
|
}
|
|
|
|
// 渲染全部
|
|
render() {
|
|
this.renderQuene.forEach((ele: any) => {
|
|
ele();
|
|
});
|
|
}
|
|
}
|
|
|
|
export { Canvas };
|