// Command particles is a fountain built from a simple particle system — a burst
// of coloured sparks that arc under gravity and fade out, re-recorded every
// frame inside one widget.Canvas.
//
// go run ./examples/particles
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 (
gravity = 520 // px/s²
drag = 0.9 // per-second velocity retention
perFrame = 14 // sparks emitted each frame
maxAlive = 1400
)
type spark struct {
pos, vel geom.Pt
life, max, size float32
col paint.Color
}
type Fountain struct{}
func (Fountain) CreateState() widget.State { return &fountainState{} }
type fountainState struct {
widget.StateBase[Fountain]
sparks []*spark
w, h float32
t float32
}
func (s *fountainState) Init(ctx widget.Ctx) { ctx.AddTicker(s) }
func (s *fountainState) Tick(dt float64) bool {
s.SetState(func() { s.step(float32(dt)) })
return true
}
func (s *fountainState) step(dt float32) {
s.t += dt
origin := geom.Pt{X: s.w * 0.5, Y: s.h * 0.92}
// Emit an upward burst with a little spread.
for i := 0; i < perFrame && len(s.sparks) < maxAlive; i++ {
a := -math.Pi/2 + (rand.Float64()-0.5)*0.9
sp := 260 + rand.Float32()*300
hue := wrapf(s.t*70+rand.Float32()*50, 0, 360)
s.sparks = append(s.sparks, &spark{
pos: origin, vel: fromAngle(a, sp),
life: 0.8 + rand.Float32()*1.3, max: 2.1, size: 2 + rand.Float32()*3,
col: paint.HSV(hue, 0.7, 1),
})
}
// Advance + cull.
live := s.sparks[:0]
dragF := float32(math.Pow(drag, float64(dt)))
for _, p := range s.sparks {
p.vel.Y += gravity * dt
p.vel = p.vel.Mul(dragF)
p.pos = p.pos.Add(p.vel.Mul(dt))
p.life -= dt
if p.life > 0 && p.pos.Y < s.h+20 {
live = append(live, p)
}
}
s.sparks = live
}
var bg = paint.RGB(0.02, 0.024, 0.043)
func (s *fountainState) Build(widget.Ctx) widget.Widget {
return widget.Canvas{Clip: true, Draw: func(c paint.Canvas, size geom.Size) {
s.w, s.h = size.W, size.H
c.FillRRectGradient(geom.Rect{Max: size.Pt()}, 0, paint.RGB(0.05, 0.06, 0.10), bg, false)
for _, p := range s.sparks {
r := p.size * (0.4 + 0.6*p.life/p.max)
col := p.col.WithAlpha(clamp(p.life/p.max, 0, 1))
c.FillRRect(geom.RectXYWH(p.pos.X-r, p.pos.Y-r, r*2, r*2), r, col)
}
}}
}
func fromAngle(a float64, l float32) geom.Pt {
return geom.Pt{X: l * float32(math.Cos(a)), Y: l * float32(math.Sin(a))}
}
func clamp(v, lo, hi float32) float32 {
if v < lo {
return lo
}
if v > hi {
return hi
}
return v
}
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(Fountain{}, app.Config{
Title: "gophics particles",
Size: geom.Size{W: 720, H: 560},
Background: bg,
}); err != nil {
log.Fatal(err)
}
}