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 + +![bouncing animation](https://user-images.githubusercontent.com/565124/32401910-7cd87fb2-c119-11e7-8121-7fb46e5e11a8.gif) + +![bouncing screenshot](screenshot.png) + +## 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 0000000..72d0fc0 Binary files /dev/null and b/examples/community/bouncing/screenshot.png differ