diff --git a/colorm/colorm.go b/colorm/colorm.go new file mode 100644 index 000000000..361a80765 --- /dev/null +++ b/colorm/colorm.go @@ -0,0 +1,80 @@ +// Copyright 2022 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorm + +import ( + "fmt" + "sync" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/internal/builtinshader" +) + +// Dim is a dimension of a ColorM. +const Dim = ebiten.ColorMDim + +// ColorM represents a matrix to transform coloring when rendering an image. +// +// ColorM is applied to the straight alpha color +// while an Image's pixels' format is alpha premultiplied. +// Before applying a matrix, a color is un-multiplied, and after applying the matrix, +// the color is multiplied again. +// +// The initial value is identity. +type ColorM = ebiten.ColorM + +func uniforms(c ColorM) map[string]interface{} { + var body [16]float32 + var translation [4]float32 + c.ReadElements(body[:], translation[:]) + + uniforms := map[string]interface{}{} + uniforms[builtinshader.UniformColorMBody] = body[:] + uniforms[builtinshader.UniformColorMTranslation] = translation[:] + return uniforms +} + +type builtinShaderKey struct { + filter builtinshader.Filter + address builtinshader.Address +} + +var ( + builtinShaders = map[builtinShaderKey]*ebiten.Shader{} + builtinShadersM sync.Mutex +) + +func builtinShader(filter builtinshader.Filter, address builtinshader.Address) *ebiten.Shader { + builtinShadersM.Lock() + defer builtinShadersM.Unlock() + + key := builtinShaderKey{ + filter: filter, + address: address, + } + if s, ok := builtinShaders[key]; ok { + return s + } + + src := builtinshader.Shader(filter, address, true) + s, err := ebiten.NewShader(src) + if err != nil { + panic(fmt.Sprintf("colorm: NewShader for a built-in shader failed: %v", err)) + } + shader := s + + builtinShaders[key] = shader + return shader +} diff --git a/colorm_test.go b/colorm/colorm_test.go similarity index 90% rename from colorm_test.go rename to colorm/colorm_test.go index 794fc4c67..d84f5eb42 100644 --- a/colorm_test.go +++ b/colorm/colorm_test.go @@ -12,20 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -package ebiten_test +package colorm_test import ( "image/color" "math" "testing" - "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" ) func TestColorMInit(t *testing.T) { - var m ebiten.ColorM - for i := 0; i < ebiten.ColorMDim-1; i++ { - for j := 0; j < ebiten.ColorMDim; j++ { + var m colorm.ColorM + for i := 0; i < colorm.Dim-1; i++ { + for j := 0; j < colorm.Dim; j++ { got := m.Element(i, j) want := 0.0 if i == j { @@ -38,8 +38,8 @@ func TestColorMInit(t *testing.T) { } m.SetElement(0, 0, 1) - for i := 0; i < ebiten.ColorMDim-1; i++ { - for j := 0; j < ebiten.ColorMDim; j++ { + for i := 0; i < colorm.Dim-1; i++ { + for j := 0; j < colorm.Dim; j++ { got := m.Element(i, j) want := 0.0 if i == j { @@ -53,7 +53,7 @@ func TestColorMInit(t *testing.T) { } func TestColorMAssign(t *testing.T) { - m := ebiten.ColorM{} + m := colorm.ColorM{} m.SetElement(0, 0, 1) m2 := m m.SetElement(0, 0, 0) @@ -71,7 +71,7 @@ func TestColorMTranslate(t *testing.T) { {0, 0, 1, 0, 2.5}, {0, 0, 0, 1, 3.5}, } - m := ebiten.ColorM{} + m := colorm.ColorM{} m.Translate(0.5, 1.5, 2.5, 3.5) for i := 0; i < 4; i++ { for j := 0; j < 5; j++ { @@ -91,7 +91,7 @@ func TestColorMScale(t *testing.T) { {0, 0, 2.5, 0, 0}, {0, 0, 0, 3.5, 0}, } - m := ebiten.ColorM{} + m := colorm.ColorM{} m.Scale(0.5, 1.5, 2.5, 3.5) for i := 0; i < 4; i++ { for j := 0; j < 5; j++ { @@ -111,7 +111,7 @@ func TestColorMTranslateAndScale(t *testing.T) { {0, 0, 1, 0, 0}, {0, 0, 0, 0.5, 0.5}, } - m := ebiten.ColorM{} + m := colorm.ColorM{} m.Translate(0, 0, 0, 1) m.Scale(1, 1, 1, 0.5) for i := 0; i < 4; i++ { @@ -132,7 +132,7 @@ func TestColorMMonochrome(t *testing.T) { {0.2990, 0.5870, 0.1140, 0, 0}, {0, 0, 0, 1, 0}, } - m := ebiten.ColorM{} + m := colorm.ColorM{} m.ChangeHSV(0, 0, 1) for i := 0; i < 4; i++ { for j := 0; j < 5; j++ { @@ -152,7 +152,7 @@ func TestColorMConcatSelf(t *testing.T) { {30, 43, 51, 39, 34}, {25, 37, 39, 46, 36}, } - m := ebiten.ColorM{} + m := colorm.ColorM{} for i := 0; i < 4; i++ { for j := 0; j < 5; j++ { m.SetElement(i, j, float64((i+j)%5+1)) @@ -178,23 +178,23 @@ func absDiffU32(x, y uint32) uint32 { } func TestColorMApply(t *testing.T) { - mono := ebiten.ColorM{} + mono := colorm.ColorM{} mono.ChangeHSV(0, 0, 1) - shiny := ebiten.ColorM{} + shiny := colorm.ColorM{} shiny.Translate(1, 1, 1, 0) - shift := ebiten.ColorM{} + shift := colorm.ColorM{} shift.Translate(0.5, 0.5, 0.5, 0.5) cases := []struct { - ColorM ebiten.ColorM + ColorM colorm.ColorM In color.Color Out color.Color Delta uint32 }{ { - ColorM: ebiten.ColorM{}, + ColorM: colorm.ColorM{}, In: color.RGBA{1, 2, 3, 4}, Out: color.RGBA{1, 2, 3, 4}, Delta: 0x101, @@ -237,7 +237,7 @@ func TestColorMApply(t *testing.T) { // #1765 func TestColorMConcat(t *testing.T) { - var a, b ebiten.ColorM + var a, b colorm.ColorM a.SetElement(1, 2, -1) a.Concat(b) if got, want := a.Element(1, 2), -1.0; got != want { diff --git a/colorm/draw.go b/colorm/draw.go new file mode 100644 index 000000000..aa60a105a --- /dev/null +++ b/colorm/draw.go @@ -0,0 +1,121 @@ +// Copyright 2022 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorm + +import ( + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/internal/builtinshader" +) + +// DrawImageOptions represents options for DrawImage. +type DrawImageOptions struct { + // GeoM is a geometry matrix to draw. + // The default (zero) value is identity, which draws the image at (0, 0). + GeoM ebiten.GeoM + + // Blend is a blending way of the source color and the destination color. + // The default (zero) value is the regular alpha blending. + Blend ebiten.Blend + + // Filter is a type of texture filter. + // The default (zero) value is ebiten.FilterNearest. + Filter ebiten.Filter +} + +// DrawImage draws src onto dst. +// +// DrawImage is basically the same as ebiten.DrawImage, but with a color matrix. +func DrawImage(dst, src *ebiten.Image, colorM ColorM, op *DrawImageOptions) { + if op == nil { + op = &DrawImageOptions{} + } + + w, h := src.Size() + opShader := &ebiten.DrawRectShaderOptions{} + opShader.GeoM = op.GeoM + opShader.CompositeMode = ebiten.CompositeModeCustom + opShader.Blend = op.Blend + opShader.Uniforms = uniforms(colorM) + opShader.Images[0] = src + s := builtinShader(builtinshader.Filter(op.Filter), builtinshader.AddressUnsafe) + dst.DrawRectShader(w, h, s, opShader) +} + +// DrawTrianglesOptions represents options for DrawTriangles. +type DrawTrianglesOptions struct { + // ColorScaleMode is the mode of color scales in vertices. + // The default (zero) value is ebiten.ColorScaleModeStraightAlpha. + ColorScaleMode ebiten.ColorScaleMode + + // Blend is a blending way of the source color and the destination color. + // The default (zero) value is the regular alpha blending. + Blend ebiten.Blend + + // Filter is a type of texture filter. + // The default (zero) value is ebiten.FilterNearest. + Filter ebiten.Filter + + // Address is a sampler address mode. + // The default (zero) value is ebiten.AddressUnsafe. + Address ebiten.Address + + // FillRule indicates the rule how an overlapped region is rendered. + // + // The rule EvenOdd is useful when you want to render a complex polygon. + // A complex polygon is a non-convex polygon like a concave polygon, a polygon with holes, or a self-intersecting polygon. + // See examples/vector for actual usages. + // + // The default (zero) value is ebiten.FillAll. + FillRule ebiten.FillRule + + // AntiAlias indicates whether the rendering uses anti-alias or not. + // AntiAlias is useful especially when you pass vertices from the vector package. + // + // AntiAlias increases internal draw calls and might affect performance. + // Use the build tag `ebitenginedebug` to check the number of draw calls if you care. + // + // The default (zero) value is false. + AntiAlias bool +} + +// DrawTriangles draws triangles onto dst. +// +// DrawTriangles is basically the same as ebiten.DrawTriangles, but with a color matrix. +func DrawTriangles(dst *ebiten.Image, vertices []ebiten.Vertex, indices []uint16, img *ebiten.Image, colorM ColorM, op *DrawTrianglesOptions) { + if op == nil { + op = &DrawTrianglesOptions{} + } + + if op.ColorScaleMode == ebiten.ColorScaleModeStraightAlpha { + vs := make([]ebiten.Vertex, len(vertices)) + copy(vs, vertices) + for i := range vertices { + vs[i].ColorR *= vs[i].ColorA + vs[i].ColorG *= vs[i].ColorA + vs[i].ColorB *= vs[i].ColorA + } + vertices = vs + } + + opShader := &ebiten.DrawTrianglesShaderOptions{} + opShader.CompositeMode = ebiten.CompositeModeCustom + opShader.Blend = op.Blend + opShader.FillRule = op.FillRule + opShader.AntiAlias = op.AntiAlias + opShader.Uniforms = uniforms(colorM) + opShader.Images[0] = img + s := builtinShader(builtinshader.Filter(op.Filter), builtinshader.Address(op.Address)) + dst.DrawTrianglesShader(vertices, indices, s, opShader) +} diff --git a/colorm/draw_test.go b/colorm/draw_test.go new file mode 100644 index 000000000..1ccc2f285 --- /dev/null +++ b/colorm/draw_test.go @@ -0,0 +1,301 @@ +// Copyright 2022 The Ebitengine Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package colorm_test + +import ( + "fmt" + "image/color" + "math" + "testing" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" + t "github.com/hajimehoshi/ebiten/v2/internal/testing" + "github.com/hajimehoshi/ebiten/v2/internal/ui" +) + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +// sameColors compares c1 and c2 and returns a boolean value indicating +// if the two colors are (almost) same. +// +// Pixels read from GPU might include errors (#492), and +// sameColors considers such errors as delta. +func sameColors(c1, c2 color.RGBA, delta int) bool { + return abs(int(c1.R)-int(c2.R)) <= delta && + abs(int(c1.G)-int(c2.G)) <= delta && + abs(int(c1.B)-int(c2.B)) <= delta && + abs(int(c1.A)-int(c2.A)) <= delta +} + +func TestMain(m *testing.M) { + ui.SetPanicOnErrorOnReadingPixelsForTesting(true) + t.MainWithRunLoop(m) +} + +func TestDrawTrianglesWithColorM(t *testing.T) { + const w, h = 16, 16 + dst0 := ebiten.NewImage(w, h) + src := ebiten.NewImage(w, h) + src.Fill(color.White) + + vs0 := []ebiten.Vertex{ + { + DstX: 0, + DstY: 0, + SrcX: 0, + SrcY: 0, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + { + DstX: w, + DstY: 0, + SrcX: w, + SrcY: 0, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + { + DstX: 0, + DstY: h, + SrcX: 0, + SrcY: h, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + { + DstX: w, + DstY: h, + SrcX: w, + SrcY: h, + ColorR: 1, + ColorG: 1, + ColorB: 1, + ColorA: 1, + }, + } + + var cm colorm.ColorM + cm.Scale(0.2, 0.4, 0.6, 0.8) + op := &colorm.DrawTrianglesOptions{} + is := []uint16{0, 1, 2, 1, 2, 3} + colorm.DrawTriangles(dst0, vs0, is, src, cm, op) + + for _, format := range []ebiten.ColorScaleMode{ + ebiten.ColorScaleModeStraightAlpha, + ebiten.ColorScaleModePremultipliedAlpha, + } { + format := format + t.Run(fmt.Sprintf("format%d", format), func(t *testing.T) { + var cr, cg, cb, ca float32 + switch format { + case ebiten.ColorScaleModeStraightAlpha: + // The values are the same as ColorM.Scale + cr = 0.2 + cg = 0.4 + cb = 0.6 + ca = 0.8 + case ebiten.ColorScaleModePremultipliedAlpha: + cr = 0.2 * 0.8 + cg = 0.4 * 0.8 + cb = 0.6 * 0.8 + ca = 0.8 + } + vs1 := []ebiten.Vertex{ + { + DstX: 0, + DstY: 0, + SrcX: 0, + SrcY: 0, + ColorR: cr, + ColorG: cg, + ColorB: cb, + ColorA: ca, + }, + { + DstX: w, + DstY: 0, + SrcX: w, + SrcY: 0, + ColorR: cr, + ColorG: cg, + ColorB: cb, + ColorA: ca, + }, + { + DstX: 0, + DstY: h, + SrcX: 0, + SrcY: h, + ColorR: cr, + ColorG: cg, + ColorB: cb, + ColorA: ca, + }, + { + DstX: w, + DstY: h, + SrcX: w, + SrcY: h, + ColorR: cr, + ColorG: cg, + ColorB: cb, + ColorA: ca, + }, + } + + dst1 := ebiten.NewImage(w, h) + op := &ebiten.DrawTrianglesOptions{} + op.ColorScaleMode = format + dst1.DrawTriangles(vs1, is, src, op) + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst0.At(i, j) + want := dst1.At(i, j) + if got != want { + t.Errorf("At(%d, %d): got: %v, want: %v", i, j, got, want) + } + } + } + }) + } +} + +func TestColorMAndScale(t *testing.T) { + const w, h = 16, 16 + src := ebiten.NewImage(w, h) + + src.Fill(color.RGBA{0x80, 0x80, 0x80, 0x80}) + vs := []ebiten.Vertex{ + { + SrcX: 0, + SrcY: 0, + DstX: 0, + DstY: 0, + ColorR: 0.5, + ColorG: 0.25, + ColorB: 0.5, + ColorA: 0.75, + }, + { + SrcX: w, + SrcY: 0, + DstX: w, + DstY: 0, + ColorR: 0.5, + ColorG: 0.25, + ColorB: 0.5, + ColorA: 0.75, + }, + { + SrcX: 0, + SrcY: h, + DstX: 0, + DstY: h, + ColorR: 0.5, + ColorG: 0.25, + ColorB: 0.5, + ColorA: 0.75, + }, + { + SrcX: w, + SrcY: h, + DstX: w, + DstY: h, + ColorR: 0.5, + ColorG: 0.25, + ColorB: 0.5, + ColorA: 0.75, + }, + } + is := []uint16{0, 1, 2, 1, 2, 3} + + for _, format := range []ebiten.ColorScaleMode{ + ebiten.ColorScaleModeStraightAlpha, + ebiten.ColorScaleModePremultipliedAlpha, + } { + format := format + t.Run(fmt.Sprintf("format%d", format), func(t *testing.T) { + dst := ebiten.NewImage(w, h) + + var cm colorm.ColorM + cm.Translate(0.25, 0.25, 0.25, 0) + op := &colorm.DrawTrianglesOptions{} + op.ColorScaleMode = format + colorm.DrawTriangles(dst, vs, is, src, cm, op) + + got := dst.At(0, 0).(color.RGBA) + alphaBeforeScale := 0.5 + var want color.RGBA + switch format { + case ebiten.ColorScaleModeStraightAlpha: + want = color.RGBA{ + byte(math.Floor(0xff * (0.5/alphaBeforeScale + 0.25) * alphaBeforeScale * 0.5 * 0.75)), + byte(math.Floor(0xff * (0.5/alphaBeforeScale + 0.25) * alphaBeforeScale * 0.25 * 0.75)), + byte(math.Floor(0xff * (0.5/alphaBeforeScale + 0.25) * alphaBeforeScale * 0.5 * 0.75)), + byte(math.Floor(0xff * alphaBeforeScale * 0.75)), + } + case ebiten.ColorScaleModePremultipliedAlpha: + want = color.RGBA{ + byte(math.Floor(0xff * (0.5/alphaBeforeScale + 0.25) * alphaBeforeScale * 0.5)), + byte(math.Floor(0xff * (0.5/alphaBeforeScale + 0.25) * alphaBeforeScale * 0.25)), + byte(math.Floor(0xff * (0.5/alphaBeforeScale + 0.25) * alphaBeforeScale * 0.5)), + byte(math.Floor(0xff * alphaBeforeScale * 0.75)), + } + } + if !sameColors(got, want, 2) { + t.Errorf("got: %v, want: %v", got, want) + } + }) + } +} + +// Issue #1213 +func TestColorMCopy(t *testing.T) { + const w, h = 16, 16 + dst := ebiten.NewImage(w, h) + src := ebiten.NewImage(w, h) + + for k := 0; k < 256; k++ { + var cm colorm.ColorM + cm.Translate(1, 1, 1, float64(k)/0xff) + op := &colorm.DrawImageOptions{} + op.Blend = ebiten.BlendCopy + colorm.DrawImage(dst, src, cm, op) + + for j := 0; j < h; j++ { + for i := 0; i < w; i++ { + got := dst.At(i, j).(color.RGBA) + want := color.RGBA{byte(k), byte(k), byte(k), byte(k)} + if !sameColors(got, want, 1) { + t.Fatalf("dst.At(%d, %d), k: %d: got %v, want %v", i, j, k, got, want) + } + } + } + } +} diff --git a/colorscale.go b/colorscale.go index f800bb090..1bb21bb68 100644 --- a/colorscale.go +++ b/colorscale.go @@ -20,6 +20,7 @@ import ( ) // ColorScale represents a scale of RGBA color. +// ColorScale is intended to be applied to a premultiplied-alpha color value. // // The initial (zero) value of ColorScale is an identity scale (1, 1, 1, 1). type ColorScale struct { @@ -33,6 +34,14 @@ func (c *ColorScale) String() string { return fmt.Sprintf("(%f,%f,%f,%f)", c.r_1+1, c.g_1+1, c.b_1+1, c.a_1+1) } +// Reset resets the ColorScale as identity. +func (c *ColorScale) Reset() { + c.r_1 = 0 + c.g_1 = 0 + c.b_1 = 0 + c.a_1 = 0 +} + // R returns the red scale. func (c *ColorScale) R() float32 { return c.r_1 + 1 @@ -85,8 +94,20 @@ func (c *ColorScale) Scale(r, g, b, a float32) { c.a_1 = (c.a_1+1)*a - 1 } +// ScaleAlpha multiplies the given alpha value to the current scale. +func (c *ColorScale) ScaleAlpha(a float32) { + c.r_1 = (c.r_1+1)*a - 1 + c.g_1 = (c.g_1+1)*a - 1 + c.b_1 = (c.b_1+1)*a - 1 + c.a_1 = (c.a_1+1)*a - 1 +} + // ScaleWithColor multiplies the given color values to the current scale. func (c *ColorScale) ScaleWithColor(clr color.Color) { cr, cg, cb, ca := clr.RGBA() c.Scale(float32(cr)/0xffff, float32(cg)/0xffff, float32(cb)/0xffff, float32(ca)/0xffff) } + +func (c *ColorScale) apply(r, g, b, a float32) (float32, float32, float32, float32) { + return (c.r_1 + 1) * r, (c.g_1 + 1) * g, (c.b_1 + 1) * b, (c.a_1 + 1) * a +} diff --git a/examples/2048/2048/board.go b/examples/2048/2048/board.go index be6982698..4a1953564 100644 --- a/examples/2048/2048/board.go +++ b/examples/2048/2048/board.go @@ -129,7 +129,7 @@ func (b *Board) Draw(boardImage *ebiten.Image) { x := i*tileSize + (i+1)*tileMargin y := j*tileSize + (j+1)*tileMargin op.GeoM.Translate(float64(x), float64(y)) - op.ColorM.ScaleWithColor(tileBackgroundColor(v)) + op.ColorScale.ScaleWithColor(tileBackgroundColor(v)) boardImage.DrawImage(tileImage, op) } } diff --git a/examples/2048/2048/tile.go b/examples/2048/2048/tile.go index 3e64fd3e8..8f0abb25e 100644 --- a/examples/2048/2048/tile.go +++ b/examples/2048/2048/tile.go @@ -383,7 +383,7 @@ func (t *Tile) Draw(boardImage *ebiten.Image) { op.GeoM.Translate(float64(tileSize/2), float64(tileSize/2)) } op.GeoM.Translate(float64(x), float64(y)) - op.ColorM.ScaleWithColor(tileBackgroundColor(v)) + op.ColorScale.ScaleWithColor(tileBackgroundColor(v)) boardImage.DrawImage(tileImage, op) str := strconv.Itoa(v) diff --git a/examples/alphablending/main.go b/examples/alphablending/main.go index 82bb0b462..813a3c6c9 100644 --- a/examples/alphablending/main.go +++ b/examples/alphablending/main.go @@ -61,7 +61,7 @@ func (g *Game) Draw(screen *ebiten.Image) { // Draw 100 Ebitens v := g.offset() op := &ebiten.DrawImageOptions{} - op.ColorM.Scale(1.0, 1.0, 1.0, 0.5) + op.ColorScale.ScaleAlpha(0.5) for i := 0; i < 10*10; i++ { op.GeoM.Reset() x := float64(i%10)*v + 15 diff --git a/examples/blocks/blocks/field.go b/examples/blocks/blocks/field.go index 489c23a46..fd3852e42 100644 --- a/examples/blocks/blocks/field.go +++ b/examples/blocks/blocks/field.go @@ -16,6 +16,7 @@ package blocks import ( "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" ) const maxFlushCount = 20 @@ -193,8 +194,8 @@ func min(a, b float64) float64 { return a } -func flushingColor(rate float64) ebiten.ColorM { - clr := ebiten.ColorM{} +func flushingColor(rate float64) colorm.ColorM { + var clr colorm.ColorM alpha := min(1, rate*2) clr.Scale(1, 1, 1, alpha) r := min(1, (1-rate)*2) @@ -211,7 +212,7 @@ func (f *Field) Draw(r *ebiten.Image, x, y int) { } } else { for i := 0; i < fieldBlockCountX; i++ { - drawBlock(r, f.blocks[i][j], i*blockWidth+x, j*blockHeight+y, ebiten.ColorM{}) + drawBlock(r, f.blocks[i][j], i*blockWidth+x, j*blockHeight+y, colorm.ColorM{}) } } } diff --git a/examples/blocks/blocks/gamescene.go b/examples/blocks/blocks/gamescene.go index 6f805f8c6..d4129ba8b 100644 --- a/examples/blocks/blocks/gamescene.go +++ b/examples/blocks/blocks/gamescene.go @@ -25,6 +25,7 @@ import ( "time" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/examples/resources/images" "github.com/hajimehoshi/ebiten/v2/inpututil" "github.com/hajimehoshi/ebiten/v2/vector" @@ -148,17 +149,17 @@ func NewGameScene() *GameScene { } var ( - lightGray ebiten.ColorM + lightGray colorm.ColorM ) func init() { - id := ebiten.ColorM{} + var id colorm.ColorM - mono := ebiten.ColorM{} + var mono colorm.ColorM mono.ChangeHSV(0, 0, 1) - for j := 0; j < ebiten.ColorMDim-1; j++ { - for i := 0; i < ebiten.ColorMDim-1; i++ { + for j := 0; j < colorm.Dim-1; j++ { + for i := 0; i < colorm.Dim-1; i++ { lightGray.SetElement(i, j, mono.Element(i, j)*0.7+id.Element(i, j)*0.3) } } @@ -177,13 +178,12 @@ func (s *GameScene) drawBackground(r *ebiten.Image) { scale = scaleH } - op := &ebiten.DrawImageOptions{} + op := &colorm.DrawImageOptions{} op.GeoM.Translate(-float64(w)/2, -float64(h)/2) op.GeoM.Scale(scale, scale) op.GeoM.Translate(ScreenWidth/2, ScreenHeight/2) - op.ColorM = lightGray op.Filter = ebiten.FilterLinear - r.DrawImage(imageGameBG, op) + colorm.DrawImage(r, imageGameBG, lightGray, op) } const ( diff --git a/examples/blocks/blocks/piece.go b/examples/blocks/blocks/piece.go index ac926a6c8..c527b756c 100644 --- a/examples/blocks/blocks/piece.go +++ b/examples/blocks/blocks/piece.go @@ -20,6 +20,7 @@ import ( _ "image/png" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" rblocks "github.com/hajimehoshi/ebiten/v2/examples/resources/images/blocks" ) @@ -165,17 +166,16 @@ const ( fieldBlockCountY = 20 ) -func drawBlock(r *ebiten.Image, block BlockType, x, y int, clr ebiten.ColorM) { +func drawBlock(r *ebiten.Image, block BlockType, x, y int, clr colorm.ColorM) { if block == BlockTypeNone { return } - op := &ebiten.DrawImageOptions{} - op.ColorM = clr + op := &colorm.DrawImageOptions{} op.GeoM.Translate(float64(x), float64(y)) srcX := (int(block) - 1) * blockWidth - r.DrawImage(imageBlocks.SubImage(image.Rect(srcX, 0, srcX+blockWidth, blockHeight)).(*ebiten.Image), op) + colorm.DrawImage(r, imageBlocks.SubImage(image.Rect(srcX, 0, srcX+blockWidth, blockHeight)).(*ebiten.Image), clr, op) } func (p *Piece) InitialPosition() (int, int) { @@ -249,7 +249,7 @@ func (p *Piece) Draw(r *ebiten.Image, x, y int, angle Angle) { for i := range p.blocks { for j := range p.blocks[i] { if p.isBlocked(i, j, angle) { - drawBlock(r, p.blockType, i*blockWidth+x, j*blockHeight+y, ebiten.ColorM{}) + drawBlock(r, p.blockType, i*blockWidth+x, j*blockHeight+y, colorm.ColorM{}) } } } diff --git a/examples/blocks/blocks/scenemanager.go b/examples/blocks/blocks/scenemanager.go index ead27f9ab..305267c3e 100644 --- a/examples/blocks/blocks/scenemanager.go +++ b/examples/blocks/blocks/scenemanager.go @@ -73,9 +73,9 @@ func (s *SceneManager) Draw(r *ebiten.Image) { r.DrawImage(transitionFrom, nil) - alpha := 1 - float64(s.transitionCount)/float64(transitionMaxCount) + alpha := 1 - float32(s.transitionCount)/float32(transitionMaxCount) op := &ebiten.DrawImageOptions{} - op.ColorM.Scale(1, 1, 1, alpha) + op.ColorScale.ScaleAlpha(alpha) r.DrawImage(transitionTo, op) } diff --git a/examples/blur/main.go b/examples/blur/main.go index 6cf18b188..6cab52baf 100644 --- a/examples/blur/main.go +++ b/examples/blur/main.go @@ -78,7 +78,7 @@ func (g *Game) Draw(screen *ebiten.Image) { // A_{n+1} = A_n * (1 - 1/(n+1)) + a_{n+1} * 1/(n+1) // which is precisely what an alpha blend with alpha 1/(n+1) does. layers++ - op.ColorM.Scale(1, 1, 1, 1.0/float64(layers)) + op.ColorScale.ScaleAlpha(1 / float32(layers)) screen.DrawImage(gophersImage, op) } } diff --git a/examples/chipmunk/main.go b/examples/chipmunk/main.go index cee1f3c5f..8912ec112 100644 --- a/examples/chipmunk/main.go +++ b/examples/chipmunk/main.go @@ -96,7 +96,7 @@ func (g *Game) Draw(screen *ebiten.Image) { screen.Fill(color.Black) op := &ebiten.DrawImageOptions{} - op.ColorM.Scale(200.0/255.0, 200.0/255.0, 200.0/255.0, 1) + op.ColorScale.Scale(200.0/255.0, 200.0/255.0, 200.0/255.0, 1) g.space.EachBody(func(body *cp.Body) { op.GeoM.Reset() diff --git a/examples/drag/main.go b/examples/drag/main.go index 470f22d7b..f83e05bbc 100644 --- a/examples/drag/main.go +++ b/examples/drag/main.go @@ -77,10 +77,10 @@ func (s *Sprite) MoveBy(x, y int) { } // Draw draws the sprite. -func (s *Sprite) Draw(screen *ebiten.Image, dx, dy int, alpha float64) { +func (s *Sprite) Draw(screen *ebiten.Image, dx, dy int, alpha float32) { op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(s.x+dx), float64(s.y+dy)) - op.ColorM.Scale(1, 1, 1, alpha) + op.ColorScale.ScaleAlpha(alpha) screen.DrawImage(s.image, op) screen.DrawImage(s.image, op) } diff --git a/examples/flood/main.go b/examples/flood/main.go index 5c9d7f7ee..d158adcb5 100644 --- a/examples/flood/main.go +++ b/examples/flood/main.go @@ -22,6 +22,7 @@ import ( "log" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/examples/resources/images" ) @@ -66,20 +67,21 @@ func (g *Game) Draw(screen *ebiten.Image) { // Fill with solid colors for i, c := range colors { - op := &ebiten.DrawImageOptions{} + op := &colorm.DrawImageOptions{} x := i % 4 y := i/4 + 1 op.GeoM.Translate(ox+float64(dx*x), oy+float64(dy*y)) // Reset RGB (not Alpha) 0 forcibly - op.ColorM.Scale(0, 0, 0, 1) + var cm colorm.ColorM + cm.Scale(0, 0, 0, 1) // Set color r := float64(c.R) / 0xff g := float64(c.G) / 0xff b := float64(c.B) / 0xff - op.ColorM.Translate(r, g, b, 0) - screen.DrawImage(ebitenImage, op) + cm.Translate(r, g, b, 0) + colorm.DrawImage(screen, ebitenImage, cm, op) } } diff --git a/examples/hsv/main.go b/examples/hsv/main.go index 163139fa7..048624597 100644 --- a/examples/hsv/main.go +++ b/examples/hsv/main.go @@ -23,6 +23,7 @@ import ( "math" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/hajimehoshi/ebiten/v2/examples/resources/images" "github.com/hajimehoshi/ebiten/v2/inpututil" @@ -100,7 +101,7 @@ func (g *Game) Update() error { func (g *Game) Draw(screen *ebiten.Image) { // Center the image on the screen. w, h := gophersImage.Size() - op := &ebiten.DrawImageOptions{} + op := &colorm.DrawImageOptions{} op.GeoM.Translate(-float64(w)/2, -float64(h)/2) op.GeoM.Scale(2, 2) op.GeoM.Translate(float64(screenWidth)/2, float64(screenHeight)/2) @@ -109,15 +110,16 @@ func (g *Game) Draw(screen *ebiten.Image) { hue := float64(g.hue128) * 2 * math.Pi / 128 saturation := float64(g.saturation128) / 128 value := float64(g.value128) / 128 - op.ColorM.ChangeHSV(hue, saturation, value) + var c colorm.ColorM + c.ChangeHSV(hue, saturation, value) // Invert the color. if g.inverted { - op.ColorM.Scale(-1, -1, -1, 1) - op.ColorM.Translate(1, 1, 1, 0) + c.Scale(-1, -1, -1, 1) + c.Translate(1, 1, 1, 0) } - screen.DrawImage(gophersImage, op) + colorm.DrawImage(screen, gophersImage, c, op) // Draw the text of the current status. msgInverted := "false" diff --git a/examples/hue/main.go b/examples/hue/main.go index 934d6a5ac..a253e5f77 100644 --- a/examples/hue/main.go +++ b/examples/hue/main.go @@ -22,6 +22,7 @@ import ( "math" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/examples/resources/images" ) @@ -46,15 +47,15 @@ func (g *Game) Update() error { func (g *Game) Draw(screen *ebiten.Image) { // Center the image on the screen. w, h := gophersImage.Size() - op := &ebiten.DrawImageOptions{} + op := &colorm.DrawImageOptions{} op.GeoM.Translate(-float64(w)/2, -float64(h)/2) op.GeoM.Scale(2, 2) op.GeoM.Translate(float64(screenWidth)/2, float64(screenHeight)/2) // Rotate the hue. - op.ColorM.RotateHue(float64(g.count%360) * 2 * math.Pi / 360) - - screen.DrawImage(gophersImage, op) + var c colorm.ColorM + c.RotateHue(float64(g.count%360) * 2 * math.Pi / 360) + colorm.DrawImage(screen, gophersImage, c, op) } func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) { diff --git a/examples/keyboard/main.go b/examples/keyboard/main.go index db493f7ee..874e48f2e 100644 --- a/examples/keyboard/main.go +++ b/examples/keyboard/main.go @@ -62,7 +62,7 @@ func (g *Game) Draw(screen *ebiten.Image) { // Draw the base (grayed) keyboard image. op := &ebiten.DrawImageOptions{} op.GeoM.Translate(offsetX, offsetY) - op.ColorM.Scale(0.5, 0.5, 0.5, 1) + op.ColorScale.Scale(0.5, 0.5, 0.5, 1) screen.DrawImage(keyboardImage, op) // Draw the highlighted keys. diff --git a/examples/paint/main.go b/examples/paint/main.go index 563652312..de3b5df13 100644 --- a/examples/paint/main.go +++ b/examples/paint/main.go @@ -22,6 +22,7 @@ import ( "math" "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/colorm" "github.com/hajimehoshi/ebiten/v2/ebitenutil" ) @@ -115,14 +116,15 @@ func (g *Game) Update() error { // paint draws the brush on the given canvas image at the position (x, y). func (g *Game) paint(canvas *ebiten.Image, x, y int) { - op := &ebiten.DrawImageOptions{} + op := &colorm.DrawImageOptions{} op.GeoM.Translate(float64(x), float64(y)) + var cm colorm.ColorM // Scale the color and rotate the hue so that colors vary on each frame. - op.ColorM.Scale(1.0, 0.50, 0.125, 1.0) + cm.Scale(1.0, 0.50, 0.125, 1.0) tps := ebiten.TPS() theta := 2.0 * math.Pi * float64(g.count%tps) / float64(tps) - op.ColorM.RotateHue(theta) - canvas.DrawImage(brushImage, op) + cm.RotateHue(theta) + colorm.DrawImage(canvas, brushImage, cm, op) } func (g *Game) Draw(screen *ebiten.Image) { diff --git a/examples/particles/main.go b/examples/particles/main.go index 9a57a78b5..d30538a8a 100644 --- a/examples/particles/main.go +++ b/examples/particles/main.go @@ -59,7 +59,7 @@ type sprite struct { img *ebiten.Image scale float64 angle float64 - alpha float64 + alpha float32 } func (s *sprite) update() { @@ -94,8 +94,8 @@ func (s *sprite) draw(screen *ebiten.Image) { op.GeoM.Translate(x, y) op.GeoM.Translate(ox, oy) - rate := float64(s.count) / float64(s.maxCount) - alpha := 0.0 + rate := float32(s.count) / float32(s.maxCount) + var alpha float32 if rate < 0.2 { alpha = rate / 0.2 } else if rate > 0.8 { @@ -104,7 +104,7 @@ func (s *sprite) draw(screen *ebiten.Image) { alpha = 1 } alpha *= s.alpha - op.ColorM.Scale(1, 1, 1, alpha) + op.ColorScale.ScaleAlpha(alpha) screen.DrawImage(s.img, op) } diff --git a/examples/raycasting/main.go b/examples/raycasting/main.go index edae19af4..36b69af76 100644 --- a/examples/raycasting/main.go +++ b/examples/raycasting/main.go @@ -253,7 +253,7 @@ func (g *Game) Draw(screen *ebiten.Image) { // Draw shadow op := &ebiten.DrawImageOptions{} - op.ColorM.Scale(1, 1, 1, 0.7) + op.ColorScale.ScaleAlpha(0.7) screen.DrawImage(shadowImage, op) // Draw walls diff --git a/examples/sprites/main.go b/examples/sprites/main.go index ac1397cec..795d015d0 100644 --- a/examples/sprites/main.go +++ b/examples/sprites/main.go @@ -50,7 +50,7 @@ func init() { ebitenImage = ebiten.NewImage(w, h) op := &ebiten.DrawImageOptions{} - op.ColorM.Scale(1, 1, 1, 0.5) + op.ColorScale.ScaleAlpha(0.5) ebitenImage.DrawImage(origEbitenImage, op) } diff --git a/examples/spriteshd/main.go b/examples/spriteshd/main.go index 73c4c6af0..b03fb5582 100644 --- a/examples/spriteshd/main.go +++ b/examples/spriteshd/main.go @@ -51,7 +51,7 @@ func init() { ebitenImage = ebiten.NewImage(w, h) op := &ebiten.DrawImageOptions{} - op.ColorM.Scale(1, 1, 1, 0.5) + op.ColorScale.ScaleAlpha(0.5) ebitenImage.DrawImage(origEbitenImage, op) } diff --git a/examples/text/main.go b/examples/text/main.go index b91f55f50..53e78b75d 100644 --- a/examples/text/main.go +++ b/examples/text/main.go @@ -127,20 +127,20 @@ func (g *Game) Draw(screen *ebiten.Image) { op.GeoM.Reset() op.GeoM.Translate(x, y) op.GeoM.Translate(gl.X, gl.Y) - op.ColorM.Reset() - r := 1.0 + op.ColorScale.Reset() + r := float32(1) if i%3 == 0 { r = 0.5 } - g := 1.0 + g := float32(1) if i%3 == 1 { g = 0.5 } - b := 1.0 + b := float32(1) if i%3 == 2 { b = 0.5 } - op.ColorM.Scale(r, g, b, 1) + op.ColorScale.Scale(r, g, b, 1) screen.DrawImage(gl.Image, op) } } diff --git a/image.go b/image.go index c2c1edaef..5f6879cc3 100644 --- a/image.go +++ b/image.go @@ -102,8 +102,16 @@ type DrawImageOptions struct { // The default (zero) value is identity, which draws the image at (0, 0). GeoM GeoM + // ColorScale is a scale of color. + // ColorScale is slightly different from ColorM's Scale in terms of alphas: + // ColorScale is applied to premultiplied-alpha colors, while ColorM is applied to straight-alpha colors. + // The default (zero) value is identity, which is (1, 1, 1, 1). + ColorScale ColorScale + // ColorM is a color matrix to draw. // The default (zero) value is identity, which doesn't change any color. + // + // Deprecated: as of v2.5. Use ColorScale or the package colorm instead. ColorM ColorM // CompositeMode is a composite mode to draw. @@ -231,6 +239,7 @@ func (i *Image) DrawImage(img *Image, options *DrawImageOptions) { sx0, sy0 := img.adjustPosition(bounds.Min.X, bounds.Min.Y) sx1, sy1 := img.adjustPosition(bounds.Max.X, bounds.Max.Y) colorm, cr, cg, cb, ca := colorMToScale(options.ColorM.affineColorM()) + cr, cg, cb, ca = options.ColorScale.apply(cr, cg, cb, ca) vs := graphics.QuadVertices(float32(sx0), float32(sy0), float32(sx1), float32(sy1), a, b, c, d, tx, ty, cr, cg, cb, ca) is := graphics.QuadIndices() @@ -324,9 +333,12 @@ type DrawTrianglesOptions struct { // ColorM is a color matrix to draw. // The default (zero) value is identity, which doesn't change any color. // ColorM is applied before vertex color scale is applied. + // + // Deprecated: as of v2.5. Use the package colorm instead. ColorM ColorM // ColorScaleMode is the mode of color scales in vertices. + // ColorScaleMode affects the color calculation with vertex colors, but doesn't affect with a color matrix. // The default (zero) value is ColorScaleModeStraightAlpha. ColorScaleMode ColorScaleMode diff --git a/image_test.go b/image_test.go index bb617b461..458363871 100644 --- a/image_test.go +++ b/image_test.go @@ -1009,12 +1009,12 @@ func TestImageMipmapColor(t *testing.T) { op := &ebiten.DrawImageOptions{} op.Filter = ebiten.FilterLinear op.GeoM.Scale(s, s) - op.ColorM.Scale(1, 1, 0, 1) + op.ColorScale.Scale(1, 1, 0, 1) img0.DrawImage(img1, op) op.GeoM.Translate(128, 0) - op.ColorM.Reset() - op.ColorM.Scale(0, 1, 1, 1) + op.ColorScale.Reset() + op.ColorScale.Scale(0, 1, 1, 1) img0.DrawImage(img1, op) want := color.RGBA{0, 0xff, 0xff, 0xff} @@ -3967,6 +3967,30 @@ func TestImageColorMScale(t *testing.T) { } } +func TestImageColorScaleAndColorM(t *testing.T) { + const w, h = 16, 16 + dst0 := ebiten.NewImage(w, h) + dst1 := ebiten.NewImage(w, h) + src := ebiten.NewImage(w, h) + src.Fill(color.RGBA{0x24, 0x3f, 0x6a, 0x88}) + + // ColorScale is applied to premultiplied-alpha colors. + op := &ebiten.DrawImageOptions{} + op.ColorScale.Scale(0.3*0.6, 0.4*0.6, 0.5*0.6, 0.6) + dst0.DrawImage(src, op) + + // ColorM.Scale is applied to straight-alpha colors. + op = &ebiten.DrawImageOptions{} + op.ColorM.Scale(0.3, 0.4, 0.5, 0.6) + dst1.DrawImage(src, op) + + got := dst0.At(0, 0) + want := dst1.At(0, 0) + if got != want { + t.Errorf("got: %v, want: %v", got, want) + } +} + // Issue #2428 func TestImageSetAndSubImage(t *testing.T) { const w, h = 16, 16 diff --git a/text/text.go b/text/text.go index 75aaee5ef..d00ad331a 100644 --- a/text/text.go +++ b/text/text.go @@ -181,7 +181,7 @@ var textM sync.Mutex func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) { op := &ebiten.DrawImageOptions{} op.GeoM.Translate(float64(x), float64(y)) - op.ColorM.ScaleWithColor(clr) + op.ColorScale.ScaleWithColor(clr) DrawWithOptions(dst, text, face, op) }