From da54f19df5dcdbb7775d0d947be9993f477e4ad2 Mon Sep 17 00:00:00 2001 From: Artem Sedykh Date: Tue, 7 Feb 2023 05:59:49 +0300 Subject: [PATCH] add blend modes example (#2563) --- examples/blend/main.go | 217 +++++++++++++++++++++ examples/resources/images/blend/dest.png | Bin 0 -> 4957 bytes examples/resources/images/blend/embed.go | 27 +++ examples/resources/images/blend/source.png | Bin 0 -> 1992 bytes examples/resources/images/license.md | 16 ++ 5 files changed, 260 insertions(+) create mode 100644 examples/blend/main.go create mode 100644 examples/resources/images/blend/dest.png create mode 100644 examples/resources/images/blend/embed.go create mode 100644 examples/resources/images/blend/source.png diff --git a/examples/blend/main.go b/examples/blend/main.go new file mode 100644 index 000000000..86407bc9c --- /dev/null +++ b/examples/blend/main.go @@ -0,0 +1,217 @@ +// Copyright 2023 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 main + +import ( + "bytes" + "fmt" + "image" + "image/color" + _ "image/png" + "log" + + "golang.org/x/image/font/inconsolata" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/hajimehoshi/ebiten/v2/examples/resources/images/blend" + "github.com/hajimehoshi/ebiten/v2/text" +) + +const ( + screenWidth = 780 + screenHeight = 600 +) + +// mode is a blend mode with description. +type mode struct { + blend ebiten.Blend + name string +} + +// Game is a canvas for drawing blend mode tiles. +type Game struct { + source *ebiten.Image + dest *ebiten.Image + offscreen *ebiten.Image + tileSize int + modes []mode +} + +func NewGame() (*Game, error) { + source, err := loadImage(blend.Source_png) + if err != nil { + return nil, fmt.Errorf("fail to load source: %w", err) + } + dest, err := loadImage(blend.Dest_png) + if err != nil { + return nil, fmt.Errorf("fail to load dest: %w", err) + } + + // Setting up a grid for drawing. + g := &Game{ + source: source, + dest: dest, + } + + // Adding all known blend modes and their names. + g.modes = []mode{ + {blend: ebiten.BlendCopy, name: "BlendCopy"}, + {blend: ebiten.BlendSourceAtop, name: "BlendSourceAtop"}, + {blend: ebiten.BlendSourceOver, name: "BlendSourceOver"}, + {blend: ebiten.BlendSourceIn, name: "BlendSourceIn"}, + {blend: ebiten.BlendSourceOut, name: "BlendSourceOut"}, + {blend: ebiten.BlendDestination, name: "BlendDestination"}, + {blend: ebiten.BlendDestinationAtop, name: "BlendDestinationAtop"}, + {blend: ebiten.BlendDestinationOver, name: "BlendDestinationOver"}, + {blend: ebiten.BlendDestinationIn, name: "BlendDestinationIn"}, + {blend: ebiten.BlendDestinationOut, name: "BlendDestinationOut"}, + {blend: ebiten.BlendXor, name: "BlendXor"}, + {blend: ebiten.BlendLighter, name: "BlendLighter"}, + {blend: ebiten.BlendClear, name: "BlendClear"}, + } + + // Adjusting the tile size for the available images. + g.tileSize = maxSide(g.source, g.dest) + g.offscreen = ebiten.NewImage(int(g.tileSize), int(g.tileSize)) + + return g, nil +} + +func (g *Game) Update() error { + return nil +} + +func (g *Game) Draw(screen *ebiten.Image) { + const ( + tileSpacing = 64 + textSpacing = 16 + gridW = 4 + gridH = 3 + ) + + // Clearing the screen. + screen.Fill(color.White) + + // Get an offset for center alignment. + // Where sw, sh is the screen size in pixels. + // And gw, gh is the grid size in pixels. + totalTileSize := g.tileSize + tileSpacing + gw, gh := gridW*totalTileSize-tileSpacing, gridH*totalTileSize + sw, sh := screen.Bounds().Dx(), screen.Bounds().Dy() + cx, cy := float64(sw-gw)/2, float64(sh-gh)/2 + + // Drawing a tilemap + for y := 0; y < gridH; y++ { + for x := 0; x < gridW; x++ { + px, py := x*totalTileSize, y*totalTileSize + if y > 0 { + // Making a place for the text. + py += textSpacing * y + } + + // Getting the right mode for the coords or skipping the rendering. + m, ok := getMode(g.modes, x, y, gridW, gridH) + if !ok { + continue + } + + // Drawing the blend mode and it's name. + tileSize := float64(g.tileSize) + alignedX, alignedY := float64(px)+cx, float64(py)+cy + halfTileSize, spacing := tileSize/2, float64(textSpacing) + g.drawBlendMode(screen, alignedX, alignedY, m.blend) + drawCenteredText(screen, alignedX+halfTileSize, alignedY+tileSize+spacing, m.name) + } + } +} + +func (g *Game) Layout(w, h int) (int, int) { + return w, h +} + +func (g *Game) drawBlendMode(screen *ebiten.Image, x, y float64, mode ebiten.Blend) { + // Copy the destination image to offscreen so as not to modify it. + g.offscreen.Clear() + g.offscreen.DrawImage(g.dest, nil) + + // Select and apply blending mode. + op := &ebiten.DrawImageOptions{} + op.Blend = mode + g.offscreen.DrawImage(g.source, op) + + // Draw the result on the passed coordinates. + op = &ebiten.DrawImageOptions{} + op.GeoM.Translate(x, y) + screen.DrawImage(g.offscreen, op) +} + +// loadImage is a util function for loading embedded images. +func loadImage(data []byte) (*ebiten.Image, error) { + m, _, err := image.Decode(bytes.NewReader(data)) + if err != nil { + return nil, fmt.Errorf("failed to decode image: %w", err) + } + return ebiten.NewImageFromImage(m), nil +} + +// max returns the largest of x or y. +func max(x, y int) int { + if x > y { + return x + } + return y +} + +// maxSide returns the largest side of a or b images. +func maxSide(a, b *ebiten.Image) int { + return max( + max(a.Bounds().Dx(), b.Bounds().Dx()), + max(a.Bounds().Dy(), b.Bounds().Dy()), + ) +} + +// getMode returns a blend mode by index and found status. +func getMode(modes []mode, x, y, w, h int) (m mode, ok bool) { + idx := y*w + x + if idx > len(modes)-1 { + return mode{}, false + } + if x < 0 || x > w || y < 0 || y > h { + return mode{}, false + } + return modes[idx], true +} + +// drawCenteredText is a util function for drawing blend mode description. +func drawCenteredText(screen *ebiten.Image, cx, cy float64, s string) { + bounds := text.BoundString(inconsolata.Bold8x16, s) + x, y := int(cx)-bounds.Min.X-bounds.Dx()/2, int(cy)-bounds.Min.Y-bounds.Dy()/2 + text.Draw(screen, s, inconsolata.Bold8x16, x, y, color.Black) +} + +func main() { + ebiten.SetWindowSize(screenWidth, screenHeight) + ebiten.SetWindowResizingMode(ebiten.WindowResizingModeEnabled) + ebiten.SetWindowTitle("Blend modes (Ebitengine Demo)") + + game, err := NewGame() + if err != nil { + log.Fatal(err) + } + + if err = ebiten.RunGame(game); err != nil { + log.Fatal(err) + } +} diff --git a/examples/resources/images/blend/dest.png b/examples/resources/images/blend/dest.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf19929c4aac501c8363b62faed26c67ff6c3f6 GIT binary patch literal 4957 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 literal 0 HcmV?d00001 diff --git a/examples/resources/images/blend/embed.go b/examples/resources/images/blend/embed.go new file mode 100644 index 000000000..c8e6d7776 --- /dev/null +++ b/examples/resources/images/blend/embed.go @@ -0,0 +1,27 @@ +// Copyright 2023 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 blend + +import ( + _ "embed" +) + +var ( + //go:embed source.png + Source_png []byte + + //go:embed dest.png + Dest_png []byte +) diff --git a/examples/resources/images/blend/source.png b/examples/resources/images/blend/source.png new file mode 100644 index 0000000000000000000000000000000000000000..eb8ee23e4996902862bcf76492b25ec1bddce41d GIT binary patch literal 1992 zcmXX{dpOhW8-KnVW66jpQk$C5WRhV{`;8{dLTgIPVaj37b0~+Vw&-#wC9DIH*kt@7 zE@#Q)kd9JHX~`?zN~)K)w70zVe!ainb6xjyf9}t7-}mRfuIGLv!F5&G*=s1OTRg*&wkwW*tl#B)j=1?~V^o zP6<$Y}i{{~65|hJX4gAQ? z0H_f>=`QTlOCQU;V%UC0gU2E`dGw8h!L0VJQ8)sj^8R``kDQPx@^)K;@3iV1m68ta zN~<+fnaV7{k<^J>d^vdys@zkqZ!xkOmq_5rt~BPHz%`e>skE^Yw|+}_=zQkL(XNLI z`#*hdHe4!BS|M8j#Tt04aG-jHWUI@^Oj7UG!0I=(((of=ui+4uv&0cTEs9gFrE z`t%;F%g?6wJWc3MXK&Qc|E0{Q$F%c97VFitHW-ai_E!KMgNhM7=i{NDvr2uCrEgDR zV#|(k|7iL?f^aJs0y=bso+hs2Ph!etRokG8Pa8i@I0px@9D`_fxxX4ee6DpSNfb5% zCOwS){ZsKBvc-K)5^gIE!rRI}iOpZebtTX4pInS2Ia{u37%o05U~zu6v!|zMl4mUjhH>-Uaz)CA|ShX?(A|>_C;)HtU7Euey={k(Q7u z%0}Zqkvl6R3T~YJt)pIj`Ul%z?_)G3JXJtL$~5_jFK%4i9-HJ1*@X!PLyTvRuyPKy zlq>1gHi)}JUl!x4tkW^Qb@rFLRKK7%cUArUfby0jIu*K2E`O{#hDo_k(NxrMQRum= z10|eb0lha}v^o@%YWVe?D!1jXg7kHypW5Ni%`-NErG$Nr5I!y{Dx3MD25aF9Im~J)}v9k&f2Ibek z+llw7^EWp!7TwP!A{D2m@sF+?=w6wnvTf5{OuDT@)q3@Ec1zF>SHwk~FjNk<{$Y_t z0)n@@B^(1&+EVxKH|bvkb5of!CH83aG}+9CmNan7ky9mpgQvs=nHh^Nt)2-xvm@dQ zbX>1?%#ZGYGp(2afxGlPx8ELL0hdrX^r+FQGa{+OvpwE-bUiFkJW{ZdR>!iJ9E^=E zCW}T}ciPR4giIa{{17c+qb0V;{<5dp5^d~zP7h$5rA7Vi9_uO_^}FGXYcb>&vSf9s z7im#8aAY4Nc)ASdQW}Ifp zN-2dj{cxc|%1UW;5N#nh?q(AY>6z;exoTFlvu8>ACIu%V7C8)Q{Q(!mzILej9OKo1 zKO%B^)Ej~&H#>>Q2ZRVOX*~-j=M+J`Fga3AGqe_7fpvIZxcqX3jX&IS1}%w1ULc7( z^O{s1D_>-I!7U3NMdUbeZyDJyqw7uVS%QI%hxtlwtZXz;$FyM2TJT>>ap&D;w8R<3 zuYw0E!WTS?>1Zi$M+>6TG4ZPuYHZVqn0yEjuRY^Du~z2M4!d_hSYtX7Cx&V>Zex&_ zOUwK02)fuU)<9UWZ4R;GB4erXhedx8OO{X#(+UVkv_CM1CmlTdv0V>coh-!qJ6Q9* zkJrKM^7C5 zV$@pXC%CQvEtCz_w@^=#o-;Sh_a-92eFKAW%u-A`X5AfgDupZjOATeY=>;zx^U~KC zj)BmiuW!HTHFG#bqqbkoeIKJL;rzG8YOUipTNP)B^zi-l$R~H_Z}f zVSAntyBg~>4#_82vTIYB5st!qDXVlqZTK*F$&j|5)G9XsbDPDTaH~br$zbMrM&K%} zlLC^*$VN@(dv_x*5XGGYYHAcPyUCudR8BTRd0WJtL~3dbFso+IHYq1Npk%me4c5sH zyfYVn$5D&rz=0vXnWJU071UUWzvHRJ{h$_Rav8Ii)WS^(D-&qOncuB*wu?(INFMZquHzEUOAkm^o>B~Yg_Yf^gmjU=?EGiJM G