// Command flowfield is the flow-field generative-art staple (Processing, Nature
// of Code): a scalar field is read as an angle at every point, and particles are
// pushed along it, tracing the invisible field as braided streamlines. The field
// here is a sum of sines (no noise tables) that slowly evolves; each particle
// keeps a short trail, so the flow draws itself. All inside one widget.Canvas.
//
// go run ./examples/flowfield
package main
import (
"log"
"math"
"math/rand"
"github.com/doug/gophics/app"
"github.com/doug/gophics/geom"
"github.com/doug/gophics/paint"
"github.com/doug/gophics/widget"
)
const (
numParticles = 700
trailLen = 15
speed = 1.8
)
type particle struct{ trail []geom.Pt }
type Flow struct{}
func (Flow) CreateState() widget.State { return &flowState{} }
type flowState struct {
widget.StateBase[Flow]
ps []*particle
w, h float32
t float32
started bool
}
func (s *flowState) Init(ctx widget.Ctx) { ctx.AddTicker(s) }
func (s *flowState) seed(w, h float32) {
s.w, s.h = w, h
s.ps = s.ps[:0]
for range numParticles {
p := geom.Pt{X: rand.Float32() * w, Y: rand.Float32() * h}
s.ps = append(s.ps, &particle{trail: []geom.Pt{p}})
}
s.started = true
}
// angleAt reads the field. Summed sines at different scales make the streamlines
// fold back on themselves and braid instead of all flowing one way.
func (s *flowState) angleAt(p geom.Pt) float64 {
x, y, t := float64(p.X), float64(p.Y), float64(s.t)
v := math.Sin(x*0.006+t*0.30) + math.Cos(y*0.006-t*0.20) + math.Sin((x+y)*0.003+t*0.15)
return v * 1.5
}
func (s *flowState) Tick(dt float64) bool {
if !s.started {
return true
}
s.SetState(func() { s.step(float32(dt)) })
return true
}
func (s *flowState) step(dt float32) {
s.t += dt
for _, p := range s.ps {
head := p.trail[len(p.trail)-1]
next := head.Add(fromAngle(s.angleAt(head), speed))
if next.X < 0 || next.X > s.w || next.Y < 0 || next.Y > s.h {
// Re-seed off-screen particles so the field stays populated.
next = geom.Pt{X: rand.Float32() * s.w, Y: rand.Float32() * s.h}
p.trail = p.trail[:0]
}
p.trail = append(p.trail, next)
if len(p.trail) > trailLen {
p.trail = p.trail[len(p.trail)-trailLen:]
}
}
}
var bg = paint.RGB(0.02, 0.024, 0.047)
func (s *flowState) Build(widget.Ctx) widget.Widget {
return widget.Canvas{Clip: true, Draw: func(c paint.Canvas, size geom.Size) {
if !s.started || s.w != size.W || s.h != size.H {
s.seed(size.W, size.H)
}
c.Clear(bg)
for _, p := range s.ps {
if len(p.trail) < 2 {
continue
}
head := p.trail[len(p.trail)-1]
hue := wrapf(float32(s.angleAt(head)*180/math.Pi)*0.5+200, 0, 360)
path := paint.NewPath()
path.MoveTo(p.trail[0])
for _, pt := range p.trail[1:] {
path.LineTo(pt)
}
c.StrokePath(path, 1.3, paint.HSV(hue, 0.6, 1).WithAlpha(0.5))
}
}}
}
func fromAngle(a float64, l float32) geom.Pt {
return geom.Pt{X: l * float32(math.Cos(a)), Y: l * float32(math.Sin(a))}
}
func wrapf(v, lo, hi float32) float32 {
r := hi - lo
for v < lo {
v += r
}
for v >= hi {
v -= r
}
return v
}
func main() {
if err := app.Run(Flow{}, app.Config{
Title: "gophics flow field",
Size: geom.Size{W: 900, H: 640},
Background: bg,
}); err != nil {
log.Fatal(err)
}
}