foliko 1.1.1 → 1.1.3
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/weixin-media/2026-04-08/img_1775618677512.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619073340.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619097536.jpg +0 -0
- package/.agent/data/weixin-media/2026-04-08/img_1775619209388.jpg +0 -0
- package/.agent/plugins/poster-plugin/package.json +2 -1
- package/.agent/plugins/poster-plugin/src/canvas.js +70 -7
- package/.agent/plugins/poster-plugin/src/components/barcode.js +120 -0
- package/.agent/plugins/poster-plugin/src/components/bubble.js +153 -0
- package/.agent/plugins/poster-plugin/src/components/button.js +124 -0
- package/.agent/plugins/poster-plugin/src/components/cta.js +26 -24
- package/.agent/plugins/poster-plugin/src/components/featureGrid.js +22 -17
- package/.agent/plugins/poster-plugin/src/components/frame.js +230 -0
- package/.agent/plugins/poster-plugin/src/components/highlightText.js +144 -0
- package/.agent/plugins/poster-plugin/src/components/icon.js +94 -0
- package/.agent/plugins/poster-plugin/src/components/index.js +19 -0
- package/.agent/plugins/poster-plugin/src/components/listItem.js +6 -5
- package/.agent/plugins/poster-plugin/src/components/qrcode.js +74 -0
- package/.agent/plugins/poster-plugin/src/components/ribbon.js +193 -0
- package/.agent/plugins/poster-plugin/src/components/seal.js +146 -0
- package/.agent/plugins/poster-plugin/src/components/table.js +17 -9
- package/.agent/plugins/poster-plugin/src/components/tagCloud.js +24 -17
- package/.agent/plugins/poster-plugin/src/components/timeline.js +24 -12
- package/.agent/plugins/poster-plugin/src/composer.js +392 -150
- package/.agent/plugins/poster-plugin/src/elements/background.js +36 -4
- package/.agent/plugins/poster-plugin/src/elements/image.js +4 -47
- package/.agent/plugins/poster-plugin/src/elements/index.js +2 -0
- package/.agent/plugins/poster-plugin/src/elements/richText.js +230 -0
- package/.agent/plugins/poster-plugin/src/elements/svg.js +35 -19
- package/.agent/plugins/poster-plugin/src/index.js +430 -7
- package/.agent/plugins/poster-plugin/src/utils/imageLoader.js +84 -0
- package/.agent/plugins/poster-plugin/test-background.svg +1 -0
- package/.agent/plugins/poster-plugin/test-full-poster.svg +2 -0
- package/.agent/plugins/poster-plugin/test-image.png +0 -0
- package/.agent/sessions/cli_default.json +1089 -145
- package/.agent/sessions/weixin_o9cq80zgZqKPA2-s59PN43GdDy1w@im.wechat.json +8902 -0
- package/.claude/settings.local.json +6 -1
- package/output/beef-love-poster.png +0 -0
- package/output/international-news-daily.png +0 -0
- package/package.json +2 -1
- package/plugins/extension-executor-plugin.js +33 -32
- package/plugins/file-system-plugin.js +4 -19
- package/plugins/subagent-plugin.js +37 -14
- package/plugins/weixin-plugin.js +167 -47
- package/poster-test-2.png +0 -0
- package/skills/poster-guide/SKILL.md +497 -5
- package/src/core/agent-chat.js +141 -8
- package/src/core/agent.js +6 -3
- package/calc_tokens_weixin.js +0 -81
- package/foliko-creative-3.png +0 -0
- package/foliko-creative-4.png +0 -0
- package/foliko-creative-5.png +0 -0
- package/story-cover-book-v2.png +0 -0
- package/story-cover-japanese-1.png +0 -0
- package/story-cover-japanese-2.png +0 -0
- package/story-cover-japanese-3.png +0 -0
- package/story-cover-moran.png +0 -0
- package/undefined.png +0 -0
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
const paper = require('paper')
|
|
6
6
|
const PRESETS = require('./presets')
|
|
7
|
+
const fs = require('fs')
|
|
8
|
+
const path = require('path')
|
|
7
9
|
|
|
8
10
|
/**
|
|
9
11
|
* 画布管理器类
|
|
@@ -20,7 +22,7 @@ class CanvasManager {
|
|
|
20
22
|
/**
|
|
21
23
|
* 创建画布
|
|
22
24
|
*/
|
|
23
|
-
create({ preset, width, height, background }) {
|
|
25
|
+
async create({ preset, width, height, background }) {
|
|
24
26
|
let w, h
|
|
25
27
|
|
|
26
28
|
if (preset) {
|
|
@@ -49,12 +51,21 @@ class CanvasManager {
|
|
|
49
51
|
|
|
50
52
|
// 添加背景
|
|
51
53
|
if (background) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
if (typeof background === 'string' && path.isAbsolute(background)) {
|
|
55
|
+
// 背景是绝对路径图片
|
|
56
|
+
await this._addBackgroundImage(background, w, h)
|
|
57
|
+
} else if (typeof background === 'object' && background.image) {
|
|
58
|
+
// 背景是对象形式 { image: 'path' }
|
|
59
|
+
await this._addBackgroundImage(background.image, w, h)
|
|
60
|
+
} else {
|
|
61
|
+
// 背景是颜色
|
|
62
|
+
const bg = new paper.Path.Rectangle({
|
|
63
|
+
point: [0, 0],
|
|
64
|
+
size: [w, h],
|
|
65
|
+
fillColor: background,
|
|
66
|
+
})
|
|
67
|
+
bg.sendToBack()
|
|
68
|
+
}
|
|
58
69
|
}
|
|
59
70
|
|
|
60
71
|
return {
|
|
@@ -65,6 +76,58 @@ class CanvasManager {
|
|
|
65
76
|
}
|
|
66
77
|
}
|
|
67
78
|
|
|
79
|
+
/**
|
|
80
|
+
* 添加背景图片
|
|
81
|
+
*/
|
|
82
|
+
async _addBackgroundImage(imageSrc, w, h) {
|
|
83
|
+
// 本地文件路径
|
|
84
|
+
let absolutePath = imageSrc
|
|
85
|
+
if (!path.isAbsolute(absolutePath)) {
|
|
86
|
+
absolutePath = path.join(process.cwd(), absolutePath)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!fs.existsSync(absolutePath)) {
|
|
90
|
+
throw new Error(`背景图片文件不存在: ${absolutePath}`)
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// 使用 loadImage 获取图片数据
|
|
94
|
+
const { loadImage } = require('canvas')
|
|
95
|
+
const imageData = await loadImage(absolutePath)
|
|
96
|
+
|
|
97
|
+
// 创建 Paper.js Raster
|
|
98
|
+
const raster = new paper.Raster(imageData)
|
|
99
|
+
|
|
100
|
+
// 等待 raster 加载完成
|
|
101
|
+
await new Promise((resolve) => {
|
|
102
|
+
if (raster.loaded) {
|
|
103
|
+
resolve()
|
|
104
|
+
} else {
|
|
105
|
+
raster.onLoad = resolve
|
|
106
|
+
}
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
// 计算 cover 模式缩放
|
|
110
|
+
const canvasRatio = w / h
|
|
111
|
+
const imageRatio = raster.width / raster.height
|
|
112
|
+
|
|
113
|
+
let scaledWidth, scaledHeight, offsetX, offsetY
|
|
114
|
+
|
|
115
|
+
if (imageRatio > canvasRatio) {
|
|
116
|
+
scaledHeight = h
|
|
117
|
+
scaledWidth = raster.width * (h / raster.height)
|
|
118
|
+
offsetX = (w - scaledWidth) / 2
|
|
119
|
+
offsetY = 0
|
|
120
|
+
} else {
|
|
121
|
+
scaledWidth = w
|
|
122
|
+
scaledHeight = raster.height * (w / raster.width)
|
|
123
|
+
offsetX = 0
|
|
124
|
+
offsetY = (h - scaledHeight) / 2
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
raster.bounds = new paper.Rectangle(offsetX, offsetY, scaledWidth, scaledHeight)
|
|
128
|
+
raster.sendToBack()
|
|
129
|
+
}
|
|
130
|
+
|
|
68
131
|
/**
|
|
69
132
|
* 获取画布
|
|
70
133
|
*/
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 条形码组件 - 静态图片
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const paper = require('paper')
|
|
6
|
+
const { loadImageAsRaster, downloadImage } = require('../utils/imageLoader')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 创建条形码
|
|
10
|
+
*/
|
|
11
|
+
async function createBarcode(project, args) {
|
|
12
|
+
const {
|
|
13
|
+
x = 0,
|
|
14
|
+
y = 0,
|
|
15
|
+
width = 300,
|
|
16
|
+
height = 100,
|
|
17
|
+
content = '1234567890',
|
|
18
|
+
showText = true,
|
|
19
|
+
textColor = '#000000',
|
|
20
|
+
fontSize = 16,
|
|
21
|
+
opacity = 1,
|
|
22
|
+
} = args
|
|
23
|
+
|
|
24
|
+
// 内容太短或像URL,回退到简单条形码
|
|
25
|
+
if (content.length < 8 || content.startsWith('http')) {
|
|
26
|
+
return createSimpleBarcode(project, { x, y, width, height, content, showText, textColor, fontSize, opacity })
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const barcodeUrl = `https://api.qrserver.com/v1/create-qr-code/?size=${width}x${height}&data=${encodeURIComponent(content)}&format=png`
|
|
31
|
+
const { raster } = await loadImageAsRaster(project, barcodeUrl, { x, y, width, height }, opacity)
|
|
32
|
+
const items = [raster]
|
|
33
|
+
|
|
34
|
+
// 文字
|
|
35
|
+
if (showText) {
|
|
36
|
+
const textItem = new paper.PointText({
|
|
37
|
+
point: [x + width / 2, y + height + fontSize + 5],
|
|
38
|
+
content: content,
|
|
39
|
+
fontSize,
|
|
40
|
+
fontFamily: 'monospace',
|
|
41
|
+
fillColor: new paper.Color(textColor),
|
|
42
|
+
justification: 'center',
|
|
43
|
+
})
|
|
44
|
+
if (opacity !== 1) textItem.opacity = opacity
|
|
45
|
+
items.push(textItem)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
success: true,
|
|
50
|
+
type: 'barcode',
|
|
51
|
+
items: items.map(i => i.id),
|
|
52
|
+
}
|
|
53
|
+
} catch (err) {
|
|
54
|
+
// 回退到简单条形码
|
|
55
|
+
return createSimpleBarcode(project, { x, y, width, height, content, showText, textColor, fontSize, opacity })
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* 创建简单的条形码(使用线条绘制)
|
|
61
|
+
*/
|
|
62
|
+
function createSimpleBarcode(project, args) {
|
|
63
|
+
const {
|
|
64
|
+
x = 0,
|
|
65
|
+
y = 0,
|
|
66
|
+
width = 300,
|
|
67
|
+
height = 80,
|
|
68
|
+
content = '123456',
|
|
69
|
+
color = '#000000',
|
|
70
|
+
showText = true,
|
|
71
|
+
textColor = '#000000',
|
|
72
|
+
fontSize = 14,
|
|
73
|
+
opacity = 1,
|
|
74
|
+
} = args
|
|
75
|
+
|
|
76
|
+
const items = []
|
|
77
|
+
const barHeight = showText ? height - 25 : height
|
|
78
|
+
const barWidth = width / (content.length * 15)
|
|
79
|
+
|
|
80
|
+
// 生成简单的条形图案
|
|
81
|
+
for (let i = 0; i < content.length; i++) {
|
|
82
|
+
const charCode = content.charCodeAt(i)
|
|
83
|
+
const density = (charCode % 3) + 1
|
|
84
|
+
|
|
85
|
+
for (let d = 0; d < density; d++) {
|
|
86
|
+
const isWide = (charCode + d) % 2 === 0
|
|
87
|
+
const barW = isWide ? barWidth * 2 : barWidth
|
|
88
|
+
|
|
89
|
+
const bar = new paper.Path.Rectangle({
|
|
90
|
+
point: [x + (i * density + d) * barWidth, y],
|
|
91
|
+
size: [barW * 0.8, barHeight],
|
|
92
|
+
fillColor: new paper.Color(color),
|
|
93
|
+
})
|
|
94
|
+
if (opacity !== 1) bar.opacity = opacity
|
|
95
|
+
items.push(bar)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// 文字
|
|
100
|
+
if (showText) {
|
|
101
|
+
const textItem = new paper.PointText({
|
|
102
|
+
point: [x + width / 2, y + barHeight + fontSize + 3],
|
|
103
|
+
content: content,
|
|
104
|
+
fontSize,
|
|
105
|
+
fontFamily: 'monospace',
|
|
106
|
+
fillColor: new paper.Color(textColor),
|
|
107
|
+
justification: 'center',
|
|
108
|
+
})
|
|
109
|
+
if (opacity !== 1) textItem.opacity = opacity
|
|
110
|
+
items.push(textItem)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
success: true,
|
|
115
|
+
type: 'barcode',
|
|
116
|
+
items: items.map(i => i.id),
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = createBarcode
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 对话气泡组件
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const paper = require('paper')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* 创建对话气泡
|
|
9
|
+
*/
|
|
10
|
+
function createBubble(project, args) {
|
|
11
|
+
const {
|
|
12
|
+
x = 0,
|
|
13
|
+
y = 0,
|
|
14
|
+
width = 300,
|
|
15
|
+
height = 100,
|
|
16
|
+
text = '',
|
|
17
|
+
fontSize = 24,
|
|
18
|
+
fontFamily,
|
|
19
|
+
color = '#000000',
|
|
20
|
+
backgroundColor = '#ffffff',
|
|
21
|
+
borderColor,
|
|
22
|
+
borderWidth = 1,
|
|
23
|
+
radius = 20,
|
|
24
|
+
tailDirection = 'bottom', // bottom, top, left, right
|
|
25
|
+
tailPosition = 'left', // left, center, right
|
|
26
|
+
shadow,
|
|
27
|
+
opacity = 1,
|
|
28
|
+
} = args
|
|
29
|
+
|
|
30
|
+
const items = []
|
|
31
|
+
|
|
32
|
+
// 气泡主体
|
|
33
|
+
const bubbleX = tailDirection === 'left' ? x + 15 : x
|
|
34
|
+
const bubbleY = tailDirection === 'top' ? y + 15 : y
|
|
35
|
+
const bubbleWidth = tailDirection === 'left' ? width - 15 : width
|
|
36
|
+
const bubbleHeight = tailDirection === 'top' ? height - 15 : height
|
|
37
|
+
|
|
38
|
+
const bgOpts = {
|
|
39
|
+
point: [bubbleX, bubbleY],
|
|
40
|
+
size: [bubbleWidth, bubbleHeight],
|
|
41
|
+
radius: radius,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (borderColor) {
|
|
45
|
+
bgOpts.strokeColor = new paper.Color(borderColor)
|
|
46
|
+
bgOpts.strokeWidth = borderWidth
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const bg = new paper.Path.Rectangle(bgOpts)
|
|
50
|
+
bg.fillColor = new paper.Color(backgroundColor)
|
|
51
|
+
|
|
52
|
+
if (shadow) {
|
|
53
|
+
bg.shadowColor = new paper.Color(shadow.color || '#000000')
|
|
54
|
+
bg.shadowBlur = shadow.blur || 10
|
|
55
|
+
bg.shadowOffset = new paper.Point(shadow.offsetX || 3, shadow.offsetY || 3)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (opacity !== 1) bg.opacity = opacity
|
|
59
|
+
items.push(bg)
|
|
60
|
+
|
|
61
|
+
// 气泡尾巴
|
|
62
|
+
const tailSize = 20
|
|
63
|
+
let tailX, tailY, tailRotation
|
|
64
|
+
|
|
65
|
+
// 计算尾巴位置
|
|
66
|
+
switch (tailDirection) {
|
|
67
|
+
case 'bottom':
|
|
68
|
+
tailY = bubbleY + bubbleHeight - 5
|
|
69
|
+
if (tailPosition === 'left') tailX = bubbleX + 30
|
|
70
|
+
else if (tailPosition === 'right') tailX = bubbleX + bubbleWidth - 30
|
|
71
|
+
else tailX = bubbleX + bubbleWidth / 2
|
|
72
|
+
tailRotation = 0
|
|
73
|
+
break
|
|
74
|
+
case 'top':
|
|
75
|
+
tailY = bubbleY + 5
|
|
76
|
+
if (tailPosition === 'left') tailX = bubbleX + 30
|
|
77
|
+
else if (tailPosition === 'right') tailX = bubbleX + bubbleWidth - 30
|
|
78
|
+
else tailX = bubbleX + bubbleWidth / 2
|
|
79
|
+
tailRotation = 180
|
|
80
|
+
break
|
|
81
|
+
case 'left':
|
|
82
|
+
tailX = bubbleX + 5
|
|
83
|
+
if (tailPosition === 'left') tailY = bubbleY + 30
|
|
84
|
+
else if (tailPosition === 'right') tailY = bubbleY + bubbleHeight - 30
|
|
85
|
+
else tailY = bubbleY + bubbleHeight / 2
|
|
86
|
+
tailRotation = 90
|
|
87
|
+
break
|
|
88
|
+
case 'right':
|
|
89
|
+
tailX = bubbleX + bubbleWidth - 5
|
|
90
|
+
if (tailPosition === 'left') tailY = bubbleY + 30
|
|
91
|
+
else if (tailPosition === 'right') tailY = bubbleY + bubbleHeight - 30
|
|
92
|
+
else tailY = bubbleY + bubbleHeight / 2
|
|
93
|
+
tailRotation = 270
|
|
94
|
+
break
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const tail = new paper.Path()
|
|
98
|
+
tail.add(new paper.Point(tailX - tailSize, tailY))
|
|
99
|
+
tail.add(new paper.Point(tailX, tailY + tailSize * 0.7))
|
|
100
|
+
tail.add(new paper.Point(tailX, tailY - tailSize * 0.7))
|
|
101
|
+
tail.closed = true
|
|
102
|
+
tail.fillColor = new paper.Color(backgroundColor)
|
|
103
|
+
if (borderColor) {
|
|
104
|
+
tail.strokeColor = new paper.Color(borderColor)
|
|
105
|
+
tail.strokeWidth = borderWidth
|
|
106
|
+
}
|
|
107
|
+
tail.rotate(tailRotation, new paper.Point(tailX, tailY))
|
|
108
|
+
|
|
109
|
+
if (opacity !== 1) tail.opacity = opacity
|
|
110
|
+
items.push(tail)
|
|
111
|
+
|
|
112
|
+
// 文字
|
|
113
|
+
const padding = 25
|
|
114
|
+
const textItem = new paper.PointText({
|
|
115
|
+
point: [bubbleX + padding, bubbleY + bubbleHeight / 2 + fontSize / 3],
|
|
116
|
+
content: text,
|
|
117
|
+
fontSize,
|
|
118
|
+
fontFamily: fontFamily || 'sans-serif',
|
|
119
|
+
fillColor: new paper.Color(color),
|
|
120
|
+
justification: tailPosition === 'left' ? 'left' : tailPosition === 'right' ? 'right' : 'center',
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
// 限制文字宽度
|
|
124
|
+
const maxTextWidth = bubbleWidth - padding * 2
|
|
125
|
+
if (textItem.bounds.width > maxTextWidth) {
|
|
126
|
+
textItem.justification = 'left'
|
|
127
|
+
const charsPerLine = Math.floor((text.length * maxTextWidth) / textItem.bounds.width)
|
|
128
|
+
// 简单换行处理
|
|
129
|
+
let lines = []
|
|
130
|
+
let currentLine = ''
|
|
131
|
+
for (const char of text) {
|
|
132
|
+
currentLine += char
|
|
133
|
+
const testText = new paper.PointText({ content: currentLine, fontSize, fontFamily: fontFamily || 'sans-serif' })
|
|
134
|
+
if (testText.bounds.width > maxTextWidth) {
|
|
135
|
+
lines.push(currentLine.slice(0, -1))
|
|
136
|
+
currentLine = char
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (currentLine) lines.push(currentLine)
|
|
140
|
+
textItem.content = lines.join('\n')
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (opacity !== 1) textItem.opacity = opacity
|
|
144
|
+
items.push(textItem)
|
|
145
|
+
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
type: 'bubble',
|
|
149
|
+
items: items.map(i => i.id),
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
module.exports = createBubble
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 按钮组件
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const paper = require('paper')
|
|
6
|
+
const { loadImageAsRaster } = require('../utils/imageLoader')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 创建按钮
|
|
10
|
+
*/
|
|
11
|
+
async function createButton(project, args) {
|
|
12
|
+
const {
|
|
13
|
+
x = 0,
|
|
14
|
+
y = 0,
|
|
15
|
+
width = 200,
|
|
16
|
+
height = 60,
|
|
17
|
+
text = '按钮',
|
|
18
|
+
fontSize = 24,
|
|
19
|
+
fontFamily,
|
|
20
|
+
color = '#ffffff',
|
|
21
|
+
backgroundColor = '#3b82f6',
|
|
22
|
+
borderColor,
|
|
23
|
+
borderWidth = 0,
|
|
24
|
+
radius = 8,
|
|
25
|
+
shadow,
|
|
26
|
+
gradient,
|
|
27
|
+
icon,
|
|
28
|
+
iconPosition = 'left',
|
|
29
|
+
opacity = 1,
|
|
30
|
+
} = args
|
|
31
|
+
|
|
32
|
+
// 创建按钮背景
|
|
33
|
+
const bgOptions = {
|
|
34
|
+
point: [x, y],
|
|
35
|
+
size: [width, height],
|
|
36
|
+
radius: radius,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (borderColor) {
|
|
40
|
+
bgOptions.strokeColor = new paper.Color(borderColor)
|
|
41
|
+
bgOptions.strokeWidth = borderWidth
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const bg = new paper.Path.Rectangle(bgOptions)
|
|
45
|
+
|
|
46
|
+
// 渐变或纯色填充
|
|
47
|
+
if (gradient && gradient.colors && gradient.colors.length > 0) {
|
|
48
|
+
const colors = gradient.colors.map(c => new paper.Color(c))
|
|
49
|
+
bg.fillColor = new paper.Color({
|
|
50
|
+
gradient: { stops: colors },
|
|
51
|
+
origin: bg.bounds.topLeft,
|
|
52
|
+
destination: bg.bounds.bottomLeft,
|
|
53
|
+
})
|
|
54
|
+
} else {
|
|
55
|
+
bg.fillColor = new paper.Color(backgroundColor)
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 阴影
|
|
59
|
+
if (shadow) {
|
|
60
|
+
bg.shadowColor = new paper.Color(shadow.color || '#000000')
|
|
61
|
+
bg.shadowBlur = shadow.blur || 10
|
|
62
|
+
bg.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (opacity !== 1) {
|
|
66
|
+
bg.opacity = opacity
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const items = [bg]
|
|
70
|
+
let textX = x + width / 2
|
|
71
|
+
|
|
72
|
+
// 添加图标
|
|
73
|
+
if (icon) {
|
|
74
|
+
const iconSize = Math.min(height * 0.5, 40)
|
|
75
|
+
const iconX = iconPosition === 'left' ? x + 20 : x + width - 40 - iconSize
|
|
76
|
+
|
|
77
|
+
if (icon.startsWith('http') || icon.startsWith('data:')) {
|
|
78
|
+
// URL图片图标
|
|
79
|
+
const { raster } = await loadImageAsRaster(project, icon, {
|
|
80
|
+
x: iconX,
|
|
81
|
+
y: y + (height - iconSize) / 2,
|
|
82
|
+
width: iconSize,
|
|
83
|
+
height: iconSize
|
|
84
|
+
}, opacity)
|
|
85
|
+
items.push(raster)
|
|
86
|
+
} else {
|
|
87
|
+
// Emoji 或文本图标
|
|
88
|
+
const iconText = new paper.PointText({
|
|
89
|
+
point: [iconX + iconSize / 2, y + height / 2 + fontSize / 3],
|
|
90
|
+
content: icon,
|
|
91
|
+
fontSize: iconSize,
|
|
92
|
+
justification: 'center',
|
|
93
|
+
})
|
|
94
|
+
if (opacity !== 1) iconText.opacity = opacity
|
|
95
|
+
items.push(iconText)
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
textX = iconPosition === 'left' ? x + width / 2 + 25 : x + width / 2 - 25
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// 添加文字
|
|
102
|
+
const textItem = new paper.PointText({
|
|
103
|
+
point: [textX, y + height / 2 + fontSize / 3],
|
|
104
|
+
content: text,
|
|
105
|
+
fontSize,
|
|
106
|
+
fontFamily: fontFamily || 'sans-serif',
|
|
107
|
+
fillColor: new paper.Color(color),
|
|
108
|
+
justification: 'center',
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
if (opacity !== 1) {
|
|
112
|
+
textItem.opacity = opacity
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
items.push(textItem)
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
success: true,
|
|
119
|
+
type: 'button',
|
|
120
|
+
items: items.map(i => i.id),
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
module.exports = createButton
|
|
@@ -6,25 +6,11 @@ const paper = require('paper')
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 创建 CTA 按钮
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} project - Paper.js 项目
|
|
11
|
-
* @param {Object} canvas - 画布对象
|
|
12
|
-
* @param {Object} args - 组件参数
|
|
13
|
-
* @param {number} args.x - X坐标(居中)
|
|
14
|
-
* @param {number} args.y - Y坐标
|
|
15
|
-
* @param {string} args.text - 按钮文字
|
|
16
|
-
* @param {string} args.background - 背景色
|
|
17
|
-
* @param {string} args.color - 文字颜色
|
|
18
|
-
* @param {string} args.border - 边框颜色
|
|
19
|
-
* @param {number} args.fontSize - 字体大小
|
|
20
|
-
* @param {number} args.padding - 内边距
|
|
21
|
-
* @param {number} args.radius - 圆角半径
|
|
22
|
-
* @param {Object} args.shadow - 阴影设置
|
|
23
9
|
*/
|
|
24
10
|
function createCTA(project, canvas, args) {
|
|
25
11
|
const {
|
|
26
|
-
x, y,
|
|
27
|
-
text,
|
|
12
|
+
x = 0, y = 0,
|
|
13
|
+
text = '',
|
|
28
14
|
background = '#007bff',
|
|
29
15
|
color = '#ffffff',
|
|
30
16
|
border,
|
|
@@ -32,13 +18,18 @@ function createCTA(project, canvas, args) {
|
|
|
32
18
|
padding = 25,
|
|
33
19
|
radius = 8,
|
|
34
20
|
shadow,
|
|
21
|
+
width: customWidth,
|
|
35
22
|
} = args
|
|
36
23
|
|
|
37
24
|
const elements = []
|
|
38
25
|
|
|
39
|
-
//
|
|
40
|
-
const
|
|
41
|
-
|
|
26
|
+
// 确保 text 是字符串
|
|
27
|
+
const textStr = String(text || '')
|
|
28
|
+
// 使用更准确的字符宽度估算:中文约1.0,英文约0.5
|
|
29
|
+
const chineseChars = (textStr.match(/[\u4e00-\u9fa5]/g) || []).length
|
|
30
|
+
const otherChars = textStr.length - chineseChars
|
|
31
|
+
const textWidth = chineseChars * fontSize * 1.0 + otherChars * fontSize * 0.5
|
|
32
|
+
const btnWidth = customWidth || (textWidth + padding * 2)
|
|
42
33
|
const btnHeight = fontSize + padding * 2
|
|
43
34
|
|
|
44
35
|
const btnX = x - btnWidth / 2
|
|
@@ -56,22 +47,32 @@ function createCTA(project, canvas, args) {
|
|
|
56
47
|
button.strokeWidth = 1
|
|
57
48
|
}
|
|
58
49
|
|
|
59
|
-
if (shadow) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
50
|
+
if (shadow && typeof shadow === 'object') {
|
|
51
|
+
try {
|
|
52
|
+
if (shadow.color) button.shadowColor = new paper.Color(shadow.color)
|
|
53
|
+
button.shadowBlur = shadow.blur || 10
|
|
54
|
+
button.shadowOffset = new paper.Point(shadow.offsetX || 0, shadow.offsetY || 4)
|
|
55
|
+
} catch (e) {
|
|
56
|
+
// 忽略阴影错误
|
|
57
|
+
}
|
|
63
58
|
}
|
|
64
59
|
|
|
60
|
+
if (project && project.activeLayer) {
|
|
61
|
+
project.activeLayer.addChild(button)
|
|
62
|
+
}
|
|
65
63
|
elements.push({ type: 'rectangle', id: button.id })
|
|
66
64
|
|
|
67
65
|
// 绘制文字
|
|
68
66
|
const buttonText = new paper.PointText({
|
|
69
67
|
point: [x, y + btnHeight / 2 + fontSize / 3],
|
|
70
|
-
content:
|
|
68
|
+
content: textStr,
|
|
71
69
|
fontSize: fontSize,
|
|
72
70
|
fillColor: new paper.Color(color),
|
|
73
71
|
justification: 'center',
|
|
74
72
|
})
|
|
73
|
+
if (project && project.activeLayer) {
|
|
74
|
+
project.activeLayer.addChild(buttonText)
|
|
75
|
+
}
|
|
75
76
|
elements.push({ type: 'text', id: buttonText.id })
|
|
76
77
|
|
|
77
78
|
return {
|
|
@@ -79,6 +80,7 @@ function createCTA(project, canvas, args) {
|
|
|
79
80
|
elements,
|
|
80
81
|
width: btnWidth,
|
|
81
82
|
height: btnHeight,
|
|
83
|
+
type: 'cta',
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -6,20 +6,6 @@ const paper = require('paper')
|
|
|
6
6
|
|
|
7
7
|
/**
|
|
8
8
|
* 创建特性网格
|
|
9
|
-
*
|
|
10
|
-
* @param {Object} project - Paper.js 项目
|
|
11
|
-
* @param {Object} canvas - 画布对象
|
|
12
|
-
* @param {Object} args - 组件参数
|
|
13
|
-
* @param {number} args.x - X坐标
|
|
14
|
-
* @param {number} args.y - Y坐标
|
|
15
|
-
* @param {number} args.columns - 列数
|
|
16
|
-
* @param {number} args.itemWidth - 每个特性宽度
|
|
17
|
-
* @param {number} args.itemHeight - 每个特性高度
|
|
18
|
-
* @param {number} args.gap - 间距
|
|
19
|
-
* @param {Array} args.items - 特性数组 [{icon, title, description}]
|
|
20
|
-
* @param {string} args.background - 背景色
|
|
21
|
-
* @param {string} args.borderColor - 边框颜色
|
|
22
|
-
* @param {number} args.radius - 圆角半径
|
|
23
9
|
*/
|
|
24
10
|
function createFeatureGrid(project, canvas, args) {
|
|
25
11
|
const {
|
|
@@ -35,7 +21,13 @@ function createFeatureGrid(project, canvas, args) {
|
|
|
35
21
|
} = args
|
|
36
22
|
|
|
37
23
|
const elements = []
|
|
38
|
-
|
|
24
|
+
|
|
25
|
+
// 确保 items 是数组
|
|
26
|
+
if (!Array.isArray(items)) {
|
|
27
|
+
items = []
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const rows = items.length > 0 ? Math.ceil(items.length / columns) : 0
|
|
39
31
|
|
|
40
32
|
for (let i = 0; i < items.length; i++) {
|
|
41
33
|
const item = items[i]
|
|
@@ -55,6 +47,9 @@ function createFeatureGrid(project, canvas, args) {
|
|
|
55
47
|
bg.strokeColor = new paper.Color(borderColor)
|
|
56
48
|
bg.strokeWidth = 0.5
|
|
57
49
|
bg.opacity = 0.8
|
|
50
|
+
if (project && project.activeLayer) {
|
|
51
|
+
project.activeLayer.addChild(bg)
|
|
52
|
+
}
|
|
58
53
|
elements.push({ type: 'rectangle', id: bg.id })
|
|
59
54
|
|
|
60
55
|
const padding = 15
|
|
@@ -69,6 +64,9 @@ function createFeatureGrid(project, canvas, args) {
|
|
|
69
64
|
fillColor: new paper.Color(item.iconColor || '#00ff88'),
|
|
70
65
|
justification: 'left',
|
|
71
66
|
})
|
|
67
|
+
if (project && project.activeLayer) {
|
|
68
|
+
project.activeLayer.addChild(iconText)
|
|
69
|
+
}
|
|
72
70
|
elements.push({ type: 'text', id: iconText.id })
|
|
73
71
|
itemYOffset += 35
|
|
74
72
|
}
|
|
@@ -82,6 +80,9 @@ function createFeatureGrid(project, canvas, args) {
|
|
|
82
80
|
fillColor: new paper.Color(item.titleColor || '#ffffff'),
|
|
83
81
|
justification: 'left',
|
|
84
82
|
})
|
|
83
|
+
if (project && project.activeLayer) {
|
|
84
|
+
project.activeLayer.addChild(titleText)
|
|
85
|
+
}
|
|
85
86
|
elements.push({ type: 'text', id: titleText.id })
|
|
86
87
|
itemYOffset += 22
|
|
87
88
|
}
|
|
@@ -95,6 +96,9 @@ function createFeatureGrid(project, canvas, args) {
|
|
|
95
96
|
fillColor: new paper.Color(item.descColor || '#888888'),
|
|
96
97
|
justification: 'left',
|
|
97
98
|
})
|
|
99
|
+
if (project && project.activeLayer) {
|
|
100
|
+
project.activeLayer.addChild(descText)
|
|
101
|
+
}
|
|
98
102
|
elements.push({ type: 'text', id: descText.id })
|
|
99
103
|
}
|
|
100
104
|
}
|
|
@@ -102,10 +106,11 @@ function createFeatureGrid(project, canvas, args) {
|
|
|
102
106
|
return {
|
|
103
107
|
success: true,
|
|
104
108
|
elements,
|
|
105
|
-
width: columns * itemWidth + (columns - 1) * gap,
|
|
106
|
-
height: rows * itemHeight + (rows - 1) * gap,
|
|
109
|
+
width: columns * itemWidth + Math.max(0, columns - 1) * gap,
|
|
110
|
+
height: rows * itemHeight + Math.max(0, rows - 1) * gap,
|
|
107
111
|
rows,
|
|
108
112
|
cols: columns,
|
|
113
|
+
type: 'featureGrid',
|
|
109
114
|
}
|
|
110
115
|
}
|
|
111
116
|
|