evomeme 0.1.7 → 0.1.9-beta.14

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 (106) hide show
  1. package/dist/canvas-public/app.js +8517 -1692
  2. package/dist/canvas-public/assets/presets/catalog.json +47 -0
  3. package/dist/canvas-public/assets/presets/tiles/A_01_CaoDi.png +0 -0
  4. package/dist/canvas-public/assets/presets/tiles/A_02_ShaMo.png +0 -0
  5. package/dist/canvas-public/assets/presets/tiles/A_03_TianKong.png +0 -0
  6. package/dist/canvas-public/assets/presets/tiles/A_04_ShuiBo.png +0 -0
  7. package/dist/canvas-public/assets/presets/tiles/A_05_HaiMian.png +0 -0
  8. package/dist/canvas-public/assets/presets/tiles/A_06_MuDiBan.png +0 -0
  9. package/dist/canvas-public/assets/presets/tiles/A_07_ShiZhuan.png +0 -0
  10. package/dist/canvas-public/assets/presets/tiles/A_08_AnRongYan.png +0 -0
  11. package/dist/canvas-public/assets/presets/tiles/A_09_QingTaiShi.png +0 -0
  12. package/dist/canvas-public/assets/presets/tiles/A_10_ShenHai.png +0 -0
  13. package/dist/canvas-public/assets/presets/tiles/A_11_HuiYanShi.png +0 -0
  14. package/dist/canvas-public/assets/presets/tiles/A_12_HongTu.png +0 -0
  15. package/dist/canvas-public/assets/presets/tiles/A_13_GengDi.png +0 -0
  16. package/dist/canvas-public/assets/presets/tiles/A_14_HuoShanYan.png +0 -0
  17. package/dist/canvas-public/assets/presets/tiles/A_15_NiTu.png +0 -0
  18. package/dist/canvas-public/assets/presets/tiles/A_16_HuangSha.png +0 -0
  19. package/dist/canvas-public/assets/presets/tiles/A_17_CongLin.png +0 -0
  20. package/dist/canvas-public/assets/presets/tiles/A_18_ZiYanShi.png +0 -0
  21. package/dist/canvas-public/assets/presets/tiles/A_19_MoGuTu.png +0 -0
  22. package/dist/canvas-public/assets/presets/tiles/A_20_HuaDian.png +0 -0
  23. package/dist/canvas-public/assets/presets/tiles/A_21_HaiLang.png +0 -0
  24. package/dist/canvas-public/assets/presets/tiles/A_22_GanTu.png +0 -0
  25. package/dist/canvas-public/assets/presets/tiles/A_23_QingTaiShiBan.png +0 -0
  26. package/dist/canvas-public/assets/presets/tiles/A_24_XingKong.png +0 -0
  27. package/dist/canvas-public/assets/presets/tiles/A_25_MoFaFuWen.png +0 -0
  28. package/dist/canvas-public/assets/presets/tiles/A_26_ShiTuYeDi.png +0 -0
  29. package/dist/canvas-public/assets/presets/tiles/A_27_LanZhuan.png +0 -0
  30. package/dist/canvas-public/assets/presets/tiles/A_28_HongZhuan.png +0 -0
  31. package/dist/canvas-public/assets/presets/tiles/A_29_YueQiu.png +0 -0
  32. package/dist/canvas-public/assets/presets/tiles/A_30_JinShuGe.png +0 -0
  33. package/dist/canvas-public/assets/presets/tiles/A_31_JinShuJingGai.png +0 -0
  34. package/dist/canvas-public/assets/presets/tiles/A_32_ShuZiChuang.png +0 -0
  35. package/dist/canvas-public/color-key-worker.js +280 -0
  36. package/dist/canvas-public/image-canvas-size-editor.js +254 -0
  37. package/dist/canvas-public/index.html +226 -119
  38. package/dist/canvas-public/perfect-pixel-worker.js +499 -0
  39. package/dist/canvas-public/smart-split.js +1 -1
  40. package/dist/canvas-public/styles.css +3902 -1225
  41. package/dist/canvas-public/tile-map-editor.js +2120 -0
  42. package/dist/canvas-public/ui-controls.js +95 -0
  43. package/dist/canvas-public/vendor/pixi-tilemap.js +126 -0
  44. package/dist/canvas-public/vendor/pixi.min.js +2340 -0
  45. package/dist/index.js +32263 -4187
  46. package/dist/stage-blender/bake_materials.py +102 -0
  47. package/dist/stage-blender/export_scene.py +275 -0
  48. package/dist/stage-blender/preview_camera.py +90 -0
  49. package/dist/stage-blender/repair_mesh.py +112 -0
  50. package/dist/stage-public/app.js +759 -0
  51. package/dist/stage-public/baked/bakePilot.js +44 -0
  52. package/dist/stage-public/baked/bakedScene.js +129 -0
  53. package/dist/stage-public/baked/sceneAppearance.js +225 -0
  54. package/dist/stage-public/baked/sceneCamera.js +45 -0
  55. package/dist/stage-public/comparison-contract.js +28 -0
  56. package/dist/stage-public/delivery.html +4 -0
  57. package/dist/stage-public/delivery.js +17 -0
  58. package/dist/stage-public/godot/appearance.gd +170 -0
  59. package/dist/stage-public/godot/preview.gd +79 -0
  60. package/dist/stage-public/index.html +141 -0
  61. package/dist/stage-public/review-app.js +289 -0
  62. package/dist/stage-public/review-runtime.js +63 -0
  63. package/dist/stage-public/review.css +24 -0
  64. package/dist/stage-public/review.html +47 -0
  65. package/dist/stage-public/scene-runtime.js +212 -0
  66. package/dist/stage-public/sceneContractRuntime.js +235 -0
  67. package/dist/stage-public/styles.css +328 -0
  68. package/dist/stage-public/three-appearance.js +104 -0
  69. package/dist/stage-public/vendor/three170/LICENSE +21 -0
  70. package/dist/stage-public/vendor/three170/build/three.module.js +54571 -0
  71. package/dist/stage-public/vendor/three170/examples/jsm/controls/OrbitControls.js +1523 -0
  72. package/dist/stage-public/vendor/three170/examples/jsm/environments/RoomEnvironment.js +144 -0
  73. package/dist/stage-public/vendor/three170/examples/jsm/libs/fflate.module.js +2672 -0
  74. package/dist/stage-public/vendor/three170/examples/jsm/loaders/EXRLoader.js +2563 -0
  75. package/dist/stage-public/vendor/three170/examples/jsm/loaders/GLTFLoader.js +4727 -0
  76. package/dist/stage-public/vendor/three170/examples/jsm/utils/BufferGeometryUtils.js +1373 -0
  77. package/dist/tile-transition-mask.svg +40 -0
  78. package/package.json +77 -69
  79. package/skills/evomeme-canvas/SKILL.md +52 -0
  80. package/skills/evomeme-canvas/agents/openai.yaml +7 -0
  81. package/skills/evomeme-cover/SKILL.md +1 -1
  82. package/skills/evomeme-game-maker/SKILL.md +79 -8
  83. package/skills/game-asset-align/SKILL.md +45 -0
  84. package/skills/game-asset-align/agents/openai.yaml +8 -0
  85. package/skills/game-asset-align/references/alignment-contract.md +61 -0
  86. package/skills/game-asset-align/references/blender-scene-pipeline.md +45 -0
  87. package/skills/game-asset-align/references/canonical-package.md +26 -0
  88. package/skills/game-asset-align/references/engine-capabilities.md +14 -0
  89. package/skills/game-asset-align/references/lightweight-review.md +32 -0
  90. package/skills/game-asset-align/references/repair-policy.md +30 -0
  91. package/skills/game-asset-align/references/tool-contract.md +39 -0
  92. package/skills/game-asset-align/references/visual-review.md +26 -0
  93. package/skills/game-asset-image-generator/SKILL.md +1 -1
  94. package/skills/godot-3d-game/SKILL.md +39 -0
  95. package/skills/godot-3d-game/agents/openai.yaml +8 -0
  96. package/skills/godot-3d-game/references/alignment-gates.md +26 -0
  97. package/skills/godot-3d-game/references/appearance-alignment.md +52 -0
  98. package/skills/godot-3d-game/references/build-and-qa.md +22 -0
  99. package/skills/godot-3d-game/references/canonical-asset-runtime.md +27 -0
  100. package/skills/godot-3d-game/references/import-and-wrapper-scenes.md +20 -0
  101. package/skills/godot-3d-game/references/physics-colliders.md +13 -0
  102. package/skills/godot-3d-game/references/project-structure.md +22 -0
  103. package/skills/godot-3d-game/references/skeleton-animation.md +16 -0
  104. package/skills/threejs-3d-game/SKILL.md +6 -2
  105. package/skills/threejs-3d-game/references/engine-asset-pipeline.md +52 -15
  106. package/dist/canvas-public/sequence-sheet.js +0 -59
