样式
<view class="poster-share">
<!-- 自定义导航栏 -->
<!-- <ayi-topbar title="商品海报" background-color="#f5f5f5" color="#282828" :border="false"></ayi-topbar> -->
<!-- 海报图片 -->
<view class="picture">
<canvas class="canvas" v-if="attr.isShow" canvas-id="cvs"></canvas>
<image class="p-img" v-else :src="attr.tempImg" mode="aspectFill"></image>
</view>
<!-- 保存图片 -->
<view class="keep">
<view class="tip">保存至相册可分享到朋友圈</view>
<view class="btn" @click="saveImg">保存图片</view>
</view>
</view>
</template>
<script lang="ts" setup>
import { reactive, nextTick, ref } from "vue";
import Canvas from "./lib/canvas";
// import ayiTopbar from "@/components/yll1024335892-ayi-topbar/index.vue"
import avatarPng from "@/static/yll1024335892-page-24/avatar.png";
import goodsPng from "@/static/yll1024335892-page-24/goods.png";
import codePng from "@/static/yll1024335892-page-24/code.png";
const attr = reactive({
isShow: true,
name: "无语",
tempImg: "",
cvs: null
});
const imageurl = ref("");
const saveImg = () => {
uni.showToast({
title: "请长按图片保存!",
icon: "none",
duration: 2000
});
uni.saveImageToPhotosAlbum({
filePath: imageurl.value,
success: () => {
uni.showToast({
title: "已保存到相册,快去访问吧!",
icon: "none"
});
}
});
attr.cvs.createImage().then((res: any) => {
console.log(res);
uni.saveImageToPhotosAlbum({
filePath: res,
success: () => {
uni.showToast({
title: "已保存到相册,快去访问吧!",
icon: "none"
});
}
});
});
};
const init = () => {
attr.cvs = new Canvas("cvs");
console.log(attr.cvs);
return new Promise((_resolve, _reject) => {
attr.cvs
.div({
x: 0,
y: 0,
radius: 8,
backgroundColor: "#fff"
})
.image({
x: 15,
y: 18,
height: 33,
width: 33,
mode: "aspectFill",
url: avatarPng
})
.text({
x: 54,
y: 22.5,
height: 24,
text: "洛克" + attr.name,
overflow: "ellipsis",
color: "#FF674E"
})
.text({
x: 140,
y: 22.5,
width: 192,
height: 24,
text: "1",
color: "#282828"
})
.image({
x: -8,
y: -10,
height: 520,
width: 355,
mode: "aspectFill",
url: goodsPng
})
.text({
x: 15,
y: 40,
width: 10,
height: 24,
overflow: "ellipsis",
lineClamp: 2,
fontSize: 18,
lineHeight: 18,
text: "免费赠送的小猫咪千万不要随意抛弃",
color: "#fff"
})
.image({
x: 232.5,
y: 393,
height: 69,
width: 69,
mode: "aspectFill",
url: codePng
})
.text({
x: 15,
y: 467,
width: 192,
height: 22,
fontSize: 22,
text: "¥3.14",
color: "#FF674E",
textDecoration: "underline", // 下划线
textStyle: "stroke" // 空心字体
})
.text({
x: 204,
y: 472,
width: 126,
height: 14,
fontSize: 14,
text: "长按识别二维码访问",
color: "#B1B1B1"
})
.draw()
.then(() => {
nextTick(() => {
attr.cvs.createImage().then((res: string) => {
console.log(res);
imageurl.value = res;
uni.downloadFile({
url: res,
success: (data) => {
if (data.statusCode === 200) {
attr.tempImg = data.tempFilePath;
attr.isShow = false;
}
}
});
});
});
});
});
};
init();
</script>
<style lang="scss">
page {
background: #f5f5f5;
}
.poster-share {
.picture {
padding: 30rpx;
width: 690rpx;
height: 1014rpx;
box-sizing: border-box;
margin: 0 auto;
.canvas {
width: 345px;
height: 507px;
}
.p-img {
width: 100%;
height: 100%;
}
}
.keep {
margin-top: 50rpx;
.tip {
text-align: center;
font-size: 28rpx;
color: #b1b1b1;
}
.btn {
margin: 0 auto;
margin-top: 10rpx;
width: 276rpx;
height: 84rpx;
line-height: 84rpx;
font-size: 32rpx;
text-align: center;
color: #ffffff;
background: #ff674e;
border-radius: 42rpx;
}
}
}
</style>
ts部分
export function isString(value) {
return typeof value === "string";
}
const isObject = (value) => {
return value != null && (typeof value == "object" || typeof value == "function");
};
export function isEmpty(val: any) {
if (typeof val == "boolean") {
return false;
}
if (typeof val == "number") {
return false;
}
if (val instanceof Array) {
if (val.length == 0) return true;
} else if (val instanceof Object) {
if (JSON.stringify(val) === "{}") return true;
} else {
if (val == "null" || val == null || val == "undefined" || val == undefined || val == "") return true;
if ((val + "").replace(/\s*/g, "") == "") return true;
return false;
}
return false;
}
const getObjType = (obj) => {
const toString = Object.prototype.toString;
const map = {
"[object Boolean]": "boolean",
"[object Number]": "number",
"[object String]": "string",
"[object Function]": "function",
"[object Array]": "array",
"[object Date]": "date",
"[object RegExp]": "regExp",
"[object Undefined]": "undefined",
"[object Null]": "null",
"[object Object]": "object"
};
if (obj instanceof Object) {
return "element";
}
return map[toString.call(obj)];
};
/**
* @description 深度克隆
* @param {object} obj 需要深度克隆的对象
* @returns {*} 克隆后的对象或者原值(不是对象)
*/
export function cloneDeep(data) {
const type = getObjType(data);
let obj;
if (type === "array") {
obj = [];
} else if (type === "object") {
obj = {};
} else {
//不再具有下一层次
return data;
}
if (type === "array") {
for (let i = 0, len = data.length; i < len; i++) {
obj.push(cloneDeep(data[i]));
}
} else if (type === "object") {
for (let key in data) {
obj[key] = cloneDeep(data[key]);
}
}
return obj;
}
// 渲染参数
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 {
mode: "aspectFill" | "aspectFit";
url: string;
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 default Canvas;