Skip to content

Commit

Permalink
Fixes for v2
Browse files Browse the repository at this point in the history
  • Loading branch information
makew0rld committed Feb 13, 2021
1 parent 02b2882 commit 3714c39
Show file tree
Hide file tree
Showing 51 changed files with 90 additions and 74 deletions.
9 changes: 8 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,16 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
## [2.0.0] - 2021-02-13
### Added
- Added `ErrorDiffusionStrength` to set the strength of error diffusion dithering (#4)
- `RoundClamp` function for making your own `PixelMappers` that round correctly

### Changed
- All linear RGB values are represented using `uint16` instead of `uint8` now, because 8-bits is not enough to accurately hold a linearized value. This is a breaking change, hence the new major version.

### Fixed
- Rounding is no longer biased, because ties are rounded to the nearest even number


## [1.0.0] - 2021-02-11
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,12 @@ More methods of dithering are being worked on, such as Riemersma, Yuliluoma, and
In your project, run

```
go get github.com/makeworld-the-better-one/dither@latest
go get github.com/makeworld-the-better-one/dither/v2@latest
go mod tidy
```

You can import it as `"github.com/makeworld-the-better-one/dither/v2"` and use it as `dither`.

## Usage

Here's a simple example using Floyd-Steinberg dithering.
Expand Down Expand Up @@ -153,8 +155,6 @@ you should definitely be using those functions over the color ones.

All the `[][]uint` matrices are supposed to be applied with `PixelMapperFromMatrix`.

If you ever find yourself multiplying a number by `255.0` (for example to scale a float in the range [0, 1]), make sure to add 0.5 before returning. Only by doing this can you get a correct answer in all cases. For example, `255.0 * n` should become `255.0 * n + 0.5`.


## Projects using `dither`

Expand Down
32 changes: 16 additions & 16 deletions color_spaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,43 +7,43 @@ import (

// linearize1 linearizes an R, G, or B channel value from an sRGB color.
// Must be in the range [0, 1].
func linearize1(v float32) float32 {
func linearize1(v float64) float64 {
if v <= 0.04045 {
return v / 12.92
}
return float32(math.Pow((float64(v)+0.055)/1.055, 2.4))
return math.Pow((v+0.055)/1.055, 2.4)
}

func linearize65535to255(i uint16) uint8 {
v := float32(i) / 65535.0
return uint8(linearize1(v)*255.0 + 0.5)
func linearize65535(i uint16) uint16 {
v := float64(i) / 65535.0
return uint16(math.RoundToEven(linearize1(v) * 65535.0))
}

func linearize255(i uint8) uint8 {
v := float32(i) / 255.0
return uint8(linearize1(v)*255.0 + 0.5)
func linearize255to65535(i uint8) uint16 {
v := float64(i) / 255.0
return uint16(math.RoundToEven(linearize1(v) * 65535.0))
}

// toLinearRGB converts a non-linear sRGB color to a linear RGB color space.
func toLinearRGB(c color.Color) (uint8, uint8, uint8) {
func toLinearRGB(c color.Color) (uint16, uint16, uint16) {
// Optimize for different color types
switch v := c.(type) {
case color.Gray:
g := linearize255(v.Y)
g := linearize255to65535(v.Y)
return g, g, g
case color.Gray16:
g := linearize65535to255(v.Y)
g := linearize65535(v.Y)
return g, g, g
case color.NRGBA:
return linearize255(v.R), linearize255(v.G), linearize255(v.B)
return linearize255to65535(v.R), linearize255to65535(v.G), linearize255to65535(v.B)
case color.NRGBA64:
return linearize65535to255(v.R), linearize65535to255(v.G), linearize65535to255(v.B)
return linearize65535(v.R), linearize65535(v.G), linearize65535(v.B)
case color.RGBA:
return linearize255(v.R), linearize255(v.G), linearize255(v.B)
return linearize255to65535(v.R), linearize255to65535(v.G), linearize255to65535(v.B)
case color.RGBA64:
return linearize65535to255(v.R), linearize65535to255(v.G), linearize65535to255(v.B)
return linearize65535(v.R), linearize65535(v.G), linearize65535(v.B)
}

r, g, b, _ := c.RGBA()
return linearize65535to255(uint16(r)), linearize65535to255(uint16(g)), linearize65535to255(uint16(b))
return linearize65535(uint16(r)), linearize65535(uint16(g)), linearize65535(uint16(b))
}
51 changes: 27 additions & 24 deletions dither.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ type Ditherer struct {
palette []color.Color

// linearPalette holds all the palette colors, but in linear RGB space.
linearPalette [][3]uint8
linearPalette [][3]uint16
}

// NewDitherer creates a new Ditherer that uses a copy of the provided palette.
Expand All @@ -70,10 +70,10 @@ func NewDitherer(palette []color.Color) *Ditherer {
copy(d.palette, palette)

// Create linear RGB version of the palette
d.linearPalette = make([][3]uint8, len(d.palette))
d.linearPalette = make([][3]uint16, len(d.palette))
for i := range d.linearPalette {
r, g, b := toLinearRGB(d.palette[i])
d.linearPalette[i] = [3]uint8{r, g, b}
d.linearPalette[i] = [3]uint16{r, g, b}
}

return d
Expand Down Expand Up @@ -104,20 +104,20 @@ func (d *Ditherer) GetPalette() []color.Color {
return p
}

func sqDiff(v1 uint8, v2 uint8) uint16 {
func sqDiff(v1 uint16, v2 uint16) uint32 {
// This optimization is copied from Go stdlib, see
// https://github.com/golang/go/blob/go1.15.7/src/image/color/color.go#L314

d := uint16(v1) - uint16(v2)
d := uint32(v1) - uint32(v2)
return (d * d) >> 2
}

// closestColor returns the index of the color in the palette that's closest to
// the provided one, using Euclidean distance in linear RGB space. The provided
// RGB values must be linear RGB.
func (d *Ditherer) closestColor(r, g, b uint8) int {
func (d *Ditherer) closestColor(r, g, b uint16) int {
// Go through each color and find the closest one
color, best := 0, uint16(math.MaxUint16)
color, best := 0, uint32(math.MaxUint32)
for i, c := range d.linearPalette {
// Euclidean distance, but the square root part is removed
dist := sqDiff(r, c[0]) + sqDiff(g, c[1]) + sqDiff(b, c[2])
Expand Down Expand Up @@ -189,16 +189,16 @@ func (d *Ditherer) Dither(src image.Image) image.Image {
// Store linear values here instead of converting back and forth and storing
// sRGB values inside the image.
// Pointers are used to differentiate between a zero value and an unset value
lins := make([][][3]*uint8, b.Dy())
lins := make([][][3]*uint16, b.Dy())
for i := 0; i < len(lins); i++ {
lins[i] = make([][3]*uint8, b.Dx())
lins[i] = make([][3]*uint16, b.Dx())
}

// Setters and getters for that linear storage
linearSet := func(x, y int, r, g, b uint8) {
lins[y][x] = [3]*uint8{&r, &g, &b}
linearSet := func(x, y int, r, g, b uint16) {
lins[y][x] = [3]*uint16{&r, &g, &b}
}
linearAt := func(x, y int) (uint8, uint8, uint8) {
linearAt := func(x, y int) (uint16, uint16, uint16) {
c := lins[y][x]
if c[0] == nil {
// This pixel hasn't been linearized yet
Expand All @@ -225,11 +225,16 @@ func (d *Ditherer) Dither(src image.Image) image.Image {

new := d.linearPalette[newColorIdx]
// Quant errors in each channel
er, eg, eb := int16(oldR)-int16(new[0]), int16(oldG)-int16(new[1]), int16(oldB)-int16(new[2])
er, eg, eb := int32(oldR)-int32(new[0]), int32(oldG)-int32(new[1]), int32(oldB)-int32(new[2])

// Diffuse error in two dimensions
for yy := range d.Matrix {
for xx := range d.Matrix[yy] {
if d.Matrix[yy][xx] == 0 {
// Skip, because it won't affect anything
continue
}

// Get the coords of the pixel the error is being applied to
deltaX, deltaY := d.Matrix.Offset(xx, yy, curPx)
if d.Serpentine && y%2 == 0 {
Expand All @@ -247,12 +252,9 @@ func (d *Ditherer) Dither(src image.Image) image.Image {

r, g, b := linearAt(pxX, pxY)
linearSet(pxX, pxY,
// +0.5 is important to prevent pure white from being quantized to black due
// to the addition of error being rounded. This can be seen if you remove
// the +0.5 and test Floyd-Steinberg dithering on the gradient.png image.
clamp(float32(r)+float32(er)*d.Matrix[yy][xx]+0.5),
clamp(float32(g)+float32(eg)*d.Matrix[yy][xx]+0.5),
clamp(float32(b)+float32(eb)*d.Matrix[yy][xx]+0.5),
RoundClamp(float32(r)+float32(er)*d.Matrix[yy][xx]),
RoundClamp(float32(g)+float32(eg)*d.Matrix[yy][xx]),
RoundClamp(float32(b)+float32(eb)*d.Matrix[yy][xx]),
)
}
}
Expand Down Expand Up @@ -328,15 +330,16 @@ func (d *Ditherer) DitherPalettedConfig(src image.Image) (*image.Paletted, image
}
}

// clamp clamps i to the interval [0, 255].
func clamp(i float32) uint8 {
// RoundClamp clamps the number and rounds it, rounding ties to the nearest even number.
// This should be used if you're writing your own PixelMapper.
func RoundClamp(i float32) uint16 {
if i < 0 {
return 0
}
if i > 255 {
return 255
if i > 65535 {
return 65535
}
return uint8(i)
return uint16(math.RoundToEven(float64(i)))
}

// copyImage copies src's pixels into dst.
Expand Down
2 changes: 1 addition & 1 deletion examples/gif_animation.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
_ "image/png" // For frame decoding
"os"

"github.com/makeworld-the-better-one/dither"
"github.com/makeworld-the-better-one/dither/v2"
)

const numFrames = 20
Expand Down
2 changes: 1 addition & 1 deletion examples/gif_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
_ "image/png" // Imported for decoding of the input image
"os"

"github.com/makeworld-the-better-one/dither"
"github.com/makeworld-the-better-one/dither/v2"
)

func main() {
Expand Down
Binary file modified examples/output/gif_animation.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified examples/output/gif_image.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/makeworld-the-better-one/dither
module github.com/makeworld-the-better-one/dither/v2

go 1.15

Expand Down
Binary file modified images/output/ClusteredDot4x4.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDot6x6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDot6x6_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDot6x6_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDot8x8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotDiagonal16x16_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotDiagonal6x6.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotDiagonal8x8.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotDiagonal8x8_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotDiagonal8x8_3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotHorizontalLine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotSpiral5x5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/ClusteredDotVerticalLine.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/Horizontal3x5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/Vertical5x3.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_16x16_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_16x16_red-green-black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_16x16_red-green-yellow-black.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_16x8_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_2x2_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_4x4_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/bayer_8x8_gradient.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/edm_atkinson.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified images/output/edm_floyd-steinberg.png
Binary file modified images/output/edm_floyd-steinberg_serpentine.png
Binary file modified images/output/edm_floyd-steinberg_strength_02.png
Binary file modified images/output/edm_jarvis-judice-ninke.png
Binary file modified images/output/edm_peppers_atkinson_red-green-black.png
Binary file modified images/output/edm_peppers_atkinson_red-green-yellow-black.png
Binary file modified images/output/edm_peppers_floyd-steinberg_red-green-black.png
Binary file modified images/output/edm_peppers_simpled2d_red-green-black.png
Binary file modified images/output/edm_peppers_simpled2d_red-green-yellow-black.png
Binary file modified images/output/edm_simple2d.png
Binary file modified images/output/edm_simple2d_serpentine.png
Binary file modified images/output/random_noise_grayscale.png
Binary file modified images/output/random_noise_rgb_red-green-black.png
Binary file modified images/output/random_noise_rgb_red-green-yellow-black.png
5 changes: 3 additions & 2 deletions ordered_ditherers.go
Original file line number Diff line number Diff line change
Expand Up @@ -219,11 +219,12 @@ var ClusteredDotVerticalLine = OrderedDitherMatrix{
var ClusteredDot8x8 = OrderedDitherMatrix{
Matrix: [][]uint{
// For some reason the values in the book were in the range of 0-64
// instead of 0-63.
// instead of 0-63. I changed the 64 value to 63, so that pure black
// didn't end up with occasional white dots.
{3, 9, 17, 27, 25, 15, 7, 1},
{11, 29, 38, 46, 44, 36, 23, 5},
{19, 40, 52, 58, 56, 50, 34, 13},
{31, 48, 60, 64, 62, 54, 42, 21},
{31, 48, 60, 63, 62, 54, 42, 21},
{30, 47, 59, 63, 61, 53, 41, 20},
{18, 39, 51, 57, 55, 49, 33, 12},
{10, 28, 37, 45, 43, 35, 22, 4},
Expand Down
55 changes: 30 additions & 25 deletions pixelmappers.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,11 @@ import (
//
// The provided RGB values are in the linear RGB space, and the returned values
// must be as well. All dithering operations should be happening in this space
// anyway, so this is done as a convenience.
// anyway, so this is done as a convenience. The RGB values are in the range
// [0, 65535], and must be returned in the same range.
//
// It must be thread-safe, as it will be called concurrently.
type PixelMapper func(x, y int, r, g, b uint8) (uint8, uint8, uint8)
type PixelMapper func(x, y int, r, g, b uint16) (uint16, uint16, uint16)

// RandomNoiseGrayscale returns a PixelMapper that adds random noise to the
// color before returning. This is the simplest form of dithering.
Expand Down Expand Up @@ -49,17 +50,17 @@ type PixelMapper func(x, y int, r, g, b uint8) (uint8, uint8, uint8)
// not wrapped. Basically, don't worry about the values of your min and max
// distorting the image in an unexpected way.
func RandomNoiseGrayscale(min, max float32) PixelMapper {
return PixelMapper(func(x, y int, r, g, b uint8) (uint8, uint8, uint8) {
return PixelMapper(func(x, y int, r, g, b uint16) (uint16, uint16, uint16) {
// These values were taken from Wikipedia:
// https://en.wikipedia.org/wiki/Grayscale#Colorimetric_(perceptual_luminance-preserving)_conversion_to_grayscale
// 0.2126, 0.7152, 0.0722
// Then multiplied by 255, to scale them for uint8 color.
// Note that 54 + 183 + 19 = 256.
// Then multiplied by 65535, to scale them for 16-bit color.
// Note that 13933 + 46871 + 4732 = 65536
//
// Basically, this takes linear RGB and gives a linear gray.
gray := (54*uint16(r) + 183*uint16(g) + 19*uint16(b) + 1<<7) >> 8
gray := (13933*uint32(r) + 46871*uint32(g) + 4732*uint32(b) + 1<<15) >> 16

new := clamp(float32(gray) + 255.0*(rand.Float32()*(max-min)+min) + 0.5)
new := RoundClamp(float32(gray) + 65535.0*(rand.Float32()*(max-min)+min))
return new, new, new
})
}
Expand All @@ -74,10 +75,10 @@ func RandomNoiseGrayscale(min, max float32) PixelMapper {
// See RandomNoiseGrayscale for more details about values and how this function
// works.
func RandomNoiseRGB(minR, maxR, minG, maxG, minB, maxB float32) PixelMapper {
return PixelMapper(func(x, y int, r, g, b uint8) (uint8, uint8, uint8) {
return clamp(float32(r) + 255.0*(rand.Float32()*(maxR-minR)+minR) + 0.5),
clamp(float32(g) + 255.0*(rand.Float32()*(maxG-minG)+minG) + 0.5),
clamp(float32(b) + 255.0*(rand.Float32()*(maxB-minB)+minB) + 0.5)
return PixelMapper(func(x, y int, r, g, b uint16) (uint16, uint16, uint16) {
return RoundClamp(float32(r) + 65535.0*(rand.Float32()*(maxR-minR)+minR)),
RoundClamp(float32(g) + 65535.0*(rand.Float32()*(maxG-minG)+minG)),
RoundClamp(float32(b) + 65535.0*(rand.Float32()*(maxB-minB)+minB))
})
}

Expand Down Expand Up @@ -165,14 +166,18 @@ func bayerMatrix(xdim, ydim uint) [][]uint {
// and returns a value that can be added to a color instead of thresholded.
//
// scale is the number that's multiplied at the end, usually you want this to be
// 255 to scale to match the color value range. value is the cell of the matrix.
// 65535 to scale to match the color value range. value is the cell of the matrix.
// max is the divisor of the cell value, usually this is the product of the matrix
// dimensions.
func convThresholdToAddition(scale float32, value uint, max uint) float32 {
// See:
// https://en.wikipedia.org/wiki/Ordered_dithering
// https://en.wikipedia.org/wiki/Talk:Ordered_dithering#Sources
return scale * (float32(value+1.0)/float32(max) - 0.5)

// 0.50000006 is next possible float32 value after 0.5. This is to correct
// a rounding error that occurs when the number is exactly 0.5, which results
// in pure black being dithered when it should be left alone.
return scale * (float32(value+1.0)/float32(max) - 0.50000006)
}

// Bayer returns a PixelMapper that applies a Bayer matrix with the specified size.
Expand All @@ -191,8 +196,8 @@ func convThresholdToAddition(scale float32, value uint, max uint) float32 {
// Source:
// https://bisqwit.iki.fi/story/howto/dither/jy/#Appendix%202ThresholdMatrix
//
// strength should be in the range [-1, 1]. It is multiplied with 255, which is
// then multiplied with the matrix.
// strength should be in the range [-1, 1]. It is multiplied with 65535
// (the max color value), which is then multiplied with the matrix.
//
// You can use this to change the amount the matrix is applied to the image, the
// "strength" of the dithering matrix. Usually just keeping it at 1.0 is fine.
Expand Down Expand Up @@ -281,7 +286,7 @@ func Bayer(x, y uint, strength float32) PixelMapper {
}

// Create precalculated matrix
scale := 255.0 * strength
scale := 65535.0 * strength
max := x * y

precalc := make([][]float32, y)
Expand All @@ -292,10 +297,10 @@ func Bayer(x, y uint, strength float32) PixelMapper {
}
}

return PixelMapper(func(xx, yy int, r, g, b uint8) (uint8, uint8, uint8) {
return clamp(float32(r) + precalc[yy%int(y)][xx%int(x)]),
clamp(float32(g) + precalc[yy%int(y)][xx%int(x)]),
clamp(float32(b) + precalc[yy%int(y)][xx%int(x)])
return PixelMapper(func(xx, yy int, r, g, b uint16) (uint16, uint16, uint16) {
return RoundClamp(float32(r) + precalc[yy%int(y)][xx%int(x)]),
RoundClamp(float32(g) + precalc[yy%int(y)][xx%int(x)]),
RoundClamp(float32(b) + precalc[yy%int(y)][xx%int(x)])
})
}

Expand All @@ -313,7 +318,7 @@ func Bayer(x, y uint, strength float32) PixelMapper {
func PixelMapperFromMatrix(odm OrderedDitherMatrix, strength float32) PixelMapper {
ydim := len(odm.Matrix)
xdim := len(odm.Matrix[0])
scale := 255.0 * strength
scale := 65535.0 * strength

// Create precalculated matrix
precalc := make([][]float32, ydim)
Expand All @@ -324,9 +329,9 @@ func PixelMapperFromMatrix(odm OrderedDitherMatrix, strength float32) PixelMappe
}
}

return PixelMapper(func(xx, yy int, r, g, b uint8) (uint8, uint8, uint8) {
return clamp(float32(r) + precalc[yy%ydim][xx%xdim]),
clamp(float32(g) + precalc[yy%ydim][xx%xdim]),
clamp(float32(b) + precalc[yy%ydim][xx%xdim])
return PixelMapper(func(xx, yy int, r, g, b uint16) (uint16, uint16, uint16) {
return RoundClamp(float32(r) + precalc[yy%ydim][xx%xdim]),
RoundClamp(float32(g) + precalc[yy%ydim][xx%xdim]),
RoundClamp(float32(b) + precalc[yy%ydim][xx%xdim])
})
}

0 comments on commit 3714c39

Please sign in to comment.