evomeme-dev 0.1.7

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 (101) hide show
  1. package/README.md +247 -0
  2. package/bin/evomeme +28 -0
  3. package/dist/canvas-public/app.js +5857 -0
  4. package/dist/canvas-public/cycle-detect.js +166 -0
  5. package/dist/canvas-public/index.html +395 -0
  6. package/dist/canvas-public/sequence-sheet.js +59 -0
  7. package/dist/canvas-public/smart-split.js +185 -0
  8. package/dist/canvas-public/styles.css +2276 -0
  9. package/dist/canvas-public/vendor/painterro.min.js +3 -0
  10. package/dist/index.js +25586 -0
  11. package/package.json +69 -0
  12. package/skills/evomeme-cloud-fs/SKILL.md +176 -0
  13. package/skills/evomeme-cloud-fs/agents/openai.yaml +7 -0
  14. package/skills/evomeme-cover/SKILL.md +132 -0
  15. package/skills/evomeme-cover/agents/openai.yaml +7 -0
  16. package/skills/evomeme-game-maker/SKILL.md +214 -0
  17. package/skills/evomeme-game-maker/agents/openai.yaml +4 -0
  18. package/skills/evomeme-login/SKILL.md +92 -0
  19. package/skills/evomeme-login/agents/openai.yaml +7 -0
  20. package/skills/evomeme-native-bridge-sdk/SKILL.md +286 -0
  21. package/skills/evomeme-native-bridge-sdk/agents/openai.yaml +7 -0
  22. package/skills/evomeme-tos-upload/SKILL.md +46 -0
  23. package/skills/evomeme-tos-upload/agents/openai.yaml +4 -0
  24. package/skills/game-art/SKILL.md +121 -0
  25. package/skills/game-art/agents/openai.yaml +7 -0
  26. package/skills/game-asset-image-generator/SKILL.md +104 -0
  27. package/skills/game-asset-image-generator/agents/openai.yaml +7 -0
  28. package/skills/game-asset-image-generator/references/background.md +32 -0
  29. package/skills/game-asset-image-generator/references/static-game-assets.md +38 -0
  30. package/skills/game-audio/SKILL.md +82 -0
  31. package/skills/game-audio/agents/openai.yaml +4 -0
  32. package/skills/game-audio/references/bgm-cdn.md +127 -0
  33. package/skills/game-audio/references/sfx-cdn.md +77 -0
  34. package/skills/game-demo/SKILL.md +103 -0
  35. package/skills/game-demo/agents/openai.yaml +7 -0
  36. package/skills/game-maker/SKILL.md +111 -0
  37. package/skills/game-maker/agents/openai.yaml +4 -0
  38. package/skills/game-test/SKILL.md +58 -0
  39. package/skills/game-test/agents/openai.yaml +7 -0
  40. package/skills/game-test/references/checklists/playtest-qa.md +12 -0
  41. package/skills/game-ui/SKILL.md +169 -0
  42. package/skills/game-ui/agents/openai.yaml +4 -0
  43. package/skills/game-ui/references/checklists/game-ui-quality.md +14 -0
  44. package/skills/game-ui/references/checklists/hud-readability.md +12 -0
  45. package/skills/game-ui/references/checklists/mobile-input.md +12 -0
  46. package/skills/game-ui/references/checklists/responsive-ui-fit.md +12 -0
  47. package/skills/game-ui/references/ui-patterns.md +140 -0
  48. package/skills/game-ui/references/ui-prompt-templates.md +63 -0
  49. package/skills/phaser-2d-game/SKILL.md +127 -0
  50. package/skills/phaser-2d-game/agents/openai.yaml +7 -0
  51. package/skills/phaser-2d-game/references/engine-performance.md +64 -0
  52. package/skills/phaser-2d-game/references/engine-physics.md +139 -0
  53. package/skills/phaser-2d-game/references/engine-runtime.md +129 -0
  54. package/skills/phaser-2d-game/references/engine-spritesheets.md +99 -0
  55. package/skills/phaser-2d-game/references/engine-tilemaps.md +80 -0
  56. package/skills/phaser-2d-game/references/file-structure.md +94 -0
  57. package/skills/phaser-2d-graphics-builder/SKILL.md +68 -0
  58. package/skills/phaser-2d-graphics-builder/agents/openai.yaml +7 -0
  59. package/skills/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-1.jpeg +0 -0
  60. package/skills/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-2.jpeg +0 -0
  61. package/skills/phaser-2d-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
  62. package/skills/phaser-2d-graphics-builder/references/checklists/compose-quality.md +10 -0
  63. package/skills/phaser-2d-graphics-builder/references/checklists/performance-safe-visual-detail.md +9 -0
  64. package/skills/phaser-2d-graphics-builder/references/checklists/premium-game-quality-gate.md +15 -0
  65. package/skills/phaser-2d-graphics-builder/references/checklists/sprite-quality.md +10 -0
  66. package/skills/phaser-2d-graphics-builder/references/checklists/technical-art-quality.md +9 -0
  67. package/skills/phaser-2d-graphics-builder/references/checklists/visual-scorecard.md +9 -0
  68. package/skills/phaser-2d-graphics-builder/references/compose-recipes.md +71 -0
  69. package/skills/phaser-2d-graphics-builder/references/graphics-prompt-templates.md +33 -0
  70. package/skills/phaser-2d-graphics-builder/references/implementation-blueprint.md +82 -0
  71. package/skills/phaser-2d-graphics-builder/references/sprite-recipes.md +90 -0
  72. package/skills/phaser-2d-graphics-builder/references/technical-art.md +67 -0
  73. package/skills/phaser-2d-graphics-builder/references/vfx-recipes.md +67 -0
  74. package/skills/phaser-2d-graphics-builder/references/visual-scorecard.md +129 -0
  75. package/skills/threejs-3d-game/SKILL.md +113 -0
  76. package/skills/threejs-3d-game/agents/openai.yaml +7 -0
  77. package/skills/threejs-3d-game/references/engine-asset-pipeline.md +116 -0
  78. package/skills/threejs-3d-game/references/engine-audio.md +70 -0
  79. package/skills/threejs-3d-game/references/engine-input-camera.md +84 -0
  80. package/skills/threejs-3d-game/references/engine-performance.md +96 -0
  81. package/skills/threejs-3d-game/references/engine-physics.md +142 -0
  82. package/skills/threejs-3d-game/references/engine-runtime.md +199 -0
  83. package/skills/threejs-3d-game/references/file-structure.md +353 -0
  84. package/skills/threejs-aaa-graphics-builder/SKILL.md +62 -0
  85. package/skills/threejs-aaa-graphics-builder/agents/openai.yaml +7 -0
  86. package/skills/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-1.jpg +0 -0
  87. package/skills/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-2.jpg +0 -0
  88. package/skills/threejs-aaa-graphics-builder/assets/scorecard-anchors/scene-3.jpg +0 -0
  89. package/skills/threejs-aaa-graphics-builder/references/checklists/aaa-game-quality-gate.md +23 -0
  90. package/skills/threejs-aaa-graphics-builder/references/checklists/aaa-visual-scorecard.md +19 -0
  91. package/skills/threejs-aaa-graphics-builder/references/checklists/material-lighting-quality.md +14 -0
  92. package/skills/threejs-aaa-graphics-builder/references/checklists/performance-safe-visual-detail.md +14 -0
  93. package/skills/threejs-aaa-graphics-builder/references/checklists/procedural-model-quality.md +15 -0
  94. package/skills/threejs-aaa-graphics-builder/references/checklists/technical-art-quality.md +17 -0
  95. package/skills/threejs-aaa-graphics-builder/references/graphics-prompt-templates.md +236 -0
  96. package/skills/threejs-aaa-graphics-builder/references/implementation-blueprint.md +161 -0
  97. package/skills/threejs-aaa-graphics-builder/references/model-recipes.md +146 -0
  98. package/skills/threejs-aaa-graphics-builder/references/render-recipes.md +90 -0
  99. package/skills/threejs-aaa-graphics-builder/references/shader-cookbook.md +292 -0
  100. package/skills/threejs-aaa-graphics-builder/references/technical-art.md +167 -0
  101. package/skills/threejs-aaa-graphics-builder/references/visual-scorecard.md +147 -0
