Files
caire/imop/comp.go
2025-04-27 10:40:09 +03:00

473 lines
11 KiB
Go

// Package imop implements the Porter-Duff composition operations
// used for mixing a graphic element with its backdrop.
// Porter and Duff presented in their paper 12 different composition operation, but the
// core image/draw core package implements only the source-over-destination and source.
// This package implements all of the 12 composite operation together with some blending modes.
package imop
import (
"fmt"
"image"
"image/color"
"math"
"github.com/esimov/caire/utils"
)
type CompType int
const (
Clear CompType = iota
Copy
Dst
SrcOver
DstOver
SrcIn
DstIn
SrcOut
DstOut
SrcAtop
DstAtop
Xor
)
// Bitmap holds an image type as a placeholder for the Porter-Duff composition
// operations which can be used as a source or destination image.
type Bitmap struct {
Img *image.NRGBA
}
// Composite struct contains the currently active composition operation and all the supported operations.
type Composite struct {
CurrentOp CompType
Ops []CompType
}
// NewBitmap initializes a new Bitmap.
func NewBitmap(rect image.Rectangle) *Bitmap {
return &Bitmap{
Img: image.NewNRGBA(rect),
}
}
// InitOp initializes a new composition operation.
func InitOp() *Composite {
return &Composite{
CurrentOp: SrcOver,
Ops: []CompType{
Clear,
Copy,
Dst,
SrcOver,
DstOver,
SrcIn,
DstIn,
SrcOut,
DstOut,
SrcAtop,
DstAtop,
Xor,
},
}
}
// Set changes the current composition operation.
func (op *Composite) Set(compType CompType) error {
if utils.Contains(op.Ops, compType) {
op.CurrentOp = compType
return nil
}
return fmt.Errorf("unsupported composition operation")
}
// Set changes the current composition operation.
func (op *Composite) Get() CompType {
return op.CurrentOp
}
// Draw applies the currently active Ported-Duff composition operation formula,
// taking as parameter the source and the destination image and draws the result into the bitmap.
// If a blend mode is activated it will plug in the alpha blending formula also into the equation.
func (op *Composite) Draw(bitmap *Bitmap, src, dst *image.NRGBA, blend *Blend) {
dx, dy := src.Bounds().Dx(), src.Bounds().Dy()
var (
r, g, b, a uint32
rn, gn, bn, an float64
)
for x := 0; x < dx; x++ {
for y := 0; y < dy; y++ {
r1, g1, b1, a1 := src.At(x, y).RGBA()
r2, g2, b2, a2 := dst.At(x, y).RGBA()
rs, gs, bs, as := r1>>8, g1>>8, b1>>8, a1>>8
rb, gb, bb, ab := r2>>8, g2>>8, b2>>8, a2>>8
// normalize the values.
rsn := float64(rs) / 255
gsn := float64(gs) / 255
bsn := float64(bs) / 255
asn := float64(as) / 255
rbn := float64(rb) / 255
gbn := float64(gb) / 255
bbn := float64(bb) / 255
abn := float64(ab) / 255
// applying the alpha composition formula
switch op.CurrentOp {
case Clear:
rn, gn, bn, an = 0, 0, 0, 0
case Copy:
rn = asn * rsn
gn = asn * gsn
bn = asn * bsn
an = asn * asn
case Dst:
rn = abn * rbn
gn = abn * gbn
bn = abn * bbn
an = abn * abn
case SrcOver:
rn = asn*rsn + abn*rbn*(1-asn)
gn = asn*gsn + abn*gbn*(1-asn)
bn = asn*bsn + abn*bbn*(1-asn)
an = asn + abn*(1-asn)
case DstOver:
rn = asn*rsn*(1-abn) + abn*rbn
gn = asn*gsn*(1-abn) + abn*gbn
bn = asn*bsn*(1-abn) + abn*bbn
an = asn*(1-abn) + abn
case SrcIn:
rn = asn * rsn * abn
gn = asn * gsn * abn
bn = asn * bsn * abn
an = asn * abn
case DstIn:
rn = abn * rbn * asn
gn = abn * gbn * asn
bn = abn * bbn * asn
an = abn * asn
case SrcOut:
rn = asn * rsn * (1 - abn)
gn = asn * gsn * (1 - abn)
bn = asn * bsn * (1 - abn)
an = asn * (1 - abn)
case DstOut:
rn = abn * rbn * (1 - asn)
gn = abn * gbn * (1 - asn)
bn = abn * bbn * (1 - asn)
an = abn * (1 - asn)
case SrcAtop:
rn = asn*rsn*abn + (1-asn)*abn*rbn
gn = asn*gsn*abn + (1-asn)*abn*gbn
bn = asn*bsn*abn + (1-asn)*abn*bbn
an = asn*abn + abn*(1-asn)
case DstAtop:
rn = asn*rsn*(1-abn) + abn*rbn*asn
gn = asn*gsn*(1-abn) + abn*gbn*asn
bn = asn*bsn*(1-abn) + abn*bbn*asn
an = asn*(1-abn) + abn*asn
case Xor:
rn = asn*rsn*(1-abn) + abn*rbn*(1-asn)
gn = asn*gsn*(1-abn) + abn*gbn*(1-asn)
bn = asn*bsn*(1-abn) + abn*bbn*(1-asn)
an = asn*(1-abn) + abn*(1-asn)
}
r = uint32(rn * 255)
g = uint32(gn * 255)
b = uint32(bn * 255)
a = uint32(an * 255)
bitmap.Img.Set(x, y, color.NRGBA{
R: uint8(r),
G: uint8(g),
B: uint8(b),
A: uint8(a),
})
// applying the blending mode
if blend != nil {
rn, gn, bn, an = 0, 0, 0, 0 // reset the colors
r1, g1, b1, a1 = src.At(x, y).RGBA()
r2, g2, b2, a2 = dst.At(x, y).RGBA()
rs, gs, bs, as = r1>>8, g1>>8, b1>>8, a1>>8
rb, gb, bb, ab = r2>>8, g2>>8, b2>>8, a2>>8
rsn = float64(rs) / 255
gsn = float64(gs) / 255
bsn = float64(bs) / 255
asn = float64(as) / 255
rbn = float64(rb) / 255
gbn = float64(gb) / 255
bbn = float64(bb) / 255
abn = float64(ab) / 255
foreground := Color{R: rsn, G: gsn, B: bsn}
background := Color{R: rbn, G: gbn, B: bbn}
switch blend.CurrentOp {
case Normal:
rn, gn, bn, an = rsn, gsn, bsn, asn
case Darken:
rn = utils.Min(rsn, rbn)
gn = utils.Min(gsn, gbn)
bn = utils.Min(bsn, bbn)
an = utils.Min(asn, abn)
case Lighten:
rn = utils.Max(rsn, rbn)
gn = utils.Max(gsn, gbn)
bn = utils.Max(bsn, bbn)
an = utils.Max(asn, abn)
case Screen:
rn = 1 - (1-rsn)*(1-rbn)
gn = 1 - (1-gsn)*(1-gbn)
bn = 1 - (1-bsn)*(1-bbn)
an = 1 - (1-asn)*(1-abn)
case Multiply:
rn = rsn * rbn
gn = gsn * gbn
bn = bsn * bbn
an = asn * abn
case Overlay:
if rsn <= 0.5 {
rn = 2 * rsn * rbn
} else {
rn = 1 - 2*(1-rsn)*(1-rbn)
}
if gsn <= 0.5 {
gn = 2 * gsn * gbn
} else {
gn = 1 - 2*(1-gsn)*(1-gbn)
}
if bsn <= 0.5 {
bn = 2 * bsn * bbn
} else {
bn = 1 - 2*(1-bsn)*(1-bbn)
}
if asn <= 0.5 {
an = 2 * asn * abn
} else {
an = 1 - 2*(1-asn)*(1-abn)
}
case SoftLight:
if rbn < 0.5 {
rn = rsn - (1-2*rbn)*rsn*(1-rsn)
} else {
var w3r float64
if rsn < 0.25 {
w3r = ((16*rsn-12)*rsn + 4) * rsn
} else {
w3r = math.Sqrt(rsn)
}
rn = rsn + (2*rbn-1)*(w3r-rsn)
}
if gbn < 0.5 {
gn = gsn - (1-2*gbn)*gsn*(1-gsn)
} else {
var w3g float64
if gsn < 0.25 {
w3g = ((16*gsn-12)*gsn + 4) * gsn
} else {
w3g = math.Sqrt(gsn)
}
gn = gsn + (2*gbn-1)*(w3g-gsn)
}
if bbn < 0.5 {
bn = bsn - (1-2*bbn)*bsn*(1-bsn)
} else {
var w3b float64
if bsn < 0.25 {
w3b = ((16*bsn-12)*bsn + 4) * bsn
} else {
w3b = math.Sqrt(bsn)
}
bn = bsn + (2*bbn-1)*(w3b-bsn)
}
if abn < 0.5 {
an = asn - (1-2*abn)*asn*(1-asn)
} else {
var w3a float64
if asn < 0.25 {
w3a = ((16*asn-12)*asn + 4) * asn
} else {
w3a = math.Sqrt(asn)
}
an = asn + (2*abn-1)*(w3a-asn)
}
case HardLight:
if rbn < 0.5 {
rn = rbn - (1-2*rsn)*rbn*(1-rbn)
} else {
var w3r float64
if rbn < 0.25 {
w3r = ((16*rbn-12)*rbn + 4) * rbn
} else {
w3r = math.Sqrt(rbn)
}
rn = rbn + (2*rsn-1)*(w3r-rbn)
}
if gbn < 0.5 {
gn = gbn - (1-2*gsn)*gbn*(1-gbn)
} else {
var w3g float64
if gbn < 0.25 {
w3g = ((16*gbn-12)*gbn + 4) * gbn
} else {
w3g = math.Sqrt(gbn)
}
gn = gbn + (2*gsn-1)*(w3g-gbn)
}
if bbn < 0.5 {
bn = bbn - (1-2*bsn)*bbn*(1-bbn)
} else {
var w3b float64
if bbn < 0.25 {
w3b = ((16*bbn-12)*bbn + 4) * bbn
} else {
w3b = math.Sqrt(bbn)
}
bn = bbn + (2*bsn-1)*(w3b-bbn)
}
if abn < 0.5 {
an = abn - (1-2*asn)*abn*(1-abn)
} else {
var w3a float64
if abn < 0.25 {
w3a = ((16*abn-12)*abn + 4) * abn
} else {
w3a = math.Sqrt(abn)
}
an = abn + (2*asn-1)*(w3a-abn)
}
case ColorDodge:
if rsn < 1 {
rn = utils.Min(1, rbn/(1-rsn))
} else if rsn == 1 {
rn = 1
}
if gsn < 1 {
gn = utils.Min(1, gbn/(1-gsn))
} else if gsn == 1 {
gn = 1
}
if bsn < 1 {
bn = utils.Min(1, bbn/(1-bsn))
} else if bsn == 1 {
bn = 1
}
if asn < 1 {
an = utils.Min(1, abn/(1-asn))
} else if asn == 1 {
an = 1
}
case ColorBurn:
if rsn > 0 {
rn = 1 - utils.Min(1, (1-rbn)/rsn)
} else if rsn == 0 {
rn = 0
}
if gsn > 0 {
gn = 1 - utils.Min(1, (1-gbn)/gsn)
} else if gsn == 0 {
gn = 0
}
if bsn > 0 {
bn = 1 - utils.Min(1, (1-bbn)/bsn)
} else if bsn == 0 {
bn = 0
}
if asn > 0 {
an = 1 - utils.Min(1, (1-abn)/asn)
} else if asn == 0 {
an = 0
}
case Difference:
rn = utils.Abs(rbn - rsn)
gn = utils.Abs(gbn - gsn)
bn = utils.Abs(bbn - bsn)
an = 1
case Exclusion:
rn = rsn + rbn - 2*rsn*rbn
gn = gsn + gbn - 2*gsn*gbn
bn = bsn + bbn - 2*bsn*bbn
an = 1
// Non-separable blend modes
// https://www.w3.org/TR/compositing-1/#blendingnonseparable
case Hue:
sat := blend.SetSat(background, blend.Sat(foreground))
rgb := blend.SetLum(sat, blend.Lum(foreground))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
case Saturation:
sat := blend.SetSat(foreground, blend.Sat(background))
rgb := blend.SetLum(sat, blend.Lum(foreground))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
case ColorMode:
rgb := blend.SetLum(background, blend.Lum(foreground))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
case Luminosity:
rgb := blend.SetLum(foreground, blend.Lum(background))
a := asn + abn - asn*abn
rn = blend.AlphaCompose(abn, asn, a, rbn*255, rsn*255, rgb.R*255)
gn = blend.AlphaCompose(abn, asn, a, gbn*255, gsn*255, rgb.G*255)
bn = blend.AlphaCompose(abn, asn, a, bbn*255, bsn*255, rgb.B*255)
rn, gn, bn = rn/255, gn/255, bn/255
an = a
}
r = uint32(rn * 255)
g = uint32(gn * 255)
b = uint32(bn * 255)
a = uint32(an * 255)
bitmap.Img.Set(x, y, color.NRGBA{
R: uint8(r),
G: uint8(g),
B: uint8(b),
A: uint8(a),
})
}
}
}
}