123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394 |
- package main
- import (
- "fmt"
- "image"
- "image/color"
- "log"
- "math"
- "math/rand"
- "os"
- "gioui.org/app"
- "gioui.org/font/gofont"
- "gioui.org/layout"
- "gioui.org/op"
- "gioui.org/op/clip"
- "gioui.org/op/paint"
- "gioui.org/text"
- "gioui.org/widget/material"
- )
- const pi Radians = 3.14159
- type C = layout.Context
- type D = layout.Dimensions
- var darkPalette = material.Palette{
- Bg: color.NRGBA{R: 0x28, G: 0x28, B: 0x28, A: 0xff},
- Fg: color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
- ContrastBg: color.NRGBA{R: 0x48, G: 0x85, B: 0x88, A: 0xff},
- ContrastFg: color.NRGBA{R: 0xeb, G: 0xdb, B: 0xb2, A: 0xff},
- }
- func main() {
- go func() {
- window := new(app.Window)
- window.Option(app.Title("Physarum"))
- err := run(window)
- if err != nil {
- log.Fatal(err)
- }
- os.Exit(0)
- }()
- app.Main()
- }
- func run(window *app.Window) error {
- theme := material.NewTheme()
- theme.Palette = darkPalette
- theme.Shaper =
- text.NewShaper(text.WithCollection(gofont.Collection()))
- seed := int64(0)
- s := Simulation{}
- s.random = rand.New(rand.NewSource(seed))
- s.trails = s._trails[:]
- s.headings = s._headings[:]
- s.halfFoV = Radians(3 * pi / 4)
- s.focus = 2
- s.offset++
- s.move = 4
- s.turn = Radians(pi / 16)
- s.deposit = 128
- initialPos := GridId(524288) // GridId(len(s.grid) >> 1)
- initialHeading := Radians(0)
- for i := range s.headings {
- s.headings[i] = initialHeading
- s.trails[i*TrailLenMax] = initialPos
- }
- var ops op.Ops
- for {
- switch e := window.Event().(type) {
- case app.DestroyEvent:
- return e.Err
- case app.FrameEvent:
- gtx := app.NewContext(&ops, e)
- if s.grid == nil {
- /*squareMax := min(
- gtx.Constraints.Max.X,
- gtx.Constraints.Max.Y,
- )*/
- // Turn this into a power of 2 for fast modulo
- /*nearestPow2 := log2(uint32(squareMax))
- squareMax = int(1 << nearestPow2)
- s.grid = make([]uint8, squareMax<<nearestPow2)*/
- s.dimension = 1024
- s.dimLog2 = 10
- s.grid = make([]uint8, s.dimension<<s.dimLog2)
- }
- paint.Fill(&ops, theme.Bg)
- s.Step()
- s.Layout(gtx, theme)
- e.Source.Execute(op.InvalidateCmd{})
- e.Frame(gtx.Ops)
- }
- }
- }
- type GridId uint32
- type TrailId uint8
- type Radians float32
- const (
- AgentCount int = 128
- TrailLenMax int = 64
- )
- type Pos struct{ x, y int32 }
- func (p1 Pos) Add(p2 Pos) Pos {
- return Pos{p1.x + p2.x, p1.y + p2.y}
- }
- func (p1 Pos) Sub(p2 Pos) Pos {
- return Pos{p1.x - p2.x, p1.y - p2.y}
- }
- type Simulation struct {
- random *rand.Rand
- _trails [TrailLenMax * AgentCount]GridId
- _headings [AgentCount]Radians
- trails []GridId // Grid ids for agent trails
- headings []Radians // Agent headings
- grid []uint8 // Screen grid
- dimension int32 // Square dimension of client space
- dimLog2 uint32 // Log base-2 of dimension
- halfFoV Radians // Agent sensor angle
- focus float64 // Agent sensor distance
- move float64 // Agent move distance
- turn Radians // Agent turn angle
- offset uint8 // Index for trail circular buffers
- deposit uint8 // Agent deposit
- }
- func (s *Simulation) Step() {
- dxx := math.Cos(float64(s.halfFoV))
- dyx := math.Sin(float64(s.halfFoV))
- dxy := -dyx
- dyy := dxx
- for i, heading := range s.headings {
- // This may roll over, so we compute mod TrailLenMax.
- prevOffset := (int(s.offset) + TrailLenMax - 1) & (TrailLenMax - 1)
- nextOffset := (int(s.offset) + TrailLenMax) & (TrailLenMax - 1)
- prev := i*TrailLenMax + prevOffset
- next := i*TrailLenMax + nextOffset
- gridId := s.trails[prev]
- p0 := gridToPos(gridId, s.dimension, s.dimLog2)
- // Sense
- cx := s.focus * math.Cos(float64(heading))
- cy := s.focus * math.Sin(float64(heading))
- lx, ly := int32(cx*dxx+cy*dxy), int32(cy*dyy+cx*dyx)
- rx, ry := int32(cx*dxx-cy*dxy), int32(cy*dyy-cx*dyx)
- centerId :=
- posToGrid(
- Pos{p0.x + int32(cx), p0.y - int32(cy)},
- s.dimLog2,
- )
- leftId := posToGrid(Pos{p0.x + lx, p0.y - ly}, s.dimLog2)
- rightId := posToGrid(Pos{p0.x + rx, p0.y - ry}, s.dimLog2)
- var left, center, right uint8
- if 0 <= leftId && leftId <= GridId(len(s.grid)-1) {
- left = s.grid[leftId]
- }
- if 0 <= centerId && centerId <= GridId(len(s.grid)-1) {
- center = s.grid[centerId]
- }
- if 0 <= rightId && rightId <= GridId(len(s.grid)-1) {
- right = s.grid[rightId]
- }
- // Rotate
- switch {
- case left < center:
- if center <= right {
- heading -= s.turn // Steer right
- }
- // Otherwise, stay the course.
- case left >= center:
- if center > right {
- heading += s.turn // Steer left
- break
- }
- fallthrough
- default:
- if s.random.Float32() < 0.5 {
- heading += s.turn // Steer left
- } else {
- heading -= s.turn // Steer right
- }
- }
- s.headings[i] = heading
- // Move
- cos := math.Cos(float64(heading))
- sin := math.Sin(float64(heading))
- mx := s.move * cos
- my := s.move * sin
- p1 := Pos{
- int32(float64(p0.x) + mx),
- int32(float64(p0.y) + my),
- }
- // If we exceed the bounds of the grid, stop at the
- // boundary and change heading according to the angle of
- // incidence.
- gridMin := Pos{0, 0}
- gridMax := Pos{s.dimension - 1, s.dimension - 1}
- fromWallMin := p1.Sub(gridMin)
- fromWallMax := gridMax.Sub(p1)
- cnt := 0
- for !inBounds(p1, s.dimension) && cnt < 4 {
- cnt++
- if !inBounds(p1, s.dimension) && cnt >= 2 {
- fmt.Fprintf(os.Stderr, "\nstart at %v; out of bounds at %v\n", p0, p1)
- }
- if p1.x < gridMin.x { // Left wall
- s.headings[i] = pi - heading
- // Cosine cannot be zero when distance to wall is zero
- distToImpact := math.Abs(float64(fromWallMin.x) / cos)
- p1.x = gridMin.x // 0
- p1.y += int32(sin * distToImpact) // -1
- if inBounds(p1, s.dimension) {
- break
- }
- }
- if gridMax.x < p1.x { // Right wall
- s.headings[i] = pi - heading
- distToImpact := math.Abs(float64(fromWallMax.x) / cos)
- p1.x = gridMax.x
- p1.y += int32(sin * distToImpact)
- if inBounds(p1, s.dimension) {
- break
- }
- }
- if p1.y < gridMin.y { // Bottom wall
- s.headings[i] = 2*pi - heading
- distToImpact := math.Abs(float64(fromWallMin.y) / sin)
- p1.x += int32(cos * distToImpact)
- p1.y = gridMin.y
- if inBounds(p1, s.dimension) {
- break
- }
- }
- if gridMax.y < p1.y { // Top wall
- s.headings[i] = 2*pi - heading
- distToImpact := math.Abs(float64(fromWallMax.y) / sin)
- p1.x += int32(cos * distToImpact)
- p1.y = gridMax.y
- if inBounds(p1, s.dimension) {
- break
- }
- }
- }
- if !inBounds(p1, s.dimension) {
- fmt.Fprintf(os.Stderr, "\nstart at %v; out of bounds at %v\n", p0, p1)
- panic("blarg")
- }
- gridId = posToGrid(p1, s.dimLog2)
- s.trails[next] = gridId
- // Deposit and diffuse
- s.grid[gridId] = s.deposit
- s.diffuse(gridId)
- // Decay trail
- for j := 1; j < TrailLenMax; j++ {
- tid := (int(s.offset) + j) & (TrailLenMax - 1)
- s.grid[s.trails[tid]]--
- }
- }
- s.offset++
- s.offset &= uint8(TrailLenMax - 1)
- }
- func (s *Simulation) diffuse(id GridId) {
- value := s.grid[id] >> 4
- s.grid[id] = s.grid[id] >> 1
- p := gridToPos(id, s.dimension, s.dimLog2)
- for i := -1; i < 2; i++ {
- for j := -1; j < 2; j++ {
- bp := Pos{p.x + int32(j), p.y + int32(i)}
- if !inBounds(bp, s.dimension) {
- // For now, just send deposits that fall outside the
- // grid back to the center of the ball.
- s.grid[id] += value
- continue
- }
- bid := posToGrid(bp, s.dimLog2)
- // Protect against overflow.
- s.grid[bid] = max(s.grid[bid], s.grid[bid]+value)
- }
- }
- }
- func (s *Simulation) gridMean(id GridId) (mean uint8) {
- p := gridToPos(id, s.dimension, s.dimLog2)
- n := 0
- for i := -1; i < 2; i++ {
- for j := -1; j < 2; j++ {
- bp := Pos{p.x + int32(j), p.y + int32(i)}
- if !inBounds(bp, s.dimension) {
- continue
- }
- mean += s.grid[posToGrid(bp, s.dimLog2)]
- n++
- }
- }
- return uint8(float32(mean) / float32(n))
- }
- func (s *Simulation) Layout(
- gtx layout.Context,
- th *material.Theme,
- ) D {
- circle := clip.Ellipse{Max: image.Pt(8, 8)}
- // Outer margin
- defer op.Offset(image.Pt(10, 10)).Push(gtx.Ops).Pop()
- halfW, halfH := circle.Max.X>>1, circle.Max.Y>>1
- trailColor := th.Fg
- for _, gridId := range s.trails {
- if gridId == 0 {
- continue
- }
- trailColor.A = s.gridMean(gridId)
- p := gridToPos(gridId, s.dimension, s.dimLog2)
- pos := image.Pt(int(p.x)-halfW, int(s.dimension-1-p.y)+halfH)
- macro := op.Record(gtx.Ops)
- stack := op.Offset(pos).Push(gtx.Ops)
- paint.FillShape(gtx.Ops, trailColor, circle.Op(gtx.Ops))
- stack.Pop()
- c := macro.Stop()
- op.Defer(gtx.Ops, c)
- }
- return layout.Dimensions{
- Size: image.Pt(int(s.dimension), int(s.dimension)),
- }
- }
- func inBounds(p Pos, dim int32) bool {
- return -1 < p.x && p.x < dim &&
- -1 < p.y && p.y < dim
- }
- func posToGrid(p Pos, logOfDim uint32) GridId {
- id := uint32(p.y) << logOfDim
- id |= uint32(p.x)
- return GridId(id)
- }
- func gridToPos(id GridId, dim int32, logOfDim uint32) Pos {
- x := int32(id) & (dim - 1)
- y := int32(id) >> logOfDim
- return Pos{x: x, y: y}
- }
- func log2(x uint32) uint32 {
- b := []uint32{0x2, 0xC, 0xF0, 0xFF00, 0xFFFF0000}
- s := []uint32{1, 2, 4, 8, 16}
- var r uint32 = 0
- for i := 4; i >= 0; i-- {
- if x&b[i] != 0 {
- x >>= s[i]
- r |= s[i]
- }
- }
- return r
- }
|