main.go 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352
  1. // Copyright 2019 The Ebiten Authors
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. // This demo is inspired by the xscreensaver 'squirals'.
  15. package main
  16. import (
  17. "fmt"
  18. "image/color"
  19. "log"
  20. "math/rand/v2"
  21. "github.com/hajimehoshi/ebiten/v2"
  22. "github.com/hajimehoshi/ebiten/v2/ebitenutil"
  23. "github.com/hajimehoshi/ebiten/v2/inpututil"
  24. )
  25. const (
  26. width = 800
  27. height = 600
  28. scale = 1
  29. numOfSquirals = width / 32
  30. )
  31. type palette struct {
  32. name string
  33. colors []color.Color
  34. }
  35. var (
  36. background = color.Black
  37. palettes = []palette{
  38. {
  39. name: "sand dunes",
  40. colors: []color.Color{
  41. color.RGBA{0xF2, 0x74, 0x05, 0xFF}, // #F27405
  42. color.RGBA{0xD9, 0x52, 0x04, 0xFF}, // #D95204
  43. color.RGBA{0x40, 0x18, 0x01, 0xFF}, // #401801
  44. color.RGBA{0xA6, 0x2F, 0x03, 0xFF}, // #A62F03
  45. color.RGBA{0x73, 0x2A, 0x19, 0xFF}, // #732A19
  46. },
  47. },
  48. {
  49. name: "mono desert sand",
  50. colors: []color.Color{
  51. color.RGBA{0x7F, 0x6C, 0x52, 0xFF}, // #7F6C52
  52. color.RGBA{0xFF, 0xBA, 0x58, 0xFF}, // #FFBA58
  53. color.RGBA{0xFF, 0xD9, 0xA5, 0xFF}, // #FFD9A5
  54. color.RGBA{0x7F, 0x50, 0x0F, 0xFF}, // #7F500F
  55. color.RGBA{0xCC, 0xAE, 0x84, 0xFF}, // #CCAE84
  56. },
  57. },
  58. {
  59. name: "land sea gradient",
  60. colors: []color.Color{
  61. color.RGBA{0x00, 0xA2, 0xE8, 0xFF}, // #00A2E8
  62. color.RGBA{0x67, 0xA3, 0xF5, 0xFF}, // #67A3F5
  63. color.RGBA{0xFF, 0xFF, 0xD5, 0xFF}, // #FFFFD5
  64. color.RGBA{0xDD, 0xE8, 0x0C, 0xFF}, // #DDE80C
  65. color.RGBA{0x74, 0x9A, 0x0D, 0xFF}, // #749A0D
  66. },
  67. },
  68. }
  69. // blocker is an arbitrary color used to prevent the
  70. // squirals from leaving the canvas.
  71. blocker = color.RGBA{0, 0, 0, 254}
  72. // dirCycles defines by offset which direction a squiral
  73. // should try next for the two cases:
  74. // clockwise:
  75. // 1. try to turn right (index+1)
  76. // 2. try to go straight (index+0)
  77. // 3. try to turn left (index+3)
  78. // counter-clockwise:
  79. // 1. try to turn left (index+3)
  80. // 2. try to go straight (index+0)
  81. // 3. try to turn right (index+1)
  82. dirCycles = [2][3]int{
  83. {1, 0, 3}, // cw
  84. {3, 0, 1}, // ccw
  85. }
  86. // dirs contains vectors for the directions: east, south, west, north
  87. // in the specified order.
  88. dirs = [4]vec2{{1, 0}, {0, 1}, {-1, 0}, {0, -1}}
  89. // neighbors defines neighboring cells depending on the moving
  90. // direction of the squiral:
  91. // index of 0 -> squiral moves vertically,
  92. // index of 1 -> squiral moves horizontally.
  93. // These neighbors are tested for "collisions" during simulation.
  94. neighbors = [2][2]vec2{
  95. {{0, 1}, {0, -1}}, // east, west
  96. {{1, 0}, {-1, 0}}, // south, north
  97. }
  98. )
  99. type vec2 struct {
  100. x int
  101. y int
  102. }
  103. type squiral struct {
  104. speed int
  105. pos vec2
  106. dir int
  107. rot int
  108. col color.Color
  109. dead bool
  110. }
  111. func (s *squiral) spawn(game *Game) {
  112. s.dead = false
  113. rx := rand.IntN(width-4) + 2
  114. ry := rand.IntN(height-4) + 2
  115. for dx := -2; dx <= 2; dx++ {
  116. for dy := -2; dy <= 2; dy++ {
  117. tx, ty := rx+dx, ry+dy
  118. if game.auto.colorMap[tx][ty] != background {
  119. s.dead = true
  120. return
  121. }
  122. }
  123. }
  124. s.speed = rand.IntN(5) + 1
  125. s.pos.x = rx
  126. s.pos.y = ry
  127. s.dir = rand.IntN(4)
  128. game.colorCycle = (game.colorCycle + 1) % len(palettes[game.selectedPalette].colors)
  129. s.col = palettes[game.selectedPalette].colors[game.colorCycle]
  130. s.rot = rand.IntN(2)
  131. }
  132. func (s *squiral) step(game *Game) {
  133. if s.dead {
  134. return
  135. }
  136. x, y := s.pos.x, s.pos.y // shorthands
  137. change := rand.IntN(1000)
  138. if change < 2 {
  139. // On 0.2% of iterations, switch rotation direction.
  140. s.rot = (s.rot + 1) % 2
  141. }
  142. // 1. try to advance the spiral in its rotation
  143. // direction (clockwise or counter-clockwise).
  144. for _, next := range dirCycles[s.rot] {
  145. dir := (s.dir + next) % 4
  146. off := dirs[dir]
  147. // Peek all targets by priority.
  148. target := vec2{
  149. x: x + off.x,
  150. y: y + off.y,
  151. }
  152. if game.auto.colorMap[target.x][target.y] == background {
  153. // If the target is free we need to also check the
  154. // surrounding cells.
  155. // a. Test if next cell in direction dir does not have
  156. // the same color as this squiral.
  157. ntarg := vec2{
  158. x: target.x + off.x,
  159. y: target.y + off.y,
  160. }
  161. if game.auto.colorMap[ntarg.x][ntarg.y] == s.col {
  162. // If this has the same color, we cannot go into this direction,
  163. // to avoid ugly blocks of equal color.
  164. continue // try next direction
  165. }
  166. // b. Test all outer fields for the color of the
  167. // squiral itself.
  168. horivert := dir % 2
  169. xtarg := vec2{}
  170. set := true
  171. for _, out := range neighbors[horivert] {
  172. xtarg.x = target.x + out.x
  173. xtarg.y = target.y + out.y
  174. // If one of the outer targets equals the squiral's
  175. // color, again continue with next direction.
  176. if game.auto.colorMap[xtarg.x][xtarg.y] == s.col {
  177. // If this is not free we cannot go into this direction.
  178. set = false
  179. break // try next direction
  180. }
  181. xtarg.x = ntarg.x + out.x
  182. xtarg.y = ntarg.y + out.y
  183. // If one of the outer targets equals the squiral's
  184. // color, again continue with next direction.
  185. if game.auto.colorMap[xtarg.x][xtarg.y] == s.col {
  186. // If this is not free we cannot go into this direction.
  187. set = false
  188. break // try next direction
  189. }
  190. }
  191. if set {
  192. s.pos = target
  193. s.dir = dir
  194. // 2. set the color of this squiral to its
  195. // current position.
  196. game.setpix(s.pos, s.col)
  197. return
  198. }
  199. }
  200. }
  201. s.dead = true
  202. }
  203. type automaton struct {
  204. squirals [numOfSquirals]squiral
  205. colorMap [width][height]color.Color
  206. }
  207. func (au *automaton) init(game *Game) {
  208. // Init the test grid with color (0,0,0,0) and the borders of
  209. // it with color(0,0,0,254) as a blocker color, so the squirals
  210. // cannot escape the scene.
  211. for x := 0; x < width; x++ {
  212. for y := 0; y < height; y++ {
  213. if x == 0 || x == width-1 || y == 0 || y == height-1 {
  214. au.colorMap[x][y] = blocker
  215. } else {
  216. au.colorMap[x][y] = background
  217. }
  218. }
  219. }
  220. for i := 0; i < numOfSquirals; i++ {
  221. au.squirals[i].spawn(game)
  222. }
  223. }
  224. func (a *automaton) step(game *Game) {
  225. for i := 0; i < numOfSquirals; i++ {
  226. for s := 0; s < a.squirals[i].speed; s++ {
  227. a.squirals[i].step(game)
  228. if a.squirals[i].dead {
  229. a.squirals[i].spawn(game)
  230. }
  231. }
  232. }
  233. }
  234. type Game struct {
  235. selectedPalette int
  236. colorCycle int
  237. canvas *ebiten.Image
  238. auto automaton
  239. }
  240. func NewGame() *Game {
  241. g := &Game{
  242. canvas: ebiten.NewImage(width, height),
  243. }
  244. g.canvas.Fill(background)
  245. g.auto.init(g)
  246. return g
  247. }
  248. func (g *Game) setpix(xy vec2, col color.Color) {
  249. g.canvas.Set(xy.x, xy.y, col)
  250. g.auto.colorMap[xy.x][xy.y] = col
  251. }
  252. func (g *Game) Update() error {
  253. reset := false
  254. if inpututil.IsKeyJustPressed(ebiten.KeyB) {
  255. if background == color.White {
  256. background = color.Black
  257. } else {
  258. background = color.White
  259. }
  260. reset = true
  261. } else if inpututil.IsKeyJustPressed(ebiten.KeyT) {
  262. g.selectedPalette = (g.selectedPalette + 1) % len(palettes)
  263. reset = true
  264. } else if inpututil.IsKeyJustPressed(ebiten.KeyR) {
  265. reset = true
  266. }
  267. if reset {
  268. g.canvas.Fill(background)
  269. g.auto.init(g)
  270. }
  271. g.auto.step(g)
  272. return nil
  273. }
  274. func (g *Game) Draw(screen *ebiten.Image) {
  275. screen.DrawImage(g.canvas, nil)
  276. ebitenutil.DebugPrintAt(
  277. screen,
  278. fmt.Sprintf("TPS: %0.2f, FPS: %0.2f", ebiten.ActualTPS(), ebiten.ActualFPS()),
  279. 1, 0,
  280. )
  281. ebitenutil.DebugPrintAt(
  282. screen,
  283. "[r]: respawn",
  284. 1, 16,
  285. )
  286. ebitenutil.DebugPrintAt(
  287. screen,
  288. "[b]: toggle background (white/black)",
  289. 1, 32,
  290. )
  291. ebitenutil.DebugPrintAt(
  292. screen,
  293. fmt.Sprintf("[t]: cycle theme (current: %s)", palettes[g.selectedPalette].name),
  294. 1, 48,
  295. )
  296. }
  297. func (g *Game) Layout(outsideWidth, outsideHeight int) (int, int) {
  298. return width, height
  299. }
  300. func main() {
  301. ebiten.SetTPS(250)
  302. ebiten.SetWindowSize(width*scale, height*scale)
  303. ebiten.SetWindowTitle("Squirals (Ebitengine Demo)")
  304. if err := ebiten.RunGame(NewGame()); err != nil {
  305. log.Fatal(err)
  306. }
  307. }