@@ -0,0 +1,166 @@
1
+ /**
2
+ * 浏览器端:缩略灰度 + z-score + 点积,从前向后找最早高相似帧。
3
+ */
4
+
5
+ function zscoreInPlace(vec) {
6
+ let sum = 0
7
+ for (let i = 0; i < vec.length; i++) sum += vec[i]
8
+ const mean = sum / vec.length
9
+ let varSum = 0
10
+ for (let i = 0; i < vec.length; i++) {
11
+ const d = vec[i] - mean
12
+ varSum += d * d
13
+ }
14
+ const std = Math.sqrt(varSum / vec.length) + 1e-8
15
+ for (let i = 0; i < vec.length; i++) vec[i] = (vec[i] - mean) / std
16
+ return vec
17
+ }
18
+
19
+ function featureFromImageData(imageData) {
20
+ const { data, width, height } = imageData
21
+ const n = width * height
22
+ const vec = new Float32Array(n)
23
+ for (let i = 0, p = 0; i < n; i++, p += 4) {
24
+ // ITU-R BT.601 亮度
25
+ vec[i] = data[p] * 0.299 + data[p + 1] * 0.587 + data[p + 2] * 0.114
26
+ }
27
+ return zscoreInPlace(vec)
28
+ }
29
+
30
+ function nccScore(a, b) {
31
+ const n = Math.min(a.length, b.length)
32
+ if (!n) return 0
33
+ let sum = 0
34
+ for (let i = 0; i < n; i++) sum += a[i] * b[i]
35
+ return sum / n
36
+ }
37
+
38
+ function waitEvent(target, event, { signal } = {}) {
39
+ return new Promise((resolve, reject) => {
40
+ if (signal?.aborted) {
41
+ reject(new DOMException('Aborted', 'AbortError'))
42
+ return
43
+ }
44
+ const onAbort = () => {
45
+ cleanup()
46
+ reject(new DOMException('Aborted', 'AbortError'))
47
+ }
48
+ const onOk = () => {
49
+ cleanup()
50
+ resolve()
51
+ }
52
+ const cleanup = () => {
53
+ target.removeEventListener(event, onOk)
54
+ signal?.removeEventListener('abort', onAbort)
55
+ }
56
+ target.addEventListener(event, onOk, { once: true })
57
+ signal?.addEventListener('abort', onAbort, { once: true })
58
+ })
59
+ }
60
+
61
+ async function seekVideo(video, time, signal) {
62
+ const duration = Number(video.duration)
63
+ const maxT = Number.isFinite(duration) && duration > 0 ? Math.max(0, duration - 1e-4) : time
64
+ const t = Math.min(Math.max(0, time), maxT)
65
+ if (Math.abs((video.currentTime || 0) - t) < 1e-4) return
66
+ const p = waitEvent(video, 'seeked', { signal })
67
+ video.currentTime = t
68
+ await p
69
+ }
70
+
71
+ function drawFeature(video, canvas, ctx, thumbWidth) {
72
+ const vw = video.videoWidth || 1
73
+ const vh = video.videoHeight || 1
74
+ const w = Math.max(8, Math.round(thumbWidth))
75
+ const h = Math.max(8, Math.round((vh / vw) * w))
76
+ if (canvas.width !== w) canvas.width = w
77
+ if (canvas.height !== h) canvas.height = h
78
+ ctx.drawImage(video, 0, 0, w, h)
79
+ return featureFromImageData(ctx.getImageData(0, 0, w, h))
80
+ }
81
+
82
+ /**
83
+ * @param {string} videoUrl
84
+ * @param {{
85
+ * threshold?: number,
86
+ * minGapSec?: number,
87
+ * thumbWidth?: number,
88
+ * onProgress?: (info: object) => void,
89
+ * signal?: AbortSignal,
90
+ * }} [options]
91
+ * @returns {Promise<{ start: number, end: number, score: number, fps: number, scanned: number }>}
92
+ */
93
+ export async function detectFirstCycle(videoUrl, options = {}) {
94
+ const threshold = Math.min(0.999, Math.max(0.5, Number(options.threshold) || 0.98))
95
+ const minGapSec = Math.max(0.05, Number(options.minGapSec) || 0.4)
96
+ const thumbWidth = Math.max(16, Math.round(Number(options.thumbWidth) || 96))
97
+ const onProgress = typeof options.onProgress === 'function' ? options.onProgress : null
98
+ const signal = options.signal
99
+
100
+ const video = document.createElement('video')
101
+ video.muted = true
102
+ video.playsInline = true
103
+ video.preload = 'auto'
104
+ video.crossOrigin = 'anonymous'
105
+
106
+ const canvas = document.createElement('canvas')
107
+ const ctx = canvas.getContext('2d', { willReadFrequently: true })
108
+ if (!ctx) throw new Error('无法创建 canvas')
109
+
110
+ try {
111
+ const loaded = waitEvent(video, 'loadedmetadata', { signal })
112
+ video.src = videoUrl
113
+ video.load()
114
+ await loaded
115
+ if (!video.videoWidth || !Number.isFinite(video.duration) || video.duration <= 0) {
116
+ throw new Error('无法读取视频元数据')
117
+ }
118
+
119
+ const duration = video.duration
120
+ const step = 1 / 24
121
+ const minT = Math.min(duration - step, minGapSec)
122
+ if (!(minT > step)) {
123
+ throw new Error('视频太短,无法提取周期')
124
+ }
125
+
126
+ await seekVideo(video, 0, signal)
127
+ // 部分浏览器 seek(0) 后需一帧稳定
128
+ await new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))
129
+ const ref = drawFeature(video, canvas, ctx, thumbWidth)
130
+
131
+ let scanned = 0
132
+ for (let t = minT; t < duration - 1e-4; t += step) {
133
+ if (signal?.aborted) throw new DOMException('Aborted', 'AbortError')
134
+ await seekVideo(video, t, signal)
135
+ const feat = drawFeature(video, canvas, ctx, thumbWidth)
136
+ const score = nccScore(ref, feat)
137
+ scanned += 1
138
+ onProgress?.({
139
+ time: t,
140
+ score,
141
+ scanned,
142
+ duration,
143
+ threshold,
144
+ })
145
+ if (score >= threshold) {
146
+ return {
147
+ start: 0,
148
+ end: Math.min(t, duration),
149
+ score,
150
+ fps: 24,
151
+ scanned,
152
+ threshold,
153
+ minGapSec,
154
+ }
155
+ }
156
+ }
157
+
158
+ throw new Error(
159
+ `未找到相似度 ≥ ${(threshold * 100).toFixed(0)}% 的周期终点(已扫 ${scanned} 帧,最短间隔 ${minGapSec}s)`,
160
+ )
161
+ } finally {
162
+ video.pause()
163
+ video.removeAttribute('src')
164
+ video.load()
165
+ }
166
+ }
@@ -0,0 +1,395 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Image Canvas</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
9
+ <link
10
+ href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,400;0,9..40,500;0,9..40,600;0,9..40,700;1,9..40,400&family=JetBrains+Mono:wght@400;500&display=swap"
11
+ rel="stylesheet"
12
+ />
13
+ <link rel="stylesheet" href="/styles.css" />
14
+ </head>
15
+ <body>
16
+ <div class="welcome" id="welcome">
17
+ <div class="welcome-card">
18
+ <p class="welcome-brand">Image Canvas</p>
19
+ <h1>打开工程目录</h1>
20
+ <p class="welcome-desc">
21
+ 选择一个本地文件夹作为工程。画布、上传与生成结果会保存在该目录下的
22
+ <code>.image-canvas/</code> 中。
23
+ </p>
24
+ <button type="button" class="primary-btn" id="welcome-open-btn">打开工程…</button>
25
+ <div class="welcome-recent" id="welcome-recent" hidden>
26
+ <h2>最近打开</h2>
27
+ <ul id="welcome-recent-list"></ul>
28
+ </div>
29
+ <p class="welcome-error" id="welcome-error" hidden></p>
30
+ </div>
31
+ </div>
32
+
33
+ <div class="app" id="app" hidden>
34
+ <div class="canvas-pane">
35
+ <aside class="inspector" id="inspector" hidden>
36
+ <header class="inspector-header">
37
+ <h2 id="inspector-title">属性</h2>
38
+ <button type="button" class="ghost-btn" id="inspector-close" title="取消选中">×</button>
39
+ </header>
40
+ <div class="inspector-body" id="inspector-body"></div>
41
+ </aside>
42
+ <div class="canvas-main">
43
+ <div class="canvas-shell">
44
+ <div class="canvas-toolbar">
45
+ <div class="project-bar">
46
+ <span class="project-name" id="project-name" title="">未打开工程</span>
47
+ <button type="button" class="ghost-btn" id="switch-project-btn">切换工程</button>
48
+ </div>
49
+ <div class="toolbar-actions" id="toolbar-actions">
50
+ <button type="button" class="tool-btn" id="tool-add-assets">添加素材</button>
51
+ <button type="button" class="tool-btn" id="tool-image-gen" title="拖到画布指定位置创建图片生成元素">图片生成</button>
52
+ <button type="button" class="tool-btn" id="tool-video-gen" title="拖到画布指定位置创建视频生成元素">视频生成</button>
53
+ <button type="button" class="ghost-btn" id="reset-view">重置视图</button>
54
+ </div>
55
+ </div>
56
+ <div class="canvas-viewport" id="viewport">
57
+ <div class="canvas-world" id="world"></div>
58
+ <div class="marquee" id="marquee" hidden></div>
59
+ </div>
60
+ </div>
61
+ <div class="banner-error" id="banner-error" hidden></div>
62
+ </div>
63
+ </div>
64
+
65
+ <div class="ctx-menu" id="ctx-menu" hidden>
66
+ <button type="button" data-action="copy">复制</button>
67
+ <button type="button" data-action="paste" hidden>粘贴</button>
68
+ <button type="button" data-action="cutout" hidden>智能抠图</button>
69
+ <button type="button" data-action="interactive-edit" hidden>交互编辑</button>
70
+ <button type="button" data-action="layer-decomposition" hidden>大模型图层分离</button>
71
+ <button type="button" data-action="video-trim" hidden>裁剪视频</button>
72
+ <button type="button" data-action="video-cycle" hidden>智能周期提取</button>
73
+ <button type="button" data-action="video-to-sequence" hidden>序列帧化</button>
74
+ <button type="button" data-action="smart-split">智能切分</button>
75
+ <button type="button" data-action="manual-split">手动切分</button>
76
+ <button type="button" data-action="edit-sequence" hidden>在左侧编辑序列</button>
77
+ <button type="button" data-action="sequence-to-sheet" hidden>拼成单图</button>
78
+ <button type="button" data-action="ungroup-sequence" hidden>拆散为单图</button>
79
+ <button type="button" data-action="combine-sequence" hidden>组合为序列帧</button>
80
+ <button type="button" data-action="delete" class="ctx-danger">删除</button>
81
+ </div>
82
+
83
+ <div class="modal-backdrop modal-backdrop-dark" id="interactive-edit-modal" hidden>
84
+ <div class="modal-dialog modal-dialog-interactive-edit" role="dialog" aria-modal="true" aria-labelledby="interactive-edit-title">
85
+ <div class="seq-panel-head">
86
+ <strong id="interactive-edit-title">交互编辑</strong>
87
+ <button type="button" class="ghost-btn" id="interactive-edit-close">关闭</button>
88
+ </div>
89
+ <div class="interactive-editor-host" id="interactive-editor-host"></div>
90
+ <div class="interactive-edit-compose">
91
+ <label class="layer-decomposition-prompt" for="interactive-edit-prompt">
92
+ 编辑要求
93
+ <textarea
94
+ id="interactive-edit-prompt"
95
+ rows="3"
96
+ placeholder="例如:将框选区域替换为木质宝箱,并保持其他内容不变"
97
+ ></textarea>
98
+ </label>
99
+ <p class="modal-hint">画面标记和文字会合并到参考图中,并使用 Pro 模型生成。</p>
100
+ <p class="modal-hint" id="interactive-edit-status" hidden></p>
101
+ <div class="modal-actions">
102
+ <button type="button" class="ghost-btn" id="interactive-edit-cancel">取消</button>
103
+ <button type="button" class="ghost-btn" id="interactive-edit-save">保存到画布</button>
104
+ <button type="button" class="tool-btn" id="interactive-edit-confirm">生成</button>
105
+ </div>
106
+ </div>
107
+ </div>
108
+ </div>
109
+
110
+ <div class="modal-backdrop modal-backdrop-dark" id="layer-decomposition-modal" hidden>
111
+ <div class="modal-dialog modal-dialog-layer-decomposition" role="dialog" aria-modal="true" aria-labelledby="layer-decomposition-title">
112
+ <div class="seq-panel-head">
113
+ <strong id="layer-decomposition-title">大模型图层分离</strong>
114
+ <button type="button" class="ghost-btn" id="layer-decomposition-close">关闭</button>
115
+ </div>
116
+ <div class="layer-decomposition-preview">
117
+ <img id="layer-decomposition-image" alt="待分离图片预览" />
118
+ </div>
119
+ <label class="layer-decomposition-prompt" for="layer-decomposition-prompt">
120
+ 拆分要求
121
+ <textarea
122
+ id="layer-decomposition-prompt"
123
+ rows="4"
124
+ placeholder="例如:将人物、背景、文字分别拆分为可独立编辑的图层"
125
+ ></textarea>
126
+ </label>
127
+ <p class="modal-hint">模型会输出一张底图和最多 16 个透明 PNG 图层。</p>
128
+ <p class="modal-hint" id="layer-decomposition-status" hidden></p>
129
+ <div class="modal-actions">
130
+ <button type="button" class="ghost-btn" id="layer-decomposition-cancel">取消</button>
131
+ <button type="button" class="tool-btn" id="layer-decomposition-confirm">开始分离</button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+
136
+ <div class="modal-backdrop modal-backdrop-dark" id="video-cycle-modal" hidden>
137
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="video-cycle-title">
138
+ <div class="seq-panel-head">
139
+ <strong id="video-cycle-title">智能周期提取</strong>
140
+ <button type="button" class="ghost-btn" id="video-cycle-close">关闭</button>
141
+ </div>
142
+ <p class="modal-hint">
143
+ 以首帧为参考,从前向后寻找最早相似度达标的一帧并立即停止,再裁切为首个周期视频。
144
+ </p>
145
+ <label class="seq-field frame-extract-field">
146
+ 相似度阈值
147
+ <input
148
+ type="number"
149
+ id="video-cycle-threshold"
150
+ min="50"
151
+ max="99.9"
152
+ step="0.1"
153
+ value="98"
154
+ />
155
+ <span class="frame-extract-range">%</span>
156
+ </label>
157
+ <label class="seq-field frame-extract-field">
158
+ 最短周期
159
+ <input
160
+ type="number"
161
+ id="video-cycle-min-gap"
162
+ min="0.1"
163
+ max="10"
164
+ step="0.1"
165
+ value="0.4"
166
+ />
167
+ <span class="frame-extract-range">秒(避免开头相邻帧误命中)</span>
168
+ </label>
169
+ <p class="modal-hint" id="video-cycle-status" hidden></p>
170
+ <div class="modal-actions">
171
+ <button type="button" class="ghost-btn" id="video-cycle-cancel">取消</button>
172
+ <button type="button" class="tool-btn" id="video-cycle-confirm">开始提取</button>
173
+ </div>
174
+ </div>
175
+ </div>
176
+
177
+ <div class="modal-backdrop modal-backdrop-dark" id="smart-split-modal" hidden>
178
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="smart-split-title">
179
+ <div class="seq-panel-head">
180
+ <strong id="smart-split-title">智能切分</strong>
181
+ <button type="button" class="ghost-btn" id="smart-split-close">关闭</button>
182
+ </div>
183
+ <p class="modal-hint">
184
+ 切分结果统一为最大主体的宽度和高度。选择主体在扩展区域中的固定基准点;主体不会缩放。
185
+ </p>
186
+ <div class="split-anchor-grid" id="smart-split-anchor-grid" role="radiogroup" aria-label="扩展基准点">
187
+ <button type="button" class="split-anchor-btn" data-anchor="top-left" role="radio" aria-checked="false"><span></span>左上</button>
188
+ <button type="button" class="split-anchor-btn" data-anchor="top" role="radio" aria-checked="false"><span></span>上</button>
189
+ <button type="button" class="split-anchor-btn" data-anchor="top-right" role="radio" aria-checked="false"><span></span>右上</button>
190
+ <button type="button" class="split-anchor-btn" data-anchor="left" role="radio" aria-checked="false"><span></span>左</button>
191
+ <button type="button" class="split-anchor-btn is-selected" data-anchor="center" role="radio" aria-checked="true"><span></span>中心</button>
192
+ <button type="button" class="split-anchor-btn" data-anchor="right" role="radio" aria-checked="false"><span></span>右</button>
193
+ <button type="button" class="split-anchor-btn" data-anchor="bottom-left" role="radio" aria-checked="false"><span></span>左下</button>
194
+ <button type="button" class="split-anchor-btn" data-anchor="bottom" role="radio" aria-checked="false"><span></span>下</button>
195
+ <button type="button" class="split-anchor-btn" data-anchor="bottom-right" role="radio" aria-checked="false"><span></span>右下</button>
196
+ </div>
197
+ <p class="modal-hint" id="smart-split-status" hidden></p>
198
+ <div class="modal-actions">
199
+ <button type="button" class="ghost-btn" id="smart-split-cancel">取消</button>
200
+ <button type="button" class="tool-btn" id="smart-split-confirm">开始切分</button>
201
+ </div>
202
+ </div>
203
+ </div>
204
+
205
+ <div class="modal-backdrop modal-backdrop-dark" id="manual-split-modal" hidden>
206
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="manual-split-title">
207
+ <div class="seq-panel-head">
208
+ <strong id="manual-split-title">手动切分</strong>
209
+ <button type="button" class="ghost-btn" id="manual-split-close">关闭</button>
210
+ </div>
211
+ <p class="modal-hint">
212
+ 按行列将原图完整均分,从左到右、从上到下生成图片,不缩放原图。
213
+ </p>
214
+ <p class="modal-hint" id="manual-split-info"></p>
215
+ <div class="seq-sheet-fields">
216
+ <label class="seq-field frame-extract-field">
217
+ 行数
218
+ <input type="number" id="manual-split-rows" min="1" max="64" step="1" value="2" />
219
+ </label>
220
+ <label class="seq-field frame-extract-field">
221
+ 列数
222
+ <input type="number" id="manual-split-cols" min="1" max="64" step="1" value="2" />
223
+ </label>
224
+ </div>
225
+ <div class="seq-sheet-preview" id="manual-split-preview" aria-hidden="true"></div>
226
+ <p class="modal-hint" id="manual-split-status" hidden></p>
227
+ <div class="modal-actions">
228
+ <button type="button" class="ghost-btn" id="manual-split-cancel">取消</button>
229
+ <button type="button" class="tool-btn" id="manual-split-confirm">开始切分</button>
230
+ </div>
231
+ </div>
232
+ </div>
233
+
234
+ <div class="modal-backdrop modal-backdrop-dark" id="frame-extract-modal" hidden>
235
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="frame-extract-title">
236
+ <div class="seq-panel-head">
237
+ <strong id="frame-extract-title">序列帧化</strong>
238
+ <button type="button" class="ghost-btn" id="frame-extract-close">关闭</button>
239
+ </div>
240
+ <p class="modal-hint">
241
+ 按总帧数计算步长均匀取帧:首帧与尾帧必含,中间为 1 + n×⌊T/(C-1)⌋。
242
+ </p>
243
+ <label class="seq-field frame-extract-field">
244
+ 序列帧长度
245
+ <input type="number" id="frame-extract-count" min="2" max="30" step="1" value="10" />
246
+ <span class="frame-extract-range">2 – 30</span>
247
+ </label>
248
+ <p class="modal-hint" id="frame-extract-status" hidden></p>
249
+ <div class="modal-actions">
250
+ <button type="button" class="ghost-btn" id="frame-extract-cancel">取消</button>
251
+ <button type="button" class="tool-btn" id="frame-extract-confirm">开始抽帧</button>
252
+ </div>
253
+ </div>
254
+ </div>
255
+
256
+ <div class="modal-backdrop modal-backdrop-dark" id="video-trim-modal" hidden>
257
+ <div class="modal-dialog modal-dialog-wide" role="dialog" aria-modal="true" aria-labelledby="video-trim-title">
258
+ <div class="seq-panel-head">
259
+ <strong id="video-trim-title">裁剪视频</strong>
260
+ <button type="button" class="ghost-btn" id="video-trim-close">关闭</button>
261
+ </div>
262
+ <p class="modal-hint">一条时间轴:拖白点定位画面,拖青绿柄设入/出点;滚轮可微调(Shift=1ms)。</p>
263
+ <div class="trim-player">
264
+ <div class="trim-stage">
265
+ <video id="video-trim-preview" muted playsinline preload="auto"></video>
266
+ </div>
267
+ <div class="trim-bar">
268
+ <button type="button" class="trim-play-btn" id="video-trim-play" title="播放/暂停">▶</button>
269
+ <span class="trim-clock">
270
+ <strong id="video-trim-now-label">0:00.000</strong>
271
+ <span>/</span>
272
+ <span id="video-trim-total-label">0:00.000</span>
273
+ </span>
274
+ </div>
275
+ <div class="trim-timeline" id="video-trim-timeline">
276
+ <div class="trim-track" id="video-trim-track">
277
+ <div class="trim-range" id="video-trim-range"></div>
278
+ <input
279
+ type="range"
280
+ class="trim-scrub-input"
281
+ id="video-trim-scrub"
282
+ min="0"
283
+ max="0"
284
+ step="0.001"
285
+ value="0"
286
+ aria-label="播放进度"
287
+ />
288
+ <div
289
+ class="trim-handle trim-handle-start"
290
+ id="video-trim-handle-start"
291
+ title="入点"
292
+ role="slider"
293
+ tabindex="0"
294
+ ></div>
295
+ <div
296
+ class="trim-handle trim-handle-end"
297
+ id="video-trim-handle-end"
298
+ title="出点"
299
+ role="slider"
300
+ tabindex="0"
301
+ ></div>
302
+ </div>
303
+ </div>
304
+ <div class="trim-times">
305
+ <span>入点 <strong id="video-trim-start-label">0:00.000</strong></span>
306
+ <span>选区 <strong id="video-trim-dur-label">0:00.000</strong></span>
307
+ <span>出点 <strong id="video-trim-end-label">0:00.000</strong></span>
308
+ </div>
309
+ </div>
310
+ <p class="modal-hint" id="video-trim-status" hidden></p>
311
+ <div class="modal-actions">
312
+ <button type="button" class="ghost-btn" id="video-trim-cancel">取消</button>
313
+ <button type="button" class="tool-btn" id="video-trim-confirm">裁剪并生成</button>
314
+ </div>
315
+ </div>
316
+ </div>
317
+
318
+ <div class="modal-backdrop modal-backdrop-dark" id="seq-sheet-modal" hidden>
319
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="seq-sheet-title">
320
+ <div class="seq-panel-head">
321
+ <strong id="seq-sheet-title">拼成单图</strong>
322
+ <button type="button" class="ghost-btn" id="seq-sheet-close">关闭</button>
323
+ </div>
324
+ <p class="modal-hint">
325
+ 按行列排布序列帧:先从左到右填满一行,再换到下一行;多出的格子保持透明。
326
+ </p>
327
+ <p class="modal-hint" id="seq-sheet-frame-info">共 0 帧</p>
328
+ <div class="seq-sheet-fields">
329
+ <label class="seq-field frame-extract-field">
330
+ 行数
331
+ <input type="number" id="seq-sheet-rows" min="1" max="64" step="1" value="1" />
332
+ </label>
333
+ <label class="seq-field frame-extract-field">
334
+ 列数
335
+ <input type="number" id="seq-sheet-cols" min="1" max="64" step="1" value="1" />
336
+ </label>
337
+ </div>
338
+ <div class="seq-sheet-preview" id="seq-sheet-preview" aria-hidden="true"></div>
339
+ <p class="modal-hint" id="seq-sheet-status" hidden></p>
340
+ <div class="modal-actions">
341
+ <button type="button" class="ghost-btn" id="seq-sheet-cancel">取消</button>
342
+ <button type="button" class="tool-btn" id="seq-sheet-confirm">生成单图</button>
343
+ </div>
344
+ </div>
345
+ </div>
346
+
347
+ <div class="modal-backdrop" id="seq-modal" hidden>
348
+ <div class="modal-dialog" role="dialog" aria-modal="true" aria-labelledby="seq-panel-title">
349
+ <div class="seq-panel-head">
350
+ <strong id="seq-panel-title">编辑序列</strong>
351
+ <button type="button" class="ghost-btn" id="seq-panel-close">关闭</button>
352
+ </div>
353
+ <p class="modal-hint">调整播放速度、循环,以及帧顺序。</p>
354
+ <label class="seq-field">
355
+ 播放速度 (FPS)
356
+ <input type="range" id="seq-fps" min="1" max="24" value="8" />
357
+ <span id="seq-fps-val">8</span>
358
+ </label>
359
+ <label class="seq-field seq-check">
360
+ <input type="checkbox" id="seq-loop" checked />
361
+ 循环播放
362
+ </label>
363
+ <div class="seq-frames" id="seq-frames"></div>
364
+ <div class="modal-actions">
365
+ <button type="button" class="ghost-btn" id="seq-panel-done">完成</button>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <input
371
+ type="file"
372
+ id="asset-file-input"
373
+ accept="image/*,video/*,.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif,.svg,.mp4,.webm,.mov,.m4v"
374
+ multiple
375
+ hidden
376
+ />
377
+ <input
378
+ type="file"
379
+ id="file-input"
380
+ accept="image/png,image/jpeg,image/jpg,image/webp,image/gif,image/bmp,image/avif,image/svg+xml,.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif,.svg"
381
+ multiple
382
+ hidden
383
+ />
384
+ <input
385
+ type="file"
386
+ id="media-file-input"
387
+ accept="image/*,video/*,audio/*,.png,.jpg,.jpeg,.webp,.gif,.bmp,.avif,.svg,.mp4,.webm,.mov,.m4v,.mp3,.wav,.m4a"
388
+ multiple
389
+ hidden
390
+ />
391
+ </div>
392
+ <script src="/vendor/painterro.min.js"></script>
393
+ <script type="module" src="/app.js"></script>
394
+ </body>
395
+ </html>
@@ -0,0 +1,59 @@
1
+ /**
2
+ * 将序列帧拼成单张透明底图:按「先左到右、再换行向下」填格。
3
+ */
4
+
5
+ function loadImage(url) {
6
+ return new Promise((resolve, reject) => {
7
+ const img = new Image()
8
+ img.onload = () => resolve(img)
9
+ img.onerror = () => reject(new Error(`无法加载帧图:${url}`))
10
+ img.src = url
11
+ })
12
+ }
13
+
14
+ /**
15
+ * @param {string[]} frameUrls
16
+ * @param {{ rows: number, cols: number }} grid
17
+ * @returns {Promise<string>} PNG data URL
18
+ */
19
+ export async function packSequenceSheet(frameUrls, { rows, cols }) {
20
+ const r = Math.round(Number(rows))
21
+ const c = Math.round(Number(cols))
22
+ if (!Number.isFinite(r) || !Number.isFinite(c) || r < 1 || c < 1) {
23
+ throw new Error('行数与列数需为正整数')
24
+ }
25
+ const frames = Array.isArray(frameUrls) ? frameUrls.filter(Boolean) : []
26
+ if (!frames.length) throw new Error('序列没有可拼接的帧')
27
+ if (r * c < frames.length) {
28
+ throw new Error(`格子不足:${r}×${c}=${r * c},但有 ${frames.length} 帧`)
29
+ }
30
+
31
+ const images = await Promise.all(frames.map((url) => loadImage(url)))
32
+ const cellW = Math.max(...images.map((img) => img.naturalWidth || img.width || 0))
33
+ const cellH = Math.max(...images.map((img) => img.naturalHeight || img.height || 0))
34
+ if (!(cellW > 0 && cellH > 0)) throw new Error('无法读取帧尺寸')
35
+
36
+ const canvas = document.createElement('canvas')
37
+ canvas.width = cellW * c
38
+ canvas.height = cellH * r
39
+ const ctx = canvas.getContext('2d')
40
+ if (!ctx) throw new Error('无法创建画布')
41
+ ctx.clearRect(0, 0, canvas.width, canvas.height)
42
+
43
+ for (let i = 0; i < images.length; i++) {
44
+ // 先从左到右填满一行,再换到下一行
45
+ const row = Math.floor(i / c)
46
+ const col = i % c
47
+ ctx.drawImage(images[i], col * cellW, row * cellH)
48
+ }
49
+
50
+ return canvas.toDataURL('image/png')
51
+ }
52
+
53
+ /** 根据帧数给出接近方形的默认行列(行优先填充)。 */
54
+ export function suggestSheetGrid(frameCount) {
55
+ const n = Math.max(1, Math.round(Number(frameCount) || 1))
56
+ const cols = Math.max(1, Math.ceil(Math.sqrt(n)))
57
+ const rows = Math.max(1, Math.ceil(n / cols))
58
+ return { rows, cols }
59
+ }