fix:提交代码

This commit is contained in:
ljy
2026-09-07 00:07:40 +08:00
parent b822d21eae
commit 11bbf6cb0c
50 changed files with 1832 additions and 510 deletions
+142 -11
View File
@@ -8,6 +8,9 @@ const root = path.resolve(__dirname, '..')
const outThumb = path.join(root, 'public/assets/stage/thumb')
const outFull = path.join(root, 'public/assets/stage/full')
/** Stage UI dark bg — fringe target */
const DARK = { r: 14, g: 12, b: 16 }
/** @type {{ src: string; name: string; thumbW: number; fullW: number }[]} */
const STAGE_SOURCES = [
{ src: 'public/assets/logo.png', name: 'logo', thumbW: 48, fullW: 512 },
@@ -22,29 +25,156 @@ async function ensureDirs() {
await fs.mkdir(outFull, { recursive: true })
}
function lum(r, g, b) {
return 0.299 * r + 0.587 * g + 0.114 * b
}
function sat(r, g, b) {
return Math.max(r, g, b) - Math.min(r, g, b)
}
/**
* White fringe → dark fringe for dark UI:
* 1) transparent RGB → black
* 2) semi-transparent: un-white-matte + premultiply toward black
* 3) opaque edge near transparency: crush whitish fringe to dark
*/
async function defringeBottle(srcPathOrBuf) {
const { data, info } = await sharp(srcPathOrBuf).ensureAlpha().raw().toBuffer({ resolveWithObject: true })
const w = info.width
const h = info.height
const out = Buffer.from(data)
const idx = (x, y) => (y * w + x) * 4
// Flood-fill only transparent pixels connected to the canvas edge.
// This prevents transparent details inside labels/text from being treated as silhouette.
const exterior = new Uint8Array(w * h)
const queue = new Int32Array(w * h)
let head = 0
let tail = 0
const enqueue = (x, y) => {
const p = y * w + x
if (exterior[p] || data[p * 4 + 3] > 16) return
exterior[p] = 1
queue[tail++] = p
}
for (let x = 0; x < w; x++) {
enqueue(x, 0)
enqueue(x, h - 1)
}
for (let y = 0; y < h; y++) {
enqueue(0, y)
enqueue(w - 1, y)
}
while (head < tail) {
const p = queue[head++]
const x = p % w
const y = Math.floor(p / w)
if (x > 0) enqueue(x - 1, y)
if (x + 1 < w) enqueue(x + 1, y)
if (y > 0) enqueue(x, y - 1)
if (y + 1 < h) enqueue(x, y + 1)
}
const R = 3
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
const p = y * w + x
const i = p * 4
if (exterior[p]) {
out[i] = out[i + 1] = out[i + 2] = 0
continue
}
let distance = R + 1
for (let dy = -R; dy <= R; dy++) {
for (let dx = -R; dx <= R; dx++) {
const xx = x + dx
const yy = y + dy
if (xx < 0 || yy < 0 || xx >= w || yy >= h) continue
if (exterior[yy * w + xx]) {
distance = Math.min(distance, Math.max(Math.abs(dx), Math.abs(dy)))
}
}
}
if (distance > R) continue
const a = data[i + 3]
let r = data[i]
let g = data[i + 1]
let b = data[i + 2]
if (a < 255) {
const af = a / 255
r = Math.min(255, Math.max(0, Math.round((r - 255 * (1 - af)) / af)))
g = Math.min(255, Math.max(0, Math.round((g - 255 * (1 - af)) / af)))
b = Math.min(255, Math.max(0, Math.round((b - 255 * (1 - af)) / af)))
}
// Turn the complete exterior silhouette into a dark feather:
// strongest on the outermost pixel, fading by the third pixel.
// Because the mask is flood-filled from the canvas edge, internal labels stay untouched.
const strength = distance === 1 ? 0.96 : distance === 2 ? 0.68 : 0.34
out[i] = Math.round(r * (1 - strength) + DARK.r * strength)
out[i + 1] = Math.round(g * (1 - strength) + DARK.g * strength)
out[i + 2] = Math.round(b * (1 - strength) + DARK.b * strength)
}
}
return sharp(out, { raw: { width: w, height: h, channels: 4 } }).png().toBuffer()
}
/** After WebP encode, force transparent RGB to black again. */
async function neutralizeTransparentRgb(webpBuf) {
const { data, info } = await sharp(webpBuf).ensureAlpha().raw().toBuffer({ resolveWithObject: true })
for (let i = 0; i < data.length; i += 4) {
if (data[i + 3] === 0) {
data[i] = data[i + 1] = data[i + 2] = 0
}
}
return sharp(data, { raw: { width: info.width, height: info.height, channels: 4 } })
.webp({ quality: 86, alphaQuality: 100 })
.toBuffer()
}
async function writeThumb(srcPath, name, width) {
const dest = path.join(outThumb, `${name}.webp`)
await sharp(srcPath)
.resize(width, null, { withoutEnlargement: true })
.blur(2)
.webp({ quality: 50 })
.toFile(dest)
let pipe = sharp(srcPath).resize(width, null, { withoutEnlargement: true }).blur(2)
let buf = await pipe.webp({ quality: 50, alphaQuality: 100 }).toBuffer()
if (name === 't30-bottle') buf = await neutralizeTransparentRgb(buf)
await fs.writeFile(dest, buf)
const stat = await fs.stat(dest)
return { dest, kb: Math.round(stat.size / 1024 * 10) / 10 }
return { dest, kb: Math.round((stat.size / 1024) * 10) / 10 }
}
async function writeFull(srcPath, name, width) {
const dest = path.join(outFull, `${name}.webp`)
await sharp(srcPath)
.resize(width, null, { withoutEnlargement: true })
.webp({ quality: 82 })
.toFile(dest)
let pipe = sharp(srcPath).resize(width, null, {
withoutEnlargement: true,
})
let buf = await pipe.webp({ quality: 86, alphaQuality: 100 }).toBuffer()
if (name === 't30-bottle') buf = await neutralizeTransparentRgb(buf)
await fs.writeFile(dest, buf)
const stat = await fs.stat(dest)
return { dest, kb: Math.round(stat.size / 1024 * 10) / 10 }
return { dest, kb: Math.round((stat.size / 1024) * 10) / 10 }
}
async function rewriteBottlePng(srcPath) {
const fixed = await defringeBottle(srcPath)
await fs.writeFile(srcPath, fixed)
const stat = await fs.stat(srcPath)
return Math.round((stat.size / 1024) * 10) / 10
}
async function main() {
await ensureDirs()
const bottleSrc = path.join(root, 'public/assets/t30-bottle.png')
const pngKb = await rewriteBottlePng(bottleSrc)
console.log(`t30-bottle.png rewritten (${pngKb} KB)`)
let thumbTotal = 0
let fullTotal = 0
@@ -57,6 +187,7 @@ async function main() {
continue
}
// Bottle PNG was already defringed once above; do not process it twice.
const thumb = await writeThumb(srcPath, item.name, item.thumbW)
const full = await writeFull(srcPath, item.name, item.fullW)
thumbTotal += thumb.kb
+6
View File
@@ -9,6 +9,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
const outDir = path.resolve(__dirname, '../dist')
const ROUTES = [
'/h5',
'/h5/brand',
'/h5/poetry',
'/h5/qingxiang',
'/h5/haoke',
'/h5/contact',
'/brand',
'/poetry',
'/qingxiang',