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 +};
This diff is collapsed. Click to expand it.
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>