hookehuyr

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

......@@ -2,7 +2,7 @@
* @Author: hookehuyr hookehuyr@gmail.com
* @Date: 2022-05-27 15:57:59
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-09-26 14:37:22
* @LastEditTime: 2022-09-27 09:41:31
* @FilePath: /swx/src/app.config.js
* @Description:
*/
......@@ -24,6 +24,7 @@ export default {
'pages/my/index',
'pages/createActivity/index',
'pages/activityDetail/index',
'pages/post/index',
],
subpackages: [ // 配置在tabBar中的页面不能分包写到subpackages中去
{
......
<template>
<canvas
type="2d"
:id="canvasId"
:style="`height: ${height}rpx; width:${width}rpx;
position: absolute;
${debug ? '' : 'transform:translate3d(-9999rpx, 0, 0)'}`"
/>
</template>
<script lang="ts">
import Taro from "@tarojs/taro"
import { defineComponent, onMounted, PropType, ref } from "vue"
import { Image, DrawConfig } from "./types"
import { drawImage, drawText, drawBlock, drawLine } from "./utils/draw"
import {
toPx,
toRpx,
getRandomId,
getImageInfo,
getLinearColor,
} from "./utils/tools"
export default defineComponent({
name: "PosterBuilder",
props: {
showLoading: {
type: Boolean,
default: false,
},
config: {
type: Object as PropType<DrawConfig>,
default: () => ({}),
},
},
emits: ["success", "fail"],
setup(props, context) {
const count = ref(1)
const {
width,
height,
backgroundColor,
texts = [],
blocks = [],
lines = [],
debug = false,
} = props.config || {}
const canvasId = getRandomId()
/**
* step1: 初始化图片资源
* @param {Array} images = imgTask
* @return {Promise} downloadImagePromise
*/
const initImages = (images: Image[]) => {
const imagesTemp = images.filter((item) => item.url)
const drawList = imagesTemp.map((item, index) =>
getImageInfo(item, index)
)
return Promise.all(drawList)
}
/**
* step2: 初始化 canvas && 获取其 dom 节点和实例
* @return {Promise} resolve 里返回其 dom 和实例
*/
const initCanvas = () =>
new Promise<any>((resolve) => {
setTimeout(() => {
const pageInstance = Taro.getCurrentInstance()?.page || {} // 拿到当前页面实例
const query = Taro.createSelectorQuery().in(pageInstance) // 确定在当前页面内匹配子元素
query
.select(`#${canvasId}`)
.fields({ node: true, size: true, context: true }, (res) => {
const canvas = res.node
const ctx = canvas.getContext("2d")
resolve({ ctx, canvas })
})
.exec()
}, 300)
})
/**
* @description 保存绘制的图片
* @param { object } config
*/
const getTempFile = (canvas) => {
Taro.canvasToTempFilePath(
{
canvas,
success: (result) => {
Taro.hideLoading()
context.emit("success", result)
},
fail: (error) => {
const { errMsg } = error
if (errMsg === "canvasToTempFilePath:fail:create bitmap failed") {
count.value += 1
if (count.value <= 3) {
getTempFile(canvas)
} else {
Taro.hideLoading()
Taro.showToast({
icon: "none",
title: errMsg || "绘制海报失败",
})
context.emit("fail", errMsg)
}
}
},
},
context
)
}
/**
* step2: 开始绘制任务
* @param { Array } drawTasks 待绘制任务
*/
const startDrawing = async (drawTasks) => {
// TODO: check
// const configHeight = getHeight(config)
const { ctx, canvas } = await initCanvas()
canvas.width = width
canvas.height = height
// 设置画布底色
if (backgroundColor) {
ctx.save() // 保存绘图上下文
const grd = getLinearColor(ctx, backgroundColor, 0, 0, width, height)
ctx.fillStyle = grd // 设置填充颜色
ctx.fillRect(0, 0, width, height) // 填充一个矩形
ctx.restore() // 恢复之前保存的绘图上下文
}
// 将要画的方块、文字、线条放进队列数组
const queue = drawTasks
.concat(
texts.map((item) => {
item.type = "text"
item.zIndex = item.zIndex || 0
return item
})
)
.concat(
blocks.map((item) => {
item.type = "block"
item.zIndex = item.zIndex || 0
return item
})
)
.concat(
lines.map((item) => {
item.type = "line"
item.zIndex = item.zIndex || 0
return item
})
)
queue.sort((a, b) => a.zIndex - b.zIndex) // 按照层叠顺序由低至高排序, 先画低的,再画高的
for (let i = 0; i < queue.length; i++) {
const drawOptions = {
canvas,
ctx,
toPx,
toRpx,
}
if (queue[i].type === "image") {
await drawImage(queue[i], drawOptions)
} else if (queue[i].type === "text") {
drawText(queue[i], drawOptions)
} else if (queue[i].type === "block") {
drawBlock(queue[i], drawOptions)
} else if (queue[i].type === "line") {
drawLine(queue[i], drawOptions)
}
}
setTimeout(() => {
getTempFile(canvas) // 需要做延时才能能正常加载图片
}, 300)
}
// start: 初始化 canvas 实例 && 下载图片资源
const init = () => {
if (props.showLoading)
Taro.showLoading({ mask: true, title: "生成中..." })
if (props.config?.images?.length) {
initImages(props.config.images)
.then((result) => {
// 1. 下载图片资源
startDrawing(result)
})
.catch((err) => {
Taro.hideLoading()
Taro.showToast({
icon: "none",
title: err.errMsg || "下载图片失败",
})
context.emit("fail", err)
})
} else {
startDrawing([])
}
}
onMounted(() => {
init()
})
return {
canvasId,
debug,
width,
height,
}
},
})
</script>
export type DrawType = 'text' | 'image' | 'block' | 'line';
export interface Block {
type?: DrawType;
x: number;
y: number;
width?: number;
height: number;
paddingLeft?: number;
paddingRight?: number;
borderWidth?: number;
borderColor?: string;
backgroundColor?: string;
borderRadius?: number;
borderRadiusGroup?: number[];
text?: Text;
opacity?: number;
zIndex?: number;
}
export interface Text {
type?: DrawType;
x?: number;
y?: number;
text: string | Text[];
fontSize?: number;
color?: string;
opacity?: 1 | 0;
lineHeight?: number;
lineNum?: number;
width?: number;
marginTop?: number;
marginLeft?: number;
marginRight?: number;
textDecoration?: 'line-through' | 'none';
baseLine?: 'top' | 'middle' | 'bottom';
textAlign?: 'left' | 'center' | 'right';
fontFamily?: string;
fontWeight?: string;
fontStyle?: string;
zIndex?: number;
}
export interface Image {
type?: DrawType;
x: number;
y: number;
url: string;
width: number;
height: number;
borderRadius?: number;
borderRadiusGroup?: number[];
borderWidth?: number;
borderColor?: string;
zIndex?: number;
}
export interface Line {
type?: DrawType;
startX: number;
startY: number;
endX: number;
endY: number;
width: number;
color?: string;
zIndex?: number;
}
export type DrawConfig = {
width: number;
height: number;
backgroundColor?: string;
debug?: boolean;
blocks?: Block[];
texts?: Text[];
images?: Image[];
lines?: Line[];
};
This diff is collapsed. Click to expand it.
/* eslint-disable prefer-destructuring */
import Taro, { CanvasContext, CanvasGradient } from '@tarojs/taro';
declare const wx: any;
/**
* @description 生成随机字符串
* @param { number } length - 字符串长度
* @returns { string }
*/
export function randomString(length) {
let str = Math.random().toString(36).substr(2);
if (str.length >= length) {
return str.substr(0, length);
}
str += randomString(length - str.length);
return str;
}
/**
* 随机创造一个id
* @param { number } length - 字符串长度
* @returns { string }
*/
export function getRandomId(prefix = 'canvas', length = 10) {
return prefix + randomString(length);
}
/**
* @description 获取最大高度
* @param {} config
* @returns { number }
*/
// export function getHeight (config) {
// const getTextHeight = text => {
// const fontHeight = text.lineHeight || text.fontSize
// let height = 0
// if (text.baseLine === 'top') {
// height = fontHeight
// } else if (text.baseLine === 'middle') {
// height = fontHeight / 2
// } else {
// height = 0
// }
// return height
// }
// const heightArr: number[] = [];
// (config.blocks || []).forEach(item => {
// heightArr.push(item.y + item.height)
// });
// (config.texts || []).forEach(item => {
// let height
// if (Object.prototype.toString.call(item.text) === '[object Array]') {
// item.text.forEach(i => {
// height = getTextHeight({ ...i, baseLine: item.baseLine })
// heightArr.push(item.y + height)
// })
// } else {
// height = getTextHeight(item)
// heightArr.push(item.y + height)
// }
// });
// (config.images || []).forEach(item => {
// heightArr.push(item.y + item.height)
// });
// (config.lines || []).forEach(item => {
// heightArr.push(item.startY)
// heightArr.push(item.endY)
// })
// const sortRes = heightArr.sort((a, b) => b - a)
// let canvasHeight = 0
// if (sortRes.length > 0) {
// canvasHeight = sortRes[0]
// }
// if (config.height < canvasHeight || !config.height) {
// return canvasHeight
// }
// return config.height
// }
/**
* 将http转为https
* @param {String}} rawUrl 图片资源url
* @returns { string }
*/
export function mapHttpToHttps(rawUrl) {
if (rawUrl.indexOf(':') < 0 || rawUrl.startsWith('http://tmp')) {
return rawUrl;
}
const urlComponent = rawUrl.split(':');
if (urlComponent.length === 2) {
if (urlComponent[0] === 'http') {
urlComponent[0] = 'https';
return `${urlComponent[0]}:${urlComponent[1]}`;
}
}
return rawUrl;
}
/**
* 获取 rpx => px 的转换系数
* @returns { number } factor 单位转换系数 1rpx = factor * px
*/
export const getFactor = () => {
const sysInfo = Taro.getSystemInfoSync();
const { screenWidth } = sysInfo;
return screenWidth / 750;
};
/**
* rpx => px 单位转换
* @param { number } rpx - 需要转换的数值
* @param { number } factor - 转化因子
* @returns { number }
*/
export const toPx = (rpx, factor = getFactor()) =>
parseInt(String(rpx * factor), 10);
/**
* px => rpx 单位转换
* @param { number } px - 需要转换的数值
* @param { number } factor - 转化因子
* @returns { number }
*/
export const toRpx = (px, factor = getFactor()) =>
parseInt(String(px / factor), 10);
/**
* 下载图片资源
* @param { string } url
* @returns { Promise }
*/
export function downImage(url) {
return new Promise<string>((resolve, reject) => {
// eslint-disable-next-line no-undef
if (/^http/.test(url) && !new RegExp(wx.env.USER_DATA_PATH).test(url)) {
// wx.env.USER_DATA_PATH 文件系统中的用户目录路径
Taro.downloadFile({
url: mapHttpToHttps(url),
success: (res) => {
if (res.statusCode === 200) {
resolve(res.tempFilePath);
} else {
console.log('下载失败', res);
reject(res);
}
},
fail(err) {
console.log('下载失败了', err);
reject(err);
}
});
} else {
resolve(url); // 支持本地地址
}
});
}
/**
* 下载图片并获取图片信息
* @param {} item 图片参数信息
* @param {} index 图片下标
* @returns { Promise } result 整理后的图片信息
*/
export const getImageInfo = (item, index) =>
new Promise((resolve, reject) => {
const { x, y, width, height, url, zIndex } = item;
downImage(url).then((imgPath) =>
Taro.getImageInfo({ src: imgPath })
.then((imgInfo) => {
// 获取图片信息
// 根据画布的宽高计算出图片绘制的大小,这里会保证图片绘制不变形, 即宽高比不变,截取再拉伸
let sx; // 截图的起点 x 坐标
let sy; // 截图的起点 y 坐标
const borderRadius = item.borderRadius || 0;
const imgWidth = toRpx(imgInfo.width); // 图片真实宽度 单位 px
const imgHeight = toRpx(imgInfo.height); // 图片真实高度 单位 px
// 根据宽高比截取图片
if (imgWidth / imgHeight <= width / height) {
sx = 0;
sy = (imgHeight - (imgWidth / width) * height) / 2;
} else {
sy = 0;
sx = (imgWidth - (imgHeight / height) * width) / 2;
}
// 给 canvas 画图准备参数,详见 ./draw.ts-drawImage
const result = {
type: 'image',
borderRadius,
borderWidth: item.borderWidth,
borderColor: item.borderColor,
borderRadiusGroup: item.borderRadiusGroup,
zIndex: typeof zIndex !== 'undefined' ? zIndex : index,
imgPath: url,
sx,
sy,
sw: imgWidth - sx * 2,
sh: imgHeight - sy * 2,
x,
y,
w: width,
h: height
};
resolve(result);
})
.catch((err) => {
console.log('读取图片信息失败', err);
reject(err);
})
);
});
/**
* 获取线性渐变色
* @param {CanvasContext} ctx canvas 实例对象
* @param {String} color 线性渐变色,如 'linear-gradient(180deg, rgba(255, 255, 255, 0) 0%, #fff 100%)'
* @param {Number} startX 起点 x 坐标
* @param {Number} startY 起点 y 坐标
* @param {Number} w 宽度
* @param {Number} h 高度
* @returns {}
*/
// TODO: 待优化, 支持所有角度,多个颜色的线性渐变
export function getLinearColor(
ctx: CanvasContext,
color,
startX,
startY,
w,
h
) {
if (
typeof startX !== 'number' ||
typeof startY !== 'number' ||
typeof w !== 'number' ||
typeof h !== 'number'
) {
console.warn('坐标或者宽高只支持数字');
return color;
}
let grd: CanvasGradient | string = color;
if (color.includes('linear-gradient')) {
// fillStyle 不支持线性渐变色
const colorList = color.match(/\((\d+)deg,\s(.+)\s\d+%,\s(.+)\s\d+%/);
const radian = colorList[1]; // 渐变弧度(角度)
const color1 = colorList[2];
const color2 = colorList[3];
const L = Math.sqrt(w * w + h * h);
const x = Math.ceil(Math.sin(180 - radian) * L);
const y = Math.ceil(Math.cos(180 - radian) * L);
// 根据弧度和宽高确定渐变色的两个点的坐标
if (Number(radian) === 180 || Number(radian) === 0) {
if (Number(radian) === 180) {
grd = ctx.createLinearGradient(startX, startY, startX, startY + h);
}
if (Number(radian) === 0) {
grd = ctx.createLinearGradient(startX, startY + h, startX, startY);
}
} else if (radian > 0 && radian < 180) {
grd = ctx.createLinearGradient(startX, startY, x + startX, y + startY);
} else {
throw new Error('只支持0 <= 颜色弧度 <= 180');
}
(grd as CanvasGradient).addColorStop(0, color1);
(grd as CanvasGradient).addColorStop(1, color2);
}
return grd;
}
/**
* 根据文字对齐方式设置坐标
* @param {*} imgPath
* @param {*} index
* @returns { Promise }
*/
export function getTextX(textAlign, x, width) {
let newX = x;
if (textAlign === 'center') {
newX = width / 2 + x;
} else if (textAlign === 'right') {
newX = width + x;
}
return newX;
}
......@@ -57,3 +57,16 @@
}
}
}
.wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
}
.block {
width: 120px;
height: 120px;
background-color: #fff;
}
......
<!--
* @Date: 2022-09-26 14:36:57
* @LastEditors: hookehuyr hookehuyr@gmail.com
* @LastEditTime: 2022-09-26 17:37:54
* @LastEditTime: 2022-09-27 16:38:57
* @FilePath: /swx/src/pages/activityDetail/index.vue
* @Description: 文件描述
-->
......@@ -122,18 +122,41 @@
<activity-bar />
<van-action-sheet
:z-index="10"
:show="show_share"
:actions="actions_share"
cancel-text="取消"
:overlay="true"
@cancel="onCancelShare"
@select="onSelectShare"
/>
<van-overlay :show="show_post" :z-index="1">
<view class="wrapper">
<view class="preview-area" @click="onClickPost">
<image v-if="posterPath" :src="posterPath" mode="widthFix" />
</view>
</view>
</van-overlay>
<PosterBuilder v-if="startDraw" custom-style="position: fixed; left: 200%;" :config="base" @success="drawSuccess"
@fail="drawFail" />
<van-action-sheet
:z-index="1"
:show="show_save"
:actions="actions_save"
cancel-text="取消"
:overlay="false"
@cancel="onCancelSave"
@select="onSelectSave"
/>
</template>
<script setup>
import img_demo from '@/images/demo@2x.png'
import img_demo1 from '@/images/demo@2x-1.png'
import activityBar from '@/components/activity-bar.vue'
import Taro from '@tarojs/taro'
import PosterBuilder from '@/components/PosterBuilder/index.vue';
import { ref } from "vue";
......@@ -149,10 +172,293 @@ const shareActivity = () => {
const onCancelShare = () => {
show_share.value = false;
}
const onSelectShare = (event) => {
const onSelectShare = ({ detail }) => {
// TODO: 需要完善 分享朋友和生成海报功能
console.warn(event.detail);
show_share.value = false;
if (detail.name === '生成海报') {
show_post.value = true;
start()
}
}
const show_post = ref(false)
const onClickPost = () => {
show_save.value = true;
}
// 生成海报
const startDraw = ref(false)
const posterPath = ref('')
const base = {
width: 1024,
height: 1334,
backgroundColor: '',
debug: false,
blocks: [
{
x: 40,
y: 20,
width: 950,
height: 1200,
paddingLeft: 0,
paddingRight: 0,
borderWidth: 1,
borderColor: '#D9DCD5',
backgroundColor: '#fff',
borderRadius: 16,
// borderRadiusGroup: [0, 0, 18, 18],
},
{
x: 40,
y: 730,
// width: 580,
height: 75,
paddingLeft: 80,
paddingRight: 0,
borderWidth: 0,
text: {
x: 0,
y: 0,
text: '2022-08-25 14:00',
fontSize: 40,
color: '#222',
opacity: 1,
baseLine: 'top',
lineHeight: 48,
lineNum: 2,
textAlign: 'left',
// width: 580,
zIndex: 0,
},
backgroundColor: '#FFF9F3',
// borderColor: 'red',
// backgroundColor: '#EFF3F5',
// borderRadius: 0,
borderRadiusGroup: [0, 25, 25, 0],
// borderRadius: 16,
// zIndex: 100,
},
{
x: 40,
y: 830,
// width: 580,
height: 75,
paddingLeft: 80,
paddingRight: 0,
borderWidth: 0,
text: {
x: 0,
y: 0,
text: '上海市杨浦区军工路100号A座05室',
fontSize: 40,
color: '#222',
opacity: 1,
baseLine: 'top',
lineHeight: 48,
lineNum: 2,
textAlign: 'left',
// width: 580,
zIndex: 0,
},
backgroundColor: '#FFF9F3',
// borderColor: 'red',
// backgroundColor: '#EFF3F5',
// borderRadius: 0,
borderRadiusGroup: [0, 25, 25, 0],
// borderRadius: 16,
// zIndex: 100,
},
],
texts: [
{
x: 80,
y: 630,
text: '八段锦-智慧没有烦恼',
fontSize: 50,
color: '#000',
opacity: 1,
baseLine: 'middle',
lineHeight: 60,
lineNum: 2,
textAlign: 'left',
width: 800,
zIndex: 999,
// fontWeight: 'bold',
fontFamily: 'Monospace',
},
{
x: 135,
y: 770,
text: '2022-08-25 14:00',
fontSize: 40,
color: '#222',
opacity: 1,
baseLine: 'middle',
lineHeight: 48,
lineNum: 2,
textAlign: 'left',
// width: 580,
zIndex: 999,
},
{
x: 135,
y: 870,
text: '上海市杨浦区军工路100号A座05室',
fontSize: 40,
color: '#222',
opacity: 1,
baseLine: 'middle',
lineHeight: 48,
lineNum: 2,
textAlign: 'left',
// width: 580,
zIndex: 999,
},
{
x: 300,
y: 1080,
text: '妙净',
fontSize: 50,
color: '#333',
opacity: 1,
baseLine: 'middle',
textAlign: 'left',
lineHeight: 50,
lineNum: 1,
zIndex: 999,
},
{
x: 300,
y: 1150,
text: '邀请你一起来活动!',
fontSize: 42,
color: '#8F9399',
opacity: 1,
baseLine: 'middle',
textAlign: 'left',
lineHeight: 42,
lineNum: 1,
zIndex: 999,
}
],
images: [
{
url: 'https://tva1.sinaimg.cn/large/5f01a858gy1h6l450x64zj20ku0bq78c.jpg',
width: 950,
height: 500,
x: 40,
y: 20,
// borderRadius: 16,
borderRadiusGroup: [18, 18, 0, 0],
zIndex: 10,
// borderRadius: 150,
// borderWidth: 10,
// borderColor: 'red',
},
{
url: 'https://tva1.sinaimg.cn/large/5f01a858gy1h6l5d2bmijj200s00s0sh.jpg',
width: 40,
height: 40,
x: 80,
y: 750,
borderRadius: 100,
borderWidth: 0,
zIndex: 10,
},
{
url: 'https://tva1.sinaimg.cn/large/5f01a858gy1h6l5dc9q31j200o00u0ol.jpg',
width: 35,
height: 40,
x: 80,
y: 850,
borderRadius: 100,
borderWidth: 0,
zIndex: 10,
},
{
url: 'https://pic.juncao.cc/cms/images/minapp.jpg',
width: 170,
height: 170,
x: 80,
y: 1030,
borderRadius: 100,
borderWidth: 0,
zIndex: 10,
},
{
url: 'https://pic.juncao.cc/cms/images/minapp.jpg',
width: 170,
height: 170,
x: 750,
y: 1030,
borderRadius: 100,
borderWidth: 0,
zIndex: 10,
},
],
lines: [
{
startY: 970,
startX: 80,
endX: 950,
endY: 971,
width: 1,
color: '#8F9399',
}
]
}
const start = () => {
startDraw.value = true;
if (!posterPath.value) Taro.showLoading();
}
const drawSuccess = (result) => {
console.warn('绘制好了', result);
const { tempFilePath, errMsg } = result;
if (errMsg === 'canvasToTempFilePath:ok') {
posterPath.value = tempFilePath;
Taro.hideLoading();
} else {
Taro.hideLoading();
Taro.showToast({
title: '失败,请稍后重试',
icon: 'none',
duration: 2500
});
}
};
const drawFail = (result) => {
console.warn('绘制失败', result);
Taro.hideLoading();
}
const savePoster = () => {
Taro.saveImageToPhotosAlbum({
filePath: posterPath.value,
success() {
Taro.showToast({
title: '已保存到相册',
icon: 'success',
duration: 2000
});
// posterPath.value = '';
}
});
}
const show_save = ref(false);
const actions_save = ref([{
name: '保存至相册'
}]);
const onCancelSave = () => {
show_save.value = false;
show_post.value = false;
// posterPath.value = '';
}
const onSelectSave = ({ detail }) => {
if (detail.name === '保存至相册') {
show_save.value = false;
show_post.value = false;
savePoster()
}
}
</script>
......
export default {
navigationBarTitleText: 'demo',
usingComponents: {
},
}
.red {
color: red;
}
<template>
<view class="index">
<view class="action-bar">
<button @tap="start">生成海报</button>
<button @tap="savePoster">下载海报</button>
</view>
<view class="preview-area">
<image v-if="posterPath" :src="posterPath" mode="widthFix" />
<view v-else class="text">预览区域</view>
</view>
<PosterBuilder v-if="startDraw" custom-style="position: fixed; left: 200%;" :config="base" @success="drawSuccess"
@fail="drawFail" />
</view>
</template>
<script>
import Taro from '@tarojs/taro'
import { defineComponent, reactive, toRefs } from 'vue';
import PosterBuilder from '@/components/PosterBuilder/index.vue';
export default defineComponent({
name: 'Index',
components: {
PosterBuilder,
},
setup() {
const state = reactive({
startDraw: false,
posterPath: ''
})
const base = {
width: 750,
height: 1334,
backgroundColor: '#232422',
debug: false,
blocks: [
// 头部底色
{
x: 32,
y: 80,
width: 686,
height: 160,
paddingLeft: 0,
paddingRight: 0,
backgroundColor: '#FFFFFF',
borderRadius: 32,
zIndex: 10
},
//底部背景
{
x: 32,
y: 950,
width: 686,
height: 302,
paddingLeft: 0,
paddingRight: 0,
borderRadiusGroup: [0, 0, 16, 16],
backgroundColor: '#FFFFFF',
zIndex: 11
}
],
texts: [
{
x: 216,
y: 108,
text: 'BiBin',
width: 380,
lineNum: 2, // 最多几行
fontSize: 36,
fontWeight: 'bold',
color: '#1A171B',
zIndex: 11
},
{
x: 216,
y: 174,
text: '为你挑选了一个好物',
width: 380,
fontSize: 28,
color: '#7C7D7A',
zIndex: 11
},
{
x: 64,
y: 994,
text: `¥6799`,
fontSize: 48,
color: '#ED2D2B',
fontWeight: 'bold',
zIndex: 12
},
{
x: 64,
y: 1092,
text: 'Apple iPhone 13 (A2634) 256GB 蓝色 支持移动联通电信5G 双卡双待手机',
fontSize: 32,
width: 380,
color: '#282925',
lineNum: 2, // 最多几行
zIndex: 12
}
],
images: [
{
x: 64,
y: 100,
width: 120,
height: 120,
borderRadius: 60,
url: 'https://thirdwx.qlogo.cn/mmopen/vi_32/Q0j4TwGTfTJP05RJ5icJkUkBjVtSb3DHib4pGRVEqjw3qNic53kd1tJmibPpzR1etJnWJFiaIJDLK6TDzD3d5SyPsXQ/132',
zIndex: 11
},
{
x: 32,
y: 272,
width: 686,
height: 686,
url: 'https://m.360buyimg.com/mobilecms/s750x750_jfs/t1/119360/32/20820/239818/618be3c6E1dbf188d/4070880e024273bb.jpg!q80.dpg',
borderRadiusGroup: [16, 16, 0, 0],
zIndex: 11
}
]
};
const start = () => {
state.startDraw = true;
Taro.showLoading();
}
const drawSuccess = (result) => {
console.warn('绘制好了', result);
const { tempFilePath, errMsg } = result;
if (errMsg === 'canvasToTempFilePath:ok') {
state.posterPath = tempFilePath;
Taro.hideLoading();
} else {
Taro.hideLoading();
Taro.showToast({
title: '失败,请稍后重试',
icon: 'none',
duration: 2500
});
}
};
const drawFail = (result) => {
console.warn('绘制失败', result);
Taro.hideLoading();
}
const savePoster = () => {
Taro.saveImageToPhotosAlbum({
filePath: state.posterPath,
success() {
Taro.showToast({
title: '已保存到相册',
icon: 'success',
duration: 2000
});
}
});
}
return {
...toRefs(state),
base,
start,
drawSuccess,
drawFail,
savePoster
}
}
});
</script>
<style>
.index {
font-family: 'Avenir', Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
text-align: center;
color: #2c3e50;
margin-top: 60px;
}
.action-bar {
display: flex;
}
.preview-area {
width: 80%;
min-height: 800px;
margin: 20px auto;
text-align: center;
border: 1px solid #cccccc;
}
.text {
line-height: 800px;
}
</style>