foliko 1.0.87 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agent/data/default.json +3 -108
- package/.agent/data/plugins-state.json +34 -1
- package/.agent/mcp_config.json +0 -1
- package/.agent/memory/core.md +1 -0
- package/.agent/memory/project/mnn93ogy-ypjn27.md +9 -0
- package/.agent/memory/project/mnn98fqy-5nhc1u.md +25 -0
- package/.agent/memory/user/mnm67t9m-x8rekk.md +9 -0
- package/.agent/memory/user/mnn5mmqh-w6aktx.md +11 -0
- package/.agent/memory/user/mnnbfhhn-dk1bd1.md +22 -0
- package/.agent/plugins/__pycache__/file_writer.cpython-312.pyc +0 -0
- package/.agent/plugins/poster-plugin/README.md +304 -0
- package/.agent/plugins/poster-plugin/fonts/PatuaOne-Regular.ttf +0 -0
- package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221.ttf +0 -0
- package/.agent/plugins/poster-plugin/fonts//345/276/256/350/275/257/351/233/205/351/273/221/347/262/227/344/275/223.ttf +0 -0
- package/.agent/plugins/poster-plugin/index.js +13 -0
- package/.agent/plugins/poster-plugin/package.json +28 -0
- package/.agent/plugins/poster-plugin/src/canvas.js +161 -0
- package/.agent/plugins/poster-plugin/src/components/arrow.js +84 -0
- package/.agent/plugins/poster-plugin/src/components/avatar.js +71 -0
- package/.agent/plugins/poster-plugin/src/components/badge.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/card.js +88 -0
- package/.agent/plugins/poster-plugin/src/components/chart.js +127 -0
- package/.agent/plugins/poster-plugin/src/components/chip.js +88 -0
- package/.agent/plugins/poster-plugin/src/components/columns.js +107 -0
- package/.agent/plugins/poster-plugin/src/components/cta.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/divider.js +55 -0
- package/.agent/plugins/poster-plugin/src/components/feature.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/featureGrid.js +112 -0
- package/.agent/plugins/poster-plugin/src/components/grid.js +118 -0
- package/.agent/plugins/poster-plugin/src/components/imageFrame.js +155 -0
- package/.agent/plugins/poster-plugin/src/components/index.js +62 -0
- package/.agent/plugins/poster-plugin/src/components/listItem.js +146 -0
- package/.agent/plugins/poster-plugin/src/components/notification.js +123 -0
- package/.agent/plugins/poster-plugin/src/components/progress.js +79 -0
- package/.agent/plugins/poster-plugin/src/components/progressCircle.js +117 -0
- package/.agent/plugins/poster-plugin/src/components/quote.js +97 -0
- package/.agent/plugins/poster-plugin/src/components/rating.js +85 -0
- package/.agent/plugins/poster-plugin/src/components/star.js +70 -0
- package/.agent/plugins/poster-plugin/src/components/statCard.js +105 -0
- package/.agent/plugins/poster-plugin/src/components/stepper.js +118 -0
- package/.agent/plugins/poster-plugin/src/components/table.js +159 -0
- package/.agent/plugins/poster-plugin/src/components/tagCloud.js +78 -0
- package/.agent/plugins/poster-plugin/src/components/timeline.js +105 -0
- package/.agent/plugins/poster-plugin/src/components/watermark.js +52 -0
- package/.agent/plugins/poster-plugin/src/composer.js +1904 -0
- package/.agent/plugins/poster-plugin/src/elements/artText.js +60 -0
- package/.agent/plugins/poster-plugin/src/elements/background.js +52 -0
- package/.agent/plugins/poster-plugin/src/elements/circle.js +31 -0
- package/.agent/plugins/poster-plugin/src/elements/image.js +71 -0
- package/.agent/plugins/poster-plugin/src/elements/index.js +26 -0
- package/.agent/plugins/poster-plugin/src/elements/line.js +23 -0
- package/.agent/plugins/poster-plugin/src/elements/polygon.js +32 -0
- package/.agent/plugins/poster-plugin/src/elements/rectangle.js +32 -0
- package/.agent/plugins/poster-plugin/src/elements/svg.js +92 -0
- package/.agent/plugins/poster-plugin/src/elements/text.js +38 -0
- package/.agent/plugins/poster-plugin/src/fonts.js +118 -0
- package/.agent/plugins/poster-plugin/src/index.js +1659 -0
- package/.agent/plugins/poster-plugin/src/presets.js +36 -0
- package/.agent/plugins/poster-plugin/src/templates/business.js +60 -0
- package/.agent/plugins/poster-plugin/src/templates/gradient.js +64 -0
- package/.agent/plugins/poster-plugin/src/templates/index.js +43 -0
- package/.agent/plugins/poster-plugin/src/templates/modern.js +69 -0
- package/.agent/plugins/poster-plugin/src/templates/simple.js +58 -0
- package/.agent/plugins/poster-plugin/src/templates/social.js +62 -0
- package/.agent/plugins/poster-plugin/src/templates/tech.js +84 -0
- package/.agent/sessions/cli_default.json +24265 -0
- package/.agent/weixin.json +6 -0
- package/.claude/settings.local.json +5 -8
- package/CLAUDE.md +144 -108
- package/docs/CONTEXT_DESIGN.md +1596 -0
- package/examples/test-concurrent-chat.js +60 -60
- package/output/beef-love-poster.png +0 -0
- package/package.json +2 -2
- package/plugins/default-plugins.js +2 -1
- package/plugins/extension-executor-plugin.js +11 -0
- package/plugins/memory-plugin.js +984 -0
- package/plugins/session-plugin.js +57 -1
- package/plugins/weixin-plugin.js +24 -22
- package/skills/poster-guide/SKILL.md +743 -0
- package/skills/python-plugin-dev/SKILL.md +238 -238
- package/skills/skill-guide/SKILL.md +130 -108
- package/src/capabilities/skill-manager.js +99 -0
- package/src/core/agent-chat.js +538 -138
- package/src/core/agent-context.js +188 -0
- package/src/core/agent.js +6 -2
- package/src/core/context-manager.js +283 -0
- package/src/core/framework.js +264 -3
- package/src/core/plugin-manager.js +79 -2
- package/src/core/request-context.js +98 -0
- package/src/core/session-context.js +341 -0
- package/src/core/session-storage.js +274 -0
- package/src/executors/mcp-executor.js +2 -2
- package/src/utils/index.js +239 -67
- package/src/utils/plugin-helpers.js +17 -0
- package//346/265/267/346/212/245/346/217/222/344/273/266.md +621 -0
- package/.agent/plugins/__pycache__/test_plugin.cpython-312.pyc +0 -0
- package/.agent/plugins/temp-repo/LICENSE +0 -201
- package/.agent/plugins/temp-repo/puppeteer-plugin/README.md +0 -147
- package/.agent/plugins/temp-repo/puppeteer-plugin/index.js +0 -1418
- package/.agent/plugins/temp-repo/puppeteer-plugin/package.json +0 -9
- package/.agent/plugins/test_plugin.py +0 -304
- package/examples/test-chat-debug.js +0 -102
- package/examples/test-chat-result.js +0 -76
- package/examples/test-chat-stream-diff.js +0 -63
|
@@ -0,0 +1,1904 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 海报组件化生成器
|
|
3
|
+
*
|
|
4
|
+
* 支持 JSON 配置驱动,一次调用生成完整海报
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const paper = require('paper')
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 辅助函数:将元素添加到活跃层
|
|
11
|
+
*/
|
|
12
|
+
function addToLayer(project, element) {
|
|
13
|
+
if (project && project.activeLayer && element) {
|
|
14
|
+
if (element.insert) {
|
|
15
|
+
project.activeLayer.addChild(element)
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 辅助函数:批量添加元素到活跃层
|
|
22
|
+
*/
|
|
23
|
+
function addAllToLayer(project, elements) {
|
|
24
|
+
if (project && project.activeLayer && elements) {
|
|
25
|
+
elements.forEach(el => {
|
|
26
|
+
if (el.id && el.type) {
|
|
27
|
+
const item = project.getItem({ id: el.id })
|
|
28
|
+
if (item) project.activeLayer.addChild(item)
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 组件类型注册表
|
|
36
|
+
*/
|
|
37
|
+
const COMPONENT_TYPES = {
|
|
38
|
+
// 基础元素
|
|
39
|
+
background: 'background',
|
|
40
|
+
rectangle: 'rectangle',
|
|
41
|
+
circle: 'circle',
|
|
42
|
+
line: 'line',
|
|
43
|
+
polygon: 'polygon',
|
|
44
|
+
text: 'text',
|
|
45
|
+
artText: 'artText',
|
|
46
|
+
image: 'image',
|
|
47
|
+
svg: 'svg',
|
|
48
|
+
imageFrame: 'imageFrame',
|
|
49
|
+
// 布局组件
|
|
50
|
+
columns: 'columns',
|
|
51
|
+
grid: 'grid',
|
|
52
|
+
// 装饰组件
|
|
53
|
+
star: 'star',
|
|
54
|
+
arrow: 'arrow',
|
|
55
|
+
progressCircle: 'progressCircle',
|
|
56
|
+
chip: 'chip',
|
|
57
|
+
chart: 'chart',
|
|
58
|
+
watermark: 'watermark',
|
|
59
|
+
table: 'table',
|
|
60
|
+
// 高级组件
|
|
61
|
+
card: 'card',
|
|
62
|
+
badge: 'badge',
|
|
63
|
+
cta: 'cta',
|
|
64
|
+
feature: 'feature',
|
|
65
|
+
featureGrid: 'featureGrid',
|
|
66
|
+
divider: 'divider',
|
|
67
|
+
avatar: 'avatar',
|
|
68
|
+
progress: 'progress',
|
|
69
|
+
rating: 'rating',
|
|
70
|
+
quote: 'quote',
|
|
71
|
+
statCard: 'statCard',
|
|
72
|
+
tagCloud: 'tagCloud',
|
|
73
|
+
stepper: 'stepper',
|
|
74
|
+
timeline: 'timeline',
|
|
75
|
+
listItem: 'listItem',
|
|
76
|
+
notification: 'notification',
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* 从配置创建海报
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} project - Paper.js 项目
|
|
83
|
+
* @param {Object} canvas - 画布对象
|
|
84
|
+
* @param {Object} config - 海报配置
|
|
85
|
+
* @returns {Object} 创建结果
|
|
86
|
+
*/
|
|
87
|
+
async function createFromConfig(project, canvas, config) {
|
|
88
|
+
const { components = [] } = config
|
|
89
|
+
const results = []
|
|
90
|
+
|
|
91
|
+
for (const component of components) {
|
|
92
|
+
const result = await createComponent(project, canvas, component)
|
|
93
|
+
results.push(result)
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 将所有创建的元素添加到活动层
|
|
97
|
+
if (project && project.activeLayer) {
|
|
98
|
+
project.activeLayer.addChildren(project.layers.flatMap(l => l.children))
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return {
|
|
102
|
+
success: true,
|
|
103
|
+
componentCount: results.length,
|
|
104
|
+
results,
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* 根据配置创建单个组件
|
|
110
|
+
*/
|
|
111
|
+
function createComponent(project, canvas, config) {
|
|
112
|
+
const { type, ...args } = config
|
|
113
|
+
|
|
114
|
+
switch (type) {
|
|
115
|
+
// 基础元素
|
|
116
|
+
case 'background':
|
|
117
|
+
return createBackgroundElement(project, canvas, args)
|
|
118
|
+
case 'rectangle':
|
|
119
|
+
return createRectangleElement(project, args)
|
|
120
|
+
case 'circle':
|
|
121
|
+
return createCircleElement(project, args)
|
|
122
|
+
case 'line':
|
|
123
|
+
return createLineElement(project, args)
|
|
124
|
+
case 'polygon':
|
|
125
|
+
return createPolygonElement(project, args)
|
|
126
|
+
case 'text':
|
|
127
|
+
return createTextElement(project, args)
|
|
128
|
+
case 'artText':
|
|
129
|
+
return createArtTextElement(project, args)
|
|
130
|
+
case 'image':
|
|
131
|
+
return createImageElement(project, args)
|
|
132
|
+
case 'svg':
|
|
133
|
+
return createSVGElement(project, args)
|
|
134
|
+
case 'imageFrame':
|
|
135
|
+
return createImageFrameComponent(project, canvas, args)
|
|
136
|
+
case 'columns':
|
|
137
|
+
return createColumnsComponent(project, canvas, args)
|
|
138
|
+
case 'grid':
|
|
139
|
+
return createGridComponent(project, canvas, args)
|
|
140
|
+
|
|
141
|
+
// 装饰组件
|
|
142
|
+
case 'star':
|
|
143
|
+
return createStarComponent(project, canvas, args)
|
|
144
|
+
case 'arrow':
|
|
145
|
+
return createArrowComponent(project, canvas, args)
|
|
146
|
+
case 'progressCircle':
|
|
147
|
+
return createProgressCircleComponent(project, canvas, args)
|
|
148
|
+
case 'chip':
|
|
149
|
+
return createChipComponent(project, canvas, args)
|
|
150
|
+
case 'chart':
|
|
151
|
+
return createChartComponent(project, canvas, args)
|
|
152
|
+
case 'watermark':
|
|
153
|
+
return createWatermarkComponent(project, canvas, args)
|
|
154
|
+
case 'table':
|
|
155
|
+
return createTableComponent(project, canvas, args)
|
|
156
|
+
|
|
157
|
+
// 高级组件
|
|
158
|
+
case 'card':
|
|
159
|
+
return createCardComponent(project, canvas, args)
|
|
160
|
+
case 'badge':
|
|
161
|
+
return createBadgeComponent(project, canvas, args)
|
|
162
|
+
case 'cta':
|
|
163
|
+
return createCTAComponent(project, canvas, args)
|
|
164
|
+
case 'feature':
|
|
165
|
+
return createFeatureComponent(project, canvas, args)
|
|
166
|
+
case 'featureGrid':
|
|
167
|
+
return createFeatureGridComponent(project, canvas, args)
|
|
168
|
+
case 'divider':
|
|
169
|
+
return createDividerComponent(project, canvas, args)
|
|
170
|
+
case 'avatar':
|
|
171
|
+
return createAvatarComponent(project, canvas, args)
|
|
172
|
+
case 'progress':
|
|
173
|
+
return createProgressComponent(project, canvas, args)
|
|
174
|
+
case 'rating':
|
|
175
|
+
return createRatingComponent(project, canvas, args)
|
|
176
|
+
case 'quote':
|
|
177
|
+
return createQuoteComponent(project, canvas, args)
|
|
178
|
+
case 'statCard':
|
|
179
|
+
return createStatCardComponent(project, canvas, args)
|
|
180
|
+
case 'tagCloud':
|
|
181
|
+
return createTagCloudComponent(project, canvas, args)
|
|
182
|
+
case 'stepper':
|
|
183
|
+
return createStepperComponent(project, canvas, args)
|
|
184
|
+
case 'timeline':
|
|
185
|
+
return createTimelineComponent(project, canvas, args)
|
|
186
|
+
case 'listItem':
|
|
187
|
+
return createListItemComponent(project, canvas, args)
|
|
188
|
+
case 'notification':
|
|
189
|
+
return createNotificationComponent(project, canvas, args)
|
|
190
|
+
|
|
191
|
+
default:
|
|
192
|
+
return { success: false, error: `Unknown component type: ${type}` }
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
// ============= 基础元素创建函数 =============
|
|
197
|
+
|
|
198
|
+
function createBackgroundElement(project, canvas, { color, gradient }) {
|
|
199
|
+
if (gradient) {
|
|
200
|
+
const paperColors = gradient.colors.map(c => new paper.Color(c))
|
|
201
|
+
const { type, direction } = gradient
|
|
202
|
+
|
|
203
|
+
if (type === 'linear') {
|
|
204
|
+
const angle = (direction || 45) * Math.PI / 180
|
|
205
|
+
const diagonal = Math.sqrt(canvas.width ** 2 + canvas.height ** 2)
|
|
206
|
+
const centerX = canvas.width / 2
|
|
207
|
+
const centerY = canvas.height / 2
|
|
208
|
+
const start = new paper.Point(
|
|
209
|
+
centerX - Math.cos(angle) * diagonal / 2,
|
|
210
|
+
centerY - Math.sin(angle) * diagonal / 2
|
|
211
|
+
)
|
|
212
|
+
const stop = new paper.Point(
|
|
213
|
+
centerX + Math.cos(angle) * diagonal / 2,
|
|
214
|
+
centerY + Math.sin(angle) * diagonal / 2
|
|
215
|
+
)
|
|
216
|
+
project.activeLayer.fillColor = new paper.Color({
|
|
217
|
+
gradient: { stops: paperColors },
|
|
218
|
+
origin: start,
|
|
219
|
+
destination: stop,
|
|
220
|
+
})
|
|
221
|
+
} else {
|
|
222
|
+
const center = new paper.Point(canvas.width / 2, canvas.height / 2)
|
|
223
|
+
const radius = Math.max(canvas.width, canvas.height) / 2
|
|
224
|
+
project.activeLayer.fillColor = new paper.Color({
|
|
225
|
+
gradient: { stops: paperColors },
|
|
226
|
+
origin: center,
|
|
227
|
+
destination: center.add(new paper.Point(radius, 0)),
|
|
228
|
+
})
|
|
229
|
+
}
|
|
230
|
+
} else if (color) {
|
|
231
|
+
project.activeLayer.fillColor = new paper.Color(color)
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return { success: true, type: 'background' }
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function createRectangleElement(project, { x, y, width, height, fill, stroke, strokeWidth, radius, opacity }) {
|
|
238
|
+
const rect = new paper.Path.Rectangle({
|
|
239
|
+
point: [x, y],
|
|
240
|
+
size: [width, height],
|
|
241
|
+
radius: radius || 0,
|
|
242
|
+
})
|
|
243
|
+
|
|
244
|
+
if (fill) rect.fillColor = new paper.Color(fill)
|
|
245
|
+
if (stroke) {
|
|
246
|
+
rect.strokeColor = new paper.Color(stroke)
|
|
247
|
+
rect.strokeWidth = strokeWidth || 1
|
|
248
|
+
}
|
|
249
|
+
if (opacity !== undefined) rect.opacity = opacity
|
|
250
|
+
|
|
251
|
+
return { success: true, id: rect.id, type: 'rectangle' }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createCircleElement(project, { cx, cy, rx, ry, fill, stroke, strokeWidth, opacity }) {
|
|
255
|
+
const circle = new paper.Path.Ellipse({
|
|
256
|
+
center: [cx, cy],
|
|
257
|
+
radius: [rx, ry || rx],
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
if (fill) circle.fillColor = new paper.Color(fill)
|
|
261
|
+
if (stroke) {
|
|
262
|
+
circle.strokeColor = new paper.Color(stroke)
|
|
263
|
+
circle.strokeWidth = strokeWidth || 1
|
|
264
|
+
}
|
|
265
|
+
if (opacity !== undefined) circle.opacity = opacity
|
|
266
|
+
|
|
267
|
+
return { success: true, id: circle.id, type: 'circle' }
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function createLineElement(project, { x1, y1, x2, y2, stroke, strokeWidth }) {
|
|
271
|
+
const line = new paper.Path.Line({
|
|
272
|
+
from: [x1, y1],
|
|
273
|
+
to: [x2, y2],
|
|
274
|
+
strokeColor: new paper.Color(stroke || '#ffffff'),
|
|
275
|
+
strokeWidth: strokeWidth || 2,
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
return { success: true, id: line.id, type: 'line' }
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function createPolygonElement(project, { cx, cy, radius, sides, fill, stroke, strokeWidth, opacity }) {
|
|
282
|
+
const polygon = new paper.Path.RegularPolygon({
|
|
283
|
+
center: [cx, cy],
|
|
284
|
+
radius: radius,
|
|
285
|
+
sides: sides,
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
if (fill) polygon.fillColor = new paper.Color(fill)
|
|
289
|
+
if (stroke) {
|
|
290
|
+
polygon.strokeColor = new paper.Color(stroke)
|
|
291
|
+
polygon.strokeWidth = strokeWidth || 1
|
|
292
|
+
}
|
|
293
|
+
if (opacity !== undefined) polygon.opacity = opacity
|
|
294
|
+
|
|
295
|
+
return { success: true, id: polygon.id, type: 'polygon' }
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function createTextElement(project, { text, x, y, fontSize, fontFamily, color, align, shadow }) {
|
|
299
|
+
const { validateFont, getDefaultFont } = require('./fonts')
|
|
300
|
+
|
|
301
|
+
const textItem = new paper.PointText({
|
|
302
|
+
point: [x, y],
|
|
303
|
+
content: text,
|
|
304
|
+
fontSize: fontSize || 48,
|
|
305
|
+
fontFamily: validateFont(fontFamily) || getDefaultFont(),
|
|
306
|
+
fillColor: new paper.Color(color || '#ffffff'),
|
|
307
|
+
justification: align || 'left',
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
if (shadow) {
|
|
311
|
+
textItem.shadowColor = new paper.Color(shadow.color)
|
|
312
|
+
textItem.shadowBlur = shadow.blur || 5
|
|
313
|
+
textItem.shadowOffset = new paper.Point(shadow.offsetX || 2, shadow.offsetY || 2)
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
return { success: true, id: textItem.id, type: 'text' }
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function createArtTextElement(project, { text, x, y, fontSize, fontFamily, gradient, strokeColor, strokeWidth, shadow }) {
|
|
320
|
+
const { validateFont, getDefaultFont } = require('./fonts')
|
|
321
|
+
|
|
322
|
+
const textItem = new paper.PointText({
|
|
323
|
+
point: [x, y],
|
|
324
|
+
content: text,
|
|
325
|
+
fontSize: fontSize || 120,
|
|
326
|
+
fontFamily: validateFont(fontFamily) || getDefaultFont(),
|
|
327
|
+
fillColor: gradient ? new paper.Color(gradient.colors[0]) : new paper.Color('#ffffff'),
|
|
328
|
+
justification: 'center',
|
|
329
|
+
})
|
|
330
|
+
|
|
331
|
+
if (gradient && gradient.colors.length > 0) {
|
|
332
|
+
const colors = gradient.colors.map(c => new paper.Color(c))
|
|
333
|
+
textItem.fillColor = new paper.Color({
|
|
334
|
+
gradient: { stops: colors },
|
|
335
|
+
origin: textItem.bounds.topLeft,
|
|
336
|
+
destination: textItem.bounds.topRight,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (strokeColor) {
|
|
341
|
+
textItem.strokeColor = new paper.Color(strokeColor)
|
|
342
|
+
textItem.strokeWidth = strokeWidth || 2
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (shadow) {
|
|
346
|
+
textItem.shadowColor = new paper.Color(shadow.color)
|
|
347
|
+
textItem.shadowBlur = shadow.blur || 10
|
|
348
|
+
textItem.shadowOffset = new paper.Point(shadow.offsetX || 3, shadow.offsetY || 3)
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return { success: true, id: textItem.id, type: 'artText' }
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function createImageElement(project, { src, x, y, width, height, opacity }) {
|
|
355
|
+
const fs = require('fs')
|
|
356
|
+
const path = require('path')
|
|
357
|
+
|
|
358
|
+
// 将图片转换为 Base64 data URL
|
|
359
|
+
let imageUrl = src
|
|
360
|
+
|
|
361
|
+
// 如果是本地文件,转换为 Base64
|
|
362
|
+
if (!src.startsWith('data:') && !src.startsWith('http')) {
|
|
363
|
+
let absolutePath = src
|
|
364
|
+
if (!path.isAbsolute(absolutePath)) {
|
|
365
|
+
absolutePath = path.join(process.cwd(), absolutePath)
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (!fs.existsSync(absolutePath)) {
|
|
369
|
+
return { success: false, error: `文件不存在: ${absolutePath}` }
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
const buffer = fs.readFileSync(absolutePath)
|
|
373
|
+
const ext = path.extname(absolutePath).toLowerCase()
|
|
374
|
+
const mimeTypes = {
|
|
375
|
+
'.png': 'image/png',
|
|
376
|
+
'.jpg': 'image/jpeg',
|
|
377
|
+
'.jpeg': 'image/jpeg',
|
|
378
|
+
'.gif': 'image/gif',
|
|
379
|
+
'.webp': 'image/webp',
|
|
380
|
+
'.bmp': 'image/bmp'
|
|
381
|
+
}
|
|
382
|
+
const mimeType = mimeTypes[ext] || 'image/png'
|
|
383
|
+
imageUrl = `data:${mimeType};base64,${buffer.toString('base64')}`
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const raster = new paper.Raster(imageUrl)
|
|
387
|
+
|
|
388
|
+
raster.onLoad = () => {
|
|
389
|
+
if (width && height) {
|
|
390
|
+
raster.bounds = new paper.Rectangle(x, y, width, height)
|
|
391
|
+
} else if (width) {
|
|
392
|
+
const scale = width / raster.width
|
|
393
|
+
raster.bounds = new paper.Rectangle(x, y, width, raster.height * scale)
|
|
394
|
+
} else if (height) {
|
|
395
|
+
const scale = height / raster.height
|
|
396
|
+
raster.bounds = new paper.Rectangle(x, y, raster.width * scale, height)
|
|
397
|
+
} else {
|
|
398
|
+
raster.position = new paper.Point(x, y)
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (opacity !== undefined) raster.opacity = opacity
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
success: true,
|
|
406
|
+
id: raster.id,
|
|
407
|
+
type: 'image',
|
|
408
|
+
width: raster.width,
|
|
409
|
+
height: raster.height,
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const fs = require('fs')
|
|
414
|
+
|
|
415
|
+
function createSVGElement(project, { src, x, y, width, height, opacity }) {
|
|
416
|
+
let svgContent = src
|
|
417
|
+
|
|
418
|
+
// 如果是文件路径,读取文件内容
|
|
419
|
+
if (!src.startsWith('<') && !src.startsWith('<?xml')) {
|
|
420
|
+
try {
|
|
421
|
+
svgContent = fs.readFileSync(src, 'utf8')
|
|
422
|
+
} catch (e) {
|
|
423
|
+
return { success: false, error: `Failed to read SVG file: ${e.message}` }
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// 导入 SVG
|
|
428
|
+
const svg = project.importSVG(svgContent)
|
|
429
|
+
|
|
430
|
+
if (!svg) {
|
|
431
|
+
return { success: false, error: 'Failed to import SVG' }
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 设置位置
|
|
435
|
+
svg.position = new paper.Point(x, y)
|
|
436
|
+
|
|
437
|
+
// 设置尺寸
|
|
438
|
+
if (width && height) {
|
|
439
|
+
svg.bounds.width = width
|
|
440
|
+
svg.bounds.height = height
|
|
441
|
+
} else if (width) {
|
|
442
|
+
svg.scale(width / svg.bounds.width)
|
|
443
|
+
} else if (height) {
|
|
444
|
+
svg.scale(height / svg.bounds.height)
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
// 设置透明度
|
|
448
|
+
if (opacity !== undefined) {
|
|
449
|
+
svg.opacity = opacity
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
id: svg.id,
|
|
455
|
+
type: 'svg',
|
|
456
|
+
width: svg.bounds.width,
|
|
457
|
+
height: svg.bounds.height,
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
// ============= 高级组件创建函数 =============
|
|
462
|
+
|
|
463
|
+
function createCardComponent(project, canvas, {
|
|
464
|
+
x, y, width, height,
|
|
465
|
+
background, border, borderWidth, radius,
|
|
466
|
+
title, titleSize, titleColor,
|
|
467
|
+
subtitle, subtitleSize, subtitleColor,
|
|
468
|
+
padding = 20,
|
|
469
|
+
}) {
|
|
470
|
+
const elements = []
|
|
471
|
+
|
|
472
|
+
// 卡片背景
|
|
473
|
+
const card = new paper.Path.Rectangle({
|
|
474
|
+
point: [x, y],
|
|
475
|
+
size: [width, height],
|
|
476
|
+
radius: radius || 0,
|
|
477
|
+
})
|
|
478
|
+
card.fillColor = new paper.Color(background || '#ffffff')
|
|
479
|
+
if (border) {
|
|
480
|
+
card.strokeColor = new paper.Color(border)
|
|
481
|
+
card.strokeWidth = borderWidth || 1
|
|
482
|
+
}
|
|
483
|
+
elements.push({ type: 'rectangle', id: card.id })
|
|
484
|
+
|
|
485
|
+
// 标题
|
|
486
|
+
if (title) {
|
|
487
|
+
const titleText = new paper.PointText({
|
|
488
|
+
point: [x + padding, y + padding + (titleSize || 24)],
|
|
489
|
+
content: title,
|
|
490
|
+
fontSize: titleSize || 24,
|
|
491
|
+
fillColor: new paper.Color(titleColor || '#000000'),
|
|
492
|
+
justification: 'left',
|
|
493
|
+
})
|
|
494
|
+
elements.push({ type: 'text', id: titleText.id })
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// 副标题
|
|
498
|
+
if (subtitle) {
|
|
499
|
+
const subY = title ? y + padding + (titleSize || 24) + (subtitleSize || 16) + 10 : y + padding
|
|
500
|
+
const subtitleText = new paper.PointText({
|
|
501
|
+
point: [x + padding, subY],
|
|
502
|
+
content: subtitle,
|
|
503
|
+
fontSize: subtitleSize || 16,
|
|
504
|
+
fillColor: new paper.Color(subtitleColor || '#666666'),
|
|
505
|
+
justification: 'left',
|
|
506
|
+
})
|
|
507
|
+
elements.push({ type: 'text', id: subtitleText.id })
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return { success: true, elements, type: 'card' }
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function createBadgeComponent(project, canvas, {
|
|
514
|
+
x, y, text,
|
|
515
|
+
background = '#007bff', color = '#ffffff',
|
|
516
|
+
border, fontSize = 18, padding = 15, radius = 4,
|
|
517
|
+
}) {
|
|
518
|
+
const textWidth = text.length * fontSize * 0.6
|
|
519
|
+
const badgeWidth = textWidth + padding * 2
|
|
520
|
+
const badgeHeight = fontSize + padding * 2
|
|
521
|
+
const badgeX = x - badgeWidth / 2
|
|
522
|
+
|
|
523
|
+
const badge = new paper.Path.Rectangle({
|
|
524
|
+
point: [badgeX, y],
|
|
525
|
+
size: [badgeWidth, badgeHeight],
|
|
526
|
+
radius: radius,
|
|
527
|
+
})
|
|
528
|
+
badge.fillColor = new paper.Color(background)
|
|
529
|
+
if (border) badge.strokeColor = new paper.Color(border)
|
|
530
|
+
|
|
531
|
+
const badgeText = new paper.PointText({
|
|
532
|
+
point: [x, y + badgeHeight / 2 + fontSize / 3],
|
|
533
|
+
content: text,
|
|
534
|
+
fontSize: fontSize,
|
|
535
|
+
fillColor: new paper.Color(color),
|
|
536
|
+
justification: 'center',
|
|
537
|
+
})
|
|
538
|
+
|
|
539
|
+
return { success: true, elements: [{ type: 'rectangle', id: badge.id }, { type: 'text', id: badgeText.id }], type: 'badge' }
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
function createCTAComponent(project, canvas, {
|
|
543
|
+
x, y, text,
|
|
544
|
+
background = '#007bff', color = '#ffffff',
|
|
545
|
+
border, fontSize = 20, padding = 25, radius = 8, shadow,
|
|
546
|
+
}) {
|
|
547
|
+
const textWidth = text.length * fontSize * 0.7
|
|
548
|
+
const btnWidth = textWidth + padding * 2
|
|
549
|
+
const btnHeight = fontSize + padding * 2
|
|
550
|
+
const btnX = x - btnWidth / 2
|
|
551
|
+
|
|
552
|
+
const button = new paper.Path.Rectangle({
|
|
553
|
+
point: [btnX, y],
|
|
554
|
+
size: [btnWidth, btnHeight],
|
|
555
|
+
radius: radius,
|
|
556
|
+
})
|
|
557
|
+
button.fillColor = new paper.Color(background)
|
|
558
|
+
if (border) button.strokeColor = new paper.Color(border)
|
|
559
|
+
|
|
560
|
+
if (shadow) {
|
|
561
|
+
button.shadowColor = new paper.Color(shadow.color || 'rgba(0,0,0,0.3)')
|
|
562
|
+
button.shadowBlur = shadow.blur || 10
|
|
563
|
+
button.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const buttonText = new paper.PointText({
|
|
567
|
+
point: [x, y + btnHeight / 2 + fontSize / 3],
|
|
568
|
+
content: text,
|
|
569
|
+
fontSize: fontSize,
|
|
570
|
+
fillColor: new paper.Color(color),
|
|
571
|
+
justification: 'center',
|
|
572
|
+
})
|
|
573
|
+
|
|
574
|
+
return { success: true, elements: [{ type: 'rectangle', id: button.id }, { type: 'text', id: buttonText.id }], type: 'cta' }
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
function createFeatureComponent(project, canvas, {
|
|
578
|
+
x, y, width,
|
|
579
|
+
icon, title, description,
|
|
580
|
+
iconColor = '#007bff', titleColor = '#ffffff', descColor = '#aaaaaa',
|
|
581
|
+
iconSize = 32, titleSize = 20, descSize = 14,
|
|
582
|
+
}) {
|
|
583
|
+
const elements = []
|
|
584
|
+
const padding = 15
|
|
585
|
+
let currentY = y
|
|
586
|
+
|
|
587
|
+
if (icon) {
|
|
588
|
+
elements.push(new paper.PointText({
|
|
589
|
+
point: [x + padding, currentY + iconSize],
|
|
590
|
+
content: icon,
|
|
591
|
+
fontSize: iconSize,
|
|
592
|
+
fillColor: new paper.Color(iconColor),
|
|
593
|
+
justification: 'left',
|
|
594
|
+
}))
|
|
595
|
+
currentY += iconSize + 5
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (title) {
|
|
599
|
+
elements.push(new paper.PointText({
|
|
600
|
+
point: [x + padding, currentY + titleSize],
|
|
601
|
+
content: title,
|
|
602
|
+
fontSize: titleSize,
|
|
603
|
+
fillColor: new paper.Color(titleColor),
|
|
604
|
+
justification: 'left',
|
|
605
|
+
}))
|
|
606
|
+
currentY += titleSize + 5
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
if (description) {
|
|
610
|
+
elements.push(new paper.PointText({
|
|
611
|
+
point: [x + padding, currentY + descSize],
|
|
612
|
+
content: description,
|
|
613
|
+
fontSize: descSize,
|
|
614
|
+
fillColor: new paper.Color(descColor),
|
|
615
|
+
justification: 'left',
|
|
616
|
+
}))
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return { success: true, elements, type: 'feature' }
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function createFeatureGridComponent(project, canvas, {
|
|
623
|
+
x, y,
|
|
624
|
+
columns = 3, itemWidth = 200, itemHeight = 120, gap = 20,
|
|
625
|
+
items = [],
|
|
626
|
+
background = '#1a1a2e', borderColor = '#00d9ff', radius = 8,
|
|
627
|
+
}) {
|
|
628
|
+
const elements = []
|
|
629
|
+
|
|
630
|
+
for (let i = 0; i < items.length; i++) {
|
|
631
|
+
const item = items[i]
|
|
632
|
+
const col = i % columns
|
|
633
|
+
const row = Math.floor(i / columns)
|
|
634
|
+
const itemX = x + col * (itemWidth + gap)
|
|
635
|
+
const itemY = y + row * (itemHeight + gap)
|
|
636
|
+
|
|
637
|
+
const bg = new paper.Path.Rectangle({
|
|
638
|
+
point: [itemX, itemY],
|
|
639
|
+
size: [itemWidth, itemHeight],
|
|
640
|
+
radius: radius,
|
|
641
|
+
})
|
|
642
|
+
bg.fillColor = new paper.Color(background)
|
|
643
|
+
bg.strokeColor = new paper.Color(borderColor)
|
|
644
|
+
bg.strokeWidth = 0.5
|
|
645
|
+
bg.opacity = 0.8
|
|
646
|
+
elements.push(bg)
|
|
647
|
+
|
|
648
|
+
const padding = 15
|
|
649
|
+
let offsetY = itemY + padding
|
|
650
|
+
|
|
651
|
+
if (item.icon) {
|
|
652
|
+
elements.push(new paper.PointText({
|
|
653
|
+
point: [itemX + padding, offsetY + 24],
|
|
654
|
+
content: item.icon,
|
|
655
|
+
fontSize: 28,
|
|
656
|
+
fillColor: new paper.Color(item.iconColor || '#00ff88'),
|
|
657
|
+
justification: 'left',
|
|
658
|
+
}))
|
|
659
|
+
offsetY += 35
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (item.title) {
|
|
663
|
+
elements.push(new paper.PointText({
|
|
664
|
+
point: [itemX + padding, offsetY + 18],
|
|
665
|
+
content: item.title,
|
|
666
|
+
fontSize: 16,
|
|
667
|
+
fillColor: new paper.Color(item.titleColor || '#ffffff'),
|
|
668
|
+
justification: 'left',
|
|
669
|
+
}))
|
|
670
|
+
offsetY += 22
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
if (item.description) {
|
|
674
|
+
elements.push(new paper.PointText({
|
|
675
|
+
point: [itemX + padding, offsetY + 14],
|
|
676
|
+
content: item.description,
|
|
677
|
+
fontSize: 12,
|
|
678
|
+
fillColor: new paper.Color(item.descColor || '#888888'),
|
|
679
|
+
justification: 'left',
|
|
680
|
+
}))
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
return {
|
|
685
|
+
success: true,
|
|
686
|
+
elements,
|
|
687
|
+
type: 'featureGrid',
|
|
688
|
+
rows: Math.ceil(items.length / columns),
|
|
689
|
+
cols: columns,
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function createDividerComponent(project, canvas, {
|
|
694
|
+
x, y, width, color = '#00d9ff', thickness = 1, style = 'solid', align = 'center',
|
|
695
|
+
}) {
|
|
696
|
+
let startX = x
|
|
697
|
+
let endX = x + width
|
|
698
|
+
|
|
699
|
+
if (align === 'center') {
|
|
700
|
+
startX = x - width / 2
|
|
701
|
+
endX = x + width / 2
|
|
702
|
+
} else if (align === 'right') {
|
|
703
|
+
startX = x - width
|
|
704
|
+
endX = x
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
const line = new paper.Path.Line({
|
|
708
|
+
from: [startX, y],
|
|
709
|
+
to: [endX, y],
|
|
710
|
+
strokeColor: new paper.Color(color),
|
|
711
|
+
strokeWidth: thickness,
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
if (style === 'dashed') line.dashArray = [10, 5]
|
|
715
|
+
|
|
716
|
+
return { success: true, id: line.id, type: 'divider' }
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
module.exports = {
|
|
720
|
+
createFromConfig,
|
|
721
|
+
createComponent,
|
|
722
|
+
COMPONENT_TYPES,
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
|
|
726
|
+
// ============= 新增组件创建函数 =============
|
|
727
|
+
|
|
728
|
+
function createAvatarComponent(project, canvas, { x, y, size = 80, initials, background = '#6366f1', border, borderWidth = 0, color = '#ffffff' }) {
|
|
729
|
+
const elements = []
|
|
730
|
+
const radius = size / 2
|
|
731
|
+
|
|
732
|
+
const circle = new paper.Path.Circle({
|
|
733
|
+
center: [x, y],
|
|
734
|
+
radius: radius,
|
|
735
|
+
})
|
|
736
|
+
circle.fillColor = new paper.Color(background)
|
|
737
|
+
if (border) {
|
|
738
|
+
circle.strokeColor = new paper.Color(border)
|
|
739
|
+
circle.strokeWidth = borderWidth
|
|
740
|
+
}
|
|
741
|
+
elements.push({ type: 'circle', id: circle.id })
|
|
742
|
+
|
|
743
|
+
if (initials) {
|
|
744
|
+
const text = new paper.PointText({
|
|
745
|
+
point: [x, y + size / 6],
|
|
746
|
+
content: initials.charAt(0).toUpperCase(),
|
|
747
|
+
fontSize: size * 0.4,
|
|
748
|
+
fillColor: new paper.Color(color),
|
|
749
|
+
justification: 'center',
|
|
750
|
+
})
|
|
751
|
+
elements.push({ type: 'text', id: text.id })
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return { success: true, elements, type: 'avatar', size }
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function createProgressComponent(project, canvas, { x, y, width = 300, height = 20, value = 50, trackColor = '#e0e0e0', fillColor = '#6366f1', radius = 10, showLabel = false, label }) {
|
|
758
|
+
const elements = []
|
|
759
|
+
|
|
760
|
+
const track = new paper.Path.Rectangle({
|
|
761
|
+
point: [x, y],
|
|
762
|
+
size: [width, height],
|
|
763
|
+
radius: radius,
|
|
764
|
+
})
|
|
765
|
+
track.fillColor = new paper.Color(trackColor)
|
|
766
|
+
elements.push({ type: 'rectangle', id: track.id })
|
|
767
|
+
|
|
768
|
+
const progressWidth = (value / 100) * width
|
|
769
|
+
if (progressWidth > 0) {
|
|
770
|
+
const fill = new paper.Path.Rectangle({
|
|
771
|
+
point: [x, y],
|
|
772
|
+
size: [progressWidth, height],
|
|
773
|
+
radius: radius,
|
|
774
|
+
})
|
|
775
|
+
fill.fillColor = new paper.Color(fillColor)
|
|
776
|
+
elements.push({ type: 'rectangle', id: fill.id })
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
if (showLabel && label) {
|
|
780
|
+
const labelText = new paper.PointText({
|
|
781
|
+
point: [x + width / 2, y - 8],
|
|
782
|
+
content: label,
|
|
783
|
+
fontSize: 14,
|
|
784
|
+
fillColor: new paper.Color('#666666'),
|
|
785
|
+
justification: 'center',
|
|
786
|
+
})
|
|
787
|
+
elements.push({ type: 'text', id: labelText.id })
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
return { success: true, elements, value }
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
function createRatingComponent(project, canvas, { x, y, value = 4, max = 5, size = 24, filledColor = '#fbbf24', emptyColor = '#e5e7eb', gap = 4 }) {
|
|
794
|
+
const elements = []
|
|
795
|
+
|
|
796
|
+
for (let i = 0; i < max; i++) {
|
|
797
|
+
const starX = x + i * (size + gap)
|
|
798
|
+
const filled = i < Math.floor(value)
|
|
799
|
+
|
|
800
|
+
const star = new paper.Path.Star({
|
|
801
|
+
center: [starX + size / 2, y + size / 2],
|
|
802
|
+
points: 5,
|
|
803
|
+
radius1: size / 4,
|
|
804
|
+
radius2: size / 2,
|
|
805
|
+
})
|
|
806
|
+
star.fillColor = new paper.Color(filled ? filledColor : emptyColor)
|
|
807
|
+
elements.push({ type: 'polygon', id: star.id })
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return { success: true, elements, value }
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function createQuoteComponent(project, canvas, { x, y, width = 400, text, author, background = '#f8fafc', borderColor = '#6366f1', borderWidth = 4, padding = 20, radius = 8, textColor = '#1e293b', authorColor = '#64748b', fontSize = 18 }) {
|
|
814
|
+
const elements = []
|
|
815
|
+
const lineHeight = 22
|
|
816
|
+
|
|
817
|
+
const bg = new paper.Path.Rectangle({
|
|
818
|
+
point: [x, y],
|
|
819
|
+
size: [width, author ? 80 + fontSize * 2 : 40 + fontSize * 1.5],
|
|
820
|
+
radius: radius,
|
|
821
|
+
})
|
|
822
|
+
bg.fillColor = new paper.Color(background)
|
|
823
|
+
elements.push({ type: 'rectangle', id: bg.id })
|
|
824
|
+
|
|
825
|
+
const border = new paper.Path.Rectangle({
|
|
826
|
+
point: [x, y],
|
|
827
|
+
size: [borderWidth, author ? 80 + fontSize * 2 : 40 + fontSize * 1.5],
|
|
828
|
+
})
|
|
829
|
+
border.fillColor = new paper.Color(borderColor)
|
|
830
|
+
elements.push({ type: 'rectangle', id: border.id })
|
|
831
|
+
|
|
832
|
+
const quoteMark = new paper.PointText({
|
|
833
|
+
point: [x + padding + 10, y + padding + fontSize],
|
|
834
|
+
content: '"',
|
|
835
|
+
fontSize: fontSize * 2,
|
|
836
|
+
fillColor: new paper.Color(borderColor),
|
|
837
|
+
justification: 'left',
|
|
838
|
+
})
|
|
839
|
+
elements.push({ type: 'text', id: quoteMark.id })
|
|
840
|
+
|
|
841
|
+
const quoteText = new paper.PointText({
|
|
842
|
+
point: [x + padding + 30, y + padding + fontSize * 1.5],
|
|
843
|
+
content: text,
|
|
844
|
+
fontSize: fontSize,
|
|
845
|
+
fillColor: new paper.Color(textColor),
|
|
846
|
+
justification: 'left',
|
|
847
|
+
})
|
|
848
|
+
elements.push({ type: 'text', id: quoteText.id })
|
|
849
|
+
|
|
850
|
+
if (author) {
|
|
851
|
+
const authorText = new paper.PointText({
|
|
852
|
+
point: [x + padding, y + padding + fontSize * 2.5 + 10],
|
|
853
|
+
content: `— ${author}`,
|
|
854
|
+
fontSize: fontSize * 0.8,
|
|
855
|
+
fillColor: new paper.Color(authorColor),
|
|
856
|
+
justification: 'left',
|
|
857
|
+
})
|
|
858
|
+
elements.push({ type: 'text', id: authorText.id })
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
return { success: true, elements, type: 'quote' }
|
|
862
|
+
}
|
|
863
|
+
|
|
864
|
+
function createStatCardComponent(project, canvas, { x, y, width = 200, height = 120, label = 'Total', value = '0', change, positive = true, icon, iconColor = '#6366f1', background = '#ffffff', border = '#e5e7eb', radius = 12 }) {
|
|
865
|
+
const elements = []
|
|
866
|
+
|
|
867
|
+
const bg = new paper.Path.Rectangle({
|
|
868
|
+
point: [x, y],
|
|
869
|
+
size: [width, height],
|
|
870
|
+
radius: radius,
|
|
871
|
+
})
|
|
872
|
+
bg.fillColor = new paper.Color(background)
|
|
873
|
+
bg.strokeColor = new paper.Color(border)
|
|
874
|
+
bg.strokeWidth = 1
|
|
875
|
+
elements.push({ type: 'rectangle', id: bg.id })
|
|
876
|
+
|
|
877
|
+
if (icon) {
|
|
878
|
+
elements.push(new paper.PointText({
|
|
879
|
+
point: [x + 20, y + 35],
|
|
880
|
+
content: icon,
|
|
881
|
+
fontSize: 24,
|
|
882
|
+
fillColor: new paper.Color(iconColor),
|
|
883
|
+
justification: 'left',
|
|
884
|
+
}))
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
elements.push(new paper.PointText({
|
|
888
|
+
point: [x + 20, y + 50 + (icon ? 10 : 0)],
|
|
889
|
+
content: label,
|
|
890
|
+
fontSize: 14,
|
|
891
|
+
fillColor: new paper.Color('#64748b'),
|
|
892
|
+
justification: 'left',
|
|
893
|
+
}))
|
|
894
|
+
|
|
895
|
+
elements.push(new paper.PointText({
|
|
896
|
+
point: [x + 20, y + 75 + (icon ? 10 : 0)],
|
|
897
|
+
content: value,
|
|
898
|
+
fontSize: 28,
|
|
899
|
+
fillColor: new paper.Color('#1e293b'),
|
|
900
|
+
justification: 'left',
|
|
901
|
+
}))
|
|
902
|
+
|
|
903
|
+
if (change) {
|
|
904
|
+
const changeColor = positive ? '#22c55e' : '#ef4444'
|
|
905
|
+
const changeIcon = positive ? '↑' : '↓'
|
|
906
|
+
elements.push(new paper.PointText({
|
|
907
|
+
point: [x + 20, y + 95 + (icon ? 10 : 0)],
|
|
908
|
+
content: `${changeIcon} ${change}`,
|
|
909
|
+
fontSize: 14,
|
|
910
|
+
fillColor: new paper.Color(changeColor),
|
|
911
|
+
justification: 'left',
|
|
912
|
+
}))
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
return { success: true, elements, type: 'statCard' }
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
function createTagCloudComponent(project, canvas, { x, y, tags = [], fontSize = 14, padding = 12, gap = 10, maxWidth = 400 }) {
|
|
919
|
+
const elements = []
|
|
920
|
+
let currentX = x
|
|
921
|
+
let currentY = y
|
|
922
|
+
let rowHeight = 0
|
|
923
|
+
|
|
924
|
+
for (const tag of tags) {
|
|
925
|
+
const textWidth = tag.text.length * fontSize * 0.6
|
|
926
|
+
const tagWidth = textWidth + padding * 2
|
|
927
|
+
const tagHeight = fontSize + padding * 2
|
|
928
|
+
|
|
929
|
+
if (currentX + tagWidth > x + maxWidth && currentX > x) {
|
|
930
|
+
currentX = x
|
|
931
|
+
currentY += rowHeight + gap
|
|
932
|
+
rowHeight = 0
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const tagBg = new paper.Path.Rectangle({
|
|
936
|
+
point: [currentX, currentY],
|
|
937
|
+
size: [tagWidth, tagHeight],
|
|
938
|
+
radius: tagHeight / 2,
|
|
939
|
+
})
|
|
940
|
+
tagBg.fillColor = new paper.Color(tag.bgColor || '#e0e7ff')
|
|
941
|
+
elements.push({ type: 'rectangle', id: tagBg.id })
|
|
942
|
+
|
|
943
|
+
elements.push(new paper.PointText({
|
|
944
|
+
point: [currentX + tagWidth / 2, currentY + tagHeight / 2 + fontSize / 3],
|
|
945
|
+
content: tag.text,
|
|
946
|
+
fontSize: fontSize,
|
|
947
|
+
fillColor: new paper.Color(tag.color || '#4338ca'),
|
|
948
|
+
justification: 'center',
|
|
949
|
+
}))
|
|
950
|
+
|
|
951
|
+
currentX += tagWidth + gap
|
|
952
|
+
rowHeight = Math.max(rowHeight, tagHeight)
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
return { success: true, elements, type: 'tagCloud', height: rowHeight }
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function createStepperComponent(project, canvas, { x, y, width = 600, steps = [], currentStep = 0, activeColor = '#6366f1', inactiveColor = '#e5e7eb', completedColor = '#22c55e', circleSize = 40 }) {
|
|
959
|
+
const elements = []
|
|
960
|
+
const stepWidth = steps.length > 1 ? width / (steps.length - 1) : width
|
|
961
|
+
const lineY = y + circleSize / 2
|
|
962
|
+
|
|
963
|
+
if (steps.length > 1) {
|
|
964
|
+
elements.push({
|
|
965
|
+
type: 'line',
|
|
966
|
+
id: new paper.Path.Line({
|
|
967
|
+
from: [x + circleSize / 2, lineY],
|
|
968
|
+
to: [x + width - circleSize / 2, lineY],
|
|
969
|
+
strokeColor: new paper.Color(inactiveColor),
|
|
970
|
+
strokeWidth: 2,
|
|
971
|
+
}).id
|
|
972
|
+
})
|
|
973
|
+
}
|
|
974
|
+
|
|
975
|
+
for (let i = 0; i < steps.length; i++) {
|
|
976
|
+
const stepX = steps.length > 1 ? x + i * stepWidth : x
|
|
977
|
+
let color = inactiveColor
|
|
978
|
+
if (i < currentStep) color = completedColor
|
|
979
|
+
else if (i === currentStep) color = activeColor
|
|
980
|
+
|
|
981
|
+
const circle = new paper.Path.Circle({
|
|
982
|
+
center: [stepX + circleSize / 2, lineY],
|
|
983
|
+
radius: circleSize / 2,
|
|
984
|
+
})
|
|
985
|
+
circle.fillColor = new paper.Color(color)
|
|
986
|
+
elements.push({ type: 'circle', id: circle.id })
|
|
987
|
+
|
|
988
|
+
const icon = i < currentStep ? '✓' : String(i + 1)
|
|
989
|
+
elements.push(new paper.PointText({
|
|
990
|
+
point: [stepX + circleSize / 2, lineY + circleSize / 6],
|
|
991
|
+
content: icon,
|
|
992
|
+
fontSize: 16,
|
|
993
|
+
fillColor: new paper.Color('#ffffff'),
|
|
994
|
+
justification: 'center',
|
|
995
|
+
}))
|
|
996
|
+
|
|
997
|
+
elements.push(new paper.PointText({
|
|
998
|
+
point: [stepX + circleSize / 2, y + circleSize + 20],
|
|
999
|
+
content: steps[i].title || `Step ${i + 1}`,
|
|
1000
|
+
fontSize: 14,
|
|
1001
|
+
fillColor: new paper.Color(i <= currentStep ? '#1e293b' : '#94a3b8'),
|
|
1002
|
+
justification: 'center',
|
|
1003
|
+
}))
|
|
1004
|
+
|
|
1005
|
+
if (steps[i].description) {
|
|
1006
|
+
elements.push(new paper.PointText({
|
|
1007
|
+
point: [stepX + circleSize / 2, y + circleSize + 38],
|
|
1008
|
+
content: steps[i].description,
|
|
1009
|
+
fontSize: 11,
|
|
1010
|
+
fillColor: new paper.Color('#94a3b8'),
|
|
1011
|
+
justification: 'center',
|
|
1012
|
+
}))
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
return { success: true, elements, type: 'stepper' }
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
function createTimelineComponent(project, canvas, { x, y, width = 500, items = [], lineColor = '#e2e8f0', dotColor = '#6366f1', dotSize = 16, gap = 60 }) {
|
|
1020
|
+
const elements = []
|
|
1021
|
+
const centerX = x + 80
|
|
1022
|
+
const contentX = x + 120
|
|
1023
|
+
|
|
1024
|
+
if (items.length > 1) {
|
|
1025
|
+
elements.push({
|
|
1026
|
+
type: 'line',
|
|
1027
|
+
id: new paper.Path.Line({
|
|
1028
|
+
from: [centerX, y + dotSize / 2],
|
|
1029
|
+
to: [centerX, y + (items.length - 1) * gap + dotSize / 2],
|
|
1030
|
+
strokeColor: new paper.Color(lineColor),
|
|
1031
|
+
strokeWidth: 2,
|
|
1032
|
+
}).id
|
|
1033
|
+
})
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
for (let i = 0; i < items.length; i++) {
|
|
1037
|
+
const itemY = y + i * gap
|
|
1038
|
+
const isActive = items[i].active !== false
|
|
1039
|
+
|
|
1040
|
+
const dot = new paper.Path.Circle({
|
|
1041
|
+
center: [centerX, itemY + dotSize / 2],
|
|
1042
|
+
radius: dotSize / 2,
|
|
1043
|
+
})
|
|
1044
|
+
dot.fillColor = new paper.Color(isActive ? dotColor : lineColor)
|
|
1045
|
+
elements.push({ type: 'circle', id: dot.id })
|
|
1046
|
+
|
|
1047
|
+
if (items[i].date) {
|
|
1048
|
+
elements.push(new paper.PointText({
|
|
1049
|
+
point: [x + 10, itemY + dotSize / 2 + 5],
|
|
1050
|
+
content: items[i].date,
|
|
1051
|
+
fontSize: 12,
|
|
1052
|
+
fillColor: new paper.Color('#94a3b8'),
|
|
1053
|
+
justification: 'left',
|
|
1054
|
+
}))
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
elements.push(new paper.PointText({
|
|
1058
|
+
point: [contentX, itemY + dotSize / 2 + 5],
|
|
1059
|
+
content: items[i].title || `Event ${i + 1}`,
|
|
1060
|
+
fontSize: 16,
|
|
1061
|
+
fillColor: new paper.Color(isActive ? '#1e293b' : '#94a3b8'),
|
|
1062
|
+
justification: 'left',
|
|
1063
|
+
}))
|
|
1064
|
+
|
|
1065
|
+
if (items[i].description) {
|
|
1066
|
+
elements.push(new paper.PointText({
|
|
1067
|
+
point: [contentX, itemY + dotSize / 2 + 28],
|
|
1068
|
+
content: items[i].description,
|
|
1069
|
+
fontSize: 13,
|
|
1070
|
+
fillColor: new paper.Color('#64748b'),
|
|
1071
|
+
justification: 'left',
|
|
1072
|
+
}))
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
return { success: true, elements, type: 'timeline', height: items.length * gap }
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
function createListItemComponent(project, canvas, { x, y, width = 400, icon = '→', title, description, badge, badgeColor = '#6366f1', iconColor = '#6366f1', background = '#ffffff', borderColor = '#e5e7eb', height = 60, radius = 8 }) {
|
|
1080
|
+
const elements = []
|
|
1081
|
+
|
|
1082
|
+
const bg = new paper.Path.Rectangle({
|
|
1083
|
+
point: [x, y],
|
|
1084
|
+
size: [width, height],
|
|
1085
|
+
radius: radius,
|
|
1086
|
+
})
|
|
1087
|
+
bg.fillColor = new paper.Color(background)
|
|
1088
|
+
bg.strokeColor = new paper.Color(borderColor)
|
|
1089
|
+
bg.strokeWidth = 1
|
|
1090
|
+
elements.push({ type: 'rectangle', id: bg.id })
|
|
1091
|
+
|
|
1092
|
+
elements.push(new paper.PointText({
|
|
1093
|
+
point: [x + 15, y + height / 2 + 6],
|
|
1094
|
+
content: icon,
|
|
1095
|
+
fontSize: 20,
|
|
1096
|
+
fillColor: new paper.Color(iconColor),
|
|
1097
|
+
justification: 'center',
|
|
1098
|
+
}))
|
|
1099
|
+
|
|
1100
|
+
elements.push(new paper.PointText({
|
|
1101
|
+
point: [x + 50, y + height / 2 - 5],
|
|
1102
|
+
content: title || 'List Item',
|
|
1103
|
+
fontSize: 16,
|
|
1104
|
+
fillColor: new paper.Color('#1e293b'),
|
|
1105
|
+
justification: 'left',
|
|
1106
|
+
}))
|
|
1107
|
+
|
|
1108
|
+
if (description) {
|
|
1109
|
+
elements.push(new paper.PointText({
|
|
1110
|
+
point: [x + 50, y + height / 2 + 15],
|
|
1111
|
+
content: description,
|
|
1112
|
+
fontSize: 12,
|
|
1113
|
+
fillColor: new paper.Color('#64748b'),
|
|
1114
|
+
justification: 'left',
|
|
1115
|
+
}))
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (badge) {
|
|
1119
|
+
const badgeWidth = badge.length * 10 + 20
|
|
1120
|
+
const badgeX = x + width - badgeWidth - 15
|
|
1121
|
+
const badgeY = y + (height - 24) / 2
|
|
1122
|
+
|
|
1123
|
+
elements.push({
|
|
1124
|
+
type: 'rectangle',
|
|
1125
|
+
id: new paper.Path.Rectangle({
|
|
1126
|
+
point: [badgeX, badgeY],
|
|
1127
|
+
size: [badgeWidth, 24],
|
|
1128
|
+
radius: 12,
|
|
1129
|
+
}).id
|
|
1130
|
+
})
|
|
1131
|
+
|
|
1132
|
+
elements.push(new paper.PointText({
|
|
1133
|
+
point: [badgeX + badgeWidth / 2, badgeY + 16],
|
|
1134
|
+
content: badge,
|
|
1135
|
+
fontSize: 12,
|
|
1136
|
+
fillColor: new paper.Color('#ffffff'),
|
|
1137
|
+
justification: 'center',
|
|
1138
|
+
}))
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
return { success: true, elements, type: 'listItem' }
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function createNotificationComponent(project, canvas, { x, y, width = 360, type = 'info', title, message, showIcon = true, radius = 12 }) {
|
|
1145
|
+
const config = {
|
|
1146
|
+
success: { icon: '✓', bgColor: '#dcfce7', iconColor: '#22c55e', borderColor: '#22c55e' },
|
|
1147
|
+
warning: { icon: '⚠', bgColor: '#fef9c3', iconColor: '#eab308', borderColor: '#eab308' },
|
|
1148
|
+
error: { icon: '✕', bgColor: '#fee2e2', iconColor: '#ef4444', borderColor: '#ef4444' },
|
|
1149
|
+
info: { icon: 'ℹ', bgColor: '#dbeafe', iconColor: '#3b82f6', borderColor: '#3b82f6' },
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
const c = config[type] || config.info
|
|
1153
|
+
const padding = 16
|
|
1154
|
+
const lineHeight = 22
|
|
1155
|
+
const iconSize = 24
|
|
1156
|
+
const height = padding * 2 + (title ? lineHeight + 8 : 0) + (message ? lineHeight : 0)
|
|
1157
|
+
const elements = []
|
|
1158
|
+
|
|
1159
|
+
const bg = new paper.Path.Rectangle({
|
|
1160
|
+
point: [x, y],
|
|
1161
|
+
size: [width, height],
|
|
1162
|
+
radius: radius,
|
|
1163
|
+
})
|
|
1164
|
+
bg.fillColor = new paper.Color(c.bgColor)
|
|
1165
|
+
bg.strokeColor = new paper.Color(c.borderColor)
|
|
1166
|
+
bg.strokeWidth = 1
|
|
1167
|
+
elements.push({ type: 'rectangle', id: bg.id })
|
|
1168
|
+
|
|
1169
|
+
if (showIcon) {
|
|
1170
|
+
elements.push(new paper.PointText({
|
|
1171
|
+
point: [x + padding + iconSize / 2, y + padding + iconSize / 2 + 6],
|
|
1172
|
+
content: c.icon,
|
|
1173
|
+
fontSize: iconSize,
|
|
1174
|
+
fillColor: new paper.Color(c.iconColor),
|
|
1175
|
+
justification: 'center',
|
|
1176
|
+
}))
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
const textX = showIcon ? x + padding + iconSize + 12 : x + padding
|
|
1180
|
+
let currentY = y + padding
|
|
1181
|
+
|
|
1182
|
+
if (title) {
|
|
1183
|
+
elements.push(new paper.PointText({
|
|
1184
|
+
point: [textX, currentY + 18],
|
|
1185
|
+
content: title,
|
|
1186
|
+
fontSize: 16,
|
|
1187
|
+
fillColor: new paper.Color('#1e293b'),
|
|
1188
|
+
justification: 'left',
|
|
1189
|
+
}))
|
|
1190
|
+
currentY += lineHeight + 8
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
if (message) {
|
|
1194
|
+
elements.push(new paper.PointText({
|
|
1195
|
+
point: [textX, currentY + 16],
|
|
1196
|
+
content: message,
|
|
1197
|
+
fontSize: 14,
|
|
1198
|
+
fillColor: new paper.Color('#475569'),
|
|
1199
|
+
justification: 'left',
|
|
1200
|
+
}))
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
return { success: true, elements, type: 'notification' }
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
/**
|
|
1207
|
+
* 创建图片框组件
|
|
1208
|
+
*/
|
|
1209
|
+
async function createImageFrameComponent(project, canvas, {
|
|
1210
|
+
src,
|
|
1211
|
+
x,
|
|
1212
|
+
y,
|
|
1213
|
+
width,
|
|
1214
|
+
height,
|
|
1215
|
+
borderColor = '#ffffff',
|
|
1216
|
+
borderWidth = 3,
|
|
1217
|
+
outerColor = '#1a1a2e',
|
|
1218
|
+
outerWidth = 6,
|
|
1219
|
+
shadowBlur = 0,
|
|
1220
|
+
shadowOffsetX = 0,
|
|
1221
|
+
shadowOffsetY = 0,
|
|
1222
|
+
shadowColor = 'rgba(0,0,0,0.3)',
|
|
1223
|
+
radius = 0,
|
|
1224
|
+
overlayColor,
|
|
1225
|
+
overlayOpacity = 0,
|
|
1226
|
+
fit = 'cover'
|
|
1227
|
+
}) {
|
|
1228
|
+
const elements = []
|
|
1229
|
+
|
|
1230
|
+
// 绘制外边框(装饰层)
|
|
1231
|
+
if (outerWidth > 0) {
|
|
1232
|
+
const outerBg = new paper.Path.Rectangle({
|
|
1233
|
+
point: [x - outerWidth, y - outerWidth],
|
|
1234
|
+
size: [width + outerWidth * 2, height + outerWidth * 2],
|
|
1235
|
+
radius: radius + outerWidth
|
|
1236
|
+
})
|
|
1237
|
+
outerBg.fillColor = new paper.Color(outerColor)
|
|
1238
|
+
elements.push({ type: 'rectangle', id: outerBg.id })
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
// 绘制内边框
|
|
1242
|
+
if (borderWidth > 0) {
|
|
1243
|
+
const innerBg = new paper.Path.Rectangle({
|
|
1244
|
+
point: [x - borderWidth, y - borderWidth],
|
|
1245
|
+
size: [width + borderWidth * 2, height + borderWidth * 2],
|
|
1246
|
+
radius: radius + borderWidth
|
|
1247
|
+
})
|
|
1248
|
+
innerBg.fillColor = new paper.Color(borderColor)
|
|
1249
|
+
elements.push({ type: 'rectangle', id: innerBg.id })
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
// 加载并绘制图片
|
|
1253
|
+
const image = await _loadImage(src)
|
|
1254
|
+
const imgWidth = image.width
|
|
1255
|
+
const imgHeight = image.height
|
|
1256
|
+
const imgRatio = imgWidth / imgHeight
|
|
1257
|
+
const boxRatio = width / height
|
|
1258
|
+
|
|
1259
|
+
let drawX = x, drawY = y, drawW = width, drawH = height
|
|
1260
|
+
|
|
1261
|
+
if (fit === 'cover') {
|
|
1262
|
+
if (imgRatio > boxRatio) {
|
|
1263
|
+
drawH = height
|
|
1264
|
+
drawW = height * imgRatio
|
|
1265
|
+
drawX = x - (drawW - width) / 2
|
|
1266
|
+
} else {
|
|
1267
|
+
drawW = width
|
|
1268
|
+
drawH = width / imgRatio
|
|
1269
|
+
drawY = y - (drawH - height) / 2
|
|
1270
|
+
}
|
|
1271
|
+
} else if (fit === 'contain') {
|
|
1272
|
+
if (imgRatio > boxRatio) {
|
|
1273
|
+
drawW = width
|
|
1274
|
+
drawH = width / imgRatio
|
|
1275
|
+
drawY = y + (height - drawH) / 2
|
|
1276
|
+
} else {
|
|
1277
|
+
drawH = height
|
|
1278
|
+
drawW = height * imgRatio
|
|
1279
|
+
drawX = x + (width - drawW) / 2
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
// 创建裁剪区域
|
|
1284
|
+
const clipPath = new paper.Path.Rectangle({
|
|
1285
|
+
point: [x, y],
|
|
1286
|
+
size: [width, height],
|
|
1287
|
+
radius: radius
|
|
1288
|
+
})
|
|
1289
|
+
|
|
1290
|
+
// 添加阴影
|
|
1291
|
+
if (shadowBlur > 0) {
|
|
1292
|
+
const shadowRect = new paper.Path.Rectangle({
|
|
1293
|
+
point: [x + shadowOffsetX, y + shadowOffsetY],
|
|
1294
|
+
size: [width, height],
|
|
1295
|
+
radius: radius
|
|
1296
|
+
})
|
|
1297
|
+
shadowRect.fillColor = new paper.Color(shadowColor)
|
|
1298
|
+
shadowRect.opacity = shadowBlur / 50
|
|
1299
|
+
shadowRect.shadowColor = new paper.Color(shadowColor)
|
|
1300
|
+
shadowRect.shadowBlur = shadowBlur
|
|
1301
|
+
elements.push({ type: 'rectangle', id: shadowRect.id })
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
// 绘制图片
|
|
1305
|
+
const raster = new paper.Raster({
|
|
1306
|
+
source: src,
|
|
1307
|
+
position: [drawX + drawW / 2, drawY + drawH / 2]
|
|
1308
|
+
})
|
|
1309
|
+
|
|
1310
|
+
await new Promise((resolve) => {
|
|
1311
|
+
raster.onLoad = resolve
|
|
1312
|
+
})
|
|
1313
|
+
|
|
1314
|
+
raster.size = new paper.Size(drawW, drawH)
|
|
1315
|
+
raster.position = new paper.Point(drawX + drawW / 2, drawY + drawH / 2)
|
|
1316
|
+
|
|
1317
|
+
// 应用裁剪
|
|
1318
|
+
clipPath.clipMask = true
|
|
1319
|
+
|
|
1320
|
+
elements.push({ type: 'raster', id: raster.id })
|
|
1321
|
+
|
|
1322
|
+
// 叠加颜色
|
|
1323
|
+
if (overlayColor && overlayOpacity > 0) {
|
|
1324
|
+
const overlay = new paper.Path.Rectangle({
|
|
1325
|
+
point: [x, y],
|
|
1326
|
+
size: [width, height],
|
|
1327
|
+
radius: radius
|
|
1328
|
+
})
|
|
1329
|
+
overlay.fillColor = new paper.Color(overlayColor)
|
|
1330
|
+
overlay.opacity = overlayOpacity
|
|
1331
|
+
elements.push({ type: 'rectangle', id: overlay.id })
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
return { success: true, elements, type: 'imageFrame' }
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// 辅助函数:加载图片
|
|
1338
|
+
async function _loadImage(src) {
|
|
1339
|
+
return new Promise((resolve, reject) => {
|
|
1340
|
+
const img = new Image()
|
|
1341
|
+
img.crossOrigin = 'anonymous'
|
|
1342
|
+
img.onload = () => resolve(img)
|
|
1343
|
+
img.onerror = () => {
|
|
1344
|
+
if (src.startsWith('data:')) {
|
|
1345
|
+
img.src = src
|
|
1346
|
+
} else {
|
|
1347
|
+
reject(new Error(`Failed to load image: ${src}`))
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
img.src = src
|
|
1351
|
+
})
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
/**
|
|
1355
|
+
* 创建分栏布局组件
|
|
1356
|
+
*/
|
|
1357
|
+
function createColumnsComponent(project, canvas, {
|
|
1358
|
+
x,
|
|
1359
|
+
y,
|
|
1360
|
+
width,
|
|
1361
|
+
height,
|
|
1362
|
+
columns = 2,
|
|
1363
|
+
gap = 20,
|
|
1364
|
+
background,
|
|
1365
|
+
borderColor,
|
|
1366
|
+
borderWidth = 1,
|
|
1367
|
+
radius = 0,
|
|
1368
|
+
direction = 'horizontal',
|
|
1369
|
+
align = 'top'
|
|
1370
|
+
}) {
|
|
1371
|
+
const elements = []
|
|
1372
|
+
|
|
1373
|
+
// 计算每列宽度
|
|
1374
|
+
const totalGap = gap * (columns - 1)
|
|
1375
|
+
const columnWidth = (width - totalGap) / columns
|
|
1376
|
+
|
|
1377
|
+
// 绘制背景
|
|
1378
|
+
if (background) {
|
|
1379
|
+
const bg = new paper.Path.Rectangle({
|
|
1380
|
+
point: [x, y],
|
|
1381
|
+
size: [width, height],
|
|
1382
|
+
radius: radius
|
|
1383
|
+
})
|
|
1384
|
+
bg.fillColor = new paper.Color(background)
|
|
1385
|
+
elements.push({ type: 'rectangle', id: bg.id })
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1388
|
+
// 绘制边框
|
|
1389
|
+
if (borderColor && borderWidth > 0) {
|
|
1390
|
+
const border = new paper.Path.Rectangle({
|
|
1391
|
+
point: [x, y],
|
|
1392
|
+
size: [width, height],
|
|
1393
|
+
radius: radius
|
|
1394
|
+
})
|
|
1395
|
+
border.fillColor = new paper.Color('transparent')
|
|
1396
|
+
border.strokeColor = new paper.Color(borderColor)
|
|
1397
|
+
border.strokeWidth = borderWidth
|
|
1398
|
+
elements.push({ type: 'rectangle', id: border.id })
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1401
|
+
// 生成分割线
|
|
1402
|
+
for (let i = 1; i < columns; i++) {
|
|
1403
|
+
const lineX = x + columnWidth * i + gap * (i - 1) + gap / 2
|
|
1404
|
+
const line = new paper.Path.Line({
|
|
1405
|
+
from: [lineX, y + 20],
|
|
1406
|
+
to: [lineX, y + height - 20]
|
|
1407
|
+
})
|
|
1408
|
+
line.strokeColor = new paper.Color('#e0e0e0')
|
|
1409
|
+
line.strokeWidth = 1
|
|
1410
|
+
elements.push({ type: 'line', id: line.id })
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
// 返回列位置信息
|
|
1414
|
+
const columnPositions = []
|
|
1415
|
+
for (let i = 0; i < columns; i++) {
|
|
1416
|
+
const colX = x + (columnWidth + gap) * i
|
|
1417
|
+
const colY = align === 'center' ? y + (height - height) / 2 : align === 'bottom' ? y + height - height : y
|
|
1418
|
+
|
|
1419
|
+
columnPositions.push({
|
|
1420
|
+
index: i,
|
|
1421
|
+
x: colX,
|
|
1422
|
+
y: colY,
|
|
1423
|
+
width: columnWidth,
|
|
1424
|
+
height: height,
|
|
1425
|
+
centerX: colX + columnWidth / 2,
|
|
1426
|
+
centerY: colY + height / 2
|
|
1427
|
+
})
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
return {
|
|
1431
|
+
success: true,
|
|
1432
|
+
elements,
|
|
1433
|
+
columnPositions,
|
|
1434
|
+
columnWidth,
|
|
1435
|
+
totalWidth: width,
|
|
1436
|
+
totalHeight: height,
|
|
1437
|
+
type: 'columns'
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
/**
|
|
1442
|
+
* 创建网格布局组件
|
|
1443
|
+
*/
|
|
1444
|
+
function createGridComponent(project, canvas, {
|
|
1445
|
+
x,
|
|
1446
|
+
y,
|
|
1447
|
+
width,
|
|
1448
|
+
height,
|
|
1449
|
+
columns = 3,
|
|
1450
|
+
rows = 2,
|
|
1451
|
+
gapX = 20,
|
|
1452
|
+
gapY = 20,
|
|
1453
|
+
background,
|
|
1454
|
+
borderColor,
|
|
1455
|
+
borderWidth = 1,
|
|
1456
|
+
radius = 0,
|
|
1457
|
+
direction = 'row'
|
|
1458
|
+
}) {
|
|
1459
|
+
const elements = []
|
|
1460
|
+
|
|
1461
|
+
// 计算单元格尺寸
|
|
1462
|
+
const totalGapX = gapX * (columns - 1)
|
|
1463
|
+
const totalGapY = gapY * (rows - 1)
|
|
1464
|
+
const cellWidth = (width - totalGapX) / columns
|
|
1465
|
+
const cellHeight = (height - totalGapY) / rows
|
|
1466
|
+
|
|
1467
|
+
// 绘制背景
|
|
1468
|
+
if (background) {
|
|
1469
|
+
const bg = new paper.Path.Rectangle({
|
|
1470
|
+
point: [x, y],
|
|
1471
|
+
size: [width, height],
|
|
1472
|
+
radius: radius
|
|
1473
|
+
})
|
|
1474
|
+
bg.fillColor = new paper.Color(background)
|
|
1475
|
+
elements.push({ type: 'rectangle', id: bg.id })
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
// 绘制边框
|
|
1479
|
+
if (borderColor && borderWidth > 0) {
|
|
1480
|
+
const border = new paper.Path.Rectangle({
|
|
1481
|
+
point: [x, y],
|
|
1482
|
+
size: [width, height],
|
|
1483
|
+
radius: radius
|
|
1484
|
+
})
|
|
1485
|
+
border.fillColor = new paper.Color('transparent')
|
|
1486
|
+
border.strokeColor = new paper.Color(borderColor)
|
|
1487
|
+
border.strokeWidth = borderWidth
|
|
1488
|
+
elements.push({ type: 'rectangle', id: border.id })
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
// 生成网格线(可选,这里不绘制让用户自己控制)
|
|
1492
|
+
|
|
1493
|
+
// 生成网格位置信息
|
|
1494
|
+
const cellPositions = []
|
|
1495
|
+
const totalCells = columns * rows
|
|
1496
|
+
|
|
1497
|
+
for (let i = 0; i < totalCells; i++) {
|
|
1498
|
+
let col, row
|
|
1499
|
+
|
|
1500
|
+
if (direction === 'row') {
|
|
1501
|
+
col = i % columns
|
|
1502
|
+
row = Math.floor(i / columns)
|
|
1503
|
+
} else {
|
|
1504
|
+
row = i % rows
|
|
1505
|
+
col = Math.floor(i / rows)
|
|
1506
|
+
}
|
|
1507
|
+
|
|
1508
|
+
const cellX = x + col * (cellWidth + gapX)
|
|
1509
|
+
const cellY = y + row * (cellHeight + gapY)
|
|
1510
|
+
|
|
1511
|
+
cellPositions.push({
|
|
1512
|
+
index: i,
|
|
1513
|
+
column: col,
|
|
1514
|
+
row: row,
|
|
1515
|
+
x: cellX,
|
|
1516
|
+
y: cellY,
|
|
1517
|
+
width: cellWidth,
|
|
1518
|
+
height: cellHeight,
|
|
1519
|
+
centerX: cellX + cellWidth / 2,
|
|
1520
|
+
centerY: cellY + cellHeight / 2
|
|
1521
|
+
})
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
// 返回网格布局信息
|
|
1525
|
+
return {
|
|
1526
|
+
success: true,
|
|
1527
|
+
elements,
|
|
1528
|
+
cellPositions,
|
|
1529
|
+
cellWidth,
|
|
1530
|
+
cellHeight,
|
|
1531
|
+
columns,
|
|
1532
|
+
rows,
|
|
1533
|
+
totalCells,
|
|
1534
|
+
totalWidth: width,
|
|
1535
|
+
totalHeight: height,
|
|
1536
|
+
type: 'grid'
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
|
|
1541
|
+
/**
|
|
1542
|
+
* 创建星形组件
|
|
1543
|
+
*/
|
|
1544
|
+
function createStarComponent(project, canvas, {
|
|
1545
|
+
cx, cy, points = 5, innerRadius: providedInnerRadius, outerRadius,
|
|
1546
|
+
fill, stroke, strokeWidth = 1, opacity = 1, rotation = 0
|
|
1547
|
+
}) {
|
|
1548
|
+
const innerRadius = providedInnerRadius || outerRadius * 0.4
|
|
1549
|
+
const path = new paper.Path()
|
|
1550
|
+
const angleStep = Math.PI / points
|
|
1551
|
+
|
|
1552
|
+
for (let i = 0; i < points * 2; i++) {
|
|
1553
|
+
const radius = i % 2 === 0 ? outerRadius : innerRadius
|
|
1554
|
+
const angle = i * angleStep - Math.PI / 2 + (rotation * Math.PI / 180)
|
|
1555
|
+
const x = cx + radius * Math.cos(angle)
|
|
1556
|
+
const y = cy + radius * Math.sin(angle)
|
|
1557
|
+
if (i === 0) path.moveTo(x, y)
|
|
1558
|
+
else path.lineTo(x, y)
|
|
1559
|
+
}
|
|
1560
|
+
path.closePath()
|
|
1561
|
+
|
|
1562
|
+
if (fill) path.fillColor = new paper.Color(fill)
|
|
1563
|
+
if (stroke) { path.strokeColor = new paper.Color(stroke); path.strokeWidth = strokeWidth }
|
|
1564
|
+
path.opacity = opacity
|
|
1565
|
+
if (project && project.activeLayer) project.activeLayer.addChild(path)
|
|
1566
|
+
|
|
1567
|
+
return { success: true, elements: [{ type: 'path', id: path.id }], type: 'star' }
|
|
1568
|
+
}
|
|
1569
|
+
|
|
1570
|
+
/**
|
|
1571
|
+
* 创建箭头组件
|
|
1572
|
+
*/
|
|
1573
|
+
function createArrowComponent(project, canvas, {
|
|
1574
|
+
x1, y1, x2, y2, color = '#333333', strokeWidth = 2, headSize = 12, style = 'solid', direction = 'end'
|
|
1575
|
+
}) {
|
|
1576
|
+
const elements = []
|
|
1577
|
+
const angle = Math.atan2(y2 - y1, x2 - x1)
|
|
1578
|
+
|
|
1579
|
+
const line = new paper.Path.Line({ from: [x1, y1], to: [x2, y2] })
|
|
1580
|
+
line.strokeColor = new paper.Color(color)
|
|
1581
|
+
line.strokeWidth = strokeWidth
|
|
1582
|
+
if (style === 'dashed') line.dashArray = [10, 5]
|
|
1583
|
+
elements.push({ type: 'line', id: line.id })
|
|
1584
|
+
|
|
1585
|
+
if (direction === 'end' || direction === 'both') {
|
|
1586
|
+
const arrowHead = new paper.Path()
|
|
1587
|
+
arrowHead.moveTo(x2, y2)
|
|
1588
|
+
arrowHead.lineTo(x2 + headSize * Math.cos(angle + Math.PI * 0.8), y2 + headSize * Math.sin(angle + Math.PI * 0.8))
|
|
1589
|
+
arrowHead.moveTo(x2, y2)
|
|
1590
|
+
arrowHead.lineTo(x2 + headSize * Math.cos(angle - Math.PI * 0.8), y2 + headSize * Math.sin(angle - Math.PI * 0.8))
|
|
1591
|
+
arrowHead.strokeColor = new paper.Color(color)
|
|
1592
|
+
arrowHead.strokeWidth = strokeWidth
|
|
1593
|
+
arrowHead.strokeCap = 'round'
|
|
1594
|
+
elements.push({ type: 'path', id: arrowHead.id })
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
if (direction === 'start' || direction === 'both') {
|
|
1598
|
+
const startAngle = angle + Math.PI
|
|
1599
|
+
const arrowHead = new paper.Path()
|
|
1600
|
+
arrowHead.moveTo(x1, y1)
|
|
1601
|
+
arrowHead.lineTo(x1 + headSize * Math.cos(startAngle + Math.PI * 0.8), y1 + headSize * Math.sin(startAngle + Math.PI * 0.8))
|
|
1602
|
+
arrowHead.moveTo(x1, y1)
|
|
1603
|
+
arrowHead.lineTo(x1 + headSize * Math.cos(startAngle - Math.PI * 0.8), y1 + headSize * Math.sin(startAngle - Math.PI * 0.8))
|
|
1604
|
+
arrowHead.strokeColor = new paper.Color(color)
|
|
1605
|
+
arrowHead.strokeWidth = strokeWidth
|
|
1606
|
+
arrowHead.strokeCap = 'round'
|
|
1607
|
+
elements.push({ type: 'path', id: arrowHead.id })
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
return { success: true, elements, type: 'arrow' }
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
/**
|
|
1614
|
+
* 创建环形进度条组件
|
|
1615
|
+
*/
|
|
1616
|
+
function createProgressCircleComponent(project, canvas, {
|
|
1617
|
+
cx, cy, radius, value, strokeWidth = 10, trackColor = '#e0e0e0',
|
|
1618
|
+
fillColor = '#3b82f6', backgroundColor, showLabel = true, labelColor, startAngle = -90
|
|
1619
|
+
}) {
|
|
1620
|
+
const elements = []
|
|
1621
|
+
|
|
1622
|
+
if (backgroundColor) {
|
|
1623
|
+
const bgCircle = new paper.Path.Circle({ center: [cx, cy], radius: radius })
|
|
1624
|
+
bgCircle.fillColor = new paper.Color(backgroundColor)
|
|
1625
|
+
elements.push({ type: 'path', id: bgCircle.id })
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
const trackCircle = new paper.Path.Circle({ center: [cx, cy], radius: radius })
|
|
1629
|
+
trackCircle.fillColor = new paper.Color('transparent')
|
|
1630
|
+
trackCircle.strokeColor = new paper.Color(trackColor)
|
|
1631
|
+
trackCircle.strokeWidth = strokeWidth
|
|
1632
|
+
elements.push({ type: 'path', id: trackCircle.id })
|
|
1633
|
+
|
|
1634
|
+
if (value > 0) {
|
|
1635
|
+
const endAngle = startAngle + (value / 100) * 360
|
|
1636
|
+
const startRad = startAngle * Math.PI / 180
|
|
1637
|
+
const endRad = endAngle * Math.PI / 180
|
|
1638
|
+
|
|
1639
|
+
const arc = new paper.Path()
|
|
1640
|
+
arc.moveTo(cx + radius * Math.cos(startRad), cy + radius * Math.sin(startRad))
|
|
1641
|
+
arc.arcTo([cx, cy], radius, endRad - startRad)
|
|
1642
|
+
arc.strokeColor = new paper.Color(fillColor)
|
|
1643
|
+
arc.strokeWidth = strokeWidth
|
|
1644
|
+
arc.strokeCap = 'round'
|
|
1645
|
+
elements.push({ type: 'path', id: arc.id })
|
|
1646
|
+
}
|
|
1647
|
+
|
|
1648
|
+
if (showLabel) {
|
|
1649
|
+
const textColor = labelColor || fillColor
|
|
1650
|
+
const label = new paper.PointText({
|
|
1651
|
+
point: [cx, cy + 6],
|
|
1652
|
+
content: `${Math.round(value)}%`,
|
|
1653
|
+
fontSize: radius * 0.4,
|
|
1654
|
+
fillColor: new paper.Color(textColor),
|
|
1655
|
+
justification: 'center',
|
|
1656
|
+
fontWeight: 'bold'
|
|
1657
|
+
})
|
|
1658
|
+
elements.push({ type: 'text', id: label.id })
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
return { success: true, elements, type: 'progressCircle' }
|
|
1662
|
+
}
|
|
1663
|
+
|
|
1664
|
+
/**
|
|
1665
|
+
* 创建 Chip 标签组件
|
|
1666
|
+
*/
|
|
1667
|
+
function createChipComponent(project, canvas, {
|
|
1668
|
+
x, y, text, background = '#e0e0e0', color = '#333333', borderColor,
|
|
1669
|
+
fontSize = 12, padding = 12, radius = 16, icon
|
|
1670
|
+
}) {
|
|
1671
|
+
const elements = []
|
|
1672
|
+
const textWidth = text.length * fontSize * 0.6
|
|
1673
|
+
const iconWidth = icon ? fontSize : 0
|
|
1674
|
+
const totalWidth = padding * 2 + textWidth + iconWidth + 4
|
|
1675
|
+
const height = fontSize + padding * 2
|
|
1676
|
+
const rectX = x - totalWidth / 2
|
|
1677
|
+
const rectY = y - height / 2
|
|
1678
|
+
|
|
1679
|
+
const bg = new paper.Path.Rectangle({ point: [rectX, rectY], size: [totalWidth, height], radius: radius })
|
|
1680
|
+
bg.fillColor = new paper.Color(background)
|
|
1681
|
+
if (borderColor) { bg.strokeColor = new paper.Color(borderColor); bg.strokeWidth = 1 }
|
|
1682
|
+
elements.push({ type: 'path', id: bg.id })
|
|
1683
|
+
|
|
1684
|
+
if (icon) {
|
|
1685
|
+
const iconText = new paper.PointText({
|
|
1686
|
+
point: [rectX + padding + iconWidth / 2, y + fontSize / 3],
|
|
1687
|
+
content: icon,
|
|
1688
|
+
fontSize: fontSize + 2,
|
|
1689
|
+
fillColor: new paper.Color(color),
|
|
1690
|
+
justification: 'center'
|
|
1691
|
+
})
|
|
1692
|
+
elements.push({ type: 'text', id: iconText.id })
|
|
1693
|
+
}
|
|
1694
|
+
|
|
1695
|
+
const textX = icon ? rectX + padding + iconWidth + 4 + textWidth / 2 : x
|
|
1696
|
+
const label = new paper.PointText({
|
|
1697
|
+
point: [textX, y + fontSize / 3],
|
|
1698
|
+
content: text,
|
|
1699
|
+
fontSize: fontSize,
|
|
1700
|
+
fillColor: new paper.Color(color),
|
|
1701
|
+
justification: 'center'
|
|
1702
|
+
})
|
|
1703
|
+
elements.push({ type: 'text', id: label.id })
|
|
1704
|
+
|
|
1705
|
+
return { success: true, elements, width: totalWidth, height, type: 'chip' }
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
/**
|
|
1709
|
+
* 创建图表组件
|
|
1710
|
+
*/
|
|
1711
|
+
function createChartComponent(project, canvas, {
|
|
1712
|
+
type = 'bar', x, y, width, height, data = [], barColor = '#3b82f6',
|
|
1713
|
+
showLabels = true, showValues = true, barGap = 4
|
|
1714
|
+
}) {
|
|
1715
|
+
const elements = []
|
|
1716
|
+
|
|
1717
|
+
if (type === 'bar' && data.length > 0) {
|
|
1718
|
+
const maxValue = Math.max(...data.map(d => d.value))
|
|
1719
|
+
const barCount = data.length
|
|
1720
|
+
const totalGap = barGap * (barCount - 1)
|
|
1721
|
+
const barWidth = (width - totalGap) / barCount
|
|
1722
|
+
const labelHeight = showLabels ? 24 : 0
|
|
1723
|
+
const valueHeight = showValues ? 20 : 0
|
|
1724
|
+
const chartHeight = height - labelHeight - valueHeight - 10
|
|
1725
|
+
|
|
1726
|
+
data.forEach((item, index) => {
|
|
1727
|
+
const barHeight = (item.value / maxValue) * chartHeight
|
|
1728
|
+
const barX = x + index * (barWidth + barGap)
|
|
1729
|
+
const barY = y + height - labelHeight - valueHeight - barHeight - 5
|
|
1730
|
+
const color = item.color || barColor
|
|
1731
|
+
|
|
1732
|
+
const bar = new paper.Path.Rectangle({
|
|
1733
|
+
point: [barX, barY],
|
|
1734
|
+
size: [barWidth, barHeight],
|
|
1735
|
+
radius: [4, 4, 0, 0]
|
|
1736
|
+
})
|
|
1737
|
+
bar.fillColor = new paper.Color(color)
|
|
1738
|
+
elements.push({ type: 'path', id: bar.id })
|
|
1739
|
+
|
|
1740
|
+
if (showValues) {
|
|
1741
|
+
const valueText = new paper.PointText({
|
|
1742
|
+
point: [barX + barWidth / 2, barY - 8],
|
|
1743
|
+
content: String(item.value),
|
|
1744
|
+
fontSize: 12,
|
|
1745
|
+
fillColor: new paper.Color('#666666'),
|
|
1746
|
+
justification: 'center'
|
|
1747
|
+
})
|
|
1748
|
+
elements.push({ type: 'text', id: valueText.id })
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
if (showLabels) {
|
|
1752
|
+
const labelText = new paper.PointText({
|
|
1753
|
+
point: [barX + barWidth / 2, y + height - 8],
|
|
1754
|
+
content: item.label || '',
|
|
1755
|
+
fontSize: 11,
|
|
1756
|
+
fillColor: new paper.Color('#333333'),
|
|
1757
|
+
justification: 'center'
|
|
1758
|
+
})
|
|
1759
|
+
elements.push({ type: 'text', id: labelText.id })
|
|
1760
|
+
}
|
|
1761
|
+
})
|
|
1762
|
+
} else if (type === 'pie' && data.length > 0) {
|
|
1763
|
+
const cx = x + width / 2
|
|
1764
|
+
const cy = y + height / 2
|
|
1765
|
+
const radius = Math.min(width, height) / 2 - 10
|
|
1766
|
+
const total = data.reduce((sum, d) => sum + d.value, 0)
|
|
1767
|
+
let currentAngle = -90
|
|
1768
|
+
const colors = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#8b5cf6', '#ec4899', '#06b6d4', '#84cc16']
|
|
1769
|
+
|
|
1770
|
+
data.forEach((item, index) => {
|
|
1771
|
+
const percentage = item.value / total
|
|
1772
|
+
const endAngle = currentAngle + percentage * 360
|
|
1773
|
+
const path = new paper.Path()
|
|
1774
|
+
path.moveTo(cx, cy)
|
|
1775
|
+
path.arc([cx, cy], radius, currentAngle * Math.PI / 180, endAngle * Math.PI / 180)
|
|
1776
|
+
path.closePath()
|
|
1777
|
+
path.fillColor = new paper.Color(item.color || colors[index % colors.length])
|
|
1778
|
+
elements.push({ type: 'path', id: path.id })
|
|
1779
|
+
|
|
1780
|
+
if (showLabels && percentage > 0.05) {
|
|
1781
|
+
const midAngle = (currentAngle + endAngle) / 2
|
|
1782
|
+
const midRad = midAngle * Math.PI / 180
|
|
1783
|
+
const labelX = cx + radius * 0.7 * Math.cos(midRad)
|
|
1784
|
+
const labelY = cy + radius * 0.7 * Math.sin(midRad)
|
|
1785
|
+
const labelText = new paper.PointText({
|
|
1786
|
+
point: [labelX, labelY + 4],
|
|
1787
|
+
content: `${Math.round(percentage * 100)}%`,
|
|
1788
|
+
fontSize: 11,
|
|
1789
|
+
fillColor: new paper.Color('#ffffff'),
|
|
1790
|
+
justification: 'center',
|
|
1791
|
+
fontWeight: 'bold'
|
|
1792
|
+
})
|
|
1793
|
+
elements.push({ type: 'text', id: labelText.id })
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
currentAngle = endAngle
|
|
1797
|
+
})
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
return { success: true, elements, type: 'chart' }
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
/**
|
|
1804
|
+
* 创建水印组件
|
|
1805
|
+
*/
|
|
1806
|
+
function createWatermarkComponent(project, canvas, {
|
|
1807
|
+
text, cx, cy, color = 'rgba(0,0,0,0.1)', fontSize = 48,
|
|
1808
|
+
fontFamily = 'sans-serif', opacity = 0.1, rotation = 0, align = 'center'
|
|
1809
|
+
}) {
|
|
1810
|
+
const label = new paper.PointText({
|
|
1811
|
+
point: [cx, cy],
|
|
1812
|
+
content: text,
|
|
1813
|
+
fontSize: fontSize,
|
|
1814
|
+
fontFamily: fontFamily,
|
|
1815
|
+
fillColor: new paper.Color(color),
|
|
1816
|
+
justification: align,
|
|
1817
|
+
opacity: opacity
|
|
1818
|
+
})
|
|
1819
|
+
|
|
1820
|
+
if (rotation !== 0) {
|
|
1821
|
+
label.rotate(rotation, new paper.Point(cx, cy))
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1824
|
+
return { success: true, elements: [{ type: 'text', id: label.id }], type: 'watermark' }
|
|
1825
|
+
}
|
|
1826
|
+
|
|
1827
|
+
/**
|
|
1828
|
+
* 创建表格组件
|
|
1829
|
+
*/
|
|
1830
|
+
function createTableComponent(project, canvas, {
|
|
1831
|
+
x, y, width, columns = [], rows = [], rowHeight = 36,
|
|
1832
|
+
headerBg = '#f0f0f0', headerColor = '#333333', borderColor = '#e0e0e0',
|
|
1833
|
+
cellColor = '#333333', fontSize = 12, headerFontSize = 13, striped = true, stripeColor = '#fafafa'
|
|
1834
|
+
}) {
|
|
1835
|
+
const elements = []
|
|
1836
|
+
if (columns.length === 0) return { success: true, elements, type: 'table' }
|
|
1837
|
+
|
|
1838
|
+
const totalHeight = rowHeight * (rows.length + 1)
|
|
1839
|
+
|
|
1840
|
+
const outerBorder = new paper.Path.Rectangle({ point: [x, y], size: [width, totalHeight] })
|
|
1841
|
+
outerBorder.fillColor = new paper.Color('transparent')
|
|
1842
|
+
outerBorder.strokeColor = new paper.Color(borderColor)
|
|
1843
|
+
outerBorder.strokeWidth = 1
|
|
1844
|
+
elements.push({ type: 'path', id: outerBorder.id })
|
|
1845
|
+
|
|
1846
|
+
const headerBgRect = new paper.Path.Rectangle({ point: [x, y], size: [width, rowHeight] })
|
|
1847
|
+
headerBgRect.fillColor = new paper.Color(headerBg)
|
|
1848
|
+
headerBgRect.strokeColor = new paper.Color(borderColor)
|
|
1849
|
+
headerBgRect.strokeWidth = 0.5
|
|
1850
|
+
elements.push({ type: 'path', id: headerBgRect.id })
|
|
1851
|
+
|
|
1852
|
+
let currentX = x
|
|
1853
|
+
columns.forEach((col, index) => {
|
|
1854
|
+
const colWidth = col.width || (width / columns.length)
|
|
1855
|
+
if (index > 0) {
|
|
1856
|
+
const line = new paper.Path.Line({ from: [currentX, y], to: [currentX, y + totalHeight] })
|
|
1857
|
+
line.strokeColor = new paper.Color(borderColor)
|
|
1858
|
+
line.strokeWidth = 0.5
|
|
1859
|
+
elements.push({ type: 'line', id: line.id })
|
|
1860
|
+
}
|
|
1861
|
+
const headerText = new paper.PointText({
|
|
1862
|
+
point: [currentX + colWidth / 2, y + rowHeight / 2 + fontSize / 3],
|
|
1863
|
+
content: col.title || '',
|
|
1864
|
+
fontSize: headerFontSize,
|
|
1865
|
+
fillColor: new paper.Color(headerColor),
|
|
1866
|
+
justification: col.align || 'center',
|
|
1867
|
+
fontWeight: 'bold'
|
|
1868
|
+
})
|
|
1869
|
+
elements.push({ type: 'text', id: headerText.id })
|
|
1870
|
+
currentX += colWidth
|
|
1871
|
+
})
|
|
1872
|
+
|
|
1873
|
+
rows.forEach((row, rowIndex) => {
|
|
1874
|
+
const rowY = y + rowHeight * (rowIndex + 1)
|
|
1875
|
+
if (striped && rowIndex % 2 === 1) {
|
|
1876
|
+
const stripeBg = new paper.Path.Rectangle({ point: [x, rowY], size: [width, rowHeight] })
|
|
1877
|
+
stripeBg.fillColor = new paper.Color(stripeColor)
|
|
1878
|
+
stripeBg.strokeColor = new paper.Color(borderColor)
|
|
1879
|
+
stripeBg.strokeWidth = 0.5
|
|
1880
|
+
elements.push({ type: 'path', id: stripeBg.id })
|
|
1881
|
+
}
|
|
1882
|
+
const rowLine = new paper.Path.Line({ from: [x, rowY], to: [x + width, rowY] })
|
|
1883
|
+
rowLine.strokeColor = new paper.Color(borderColor)
|
|
1884
|
+
rowLine.strokeWidth = 0.5
|
|
1885
|
+
elements.push({ type: 'line', id: rowLine.id })
|
|
1886
|
+
|
|
1887
|
+
let cellX = x
|
|
1888
|
+
columns.forEach((col, colIndex) => {
|
|
1889
|
+
const colWidth = col.width || (width / columns.length)
|
|
1890
|
+
const cellValue = row[colIndex] || ''
|
|
1891
|
+
const cellText = new paper.PointText({
|
|
1892
|
+
point: [cellX + colWidth / 2, rowY + rowHeight / 2 + fontSize / 3],
|
|
1893
|
+
content: String(cellValue),
|
|
1894
|
+
fontSize: fontSize,
|
|
1895
|
+
fillColor: new paper.Color(cellColor),
|
|
1896
|
+
justification: col.align || 'center'
|
|
1897
|
+
})
|
|
1898
|
+
elements.push({ type: 'text', id: cellText.id })
|
|
1899
|
+
cellX += colWidth
|
|
1900
|
+
})
|
|
1901
|
+
})
|
|
1902
|
+
|
|
1903
|
+
return { success: true, elements, width, height: totalHeight, type: 'table' }
|
|
1904
|
+
}
|