// cb is backdrop colour, cs is source (blend layer) colour func BlendPixel(cb, cs color.Color, f Blender) color.Color { // Uses methods described in "PDF Reference, Third Edition" from Adobe // see: http://www.adobe.com/devnet/pdf/pdf_reference_archive.html // result colour cr := f(cb, cs) rb, gb, bb, ab := utils.RatioRGBA(cb) rs, gs, bs, as := utils.RatioRGBA(cs) rr, gr, br, _ := utils.RatioRGBA(cr) // Color compositing formula, expanded form. (Section 7.2.5) red := ((1 - as) * ab * rb) + ((1 - ab) * as * rs) + (ab * as * rr) green := ((1 - as) * ab * gb) + ((1 - ab) * as * gs) + (ab * as * gr) blue := ((1 - as) * ab * bb) + ((1 - ab) * as * bs) + (ab * as * br) // Union function. (Section 7.2.6) alpha := ab + as - (ab * as) return color.RGBA{ uint8(utils.Truncatef(red * 255)), uint8(utils.Truncatef(green * 255)), uint8(utils.Truncatef(blue * 255)), uint8(utils.Truncatef(alpha * 255)), } }
// Dissolve randomly selects pixels from the blend image, depending on their // opacity. Blend pixels with higher opacities are more likely to be displayed. func Dissolve(a, b image.Image) image.Image { ba := a.Bounds() bb := b.Bounds() width := int(utils.Min(uint32(ba.Dx()), uint32(bb.Dx()))) height := int(utils.Min(uint32(ba.Dy()), uint32(bb.Dy()))) result := image.NewRGBA(image.Rect(0, 0, width, height)) for y := 0; y < height; y++ { for x := 0; x < width; x++ { // base colour rb, gb, bb, ab := utils.RatioRGBA(a.At(x, y)) // blend colour rs, gs, bs, as := utils.RatioRGBA(b.At(x, y)) toPaint := ratioNRGBA(rb, gb, bb, ab) if rand.Float64() < as { toPaint = ratioNRGBA(rs, gs, bs, 1) } result.Set(x, y, toPaint) } } return result }
// Lighter chooses the lightest colour by comparing the sum of the colour // channels. func Lighter(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, _ := utils.RatioRGBA(c) m, n, o, _ := utils.RatioRGBA(d) if i+j+k > m+n+o { return c } return d }) }
// Dodge brightens the base colour to reflect the blend colour. func Dodge(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := i / (1 - m) g := j / (1 - n) b := k / (1 - o) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// Screen multiplies the complements of the base and blend colour channel // values, then complements the result. func Screen(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := 1 - ((1 - i) * (1 - m)) g := 1 - ((1 - j) * (1 - n)) b := 1 - ((1 - k) * (1 - o)) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// Lightne selects the lighter of each pixels' colour channels. func Lighten(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := utils.Maxf(i, m) g := utils.Maxf(j, n) b := utils.Maxf(k, o) a := utils.Maxf(l, p) return ratioNRGBA(r, g, b, a) }) }
// LinearBurn adds the values of each colour channel together, then subtracts // white to produce a darker image. func LinearBurn(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := i + m - 1 g := j + n - 1 b := k + o - 1 a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// Burn darkens the base colour to reflect the blend colour. func Burn(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := 1 - ((1 - i) / m) g := 1 - ((1 - j) / n) b := 1 - ((1 - k) / o) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// Multiply multiplies the base and blend image colour channels. func Multiply(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := i * m g := j * n b := k * o a := l * p return ratioNRGBA(r, g, b, a) }) }
// Exclusion creates an effect similar to, but lower in contrast than, // difference. func Exclusion(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := m + i - (2 * m * i) g := n + j - (2 * n * j) b := o + k - (2 * o * k) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// Difference finds the absolute difference between the base and blend colours. func Difference(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) r := math.Abs(m - i) g := math.Abs(n - j) b := math.Abs(o - k) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// BlendPixels takes the base and blend images and applies the given Blender to // each of their pixel pairs. func BlendPixels(a, b image.Image, f Blender) image.Image { ba := a.Bounds() bb := b.Bounds() width := int(utils.Min(uint32(ba.Dx()), uint32(bb.Dx()))) height := int(utils.Min(uint32(ba.Dy()), uint32(bb.Dy()))) result := image.NewRGBA(image.Rect(0, 0, width, height)) for y := 0; y < height; y++ { for x := 0; x < width; x++ { // Uses methods described in "PDF Reference, Third Edition" from Adobe // see: http://www.adobe.com/devnet/pdf/pdf_reference_archive.html // backdrop colour cb := a.At(x, y) // source colour cs := b.At(x, y) // result colour cr := f(cb, cs) rb, gb, bb, ab := utils.RatioRGBA(cb) rs, gs, bs, as := utils.RatioRGBA(cs) rr, gr, br, _ := utils.RatioRGBA(cr) // Color compositing formula, expanded form. (Section 7.2.5) red := ((1 - as) * ab * rb) + ((1 - ab) * as * rs) + (ab * as * rr) green := ((1 - as) * ab * gb) + ((1 - ab) * as * gs) + (ab * as * gr) blue := ((1 - as) * ab * bb) + ((1 - ab) * as * bs) + (ab * as * br) // Union function. (Section 7.2.6) alpha := ab + as - (ab * as) result.Set(x, y, color.RGBA{ uint8(utils.Truncatef(red * 255)), uint8(utils.Truncatef(green * 255)), uint8(utils.Truncatef(blue * 255)), uint8(utils.Truncatef(alpha * 255)), }) } } return result }
// HardMix adds the red, green and blue channel values of the blend colour to // the RGB values of the base colour. It sets any values greater than 255 to // 255, and anything less to 0. This therefore makes all pixels either red, // green, blue, white or black. func HardMix(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) f := func(i, j float64) float64 { if j < 1-i { return 0 } return 1 } r := f(i, m) g := f(j, n) b := f(k, o) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// LinearLight lightens or darkens the image by changing the brightness. If the // blend colour is lighter, the image is lightened; if the blend colour is // darker, the image is darkened. It uses linear burn and linear dodge to darken // or lighten. func LinearLight(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) f := func(i, j float64) float64 { if j > 0.5 { return i + 2*(j-0.5) } return i + 2*j - 1 } r := f(i, m) g := f(j, n) b := f(k, o) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
// VividLight combines Dodge and Burn. Dodge applies to lighter colours, and // Burn to darker. func VividLight(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) f := func(i, j float64) float64 { if j > 0.5 { return i / (2 * (1 - j)) } return 1 - (1-i)/(2*j) } r := f(i, m) g := f(j, n) b := f(k, o) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
func AdjustC(value float64) utils.Composable { return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) r = utils.Truncatef(math.Pow(r, 1/value) * 255) g = utils.Truncatef(math.Pow(g, 1/value) * 255) b = utils.Truncatef(math.Pow(b, 1/value) * 255) return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a * 255)} } }
// UnsharpMask sharpens the given Image using the unsharp mask technique. // Basically the image is blurred, then subtracted from the original for // differences above the threshold value. func UnsharpMask(in image.Image, radius int, sigma, amount, threshold float64) image.Image { blurred := blur.Gaussian(in, radius, sigma, blur.IGNORE) bounds := in.Bounds() out := image.NewRGBA(bounds) // Absolute difference between a and b, returns float64 between 0 and 1. diff := func(a, b float64) float64 { if a > b { return a - b } return b - a } for y := bounds.Min.Y; y < bounds.Max.Y; y++ { for x := bounds.Min.X; x < bounds.Max.X; x++ { ar, ag, ab, aa := utils.RatioRGBA(in.At(x, y)) br, bg, bb, _ := utils.RatioRGBA(blurred.At(x, y)) if diff(ar, br) >= threshold { ar = amount*(ar-br) + ar } if diff(ag, bg) >= threshold { ag = amount*(ag-bg) + ag } if diff(ab, bb) >= threshold { ab = amount*(ab-bb) + ab } out.Set(x, y, color.NRGBA{ uint8(utils.Truncatef(ar * 255)), uint8(utils.Truncatef(ag * 255)), uint8(utils.Truncatef(ab * 255)), uint8(aa * 255), }) } } return out }
func AlphaC(adj utils.Adjuster) utils.Composable { return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) a = adj(a) if a > 1 { a = 1 } else if a < 0 { a = 0 } return color.NRGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), uint8(a * 255)} } }
func BlueC(adj utils.Adjuster) utils.Composable { return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) b = adj(b) if b > 1 { b = 1 } else if b < 0 { b = 0 } return color.NRGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), uint8(a * 255)} } }
func GreenC(adj utils.Adjuster) utils.Composable { return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) g = adj(g) if g > 1 { g = 1 } else if g < 0 { g = 0 } return color.NRGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), uint8(a * 255)} } }
func RedC(adj utils.Adjuster) utils.Composable { return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) r = adj(r) if r > 1 { r = 1 } else if r < 0 { r = 0 } return color.NRGBA{uint8(r * 255), uint8(g * 255), uint8(b * 255), uint8(a * 255)} } }
func AdjustC(value float64) utils.Composable { return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) r = utils.Truncatef((((r - 0.5) * value) + 0.5) * 255) g = utils.Truncatef((((g - 0.5) * value) + 0.5) * 255) b = utils.Truncatef((((b - 0.5) * value) + 0.5) * 255) a = a * 255 return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} } }
// PinLight replaces the colours, depending on the blend colour. func PinLight(a, b image.Image) image.Image { return BlendPixels(a, b, func(c, d color.Color) color.Color { i, j, k, l := utils.RatioRGBA(c) m, n, o, p := utils.RatioRGBA(d) f := func(i, j float64) float64 { if i < 2*j-1 { return 2*j - 1 } else if i > 2*j { return 2 * j } return i } r := f(i, m) g := f(j, n) b := f(k, o) a := p + l*(1-p) return ratioNRGBA(r, g, b, a) }) }
func hsiaModel(c color.Color) color.Color { if _, ok := c.(HSIA); ok { return c } r, g, b, a := utils.RatioRGBA(c) maxi := utils.Maxf(r, g, b) mini := utils.Minf(r, g, b) chroma := maxi - mini // Work out hue hdash := 0.0 if chroma == 0 { hdash = 0 } else if maxi == r { hdash = math.Mod((g-b)/chroma, 6) } else if maxi == g { hdash = (b-r)/chroma + 2.0 } else if maxi == b { hdash = (r-g)/chroma + 4.0 } hue := hdash * 60 if chroma == 0 { hue = 0 } // Work out intensity intensity := (r + g + b) / 3 // Work out saturation saturation := 0.0 if chroma != 0 { saturation = 1 - mini/intensity } // prefer positive hues if hue < 0 { hue += 360 } return HSIA{hue, saturation, intensity, a} }
func hsvaModel(c color.Color) color.Color { if _, ok := c.(HSVA); ok { return c } r, g, b, a := utils.RatioRGBA(c) maxi := utils.Maxf(r, g, b) mini := utils.Minf(r, g, b) chroma := maxi - mini // Work out hue hdash := 0.0 if chroma == 0 { hdash = 0 } else if maxi == r { hdash = math.Mod((g-b)/chroma, 6) } else if maxi == g { hdash = (b-r)/chroma + 2.0 } else if maxi == b { hdash = (r-g)/chroma + 4.0 } hue := hdash * 60 if chroma == 0 { hue = 0 } // Work out value value := maxi // Work out saturation saturation := 0.0 if chroma != 0 { saturation = chroma / value } return HSVA{hue, saturation, value, a} }
func hslaModel(c color.Color) color.Color { if _, ok := c.(HSLA); ok { return c } r, g, b, a := utils.RatioRGBA(c) maxi := utils.Maxf(r, g, b) mini := utils.Minf(r, g, b) chroma := maxi - mini // Work out hue hdash := 0.0 if chroma == 0 { hdash = 0 } else if maxi == r { hdash = math.Mod((g-b)/chroma, 6) } else if maxi == g { hdash = (b-r)/chroma + 2.0 } else if maxi == b { hdash = (r-g)/chroma + 4.0 } hue := hdash * 60 if chroma == 0 { hue = 0 } // Work out lightness lightness := 0.5 * (maxi + mini) // Work out saturation saturation := 0.0 if chroma != 0 { saturation = chroma / (1 - math.Abs(2*lightness-1)) } return HSLA{hue, saturation, lightness, a} }
func SigmoidalC(factor, midpoint float64) utils.Composable { alpha := midpoint beta := factor f := func(u float64) float64 { return (1.0/(1.0+math.Exp(beta*(alpha-u))) - 1.0/(1.0+math.Exp(beta))) / (1.0/(1.0+math.Exp(beta*(alpha-1))) - 1.0/(1.0+math.Exp(beta*alpha))) } return func(c color.Color) color.Color { r, g, b, a := utils.RatioRGBA(c) r = utils.Truncatef(f(r) * 255) g = utils.Truncatef(f(g) * 255) b = utils.Truncatef(f(b) * 255) a = a * 255 return color.NRGBA{uint8(r), uint8(g), uint8(b), uint8(a)} } }
func getAlpha(c color.Color) float64 { _, _, _, a := utils.RatioRGBA(c) return a }
func getBlue(c color.Color) float64 { _, _, b, _ := utils.RatioRGBA(c) return b }
func getGreen(c color.Color) float64 { _, g, _, _ := utils.RatioRGBA(c) return g }