diff --git a/.gitignore b/.gitignore index 4392f62e4..27ed88c2d 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ *~ *.aar *.apk +.vscode diff --git a/examples/squiral/main.go b/examples/squiral/main.go new file mode 100644 index 000000000..d4e14d6d3 --- /dev/null +++ b/examples/squiral/main.go @@ -0,0 +1,351 @@ +// 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. + +// +build example jsgo + +// This demo is inspired by the xscreensaver 'squirals'. + +package main + +import ( + "fmt" + "image/color" + "math/rand" + "time" + + "github.com/hajimehoshi/ebiten" + "github.com/hajimehoshi/ebiten/ebitenutil" + "github.com/hajimehoshi/ebiten/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 + }, + }, + } + selectedPalette = 0 + colorCycle = 0 + canvas *ebiten.Image + auto *automaton + // 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{ + [3]int{1, 0, 3}, // cw + [3]int{3, 0, 1}, // ccw + } + + // dirs contains vectors for the directions: east, south, west, north + // in the specified order. + dirs = [4]vec2{vec2{1, 0}, vec2{0, 1}, vec2{-1, 0}, vec2{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{ + [2]vec2{vec2{0, 1}, vec2{0, -1}}, // east, west + [2]vec2{vec2{1, 0}, vec2{-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() { + 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 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) + + colorCycle = (colorCycle + 1) % len(palettes[selectedPalette].colors) + s.col = palettes[selectedPalette].colors[colorCycle] + + s.rot = rand.Intn(2) +} + +func (s *squiral) step(debug int) { + 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 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 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 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 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. + setpix(s.pos, s.col) + return + } + } + } + + s.dead = true +} + +type automaton struct { + squirals [numOfSquirals]squiral + colorMap [width][height]color.Color +} + +func (au *automaton) init() { + // 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 { + auto.colorMap[x][y] = blocker + } else { + auto.colorMap[x][y] = background + } + } + } + + for i := 0; i < numOfSquirals; i++ { + auto.squirals[i].spawn() + } +} + +func (au *automaton) step() { + for i := 0; i < numOfSquirals; i++ { + for s := 0; s < au.squirals[i].speed; s++ { + au.squirals[i].step(i) + if au.squirals[i].dead { + au.squirals[i].spawn() + } + } + } +} + +func setpix(xy vec2, col color.Color) { + canvas.Set(xy.x, xy.y, col) + auto.colorMap[xy.x][xy.y] = col +} + +func init() { + rand.Seed(time.Now().UnixNano()) + c, _ := ebiten.NewImage(width, height, ebiten.FilterDefault) + canvas = c + canvas.Fill(background) + + auto = &automaton{} + auto.init() +} + +func update(screen *ebiten.Image) 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) { + selectedPalette = (selectedPalette + 1) % len(palettes) + reset = true + } else if inpututil.IsKeyJustPressed(ebiten.KeyR) { + reset = true + } + + if reset { + canvas.Fill(background) + auto.init() + } + + auto.step() + + if ebiten.IsDrawingSkipped() { + return nil + } + + draw(screen) + + return nil +} + +func draw(screen *ebiten.Image) { + screen.DrawImage(canvas, nil) + ebitenutil.DebugPrintAt( + screen, + fmt.Sprintf("TPS: %0.2f, FPS: %0.2f", ebiten.CurrentTPS(), ebiten.CurrentFPS()), + 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[selectedPalette].name), + 1, 48, + ) +} + +func main() { + ebiten.SetMaxTPS(250) + if err := ebiten.Run(update, width, height, scale, "Squirals"); err != nil { + panic(err) + } +}