fix:提交代码
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user