mirror of
https://github.com/hajimehoshi/ebiten.git
synced 2024-12-25 11:18:54 +01:00
parent
8c5f525ac2
commit
156c34a316
80
colorm/colorm.go
Normal file
80
colorm/colorm.go
Normal 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
|
||||||
|
}
|
@ -12,20 +12,20 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package ebiten_test
|
package colorm_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"image/color"
|
"image/color"
|
||||||
"math"
|
"math"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestColorMInit(t *testing.T) {
|
func TestColorMInit(t *testing.T) {
|
||||||
var m ebiten.ColorM
|
var m colorm.ColorM
|
||||||
for i := 0; i < ebiten.ColorMDim-1; i++ {
|
for i := 0; i < colorm.Dim-1; i++ {
|
||||||
for j := 0; j < ebiten.ColorMDim; j++ {
|
for j := 0; j < colorm.Dim; j++ {
|
||||||
got := m.Element(i, j)
|
got := m.Element(i, j)
|
||||||
want := 0.0
|
want := 0.0
|
||||||
if i == j {
|
if i == j {
|
||||||
@ -38,8 +38,8 @@ func TestColorMInit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
m.SetElement(0, 0, 1)
|
m.SetElement(0, 0, 1)
|
||||||
for i := 0; i < ebiten.ColorMDim-1; i++ {
|
for i := 0; i < colorm.Dim-1; i++ {
|
||||||
for j := 0; j < ebiten.ColorMDim; j++ {
|
for j := 0; j < colorm.Dim; j++ {
|
||||||
got := m.Element(i, j)
|
got := m.Element(i, j)
|
||||||
want := 0.0
|
want := 0.0
|
||||||
if i == j {
|
if i == j {
|
||||||
@ -53,7 +53,7 @@ func TestColorMInit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestColorMAssign(t *testing.T) {
|
func TestColorMAssign(t *testing.T) {
|
||||||
m := ebiten.ColorM{}
|
m := colorm.ColorM{}
|
||||||
m.SetElement(0, 0, 1)
|
m.SetElement(0, 0, 1)
|
||||||
m2 := m
|
m2 := m
|
||||||
m.SetElement(0, 0, 0)
|
m.SetElement(0, 0, 0)
|
||||||
@ -71,7 +71,7 @@ func TestColorMTranslate(t *testing.T) {
|
|||||||
{0, 0, 1, 0, 2.5},
|
{0, 0, 1, 0, 2.5},
|
||||||
{0, 0, 0, 1, 3.5},
|
{0, 0, 0, 1, 3.5},
|
||||||
}
|
}
|
||||||
m := ebiten.ColorM{}
|
m := colorm.ColorM{}
|
||||||
m.Translate(0.5, 1.5, 2.5, 3.5)
|
m.Translate(0.5, 1.5, 2.5, 3.5)
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
for j := 0; j < 5; j++ {
|
for j := 0; j < 5; j++ {
|
||||||
@ -91,7 +91,7 @@ func TestColorMScale(t *testing.T) {
|
|||||||
{0, 0, 2.5, 0, 0},
|
{0, 0, 2.5, 0, 0},
|
||||||
{0, 0, 0, 3.5, 0},
|
{0, 0, 0, 3.5, 0},
|
||||||
}
|
}
|
||||||
m := ebiten.ColorM{}
|
m := colorm.ColorM{}
|
||||||
m.Scale(0.5, 1.5, 2.5, 3.5)
|
m.Scale(0.5, 1.5, 2.5, 3.5)
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
for j := 0; j < 5; j++ {
|
for j := 0; j < 5; j++ {
|
||||||
@ -111,7 +111,7 @@ func TestColorMTranslateAndScale(t *testing.T) {
|
|||||||
{0, 0, 1, 0, 0},
|
{0, 0, 1, 0, 0},
|
||||||
{0, 0, 0, 0.5, 0.5},
|
{0, 0, 0, 0.5, 0.5},
|
||||||
}
|
}
|
||||||
m := ebiten.ColorM{}
|
m := colorm.ColorM{}
|
||||||
m.Translate(0, 0, 0, 1)
|
m.Translate(0, 0, 0, 1)
|
||||||
m.Scale(1, 1, 1, 0.5)
|
m.Scale(1, 1, 1, 0.5)
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
@ -132,7 +132,7 @@ func TestColorMMonochrome(t *testing.T) {
|
|||||||
{0.2990, 0.5870, 0.1140, 0, 0},
|
{0.2990, 0.5870, 0.1140, 0, 0},
|
||||||
{0, 0, 0, 1, 0},
|
{0, 0, 0, 1, 0},
|
||||||
}
|
}
|
||||||
m := ebiten.ColorM{}
|
m := colorm.ColorM{}
|
||||||
m.ChangeHSV(0, 0, 1)
|
m.ChangeHSV(0, 0, 1)
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
for j := 0; j < 5; j++ {
|
for j := 0; j < 5; j++ {
|
||||||
@ -152,7 +152,7 @@ func TestColorMConcatSelf(t *testing.T) {
|
|||||||
{30, 43, 51, 39, 34},
|
{30, 43, 51, 39, 34},
|
||||||
{25, 37, 39, 46, 36},
|
{25, 37, 39, 46, 36},
|
||||||
}
|
}
|
||||||
m := ebiten.ColorM{}
|
m := colorm.ColorM{}
|
||||||
for i := 0; i < 4; i++ {
|
for i := 0; i < 4; i++ {
|
||||||
for j := 0; j < 5; j++ {
|
for j := 0; j < 5; j++ {
|
||||||
m.SetElement(i, j, float64((i+j)%5+1))
|
m.SetElement(i, j, float64((i+j)%5+1))
|
||||||
@ -178,23 +178,23 @@ func absDiffU32(x, y uint32) uint32 {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestColorMApply(t *testing.T) {
|
func TestColorMApply(t *testing.T) {
|
||||||
mono := ebiten.ColorM{}
|
mono := colorm.ColorM{}
|
||||||
mono.ChangeHSV(0, 0, 1)
|
mono.ChangeHSV(0, 0, 1)
|
||||||
|
|
||||||
shiny := ebiten.ColorM{}
|
shiny := colorm.ColorM{}
|
||||||
shiny.Translate(1, 1, 1, 0)
|
shiny.Translate(1, 1, 1, 0)
|
||||||
|
|
||||||
shift := ebiten.ColorM{}
|
shift := colorm.ColorM{}
|
||||||
shift.Translate(0.5, 0.5, 0.5, 0.5)
|
shift.Translate(0.5, 0.5, 0.5, 0.5)
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
ColorM ebiten.ColorM
|
ColorM colorm.ColorM
|
||||||
In color.Color
|
In color.Color
|
||||||
Out color.Color
|
Out color.Color
|
||||||
Delta uint32
|
Delta uint32
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
ColorM: ebiten.ColorM{},
|
ColorM: colorm.ColorM{},
|
||||||
In: color.RGBA{1, 2, 3, 4},
|
In: color.RGBA{1, 2, 3, 4},
|
||||||
Out: color.RGBA{1, 2, 3, 4},
|
Out: color.RGBA{1, 2, 3, 4},
|
||||||
Delta: 0x101,
|
Delta: 0x101,
|
||||||
@ -237,7 +237,7 @@ func TestColorMApply(t *testing.T) {
|
|||||||
|
|
||||||
// #1765
|
// #1765
|
||||||
func TestColorMConcat(t *testing.T) {
|
func TestColorMConcat(t *testing.T) {
|
||||||
var a, b ebiten.ColorM
|
var a, b colorm.ColorM
|
||||||
a.SetElement(1, 2, -1)
|
a.SetElement(1, 2, -1)
|
||||||
a.Concat(b)
|
a.Concat(b)
|
||||||
if got, want := a.Element(1, 2), -1.0; got != want {
|
if got, want := a.Element(1, 2), -1.0; got != want {
|
121
colorm/draw.go
Normal file
121
colorm/draw.go
Normal 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
301
colorm/draw_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -20,6 +20,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// ColorScale represents a scale of RGBA color.
|
// 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).
|
// The initial (zero) value of ColorScale is an identity scale (1, 1, 1, 1).
|
||||||
type ColorScale struct {
|
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)
|
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.
|
// R returns the red scale.
|
||||||
func (c *ColorScale) R() float32 {
|
func (c *ColorScale) R() float32 {
|
||||||
return c.r_1 + 1
|
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
|
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.
|
// ScaleWithColor multiplies the given color values to the current scale.
|
||||||
func (c *ColorScale) ScaleWithColor(clr color.Color) {
|
func (c *ColorScale) ScaleWithColor(clr color.Color) {
|
||||||
cr, cg, cb, ca := clr.RGBA()
|
cr, cg, cb, ca := clr.RGBA()
|
||||||
c.Scale(float32(cr)/0xffff, float32(cg)/0xffff, float32(cb)/0xffff, float32(ca)/0xffff)
|
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
|
||||||
|
}
|
||||||
|
@ -129,7 +129,7 @@ func (b *Board) Draw(boardImage *ebiten.Image) {
|
|||||||
x := i*tileSize + (i+1)*tileMargin
|
x := i*tileSize + (i+1)*tileMargin
|
||||||
y := j*tileSize + (j+1)*tileMargin
|
y := j*tileSize + (j+1)*tileMargin
|
||||||
op.GeoM.Translate(float64(x), float64(y))
|
op.GeoM.Translate(float64(x), float64(y))
|
||||||
op.ColorM.ScaleWithColor(tileBackgroundColor(v))
|
op.ColorScale.ScaleWithColor(tileBackgroundColor(v))
|
||||||
boardImage.DrawImage(tileImage, op)
|
boardImage.DrawImage(tileImage, op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -383,7 +383,7 @@ func (t *Tile) Draw(boardImage *ebiten.Image) {
|
|||||||
op.GeoM.Translate(float64(tileSize/2), float64(tileSize/2))
|
op.GeoM.Translate(float64(tileSize/2), float64(tileSize/2))
|
||||||
}
|
}
|
||||||
op.GeoM.Translate(float64(x), float64(y))
|
op.GeoM.Translate(float64(x), float64(y))
|
||||||
op.ColorM.ScaleWithColor(tileBackgroundColor(v))
|
op.ColorScale.ScaleWithColor(tileBackgroundColor(v))
|
||||||
boardImage.DrawImage(tileImage, op)
|
boardImage.DrawImage(tileImage, op)
|
||||||
str := strconv.Itoa(v)
|
str := strconv.Itoa(v)
|
||||||
|
|
||||||
|
@ -61,7 +61,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
// Draw 100 Ebitens
|
// Draw 100 Ebitens
|
||||||
v := g.offset()
|
v := g.offset()
|
||||||
op := &ebiten.DrawImageOptions{}
|
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++ {
|
for i := 0; i < 10*10; i++ {
|
||||||
op.GeoM.Reset()
|
op.GeoM.Reset()
|
||||||
x := float64(i%10)*v + 15
|
x := float64(i%10)*v + 15
|
||||||
|
@ -16,6 +16,7 @@ package blocks
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
const maxFlushCount = 20
|
const maxFlushCount = 20
|
||||||
@ -193,8 +194,8 @@ func min(a, b float64) float64 {
|
|||||||
return a
|
return a
|
||||||
}
|
}
|
||||||
|
|
||||||
func flushingColor(rate float64) ebiten.ColorM {
|
func flushingColor(rate float64) colorm.ColorM {
|
||||||
clr := ebiten.ColorM{}
|
var clr colorm.ColorM
|
||||||
alpha := min(1, rate*2)
|
alpha := min(1, rate*2)
|
||||||
clr.Scale(1, 1, 1, alpha)
|
clr.Scale(1, 1, 1, alpha)
|
||||||
r := min(1, (1-rate)*2)
|
r := min(1, (1-rate)*2)
|
||||||
@ -211,7 +212,7 @@ func (f *Field) Draw(r *ebiten.Image, x, y int) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
for i := 0; i < fieldBlockCountX; i++ {
|
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{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
"github.com/hajimehoshi/ebiten/v2/vector"
|
"github.com/hajimehoshi/ebiten/v2/vector"
|
||||||
@ -148,17 +149,17 @@ func NewGameScene() *GameScene {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
lightGray ebiten.ColorM
|
lightGray colorm.ColorM
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
id := ebiten.ColorM{}
|
var id colorm.ColorM
|
||||||
|
|
||||||
mono := ebiten.ColorM{}
|
var mono colorm.ColorM
|
||||||
mono.ChangeHSV(0, 0, 1)
|
mono.ChangeHSV(0, 0, 1)
|
||||||
|
|
||||||
for j := 0; j < ebiten.ColorMDim-1; j++ {
|
for j := 0; j < colorm.Dim-1; j++ {
|
||||||
for i := 0; i < ebiten.ColorMDim-1; i++ {
|
for i := 0; i < colorm.Dim-1; i++ {
|
||||||
lightGray.SetElement(i, j, mono.Element(i, j)*0.7+id.Element(i, j)*0.3)
|
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
|
scale = scaleH
|
||||||
}
|
}
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &colorm.DrawImageOptions{}
|
||||||
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
|
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
|
||||||
op.GeoM.Scale(scale, scale)
|
op.GeoM.Scale(scale, scale)
|
||||||
op.GeoM.Translate(ScreenWidth/2, ScreenHeight/2)
|
op.GeoM.Translate(ScreenWidth/2, ScreenHeight/2)
|
||||||
op.ColorM = lightGray
|
|
||||||
op.Filter = ebiten.FilterLinear
|
op.Filter = ebiten.FilterLinear
|
||||||
r.DrawImage(imageGameBG, op)
|
colorm.DrawImage(r, imageGameBG, lightGray, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
_ "image/png"
|
_ "image/png"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
rblocks "github.com/hajimehoshi/ebiten/v2/examples/resources/images/blocks"
|
rblocks "github.com/hajimehoshi/ebiten/v2/examples/resources/images/blocks"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -165,17 +166,16 @@ const (
|
|||||||
fieldBlockCountY = 20
|
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 {
|
if block == BlockTypeNone {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &colorm.DrawImageOptions{}
|
||||||
op.ColorM = clr
|
|
||||||
op.GeoM.Translate(float64(x), float64(y))
|
op.GeoM.Translate(float64(x), float64(y))
|
||||||
|
|
||||||
srcX := (int(block) - 1) * blockWidth
|
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) {
|
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 i := range p.blocks {
|
||||||
for j := range p.blocks[i] {
|
for j := range p.blocks[i] {
|
||||||
if p.isBlocked(i, j, angle) {
|
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{})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -73,9 +73,9 @@ func (s *SceneManager) Draw(r *ebiten.Image) {
|
|||||||
|
|
||||||
r.DrawImage(transitionFrom, nil)
|
r.DrawImage(transitionFrom, nil)
|
||||||
|
|
||||||
alpha := 1 - float64(s.transitionCount)/float64(transitionMaxCount)
|
alpha := 1 - float32(s.transitionCount)/float32(transitionMaxCount)
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.ColorM.Scale(1, 1, 1, alpha)
|
op.ColorScale.ScaleAlpha(alpha)
|
||||||
r.DrawImage(transitionTo, op)
|
r.DrawImage(transitionTo, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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)
|
// 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.
|
// which is precisely what an alpha blend with alpha 1/(n+1) does.
|
||||||
layers++
|
layers++
|
||||||
op.ColorM.Scale(1, 1, 1, 1.0/float64(layers))
|
op.ColorScale.ScaleAlpha(1 / float32(layers))
|
||||||
screen.DrawImage(gophersImage, op)
|
screen.DrawImage(gophersImage, op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -96,7 +96,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
screen.Fill(color.Black)
|
screen.Fill(color.Black)
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
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) {
|
g.space.EachBody(func(body *cp.Body) {
|
||||||
op.GeoM.Reset()
|
op.GeoM.Reset()
|
||||||
|
@ -77,10 +77,10 @@ func (s *Sprite) MoveBy(x, y int) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Draw draws the sprite.
|
// 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 := &ebiten.DrawImageOptions{}
|
||||||
op.GeoM.Translate(float64(s.x+dx), float64(s.y+dy))
|
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)
|
||||||
screen.DrawImage(s.image, op)
|
screen.DrawImage(s.image, op)
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -66,20 +67,21 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
|
|
||||||
// Fill with solid colors
|
// Fill with solid colors
|
||||||
for i, c := range colors {
|
for i, c := range colors {
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &colorm.DrawImageOptions{}
|
||||||
x := i % 4
|
x := i % 4
|
||||||
y := i/4 + 1
|
y := i/4 + 1
|
||||||
op.GeoM.Translate(ox+float64(dx*x), oy+float64(dy*y))
|
op.GeoM.Translate(ox+float64(dx*x), oy+float64(dy*y))
|
||||||
|
|
||||||
// Reset RGB (not Alpha) 0 forcibly
|
// 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
|
// Set color
|
||||||
r := float64(c.R) / 0xff
|
r := float64(c.R) / 0xff
|
||||||
g := float64(c.G) / 0xff
|
g := float64(c.G) / 0xff
|
||||||
b := float64(c.B) / 0xff
|
b := float64(c.B) / 0xff
|
||||||
op.ColorM.Translate(r, g, b, 0)
|
cm.Translate(r, g, b, 0)
|
||||||
screen.DrawImage(ebitenImage, op)
|
colorm.DrawImage(screen, ebitenImage, cm, op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
||||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||||
@ -100,7 +101,7 @@ func (g *Game) Update() error {
|
|||||||
func (g *Game) Draw(screen *ebiten.Image) {
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
// Center the image on the screen.
|
// Center the image on the screen.
|
||||||
w, h := gophersImage.Size()
|
w, h := gophersImage.Size()
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &colorm.DrawImageOptions{}
|
||||||
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
|
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
|
||||||
op.GeoM.Scale(2, 2)
|
op.GeoM.Scale(2, 2)
|
||||||
op.GeoM.Translate(float64(screenWidth)/2, float64(screenHeight)/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
|
hue := float64(g.hue128) * 2 * math.Pi / 128
|
||||||
saturation := float64(g.saturation128) / 128
|
saturation := float64(g.saturation128) / 128
|
||||||
value := float64(g.value128) / 128
|
value := float64(g.value128) / 128
|
||||||
op.ColorM.ChangeHSV(hue, saturation, value)
|
var c colorm.ColorM
|
||||||
|
c.ChangeHSV(hue, saturation, value)
|
||||||
|
|
||||||
// Invert the color.
|
// Invert the color.
|
||||||
if g.inverted {
|
if g.inverted {
|
||||||
op.ColorM.Scale(-1, -1, -1, 1)
|
c.Scale(-1, -1, -1, 1)
|
||||||
op.ColorM.Translate(1, 1, 1, 0)
|
c.Translate(1, 1, 1, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
screen.DrawImage(gophersImage, op)
|
colorm.DrawImage(screen, gophersImage, c, op)
|
||||||
|
|
||||||
// Draw the text of the current status.
|
// Draw the text of the current status.
|
||||||
msgInverted := "false"
|
msgInverted := "false"
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
"github.com/hajimehoshi/ebiten/v2/examples/resources/images"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -46,15 +47,15 @@ func (g *Game) Update() error {
|
|||||||
func (g *Game) Draw(screen *ebiten.Image) {
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
// Center the image on the screen.
|
// Center the image on the screen.
|
||||||
w, h := gophersImage.Size()
|
w, h := gophersImage.Size()
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &colorm.DrawImageOptions{}
|
||||||
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
|
op.GeoM.Translate(-float64(w)/2, -float64(h)/2)
|
||||||
op.GeoM.Scale(2, 2)
|
op.GeoM.Scale(2, 2)
|
||||||
op.GeoM.Translate(float64(screenWidth)/2, float64(screenHeight)/2)
|
op.GeoM.Translate(float64(screenWidth)/2, float64(screenHeight)/2)
|
||||||
|
|
||||||
// Rotate the hue.
|
// Rotate the hue.
|
||||||
op.ColorM.RotateHue(float64(g.count%360) * 2 * math.Pi / 360)
|
var c colorm.ColorM
|
||||||
|
c.RotateHue(float64(g.count%360) * 2 * math.Pi / 360)
|
||||||
screen.DrawImage(gophersImage, op)
|
colorm.DrawImage(screen, gophersImage, c, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
||||||
|
@ -62,7 +62,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
// Draw the base (grayed) keyboard image.
|
// Draw the base (grayed) keyboard image.
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.GeoM.Translate(offsetX, offsetY)
|
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)
|
screen.DrawImage(keyboardImage, op)
|
||||||
|
|
||||||
// Draw the highlighted keys.
|
// Draw the highlighted keys.
|
||||||
|
@ -22,6 +22,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten/v2"
|
"github.com/hajimehoshi/ebiten/v2"
|
||||||
|
"github.com/hajimehoshi/ebiten/v2/colorm"
|
||||||
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
|
"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).
|
// paint draws the brush on the given canvas image at the position (x, y).
|
||||||
func (g *Game) paint(canvas *ebiten.Image, x, y int) {
|
func (g *Game) paint(canvas *ebiten.Image, x, y int) {
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &colorm.DrawImageOptions{}
|
||||||
op.GeoM.Translate(float64(x), float64(y))
|
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.
|
// 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()
|
tps := ebiten.TPS()
|
||||||
theta := 2.0 * math.Pi * float64(g.count%tps) / float64(tps)
|
theta := 2.0 * math.Pi * float64(g.count%tps) / float64(tps)
|
||||||
op.ColorM.RotateHue(theta)
|
cm.RotateHue(theta)
|
||||||
canvas.DrawImage(brushImage, op)
|
colorm.DrawImage(canvas, brushImage, cm, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (g *Game) Draw(screen *ebiten.Image) {
|
func (g *Game) Draw(screen *ebiten.Image) {
|
||||||
|
@ -59,7 +59,7 @@ type sprite struct {
|
|||||||
img *ebiten.Image
|
img *ebiten.Image
|
||||||
scale float64
|
scale float64
|
||||||
angle float64
|
angle float64
|
||||||
alpha float64
|
alpha float32
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *sprite) update() {
|
func (s *sprite) update() {
|
||||||
@ -94,8 +94,8 @@ func (s *sprite) draw(screen *ebiten.Image) {
|
|||||||
op.GeoM.Translate(x, y)
|
op.GeoM.Translate(x, y)
|
||||||
op.GeoM.Translate(ox, oy)
|
op.GeoM.Translate(ox, oy)
|
||||||
|
|
||||||
rate := float64(s.count) / float64(s.maxCount)
|
rate := float32(s.count) / float32(s.maxCount)
|
||||||
alpha := 0.0
|
var alpha float32
|
||||||
if rate < 0.2 {
|
if rate < 0.2 {
|
||||||
alpha = rate / 0.2
|
alpha = rate / 0.2
|
||||||
} else if rate > 0.8 {
|
} else if rate > 0.8 {
|
||||||
@ -104,7 +104,7 @@ func (s *sprite) draw(screen *ebiten.Image) {
|
|||||||
alpha = 1
|
alpha = 1
|
||||||
}
|
}
|
||||||
alpha *= s.alpha
|
alpha *= s.alpha
|
||||||
op.ColorM.Scale(1, 1, 1, alpha)
|
op.ColorScale.ScaleAlpha(alpha)
|
||||||
|
|
||||||
screen.DrawImage(s.img, op)
|
screen.DrawImage(s.img, op)
|
||||||
}
|
}
|
||||||
|
@ -253,7 +253,7 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
|
|
||||||
// Draw shadow
|
// Draw shadow
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.ColorM.Scale(1, 1, 1, 0.7)
|
op.ColorScale.ScaleAlpha(0.7)
|
||||||
screen.DrawImage(shadowImage, op)
|
screen.DrawImage(shadowImage, op)
|
||||||
|
|
||||||
// Draw walls
|
// Draw walls
|
||||||
|
@ -50,7 +50,7 @@ func init() {
|
|||||||
ebitenImage = ebiten.NewImage(w, h)
|
ebitenImage = ebiten.NewImage(w, h)
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.ColorM.Scale(1, 1, 1, 0.5)
|
op.ColorScale.ScaleAlpha(0.5)
|
||||||
ebitenImage.DrawImage(origEbitenImage, op)
|
ebitenImage.DrawImage(origEbitenImage, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +51,7 @@ func init() {
|
|||||||
ebitenImage = ebiten.NewImage(w, h)
|
ebitenImage = ebiten.NewImage(w, h)
|
||||||
|
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.ColorM.Scale(1, 1, 1, 0.5)
|
op.ColorScale.ScaleAlpha(0.5)
|
||||||
ebitenImage.DrawImage(origEbitenImage, op)
|
ebitenImage.DrawImage(origEbitenImage, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,20 +127,20 @@ func (g *Game) Draw(screen *ebiten.Image) {
|
|||||||
op.GeoM.Reset()
|
op.GeoM.Reset()
|
||||||
op.GeoM.Translate(x, y)
|
op.GeoM.Translate(x, y)
|
||||||
op.GeoM.Translate(gl.X, gl.Y)
|
op.GeoM.Translate(gl.X, gl.Y)
|
||||||
op.ColorM.Reset()
|
op.ColorScale.Reset()
|
||||||
r := 1.0
|
r := float32(1)
|
||||||
if i%3 == 0 {
|
if i%3 == 0 {
|
||||||
r = 0.5
|
r = 0.5
|
||||||
}
|
}
|
||||||
g := 1.0
|
g := float32(1)
|
||||||
if i%3 == 1 {
|
if i%3 == 1 {
|
||||||
g = 0.5
|
g = 0.5
|
||||||
}
|
}
|
||||||
b := 1.0
|
b := float32(1)
|
||||||
if i%3 == 2 {
|
if i%3 == 2 {
|
||||||
b = 0.5
|
b = 0.5
|
||||||
}
|
}
|
||||||
op.ColorM.Scale(r, g, b, 1)
|
op.ColorScale.Scale(r, g, b, 1)
|
||||||
screen.DrawImage(gl.Image, op)
|
screen.DrawImage(gl.Image, op)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
12
image.go
12
image.go
@ -102,8 +102,16 @@ type DrawImageOptions struct {
|
|||||||
// The default (zero) value is identity, which draws the image at (0, 0).
|
// The default (zero) value is identity, which draws the image at (0, 0).
|
||||||
GeoM GeoM
|
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.
|
// ColorM is a color matrix to draw.
|
||||||
// The default (zero) value is identity, which doesn't change any color.
|
// 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
|
ColorM ColorM
|
||||||
|
|
||||||
// CompositeMode is a composite mode to draw.
|
// 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)
|
sx0, sy0 := img.adjustPosition(bounds.Min.X, bounds.Min.Y)
|
||||||
sx1, sy1 := img.adjustPosition(bounds.Max.X, bounds.Max.Y)
|
sx1, sy1 := img.adjustPosition(bounds.Max.X, bounds.Max.Y)
|
||||||
colorm, cr, cg, cb, ca := colorMToScale(options.ColorM.affineColorM())
|
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)
|
vs := graphics.QuadVertices(float32(sx0), float32(sy0), float32(sx1), float32(sy1), a, b, c, d, tx, ty, cr, cg, cb, ca)
|
||||||
is := graphics.QuadIndices()
|
is := graphics.QuadIndices()
|
||||||
|
|
||||||
@ -324,9 +333,12 @@ type DrawTrianglesOptions struct {
|
|||||||
// ColorM is a color matrix to draw.
|
// ColorM is a color matrix to draw.
|
||||||
// The default (zero) value is identity, which doesn't change any color.
|
// The default (zero) value is identity, which doesn't change any color.
|
||||||
// ColorM is applied before vertex color scale is applied.
|
// ColorM is applied before vertex color scale is applied.
|
||||||
|
//
|
||||||
|
// Deprecated: as of v2.5. Use the package colorm instead.
|
||||||
ColorM ColorM
|
ColorM ColorM
|
||||||
|
|
||||||
// ColorScaleMode is the mode of color scales in vertices.
|
// 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.
|
// The default (zero) value is ColorScaleModeStraightAlpha.
|
||||||
ColorScaleMode ColorScaleMode
|
ColorScaleMode ColorScaleMode
|
||||||
|
|
||||||
|
@ -1009,12 +1009,12 @@ func TestImageMipmapColor(t *testing.T) {
|
|||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.Filter = ebiten.FilterLinear
|
op.Filter = ebiten.FilterLinear
|
||||||
op.GeoM.Scale(s, s)
|
op.GeoM.Scale(s, s)
|
||||||
op.ColorM.Scale(1, 1, 0, 1)
|
op.ColorScale.Scale(1, 1, 0, 1)
|
||||||
img0.DrawImage(img1, op)
|
img0.DrawImage(img1, op)
|
||||||
|
|
||||||
op.GeoM.Translate(128, 0)
|
op.GeoM.Translate(128, 0)
|
||||||
op.ColorM.Reset()
|
op.ColorScale.Reset()
|
||||||
op.ColorM.Scale(0, 1, 1, 1)
|
op.ColorScale.Scale(0, 1, 1, 1)
|
||||||
img0.DrawImage(img1, op)
|
img0.DrawImage(img1, op)
|
||||||
|
|
||||||
want := color.RGBA{0, 0xff, 0xff, 0xff}
|
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
|
// Issue #2428
|
||||||
func TestImageSetAndSubImage(t *testing.T) {
|
func TestImageSetAndSubImage(t *testing.T) {
|
||||||
const w, h = 16, 16
|
const w, h = 16, 16
|
||||||
|
@ -181,7 +181,7 @@ var textM sync.Mutex
|
|||||||
func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) {
|
func Draw(dst *ebiten.Image, text string, face font.Face, x, y int, clr color.Color) {
|
||||||
op := &ebiten.DrawImageOptions{}
|
op := &ebiten.DrawImageOptions{}
|
||||||
op.GeoM.Translate(float64(x), float64(y))
|
op.GeoM.Translate(float64(x), float64(y))
|
||||||
op.ColorM.ScaleWithColor(clr)
|
op.ColorScale.ScaleWithColor(clr)
|
||||||
DrawWithOptions(dst, text, face, op)
|
DrawWithOptions(dst, text, face, op)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user