From da55b9ddca557326b6e71bc2c82b0dc1a962e135 Mon Sep 17 00:00:00 2001 From: Peter Hellberg <peter@c7.se> Date: Thu, 11 Jan 2018 22:42:04 +0100 Subject: [PATCH] Add bouncing experiment to community examples --- examples/community/bouncing/README.md | 16 ++ examples/community/bouncing/bouncing.go | 303 +++++++++++++++++++++ examples/community/bouncing/screenshot.png | Bin 0 -> 7056 bytes 3 files changed, 319 insertions(+) create mode 100644 examples/community/bouncing/README.md create mode 100644 examples/community/bouncing/bouncing.go create mode 100644 examples/community/bouncing/screenshot.png diff --git a/examples/community/bouncing/README.md b/examples/community/bouncing/README.md new file mode 100644 index 0000000..cb1c7f1 --- /dev/null +++ b/examples/community/bouncing/README.md @@ -0,0 +1,16 @@ +# bouncing + +Bouncing particles using the [imdraw](https://godoc.org/github.com/faiface/pixel/imdraw) package. + +Made by [Peter Hellberg](https://github.com/peterhellberg/) as part of his [pixel-experiments](https://github.com/peterhellberg/pixel-experiments) + +## Screenshots + + + + + +## Links + + - https://github.com/peterhellberg/pixel-experiments/tree/master/bouncing + - https://gist.github.com/peterhellberg/674f32a15a7d2d249e634ce781f333e8 diff --git a/examples/community/bouncing/bouncing.go b/examples/community/bouncing/bouncing.go new file mode 100644 index 0000000..b2b43f2 --- /dev/null +++ b/examples/community/bouncing/bouncing.go @@ -0,0 +1,303 @@ +package main + +import ( + "image/color" + "math" + "math/rand" + "time" + + "github.com/faiface/pixel" + "github.com/faiface/pixel/imdraw" + "github.com/faiface/pixel/pixelgl" +) + +var ( + w, h, s, scale = float64(640), float64(360), float64(2.3), float64(32) + + p, bg = newPalette(Colors), color.RGBA{32, p.color().G, 32, 255} + + balls = []*ball{ + newRandomBall(scale), + newRandomBall(scale), + } +) + +func run() { + win, err := pixelgl.NewWindow(pixelgl.WindowConfig{ + Bounds: pixel.R(0, 0, w, h), + VSync: true, + Undecorated: true, + }) + if err != nil { + panic(err) + } + + imd := imdraw.New(nil) + + imd.EndShape = imdraw.RoundEndShape + imd.Precision = 3 + + go func() { + start := time.Now() + + for range time.Tick(16 * time.Millisecond) { + bg = color.RGBA{32 + (p.color().R/128)*4, 32 + (p.color().G/128)*4, 32 + (p.color().B/128)*4, 255} + s = pixel.V(math.Sin(time.Since(start).Seconds())*0.8, 0).Len()*2 - 1 + scale = 64 + 15*s + imd.Intensity = 1.2 * s + } + }() + + for !win.Closed() { + win.SetClosed(win.JustPressed(pixelgl.KeyEscape) || win.JustPressed(pixelgl.KeyQ)) + + if win.JustPressed(pixelgl.KeySpace) { + for _, ball := range balls { + ball.color = ball.palette.next() + } + } + + if win.JustPressed(pixelgl.KeyEnter) { + for _, ball := range balls { + ball.pos = center() + ball.vel = randomVelocity() + } + } + + imd.Clear() + + for _, ball := range balls { + imd.Color = ball.color + imd.Push(ball.pos) + } + + imd.Polygon(scale) + + for _, ball := range balls { + imd.Color = color.RGBA{ball.color.R, ball.color.G, ball.color.B, 128 - uint8(128*s)} + imd.Push(ball.pos) + } + + imd.Polygon(scale * s) + + for _, ball := range balls { + aliveParticles := []*particle{} + + for _, particle := range ball.particles { + if particle.life > 0 { + aliveParticles = append(aliveParticles, particle) + } + } + + for _, particle := range aliveParticles { + imd.Color = particle.color + imd.Push(particle.pos) + imd.Circle(16*particle.life, 0) + } + } + + win.Clear(bg) + imd.Draw(win) + win.Update() + } +} + +func main() { + rand.Seed(4) + + go func() { + for range time.Tick(32 * time.Millisecond) { + for _, ball := range balls { + go ball.update() + + for _, particle := range ball.particles { + go particle.update() + } + } + } + }() + + pixelgl.Run(run) +} + +func newParticleAt(pos, vel pixel.Vec) *particle { + c := p.color() + c.A = 5 + + return &particle{pos, vel, c, rand.Float64() * 1.5} +} + +func newRandomBall(radius float64) *ball { + return &ball{ + center(), randomVelocity(), + math.Pi * (radius * radius), + radius, p.random(), p, []*particle{}, + } +} + +func center() pixel.Vec { + return pixel.V(w/2, h/2) +} + +func randomVelocity() pixel.Vec { + return pixel.V((rand.Float64()*2)-1, (rand.Float64()*2)-1).Scaled(scale / 4) +} + +type particle struct { + pos pixel.Vec + vel pixel.Vec + color color.RGBA + life float64 +} + +func (p *particle) update() { + p.pos = p.pos.Add(p.vel) + p.life -= 0.03 + + switch { + case p.pos.Y < 0 || p.pos.Y >= h: + p.vel.Y *= -1.0 + case p.pos.X < 0 || p.pos.X >= w: + p.vel.X *= -1.0 + } +} + +type ball struct { + pos pixel.Vec + vel pixel.Vec + mass float64 + radius float64 + color color.RGBA + palette *Palette + particles []*particle +} + +func (b *ball) update() { + b.pos = b.pos.Add(b.vel) + + var bounced bool + + switch { + case b.pos.Y <= b.radius || b.pos.Y >= h-b.radius: + b.vel.Y *= -1.0 + bounced = true + + if b.pos.Y < b.radius { + b.pos.Y = b.radius + } else { + b.pos.Y = h - b.radius + } + case b.pos.X <= b.radius || b.pos.X >= w-b.radius: + b.vel.X *= -1.0 + bounced = true + + if b.pos.X < b.radius { + b.pos.X = b.radius + } else { + b.pos.X = w - b.radius + } + } + + for _, a := range balls { + if a != b { + d := a.pos.Sub(b.pos) + + if d.Len() > a.radius+b.radius { + continue + } + + pen := d.Unit().Scaled(a.radius + b.radius - d.Len()) + + a.pos = a.pos.Add(pen.Scaled(b.mass / (a.mass + b.mass))) + b.pos = b.pos.Sub(pen.Scaled(a.mass / (a.mass + b.mass))) + + u := d.Unit() + v := 2 * (a.vel.Dot(u) - b.vel.Dot(u)) / (a.mass + b.mass) + + a.vel = a.vel.Sub(u.Scaled(v * b.mass)) + b.vel = b.vel.Add(u.Scaled(v * a.mass)) + + bounced = true + } + } + + if bounced { + b.color = p.next() + b.particles = append(b.particles, + newParticleAt(b.pos, b.vel.Rotated(1).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(2).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(3).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(4).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(5).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(6).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(7).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(8).Scaled(rand.Float64())), + newParticleAt(b.pos, b.vel.Rotated(9).Scaled(rand.Float64())), + + newParticleAt(b.pos, b.vel.Rotated(10).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(20).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(30).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(40).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(50).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(60).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(70).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(80).Scaled(rand.Float64()+1)), + newParticleAt(b.pos, b.vel.Rotated(90).Scaled(rand.Float64()+1)), + ) + } +} + +func newPalette(cc []color.Color) *Palette { + colors := []color.RGBA{} + + for _, v := range cc { + if c, ok := v.(color.RGBA); ok { + colors = append(colors, c) + } + } + + return &Palette{colors, len(colors), 0} +} + +type Palette struct { + colors []color.RGBA + size int + index int +} + +func (p *Palette) clone() *Palette { + return &Palette{p.colors, p.size, p.index} +} + +func (p *Palette) next() color.RGBA { + if p.index++; p.index >= p.size { + p.index = 0 + } + + return p.colors[p.index] +} + +func (p *Palette) color() color.RGBA { + return p.colors[p.index] +} + +func (p *Palette) random() color.RGBA { + p.index = rand.Intn(p.size) + + return p.colors[p.index] +} + +var Colors = []color.Color{ + color.RGBA{190, 38, 51, 255}, + color.RGBA{224, 111, 139, 255}, + color.RGBA{73, 60, 43, 255}, + color.RGBA{164, 100, 34, 255}, + color.RGBA{235, 137, 49, 255}, + color.RGBA{247, 226, 107, 255}, + color.RGBA{47, 72, 78, 255}, + color.RGBA{68, 137, 26, 255}, + color.RGBA{163, 206, 39, 255}, + color.RGBA{0, 87, 132, 255}, + color.RGBA{49, 162, 242, 255}, + color.RGBA{178, 220, 239, 255}, +} diff --git a/examples/community/bouncing/screenshot.png b/examples/community/bouncing/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..72d0fc03d286bef50d46343b1ae6f24ee96c18fa GIT binary patch literal 7056 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 literal 0 HcmV?d00001 -- GitLab