goids/main.go

235 lines
4.9 KiB
Go

package main
import (
"bytes"
"encoding/base64"
"fmt"
"image"
"image/color"
"image/png"
"io/ioutil"
"math"
"math/rand"
"os"
"sort"
"github.com/llgcode/draw2d/draw2dimg"
"gopkg.in/yaml.v2"
)
// parameters
var windowWidth, windowHeight = 1000, 700
var goidSize = 3
var goidColor = color.RGBA{200, 200, 100, 255} // gray, 50% transparency
var populationSize = 20
var loops = 500
var numNeighbours = 10
var separationFactor = float64(goidSize * 5)
var coherenceFactor = 8
type steps struct {
Step int `yaml:"step"`
Amount int `yaml:"amount"`
NumNeighbours int `yaml:"neighbours"`
}
func fixGoids(gs []*Goid, actual int, wished int) ([]*Goid, int) {
if actual < wished {
g := createRandomGoid()
gs = append(gs, &g)
return gs, actual + 1
} else if wished < actual {
return gs[:len(gs)-1], actual -1
} else {
return gs, actual
}
}
func main() {
fo, err := os.Create("/tmp/goids.txt")
if err != nil {
panic(err)
}
conf, err := ioutil.ReadFile("config.yaml")
if err != nil {
panic(err)
}
td := []steps{}
err = yaml.Unmarshal(conf, &td)
defer func() {
if err := fo.Close(); err != nil {
panic(err)
}
}()
clearScreen()
hideCursor()
var goids []*Goid
for i := 0; i < populationSize; i++ {
g := createRandomGoid()
goids = append(goids, &g)
}
current_stage := 0
for i := 0; i < loops; i++ {
goids, populationSize = fixGoids(goids, populationSize, td[current_stage].Amount)
numNeighbours = td[current_stage].NumNeighbours
if i > td[current_stage].Step {
current_stage += 1
}
move(goids, fo)
frame := draw(goids)
printImage(frame.SubImage(frame.Rect))
}
showCursor()
}
// Goid represents a drawn goid
type Goid struct {
X int // position
Y int
Vx int // velocity
Vy int
R int // radius
Color color.Color
}
func createRandomGoid() (g Goid) {
g = Goid{
X: rand.Intn(windowWidth),
Y: rand.Intn(windowHeight),
Vx: rand.Intn(goidSize),
Vy: rand.Intn(goidSize),
R: goidSize,
Color: goidColor,
}
return
}
// find the nearest neighbours
func (g *Goid) nearestNeighbours(goids []*Goid) (neighbours []Goid) {
neighbours = make([]Goid, len(goids))
for _, goid := range goids {
neighbours = append(neighbours, *goid)
}
sort.SliceStable(neighbours, func(i, j int) bool {
return g.distance(neighbours[i]) < g.distance(neighbours[j])
})
return
}
// distance between 2 goids
func (g *Goid) distance(n Goid) float64 {
x := g.X - n.X
y := g.Y - n.Y
return math.Sqrt(float64(x*x + y*y))
}
// move the goids with the 3 classic boid rules
func move(goids []*Goid, file *os.File) {
for i, goid := range goids {
neighbours := goid.nearestNeighbours(goids)
separate(goid, neighbours)
align(goid, neighbours)
cohere(goid, neighbours)
position := fmt.Sprintf("- coor: %d\n data:\n x: %d\n y: %d\n", i, goid.X, goid.Y)
file.Write([]byte(position))
stayInWindow(goid)
}
}
// if goid goes out of the window frame it comes back on the other side
func stayInWindow(goid *Goid) {
if goid.X < 0 {
goid.X = windowWidth - goid.X
} else if goid.X > windowWidth {
goid.X = windowWidth - goid.X
}
if goid.Y < 0 {
goid.Y = windowHeight - goid.Y
} else if goid.Y > windowHeight {
goid.Y = windowHeight - goid.Y
}
}
// steer to avoid crowding local goids
func separate(g *Goid, neighbours []Goid) {
x, y := 0, 0
for _, n := range neighbours[0:numNeighbours] {
if g.distance(n) < separationFactor {
x += g.X - n.X
y += g.Y - n.Y
}
}
g.Vx = x
g.Vy = y
g.X += x
g.Y += y
}
// steer towards the average heading of local goids
func align(g *Goid, neighbours []Goid) {
x, y := 0, 0
for _, n := range neighbours[0:numNeighbours] {
x += n.Vx
y += n.Vy
}
dx, dy := x/numNeighbours, y/numNeighbours
g.Vx += dx
g.Vy += dy
g.X += dx
g.Y += dy
}
// steer to move toward the average position of local goids
func cohere(g *Goid, neighbours []Goid) {
x, y := 0, 0
for _, n := range neighbours[0:numNeighbours] {
x += n.X
y += n.Y
}
dx, dy := ((x/numNeighbours)-g.X)/coherenceFactor, ((y/numNeighbours)-g.Y)/coherenceFactor
g.Vx += dx
g.Vy += dy
g.X += dx
g.Y += dy
}
// draw the goids
func draw(goids []*Goid) *image.RGBA {
dest := image.NewRGBA(image.Rect(0, 0, windowWidth, windowHeight))
gc := draw2dimg.NewGraphicContext(dest)
for _, goid := range goids {
gc.SetFillColor(goid.Color)
gc.MoveTo(float64(goid.X), float64(goid.Y))
gc.ArcTo(float64(goid.X), float64(goid.Y), float64(goid.R), float64(goid.R), 0, -math.Pi*2)
gc.LineTo(float64(goid.X-goid.Vx), float64(goid.Y-goid.Vy))
gc.Close()
gc.Fill()
}
return dest
}
// ANSI escape sequence codes to perform action on terminal
func hideCursor() {
fmt.Print("\033[?25l")
}
func showCursor() {
fmt.Print("\x1b[?25h\n")
}
func clearScreen() {
fmt.Print("\x1b[2J")
}
// this only works for iTerm!
func printImage(img image.Image) {
var buf bytes.Buffer
png.Encode(&buf, img)
imgBase64Str := base64.StdEncoding.EncodeToString(buf.Bytes())
fmt.Printf("\x1b[2;0H\x1b]1337;File=inline=1:%s\a", imgBase64Str)
}