v0.1.0 initial commit

This commit is contained in:
2026-07-12 17:01:37 -04:00
commit c3ca2ebb63
64 changed files with 10453 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
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}
}