ebiten: separate a package for ColorM: colorm

Closes #2171
This commit is contained in:
Hajime Hoshi 2022-11-02 02:43:42 +09:00
parent 8c5f525ac2
commit 156c34a316
28 changed files with 645 additions and 78 deletions

80
colorm/colorm.go Normal file
View File

@ -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
}

View File

@ -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 {

121
colorm/draw.go Normal file
View File

@ -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)
}

301
colorm/draw_test.go Normal file
View File

@ -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)
}
}
}
}
}

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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)

View File

@ -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

View File

@ -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{})
}
}
}

View File

@ -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 (

View File

@ -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{})
}
}
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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) {

View File

@ -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.

View File

@ -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) {

View File

@ -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)
}

View File

@ -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

View File

@ -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)
}

View File

@ -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)
}

View File

@ -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)
}
}

View File

@ -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

View File

@ -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

View File

@ -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)
}