measure-master.mjs 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
  1. // 测量两个白母版的 alpha 包围盒、尖端(最低不透明点)、圆帽几何(帽心/帽径)
  2. // 输出将作为 markerAssets.js 的实测依据(规格 10:锚点/内层几何按素材属性登记)
  3. import { readFileSync } from 'node:fs'
  4. import { inflateSync } from 'node:zlib'
  5. function decodePng(path) {
  6. const buf = readFileSync(path)
  7. if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('非 PNG')
  8. let pos = 8
  9. const idat = []
  10. let width = 0, height = 0, bitDepth = 0, colorType = 0, palette = null, trns = null
  11. while (pos < buf.length) {
  12. const len = buf.readUInt32BE(pos)
  13. const type = buf.toString('ascii', pos + 4, pos + 8)
  14. const data = buf.subarray(pos + 8, pos + 8 + len)
  15. if (type === 'IHDR') {
  16. width = data.readUInt32BE(0); height = data.readUInt32BE(4)
  17. bitDepth = data[8]; colorType = data[9]
  18. } else if (type === 'PLTE') palette = Buffer.from(data)
  19. else if (type === 'tRNS') trns = Buffer.from(data)
  20. else if (type === 'IDAT') idat.push(data)
  21. else if (type === 'IEND') break
  22. pos += 12 + len
  23. }
  24. const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 1
  25. const stride = width * channels
  26. const raw = inflateSync(Buffer.concat(idat))
  27. const img = Buffer.alloc(height * stride)
  28. for (let y = 0; y < height; y++) {
  29. const filter = raw[y * (stride + 1)]
  30. const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1))
  31. const prev = y > 0 ? img.subarray((y - 1) * stride, y * stride) : null
  32. const out = img.subarray(y * stride, (y + 1) * stride)
  33. for (let x = 0; x < stride; x++) {
  34. const a = x >= channels ? out[x - channels] : 0
  35. const b = prev ? prev[x] : 0
  36. const c = prev && x >= channels ? prev[x - channels] : 0
  37. let v = row[x]
  38. if (filter === 1) v += a
  39. else if (filter === 2) v += b
  40. else if (filter === 3) v += (a + b) >> 1
  41. else if (filter === 4) {
  42. const p = a + b - c
  43. const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c)
  44. v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c
  45. }
  46. out[x] = v & 0xff
  47. }
  48. }
  49. // 提取 alpha 通道函数
  50. const alphaAt = (x, y) => {
  51. if (colorType === 6) return img[y * stride + x * 4 + 3]
  52. if (colorType === 2) return 255
  53. const idx = img[y * stride + x]
  54. if (trns && idx < trns.length) return trns[idx]
  55. return 255
  56. }
  57. return { width, height, alphaAt }
  58. }
  59. for (const file of ['public/cockpit/assets/icons/T_MarkPoint_WhiteFull.png', 'public/cockpit/assets/icons/T_MarkPoint_White.png']) {
  60. const { width, height, alphaAt } = decodePng(file)
  61. let minX = width, maxX = -1, minY = height, maxY = -1
  62. for (let y = 0; y < height; y++) {
  63. for (let x = 0; x < width; x++) {
  64. if (alphaAt(x, y) > 8) {
  65. if (x < minX) minX = x
  66. if (x > maxX) maxX = x
  67. if (y < minY) minY = y
  68. if (y > maxY) maxY = y
  69. }
  70. }
  71. }
  72. const bw = maxX - minX + 1
  73. const bh = maxY - minY + 1
  74. // 尖端:包围盒底部一行的不透明质心 x
  75. let tipSum = 0, tipCount = 0
  76. for (let x = minX; x <= maxX; x++) {
  77. if (alphaAt(x, maxY) > 8) { tipSum += x; tipCount++ }
  78. }
  79. const tipX = tipCount ? tipSum / tipCount : -1
  80. // 圆帽几何:从顶往下找「宽度最大的行」——实心版即圆帽直径线;镂空版因孔洞偏下,取首次接近 95% 最大宽的行
  81. let widest = 0, widestY = minY
  82. let firstNearMaxY = -1
  83. for (let y = minY; y <= maxY; y++) {
  84. let cnt = 0
  85. for (let x = minX; x <= maxX; x++) if (alphaAt(x, y) > 8) cnt++
  86. if (cnt > widest) { widest = cnt; widestY = y }
  87. if (firstNearMaxY < 0 && widest > 0 && cnt >= widest * 0.95) firstNearMaxY = y
  88. }
  89. const capCY = firstNearMaxY
  90. const capCX = (() => {
  91. let s = 0, n = 0
  92. for (let x = minX; x <= maxX; x++) if (alphaAt(x, capCY) > 8) { s += x; n++ }
  93. return n ? s / n : -1
  94. })()
  95. console.log(`${file.split('/').pop()}:`)
  96. console.log(` 画布 ${width}x${height} | bbox x${minX}..${maxX} y${minY}..${maxY} (${bw}x${bh})`)
  97. console.log(` 宽高比 ${ (bh / bw).toFixed(3) }`)
  98. console.log(` 尖端 x=${tipX.toFixed(1)} (bbox 相对 ${(100 * (tipX - minX) / bw).toFixed(1)}%) y=${maxY}`)
  99. console.log(` 圆帽线 y=${capCY}(帽心相对高度 ${((capCY - minY) / bh).toFixed(3)})帽心x=${capCX.toFixed(1)}(bbox 相对 ${(100 * (capCX - minX) / bw).toFixed(1)}%)`)
  100. console.log(` 锚点(尖端)相对 bbox: x=${((tipX - minX) / bw).toFixed(4)}, y=1.0`)
  101. }