| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103 |
- // 测量两个白母版的 alpha 包围盒、尖端(最低不透明点)、圆帽几何(帽心/帽径)
- // 输出将作为 markerAssets.js 的实测依据(规格 10:锚点/内层几何按素材属性登记)
- import { readFileSync } from 'node:fs'
- import { inflateSync } from 'node:zlib'
- function decodePng(path) {
- const buf = readFileSync(path)
- if (buf.readUInt32BE(0) !== 0x89504e47) throw new Error('非 PNG')
- let pos = 8
- const idat = []
- let width = 0, height = 0, bitDepth = 0, colorType = 0, palette = null, trns = null
- while (pos < buf.length) {
- const len = buf.readUInt32BE(pos)
- const type = buf.toString('ascii', pos + 4, pos + 8)
- const data = buf.subarray(pos + 8, pos + 8 + len)
- if (type === 'IHDR') {
- width = data.readUInt32BE(0); height = data.readUInt32BE(4)
- bitDepth = data[8]; colorType = data[9]
- } else if (type === 'PLTE') palette = Buffer.from(data)
- else if (type === 'tRNS') trns = Buffer.from(data)
- else if (type === 'IDAT') idat.push(data)
- else if (type === 'IEND') break
- pos += 12 + len
- }
- const channels = colorType === 6 ? 4 : colorType === 2 ? 3 : 1
- const stride = width * channels
- const raw = inflateSync(Buffer.concat(idat))
- const img = Buffer.alloc(height * stride)
- for (let y = 0; y < height; y++) {
- const filter = raw[y * (stride + 1)]
- const row = raw.subarray(y * (stride + 1) + 1, (y + 1) * (stride + 1))
- const prev = y > 0 ? img.subarray((y - 1) * stride, y * stride) : null
- const out = img.subarray(y * stride, (y + 1) * stride)
- for (let x = 0; x < stride; x++) {
- const a = x >= channels ? out[x - channels] : 0
- const b = prev ? prev[x] : 0
- const c = prev && x >= channels ? prev[x - channels] : 0
- let v = row[x]
- if (filter === 1) v += a
- else if (filter === 2) v += b
- else if (filter === 3) v += (a + b) >> 1
- else if (filter === 4) {
- const p = a + b - c
- const pa = Math.abs(p - a), pb = Math.abs(p - b), pc = Math.abs(p - c)
- v += pa <= pb && pa <= pc ? a : pb <= pc ? b : c
- }
- out[x] = v & 0xff
- }
- }
- // 提取 alpha 通道函数
- const alphaAt = (x, y) => {
- if (colorType === 6) return img[y * stride + x * 4 + 3]
- if (colorType === 2) return 255
- const idx = img[y * stride + x]
- if (trns && idx < trns.length) return trns[idx]
- return 255
- }
- return { width, height, alphaAt }
- }
- for (const file of ['public/cockpit/assets/icons/T_MarkPoint_WhiteFull.png', 'public/cockpit/assets/icons/T_MarkPoint_White.png']) {
- const { width, height, alphaAt } = decodePng(file)
- let minX = width, maxX = -1, minY = height, maxY = -1
- for (let y = 0; y < height; y++) {
- for (let x = 0; x < width; x++) {
- if (alphaAt(x, y) > 8) {
- if (x < minX) minX = x
- if (x > maxX) maxX = x
- if (y < minY) minY = y
- if (y > maxY) maxY = y
- }
- }
- }
- const bw = maxX - minX + 1
- const bh = maxY - minY + 1
- // 尖端:包围盒底部一行的不透明质心 x
- let tipSum = 0, tipCount = 0
- for (let x = minX; x <= maxX; x++) {
- if (alphaAt(x, maxY) > 8) { tipSum += x; tipCount++ }
- }
- const tipX = tipCount ? tipSum / tipCount : -1
- // 圆帽几何:从顶往下找「宽度最大的行」——实心版即圆帽直径线;镂空版因孔洞偏下,取首次接近 95% 最大宽的行
- let widest = 0, widestY = minY
- let firstNearMaxY = -1
- for (let y = minY; y <= maxY; y++) {
- let cnt = 0
- for (let x = minX; x <= maxX; x++) if (alphaAt(x, y) > 8) cnt++
- if (cnt > widest) { widest = cnt; widestY = y }
- if (firstNearMaxY < 0 && widest > 0 && cnt >= widest * 0.95) firstNearMaxY = y
- }
- const capCY = firstNearMaxY
- const capCX = (() => {
- let s = 0, n = 0
- for (let x = minX; x <= maxX; x++) if (alphaAt(x, capCY) > 8) { s += x; n++ }
- return n ? s / n : -1
- })()
- console.log(`${file.split('/').pop()}:`)
- console.log(` 画布 ${width}x${height} | bbox x${minX}..${maxX} y${minY}..${maxY} (${bw}x${bh})`)
- console.log(` 宽高比 ${ (bh / bw).toFixed(3) }`)
- console.log(` 尖端 x=${tipX.toFixed(1)} (bbox 相对 ${(100 * (tipX - minX) / bw).toFixed(1)}%) y=${maxY}`)
- console.log(` 圆帽线 y=${capCY}(帽心相对高度 ${((capCY - minY) / bh).toFixed(3)})帽心x=${capCX.toFixed(1)}(bbox 相对 ${(100 * (capCX - minX) / bw).toFixed(1)}%)`)
- console.log(` 锚点(尖端)相对 bbox: x=${((tipX - minX) / bw).toFixed(4)}, y=1.0`)
- }
|