hookehuyr

✨ feat: 新增生成canvas海报控件,调试分享海报功能

...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
2 * @Author: hookehuyr hookehuyr@gmail.com 2 * @Author: hookehuyr hookehuyr@gmail.com
3 * @Date: 2022-05-27 15:57:59 3 * @Date: 2022-05-27 15:57:59
4 * @LastEditors: hookehuyr hookehuyr@gmail.com 4 * @LastEditors: hookehuyr hookehuyr@gmail.com
5 - * @LastEditTime: 2022-09-26 14:37:22 5 + * @LastEditTime: 2022-09-27 09:41:31
6 * @FilePath: /swx/src/app.config.js 6 * @FilePath: /swx/src/app.config.js
7 * @Description: 7 * @Description:
8 */ 8 */
...@@ -24,6 +24,7 @@ export default { ...@@ -24,6 +24,7 @@ export default {
24 'pages/my/index', 24 'pages/my/index',
25 'pages/createActivity/index', 25 'pages/createActivity/index',
26 'pages/activityDetail/index', 26 'pages/activityDetail/index',
27 + 'pages/post/index',
27 ], 28 ],
28 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去 29 subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
29 { 30 {
......
1 +<template>
2 + <canvas
3 + type="2d"
4 + :id="canvasId"
5 + :style="`height: ${height}rpx; width:${width}rpx;
6 + position: absolute;
7 + ${debug ? '' : 'transform:translate3d(-9999rpx, 0, 0)'}`"
8 + />
9 +</template>
10 +<script lang="ts">
11 +import Taro from "@tarojs/taro"
12 +import { defineComponent, onMounted, PropType, ref } from "vue"
13 +import { Image, DrawConfig } from "./types"
14 +import { drawImage, drawText, drawBlock, drawLine } from "./utils/draw"
15 +import {
16 + toPx,
17 + toRpx,
18 + getRandomId,
19 + getImageInfo,
20 + getLinearColor,
21 +} from "./utils/tools"
22 +
23 +export default defineComponent({
24 + name: "PosterBuilder",
25 + props: {
26 + showLoading: {
27 + type: Boolean,
28 + default: false,
29 + },
30 + config: {
31 + type: Object as PropType<DrawConfig>,
32 + default: () => ({}),
33 + },
34 + },
35 + emits: ["success", "fail"],
36 + setup(props, context) {
37 + const count = ref(1)
38 + const {
39 + width,
40 + height,
41 + backgroundColor,
42 + texts = [],
43 + blocks = [],
44 + lines = [],
45 + debug = false,
46 + } = props.config || {}
47 +
48 + const canvasId = getRandomId()
49 +
50 + /**
51 + * step1: 初始化图片资源
52 + * @param {Array} images = imgTask
53 + * @return {Promise} downloadImagePromise
54 + */
55 + const initImages = (images: Image[]) => {
56 + const imagesTemp = images.filter((item) => item.url)
57 + const drawList = imagesTemp.map((item, index) =>
58 + getImageInfo(item, index)
59 + )
60 + return Promise.all(drawList)
61 + }
62 +
63 + /**
64 + * step2: 初始化 canvas && 获取其 dom 节点和实例
65 + * @return {Promise} resolve 里返回其 dom 和实例
66 + */
67 + const initCanvas = () =>
68 + new Promise<any>((resolve) => {
69 + setTimeout(() => {
70 + const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例
71 + const query = Taro.createSelectorQuery().in(pageInstance) // 确定在当前页面内匹配子元素
72 + query
73 + .select(`#${canvasId}`)
74 + .fields({ node: true, size: true, context: true }, (res) => {
75 + const canvas = res.node
76 + const ctx = canvas.getContext("2d")
77 + resolve({ ctx, canvas })
78 + })
79 + .exec()
80 + }, 300)
81 + })
82 +
83 + /**
84 + * @description 保存绘制的图片
85 + * @param { object } config
86 + */
87 + const getTempFile = (canvas) => {
88 + Taro.canvasToTempFilePath(
89 + {
90 + canvas,
91 + success: (result) => {
92 + Taro.hideLoading()
93 + context.emit("success", result)
94 + },
95 + fail: (error) => {
96 + const { errMsg } = error
97 + if (errMsg === "canvasToTempFilePath:fail:create bitmap failed") {
98 + count.value += 1
99 + if (count.value <= 3) {
100 + getTempFile(canvas)
101 + } else {
102 + Taro.hideLoading()
103 + Taro.showToast({
104 + icon: "none",
105 + title: errMsg || "绘制海报失败",
106 + })
107 + context.emit("fail", errMsg)
108 + }
109 + }
110 + },
111 + },
112 + context
113 + )
114 + }
115 +
116 + /**
117 + * step2: 开始绘制任务
118 + * @param { Array } drawTasks 待绘制任务
119 + */
120 + const startDrawing = async (drawTasks) => {
121 + // TODO: check
122 + // const configHeight = getHeight(config)
123 + const { ctx, canvas } = await initCanvas()
124 +
125 + canvas.width = width
126 + canvas.height = height
127 +
128 + // 设置画布底色
129 + if (backgroundColor) {
130 + ctx.save() // 保存绘图上下文
131 + const grd = getLinearColor(ctx, backgroundColor, 0, 0, width, height)
132 + ctx.fillStyle = grd // 设置填充颜色
133 + ctx.fillRect(0, 0, width, height) // 填充一个矩形
134 + ctx.restore() // 恢复之前保存的绘图上下文
135 + }
136 + // 将要画的方块、文字、线条放进队列数组
137 + const queue = drawTasks
138 + .concat(
139 + texts.map((item) => {
140 + item.type = "text"
141 + item.zIndex = item.zIndex || 0
142 + return item
143 + })
144 + )
145 + .concat(
146 + blocks.map((item) => {
147 + item.type = "block"
148 + item.zIndex = item.zIndex || 0
149 + return item
150 + })
151 + )
152 + .concat(
153 + lines.map((item) => {
154 + item.type = "line"
155 + item.zIndex = item.zIndex || 0
156 + return item
157 + })
158 + )
159 +
160 + queue.sort((a, b) => a.zIndex - b.zIndex) // 按照层叠顺序由低至高排序, 先画低的,再画高的
161 + for (let i = 0; i < queue.length; i++) {
162 + const drawOptions = {
163 + canvas,
164 + ctx,
165 + toPx,
166 + toRpx,
167 + }
168 + if (queue[i].type === "image") {
169 + await drawImage(queue[i], drawOptions)
170 + } else if (queue[i].type === "text") {
171 + drawText(queue[i], drawOptions)
172 + } else if (queue[i].type === "block") {
173 + drawBlock(queue[i], drawOptions)
174 + } else if (queue[i].type === "line") {
175 + drawLine(queue[i], drawOptions)
176 + }
177 + }
178 +
179 + setTimeout(() => {
180 + getTempFile(canvas) // 需要做延时才能能正常加载图片
181 + }, 300)
182 + }
183 +
184 + // start: 初始化 canvas 实例 && 下载图片资源
185 + const init = () => {
186 + if (props.showLoading)
187 + Taro.showLoading({ mask: true, title: "生成中..." })
188 + if (props.config?.images?.length) {
189 + initImages(props.config.images)
190 + .then((result) => {
191 + // 1. 下载图片资源
192 + startDrawing(result)
193 + })
194 + .catch((err) => {
195 + Taro.hideLoading()
196 + Taro.showToast({
197 + icon: "none",
198 + title: err.errMsg || "下载图片失败",
199 + })
200 + context.emit("fail", err)
201 + })
202 + } else {
203 + startDrawing([])
204 + }
205 + }
206 +
207 + onMounted(() => {
208 + init()
209 + })
210 +
211 + return {
212 + canvasId,
213 + debug,
214 + width,
215 + height,
216 + }
217 + },
218 +})
219 +</script>
1 +export type DrawType = 'text' | 'image' | 'block' | 'line';
2 +
3 +export interface Block {
4 + type?: DrawType;
5 + x: number;
6 + y: number;
7 + width?: number;
8 + height: number;
9 + paddingLeft?: number;
10 + paddingRight?: number;
11 + borderWidth?: number;
12 + borderColor?: string;
13 + backgroundColor?: string;
14 + borderRadius?: number;
15 + borderRadiusGroup?: number[];
16 + text?: Text;
17 + opacity?: number;
18 + zIndex?: number;
19 +}
20 +
21 +export interface Text {
22 + type?: DrawType;
23 + x?: number;
24 + y?: number;
25 + text: string | Text[];
26 + fontSize?: number;
27 + color?: string;
28 + opacity?: 1 | 0;
29 + lineHeight?: number;
30 + lineNum?: number;
31 + width?: number;
32 + marginTop?: number;
33 + marginLeft?: number;
34 + marginRight?: number;
35 + textDecoration?: 'line-through' | 'none';
36 + baseLine?: 'top' | 'middle' | 'bottom';
37 + textAlign?: 'left' | 'center' | 'right';
38 + fontFamily?: string;
39 + fontWeight?: string;
40 + fontStyle?: string;
41 + zIndex?: number;
42 +}
43 +
44 +export interface Image {
45 + type?: DrawType;
46 + x: number;
47 + y: number;
48 + url: string;
49 + width: number;
50 + height: number;
51 + borderRadius?: number;
52 + borderRadiusGroup?: number[];
53 + borderWidth?: number;
54 + borderColor?: string;
55 + zIndex?: number;
56 +}
57 +
58 +export interface Line {
59 + type?: DrawType;
60 + startX: number;
61 + startY: number;
62 + endX: number;
63 + endY: number;
64 + width: number;
65 + color?: string;
66 + zIndex?: number;
67 +}
68 +
69 +export type DrawConfig = {
70 + width: number;
71 + height: number;
72 + backgroundColor?: string;
73 + debug?: boolean;
74 + blocks?: Block[];
75 + texts?: Text[];
76 + images?: Image[];
77 + lines?: Line[];
78 +};
1 +/* eslint-disable no-underscore-dangle */
2 +import { getLinearColor, getTextX, toPx } from './tools';
3 +
4 +/**
5 + * 绘制圆角矩形
6 + * @param { object } drawData - 绘制数据
7 + * @param { number } drawData.x - 左上角x坐标
8 + * @param { number } drawData.y - 左上角y坐标
9 + * @param { number } drawData.w - 矩形的宽
10 + * @param { number } drawData.h - 矩形的高
11 + * @param { number } drawData.r - 圆角半径
12 + * @param { object } drawOptions - 绘制对象
13 + * @param { object } drawOptions.ctx - ctx对象
14 + * @description arcTo 比 arc 更加简洁,三点画弧,但是比较难理解 参考资料:http://www.yanghuiqing.com/web/346
15 + * ctx.arc(x, y, radius, startAngle, endAngle, anticlockwise(是否逆时针画弧))
16 + * ctx.arcTo(x1, y1, x2, y2, radius); // 当前点-x1点 画切线 x1点到x2点画切线, 用半径为radius的圆弧替换掉切线部分
17 + */
18 +export function _drawRadiusRect({ x, y, w, h, r }, { ctx }) {
19 + const minSize = Math.min(w, h);
20 + if (r > minSize / 2) r = minSize / 2;
21 + ctx.beginPath();
22 + ctx.moveTo(x + r, y);
23 + ctx.arcTo(x + w, y, x + w, y + h, r); // 绘制上边框和右上角弧线
24 + ctx.arcTo(x + w, y + h, x, y + h, r); // 绘制右边框和右下角弧线
25 + ctx.arcTo(x, y + h, x, y, r); // 绘制下边框和左下角弧线
26 + ctx.arcTo(x, y, x + w, y, r); // 绘制左边框和左上角弧线
27 + ctx.closePath();
28 +}
29 +
30 +/**
31 + * 绘制圆角矩形
32 + * @param { object } drawData - 绘制数据
33 + * @param { number } drawData.x - 左上角x坐标
34 + * @param { number } drawData.y - 左上角y坐标
35 + * @param { number } drawData.w - 矩形的宽
36 + * @param { number } drawData.h - 矩形的高
37 + * @param { number } drawData.g - 圆角半径数组
38 + * @param { object } drawOptions - 绘制对象
39 + * @param { object } drawOptions.ctx - ctx对象
40 + */
41 +export function _drawRadiusGroupRect({ x, y, w, h, g }, { ctx }) {
42 + const [
43 + borderTopLeftRadius,
44 + borderTopRightRadius,
45 + borderBottomRightRadius,
46 + borderBottomLeftRadius
47 + ] = g;
48 + ctx.beginPath();
49 + ctx.arc(
50 + x + w - borderBottomRightRadius,
51 + y + h - borderBottomRightRadius,
52 + borderBottomRightRadius,
53 + 0,
54 + Math.PI * 0.5
55 + );
56 + ctx.lineTo(x + borderBottomLeftRadius, y + h);
57 + // 左下角
58 + ctx.arc(
59 + x + borderBottomLeftRadius,
60 + y + h - borderBottomLeftRadius,
61 + borderBottomLeftRadius,
62 + Math.PI * 0.5,
63 + Math.PI
64 + );
65 + ctx.lineTo(x, y + borderTopLeftRadius);
66 + // 左上角
67 + ctx.arc(
68 + x + borderTopLeftRadius,
69 + y + borderTopLeftRadius,
70 + borderTopLeftRadius,
71 + Math.PI,
72 + Math.PI * 1.5
73 + );
74 + ctx.lineTo(x + w - borderTopRightRadius, y);
75 + // 右上角
76 + ctx.arc(
77 + x + w - borderTopRightRadius,
78 + y + borderTopRightRadius,
79 + borderTopRightRadius,
80 + Math.PI * 1.5,
81 + Math.PI * 2
82 + );
83 + ctx.lineTo(x + w, y + h - borderBottomRightRadius);
84 + // ctx.arcTo(x + w, y, x + w, y + h, r); // 绘制上边框和右上角弧线
85 + // ctx.arcTo(x + w, y + h, x, y + h, r); // 绘制右边框和右下角弧线
86 + // ctx.arcTo(x, y + h, x, y, r); // 绘制下边框和左下角弧线
87 + // ctx.arcTo(x, y, x + w, y, r); // 绘制左边框和左上角弧线
88 + ctx.closePath();
89 +}
90 +
91 +/**
92 + * 计算文本长度
93 + * @param { Array | Object } text 数组 或者 对象
94 + * @param { object } drawOptions - 绘制对象
95 + * @param { object } drawOptions.ctx - ctx对象
96 + */
97 +export function _getTextWidth(text, drawOptions) {
98 + const { ctx } = drawOptions;
99 + let texts: any[] = [];
100 + if (Object.prototype.toString.call(text) === '[object Object]') {
101 + texts.push(text);
102 + } else {
103 + texts = text;
104 + }
105 + let width = 0;
106 + texts.forEach(
107 + ({
108 + fontSize,
109 + text: textStr,
110 + fontStyle = 'normal',
111 + fontWeight = 'normal',
112 + fontFamily = 'sans-serif',
113 + marginLeft = 0,
114 + marginRight = 0
115 + }) => {
116 + ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
117 + width += ctx.measureText(textStr).width + marginLeft + marginRight;
118 + }
119 + );
120 + return width;
121 +}
122 +
123 +/**
124 + * 渲染一段文字
125 + * @param { object } drawData - 绘制数据
126 + * @param { number } drawData.x - x坐标 rpx
127 + * @param { number } drawData.y - y坐标 rpx
128 + * @param { number } drawData.fontSize - 文字大小 rpx
129 + * @param { number } [drawData.color] - 颜色
130 + * @param { string } [drawData.baseLine] - 基线对齐方式 top| middle|bottom|...
131 + * @param { string } [drawData.textAlign='left'] - 对齐方式 left|center|right
132 + * @param { string } drawData.text - 当Object类型时,参数为 text 字段的参数,marginLeft、marginRight这两个字段可用
133 + * @param { number } [drawData.opacity=1] - 1为不透明,0为透明
134 + * @param { string } [drawData.textDecoration='none']
135 + * @param { number } [drawData.width] - 文字宽度 没有指定为画布宽度
136 + * @param { number } [drawData.lineNum=1] - 根据宽度换行,最多的行数
137 + * @param { number } [drawData.lineHeight=0] - 行高
138 + * @param { string } [drawData.fontWeight='normal'] - 'bold' 加粗字体,目前小程序不支持 100 - 900 加粗
139 + * @param { string } [drawData.fontStyle='normal'] - 'italic' 倾斜字体
140 + * @param { string } [drawData.fontFamily="sans-serif"] - 小程序默认字体为 'sans-serif', 请输入小程序支持的字体
141 + *
142 + * @param { object } drawOptions - 绘制对象
143 + * @param { object } drawOptions.ctx - ctx对象
144 + */
145 +export function _drawSingleText(drawData, drawOptions) {
146 + const {
147 + x = 0,
148 + y = 0,
149 + text,
150 + color,
151 + width,
152 + fontSize = 28,
153 + baseLine = 'top',
154 + textAlign = 'left',
155 + opacity = 1,
156 + textDecoration = 'none',
157 + lineNum = 1,
158 + lineHeight = 0,
159 + fontWeight = 'normal',
160 + fontStyle = 'normal',
161 + fontFamily = 'sans-serif'
162 + } = drawData;
163 + const { ctx } = drawOptions;
164 + // 画笔初始化
165 + ctx.save();
166 + ctx.beginPath();
167 + ctx.font = `${fontStyle} ${fontWeight} ${fontSize}px ${fontFamily}`;
168 + ctx.globalAlpha = opacity;
169 + ctx.fillStyle = color;
170 + ctx.textBaseline = baseLine;
171 + ctx.textAlign = textAlign;
172 + let textWidth = ctx.measureText(text).width; // 测量文本宽度
173 + const textArr: string[] = [];
174 +
175 + // 文本超出换行
176 + if (textWidth > width) {
177 + // 如果超出一行 ,则判断要分为几行
178 + let fillText = ''; // 当前行已拼接的文字
179 + let line = 1; // 当前是第几行
180 + for (let i = 0; i <= text.length - 1; i++) {
181 + // 将文字转为数组,一行文字一个元素
182 + fillText += text[i]; // 当前已拼接文字串
183 + const nextText = i < text.length - 1 ? fillText + text[i + 1] : fillText; // 再拼接下一个文字
184 + const restWidth = width - ctx.measureText(nextText).width; // 拼接下一个文字后的剩余宽度
185 +
186 + if (restWidth < 0) {
187 + // 如果拼接下一个字就超出宽度则添加者省略号或者换行
188 + if (line === lineNum) {
189 + // 已经是最后一行,就拼接省略号
190 + if (
191 + restWidth + ctx.measureText(text[i + 1]).width >
192 + ctx.measureText('...').width
193 + ) {
194 + // 剩余宽度能否放下省略号
195 + fillText = `${fillText}...`;
196 + } else {
197 + fillText = `${fillText.substr(0, fillText.length - 1)}...`;
198 + }
199 + textArr.push(fillText);
200 + break;
201 + } else {
202 + // 如果不是最后一行,就换行
203 + textArr.push(fillText);
204 + line++;
205 + fillText = '';
206 + }
207 + } else if (i === text.length - 1) {
208 + textArr.push(fillText);
209 + }
210 + }
211 + textWidth = width;
212 + } else {
213 + textArr.push(text);
214 + }
215 +
216 + // 按行渲染文字
217 + textArr.forEach((item, index) =>
218 + ctx.fillText(
219 + item,
220 + getTextX(textAlign, x, width), // 根据文本对齐方式和宽度确定 x 坐标
221 + y + (lineHeight || fontSize) * index // 根据行数、行高 || 字体大小确定 y 坐标
222 + )
223 + );
224 + ctx.restore();
225 +
226 + // 文本修饰,下划线、删除线什么的
227 + if (textDecoration !== 'none') {
228 + let lineY = y;
229 + if (textDecoration === 'line-through') {
230 + // 目前只支持贯穿线
231 + lineY = y;
232 + }
233 + ctx.save();
234 + ctx.moveTo(x, lineY);
235 + ctx.lineTo(x + textWidth, lineY);
236 + ctx.strokeStyle = color;
237 + ctx.stroke();
238 + ctx.restore();
239 + }
240 + return textWidth;
241 +}
242 +
243 +/**
244 + * 渲染文字
245 + * @param { object } params - 绘制数据
246 + * @param { number } params.x - x坐标 rpx
247 + * @param { number } params.y - y坐标 rpx
248 + * @param { number } params.fontSize - 文字大小 rpx
249 + * @param { number } [params.color] - 颜色
250 + * @param { string } [params.baseLine] - 基线对齐方式 top| middle|bottom
251 + * @param { string } [params.textAlign='left'] - 对齐方式 left|center|right
252 + * @param { string } params.text - 当Object类型时,参数为 text 字段的参数,marginLeft、marginRight这两个字段可用
253 + * @param { number } [params.opacity=1] - 1为不透明,0为透明
254 + * @param { string } [params.textDecoration='none']
255 + * @param { number } [params.width] - 文字宽度 没有指定为画布宽度
256 + * @param { number } [params.lineNum=1] - 根据宽度换行,最多的行数
257 + * @param { number } [params.lineHeight=0] - 行高
258 + * @param { string } [params.fontWeight='normal'] - 'bold' 加粗字体,目前小程序不支持 100 - 900 加粗
259 + * @param { string } [params.fontStyle='normal'] - 'italic' 倾斜字体
260 + * @param { string } [params.fontFamily="sans-serif"] - 小程序默认字体为 'sans-serif', 请输入小程序支持的字体
261 + *
262 + * @param { object } drawOptions - 绘制对象
263 + * @param { object } drawOptions.ctx - ctx对象
264 + */
265 +export function drawText(params, drawOptions) {
266 + const { x = 0, y = 0, text, baseLine } = params;
267 + if (Object.prototype.toString.call(text) === '[object Array]') {
268 + const preText = { x, y, baseLine };
269 +
270 + // 遍历多行文字,一行一行渲染
271 + text.forEach((item) => {
272 + preText.x += item.marginLeft || 0;
273 + // TODO:多段文字超出一行的处理
274 + const textWidth = _drawSingleText(
275 + Object.assign(item, { ...preText, y: y + (item.marginTop || 0) }),
276 + drawOptions
277 + );
278 + preText.x += textWidth + (item.marginRight || 0); // 下一段文字的 x 坐标为上一段字 x坐标 + 文字宽度 + marginRight
279 + });
280 + } else {
281 + _drawSingleText(params, drawOptions);
282 + }
283 +}
284 +
285 +/**
286 + * @description 渲染线
287 + * @param { number } startX - 起始坐标
288 + * @param { number } startY - 起始坐标
289 + * @param { number } endX - 终结坐标
290 + * @param { number } endY - 终结坐标
291 + * @param { number } width - 线的宽度
292 + * @param { string } [color] - 线的颜色
293 + *
294 + * @param { object } drawOptions - 绘制对象
295 + * @param { object } drawOptions.ctx - ctx对象
296 + */
297 +export function drawLine(drawData, drawOptions) {
298 + const { startX, startY, endX, endY, color, width } = drawData;
299 + const { ctx } = drawOptions;
300 + if (!width) return;
301 + ctx.save();
302 + ctx.beginPath();
303 + ctx.strokeStyle = color;
304 + ctx.lineWidth = width;
305 + ctx.moveTo(startX, startY);
306 + ctx.lineTo(endX, endY);
307 + ctx.stroke();
308 + ctx.closePath();
309 + ctx.restore();
310 +}
311 +
312 +/**
313 + * 渲染矩形
314 + * @param { number } x - x坐标
315 + * @param { number } y - y坐标
316 + * @param { number } height -高
317 + * @param { string|object } [text] - 块里面可以填充文字,参考texts字段
318 + * @param { number } [width=0] - 宽 如果内部有文字,由文字宽度和内边距决定
319 + * @param { number } [paddingLeft=0] - 内左边距
320 + * @param { number } [paddingRight=0] - 内右边距
321 + * @param { number } [borderWidth] - 边框宽度
322 + * @param { string } [backgroundColor] - 背景颜色
323 + * @param { string } [borderColor] - 边框颜色
324 + * @param { number } [borderRadius=0] - 圆角
325 + * @param { array | null } [borderRadiusGroup= null] - 圆角数组
326 + * @param { number } [opacity=1] - 透明度
327 + *
328 + * @param { object } drawOptions - 绘制对象
329 + * @param { object } drawOptions.ctx - ctx对象
330 + */
331 +export function drawBlock(data, drawOptions) {
332 + const {
333 + x,
334 + y,
335 + text,
336 + width = 0,
337 + height,
338 + opacity = 1,
339 + paddingLeft = 0,
340 + paddingRight = 0,
341 + borderWidth,
342 + backgroundColor,
343 + borderColor,
344 + borderRadius = 0,
345 + borderRadiusGroup = null
346 + } = data || {};
347 + const { ctx } = drawOptions;
348 + ctx.save(); // 先保存画笔样式,等下恢复回来
349 + ctx.globalAlpha = opacity;
350 +
351 + let blockWidth = 0; // 块的宽度
352 + let textX = 0;
353 + let textY = 0;
354 +
355 + // 渲染块内文字
356 + if (text) {
357 + // 如果文字宽度超出块宽度,则块的宽度为:文字的宽度 + 内边距
358 + const textWidth = _getTextWidth(
359 + typeof text.text === 'string' ? text : text.text,
360 + drawOptions
361 + );
362 + blockWidth = textWidth > width ? textWidth : width;
363 + blockWidth += paddingLeft + paddingLeft;
364 +
365 + const { textAlign = 'left' } = text;
366 + textY = y; // 文字默认定位在块的左上角
367 + textX = x + paddingLeft;
368 +
369 + // 文字居中
370 + if (textAlign === 'center') {
371 + textX = blockWidth / 2 + x;
372 + } else if (textAlign === 'right') {
373 + textX = x + blockWidth - paddingRight;
374 + }
375 + drawText(Object.assign(text, { x: textX, y: textY }), drawOptions);
376 + } else {
377 + blockWidth = width;
378 + }
379 +
380 + // 画矩形背景
381 + if (backgroundColor) {
382 + const grd = getLinearColor(ctx, backgroundColor, x, y, blockWidth, height);
383 + ctx.fillStyle = grd;
384 +
385 + // 画圆角矩形
386 + if (borderRadius > 0) {
387 + const drawData = {
388 + x,
389 + y,
390 + w: blockWidth,
391 + h: height,
392 + r: borderRadius
393 + };
394 + _drawRadiusRect(drawData, drawOptions);
395 + ctx.fill(); // 填充路径
396 + } else if (borderRadiusGroup) {
397 + const drawData = {
398 + x,
399 + y,
400 + w: blockWidth,
401 + h: height,
402 + g: borderRadiusGroup
403 + };
404 + _drawRadiusGroupRect(drawData, drawOptions);
405 + ctx.fill(); // 填充路径
406 + } else {
407 + ctx.fillRect(x, y, blockWidth, height); // 绘制矩形
408 + }
409 + }
410 +
411 + // 画边框
412 + if (borderWidth && borderRadius > 0) {
413 + ctx.strokeStyle = borderColor;
414 + ctx.lineWidth = borderWidth;
415 + if (borderRadius > 0) {
416 + // 画圆角矩形边框
417 + const drawData = {
418 + x,
419 + y,
420 + w: blockWidth,
421 + h: height,
422 + r: borderRadius
423 + };
424 + _drawRadiusRect(drawData, drawOptions);
425 + ctx.stroke();
426 + } else {
427 + ctx.strokeRect(x, y, blockWidth, height);
428 + }
429 + }
430 + ctx.restore(); // 将 canvas 恢复到最近的保存状态的方法
431 +}
432 +
433 +/**
434 + * @description 渲染图片
435 + * @param { object } data
436 + * @param { number } sx - 源图像的矩形选择框的左上角 x 坐标 裁剪
437 + * @param { number } sy - 源图像的矩形选择框的左上角 y 坐标 裁剪
438 + * @param { number } sw - 源图像的矩形选择框的宽度 裁剪
439 + * @param { number } sh - 源图像的矩形选择框的高度 裁剪
440 + * @param { number } x - 图像的左上角在目标 canvas 上 x 轴的位置 定位
441 + * @param { number } y - 图像的左上角在目标 canvas 上 y 轴的位置 定位
442 + * @param { number } w - 在目标画布上绘制图像的宽度,允许对绘制的图像进行缩放 定位
443 + * @param { number } h - 在目标画布上绘制图像的高度,允许对绘制的图像进行缩放 定位
444 + * @param { number } [borderRadius=0] - 圆角
445 + * @param { array | null } [borderRadiusGroup= null] - 圆角数组
446 + * @param { number } [borderWidth=0] - 边框
447 + *
448 + * @param { object } drawOptions - 绘制对象
449 + * @param { object } drawOptions.ctx - ctx对象
450 + */
451 +export const drawImage = (data, drawOptions) =>
452 + new Promise<void>((resolve) => {
453 + const { canvas, ctx } = drawOptions;
454 + const {
455 + x,
456 + y,
457 + w,
458 + h,
459 + sx,
460 + sy,
461 + sw,
462 + sh,
463 + imgPath,
464 + borderRadius = 0,
465 + borderWidth = 0,
466 + borderColor,
467 + borderRadiusGroup = null
468 + } = data;
469 +
470 + ctx.save();
471 + if (borderRadius > 0) {
472 + _drawRadiusRect(
473 + {
474 + x,
475 + y,
476 + w,
477 + h,
478 + r: borderRadius
479 + },
480 + drawOptions
481 + );
482 + ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角
483 + ctx.fill();
484 + const img = canvas.createImage(); // 创建图片对象
485 + img.src = imgPath;
486 + img.onload = () => {
487 + ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
488 + if (borderWidth > 0) {
489 + ctx.strokeStyle = borderColor;
490 + ctx.lineWidth = borderWidth;
491 + ctx.stroke();
492 + }
493 + resolve();
494 + ctx.restore();
495 + };
496 + } else if (borderRadiusGroup) {
497 + _drawRadiusGroupRect(
498 + {
499 + x,
500 + y,
501 + w,
502 + h,
503 + g: borderRadiusGroup
504 + },
505 + drawOptions
506 + );
507 + ctx.clip(); // 裁切,后续绘图限制在这个裁切范围内,保证图片圆角
508 + ctx.fill();
509 + const img = canvas.createImage(); // 创建图片对象
510 + img.src = imgPath;
511 + img.onload = () => {
512 + ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
513 + resolve();
514 + ctx.restore();
515 + };
516 + } else {
517 + const img = canvas.createImage(); // 创建图片对象
518 + img.src = imgPath;
519 + img.onload = () => {
520 + ctx.drawImage(img, toPx(sx), toPx(sy), toPx(sw), toPx(sh), x, y, w, h);
521 + resolve();
522 + ctx.restore();
523 + };
524 + }
525 + });
1 +/* eslint-disable prefer-destructuring */
2 +import Taro, { CanvasContext, CanvasGradient } from '@tarojs/taro';
3 +
4 +declare const wx: any;
5 +
6 +/**
7 + * @description 生成随机字符串
8 + * @param { number } length - 字符串长度
9 + * @returns { string }
10 + */
11 +export function randomString(length) {
12 + let str = Math.random().toString(36).substr(2);
13 + if (str.length >= length) {
14 + return str.substr(0, length);
15 + }
16 + str += randomString(length - str.length);
17 + return str;
18 +}
19 +
20 +/**
21 + * 随机创造一个id
22 + * @param { number } length - 字符串长度
23 + * @returns { string }
24 + */
25 +export function getRandomId(prefix = 'canvas', length = 10) {
26 + return prefix + randomString(length);
27 +}
28 +
29 +/**
30 + * @description 获取最大高度
31 + * @param {} config
32 + * @returns { number }
33 + */
34 +// export function getHeight (config) {
35 +// const getTextHeight = text => {
36 +// const fontHeight = text.lineHeight || text.fontSize
37 +// let height = 0
38 +// if (text.baseLine === 'top') {
39 +// height = fontHeight
40 +// } else if (text.baseLine === 'middle') {
41 +// height = fontHeight / 2
42 +// } else {
43 +// height = 0
44 +// }
45 +// return height
46 +// }
47 +// const heightArr: number[] = [];
48 +// (config.blocks || []).forEach(item => {
49 +// heightArr.push(item.y + item.height)
50 +// });
51 +// (config.texts || []).forEach(item => {
52 +// let height
53 +// if (Object.prototype.toString.call(item.text) === '[object Array]') {
54 +// item.text.forEach(i => {
55 +// height = getTextHeight({ ...i, baseLine: item.baseLine })
56 +// heightArr.push(item.y + height)
57 +// })
58 +// } else {
59 +// height = getTextHeight(item)
60 +// heightArr.push(item.y + height)
61 +// }
62 +// });
63 +// (config.images || []).forEach(item => {
64 +// heightArr.push(item.y + item.height)
65 +// });
66 +// (config.lines || []).forEach(item => {
67 +// heightArr.push(item.startY)
68 +// heightArr.push(item.endY)
69 +// })
70 +// const sortRes = heightArr.sort((a, b) => b - a)
71 +// let canvasHeight = 0
72 +// if (sortRes.length > 0) {
73 +// canvasHeight = sortRes[0]
74 +// }
75 +// if (config.height < canvasHeight || !config.height) {
76 +// return canvasHeight
77 +// }
78 +// return config.height
79 +// }
80 +
81 +/**
82 + * 将http转为https
83 + * @param {String}} rawUrl 图片资源url
84 + * @returns { string }
85 + */
86 +export function mapHttpToHttps(rawUrl) {
87 + if (rawUrl.indexOf(':') < 0 || rawUrl.startsWith('http://tmp')) {
88 + return rawUrl;
89 + }
90 + const urlComponent = rawUrl.split(':');
91 + if (urlComponent.length === 2) {
92 + if (urlComponent[0] === 'http') {
93 + urlComponent[0] = 'https';
94 + return `${urlComponent[0]}:${urlComponent[1]}`;
95 + }
96 + }
97 + return rawUrl;
98 +}
99 +
100 +/**
101 + * 获取 rpx => px 的转换系数
102 + * @returns { number } factor 单位转换系数 1rpx = factor * px
103 + */
104 +export const getFactor = () => {
105 + const sysInfo = Taro.getSystemInfoSync();
106 + const { screenWidth } = sysInfo;
107 + return screenWidth / 750;
108 +};
109 +
110 +/**
111 + * rpx => px 单位转换
112 + * @param { number } rpx - 需要转换的数值
113 + * @param { number } factor - 转化因子
114 + * @returns { number }
115 + */
116 +export const toPx = (rpx, factor = getFactor()) =>
117 + parseInt(String(rpx * factor), 10);
118 +
119 +/**
120 + * px => rpx 单位转换
121 + * @param { number } px - 需要转换的数值
122 + * @param { number } factor - 转化因子
123 + * @returns { number }
124 + */
125 +export const toRpx = (px, factor = getFactor()) =>
126 + parseInt(String(px / factor), 10);
127 +
128 +/**
129 + * 下载图片资源
130 + * @param { string } url
131 + * @returns { Promise }
132 + */
133 +export function downImage(url) {
134 + return new Promise<string>((resolve, reject) => {
135 + // eslint-disable-next-line no-undef
136 + if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) {
137 + // wx.env.USER_DATA_PATH 文件系统中的用户目录路径
138 + Taro.downloadFile({
139 + url: mapHttpToHttps(url),
140 + success: (res) => {
141 + if (res.statusCode === 200) {
142 + resolve(res.tempFilePath);
143 + } else {
144 + console.log('下载失败', res);
145 + reject(res);
146 + }
147 + },
148 + fail(err) {
149 + console.log('下载失败了', err);
150 + reject(err);
151 + }
152 + });
153 + } else {
154 + resolve(url); // 支持本地地址
155 + }
156 + });
157 +}
158 +
159 +/**
160 + * 下载图片并获取图片信息
161 + * @param {} item 图片参数信息
162 + * @param {} index 图片下标
163 + * @returns { Promise } result 整理后的图片信息
164 + */
165 +export const getImageInfo = (item, index) =>
166 + new Promise((resolve, reject) => {
167 + const { x, y, width, height, url, zIndex } = item;
168 + downImage(url).then((imgPath) =>
169 + Taro.getImageInfo({ src: imgPath })
170 + .then((imgInfo) => {
171 + // 获取图片信息
172 + // 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形, 即宽高比不变,截取再拉伸
173 + let sx; // 截图的起点 x 坐标
174 + let sy; // 截图的起点 y 坐标
175 + const borderRadius = item.borderRadius || 0;
176 + const imgWidth = toRpx(imgInfo.width); // 图片真实宽度 单位 px
177 + const imgHeight = toRpx(imgInfo.height); // 图片真实高度 单位 px
178 + // 根据宽高比截取图片
179 + if (imgWidth / imgHeight <= width / height) {
180 + sx = 0;
181 + sy = (imgHeight - (imgWidth / width) * height) / 2;
182 + } else {
183 + sy = 0;
184 + sx = (imgWidth - (imgHeight / height) * width) / 2;
185 + }
186 + // 给 canvas 画图准备参数,详见 ./draw.ts-drawImage
187 + const result = {
188 + type: 'image',
189 + borderRadius,
190 + borderWidth: item.borderWidth,
191 + borderColor: item.borderColor,
192 + borderRadiusGroup: item.borderRadiusGroup,
193 + zIndex: typeof zIndex !== 'undefined' ? zIndex : index,
194 + imgPath: url,
195 + sx,
196 + sy,
197 + sw: imgWidth - sx * 2,
198 + sh: imgHeight - sy * 2,
199 + x,
200 + y,
201 + w: width,
202 + h: height
203 + };
204 + resolve(result);
205 + })
206 + .catch((err) => {
207 + console.log('读取图片信息失败', err);
208 + reject(err);
209 + })
210 + );
211 + });
212 +
213 +/**
214 + * 获取线性渐变色
215 + * @param {CanvasContext} ctx canvas 实例对象
216 + * @param {String} color 线性渐变色,如 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #fff 100%)'
217 + * @param {Number} startX 起点 x 坐标
218 + * @param {Number} startY 起点 y 坐标
219 + * @param {Number} w 宽度
220 + * @param {Number} h 高度
221 + * @returns {}
222 + */
223 +// TODO: 待优化, 支持所有角度,多个颜色的线性渐变
224 +export function getLinearColor(
225 + ctx: CanvasContext,
226 + color,
227 + startX,
228 + startY,
229 + w,
230 + h
231 +) {
232 + if (
233 + typeof startX !== 'number' ||
234 + typeof startY !== 'number' ||
235 + typeof w !== 'number' ||
236 + typeof h !== 'number'
237 + ) {
238 + console.warn('坐标或者宽高只支持数字');
239 + return color;
240 + }
241 + let grd: CanvasGradient | string = color;
242 + if (color.includes('linear-gradient')) {
243 + // fillStyle 不支持线性渐变色
244 + const colorList = color.match(/\((\d+)deg,\s(.+)\s\d+%,\s(.+)\s\d+%/);
245 + const radian = colorList[1]; // 渐变弧度(角度)
246 + const color1 = colorList[2];
247 + const color2 = colorList[3];
248 +
249 + const L = Math.sqrt(w * w + h * h);
250 + const x = Math.ceil(Math.sin(180 - radian) * L);
251 + const y = Math.ceil(Math.cos(180 - radian) * L);
252 +
253 + // 根据弧度和宽高确定渐变色的两个点的坐标
254 + if (Number(radian) === 180 || Number(radian) === 0) {
255 + if (Number(radian) === 180) {
256 + grd = ctx.createLinearGradient(startX, startY, startX, startY + h);
257 + }
258 + if (Number(radian) === 0) {
259 + grd = ctx.createLinearGradient(startX, startY + h, startX, startY);
260 + }
261 + } else if (radian > 0 && radian < 180) {
262 + grd = ctx.createLinearGradient(startX, startY, x + startX, y + startY);
263 + } else {
264 + throw new Error('只支持0 <= 颜色弧度 <= 180');
265 + }
266 + (grd as CanvasGradient).addColorStop(0, color1);
267 + (grd as CanvasGradient).addColorStop(1, color2);
268 + }
269 + return grd;
270 +}
271 +
272 +/**
273 + * 根据文字对齐方式设置坐标
274 + * @param {*} imgPath
275 + * @param {*} index
276 + * @returns { Promise }
277 + */
278 +export function getTextX(textAlign, x, width) {
279 + let newX = x;
280 + if (textAlign === 'center') {
281 + newX = width / 2 + x;
282 + } else if (textAlign === 'right') {
283 + newX = width + x;
284 + }
285 + return newX;
286 +}
...@@ -57,3 +57,16 @@ ...@@ -57,3 +57,16 @@
57 } 57 }
58 } 58 }
59 } 59 }
60 +
61 +.wrapper {
62 + display: flex;
63 + align-items: center;
64 + justify-content: center;
65 + height: 100%;
66 +}
67 +
68 +.block {
69 + width: 120px;
70 + height: 120px;
71 + background-color: #fff;
72 +}
......
1 <!-- 1 <!--
2 * @Date: 2022-09-26 14:36:57 2 * @Date: 2022-09-26 14:36:57
3 * @LastEditors: hookehuyr hookehuyr@gmail.com 3 * @LastEditors: hookehuyr hookehuyr@gmail.com
4 - * @LastEditTime: 2022-09-26 17:37:54 4 + * @LastEditTime: 2022-09-27 16:38:57
5 * @FilePath: /swx/src/pages/activityDetail/index.vue 5 * @FilePath: /swx/src/pages/activityDetail/index.vue
6 * @Description: 文件描述 6 * @Description: 文件描述
7 --> 7 -->
...@@ -122,18 +122,41 @@ ...@@ -122,18 +122,41 @@
122 <activity-bar /> 122 <activity-bar />
123 123
124 <van-action-sheet 124 <van-action-sheet
125 + :z-index="10"
125 :show="show_share" 126 :show="show_share"
126 :actions="actions_share" 127 :actions="actions_share"
127 cancel-text="取消" 128 cancel-text="取消"
129 + :overlay="true"
128 @cancel="onCancelShare" 130 @cancel="onCancelShare"
129 @select="onSelectShare" 131 @select="onSelectShare"
130 /> 132 />
133 +
134 + <van-overlay :show="show_post" :z-index="1">
135 + <view class="wrapper">
136 + <view class="preview-area" @click="onClickPost">
137 + <image v-if="posterPath" :src="posterPath" mode="widthFix" />
138 + </view>
139 + </view>
140 + </van-overlay>
141 + <PosterBuilder v-if="startDraw" custom-style="position: fixed; left: 200%;" :config="base" @success="drawSuccess"
142 + @fail="drawFail" />
143 + <van-action-sheet
144 + :z-index="1"
145 + :show="show_save"
146 + :actions="actions_save"
147 + cancel-text="取消"
148 + :overlay="false"
149 + @cancel="onCancelSave"
150 + @select="onSelectSave"
151 + />
131 </template> 152 </template>
132 153
133 <script setup> 154 <script setup>
134 import img_demo from '@/images/demo@2x.png' 155 import img_demo from '@/images/demo@2x.png'
135 import img_demo1 from '@/images/demo@2x-1.png' 156 import img_demo1 from '@/images/demo@2x-1.png'
136 import activityBar from '@/components/activity-bar.vue' 157 import activityBar from '@/components/activity-bar.vue'
158 +import Taro from '@tarojs/taro'
159 +import PosterBuilder from '@/components/PosterBuilder/index.vue';
137 160
138 import { ref } from "vue"; 161 import { ref } from "vue";
139 162
...@@ -149,10 +172,293 @@ const shareActivity = () => { ...@@ -149,10 +172,293 @@ const shareActivity = () => {
149 const onCancelShare = () => { 172 const onCancelShare = () => {
150 show_share.value = false; 173 show_share.value = false;
151 } 174 }
152 -const onSelectShare = (event) => { 175 +const onSelectShare = ({ detail }) => {
153 // TODO: 需要完善 分享朋友和生成海报功能 176 // TODO: 需要完善 分享朋友和生成海报功能
154 - console.warn(event.detail);
155 show_share.value = false; 177 show_share.value = false;
178 + if (detail.name === '生成海报') {
179 + show_post.value = true;
180 + start()
181 + }
182 +}
183 +
184 +const show_post = ref(false)
185 +const onClickPost = () => {
186 + show_save.value = true;
187 +}
188 +
189 +// 生成海报
190 +const startDraw = ref(false)
191 +const posterPath = ref('')
192 +const base = {
193 + width: 1024,
194 + height: 1334,
195 + backgroundColor: '',
196 + debug: false,
197 + blocks: [
198 + {
199 + x: 40,
200 + y: 20,
201 + width: 950,
202 + height: 1200,
203 + paddingLeft: 0,
204 + paddingRight: 0,
205 + borderWidth: 1,
206 + borderColor: '#D9DCD5',
207 + backgroundColor: '#fff',
208 + borderRadius: 16,
209 + // borderRadiusGroup: [0, 0, 18, 18],
210 + },
211 + {
212 + x: 40,
213 + y: 730,
214 + // width: 580,
215 + height: 75,
216 + paddingLeft: 80,
217 + paddingRight: 0,
218 + borderWidth: 0,
219 + text: {
220 + x: 0,
221 + y: 0,
222 + text: '2022-08-25 14:00',
223 + fontSize: 40,
224 + color: '#222',
225 + opacity: 1,
226 + baseLine: 'top',
227 + lineHeight: 48,
228 + lineNum: 2,
229 + textAlign: 'left',
230 + // width: 580,
231 + zIndex: 0,
232 + },
233 + backgroundColor: '#FFF9F3',
234 + // borderColor: 'red',
235 + // backgroundColor: '#EFF3F5',
236 + // borderRadius: 0,
237 + borderRadiusGroup: [0, 25, 25, 0],
238 + // borderRadius: 16,
239 + // zIndex: 100,
240 + },
241 + {
242 + x: 40,
243 + y: 830,
244 + // width: 580,
245 + height: 75,
246 + paddingLeft: 80,
247 + paddingRight: 0,
248 + borderWidth: 0,
249 + text: {
250 + x: 0,
251 + y: 0,
252 + text: '上海市杨浦区军工路100号A座05室',
253 + fontSize: 40,
254 + color: '#222',
255 + opacity: 1,
256 + baseLine: 'top',
257 + lineHeight: 48,
258 + lineNum: 2,
259 + textAlign: 'left',
260 + // width: 580,
261 + zIndex: 0,
262 + },
263 + backgroundColor: '#FFF9F3',
264 + // borderColor: 'red',
265 + // backgroundColor: '#EFF3F5',
266 + // borderRadius: 0,
267 + borderRadiusGroup: [0, 25, 25, 0],
268 + // borderRadius: 16,
269 + // zIndex: 100,
270 + },
271 + ],
272 + texts: [
273 + {
274 + x: 80,
275 + y: 630,
276 + text: '八段锦-智慧没有烦恼',
277 + fontSize: 50,
278 + color: '#000',
279 + opacity: 1,
280 + baseLine: 'middle',
281 + lineHeight: 60,
282 + lineNum: 2,
283 + textAlign: 'left',
284 + width: 800,
285 + zIndex: 999,
286 + // fontWeight: 'bold',
287 + fontFamily: 'Monospace',
288 + },
289 + {
290 + x: 135,
291 + y: 770,
292 + text: '2022-08-25 14:00',
293 + fontSize: 40,
294 + color: '#222',
295 + opacity: 1,
296 + baseLine: 'middle',
297 + lineHeight: 48,
298 + lineNum: 2,
299 + textAlign: 'left',
300 + // width: 580,
301 + zIndex: 999,
302 + },
303 + {
304 + x: 135,
305 + y: 870,
306 + text: '上海市杨浦区军工路100号A座05室',
307 + fontSize: 40,
308 + color: '#222',
309 + opacity: 1,
310 + baseLine: 'middle',
311 + lineHeight: 48,
312 + lineNum: 2,
313 + textAlign: 'left',
314 + // width: 580,
315 + zIndex: 999,
316 + },
317 + {
318 + x: 300,
319 + y: 1080,
320 + text: '妙净',
321 + fontSize: 50,
322 + color: '#333',
323 + opacity: 1,
324 + baseLine: 'middle',
325 + textAlign: 'left',
326 + lineHeight: 50,
327 + lineNum: 1,
328 + zIndex: 999,
329 + },
330 + {
331 + x: 300,
332 + y: 1150,
333 + text: '邀请你一起来活动!',
334 + fontSize: 42,
335 + color: '#8F9399',
336 + opacity: 1,
337 + baseLine: 'middle',
338 + textAlign: 'left',
339 + lineHeight: 42,
340 + lineNum: 1,
341 + zIndex: 999,
342 + }
343 + ],
344 + images: [
345 + {
346 + url: 'https://tva1.sinaimg.cn/large/5f01a858gy1h6l450x64zj20ku0bq78c.jpg',
347 + width: 950,
348 + height: 500,
349 + x: 40,
350 + y: 20,
351 + // borderRadius: 16,
352 + borderRadiusGroup: [18, 18, 0, 0],
353 + zIndex: 10,
354 + // borderRadius: 150,
355 + // borderWidth: 10,
356 + // borderColor: 'red',
357 + },
358 + {
359 + url: 'https://tva1.sinaimg.cn/large/5f01a858gy1h6l5d2bmijj200s00s0sh.jpg',
360 + width: 40,
361 + height: 40,
362 + x: 80,
363 + y: 750,
364 + borderRadius: 100,
365 + borderWidth: 0,
366 + zIndex: 10,
367 + },
368 + {
369 + url: 'https://tva1.sinaimg.cn/large/5f01a858gy1h6l5dc9q31j200o00u0ol.jpg',
370 + width: 35,
371 + height: 40,
372 + x: 80,
373 + y: 850,
374 + borderRadius: 100,
375 + borderWidth: 0,
376 + zIndex: 10,
377 + },
378 + {
379 + url: 'https://pic.juncao.cc/cms/images/minapp.jpg',
380 + width: 170,
381 + height: 170,
382 + x: 80,
383 + y: 1030,
384 + borderRadius: 100,
385 + borderWidth: 0,
386 + zIndex: 10,
387 + },
388 + {
389 + url: 'https://pic.juncao.cc/cms/images/minapp.jpg',
390 + width: 170,
391 + height: 170,
392 + x: 750,
393 + y: 1030,
394 + borderRadius: 100,
395 + borderWidth: 0,
396 + zIndex: 10,
397 + },
398 + ],
399 + lines: [
400 + {
401 + startY: 970,
402 + startX: 80,
403 + endX: 950,
404 + endY: 971,
405 + width: 1,
406 + color: '#8F9399',
407 + }
408 + ]
409 +}
410 +const start = () => {
411 + startDraw.value = true;
412 + if (!posterPath.value) Taro.showLoading();
413 +}
414 +const drawSuccess = (result) => {
415 + console.warn('绘制好了', result);
416 + const { tempFilePath, errMsg } = result;
417 + if (errMsg === 'canvasToTempFilePath:ok') {
418 + posterPath.value = tempFilePath;
419 + Taro.hideLoading();
420 + } else {
421 + Taro.hideLoading();
422 + Taro.showToast({
423 + title: '失败,请稍后重试',
424 + icon: 'none',
425 + duration: 2500
426 + });
427 + }
428 +};
429 +const drawFail = (result) => {
430 + console.warn('绘制失败', result);
431 + Taro.hideLoading();
432 +}
433 +const savePoster = () => {
434 + Taro.saveImageToPhotosAlbum({
435 + filePath: posterPath.value,
436 + success() {
437 + Taro.showToast({
438 + title: '已保存到相册',
439 + icon: 'success',
440 + duration: 2000
441 + });
442 + // posterPath.value = '';
443 + }
444 + });
445 +}
446 +
447 +const show_save = ref(false);
448 +const actions_save = ref([{
449 + name: '保存至相册'
450 +}]);
451 +const onCancelSave = () => {
452 + show_save.value = false;
453 + show_post.value = false;
454 + // posterPath.value = '';
455 +}
456 +const onSelectSave = ({ detail }) => {
457 + if (detail.name === '保存至相册') {
458 + show_save.value = false;
459 + show_post.value = false;
460 + savePoster()
461 + }
156 } 462 }
157 </script> 463 </script>
158 464
......
1 +export default {
2 + navigationBarTitleText: 'demo',
3 + usingComponents: {
4 + },
5 +}
1 +.red {
2 + color: red;
3 +}
1 +<template>
2 + <view class="index">
3 + <view class="action-bar">
4 + <button @tap="start">生成海报</button>
5 + <button @tap="savePoster">下载海报</button>
6 + </view>
7 +
8 + <view class="preview-area">
9 + <image v-if="posterPath" :src="posterPath" mode="widthFix" />
10 + <view v-else class="text">预览区域</view>
11 + </view>
12 + <PosterBuilder v-if="startDraw" custom-style="position: fixed; left: 200%;" :config="base" @success="drawSuccess"
13 + @fail="drawFail" />
14 + </view>
15 +</template>
16 +
17 +<script>
18 +import Taro from '@tarojs/taro'
19 +import { defineComponent, reactive, toRefs } from 'vue';
20 +import PosterBuilder from '@/components/PosterBuilder/index.vue';
21 +
22 +export default defineComponent({
23 + name: 'Index',
24 + components: {
25 + PosterBuilder,
26 + },
27 + setup() {
28 + const state = reactive({
29 + startDraw: false,
30 + posterPath: ''
31 + })
32 + const base = {
33 + width: 750,
34 + height: 1334,
35 + backgroundColor: '#232422',
36 + debug: false,
37 + blocks: [
38 + // 头部底色
39 + {
40 + x: 32,
41 + y: 80,
42 + width: 686,
43 + height: 160,
44 + paddingLeft: 0,
45 + paddingRight: 0,
46 + backgroundColor: '#FFFFFF',
47 + borderRadius: 32,
48 + zIndex: 10
49 + },
50 + //底部背景
51 + {
52 + x: 32,
53 + y: 950,
54 + width: 686,
55 + height: 302,
56 + paddingLeft: 0,
57 + paddingRight: 0,
58 + borderRadiusGroup: [0, 0, 16, 16],
59 + backgroundColor: '#FFFFFF',
60 + zIndex: 11
61 + }
62 + ],
63 + texts: [
64 + {
65 + x: 216,
66 + y: 108,
67 + text: 'BiBin',
68 + width: 380,
69 + lineNum: 2, // 最多几行
70 + fontSize: 36,
71 + fontWeight: 'bold',
72 + color: '#1A171B',
73 + zIndex: 11
74 + },
75 + {
76 + x: 216,
77 + y: 174,
78 + text: '为你挑选了一个好物',
79 + width: 380,
80 + fontSize: 28,
81 + color: '#7C7D7A',
82 + zIndex: 11
83 + },
84 + {
85 + x: 64,
86 + y: 994,
87 + text: `¥6799`,
88 + fontSize: 48,
89 + color: '#ED2D2B',
90 + fontWeight: 'bold',
91 + zIndex: 12
92 + },
93 + {
94 + x: 64,
95 + y: 1092,
96 + text: 'Apple iPhone 13 (A2634) 256GB 蓝色 支持移动联通电信5G 双卡双待手机',
97 + fontSize: 32,
98 + width: 380,
99 + color: '#282925',
100 + lineNum: 2, // 最多几行
101 + zIndex: 12
102 + }
103 + ],
104 + images: [
105 + {
106 + x: 64,
107 + y: 100,
108 + width: 120,
109 + height: 120,
110 + borderRadius: 60,
111 + url: 'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJP05RJ5icJkUkBjVtSb3DHib4pGRVEqjw3qNic53kd1tJmibPpzR1etJnWJFiaIJDLK6TDzD3d5SyPsXQ/132',
112 + zIndex: 11
113 + },
114 + {
115 + x: 32,
116 + y: 272,
117 + width: 686,
118 + height: 686,
119 + url: 'https://m.360buyimg.com/mobilecms/s750x750_jfs/t1/119360/32/20820/239818/618be3c6E1dbf188d/4070880e024273bb.jpg!q80.dpg',
120 + borderRadiusGroup: [16, 16, 0, 0],
121 + zIndex: 11
122 + }
123 + ]
124 + };
125 +
126 + const start = () => {
127 + state.startDraw = true;
128 + Taro.showLoading();
129 + }
130 + const drawSuccess = (result) => {
131 + console.warn('绘制好了', result);
132 + const { tempFilePath, errMsg } = result;
133 + if (errMsg === 'canvasToTempFilePath:ok') {
134 + state.posterPath = tempFilePath;
135 + Taro.hideLoading();
136 + } else {
137 + Taro.hideLoading();
138 + Taro.showToast({
139 + title: '失败,请稍后重试',
140 + icon: 'none',
141 + duration: 2500
142 + });
143 + }
144 + };
145 + const drawFail = (result) => {
146 + console.warn('绘制失败', result);
147 + Taro.hideLoading();
148 + }
149 + const savePoster = () => {
150 + Taro.saveImageToPhotosAlbum({
151 + filePath: state.posterPath,
152 + success() {
153 + Taro.showToast({
154 + title: '已保存到相册',
155 + icon: 'success',
156 + duration: 2000
157 + });
158 + }
159 + });
160 + }
161 + return {
162 + ...toRefs(state),
163 + base,
164 + start,
165 + drawSuccess,
166 + drawFail,
167 + savePoster
168 + }
169 + }
170 +});
171 +</script>
172 +
173 +<style>
174 +.index {
175 + font-family: 'Avenir', Helvetica, Arial, sans-serif;
176 + -webkit-font-smoothing: antialiased;
177 + -moz-osx-font-smoothing: grayscale;
178 + text-align: center;
179 + color: #2c3e50;
180 + margin-top: 60px;
181 +
182 +}
183 +
184 +.action-bar {
185 + display: flex;
186 +}
187 +
188 +.preview-area {
189 + width: 80%;
190 + min-height: 800px;
191 + margin: 20px auto;
192 + text-align: center;
193 + border: 1px solid #cccccc;
194 +}
195 +
196 +.text {
197 + line-height: 800px;
198 +}
199 +</style>