foliko 1.1.7 → 1.1.9

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