@chuchen3d/wallpaper-plugin-sdk 0.0.0-stage → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +449 -2
- package/package.json +57 -4
- package/src/cli.js +99 -0
- package/src/commands/build.js +135 -0
- package/src/commands/example.js +375 -0
- package/src/commands/init.js +200 -0
- package/src/commands/validate.js +76 -0
- package/src/index.js +40 -0
- package/src/templates/dev-project.js +357 -0
- package/src/templates/manifest.js +143 -0
- package/src/templates/plugin-entry.js +82 -0
- package/src/templates/vue-component.js +213 -0
- package/src/utils/colors.js +31 -0
- package/src/utils/packager.js +210 -0
- package/src/utils/paramsConverter.js +44 -0
- package/src/utils/validator.js +119 -0
- package/src/utils/viteBuilder.js +79 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* build 命令 — 编译 Vue SFC 并打包为 .yizirwp 文件
|
|
3
|
+
*
|
|
4
|
+
* 流程:
|
|
5
|
+
* 1. 读取并校验 manifest.json
|
|
6
|
+
* 2. 调用 Vite 编译 SFC → dist/index.js (IIFE bundle)
|
|
7
|
+
* 3. 重写 manifest.json(entry=index.js, params 转换格式)
|
|
8
|
+
* 4. 打包 dist/ + manifest.json 为 .yizirwp
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import fs from 'fs'
|
|
12
|
+
import path from 'path'
|
|
13
|
+
import ora from 'ora'
|
|
14
|
+
import { colors, printSeparator } from '../utils/colors.js'
|
|
15
|
+
import { validateManifest } from '../utils/validator.js'
|
|
16
|
+
import { packFromDist } from '../utils/packager.js'
|
|
17
|
+
import { buildPlugin } from '../utils/viteBuilder.js'
|
|
18
|
+
import { convertParamsToAppFormat } from '../utils/paramsConverter.js'
|
|
19
|
+
|
|
20
|
+
/** 打包为 .yizirwp 文件 */
|
|
21
|
+
export async function cmdBuild(options = {}) {
|
|
22
|
+
console.log('')
|
|
23
|
+
console.log(colors.title('📦 打包壁纸插件'))
|
|
24
|
+
printSeparator()
|
|
25
|
+
|
|
26
|
+
const projectDir = process.cwd()
|
|
27
|
+
|
|
28
|
+
// 1. 读取 manifest.json
|
|
29
|
+
const manifestPath = path.join(projectDir, 'manifest.json')
|
|
30
|
+
if (!fs.existsSync(manifestPath)) {
|
|
31
|
+
console.log(colors.error('未找到 manifest.json'))
|
|
32
|
+
console.log(' 请在壁纸项目根目录下执行此命令')
|
|
33
|
+
process.exit(1)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let manifest
|
|
37
|
+
try {
|
|
38
|
+
const content = fs.readFileSync(manifestPath, 'utf-8')
|
|
39
|
+
manifest = JSON.parse(content)
|
|
40
|
+
} catch (err) {
|
|
41
|
+
console.log(colors.error(`manifest.json 解析失败: ${err.message}`))
|
|
42
|
+
process.exit(1)
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// 2. 校验 manifest
|
|
46
|
+
const validation = validateManifest(manifest, projectDir)
|
|
47
|
+
if (!validation.valid) {
|
|
48
|
+
console.log(colors.error('manifest.json 校验失败:'))
|
|
49
|
+
for (const error of validation.errors) {
|
|
50
|
+
console.log(` ❌ ${error}`)
|
|
51
|
+
}
|
|
52
|
+
process.exit(1)
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (validation.warnings.length > 0) {
|
|
56
|
+
console.log(colors.warn('manifest.json 校验警告:'))
|
|
57
|
+
for (const warning of validation.warnings) {
|
|
58
|
+
console.log(` ⚠️ ${warning}`)
|
|
59
|
+
}
|
|
60
|
+
console.log('')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// dry-run 模式只校验不构建
|
|
64
|
+
if (options.dryRun) {
|
|
65
|
+
console.log(colors.success('✅ manifest.json 校验通过(dry-run 模式,未生成文件)'))
|
|
66
|
+
console.log('')
|
|
67
|
+
console.log('📋 打包信息:')
|
|
68
|
+
console.log(` ID: ${manifest.id}`)
|
|
69
|
+
console.log(` 名称: ${manifest.name}`)
|
|
70
|
+
console.log(` 版本: ${manifest.version}`)
|
|
71
|
+
console.log(` 入口: ${manifest.entry}`)
|
|
72
|
+
console.log(` 文件数: ${manifest.files?.length || 1} + manifest.json`)
|
|
73
|
+
console.log('')
|
|
74
|
+
return
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Vite 编译 SFC → dist/index.js
|
|
78
|
+
const buildSpinner = ora('正在编译 Vue SFC (Vite)...').start()
|
|
79
|
+
let buildResult
|
|
80
|
+
try {
|
|
81
|
+
buildResult = await buildPlugin({ projectDir, manifest })
|
|
82
|
+
buildSpinner.succeed(`编译完成: dist/${buildResult.entryFileName}`)
|
|
83
|
+
} catch (err) {
|
|
84
|
+
buildSpinner.fail('编译失败')
|
|
85
|
+
console.log(colors.error(err.message))
|
|
86
|
+
if (err.stack) {
|
|
87
|
+
console.log(colors.warn(err.stack))
|
|
88
|
+
}
|
|
89
|
+
process.exit(1)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// 4. 重写 manifest:更新 entry 和 params 格式
|
|
93
|
+
const builtManifest = {
|
|
94
|
+
...manifest,
|
|
95
|
+
entry: 'index.js',
|
|
96
|
+
files: ['index.js'],
|
|
97
|
+
params: convertParamsToAppFormat(manifest.params || {}),
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 5. 打包
|
|
101
|
+
const packSpinner = ora('正在打包 .yizirwp...').start()
|
|
102
|
+
try {
|
|
103
|
+
const outputName = options.output || `${manifest.id}-${manifest.version}.yizirwp`
|
|
104
|
+
const result = await packFromDist(projectDir, outputName, builtManifest, { preview: options.preview })
|
|
105
|
+
|
|
106
|
+
packSpinner.succeed('打包成功!')
|
|
107
|
+
console.log('')
|
|
108
|
+
console.log(colors.success(`已生成: ${colors.file(result.outputPath)}`))
|
|
109
|
+
console.log('')
|
|
110
|
+
console.log('📋 打包信息:')
|
|
111
|
+
console.log(` ID: ${manifest.id}`)
|
|
112
|
+
console.log(` 名称: ${manifest.name}`)
|
|
113
|
+
console.log(` 版本: ${manifest.version}`)
|
|
114
|
+
console.log(` 原始入口: ${manifest.entry} (Vue SFC)`)
|
|
115
|
+
console.log(` 编译入口: index.js (IIFE bundle)`)
|
|
116
|
+
console.log(` 文件大小: ${result.fileSize}`)
|
|
117
|
+
console.log(` 文件数: ${result.fileCount}`)
|
|
118
|
+
console.log('')
|
|
119
|
+
console.log('📂 包含文件:')
|
|
120
|
+
console.log(` ✅ manifest.json`)
|
|
121
|
+
console.log(` ✅ index.js`)
|
|
122
|
+
console.log('')
|
|
123
|
+
console.log('🚀 下一步:')
|
|
124
|
+
console.log(' 在初尘3D壁纸引擎中上传此插件:')
|
|
125
|
+
console.log(` 1. 打开初尘3D壁纸引擎 → 功能面板 → 开发者中心`)
|
|
126
|
+
console.log(` 2. 点击「上传插件」`)
|
|
127
|
+
console.log(` 3. 选择 ${colors.code(result.outputPath)} 文件`)
|
|
128
|
+
console.log('')
|
|
129
|
+
|
|
130
|
+
} catch (err) {
|
|
131
|
+
packSpinner.fail('打包失败')
|
|
132
|
+
console.log(colors.error(err.message))
|
|
133
|
+
process.exit(1)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* example 命令 — 生成完整示例项目(粒子之舞)
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import ora from 'ora'
|
|
8
|
+
import { colors, printSeparator } from '../utils/colors.js'
|
|
9
|
+
import { generateDevProjectFiles } from '../templates/dev-project.js'
|
|
10
|
+
|
|
11
|
+
/** 生成完整示例项目 */
|
|
12
|
+
export async function cmdExample(projectName) {
|
|
13
|
+
console.log('')
|
|
14
|
+
console.log(colors.title('🌰 生成示例项目:粒子之舞'))
|
|
15
|
+
printSeparator()
|
|
16
|
+
|
|
17
|
+
// 检查目录是否已存在
|
|
18
|
+
const projectDir = path.resolve(process.cwd(), projectName)
|
|
19
|
+
if (fs.existsSync(projectDir)) {
|
|
20
|
+
console.log(colors.error(`目录已存在: ${projectName}`))
|
|
21
|
+
console.log(' 请删除或重命名现有目录后重试')
|
|
22
|
+
process.exit(1)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const spinner = ora('正在创建示例项目...').start()
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
// 1. 创建目录
|
|
29
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
30
|
+
|
|
31
|
+
// 2. 示例 manifest.json
|
|
32
|
+
const manifest = {
|
|
33
|
+
schemaVersion: '2.0',
|
|
34
|
+
id: projectName,
|
|
35
|
+
name: '粒子之舞',
|
|
36
|
+
version: '1.0.0',
|
|
37
|
+
author: { name: 'Chuchen Team' },
|
|
38
|
+
description: '粒子随音乐节奏舞动,鼠标交互产生涟漪效果',
|
|
39
|
+
tags: ['粒子', '动画', '交互'],
|
|
40
|
+
category: 'abstract',
|
|
41
|
+
entry: 'ParticleDance.vue',
|
|
42
|
+
files: ['ParticleDance.vue'],
|
|
43
|
+
params: {
|
|
44
|
+
particleCount: {
|
|
45
|
+
type: 'number',
|
|
46
|
+
default: 200,
|
|
47
|
+
min: 50,
|
|
48
|
+
max: 500,
|
|
49
|
+
step: 10,
|
|
50
|
+
label: '粒子数量',
|
|
51
|
+
},
|
|
52
|
+
speed: {
|
|
53
|
+
type: 'number',
|
|
54
|
+
default: 1.0,
|
|
55
|
+
min: 0.2,
|
|
56
|
+
max: 3.0,
|
|
57
|
+
step: 0.1,
|
|
58
|
+
label: '动画速度',
|
|
59
|
+
},
|
|
60
|
+
size: {
|
|
61
|
+
type: 'number',
|
|
62
|
+
default: 3.0,
|
|
63
|
+
min: 1.0,
|
|
64
|
+
max: 8.0,
|
|
65
|
+
step: 0.5,
|
|
66
|
+
label: '粒子大小',
|
|
67
|
+
},
|
|
68
|
+
colorPreset: {
|
|
69
|
+
type: 'select',
|
|
70
|
+
default: 'rainbow',
|
|
71
|
+
options: [
|
|
72
|
+
{ value: 'rainbow', label: '彩虹' },
|
|
73
|
+
{ value: 'ocean', label: '海洋蓝' },
|
|
74
|
+
{ value: 'sunset', label: '日落橙' },
|
|
75
|
+
{ value: 'neon', label: '霓虹紫' },
|
|
76
|
+
],
|
|
77
|
+
label: '配色方案',
|
|
78
|
+
},
|
|
79
|
+
trailLength: {
|
|
80
|
+
type: 'number',
|
|
81
|
+
default: 0.85,
|
|
82
|
+
min: 0,
|
|
83
|
+
max: 0.98,
|
|
84
|
+
step: 0.01,
|
|
85
|
+
label: '拖尾长度',
|
|
86
|
+
},
|
|
87
|
+
mouseInteraction: {
|
|
88
|
+
type: 'toggle',
|
|
89
|
+
default: true,
|
|
90
|
+
label: '鼠标交互',
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
previewBg: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
|
|
94
|
+
emoji: '💃',
|
|
95
|
+
minAppVersion: '1.0.0',
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 3. 写入 manifest.json
|
|
99
|
+
fs.writeFileSync(
|
|
100
|
+
path.join(projectDir, 'manifest.json'),
|
|
101
|
+
JSON.stringify(manifest, null, 2)
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
// 4. 写入 ParticleDance.vue
|
|
105
|
+
const vueContent = generateExampleVueComponent()
|
|
106
|
+
fs.writeFileSync(
|
|
107
|
+
path.join(projectDir, 'ParticleDance.vue'),
|
|
108
|
+
vueContent
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
// 5. 生成开发环境文件
|
|
112
|
+
const devFiles = generateDevProjectFiles('ParticleDance', manifest)
|
|
113
|
+
for (const [filename, content] of Object.entries(devFiles)) {
|
|
114
|
+
fs.writeFileSync(
|
|
115
|
+
path.join(projectDir, filename),
|
|
116
|
+
content
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
spinner.succeed('示例项目创建成功!')
|
|
121
|
+
console.log('')
|
|
122
|
+
console.log(colors.success(`项目已创建: ${colors.file(projectName)}/`))
|
|
123
|
+
console.log('')
|
|
124
|
+
console.log('📂 项目结构:')
|
|
125
|
+
console.log(` ${colors.file('manifest.json')} — 插件元数据`)
|
|
126
|
+
console.log(` ${colors.file('ParticleDance.vue')} — 粒子之舞组件`)
|
|
127
|
+
console.log(` ${colors.file('package.json')} — 开发依赖`)
|
|
128
|
+
console.log(` ${colors.file('vite.config.js')} — Vite 配置`)
|
|
129
|
+
console.log(` ${colors.file('index.html')} — 预览入口`)
|
|
130
|
+
console.log(` ${colors.file('dev-preview.js')} — Mock props 注入`)
|
|
131
|
+
console.log('')
|
|
132
|
+
console.log('🚀 下一步:')
|
|
133
|
+
console.log(` 1. cd ${colors.code(projectName)}`)
|
|
134
|
+
console.log(` 2. npm install`)
|
|
135
|
+
console.log(` 3. npm run dev # 启动预览服务器`)
|
|
136
|
+
console.log(` 4. 编辑 ParticleDance.vue 自定义效果`)
|
|
137
|
+
console.log(` 5. chuchen-plugin build # 打包为 .yizirwp`)
|
|
138
|
+
console.log('')
|
|
139
|
+
|
|
140
|
+
} catch (err) {
|
|
141
|
+
spinner.fail('示例项目创建失败')
|
|
142
|
+
console.log(colors.error(err.message))
|
|
143
|
+
process.exit(1)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** 生成示例 Vue 组件 */
|
|
148
|
+
function generateExampleVueComponent() {
|
|
149
|
+
return `<template>
|
|
150
|
+
<canvas ref="canvasRef" class="wallpaper-canvas"></canvas>
|
|
151
|
+
</template>
|
|
152
|
+
|
|
153
|
+
<script setup>
|
|
154
|
+
/**
|
|
155
|
+
* ════════════════════════════════════════════════════════════
|
|
156
|
+
* 💃 粒子之舞 (Particle Dance) — 初尘3D壁纸插件示例
|
|
157
|
+
* ════════════════════════════════════════════════════════════
|
|
158
|
+
*
|
|
159
|
+
* 特性:
|
|
160
|
+
* - 粒子系统 + 鼠标交互
|
|
161
|
+
* - 多种配色方案
|
|
162
|
+
* - 拖尾效果
|
|
163
|
+
* - 性能模式兼容
|
|
164
|
+
*/
|
|
165
|
+
|
|
166
|
+
import { ref, onMounted, onUnmounted } from 'vue'
|
|
167
|
+
|
|
168
|
+
const props = defineProps({
|
|
169
|
+
containerRef: { type: Object, default: null },
|
|
170
|
+
paramValues: { type: Object, default: () => ({}) },
|
|
171
|
+
performancePolicy: { type: Object, default: () => ({}) },
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
const canvasRef = ref(null)
|
|
175
|
+
|
|
176
|
+
// ===== 参数读取 =====
|
|
177
|
+
function getParam(key, defaultValue) {
|
|
178
|
+
return props.paramValues?.[key] !== undefined ? props.paramValues[key] : defaultValue
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ===== 核心变量 =====
|
|
182
|
+
let animationId = null
|
|
183
|
+
let lastFrameTime = 0
|
|
184
|
+
let particles = []
|
|
185
|
+
let mouseX = 0, mouseY = 0
|
|
186
|
+
|
|
187
|
+
// ===== 颜色预设 =====
|
|
188
|
+
const COLOR_PRESETS = {
|
|
189
|
+
rainbow: ['#ff6b6b', '#feca57', '#48dbfb', '#ff9ff3', '#54a0ff'],
|
|
190
|
+
ocean: ['#0077b6', '#00b4d8', '#90e0ef', '#caf0f8', '#48cae4'],
|
|
191
|
+
sunset: ['#ff7b00', '#ff8800', '#ffa200', '#ffc300', '#ffe600'],
|
|
192
|
+
neon: ['#9d4edd', '#c77dff', '#e0aaff', '#7b2cbf', '#5a189a'],
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// ===== 粒子类 =====
|
|
196
|
+
class Particle {
|
|
197
|
+
constructor(width, height) {
|
|
198
|
+
this.x = Math.random() * width
|
|
199
|
+
this.y = Math.random() * height
|
|
200
|
+
this.vx = (Math.random() - 0.5) * 2
|
|
201
|
+
this.vy = (Math.random() - 0.5) * 2
|
|
202
|
+
this.life = 1.0
|
|
203
|
+
this.decay = 0.002 + Math.random() * 0.01
|
|
204
|
+
this.size = 1 + Math.random() * 2
|
|
205
|
+
this.hue = Math.random() * 360
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
update(speed, width, height, mx, my, interaction) {
|
|
209
|
+
// 基础运动
|
|
210
|
+
this.x += this.vx * speed
|
|
211
|
+
this.y += this.vy * speed
|
|
212
|
+
|
|
213
|
+
// 鼠标交互:吸引/排斥
|
|
214
|
+
if (interaction && mx > 0 && my > 0) {
|
|
215
|
+
const dx = mx - this.x
|
|
216
|
+
const dy = my - this.y
|
|
217
|
+
const dist = Math.sqrt(dx * dx + dy * dy)
|
|
218
|
+
if (dist < 150) {
|
|
219
|
+
const force = (150 - dist) / 150
|
|
220
|
+
this.vx -= (dx / dist) * force * 0.5
|
|
221
|
+
this.vy -= (dy / dist) * force * 0.5
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// 边界处理
|
|
226
|
+
if (this.x < 0 || this.x > width) this.vx *= -1
|
|
227
|
+
if (this.y < 0 || this.y > height) this.vy *= -1
|
|
228
|
+
|
|
229
|
+
// 生命衰减
|
|
230
|
+
this.life -= this.decay / speed
|
|
231
|
+
if (this.life <= 0) {
|
|
232
|
+
this.reset(width, height)
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
reset(width, height) {
|
|
237
|
+
this.x = Math.random() * width
|
|
238
|
+
this.y = Math.random() * height
|
|
239
|
+
this.vx = (Math.random() - 0.5) * 2
|
|
240
|
+
this.vy = (Math.random() - 0.5) * 2
|
|
241
|
+
this.life = 1.0
|
|
242
|
+
this.hue = Math.random() * 360
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// ===== 初始化 =====
|
|
247
|
+
function init() {
|
|
248
|
+
const canvas = canvasRef.value
|
|
249
|
+
const container = props.containerRef || canvas.parentElement
|
|
250
|
+
|
|
251
|
+
if (!canvas || !container) return
|
|
252
|
+
|
|
253
|
+
const ctx = canvas.getContext('2d')
|
|
254
|
+
|
|
255
|
+
function resize() {
|
|
256
|
+
const w = container.clientWidth || window.innerWidth
|
|
257
|
+
const h = container.clientHeight || window.innerHeight
|
|
258
|
+
canvas.width = w
|
|
259
|
+
canvas.height = h
|
|
260
|
+
}
|
|
261
|
+
resize()
|
|
262
|
+
|
|
263
|
+
// 初始化粒子
|
|
264
|
+
initParticles()
|
|
265
|
+
|
|
266
|
+
// 事件绑定
|
|
267
|
+
if (container) {
|
|
268
|
+
container.addEventListener('mousemove', onMouseMove)
|
|
269
|
+
}
|
|
270
|
+
window.addEventListener('resize', resize)
|
|
271
|
+
|
|
272
|
+
animate()
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function initParticles() {
|
|
276
|
+
const count = getParam('particleCount', 200)
|
|
277
|
+
const canvas = canvasRef.value
|
|
278
|
+
particles = []
|
|
279
|
+
for (let i = 0; i < count; i++) {
|
|
280
|
+
particles.push(new Particle(canvas.width, canvas.height))
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ===== 动画循环 =====
|
|
285
|
+
function animate(timestamp) {
|
|
286
|
+
animationId = requestAnimationFrame(animate)
|
|
287
|
+
|
|
288
|
+
const fpsLimit = props.performancePolicy?.fpsLimit || 60
|
|
289
|
+
if (fpsLimit > 0 && fpsLimit < 60) {
|
|
290
|
+
if (!lastFrameTime) lastFrameTime = timestamp
|
|
291
|
+
if (timestamp - lastFrameTime < 1000 / fpsLimit) return
|
|
292
|
+
lastFrameTime = timestamp
|
|
293
|
+
} else if (fpsLimit === 0) {
|
|
294
|
+
return
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const canvas = canvasRef.value
|
|
298
|
+
if (!canvas) return
|
|
299
|
+
const ctx = canvas.getContext('2d')
|
|
300
|
+
const width = canvas.width
|
|
301
|
+
const height = canvas.height
|
|
302
|
+
|
|
303
|
+
// 读取参数
|
|
304
|
+
const speed = getParam('speed', 1.0)
|
|
305
|
+
const size = getParam('size', 3.0)
|
|
306
|
+
const colorPreset = getParam('colorPreset', 'rainbow')
|
|
307
|
+
const trailLength = getParam('trailLength', 0.85)
|
|
308
|
+
const interaction = getParam('mouseInteraction', true)
|
|
309
|
+
|
|
310
|
+
// 拖尾效果
|
|
311
|
+
ctx.fillStyle = \`rgba(10, 10, 20, \${1 - trailLength})\`
|
|
312
|
+
ctx.fillRect(0, 0, width, height)
|
|
313
|
+
|
|
314
|
+
// 获取颜色数组
|
|
315
|
+
const colors = COLOR_PRESETS[colorPreset] || COLOR_PRESETS.rainbow
|
|
316
|
+
|
|
317
|
+
// 更新和绘制粒子
|
|
318
|
+
for (const p of particles) {
|
|
319
|
+
p.update(speed, width, height, mouseX, mouseY, interaction)
|
|
320
|
+
|
|
321
|
+
const alpha = p.life
|
|
322
|
+
const colorIndex = Math.floor(p.hue / 72) % colors.length
|
|
323
|
+
const color = colors[colorIndex]
|
|
324
|
+
|
|
325
|
+
ctx.beginPath()
|
|
326
|
+
ctx.arc(p.x, p.y, p.size * size * alpha, 0, Math.PI * 2)
|
|
327
|
+
ctx.fillStyle = hexToRgba(color, alpha * 0.8)
|
|
328
|
+
ctx.fill()
|
|
329
|
+
|
|
330
|
+
// 发光效果
|
|
331
|
+
ctx.shadowBlur = size * 2
|
|
332
|
+
ctx.shadowColor = color
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
ctx.shadowBlur = 0
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ===== 工具函数 =====
|
|
339
|
+
function hexToRgba(hex, alpha) {
|
|
340
|
+
const r = parseInt(hex.slice(1, 3), 16)
|
|
341
|
+
const g = parseInt(hex.slice(3, 5), 16)
|
|
342
|
+
const b = parseInt(hex.slice(5, 7), 16)
|
|
343
|
+
return \`rgba(\${r},\${g},\${b},\${alpha})\`
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
// ===== 事件处理 =====
|
|
347
|
+
function onMouseMove(event) {
|
|
348
|
+
const rect = event.currentTarget.getBoundingClientRect()
|
|
349
|
+
mouseX = ((event.clientX - rect.left) / rect.width) * canvasRef.value.width
|
|
350
|
+
mouseY = ((event.clientY - rect.top) / rect.height) * canvasRef.value.height
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// ===== 生命周期 =====
|
|
354
|
+
onMounted(() => {
|
|
355
|
+
init()
|
|
356
|
+
})
|
|
357
|
+
|
|
358
|
+
onUnmounted(() => {
|
|
359
|
+
if (animationId) cancelAnimationFrame(animationId)
|
|
360
|
+
if (props.containerRef) {
|
|
361
|
+
props.containerRef.removeEventListener('mousemove', onMouseMove)
|
|
362
|
+
}
|
|
363
|
+
window.removeEventListener('resize', () => {})
|
|
364
|
+
})
|
|
365
|
+
</script>
|
|
366
|
+
|
|
367
|
+
<style scoped>
|
|
368
|
+
.wallpaper-canvas {
|
|
369
|
+
width: 100%;
|
|
370
|
+
height: 100%;
|
|
371
|
+
display: block;
|
|
372
|
+
}
|
|
373
|
+
</style>
|
|
374
|
+
`
|
|
375
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* init 命令 — 初始化壁纸插件项目
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'fs'
|
|
6
|
+
import path from 'path'
|
|
7
|
+
import ora from 'ora'
|
|
8
|
+
import { colors, printSeparator } from '../utils/colors.js'
|
|
9
|
+
import { generateManifest } from '../templates/manifest.js'
|
|
10
|
+
import { generateVueComponentTemplate } from '../templates/vue-component.js'
|
|
11
|
+
import { generateDevProjectFiles } from '../templates/dev-project.js'
|
|
12
|
+
|
|
13
|
+
/** 初始化壁纸插件项目 */
|
|
14
|
+
export async function cmdInit(projectName, options = {}) {
|
|
15
|
+
console.log('')
|
|
16
|
+
console.log(colors.title('📁 初始化壁纸插件项目'))
|
|
17
|
+
printSeparator()
|
|
18
|
+
|
|
19
|
+
// 校验项目名称
|
|
20
|
+
if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(projectName)) {
|
|
21
|
+
console.log(colors.error('项目名称格式错误'))
|
|
22
|
+
console.log(' 仅允许小写字母、数字、连字符(如: my-wallpaper)')
|
|
23
|
+
process.exit(1)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// 检查目录是否已存在
|
|
27
|
+
const projectDir = path.resolve(process.cwd(), projectName)
|
|
28
|
+
if (fs.existsSync(projectDir)) {
|
|
29
|
+
console.log(colors.error(`目录已存在: ${projectName}`))
|
|
30
|
+
console.log(' 请删除或重命名现有目录后重试')
|
|
31
|
+
process.exit(1)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const spinner = ora('正在创建项目...').start()
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
// 1. 生成 manifest.json
|
|
38
|
+
const manifest = generateManifest(projectName, options)
|
|
39
|
+
|
|
40
|
+
// 2. 创建目录
|
|
41
|
+
fs.mkdirSync(projectDir, { recursive: true })
|
|
42
|
+
|
|
43
|
+
// 3. 写入 manifest.json
|
|
44
|
+
fs.writeFileSync(
|
|
45
|
+
path.join(projectDir, 'manifest.json'),
|
|
46
|
+
JSON.stringify(manifest, null, 2)
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
// 4. 生成 Vue 组件
|
|
50
|
+
const componentName = toPascalCase(projectName)
|
|
51
|
+
const vueTemplate = generateVueComponentTemplate(componentName, manifest)
|
|
52
|
+
fs.writeFileSync(
|
|
53
|
+
path.join(projectDir, manifest.entry),
|
|
54
|
+
vueTemplate
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
// 5. 生成开发环境文件
|
|
58
|
+
const devFiles = generateDevProjectFiles(componentName, manifest)
|
|
59
|
+
for (const [filename, content] of Object.entries(devFiles)) {
|
|
60
|
+
fs.writeFileSync(
|
|
61
|
+
path.join(projectDir, filename),
|
|
62
|
+
content
|
|
63
|
+
)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 6. 生成 README.md
|
|
67
|
+
const readme = generateReadme(manifest, componentName)
|
|
68
|
+
fs.writeFileSync(
|
|
69
|
+
path.join(projectDir, 'README.md'),
|
|
70
|
+
readme
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
// 7. 生成 ICON_GUIDE.md
|
|
74
|
+
const iconGuide = generateIconGuide()
|
|
75
|
+
fs.writeFileSync(
|
|
76
|
+
path.join(projectDir, 'ICON_GUIDE.md'),
|
|
77
|
+
iconGuide
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
spinner.succeed('项目创建成功!')
|
|
81
|
+
console.log('')
|
|
82
|
+
console.log(colors.success(`项目已创建: ${colors.file(projectName)}/`))
|
|
83
|
+
console.log('')
|
|
84
|
+
console.log('📂 项目结构:')
|
|
85
|
+
console.log(` ${colors.file('manifest.json')} — 插件元数据`)
|
|
86
|
+
console.log(` ${colors.file(manifest.entry)} — 壁纸组件(主要编辑文件)`)
|
|
87
|
+
console.log(` ${colors.file('package.json')} — 开发依赖`)
|
|
88
|
+
console.log(` ${colors.file('vite.config.js')} — Vite 配置`)
|
|
89
|
+
console.log(` ${colors.file('index.html')} — 预览入口`)
|
|
90
|
+
console.log(` ${colors.file('dev-preview.js')} — Mock props 注入`)
|
|
91
|
+
console.log(` ${colors.file('README.md')} — 开发说明`)
|
|
92
|
+
console.log(` ${colors.file('ICON_GUIDE.md')} — 图标制作指南`)
|
|
93
|
+
console.log('')
|
|
94
|
+
console.log('🚀 下一步:')
|
|
95
|
+
console.log(` 1. cd ${colors.code(projectName)}`)
|
|
96
|
+
console.log(` 2. npm install`)
|
|
97
|
+
console.log(` 3. npm run dev # 启动预览服务器`)
|
|
98
|
+
console.log(` 4. 编辑 ${colors.code(manifest.entry)} 实现你的壁纸`)
|
|
99
|
+
console.log(` 5. chuchen-plugin build # 打包为 .yizirwp`)
|
|
100
|
+
console.log('')
|
|
101
|
+
|
|
102
|
+
} catch (err) {
|
|
103
|
+
spinner.fail('项目创建失败')
|
|
104
|
+
console.log(colors.error(err.message))
|
|
105
|
+
process.exit(1)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** 生成 README.md */
|
|
110
|
+
function generateReadme(manifest, componentName) {
|
|
111
|
+
return `# ${manifest.name}
|
|
112
|
+
|
|
113
|
+
一款精美的初尘3D壁纸插件
|
|
114
|
+
|
|
115
|
+
**作者**: ${manifest.author?.name || '开发者'}
|
|
116
|
+
**版本**: ${manifest.version}
|
|
117
|
+
**分类**: ${manifest.category}
|
|
118
|
+
|
|
119
|
+
## 开发指南
|
|
120
|
+
|
|
121
|
+
### 1. 安装依赖
|
|
122
|
+
|
|
123
|
+
\`\`\`bash
|
|
124
|
+
npm install
|
|
125
|
+
\`\`\`
|
|
126
|
+
|
|
127
|
+
### 2. 启动预览
|
|
128
|
+
|
|
129
|
+
\`\`\`bash
|
|
130
|
+
npm run dev
|
|
131
|
+
\`\`\`
|
|
132
|
+
|
|
133
|
+
浏览器将自动打开预览页面,右侧有参数调节面板。
|
|
134
|
+
|
|
135
|
+
### 3. 编辑壁纸
|
|
136
|
+
|
|
137
|
+
打开 \`${manifest.entry}\`,在 \`init()\` 和 \`animate()\` 中实现你的动画逻辑。
|
|
138
|
+
|
|
139
|
+
### 4. 调整参数
|
|
140
|
+
|
|
141
|
+
在 \`manifest.json\` 的 \`params\` 字段中定义可调参数,预览页面的参数面板会自动更新。
|
|
142
|
+
|
|
143
|
+
### 5. 打包发布
|
|
144
|
+
|
|
145
|
+
\`\`\`bash
|
|
146
|
+
chuchen-plugin build
|
|
147
|
+
\`\`\`
|
|
148
|
+
|
|
149
|
+
生成 \`${manifest.id}-${manifest.version}.yizirwp\` 文件,可在初尘3D壁纸中安装。
|
|
150
|
+
|
|
151
|
+
## 参数说明
|
|
152
|
+
|
|
153
|
+
${Object.entries(manifest.params || {}).map(([key, def]) => {
|
|
154
|
+
return `- **${def.label || key}** (\`${key}\`): ${def.type} 类型,默认值 \`${def.default}\`${def.min !== undefined ? `,范围 ${def.min}~${def.max}` : ''}`
|
|
155
|
+
}).join('\n')}
|
|
156
|
+
`
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** 生成 ICON_GUIDE.md */
|
|
160
|
+
function generateIconGuide() {
|
|
161
|
+
return `# 缩略图制作指南
|
|
162
|
+
|
|
163
|
+
为壁纸插件添加缩略图,让用户在商店中快速预览效果。
|
|
164
|
+
|
|
165
|
+
## 要求
|
|
166
|
+
|
|
167
|
+
- **格式**: PNG / JPG / SVG
|
|
168
|
+
- **尺寸**: 256×256 像素(推荐)
|
|
169
|
+
- **文件名**: \`thumbnail.png\`
|
|
170
|
+
- **位置**: 项目根目录
|
|
171
|
+
|
|
172
|
+
## 制作步骤
|
|
173
|
+
|
|
174
|
+
### 方法 1: 从预览截图
|
|
175
|
+
|
|
176
|
+
1. 运行 \`npm run dev\` 启动预览
|
|
177
|
+
2. 调整参数到最佳效果
|
|
178
|
+
3. 截图并裁剪为 256×256
|
|
179
|
+
4. 保存为 \`thumbnail.png\`
|
|
180
|
+
|
|
181
|
+
### 方法 2: 使用设计工具
|
|
182
|
+
|
|
183
|
+
1. 打开 Figma / Photoshop / Sketch
|
|
184
|
+
2. 创建 256×256 画布
|
|
185
|
+
3. 设计缩略图(建议包含壁纸核心元素)
|
|
186
|
+
4. 导出为 PNG
|
|
187
|
+
|
|
188
|
+
## 注意事项
|
|
189
|
+
|
|
190
|
+
- 避免文字过多(小尺寸下看不清)
|
|
191
|
+
- 使用高对比度颜色
|
|
192
|
+
- 突出壁纸的核心视觉效果
|
|
193
|
+
- 文件大小建议 < 50KB
|
|
194
|
+
`
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** 工具函数:kebab-case → PascalCase */
|
|
198
|
+
function toPascalCase(str) {
|
|
199
|
+
return str.replace(/(?:^|-)(\w)/g, (_, c) => c.toUpperCase())
|
|
200
|
+
}
|