diff --git a/backends/opengl/canvas.go b/backends/opengl/canvas.go index 34d452c..9a5ca7e 100644 --- a/backends/opengl/canvas.go +++ b/backends/opengl/canvas.go @@ -193,24 +193,20 @@ func setBlendFunc(cmp pixel.ComposeMethod) { func (c *Canvas) Clear(color color.Color) { c.gf.Dirty() - rgba := pixel.ToRGBA(color) - - // color masking - rgba = rgba.Mul(pixel.RGBA{ - R: float64(c.col[0]), - G: float64(c.col[1]), - B: float64(c.col[2]), - A: float64(c.col[3]), - }) + r, g, b, a := pixel.ColorToFloats[float32]( + pixel.ToRGBA(color).Mul( + pixel.FloatsToColor(c.col[0], c.col[1], c.col[2], c.col[3]), + ), + ) mainthread.CallNonBlock(func() { c.setGlhfBounds() c.gf.Frame().Begin() glhf.Clear( - float32(rgba.R), - float32(rgba.G), - float32(rgba.B), - float32(rgba.A), + r, + g, + b, + a, ) c.gf.Frame().End() }) diff --git a/backends/opengl/color.go b/backends/opengl/color.go new file mode 100644 index 0000000..22dde74 --- /dev/null +++ b/backends/opengl/color.go @@ -0,0 +1,26 @@ +package opengl + +import ( + "image/color" + + "github.com/go-gl/mathgl/mgl32" +) + +type GLColor mgl32.Vec4 + +func (c GLColor) RGBA() (r, g, b, a uint32) { + return uint32(c[0] * 0xffff), uint32(c[1] * 0xffff), uint32(c[2] * 0xffff), uint32(c[3] * 0xffff) +} + +func ToGLColor(col color.Color) GLColor { + if c, ok := col.(GLColor); ok { + return c + } + r, g, b, a := col.RGBA() + return GLColor{ + float32(r) / 0xffff, + float32(g) / 0xffff, + float32(b) / 0xffff, + float32(a) / 0xffff, + } +} diff --git a/backends/opengl/glframe.go b/backends/opengl/glframe.go index f943ea1..6b80dc1 100644 --- a/backends/opengl/glframe.go +++ b/backends/opengl/glframe.go @@ -79,10 +79,10 @@ func (gf *GLFrame) Color(at pixel.Vec) pixel.RGBA { x, y := int(at.X)-bx, int(at.Y)-by off := y*bw + x return pixel.RGBA{ - R: float64(gf.pixels[off*4+0]) / 255, - G: float64(gf.pixels[off*4+1]) / 255, - B: float64(gf.pixels[off*4+2]) / 255, - A: float64(gf.pixels[off*4+3]) / 255, + R: gf.pixels[off*4+0], + G: gf.pixels[off*4+1], + B: gf.pixels[off*4+2], + A: gf.pixels[off*4+3], } } diff --git a/backends/opengl/glpicture.go b/backends/opengl/glpicture.go index 3f16504..4065702 100644 --- a/backends/opengl/glpicture.go +++ b/backends/opengl/glpicture.go @@ -90,9 +90,9 @@ func (gp *glPicture) Color(at pixel.Vec) pixel.RGBA { x, y := int(at.X)-bx, int(at.Y)-by off := y*bw + x return pixel.RGBA{ - R: float64(gp.pixels[off*4+0]) / 255, - G: float64(gp.pixels[off*4+1]) / 255, - B: float64(gp.pixels[off*4+2]) / 255, - A: float64(gp.pixels[off*4+3]) / 255, + R: gp.pixels[off*4+0], + G: gp.pixels[off*4+1], + B: gp.pixels[off*4+2], + A: gp.pixels[off*4+3], } } diff --git a/backends/opengl/gltriangles.go b/backends/opengl/gltriangles.go index 7220d4a..2375921 100644 --- a/backends/opengl/gltriangles.go +++ b/backends/opengl/gltriangles.go @@ -127,19 +127,20 @@ func (gt *GLTriangles) updateData(t pixel.Triangles) { if t, ok := t.(*pixel.TrianglesData); ok { for i := 0; i < length; i++ { var ( - px, py = (*t)[i].Position.XY() - col = (*t)[i].Color - tx, ty = (*t)[i].Picture.XY() - in = (*t)[i].Intensity - rec = (*t)[i].ClipRect + px, py = (*t)[i].Position.XY() + col = (*t)[i].Color + tx, ty = (*t)[i].Picture.XY() + in = (*t)[i].Intensity + rec = (*t)[i].ClipRect + r, g, b, a = pixel.ColorToFloats[float32](col) ) d := gt.data[i*stride : i*stride+trisAttrLen] d[triPosX] = float32(px) d[triPosY] = float32(py) - d[triColorR] = float32(col.R) - d[triColorG] = float32(col.G) - d[triColorB] = float32(col.B) - d[triColorA] = float32(col.A) + d[triColorR] = r + d[triColorG] = g + d[triColorB] = b + d[triColorA] = a d[triPicX] = float32(tx) d[triPicY] = float32(ty) d[triIntensity] = float32(in) @@ -251,12 +252,7 @@ func (gt *GLTriangles) Color(i int) pixel.RGBA { g := gt.data[gt.index(i, triColorG)] b := gt.data[gt.index(i, triColorB)] a := gt.data[gt.index(i, triColorA)] - return pixel.RGBA{ - R: float64(r), - G: float64(g), - B: float64(b), - A: float64(a), - } + return pixel.FloatsToColor(r, g, b, a) } // SetColor sets the color property of the i-th vertex. diff --git a/color.go b/color.go index 4b41af3..5611096 100644 --- a/color.go +++ b/color.go @@ -1,91 +1,97 @@ package pixel -import "image/color" +import ( + "image/color" + + "golang.org/x/exp/constraints" +) // RGBA represents an alpha-premultiplied RGBA color with components within range [0, 1]. // // The difference between color.RGBA is that the value range is [0, 1] and the values are floats. -type RGBA struct { - R, G, B, A float64 -} +type RGBA color.RGBA // RGB returns a fully opaque RGBA color with the given RGB values. // // A common way to construct a transparent color is to create one with RGB constructor, then // multiply it by a color obtained from the Alpha constructor. func RGB(r, g, b float64) RGBA { - return RGBA{r, g, b, 1} + return RGBA{R: uint8(r * 255), G: uint8(g * 255), B: uint8(b * 255), A: 255} } // Alpha returns a white RGBA color with the given alpha component. func Alpha(a float64) RGBA { - return RGBA{a, a, a, a} + A := uint8(a * 255) + return RGBA{A, A, A, A} } // Add adds color d to color c component-wise and returns the result (the components are not // clamped). -func (c RGBA) Add(d RGBA) RGBA { +func (c RGBA) Add(d color.Color) RGBA { + rgba := ToRGBA(d) return RGBA{ - R: c.R + d.R, - G: c.G + d.G, - B: c.B + d.B, - A: c.A + d.A, + R: c.R + rgba.R, + G: c.G + rgba.G, + B: c.B + rgba.B, + A: c.A + rgba.A, } } // Sub subtracts color d from color c component-wise and returns the result (the components // are not clamped). -func (c RGBA) Sub(d RGBA) RGBA { +func (c RGBA) Sub(d color.Color) RGBA { + rgba := ToRGBA(d) return RGBA{ - R: c.R - d.R, - G: c.G - d.G, - B: c.B - d.B, - A: c.A - d.A, + R: c.R - rgba.R, + G: c.G - rgba.G, + B: c.B - rgba.B, + A: c.A - rgba.A, } } // Mul multiplies color c by color d component-wise (the components are not clamped). -func (c RGBA) Mul(d RGBA) RGBA { - return RGBA{ - R: c.R * d.R, - G: c.G * d.G, - B: c.B * d.B, - A: c.A * d.A, - } +func (c RGBA) Mul(d color.Color) RGBA { + r1, g1, b1, a1 := ColorToFloats[float64](c) + r2, g2, b2, a2 := ColorToFloats[float64](d) + return FloatsToColor(r1*r2, g1*g2, b1*b2, a1*a2) } -// Scaled multiplies each component of color c by scale and returns the result (the components -// are not clamped). +// Scaled multiplies each component of color c by scale and returns the result. func (c RGBA) Scaled(scale float64) RGBA { + r, g, b, a := ColorToFloats[float64](c) return RGBA{ - R: c.R * scale, - G: c.G * scale, - B: c.B * scale, - A: c.A * scale, + R: uint8(r * scale), + G: uint8(g * scale), + B: uint8(b * scale), + A: uint8(a * scale), } } -// RGBA returns alpha-premultiplied red, green, blue and alpha components of the RGBA color. +// RGBA returns components of the color. func (c RGBA) RGBA() (r, g, b, a uint32) { - r = uint32(0xffff * c.R) - g = uint32(0xffff * c.G) - b = uint32(0xffff * c.B) - a = uint32(0xffff * c.A) - return + return color.RGBA(c).RGBA() } -// ToRGBA converts a color to RGBA format. Using this function is preferred to using RGBAModel, for -// performance (using RGBAModel introduces additional unnecessary allocations). +// ColorToFloats converts a color to float32 or float64 components. +func ColorToFloats[F constraints.Float](c color.Color) (r, g, b, a F) { + r1, g1, b1, a1 := c.RGBA() + return F(r1) / 0xffff, F(g1) / 0xffff, F(b1) / 0xffff, F(a1) / 0xffff +} + +// FloatsToColor converts float32 or float64 components to a color. +func FloatsToColor[F constraints.Float](r, g, b, a F) RGBA { + return RGBA{uint8(r * F(255)), uint8(g * F(255)), uint8(b * F(255)), uint8(a * F(255))} +} + +// ToRGBA converts a color to RGBA format. func ToRGBA(c color.Color) RGBA { - if c, ok := c.(RGBA); ok { + switch c := c.(type) { + case RGBA: return c - } - r, g, b, a := c.RGBA() - return RGBA{ - float64(r) / 0xffff, - float64(g) / 0xffff, - float64(b) / 0xffff, - float64(a) / 0xffff, + case color.RGBA: + return RGBA(c) + default: + return RGBA(color.RGBAModel.Convert(c).(color.RGBA)) } } diff --git a/color_test.go b/color_test.go new file mode 100644 index 0000000..d9b3040 --- /dev/null +++ b/color_test.go @@ -0,0 +1,54 @@ +package pixel + +import ( + "image/color" + "reflect" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestColorConversions(t *testing.T) { + c0 := RGBA{64, 127, 128, 255} + c1 := FloatsToColor(ColorToFloats[float32](c0)) + require.Equal(t, c0, c1) +} + +func TestToRGBA(t *testing.T) { + type args struct { + c color.Color + } + tests := []struct { + name string + args args + want RGBA + }{ + { + name: "pixel.rgba", + args: args{c: RGBA{64, 127, 128, 255}}, + want: RGBA{64, 127, 128, 255}, + }, + { + name: "color.rgba", + args: args{c: color.RGBA{64, 127, 128, 255}}, + want: RGBA{64, 127, 128, 255}, + }, + { + name: "color.nrgba", + args: args{c: color.NRGBA{64, 127, 128, 255}}, + want: RGBA{64, 127, 128, 255}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := ToRGBA(tt.args.c); !reflect.DeepEqual(got, tt.want) { + t.Errorf("ToRGBA() = %v, want %v", got, tt.want) + } + }) + } +} + +func TestRGB(t *testing.T) { + rgba := RGB(0.25, 0.5, 0.75) + require.Equal(t, RGBA{63, 127, 191, 255}, rgba) +} diff --git a/compose.go b/compose.go index a4a8783..9b1f630 100644 --- a/compose.go +++ b/compose.go @@ -1,6 +1,9 @@ package pixel -import "errors" +import ( + "errors" + "image/color" +) // ComposeTarget is a BasicTarget capable of Porter-Duff composition. type ComposeTarget interface { @@ -31,28 +34,34 @@ const ( // Compose composes two colors together according to the ComposeMethod. A is the foreground, B is // the background. -func (cm ComposeMethod) Compose(a, b RGBA) RGBA { +func (cm ComposeMethod) Compose(a, b color.Color) RGBA { var fa, fb float64 + ac := ToRGBA(a) + bc := ToRGBA(b) + + aa := float64(ac.A) / 255 + ba := float64(bc.A) / 255 + switch cm { case ComposeOver: - fa, fb = 1, 1-a.A + fa, fb = 1, 1-aa case ComposeIn: - fa, fb = b.A, 0 + fa, fb = ba, 0 case ComposeOut: - fa, fb = 1-b.A, 0 + fa, fb = 1-ba, 0 case ComposeAtop: - fa, fb = b.A, 1-a.A + fa, fb = ba, 1-aa case ComposeRover: - fa, fb = 1-b.A, 1 + fa, fb = 1-ba, 1 case ComposeRin: - fa, fb = 0, a.A + fa, fb = 0, aa case ComposeRout: - fa, fb = 0, 1-a.A + fa, fb = 0, 1-aa case ComposeRatop: - fa, fb = 1-b.A, a.A + fa, fb = 1-ba, aa case ComposeXor: - fa, fb = 1-b.A, 1-a.A + fa, fb = 1-ba, 1-aa case ComposePlus: fa, fb = 1, 1 case ComposeCopy: @@ -61,5 +70,5 @@ func (cm ComposeMethod) Compose(a, b RGBA) RGBA { panic(errors.New("Compose: invalid ComposeMethod")) } - return a.Mul(Alpha(fa)).Add(b.Mul(Alpha(fb))) + return ac.Mul(Alpha(fa)).Add(bc.Mul(Alpha(fb))) } diff --git a/ext/imdraw/imdraw.go b/ext/imdraw/imdraw.go index d216836..50e8e0b 100644 --- a/ext/imdraw/imdraw.go +++ b/ext/imdraw/imdraw.go @@ -128,13 +128,8 @@ func (imd *IMDraw) Draw(t pixel.Target) { // Push adds some points to the IM queue. All Pushed points will have the same properties except for // the position. func (imd *IMDraw) Push(pts ...pixel.Vec) { - // Assert that Color is of type pixel.RGBA, - if _, ok := imd.Color.(pixel.RGBA); !ok { - // otherwise cast it - imd.Color = pixel.ToRGBA(imd.Color) - } opts := point{ - col: imd.Color.(pixel.RGBA), + col: pixel.ToRGBA(imd.Color), pic: imd.Picture, in: imd.Intensity, precision: imd.Precision, diff --git a/go.mod b/go.mod index 1966237..6b76e11 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gopxl/mainthread/v2 v2.0.0 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.8.4 + golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa golang.org/x/image v0.13.0 ) diff --git a/go.sum b/go.sum index b86c224..d3832a9 100644 --- a/go.sum +++ b/go.sum @@ -18,6 +18,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa h1:ELnwvuAXPNtPk1TJRuGkI9fDTwym6AYBu0qzT8AcHdI= +golang.org/x/exp v0.0.0-20240808152545-0cdaa3abc0fa/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.13.0 h1:3cge/F/QTkNLauhf2QoE9zp+7sr+ZcL4HnoZmdwg9sg= golang.org/x/image v0.13.0/go.mod h1:6mmbMOeV28HuMTgA6OSRkdXKYw/t5W9Uwn2Yv1r3Yxk= diff --git a/sprite.go b/sprite.go index 7adc845..a7e2019 100644 --- a/sprite.go +++ b/sprite.go @@ -1,6 +1,8 @@ package pixel -import "image/color" +import ( + "image/color" +) // Sprite is a drawable frame of a Picture. It's anchored by the center of it's Picture's frame. //