ebiten/examples/squiral/main.go
2024-09-12 01:17:26 +09:00

353 lines
8.2 KiB
Go

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