main.go 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394
  1. package main
  2. import (
  3. "fmt"
  4. "image"
  5. "image/color"
  6. "log"
  7. "math"
  8. "math/rand"
  9. "os"
  10. "gioui.org/app"
  11. "gioui.org/font/gofont"
  12. "gioui.org/layout"
  13. "gioui.org/op"
  14. "gioui.org/op/clip"
  15. "gioui.org/op/paint"
  16. "gioui.org/text"
  17. "gioui.org/widget/material"
  18. )
  19. const pi Radians = 3.14159
  20. type C = layout.Context
  21. type D = layout.Dimensions
  22. var darkPalette = material.Palette{
  23. Bg: color.NRGBA{R: 0x28, G: 0x28, B: 0x28, A: 0xff},
  24. Fg: color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
  25. ContrastBg: color.NRGBA{R: 0x48, G: 0x85, B: 0x88, A: 0xff},
  26. ContrastFg: color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
  27. }
  28. func main() {
  29. go func() {
  30. window := new(app.Window)
  31. window.Option(app.Title("Physarum"))
  32. err := run(window)
  33. if err != nil {
  34. log.Fatal(err)
  35. }
  36. os.Exit(0)
  37. }()
  38. app.Main()
  39. }
  40. func run(window *app.Window) error {
  41. theme := material.NewTheme()
  42. theme.Palette = darkPalette
  43. theme.Shaper =
  44. text.NewShaper(text.WithCollection(gofont.Collection()))
  45. seed := int64(0)
  46. s := Simulation{}
  47. s.random = rand.New(rand.NewSource(seed))
  48. s.trails = s._trails[:]
  49. s.headings = s._headings[:]
  50. s.halfFoV = Radians(3 * pi / 4)
  51. s.focus = 2
  52. s.offset++
  53. s.move = 4
  54. s.turn = Radians(pi / 16)
  55. s.deposit = 128
  56. initialPos := GridId(524288) // GridId(len(s.grid) >> 1)
  57. initialHeading := Radians(0)
  58. for i := range s.headings {
  59. s.headings[i] = initialHeading
  60. s.trails[i*TrailLenMax] = initialPos
  61. }
  62. var ops op.Ops
  63. for {
  64. switch e := window.Event().(type) {
  65. case app.DestroyEvent:
  66. return e.Err
  67. case app.FrameEvent:
  68. gtx := app.NewContext(&ops, e)
  69. if s.grid == nil {
  70. /*squareMax := min(
  71. gtx.Constraints.Max.X,
  72. gtx.Constraints.Max.Y,
  73. )*/
  74. // Turn this into a power of 2 for fast modulo
  75. /*nearestPow2 := log2(uint32(squareMax))
  76. squareMax = int(1 << nearestPow2)
  77. s.grid = make([]uint8, squareMax<<nearestPow2)*/
  78. s.dimension = 1024
  79. s.dimLog2 = 10
  80. s.grid = make([]uint8, s.dimension<<s.dimLog2)
  81. }
  82. paint.Fill(&ops, theme.Bg)
  83. s.Step()
  84. s.Layout(gtx, theme)
  85. e.Source.Execute(op.InvalidateCmd{})
  86. e.Frame(gtx.Ops)
  87. }
  88. }
  89. }
  90. type GridId uint32
  91. type TrailId uint8
  92. type Radians float32
  93. const (
  94. AgentCount int = 128
  95. TrailLenMax int = 64
  96. )
  97. type Pos struct{ x, y int32 }
  98. func (p1 Pos) Add(p2 Pos) Pos {
  99. return Pos{p1.x + p2.x, p1.y + p2.y}
  100. }
  101. func (p1 Pos) Sub(p2 Pos) Pos {
  102. return Pos{p1.x - p2.x, p1.y - p2.y}
  103. }
  104. type Simulation struct {
  105. random *rand.Rand
  106. _trails [TrailLenMax * AgentCount]GridId
  107. _headings [AgentCount]Radians
  108. trails []GridId // Grid ids for agent trails
  109. headings []Radians // Agent headings
  110. grid []uint8 // Screen grid
  111. dimension int32 // Square dimension of client space
  112. dimLog2 uint32 // Log base-2 of dimension
  113. halfFoV Radians // Agent sensor angle
  114. focus float64 // Agent sensor distance
  115. move float64 // Agent move distance
  116. turn Radians // Agent turn angle
  117. offset uint8 // Index for trail circular buffers
  118. deposit uint8 // Agent deposit
  119. }
  120. func (s *Simulation) Step() {
  121. dxx := math.Cos(float64(s.halfFoV))
  122. dyx := math.Sin(float64(s.halfFoV))
  123. dxy := -dyx
  124. dyy := dxx
  125. for i, heading := range s.headings {
  126. // This may roll over, so we compute mod TrailLenMax.
  127. prevOffset := (int(s.offset) + TrailLenMax - 1) & (TrailLenMax - 1)
  128. nextOffset := (int(s.offset) + TrailLenMax) & (TrailLenMax - 1)
  129. prev := i*TrailLenMax + prevOffset
  130. next := i*TrailLenMax + nextOffset
  131. gridId := s.trails[prev]
  132. p0 := gridToPos(gridId, s.dimension, s.dimLog2)
  133. // Sense
  134. cx := s.focus * math.Cos(float64(heading))
  135. cy := s.focus * math.Sin(float64(heading))
  136. lx, ly := int32(cx*dxx+cy*dxy), int32(cy*dyy+cx*dyx)
  137. rx, ry := int32(cx*dxx-cy*dxy), int32(cy*dyy-cx*dyx)
  138. centerId :=
  139. posToGrid(
  140. Pos{p0.x + int32(cx), p0.y - int32(cy)},
  141. s.dimLog2,
  142. )
  143. leftId := posToGrid(Pos{p0.x + lx, p0.y - ly}, s.dimLog2)
  144. rightId := posToGrid(Pos{p0.x + rx, p0.y - ry}, s.dimLog2)
  145. var left, center, right uint8
  146. if 0 <= leftId && leftId <= GridId(len(s.grid)-1) {
  147. left = s.grid[leftId]
  148. }
  149. if 0 <= centerId && centerId <= GridId(len(s.grid)-1) {
  150. center = s.grid[centerId]
  151. }
  152. if 0 <= rightId && rightId <= GridId(len(s.grid)-1) {
  153. right = s.grid[rightId]
  154. }
  155. // Rotate
  156. switch {
  157. case left < center:
  158. if center <= right {
  159. heading -= s.turn // Steer right
  160. }
  161. // Otherwise, stay the course.
  162. case left >= center:
  163. if center > right {
  164. heading += s.turn // Steer left
  165. break
  166. }
  167. fallthrough
  168. default:
  169. if s.random.Float32() < 0.5 {
  170. heading += s.turn // Steer left
  171. } else {
  172. heading -= s.turn // Steer right
  173. }
  174. }
  175. s.headings[i] = heading
  176. // Move
  177. cos := math.Cos(float64(heading))
  178. sin := math.Sin(float64(heading))
  179. mx := s.move * cos
  180. my := s.move * sin
  181. p1 := Pos{
  182. int32(float64(p0.x) + mx),
  183. int32(float64(p0.y) + my),
  184. }
  185. // If we exceed the bounds of the grid, stop at the
  186. // boundary and change heading according to the angle of
  187. // incidence.
  188. gridMin := Pos{0, 0}
  189. gridMax := Pos{s.dimension - 1, s.dimension - 1}
  190. fromWallMin := p1.Sub(gridMin)
  191. fromWallMax := gridMax.Sub(p1)
  192. cnt := 0
  193. for !inBounds(p1, s.dimension) && cnt < 4 {
  194. cnt++
  195. if !inBounds(p1, s.dimension) && cnt >= 2 {
  196. fmt.Fprintf(os.Stderr, "\nstart at %v; out of bounds at %v\n", p0, p1)
  197. }
  198. if p1.x < gridMin.x { // Left wall
  199. s.headings[i] = pi - heading
  200. // Cosine cannot be zero when distance to wall is zero
  201. distToImpact := math.Abs(float64(fromWallMin.x) / cos)
  202. p1.x = gridMin.x // 0
  203. p1.y += int32(sin * distToImpact) // -1
  204. if inBounds(p1, s.dimension) {
  205. break
  206. }
  207. }
  208. if gridMax.x < p1.x { // Right wall
  209. s.headings[i] = pi - heading
  210. distToImpact := math.Abs(float64(fromWallMax.x) / cos)
  211. p1.x = gridMax.x
  212. p1.y += int32(sin * distToImpact)
  213. if inBounds(p1, s.dimension) {
  214. break
  215. }
  216. }
  217. if p1.y < gridMin.y { // Bottom wall
  218. s.headings[i] = 2*pi - heading
  219. distToImpact := math.Abs(float64(fromWallMin.y) / sin)
  220. p1.x += int32(cos * distToImpact)
  221. p1.y = gridMin.y
  222. if inBounds(p1, s.dimension) {
  223. break
  224. }
  225. }
  226. if gridMax.y < p1.y { // Top wall
  227. s.headings[i] = 2*pi - heading
  228. distToImpact := math.Abs(float64(fromWallMax.y) / sin)
  229. p1.x += int32(cos * distToImpact)
  230. p1.y = gridMax.y
  231. if inBounds(p1, s.dimension) {
  232. break
  233. }
  234. }
  235. }
  236. if !inBounds(p1, s.dimension) {
  237. fmt.Fprintf(os.Stderr, "\nstart at %v; out of bounds at %v\n", p0, p1)
  238. panic("blarg")
  239. }
  240. gridId = posToGrid(p1, s.dimLog2)
  241. s.trails[next] = gridId
  242. // Deposit and diffuse
  243. s.grid[gridId] = s.deposit
  244. s.diffuse(gridId)
  245. // Decay trail
  246. for j := 1; j < TrailLenMax; j++ {
  247. tid := (int(s.offset) + j) & (TrailLenMax - 1)
  248. s.grid[s.trails[tid]]--
  249. }
  250. }
  251. s.offset++
  252. s.offset &= uint8(TrailLenMax - 1)
  253. }
  254. func (s *Simulation) diffuse(id GridId) {
  255. value := s.grid[id] >> 4
  256. s.grid[id] = s.grid[id] >> 1
  257. p := gridToPos(id, s.dimension, s.dimLog2)
  258. for i := -1; i < 2; i++ {
  259. for j := -1; j < 2; j++ {
  260. bp := Pos{p.x + int32(j), p.y + int32(i)}
  261. if !inBounds(bp, s.dimension) {
  262. // For now, just send deposits that fall outside the
  263. // grid back to the center of the ball.
  264. s.grid[id] += value
  265. continue
  266. }
  267. bid := posToGrid(bp, s.dimLog2)
  268. // Protect against overflow.
  269. s.grid[bid] = max(s.grid[bid], s.grid[bid]+value)
  270. }
  271. }
  272. }
  273. func (s *Simulation) gridMean(id GridId) (mean uint8) {
  274. p := gridToPos(id, s.dimension, s.dimLog2)
  275. n := 0
  276. for i := -1; i < 2; i++ {
  277. for j := -1; j < 2; j++ {
  278. bp := Pos{p.x + int32(j), p.y + int32(i)}
  279. if !inBounds(bp, s.dimension) {
  280. continue
  281. }
  282. mean += s.grid[posToGrid(bp, s.dimLog2)]
  283. n++
  284. }
  285. }
  286. return uint8(float32(mean) / float32(n))
  287. }
  288. func (s *Simulation) Layout(
  289. gtx layout.Context,
  290. th *material.Theme,
  291. ) D {
  292. circle := clip.Ellipse{Max: image.Pt(8, 8)}
  293. // Outer margin
  294. defer op.Offset(image.Pt(10, 10)).Push(gtx.Ops).Pop()
  295. halfW, halfH := circle.Max.X>>1, circle.Max.Y>>1
  296. trailColor := th.Fg
  297. for _, gridId := range s.trails {
  298. if gridId == 0 {
  299. continue
  300. }
  301. trailColor.A = s.gridMean(gridId)
  302. p := gridToPos(gridId, s.dimension, s.dimLog2)
  303. pos := image.Pt(int(p.x)-halfW, int(s.dimension-1-p.y)+halfH)
  304. macro := op.Record(gtx.Ops)
  305. stack := op.Offset(pos).Push(gtx.Ops)
  306. paint.FillShape(gtx.Ops, trailColor, circle.Op(gtx.Ops))
  307. stack.Pop()
  308. c := macro.Stop()
  309. op.Defer(gtx.Ops, c)
  310. }
  311. return layout.Dimensions{
  312. Size: image.Pt(int(s.dimension), int(s.dimension)),
  313. }
  314. }
  315. func inBounds(p Pos, dim int32) bool {
  316. return -1 < p.x && p.x < dim &&
  317. -1 < p.y && p.y < dim
  318. }
  319. func posToGrid(p Pos, logOfDim uint32) GridId {
  320. id := uint32(p.y) << logOfDim
  321. id |= uint32(p.x)
  322. return GridId(id)
  323. }
  324. func gridToPos(id GridId, dim int32, logOfDim uint32) Pos {
  325. x := int32(id) & (dim - 1)
  326. y := int32(id) >> logOfDim
  327. return Pos{x: x, y: y}
  328. }
  329. func log2(x uint32) uint32 {
  330. b := []uint32{0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000}
  331. s := []uint32{1, 2, 4, 8, 16}
  332. var r uint32 = 0
  333. for i := 4; i >= 0; i-- {
  334. if x&b[i] != 0 {
  335. x >>= s[i]
  336. r |= s[i]
  337. }
  338. }
  339. return r
  340. }