@@ -0,0 +1,499 @@
1
+ let sourceBitmap = null
2
+
3
+ self.addEventListener('message', async (event) => {
4
+ const message = event.data || {}
5
+ try {
6
+ if (message.type === 'init') {
7
+ const response = await fetch(String(message.url || ''))
8
+ if (!response.ok) throw new Error(`图片载入失败:${response.status}`)
9
+ sourceBitmap?.close?.()
10
+ sourceBitmap = await createImageBitmap(await response.blob())
11
+ self.postMessage({
12
+ type: 'ready',
13
+ requestId: message.requestId,
14
+ width: sourceBitmap.width,
15
+ height: sourceBitmap.height,
16
+ })
17
+ return
18
+ }
19
+
20
+ if (message.type !== 'process') return
21
+ if (!sourceBitmap) throw new Error('完美像素 Worker 尚未初始化')
22
+
23
+ const settings = normalizeSettings(message.settings)
24
+ const sourceCanvas = new OffscreenCanvas(sourceBitmap.width, sourceBitmap.height)
25
+ const sourceContext = sourceCanvas.getContext('2d', { willReadFrequently: true })
26
+ sourceContext.drawImage(sourceBitmap, 0, 0)
27
+ const source = sourceContext.getImageData(0, 0, sourceBitmap.width, sourceBitmap.height)
28
+ const result = restorePixelGrid(source, settings)
29
+
30
+ const lowResolution = new OffscreenCanvas(result.width, result.height)
31
+ lowResolution.getContext('2d').putImageData(
32
+ new ImageData(result.pixels, result.width, result.height),
33
+ 0,
34
+ 0,
35
+ )
36
+
37
+ const outputWidth = result.width * settings.exportScale
38
+ const outputHeight = result.height * settings.exportScale
39
+ const output = new OffscreenCanvas(outputWidth, outputHeight)
40
+ const outputContext = output.getContext('2d')
41
+ outputContext.imageSmoothingEnabled = false
42
+ outputContext.clearRect(0, 0, outputWidth, outputHeight)
43
+ outputContext.drawImage(lowResolution, 0, 0, outputWidth, outputHeight)
44
+ const blob = await output.convertToBlob({ type: 'image/png' })
45
+
46
+ self.postMessage({
47
+ type: 'result',
48
+ requestId: message.requestId,
49
+ blob,
50
+ width: outputWidth,
51
+ height: outputHeight,
52
+ gridWidth: result.width,
53
+ gridHeight: result.height,
54
+ pixelSizeX: result.pixelSizeX,
55
+ pixelSizeY: result.pixelSizeY,
56
+ confidence: result.confidence,
57
+ automatic: settings.autoGrid,
58
+ })
59
+ } catch (error) {
60
+ self.postMessage({
61
+ type: 'error',
62
+ requestId: message.requestId,
63
+ message: String(error?.message || error),
64
+ })
65
+ }
66
+ })
67
+
68
+ function normalizeSettings(value) {
69
+ const settings = value && typeof value === 'object' ? value : {}
70
+ const sampleMethod = ['majority', 'median', 'center'].includes(settings.sampleMethod)
71
+ ? settings.sampleMethod
72
+ : 'median'
73
+ const alphaMode = settings.alphaMode === 'preserve' ? 'preserve' : 'hard'
74
+ return {
75
+ autoGrid: settings.autoGrid !== false,
76
+ pixelSize: clampInteger(settings.pixelSize, 2, 256, 8),
77
+ sampleMethod,
78
+ exportScale: clampInteger(settings.exportScale, 1, 16, 4),
79
+ alphaMode,
80
+ alphaThreshold: clampInteger(settings.alphaThreshold, 0, 255, 128),
81
+ }
82
+ }
83
+
84
+ function clampInteger(value, min, max, fallback) {
85
+ const number = Math.round(Number(value))
86
+ return Number.isFinite(number) ? Math.max(min, Math.min(max, number)) : fallback
87
+ }
88
+
89
+ function restorePixelGrid(image, settings) {
90
+ const width = image.width
91
+ const height = image.height
92
+ if (!width || !height) throw new Error('来源图片尺寸无效')
93
+
94
+ const projections = buildAlphaAwareGradientProjections(image.data, width, height)
95
+ let pixelSizeX = settings.pixelSize
96
+ let pixelSizeY = settings.pixelSize
97
+ let gridWidth = Math.ceil(width / settings.pixelSize)
98
+ let gridHeight = Math.ceil(height / settings.pixelSize)
99
+ let confidence = 1
100
+
101
+ if (settings.autoGrid) {
102
+ const detectedX = detectPixelPeriod(projections.x)
103
+ const detectedY = detectPixelPeriod(projections.y)
104
+ if (!detectedX && !detectedY) {
105
+ throw new Error('无法可靠识别像素网格,请切换为手动网格')
106
+ }
107
+ let squarePixelSize
108
+ if (detectedX && detectedY) {
109
+ const ratio = Math.max(detectedX.period, detectedY.period) / Math.min(detectedX.period, detectedY.period)
110
+ if (ratio <= 1.5) {
111
+ const totalConfidence = Math.max(1e-6, detectedX.confidence + detectedY.confidence)
112
+ squarePixelSize = (
113
+ detectedX.period * detectedX.confidence +
114
+ detectedY.period * detectedY.confidence
115
+ ) / totalConfidence
116
+ } else {
117
+ // Pixel-art cells are square. If one axis is dominated by illustration
118
+ // content instead of the grid, trust the stronger axis for both directions.
119
+ squarePixelSize = detectedX.confidence >= detectedY.confidence
120
+ ? detectedX.period
121
+ : detectedY.period
122
+ }
123
+ confidence = Math.min(detectedX.confidence, detectedY.confidence)
124
+ } else {
125
+ const detected = detectedX || detectedY
126
+ squarePixelSize = detected.period
127
+ // A single reliable axis is enough for square pixels, but report that the
128
+ // estimate had less supporting evidence than a two-axis match.
129
+ confidence = detected.confidence * 0.78
130
+ }
131
+ pixelSizeX = squarePixelSize
132
+ pixelSizeY = squarePixelSize
133
+ gridWidth = Math.max(1, Math.round(width / pixelSizeX))
134
+ gridHeight = Math.max(1, Math.round(height / pixelSizeY))
135
+ pixelSizeX = width / gridWidth
136
+ pixelSizeY = height / gridHeight
137
+ }
138
+
139
+ const xCoordinates = settings.autoGrid
140
+ ? refineGridCoordinates(width, gridWidth, projections.x)
141
+ : fixedGridCoordinates(width, settings.pixelSize)
142
+ const yCoordinates = settings.autoGrid
143
+ ? refineGridCoordinates(height, gridHeight, projections.y)
144
+ : fixedGridCoordinates(height, settings.pixelSize)
145
+ const outputWidth = xCoordinates.length - 1
146
+ const outputHeight = yCoordinates.length - 1
147
+ if (outputWidth < 1 || outputHeight < 1) throw new Error('识别出的像素网格为空')
148
+ if (outputWidth * outputHeight > 4_194_304) {
149
+ throw new Error('输出像素网格过大,请降低手动网格尺寸')
150
+ }
151
+
152
+ const pixels = sampleGrid(
153
+ image.data,
154
+ width,
155
+ height,
156
+ xCoordinates,
157
+ yCoordinates,
158
+ settings,
159
+ )
160
+ return {
161
+ pixels,
162
+ width: outputWidth,
163
+ height: outputHeight,
164
+ pixelSizeX,
165
+ pixelSizeY,
166
+ confidence,
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Transparent pixels may contain arbitrary hidden RGB values. Work in premultiplied
172
+ * RGB and add the alpha edge itself to the signal, so those hidden colors can never
173
+ * create a fake grid while dark opaque sprites remain detectable.
174
+ */
175
+ function buildAlphaAwareGradientProjections(source, width, height) {
176
+ const x = new Float64Array(width)
177
+ const y = new Float64Array(height)
178
+ for (let row = 0; row < height; row += 1) {
179
+ for (let column = 0; column < width; column += 1) {
180
+ const offset = (row * width + column) * 4
181
+ const alpha = source[offset + 3] / 255
182
+ const red = source[offset] * alpha
183
+ const green = source[offset + 1] * alpha
184
+ const blue = source[offset + 2] * alpha
185
+
186
+ if (column > 0) {
187
+ const previous = offset - 4
188
+ const previousAlpha = source[previous + 3] / 255
189
+ const delta =
190
+ Math.abs(red - source[previous] * previousAlpha) +
191
+ Math.abs(green - source[previous + 1] * previousAlpha) +
192
+ Math.abs(blue - source[previous + 2] * previousAlpha) +
193
+ Math.abs(alpha - previousAlpha) * 255
194
+ x[column] += delta
195
+ }
196
+ if (row > 0) {
197
+ const previous = offset - width * 4
198
+ const previousAlpha = source[previous + 3] / 255
199
+ const delta =
200
+ Math.abs(red - source[previous] * previousAlpha) +
201
+ Math.abs(green - source[previous + 1] * previousAlpha) +
202
+ Math.abs(blue - source[previous + 2] * previousAlpha) +
203
+ Math.abs(alpha - previousAlpha) * 255
204
+ y[row] += delta
205
+ }
206
+ }
207
+ }
208
+ return { x, y }
209
+ }
210
+
211
+ /**
212
+ * Grid edges share one residue modulo the source pixel-block size. Scoring that
213
+ * concentration together with short-lag autocorrelation recovers the same periodic
214
+ * grid targeted by Perfect Pixel's FFT/gradient stages without shipping OpenCV.
215
+ */
216
+ function detectPixelPeriod(projection) {
217
+ const length = projection.length
218
+ if (length < 4) return null
219
+ const maximum = Math.min(64, Math.max(2, Math.floor(length / 3)))
220
+ let total = 0
221
+ for (let i = 1; i < length; i += 1) total += projection[i]
222
+ if (total <= 1e-6) return null
223
+
224
+ let best = null
225
+ for (let period = 2; period <= maximum; period += 1) {
226
+ const bins = new Float64Array(period)
227
+ for (let i = 1; i < length; i += 1) bins[i % period] += projection[i]
228
+ let maximumBin = 0
229
+ for (const value of bins) maximumBin = Math.max(maximumBin, value)
230
+ const concentration = (maximumBin - total / period) / total
231
+
232
+ let numerator = 0
233
+ let leftEnergy = 0
234
+ let rightEnergy = 0
235
+ for (let i = 1; i + period < length; i += 1) {
236
+ const left = projection[i]
237
+ const right = projection[i + period]
238
+ numerator += left * right
239
+ leftEnergy += left * left
240
+ rightEnergy += right * right
241
+ }
242
+ const correlation = numerator / Math.sqrt(Math.max(1e-12, leftEnergy * rightEnergy))
243
+ const score = concentration * 0.78 + correlation * 0.22
244
+ if (!best || score > best.score) best = { period, score, concentration, correlation }
245
+ }
246
+
247
+ const phaseResult = best && best.score >= 0.025 && best.concentration >= 0.01
248
+ ? {
249
+ period: best.period,
250
+ confidence: Math.max(0, Math.min(1, best.score * 2.5)),
251
+ }
252
+ : null
253
+ const peakResult = detectPixelPeriodFromGradientPeaks(projection)
254
+
255
+ if (!phaseResult) return peakResult
256
+ if (!peakResult) return phaseResult
257
+ const ratio = Math.max(phaseResult.period, peakResult.period) /
258
+ Math.min(phaseResult.period, peakResult.period)
259
+ if (ratio <= 1.2) {
260
+ return phaseResult.confidence >= peakResult.confidence ? phaseResult : peakResult
261
+ }
262
+ return phaseResult.confidence >= peakResult.confidence * 1.2 ? phaseResult : peakResult
263
+ }
264
+
265
+ function detectPixelPeriodFromGradientPeaks(projection) {
266
+ let maximum = 0
267
+ for (const value of projection) maximum = Math.max(maximum, value)
268
+ if (maximum <= 1e-6) return null
269
+ const thresholds = [0.2, 0.12, 0.07]
270
+ for (const relativeThreshold of thresholds) {
271
+ const threshold = maximum * relativeThreshold
272
+ const peaks = []
273
+ for (let index = 1; index < projection.length - 1; index += 1) {
274
+ if (
275
+ projection[index] >= threshold &&
276
+ projection[index] > projection[index - 1] &&
277
+ projection[index] >= projection[index + 1]
278
+ ) {
279
+ const previous = peaks[peaks.length - 1]
280
+ if (previous == null || index - previous >= 2) peaks.push(index)
281
+ else if (projection[index] > projection[previous]) peaks[peaks.length - 1] = index
282
+ }
283
+ }
284
+ if (peaks.length < 3) continue
285
+ const intervals = []
286
+ for (let index = 1; index < peaks.length; index += 1) {
287
+ const interval = peaks[index] - peaks[index - 1]
288
+ if (interval >= 2 && interval <= 128) intervals.push(interval)
289
+ }
290
+ if (intervals.length < 2) continue
291
+ const period = median(intervals)
292
+ if (period < 2 || period > 128) continue
293
+ const deviations = intervals.map((interval) => {
294
+ const multiple = Math.max(1, Math.round(interval / period))
295
+ return Math.abs(interval - multiple * period) / period
296
+ })
297
+ const meanDeviation = deviations.reduce((sum, value) => sum + value, 0) / deviations.length
298
+ const support = Math.min(1, intervals.length / 10)
299
+ const confidence = Math.max(0.08, Math.min(0.9, (1 - meanDeviation) * (0.45 + support * 0.45)))
300
+ return { period, confidence }
301
+ }
302
+ return null
303
+ }
304
+
305
+ function refineGridCoordinates(length, gridCount, projection, intensity = 0.25) {
306
+ const count = Math.max(1, Math.min(length, Math.round(gridCount)))
307
+ const cellSize = length / count
308
+ const coordinates = new Int32Array(count + 1)
309
+ coordinates[0] = 0
310
+ coordinates[count] = length
311
+
312
+ for (let index = 1; index < count; index += 1) {
313
+ const expected = index * cellSize
314
+ const radius = Math.max(1, Math.round(cellSize * intensity))
315
+ const minimum = Math.max(coordinates[index - 1] + 1, Math.round(expected) - radius)
316
+ const maximum = Math.min(length - (count - index), Math.round(expected) + radius)
317
+ let best = Math.max(minimum, Math.min(maximum, Math.round(expected)))
318
+ let bestValue = projection[best] || 0
319
+ for (let position = minimum; position <= maximum; position += 1) {
320
+ const value = projection[position] || 0
321
+ if (value > bestValue) {
322
+ best = position
323
+ bestValue = value
324
+ }
325
+ }
326
+ coordinates[index] = best
327
+ }
328
+ return coordinates
329
+ }
330
+
331
+ function fixedGridCoordinates(length, pixelSize) {
332
+ const size = Math.max(2, Math.round(pixelSize))
333
+ const coordinates = []
334
+ for (let position = 0; position < length; position += size) coordinates.push(position)
335
+ if (coordinates[coordinates.length - 1] !== length) coordinates.push(length)
336
+ return Int32Array.from(coordinates)
337
+ }
338
+
339
+ function sampleGrid(source, width, height, xCoordinates, yCoordinates, settings) {
340
+ const outputWidth = xCoordinates.length - 1
341
+ const outputHeight = yCoordinates.length - 1
342
+ const output = new Uint8ClampedArray(outputWidth * outputHeight * 4)
343
+ for (let outputY = 0; outputY < outputHeight; outputY += 1) {
344
+ const y0 = Math.max(0, Math.min(height - 1, yCoordinates[outputY]))
345
+ const y1 = Math.max(y0 + 1, Math.min(height, yCoordinates[outputY + 1]))
346
+ for (let outputX = 0; outputX < outputWidth; outputX += 1) {
347
+ const x0 = Math.max(0, Math.min(width - 1, xCoordinates[outputX]))
348
+ const x1 = Math.max(x0 + 1, Math.min(width, xCoordinates[outputX + 1]))
349
+ const color = sampleCell(source, width, x0, y0, x1, y1, settings)
350
+ const outputOffset = (outputY * outputWidth + outputX) * 4
351
+ output[outputOffset] = color[0]
352
+ output[outputOffset + 1] = color[1]
353
+ output[outputOffset + 2] = color[2]
354
+ output[outputOffset + 3] = color[3]
355
+ }
356
+ }
357
+ return output
358
+ }
359
+
360
+ function sampleCell(source, width, x0, y0, x1, y1, settings) {
361
+ const pixelCount = Math.max(1, (x1 - x0) * (y1 - y0))
362
+ const sampleStep = Math.max(1, Math.ceil(pixelCount / 128))
363
+ const samples = []
364
+ const alphaSamples = []
365
+ let alphaSum = 0
366
+ let sampleIndex = 0
367
+ let centerSample = null
368
+ const centerX = Math.min(x1 - 1, Math.floor((x0 + x1) / 2))
369
+ const centerY = Math.min(y1 - 1, Math.floor((y0 + y1) / 2))
370
+
371
+ for (let y = y0; y < y1; y += 1) {
372
+ for (let x = x0; x < x1; x += 1) {
373
+ const offset = (y * width + x) * 4
374
+ const alpha = source[offset + 3]
375
+ alphaSum += alpha
376
+ if (x === centerX && y === centerY) {
377
+ centerSample = [source[offset], source[offset + 1], source[offset + 2], alpha]
378
+ }
379
+ if (sampleIndex % sampleStep === 0) {
380
+ alphaSamples.push(alpha)
381
+ if (alpha > 4) {
382
+ samples.push([source[offset], source[offset + 1], source[offset + 2], alpha])
383
+ }
384
+ }
385
+ sampleIndex += 1
386
+ }
387
+ }
388
+
389
+ let outputAlpha
390
+ if (settings.alphaMode === 'preserve') {
391
+ outputAlpha = alphaSamples.length ? median(alphaSamples) : 0
392
+ } else {
393
+ const coverageAlpha = alphaSum / pixelCount
394
+ outputAlpha = coverageAlpha >= settings.alphaThreshold ? 255 : 0
395
+ }
396
+ if (outputAlpha <= 0 || !samples.length) return [0, 0, 0, 0]
397
+
398
+ let rgb
399
+ if (settings.sampleMethod === 'center' && centerSample?.[3] > 4) {
400
+ rgb = centerSample.slice(0, 3)
401
+ } else if (settings.sampleMethod === 'median') {
402
+ rgb = weightedMedianColor(samples)
403
+ } else if (settings.sampleMethod === 'center') {
404
+ rgb = weightedMeanColor(samples)
405
+ } else {
406
+ rgb = majorityClusterColor(samples)
407
+ }
408
+ return [rgb[0], rgb[1], rgb[2], Math.round(outputAlpha)]
409
+ }
410
+
411
+ function sampleWeight(sample) {
412
+ const alpha = sample[3] / 255
413
+ return alpha * alpha
414
+ }
415
+
416
+ function weightedMeanColor(samples) {
417
+ let red = 0
418
+ let green = 0
419
+ let blue = 0
420
+ let weightSum = 0
421
+ for (const sample of samples) {
422
+ const weight = sampleWeight(sample)
423
+ red += sample[0] * weight
424
+ green += sample[1] * weight
425
+ blue += sample[2] * weight
426
+ weightSum += weight
427
+ }
428
+ if (weightSum <= 1e-8) return [0, 0, 0]
429
+ return [Math.round(red / weightSum), Math.round(green / weightSum), Math.round(blue / weightSum)]
430
+ }
431
+
432
+ function weightedMedianColor(samples) {
433
+ const channelMedian = (channel) => {
434
+ const values = samples
435
+ .map((sample) => ({ value: sample[channel], weight: sampleWeight(sample) }))
436
+ .sort((left, right) => left.value - right.value)
437
+ const total = values.reduce((sum, item) => sum + item.weight, 0)
438
+ let accumulated = 0
439
+ for (const item of values) {
440
+ accumulated += item.weight
441
+ if (accumulated >= total / 2) return item.value
442
+ }
443
+ return values[values.length - 1]?.value || 0
444
+ }
445
+ return [channelMedian(0), channelMedian(1), channelMedian(2)]
446
+ }
447
+
448
+ function majorityClusterColor(samples) {
449
+ if (samples.length === 1) return samples[0].slice(0, 3)
450
+ let first = samples[0].slice(0, 3)
451
+ let second = first
452
+ let farthestDistance = -1
453
+ for (const sample of samples) {
454
+ const distance = colorDistanceSquared(sample, first)
455
+ if (distance > farthestDistance) {
456
+ farthestDistance = distance
457
+ second = sample.slice(0, 3)
458
+ }
459
+ }
460
+ if (farthestDistance <= 0) return first
461
+
462
+ let firstWeight = 0
463
+ let secondWeight = 0
464
+ for (let iteration = 0; iteration < 6; iteration += 1) {
465
+ const firstSum = [0, 0, 0]
466
+ const secondSum = [0, 0, 0]
467
+ firstWeight = 0
468
+ secondWeight = 0
469
+ for (const sample of samples) {
470
+ const weight = sampleWeight(sample)
471
+ const useSecond = colorDistanceSquared(sample, second) < colorDistanceSquared(sample, first)
472
+ const sum = useSecond ? secondSum : firstSum
473
+ sum[0] += sample[0] * weight
474
+ sum[1] += sample[1] * weight
475
+ sum[2] += sample[2] * weight
476
+ if (useSecond) secondWeight += weight
477
+ else firstWeight += weight
478
+ }
479
+ if (firstWeight > 0) first = firstSum.map((value) => value / firstWeight)
480
+ if (secondWeight > 0) second = secondSum.map((value) => value / secondWeight)
481
+ }
482
+ const result = secondWeight >= firstWeight ? second : first
483
+ return result.map((value) => Math.max(0, Math.min(255, Math.round(value))))
484
+ }
485
+
486
+ function colorDistanceSquared(sample, color) {
487
+ const red = sample[0] - color[0]
488
+ const green = sample[1] - color[1]
489
+ const blue = sample[2] - color[2]
490
+ return red * red + green * green + blue * blue
491
+ }
492
+
493
+ function median(values) {
494
+ const sorted = [...values].sort((left, right) => left - right)
495
+ const middle = Math.floor(sorted.length / 2)
496
+ return sorted.length % 2
497
+ ? sorted[middle]
498
+ : (sorted[middle - 1] + sorted[middle]) / 2
499
+ }
@@ -139,7 +139,7 @@ export async function smartSplitImage(imageUrl) {
139
139
  return a.minX - b.minX
140
140
  })
141
141
 
142
- // 原图尺寸裁切(更高清),并统一到完全相同的画布尺寸,保证序列播放不抖动
142
+ // 原图尺寸裁切(更高清),并统一到完全相同的画布尺寸,保证序列播放不抖动
143
143
  const inv = 1 / scale
144
144
  const crops = []
145
145
  for (const box of boxes) {