Showing
10 changed files
with
1639 additions
and
4 deletions
| ... | @@ -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 | { | ... | ... |
src/components/PosterBuilder/index.vue
0 → 100644
| 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> |
src/components/PosterBuilder/types.d.ts
0 → 100644
| 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 | +}; |
src/components/PosterBuilder/utils/draw.ts
0 → 100644
| 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 | + }); |
src/components/PosterBuilder/utils/tools.ts
0 → 100644
| 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 | +} |
| 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 | ... | ... |
src/pages/post/index.config.js
0 → 100755
src/pages/post/index.less
0 → 100644
src/pages/post/index.vue
0 → 100644
| 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> |
-
Please register or login to post a comment