185 lines
4.4 KiB
Go
185 lines
4.4 KiB
Go
package terminal
|
|
|
|
import "math"
|
|
|
|
// Perez SoftLight lookup tables (array access, no pointers)
|
|
// Pre-computed at init to avoid sqrt/division in per-cell loops
|
|
var (
|
|
softLightG [256]float64
|
|
softLightDF [256]float64
|
|
)
|
|
|
|
func init() {
|
|
for i := 0; i < 256; i++ {
|
|
df := float64(i) / 255.0
|
|
softLightDF[i] = df
|
|
if df <= 0.25 {
|
|
softLightG[i] = ((16.0*df-12.0)*df + 4.0) * df
|
|
} else {
|
|
softLightG[i] = math.Sqrt(df)
|
|
}
|
|
}
|
|
}
|
|
|
|
// clampU8 converts float to uint8 with saturation
|
|
func clampU8(v float64) uint8 {
|
|
if v >= 255.0 {
|
|
return 255
|
|
}
|
|
if v <= 0.0 {
|
|
return 0
|
|
}
|
|
return uint8(v)
|
|
}
|
|
|
|
// addU8 is saturating uint8 addition
|
|
func addU8(a, b uint8) uint8 {
|
|
sum := int(a) + int(b)
|
|
if sum > 255 {
|
|
return 255
|
|
}
|
|
return uint8(sum)
|
|
}
|
|
|
|
// fastDiv255 approximates x / 255 using integer math: (x + (x >> 8) + 1) >> 8
|
|
// Faster than DIV instruction, exact for x in [0, 255*255]
|
|
func fastDiv255(x int) int {
|
|
return (x + (x >> 8) + 1) >> 8
|
|
}
|
|
|
|
// softLightChannel applies Perez soft light to one channel via LUTs
|
|
func softLightChannel(d, s uint8, intensity float64) uint8 {
|
|
df := softLightDF[d]
|
|
sf := softLightDF[s]
|
|
|
|
var result float64
|
|
if sf < 0.5 {
|
|
result = df - (1.0-2.0*sf)*df*(1.0-df)
|
|
} else {
|
|
// LUT replaces math.Sqrt
|
|
result = df + (2.0*sf-1.0)*(softLightG[d]-df)
|
|
}
|
|
|
|
// Lerp toward result by intensity, single dependency chain
|
|
result = df + (result-df)*intensity
|
|
|
|
return clampU8(result*255.0 + 0.5) // +0.5 for rounding
|
|
}
|
|
|
|
// overlayChannel combines multiply (d < 128) and screen (d >= 128),
|
|
// preserving destination highlights and shadows
|
|
func overlayChannel(d, s uint8) uint8 {
|
|
if d < 128 {
|
|
return uint8(fastDiv255(2 * int(d) * int(s)))
|
|
}
|
|
return uint8(255 - fastDiv255(2*(255-int(d))*(255-int(s))))
|
|
}
|
|
|
|
// Blend performs linear alpha blend of src over dst
|
|
// alpha <= 0 returns dst, alpha >= 1 returns src
|
|
func Blend(dst, src RGB, alpha float64) RGB {
|
|
if alpha >= 1.0 {
|
|
return src
|
|
}
|
|
if alpha <= 0.0 {
|
|
return dst
|
|
}
|
|
inv := 1.0 - alpha
|
|
return RGB{
|
|
R: uint8(float64(src.R)*alpha + float64(dst.R)*inv),
|
|
G: uint8(float64(src.G)*alpha + float64(dst.G)*inv),
|
|
B: uint8(float64(src.B)*alpha + float64(dst.B)*inv),
|
|
}
|
|
}
|
|
|
|
// SoftLight applies Perez soft light blend, gentler than linear alpha
|
|
// intensity in [0,1] mixes between dst and the blended result
|
|
func SoftLight(dst, src RGB, intensity float64) RGB {
|
|
return RGB{
|
|
R: softLightChannel(dst.R, src.R, intensity),
|
|
G: softLightChannel(dst.G, src.G, intensity),
|
|
B: softLightChannel(dst.B, src.B, intensity),
|
|
}
|
|
}
|
|
|
|
// Max returns per-channel maximum, alpha-blended over dst
|
|
func Max(dst, src RGB, alpha float64) RGB {
|
|
if alpha <= 0.0 {
|
|
return dst
|
|
}
|
|
maxed := RGB{
|
|
R: max(dst.R, src.R),
|
|
G: max(dst.G, src.G),
|
|
B: max(dst.B, src.B),
|
|
}
|
|
if alpha >= 1.0 {
|
|
return maxed
|
|
}
|
|
return Blend(dst, maxed, alpha)
|
|
}
|
|
|
|
// Add performs saturating additive blend, alpha-blended over dst
|
|
func Add(dst, src RGB, alpha float64) RGB {
|
|
if alpha <= 0.0 {
|
|
return dst
|
|
}
|
|
added := RGB{
|
|
R: addU8(dst.R, src.R),
|
|
G: addU8(dst.G, src.G),
|
|
B: addU8(dst.B, src.B),
|
|
}
|
|
if alpha >= 1.0 {
|
|
return added
|
|
}
|
|
return Blend(dst, added, alpha)
|
|
}
|
|
|
|
// Screen applies 1-(1-dst)*(1-src), alpha-blended over dst
|
|
// Always lightens; useful for glow accumulation without clipping harshness of Add
|
|
func Screen(dst, src RGB, alpha float64) RGB {
|
|
if alpha <= 0.0 {
|
|
return dst
|
|
}
|
|
screened := RGB{
|
|
R: uint8(255 - fastDiv255((255-int(dst.R))*(255-int(src.R)))),
|
|
G: uint8(255 - fastDiv255((255-int(dst.G))*(255-int(src.G)))),
|
|
B: uint8(255 - fastDiv255((255-int(dst.B))*(255-int(src.B)))),
|
|
}
|
|
if alpha >= 1.0 {
|
|
return screened
|
|
}
|
|
return Blend(dst, screened, alpha)
|
|
}
|
|
|
|
// Overlay combines multiply (darks) and screen (lights), alpha-blended over dst
|
|
func Overlay(dst, src RGB, alpha float64) RGB {
|
|
if alpha <= 0.0 {
|
|
return dst
|
|
}
|
|
overlaid := RGB{
|
|
R: overlayChannel(dst.R, src.R),
|
|
G: overlayChannel(dst.G, src.G),
|
|
B: overlayChannel(dst.B, src.B),
|
|
}
|
|
if alpha >= 1.0 {
|
|
return overlaid
|
|
}
|
|
return Blend(dst, overlaid, alpha)
|
|
}
|
|
|
|
// Scale multiplies all channels by factor, saturating (factor > 1.0 brightens)
|
|
func Scale(c RGB, factor float64) RGB {
|
|
return RGB{
|
|
R: clampU8(float64(c.R) * factor),
|
|
G: clampU8(float64(c.G) * factor),
|
|
B: clampU8(float64(c.B) * factor),
|
|
}
|
|
}
|
|
|
|
// Grayscale converts to grayscale using Rec. 601 luma coefficients
|
|
// Y = R*0.299 + G*0.587 + B*0.114, integer math
|
|
func Grayscale(c RGB) RGB {
|
|
gray := uint8((int(c.R)*299 + int(c.G)*587 + int(c.B)*114) / 1000)
|
|
return RGB{R: gray, G: gray, B: gray}
|
|
}
|