123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352 |
- // Copyright 2019 The Ebiten 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.
- // This demo is inspired by the xscreensaver 'squirals'.
- package main
- import (
- "fmt"
- "image/color"
- "log"
- "math/rand/v2"
- "github.com/hajimehoshi/ebiten/v2"
- "github.com/hajimehoshi/ebiten/v2/ebitenutil"
- "github.com/hajimehoshi/ebiten/v2/inpututil"
- )
- const (
- width = 800
- height = 600
- scale = 1
- numOfSquirals = width / 32
- )
- type palette struct {
- name string
- colors []color.Color
- }
- var (
- background = color.Black
- palettes = []palette{
- {
- name: "sand dunes",
- colors: []color.Color{
- color.RGBA{0xF2, 0x74, 0x05, 0xFF}, // #F27405
- color.RGBA{0xD9, 0x52, 0x04, 0xFF}, // #D95204
- color.RGBA{0x40, 0x18, 0x01, 0xFF}, // #401801
- color.RGBA{0xA6, 0x2F, 0x03, 0xFF}, // #A62F03
- color.RGBA{0x73, 0x2A, 0x19, 0xFF}, // #732A19
- },
- },
- {
- name: "mono desert sand",
- colors: []color.Color{
- color.RGBA{0x7F, 0x6C, 0x52, 0xFF}, // #7F6C52
- color.RGBA{0xFF, 0xBA, 0x58, 0xFF}, // #FFBA58
- color.RGBA{0xFF, 0xD9, 0xA5, 0xFF}, // #FFD9A5
- color.RGBA{0x7F, 0x50, 0x0F, 0xFF}, // #7F500F
- color.RGBA{0xCC, 0xAE, 0x84, 0xFF}, // #CCAE84
- },
- },
- {
- name: "land sea gradient",
- colors: []color.Color{
- color.RGBA{0x00, 0xA2, 0xE8, 0xFF}, // #00A2E8
- color.RGBA{0x67, 0xA3, 0xF5, 0xFF}, // #67A3F5
- color.RGBA{0xFF, 0xFF, 0xD5, 0xFF}, // #FFFFD5
- color.RGBA{0xDD, 0xE8, 0x0C, 0xFF}, // #DDE80C
- color.RGBA{0x74, 0x9A, 0x0D, 0xFF}, // #749A0D
- },
- },
- }
- // blocker is an arbitrary color used to prevent the
- // squirals from leaving the canvas.
- blocker = color.RGBA{0, 0, 0, 254}
- // dirCycles defines by offset which direction a squiral
- // should try next for the two cases:
- // clockwise:
- // 1. try to turn right (index+1)
- // 2. try to go straight (index+0)
- // 3. try to turn left (index+3)
- // counter-clockwise:
- // 1. try to turn left (index+3)
- // 2. try to go straight (index+0)
- // 3. try to turn right (index+1)
- dirCycles = [2][3]int{
- {1, 0, 3}, // cw
- {3, 0, 1}, // ccw
- }
- // dirs contains vectors for the directions: east, south, west, north
- // in the specified order.
- dirs = [4]vec2{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}
- // neighbors defines neighboring cells depending on the moving
- // direction of the squiral:
- // index of 0 -> squiral moves vertically,
- // index of 1 -> squiral moves horizontally.
- // These neighbors are tested for "collisions" during simulation.
- neighbors = [2][2]vec2{
- {{0, 1}, {0, -1}}, // east, west
- {{1, 0}, {-1, 0}}, // south, north
- }
- )
- type vec2 struct {
- x int
- y int
- }
- type squiral struct {
- speed int
- pos vec2
- dir int
- rot int
- col color.Color
- dead bool
- }
- func (s *squiral) spawn(game *Game) {
- s.dead = false
- rx := rand.IntN(width-4) + 2
- ry := rand.IntN(height-4) + 2
- for dx := -2; dx <= 2; dx++ {
- for dy := -2; dy <= 2; dy++ {
- tx, ty := rx+dx, ry+dy
- if game.auto.colorMap[tx][ty] != background {
- s.dead = true
- return
- }
- }
- }
- s.speed = rand.IntN(5) + 1
- s.pos.x = rx
- s.pos.y = ry
- s.dir = rand.IntN(4)
- game.colorCycle = (game.colorCycle + 1) % len(palettes[game.selectedPalette].colors)
- s.col = palettes[game.selectedPalette].colors[game.colorCycle]
- s.rot = rand.IntN(2)
- }
- func (s *squiral) step(game *Game) {
- if s.dead {
- return
- }
- x, y := s.pos.x, s.pos.y // shorthands
- change := rand.IntN(1000)
- if change < 2 {
- // On 0.2% of iterations, switch rotation direction.
- s.rot = (s.rot + 1) % 2
- }
- // 1. try to advance the spiral in its rotation
- // direction (clockwise or counter-clockwise).
- for _, next := range dirCycles[s.rot] {
- dir := (s.dir + next) % 4
- off := dirs[dir]
- // Peek all targets by priority.
- target := vec2{
- x: x + off.x,
- y: y + off.y,
- }
- if game.auto.colorMap[target.x][target.y] == background {
- // If the target is free we need to also check the
- // surrounding cells.
- // a. Test if next cell in direction dir does not have
- // the same color as this squiral.
- ntarg := vec2{
- x: target.x + off.x,
- y: target.y + off.y,
- }
- if game.auto.colorMap[ntarg.x][ntarg.y] == s.col {
- // If this has the same color, we cannot go into this direction,
- // to avoid ugly blocks of equal color.
- continue // try next direction
- }
- // b. Test all outer fields for the color of the
- // squiral itself.
- horivert := dir % 2
- xtarg := vec2{}
- set := true
- for _, out := range neighbors[horivert] {
- xtarg.x = target.x + out.x
- xtarg.y = target.y + out.y
- // If one of the outer targets equals the squiral's
- // color, again continue with next direction.
- if game.auto.colorMap[xtarg.x][xtarg.y] == s.col {
- // If this is not free we cannot go into this direction.
- set = false
- break // try next direction
- }
- xtarg.x = ntarg.x + out.x
- xtarg.y = ntarg.y + out.y
- // If one of the outer targets equals the squiral's
- // color, again continue with next direction.
- if game.auto.colorMap[xtarg.x][xtarg.y] == s.col {
- // If this is not free we cannot go into this direction.
- set = false
- break // try next direction
- }
- }
- if set {
- s.pos = target
- s.dir = dir
- // 2. set the color of this squiral to its
- // current position.
- game.setpix(s.pos, s.col)
- return
- }
- }
- }
- s.dead = true
- }
- type automaton struct {
- squirals [numOfSquirals]squiral
- colorMap [width][height]color.Color
- }
- func (au *automaton) init(game *Game) {
- // Init the test grid with color (0,0,0,0) and the borders of
- // it with color(0,0,0,254) as a blocker color, so the squirals
- // cannot escape the scene.
- for x := 0; x < width; x++ {
- for y := 0; y < height; y++ {
- if x == 0 || x == width-1 || y == 0 || y == height-1 {
- au.colorMap[x][y] = blocker
- } else {
- au.colorMap[x][y] = background
- }
- }
- }
- for i := 0; i < numOfSquirals; i++ {
- au.squirals[i].spawn(game)
- }
- }
- func (a *automaton) step(game *Game) {
- for i := 0; i < numOfSquirals; i++ {
- for s := 0; s < a.squirals[i].speed; s++ {
- a.squirals[i].step(game)
- if a.squirals[i].dead {
- a.squirals[i].spawn(game)
- }
- }
- }
- }
- type Game struct {
- selectedPalette int
- colorCycle int
- canvas *ebiten.Image
- auto automaton
- }
- func NewGame() *Game {
- g := &Game{
- canvas: ebiten.NewImage(width, height),
- }
- g.canvas.Fill(background)
- g.auto.init(g)
- return g
- }
- func (g *Game) setpix(xy vec2, col color.Color) {
- g.canvas.Set(xy.x, xy.y, col)
- g.auto.colorMap[xy.x][xy.y] = col
- }
- func (g *Game) Update() error {
- reset := false
- if inpututil.IsKeyJustPressed(ebiten.KeyB) {
- if background == color.White {
- background = color.Black
- } else {
- background = color.White
- }
- reset = true
- } else if inpututil.IsKeyJustPressed(ebiten.KeyT) {
- g.selectedPalette = (g.selectedPalette + 1) % len(palettes)
- reset = true
- } else if inpututil.IsKeyJustPressed(ebiten.KeyR) {
- reset = true
- }
- if reset {
- g.canvas.Fill(background)
- g.auto.init(g)
- }
- g.auto.step(g)
- return nil
- }
- func (g *Game) Draw(screen *ebiten.Image) {
- screen.DrawImage(g.canvas, nil)
- ebitenutil.DebugPrintAt(
- screen,
- fmt.Sprintf("TPS: %0.2f, FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()),
- 1, 0,
- )
- ebitenutil.DebugPrintAt(
- screen,
- "[r]: respawn",
- 1, 16,
- )
- ebitenutil.DebugPrintAt(
- screen,
- "[b]: toggle background (white/black)",
- 1, 32,
- )
- ebitenutil.DebugPrintAt(
- screen,
- fmt.Sprintf("[t]: cycle theme (current: %s)", palettes[g.selectedPalette].name),
- 1, 48,
- )
- }
- func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
- return width, height
- }
- func main() {
- ebiten.SetTPS(250)
- ebiten.SetWindowSize(width*scale, height*scale)
- ebiten.SetWindowTitle("Squirals (Ebitengine Demo)")
- if err := ebiten.RunGame(NewGame()); err != nil {
- log.Fatal(err)
- }
- }
|