feat(PosterBuilder): 添加海报圆角功能支持
为海报生成器添加圆角功能,支持统一圆角和自定义四角不同圆角 新增 borderRadius 和 borderRadiusGroup 配置参数 使用 Canvas clip() 方法确保圆角效果全局生效 添加测试页面和详细使用文档
Showing
5 changed files
with
392 additions
and
6 deletions
src/components/PosterBuilder/README.md
0 → 100644
| 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 | { | ... | ... |
src/pages/TestPoster/index.vue
0 → 100644
| 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 |
-
Please register or login to post a comment