hookehuyr

feat(PosterBuilder): 添加海报圆角功能支持

为海报生成器添加圆角功能,支持统一圆角和自定义四角不同圆角
新增 borderRadius 和 borderRadiusGroup 配置参数
使用 Canvas clip() 方法确保圆角效果全局生效
添加测试页面和详细使用文档
1 +# PosterBuilder 海报生成器
2 +
3 +## 圆角功能使用说明
4 +
5 +### 功能概述
6 +
7 +PosterBuilder 组件现在支持为生成的海报添加圆角效果。您可以通过配置参数来控制海报的圆角样式。
8 +
9 +### 配置参数
10 +
11 +#### 1. 统一圆角 (borderRadius)
12 +
13 +为海报四个角设置相同的圆角半径:
14 +
15 +```javascript
16 +const config = {
17 + width: 600,
18 + height: 800,
19 + backgroundColor: '#4F46E5',
20 + borderRadius: 30, // 统一圆角半径
21 + // ... 其他配置
22 +}
23 +```
24 +
25 +#### 2. 自定义圆角 (borderRadiusGroup)
26 +
27 +为海报四个角分别设置不同的圆角半径:
28 +
29 +```javascript
30 +const config = {
31 + width: 600,
32 + height: 800,
33 + backgroundColor: '#4F46E5',
34 + borderRadiusGroup: [50, 20, 50, 20], // [左上, 右上, 右下, 左下]
35 + // ... 其他配置
36 +}
37 +```
38 +
39 +#### 3. 无圆角(默认)
40 +
41 +如果不设置 `borderRadius``borderRadiusGroup`,海报将保持原有的直角样式:
42 +
43 +```javascript
44 +const config = {
45 + width: 600,
46 + height: 800,
47 + backgroundColor: '#4F46E5',
48 + // 不设置圆角参数,保持直角
49 + // ... 其他配置
50 +}
51 +```
52 +
53 +### 使用示例
54 +
55 +```vue
56 +<template>
57 + <PosterBuilder
58 + :config="posterConfig"
59 + :show-loading="true"
60 + @success="onPosterSuccess"
61 + @fail="onPosterFail"
62 + />
63 +</template>
64 +
65 +<script>
66 +import { ref } from 'vue'
67 +import PosterBuilder from '@/components/PosterBuilder/index.vue'
68 +
69 +export default {
70 + components: {
71 + PosterBuilder
72 + },
73 + setup() {
74 + const posterConfig = ref({
75 + width: 600,
76 + height: 800,
77 + backgroundColor: '#4F46E5',
78 + borderRadius: 30, // 添加圆角
79 + texts: [
80 + {
81 + text: '圆角海报标题',
82 + x: 300,
83 + y: 100,
84 + fontSize: 32,
85 + color: '#FFFFFF',
86 + textAlign: 'center'
87 + }
88 + ]
89 + })
90 +
91 + const onPosterSuccess = (result) => {
92 + console.log('海报生成成功:', result.tempFilePath)
93 + }
94 +
95 + const onPosterFail = (error) => {
96 + console.error('海报生成失败:', error)
97 + }
98 +
99 + return {
100 + posterConfig,
101 + onPosterSuccess,
102 + onPosterFail
103 + }
104 + }
105 +}
106 +</script>
107 +```
108 +
109 +### 注意事项
110 +
111 +1. **优先级**:如果同时设置了 `borderRadius``borderRadiusGroup`,将优先使用 `borderRadiusGroup`
112 +2. **数组格式**`borderRadiusGroup` 必须是包含4个数字的数组,分别对应 [左上, 右上, 右下, 左下] 四个角的圆角半径
113 +3. **单位**:圆角半径的单位与画布尺寸单位一致
114 +4. **兼容性**:该功能向下兼容,不设置圆角参数时保持原有行为
115 +
116 +### 重要修复说明
117 +
118 +**问题**:之前版本中,即使设置了 `borderRadius``borderRadiusGroup`,生成的海报四个角仍然是直角。
119 +
120 +**原因**:圆角背景被后续绘制的图片(特别是背景图)覆盖,导致圆角效果失效。
121 +
122 +**解决方案**:使用 Canvas 的 `clip()` 方法设置全局裁剪路径,确保所有绘制内容都在圆角范围内。
123 +
124 +### 测试页面
125 +
126 +项目中提供了测试页面 `src/pages/TestPoster/index.vue`,您可以通过该页面测试不同的圆角效果。
127 +
128 +### 技术实现细节
129 +
130 +1. **裁剪路径设置**:在绘制任何内容之前,先根据圆角配置创建裁剪路径
131 +2. **全局生效**:裁剪路径对后续所有绘制操作(图片、文字、块等)都生效
132 +3. **上下文管理**:使用 `ctx.save()``ctx.restore()` 正确管理画布上下文
...\ No newline at end of file ...\ No newline at end of file
...@@ -11,7 +11,7 @@ ...@@ -11,7 +11,7 @@
11 import Taro from "@tarojs/taro" 11 import Taro from "@tarojs/taro"
12 import { defineComponent, onMounted, PropType, ref } from "vue" 12 import { defineComponent, onMounted, PropType, ref } from "vue"
13 import { Image, DrawConfig } from "./types" 13 import { Image, DrawConfig } from "./types"
14 -import { drawImage, drawText, drawBlock, drawLine } from "./utils/draw" 14 +import { drawImage, drawText, drawBlock, drawLine, _drawRadiusRect, _drawRadiusGroupRect } from "./utils/draw"
15 import { 15 import {
16 toPx, 16 toPx,
17 toRpx, 17 toRpx,
...@@ -41,8 +41,11 @@ export default defineComponent({ ...@@ -41,8 +41,11 @@ export default defineComponent({
41 backgroundColor, 41 backgroundColor,
42 texts = [], 42 texts = [],
43 blocks = [], 43 blocks = [],
44 + images = [],
44 lines = [], 45 lines = [],
45 debug = false, 46 debug = false,
47 + borderRadius,
48 + borderRadiusGroup,
46 } = props.config || {} 49 } = props.config || {}
47 50
48 const canvasId = getRandomId() 51 const canvasId = getRandomId()
...@@ -125,12 +128,29 @@ export default defineComponent({ ...@@ -125,12 +128,29 @@ export default defineComponent({
125 canvas.width = width 128 canvas.width = width
126 canvas.height = height 129 canvas.height = height
127 130
131 + // 如果有圆角配置,设置全局裁剪路径
132 + if (borderRadius || borderRadiusGroup) {
133 + ctx.save() // 保存绘图上下文
134 + if (borderRadiusGroup && borderRadiusGroup.length === 4) {
135 + _drawRadiusGroupRect(
136 + { x: 0, y: 0, w: width, h: height, g: borderRadiusGroup },
137 + { ctx }
138 + )
139 + } else if (borderRadius) {
140 + _drawRadiusRect(
141 + { x: 0, y: 0, w: width, h: height, r: borderRadius },
142 + { ctx }
143 + )
144 + }
145 + ctx.clip() // 设置裁剪路径
146 + }
147 +
128 // 设置画布底色 148 // 设置画布底色
129 if (backgroundColor) { 149 if (backgroundColor) {
130 ctx.save() // 保存绘图上下文 150 ctx.save() // 保存绘图上下文
131 const grd = getLinearColor(ctx, backgroundColor, 0, 0, width, height) 151 const grd = getLinearColor(ctx, backgroundColor, 0, 0, width, height)
132 ctx.fillStyle = grd // 设置填充颜色 152 ctx.fillStyle = grd // 设置填充颜色
133 - ctx.fillRect(0, 0, width, height) // 填充一个矩形 153 + ctx.fillRect(0, 0, width, height) // 填充矩形
134 ctx.restore() // 恢复之前保存的绘图上下文 154 ctx.restore() // 恢复之前保存的绘图上下文
135 } 155 }
136 // 将要画的方块、文字、线条放进队列数组 156 // 将要画的方块、文字、线条放进队列数组
...@@ -176,6 +196,11 @@ export default defineComponent({ ...@@ -176,6 +196,11 @@ export default defineComponent({
176 } 196 }
177 } 197 }
178 198
199 + // 如果设置了圆角裁剪,恢复画布上下文
200 + if (borderRadius || borderRadiusGroup) {
201 + ctx.restore() // 恢复之前保存的绘图上下文
202 + }
203 +
179 setTimeout(() => { 204 setTimeout(() => {
180 getTempFile(canvas) // 需要做延时才能能正常加载图片 205 getTempFile(canvas) // 需要做延时才能能正常加载图片
181 }, 300) 206 }, 300)
......
...@@ -71,6 +71,8 @@ export type DrawConfig = { ...@@ -71,6 +71,8 @@ export type DrawConfig = {
71 height: number; 71 height: number;
72 backgroundColor?: string; 72 backgroundColor?: string;
73 debug?: boolean; 73 debug?: boolean;
74 + borderRadius?: number;
75 + borderRadiusGroup?: number[];
74 blocks?: Block[]; 76 blocks?: Block[];
75 texts?: Text[]; 77 texts?: Text[];
76 images?: Image[]; 78 images?: Image[];
......
...@@ -28,8 +28,8 @@ ...@@ -28,8 +28,8 @@
28 </view> 28 </view>
29 29
30 <!-- 海报预览区域 - 正常状态 --> 30 <!-- 海报预览区域 - 正常状态 -->
31 - <view v-if="pageState === 'normal'" class="flex-1 mx-4 mb-2 bg-white rounded-lg shadow-sm relative" style="overflow: visible;"> 31 + <view v-if="pageState === 'normal'" class="flex-1 mx-4 mb-2 bg-white rounded-lg relative" style="overflow: visible;">
32 - <view class="h-full relative bg-gray-100 flex items-center justify-center"> 32 + <view class="h-full relative flex items-center justify-center">
33 <view v-if="currentPoster.path" class="w-full h-full relative"> 33 <view v-if="currentPoster.path" class="w-full h-full relative">
34 <image 34 <image
35 :src="currentPoster.path" 35 :src="currentPoster.path"
...@@ -41,9 +41,9 @@ ...@@ -41,9 +41,9 @@
41 {{ posterList[currentPosterIndex]?.title || '海报生成中' }} 41 {{ posterList[currentPosterIndex]?.title || '海报生成中' }}
42 </view> --> 42 </view> -->
43 <!-- 点击预览提示 --> 43 <!-- 点击预览提示 -->
44 - <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded"> 44 + <!-- <view @tap="previewPoster" class="absolute bottom-2 right-2 bg-black bg-opacity-50 text-white text-xs px-2 py-1 rounded">
45 点击预览 45 点击预览
46 - </view> 46 + </view> -->
47 </view> 47 </view>
48 </view> 48 </view>
49 49
...@@ -302,6 +302,7 @@ const posterConfig = computed(() => { ...@@ -302,6 +302,7 @@ const posterConfig = computed(() => {
302 height: 1334, 302 height: 1334,
303 backgroundColor: '#f5f5f5', 303 backgroundColor: '#f5f5f5',
304 debug: false, 304 debug: false,
305 + borderRadius: 15,
305 images: [ 306 images: [
306 // 背景图 307 // 背景图
307 { 308 {
......
1 +<template>
2 + <view class="test-poster-page">
3 + <view class="mb-4">
4 + <text class="text-lg font-bold">海报圆角测试</text>
5 + </view>
6 +
7 + <!-- 测试按钮 -->
8 + <view class="flex flex-col gap-4 mb-6">
9 + <nut-button @click="generateNormalPoster" type="primary">
10 + 生成普通海报(无圆角)
11 + </nut-button>
12 + <nut-button @click="generateRoundPoster" type="success">
13 + 生成圆角海报(统一圆角)
14 + </nut-button>
15 + <nut-button @click="generateCustomRoundPoster" type="warning">
16 + 生成自定义圆角海报(四角不同)
17 + </nut-button>
18 + </view>
19 +
20 + <!-- 海报生成组件 -->
21 + <PosterBuilder
22 + v-if="showPoster"
23 + :config="posterConfig"
24 + :show-loading="true"
25 + @success="onPosterSuccess"
26 + @fail="onPosterFail"
27 + />
28 +
29 + <!-- 生成的海报预览 -->
30 + <view v-if="posterUrl" class="mt-6">
31 + <text class="text-base font-semibold mb-2">生成的海报:</text>
32 + <image
33 + :src="posterUrl"
34 + mode="widthFix"
35 + class="w-full max-w-xs mx-auto block"
36 + />
37 + </view>
38 + </view>
39 +</template>
40 +
41 +<script>
42 +import { defineComponent, ref } from 'vue'
43 +import Taro from '@tarojs/taro'
44 +import PosterBuilder from '../../components/PosterBuilder/index.vue'
45 +
46 +export default defineComponent({
47 + name: 'TestPoster',
48 + components: {
49 + PosterBuilder
50 + },
51 + setup() {
52 + const showPoster = ref(false)
53 + const posterUrl = ref('')
54 + const posterConfig = ref({})
55 +
56 + /**
57 + * 生成普通海报(无圆角)
58 + */
59 + const generateNormalPoster = () => {
60 + posterConfig.value = {
61 + width: 600,
62 + height: 800,
63 + backgroundColor: '#4F46E5',
64 + texts: [
65 + {
66 + text: '普通海报测试',
67 + x: 300,
68 + y: 100,
69 + fontSize: 32,
70 + color: '#FFFFFF',
71 + textAlign: 'center',
72 + fontWeight: 'bold'
73 + },
74 + {
75 + text: '这是一个没有圆角的海报',
76 + x: 300,
77 + y: 200,
78 + fontSize: 24,
79 + color: '#E5E7EB',
80 + textAlign: 'center'
81 + }
82 + ],
83 + blocks: [
84 + {
85 + x: 50,
86 + y: 300,
87 + width: 500,
88 + height: 200,
89 + backgroundColor: '#FFFFFF',
90 + borderRadius: 0
91 + }
92 + ]
93 + }
94 + showPoster.value = true
95 + }
96 +
97 + /**
98 + * 生成圆角海报(统一圆角)
99 + */
100 + const generateRoundPoster = () => {
101 + posterConfig.value = {
102 + width: 600,
103 + height: 800,
104 + backgroundColor: '#10B981',
105 + borderRadius: 30, // 统一圆角
106 + texts: [
107 + {
108 + text: '圆角海报测试',
109 + x: 300,
110 + y: 100,
111 + fontSize: 32,
112 + color: '#FFFFFF',
113 + textAlign: 'center',
114 + fontWeight: 'bold'
115 + },
116 + {
117 + text: '这是一个有统一圆角的海报',
118 + x: 300,
119 + y: 200,
120 + fontSize: 24,
121 + color: '#E5E7EB',
122 + textAlign: 'center'
123 + }
124 + ],
125 + blocks: [
126 + {
127 + x: 50,
128 + y: 300,
129 + width: 500,
130 + height: 200,
131 + backgroundColor: '#FFFFFF',
132 + borderRadius: 20
133 + }
134 + ]
135 + }
136 + showPoster.value = true
137 + }
138 +
139 + /**
140 + * 生成自定义圆角海报(四角不同)
141 + */
142 + const generateCustomRoundPoster = () => {
143 + posterConfig.value = {
144 + width: 600,
145 + height: 800,
146 + backgroundColor: '#F59E0B',
147 + borderRadiusGroup: [50, 20, 50, 20], // 左上、右上、右下、左下
148 + texts: [
149 + {
150 + text: '自定义圆角海报',
151 + x: 300,
152 + y: 100,
153 + fontSize: 32,
154 + color: '#FFFFFF',
155 + textAlign: 'center',
156 + fontWeight: 'bold'
157 + },
158 + {
159 + text: '四个角有不同的圆角半径',
160 + x: 300,
161 + y: 200,
162 + fontSize: 24,
163 + color: '#1F2937',
164 + textAlign: 'center'
165 + }
166 + ],
167 + blocks: [
168 + {
169 + x: 50,
170 + y: 300,
171 + width: 500,
172 + height: 200,
173 + backgroundColor: '#FFFFFF',
174 + borderRadiusGroup: [30, 10, 30, 10]
175 + }
176 + ]
177 + }
178 + showPoster.value = true
179 + }
180 +
181 + /**
182 + * 海报生成成功回调
183 + */
184 + const onPosterSuccess = (result) => {
185 + console.log('海报生成成功:', result)
186 + posterUrl.value = result.tempFilePath
187 + showPoster.value = false
188 + Taro.showToast({
189 + title: '海报生成成功',
190 + icon: 'success'
191 + })
192 + }
193 +
194 + /**
195 + * 海报生成失败回调
196 + */
197 + const onPosterFail = (error) => {
198 + console.error('海报生成失败:', error)
199 + showPoster.value = false
200 + Taro.showToast({
201 + title: '海报生成失败',
202 + icon: 'error'
203 + })
204 + }
205 +
206 + return {
207 + showPoster,
208 + posterUrl,
209 + posterConfig,
210 + generateNormalPoster,
211 + generateRoundPoster,
212 + generateCustomRoundPoster,
213 + onPosterSuccess,
214 + onPosterFail
215 + }
216 + }
217 +})
218 +</script>
219 +
220 +<style lang="less" scoped>
221 +.test-poster-page {
222 + padding: 32rpx;
223 + min-height: 100vh;
224 + background-color: #f5f5f5;
225 +}
226 +</style>
...\ No newline at end of file ...\ No newline at end of file