package compositor import ( "image" "math" ) // DrawMetallicSheen 绘制对角金属反光梯度(screen 混合) // 复刻 laserBatchExport.js 中 drawMetallicSheen func DrawMetallicSheen(dst *image.NRGBA, cfg GratingConfig) { cw := dst.Bounds().Dx() ch := dst.Bounds().Dy() deg := cfg.SheenBandAngle if deg == 0 { deg = 135 } // 与色带垂直方向 deg += 90 rad := deg * math.Pi / 180 diag := math.Sqrt(float64(cw*cw + ch*ch)) cx := float64(cw) / 2 cy := float64(ch) / 2 x0 := cx - math.Cos(rad)*diag/2 y0 := cy - math.Sin(rad)*diag/2 x1 := cx + math.Cos(rad)*diag/2 y1 := cy + math.Sin(rad)*diag/2 alpha := 0.22 for y := 0; y < ch; y++ { for x := 0; x < cw; x++ { // 计算投影位置 0-1 t := projectPointOnLine(float64(x), float64(y), x0, y0, x1, y1) // 中间带高亮,两端透明 var brightness float64 if t > 0.30 && t < 0.70 { // 高亮区域:余弦波 brightness = math.Cos((t-0.50)/0.20*math.Pi) * 0.5 if brightness < 0 { brightness = 0 } } if brightness < 0.01 { continue } applyScreenPixel(dst, x, y, 220.0/255.0, 230.0/255.0, 240.0/255.0, brightness*alpha) } } } func projectPointOnLine(px, py, lx0, ly0, lx1, ly1 float64) float64 { dx := lx1 - lx0 dy := ly1 - ly0 lenSq := dx*dx + dy*dy if lenSq < 0.001 { return 0.5 } t := ((px-lx0)*dx + (py-ly0)*dy) / lenSq return clamp01(t) } // DrawFrostMatte 磨砂哑光层 // 复刻 laserBatchExport.js 中 drawFrostMatte func DrawFrostMatte(dst *image.NRGBA, variantIndex int) { cw := dst.Bounds().Dx() ch := dst.Bounds().Dy() frost := 0.18 // 固定强度 // 颗粒噪声 seed := variantIndex*1201 + 77 n := int(3500 + frost*7500) for k := 0; k < n; k++ { seed = (seed*9301 + 49297) % 233280 x := seed % cw seed = (seed*9301 + 49297) % 233280 y := seed % ch seed = (seed*9301 + 49297) % 233280 v := 190 + seed%65 a := 0.022 + float64(seed%55)/1000.0 idx := dst.PixOffset(x, y) // soft-light 混合简化 factor := a * (0.35 + frost*0.25) dst.Pix[idx] = uint8(clamp01(float64(dst.Pix[idx])/255.0 + float64(v)*factor/255.0) * 255) dst.Pix[idx+1] = uint8(clamp01(float64(dst.Pix[idx+1])/255.0 + float64(v)*factor/255.0) * 255) dst.Pix[idx+2] = uint8(clamp01(float64(dst.Pix[idx+2])/255.0 + float64(v+6)*factor/255.0) * 255) } } // DrawFilmGrain 胶片颗粒 // 复刻 laserBatchExport.js 中 drawFilmGrain func DrawFilmGrain(dst *image.NRGBA, variantIndex int) { cw := dst.Bounds().Dx() ch := dst.Bounds().Dy() maxDots := 8 seed := variantIndex*313 + 17 for i := 0; i < maxDots; i++ { seed = (seed*1103515245 + 12345) & 0x7fffffff fx := float64(seed) / float64(0x7fffffff) * float64(cw) seed = (seed*1103515245 + 12345) & 0x7fffffff fy := float64(seed) / float64(0x7fffffff) * float64(ch) x := int(fx) y := int(fy) if x < 0 || x >= cw || y < 0 || y >= ch { continue } seed = (seed*1103515245 + 12345) & 0x7fffffff radius := float64(seed)/float64(0x7fffffff)*1.5 + 0.3 alpha := 0.08 + float64(seed%12)/100.0 drawSoftDot(dst, x, y, int(radius), alpha) } } func drawSoftDot(dst *image.NRGBA, cx, cy, radius int, alpha float64) { for dy := -radius; dy <= radius; dy++ { for dx := -radius; dx <= radius; dx++ { if dx*dx+dy*dy > radius*radius { continue } px, py := cx+dx, cy+dy if px < 0 || px >= dst.Bounds().Dx() || py < 0 || py >= dst.Bounds().Dy() { continue } dist := math.Sqrt(float64(dx*dx + dy*dy)) fade := 1 - dist/float64(radius) idx := dst.PixOffset(px, py) sc := alpha * fade dst.Pix[idx] = uint8(clamp01(float64(dst.Pix[idx])/255.0 + 1.0*sc) * 255) dst.Pix[idx+1] = uint8(clamp01(float64(dst.Pix[idx+1])/255.0 + 1.0*sc) * 255) dst.Pix[idx+2] = uint8(clamp01(float64(dst.Pix[idx+2])/255.0 + 1.0*sc) * 255) } } } // DrawGratingFinish 光栅 finish:颗粒 + 磨砂 func DrawGratingFinish(dst *image.NRGBA, variantIndex int) { DrawFilmGrain(dst, variantIndex) DrawFrostMatte(dst, variantIndex) }