diff --git a/examples/canvas/main.go b/examples/canvas/main.go deleted file mode 100644 index 9992852..0000000 --- a/examples/canvas/main.go +++ /dev/null @@ -1,136 +0,0 @@ -package main - -import ( - "time" - - "github.com/charmbracelet/harmonica" - "github.com/h8gi/canvas" -) - -const ( - canvasWidth = 1024 - canvasHeight = 768 - fps = 60 - circleRadius = 100 - bgColor = "#111111" - idleTimeout = time.Second * 5 -) - -type State int - -const ( - unstarted State = iota - running - timeout -) - -func main() { - var ( - circ Circle - state State - lastClick time.Time - ) - - c := canvas.NewCanvas(&canvas.CanvasConfig{ - Width: canvasWidth, - Height: canvasHeight, - FrameRate: fps, - Title: "Harmonica", - }) - - c.Draw(func(ctx *canvas.Context) { - ctx.Clear() - ctx.SetHexColor(bgColor) - - switch state { - case unstarted: - if ctx.IsMouseDragged { - const frequency = 0.8 - const damping = 1.0 - - circ = Circle{ - // Setup a new spring. - spring: harmonica.NewSpring(harmonica.FPS(fps), frequency, damping), - - // Set target radius. - Radius: circleRadius, - } - - // Set starting position. - circ.SetPos(ctx.Mouse.X, ctx.Mouse.Y) - - lastClick = time.Now() - state = running - } - - case running: - if ctx.IsMouseDragged { - lastClick = time.Now() - - if circ.Hidden { - circ.SetPos(ctx.Mouse.X, ctx.Mouse.Y) - circ.Hidden = false - } - - // Update targets for our spring animation. - circ.X = ctx.Mouse.X - circ.Y = ctx.Mouse.Y - circ.Radius = circleRadius - } - - circ.Draw(ctx) - - if time.Now().After(lastClick.Add(idleTimeout)) { - state = timeout - } - - case timeout: - circ.Radius = 0 - circ.Draw(ctx) - if circ.rad == 0 { - state = running - lastClick = time.Now() - } - } - }) -} - -type Circle struct { - // Target values. This is what the spring will use as an equilibrium to - // animate towards. - X, Y, Radius float64 - - x, xVel float64 - y, yVel float64 - rad, radVel float64 - spring harmonica.Spring - - Hidden bool -} - -func (c *Circle) SetPos(x, y float64) { - c.x = x - c.y = y - c.Hidden = false -} - -func (c *Circle) Draw(ctx *canvas.Context) { - // Update position and radius. - c.spring.Update(&c.x, &c.xVel, c.X) - c.spring.Update(&c.y, &c.yVel, c.Y) - c.spring.Update(&c.rad, &c.radVel, c.Radius) - - if c.rad < 0 { - c.rad = 0 - c.radVel = 0 - c.Hidden = true - } - - const color = "#f1f1f1" - - ctx.Push() - ctx.DrawCircle(c.x, c.y, c.rad) - ctx.SetHexColor(color) - ctx.Fill() - ctx.Pop() -} diff --git a/examples/go.mod b/examples/go.mod index e91bb64..22c336b 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -5,12 +5,10 @@ go 1.16 replace github.com/charmbracelet/harmonica => ../ require ( - github.com/charmbracelet/bubbletea v0.14.1 // indirect - github.com/charmbracelet/harmonica v0.0.0-20210709144143-16876a63b30d // indirect - github.com/charmbracelet/lipgloss v0.3.0 // indirect - github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f // indirect - github.com/h8gi/canvas v0.0.0-20200710004640-a6361a06ab49 - github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03 // indirect - golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee // indirect + github.com/charmbracelet/bubbletea v0.14.1 + github.com/charmbracelet/harmonica v0.0.0-20210709144143-16876a63b30d + github.com/charmbracelet/lipgloss v0.3.0 + github.com/faiface/pixel v0.10.0 + github.com/fogleman/gg v1.3.0 golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff ) diff --git a/examples/go.sum b/examples/go.sum index 38b9e3a..a2802b2 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -4,30 +4,28 @@ github.com/charmbracelet/lipgloss v0.3.0 h1:5MysOD6sHr4RP4jkZNWGVIul5GKoOsP12Ngb github.com/charmbracelet/lipgloss v0.3.0/go.mod h1:VkhdBS2eNAmRkTwRKLJCFhCOVkjntMusBDxv7TXahuk= github.com/containerd/console v1.0.1 h1:u7SFAJyRqWcG6ogaMAx3KjSTy1e3hT9QxqX7Jco7dRc= github.com/containerd/console v1.0.1/go.mod h1:XUsP6YE/mKtz6bxc+I8UiKKTP04qjQL4qcS3XoQ5xkw= +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380 h1:FvZ0mIGh6b3kOITxUnxS3tLZMh7yEoHo75v3/AgUqg0= github.com/faiface/glhf v0.0.0-20181018222622-82a6317ac380/go.mod h1:zqnPFFIuYFFxl7uH2gYByJwIVKG7fRqlqQCbzAnHs9g= github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3 h1:baVdMKlASEHrj19iqjARrPbaRisD7EuZEVJj6ZMLl1Q= github.com/faiface/mainthread v0.0.0-20171120011319-8b78f0a41ae3/go.mod h1:VEPNJUlxl5KdWjDvz6Q1l+rJlxF2i6xqDeGuGAxa87M= -github.com/faiface/pixel v0.8.0 h1:phOHW6ixfMAKRamjnvhI6FFI2VRyPEq7+LmmkDGXB/4= -github.com/faiface/pixel v0.8.0/go.mod h1:CEUU/s9E82Kqp01Boj1O67KnBskqiLghANqvUJGgDAM= +github.com/faiface/pixel v0.10.0 h1:EHm3ZdQw2Ck4y51cZqFfqQpwLqNHOoXwbNEc9Dijql0= +github.com/faiface/pixel v0.10.0/go.mod h1:lU0YYcW77vL0F1CG8oX51GXurymL45MXd57otHNLK7A= github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8= github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7 h1:SCYMcCJ89LjRGwEa0tRluNRiMjZHalQZrVrvTbPh+qw= github.com/go-gl/gl v0.0.0-20190320180904-bf2b1f2f34d7/go.mod h1:482civXOzJJCPzJ4ZOX/pwvXBWSnzD4OKMdH4ClKGbk= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1 h1:QbL/5oDUmRBzO9/Z7Seo6zf912W/a6Sr4Eu0G/3Jho0= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72 h1:b+9H1GAsx5RsjvDFLoS5zkNBzIQMuVKUYQDmxU3N5XE= +github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7 h1:THttjeRn1iiz69E875U6gAik8KTWk/JYAHoSVpUxBBI= github.com/go-gl/mathgl v0.0.0-20190416160123-c4601bc793c7/go.mod h1:yhpkQzEiH9yPyxDUGzkmgScbaBVlhC06qodikEM0ZwQ= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= -github.com/google/goterm v0.0.0-20190703233501-fc88cf888a3f/go.mod h1:nOFQdrUlIlx6M6ODdSpBj1NVA+VgLC6kmw60mkw34H4= -github.com/h8gi/canvas v0.0.0-20200710004640-a6361a06ab49 h1:svddC2dRqy+Y1zGH4+AvvCFZLKuPhAfEvlgYSNP4lcg= -github.com/h8gi/canvas v0.0.0-20200710004640-a6361a06ab49/go.mod h1:7iSUKfOcA+GiWc+LpxXggMogUFIo8pm1FZcYTUmL3YY= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-runewidth v0.0.10 h1:CoZ3S2P7pvtP45xOtBw+/mDL2z0RKI576gSkzRRpdGg= github.com/mattn/go-runewidth v0.0.10/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= @@ -35,25 +33,22 @@ github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68 h1:y1p/ycavWjGT9Fn github.com/muesli/reflow v0.2.1-0.20210115123740-9e1d0d53df68/go.mod h1:Xk+z4oIWdQqJzsxyjgl3P22oYZnHdZ8FFTHAQQt5BMQ= github.com/muesli/termenv v0.8.1 h1:9q230czSP3DHVpkaPDXGp0TOfAwyjyYwXlUCQxQSaBk= github.com/muesli/termenv v0.8.1/go.mod h1:kzt/D/4a88RoheZmwfqorY3A+tnsSMA9HJC/fQSFKo0= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/term v0.0.0-20200520122047-c3ffed290a03/go.mod h1:Z9+Ul5bCbBKnbCvdOWbLqTHhJiYV414CURZJba6L8qA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= golang.org/x/image v0.0.0-20190321063152-3fc05d484e9f/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff h1:+2zgJKVDVAz/BWSsuniCmU1kLCjL88Z8/kv39xCI9NQ= golang.org/x/image v0.0.0-20190523035834-f03afa92d3ff/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200916030750-2334cc1a136f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68 h1:nxC68pudNYkKU6jWhgrqdreuFiOQWj1Fs7T3VrH4Pjw= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4 h1:myAQVi0cGEoqQVR5POX+8RR2mrocKqNN1hmeMqhX27k= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/examples/pixel/JetBrainsMono-Regular.ttf b/examples/pixel/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..5f2f00f Binary files /dev/null and b/examples/pixel/JetBrainsMono-Regular.ttf differ diff --git a/examples/pixel/main.go b/examples/pixel/main.go new file mode 100644 index 0000000..1394c8e --- /dev/null +++ b/examples/pixel/main.go @@ -0,0 +1,332 @@ +package main + +import ( + "fmt" + "image" + "math" + "math/rand" + "time" + + "github.com/charmbracelet/harmonica" + "github.com/faiface/pixel" + "github.com/faiface/pixel/pixelgl" + "github.com/fogleman/gg" + "golang.org/x/image/font" +) + +const ( + width = 1024 + height = 768 + fontFile = "JetBrainsMono-Regular.ttf" + idleTimeout = time.Second * 4 +) + +func main() { + rand.Seed(time.Now().UnixNano()) + pixelgl.Run(Game{}.Run) +} + +type Game struct { + deltaTime float64 + frequency float64 + damping float64 + win *pixelgl.Window + bgColor string + textColor string + spriteColor string + shift bool + font font.Face + sprite *Sprite + lastClick time.Time + dirty bool + showHelp bool +} + +func (g Game) Size() (width, height float64) { + return g.win.Bounds().W(), g.win.Bounds().H() +} + +func (g Game) MousePosition() (x, y float64) { + return g.win.MousePosition().X, g.win.MousePosition().Y +} + +func (g Game) Run() { + g.frequency = 2.0 + g.damping = 2.0 + g.bgColor = "#575BD8" + g.textColor = "#827EFF" + g.spriteColor = "#FFFDF5" + g.showHelp = true + + var err error + if g.font, err = gg.LoadFontFace(fontFile, 24); err != nil { + panic(err) + } + + cfg := pixelgl.WindowConfig{ + Title: "Harmonica Example", + Bounds: pixel.R(0, 0, width, height), + VSync: true, + Resizable: false, + } + + if g.win, err = pixelgl.NewWindow(cfg); err != nil { + panic(err) + } + + last := time.Now() + for !g.win.Closed() { + w, h := g.Size() + + // Get delta time + g.deltaTime = time.Since(last).Seconds() + last = time.Now() + + g.Update() + + // Use fogleman/gg to render everything + ctx := gg.NewContext(int(w), int(h)) + g.Draw(ctx) + canvas := g.win.Canvas() + canvas.SetPixels(ctx.Image().(*image.RGBA).Pix) + + // Render + g.win.Update() + } +} + +func (g *Game) Update() { + pressed := g.win.JustPressed + released := g.win.JustReleased + + // Handle shift key + switch { + case pressed(pixelgl.KeyLeftShift), pressed(pixelgl.KeyRightShift): + g.shift = true + case released(pixelgl.KeyLeftShift), released(pixelgl.KeyRightShift): + g.shift = false + } + + adjustFreq, adjustDamp := 0.0, 0.0 + + // Handle arrow keys to adjust frequency and damping + switch { + case released(pixelgl.KeyLeft): + adjustFreq = -0.1 + case released(pixelgl.KeyRight): + adjustFreq = 0.1 + case released(pixelgl.KeyUp): + adjustDamp = 0.1 + case released(pixelgl.KeyDown): + adjustDamp = -0.1 + case released(pixelgl.KeySpace): + g.showHelp = !g.showHelp + } + + if g.shift { + adjustDamp *= 10 + adjustFreq *= 10 + } + + if adjustFreq != 0 || adjustDamp != 0 { + g.frequency = math.Max(0, g.frequency+adjustFreq) + g.damping = math.Max(0, g.damping+adjustDamp) + g.dirty = true + } + + if time.Now().After(g.lastClick.Add(idleTimeout)) { + if g.sprite != nil { + g.sprite.Hide() + } + } + + if released(pixelgl.MouseButtonLeft) { + s := g.sprite + + if s == nil { + g.sprite = NewSprite(g) + } else { + x, y := g.MousePosition() + s.TargetX = x + s.TargetY = y + + switch s.State() { + case hiding, gone: + s.Show() + } + } + + g.dirty = false + g.lastClick = time.Now() + } + + // Sprite + if g.sprite != nil { + g.sprite.Update() + } + + g.dirty = false +} + +func (g Game) Draw(ctx *gg.Context) { + w, h := g.Size() + + // BG + ctx.SetHexColor(g.bgColor) + ctx.DrawRectangle(0, 0, w, h) + ctx.Fill() + + // Help text + if g.showHelp { + // For some reason our text renders upside down and backwards. Apply a + // matrix to fix it. + ctx.ScaleAbout(-1, 1, w/2, h/2) + ctx.RotateAbout(math.Pi, w/2, h/2) + + // Instructional text + ctx.Push() + ctx.SetHexColor(g.textColor) + ctx.SetFontFace(g.font) + ctx.DrawStringAnchored("Click!", w/2, h/2, 0.5, 0.5) + ctx.Fill() + ctx.Pop() + + // Status + str := fmt.Sprintf( + "Frequency: %.1f (←/→: adjust) • Damping: %.1f (↑/↓: adjust)", + g.frequency, g.damping, + ) + ctx.Push() + ctx.SetHexColor(g.textColor) + ctx.SetFontFace(g.font) + ctx.DrawStringAnchored(str, w/2, h-34, 0.5, 0) + ctx.Fill() + ctx.Pop() + + // Reset matrix + ctx.Identity() + } + + // Draw sprites + if g.sprite != nil { + g.sprite.Draw(ctx) + } +} + +type SpriteState int + +const ( + moving SpriteState = iota + stopped + hiding + gone +) + +type Sprite struct { + TargetX, TargetY, TargetRadius float64 + X, xVel float64 + Y, yVel float64 + radius, radiusVel float64 + color string + spring harmonica.Spring + game *Game +} + +func NewSprite(g *Game) *Sprite { + mouseX, mouseY := g.MousePosition() + + s := &Sprite{ + X: mouseX, + Y: mouseY, + TargetX: mouseX, + TargetY: mouseY, + radius: 0.1, + color: g.spriteColor, + game: g, + } + s.computeSpring() + s.randomRadius() + + return s +} + +func (s *Sprite) Update() { + if s.State() == gone { + return + } + + g := s.game + + if g.dirty { + // Recompute spring coefficients since our frequency or damping has + // changed. + s.computeSpring() + } + + // Calculate positions based on our spring + s.spring.Update(&s.X, &s.xVel, s.TargetX) + s.spring.Update(&s.Y, &s.yVel, s.TargetY) + s.spring.Update(&s.radius, &s.radiusVel, s.TargetRadius) +} + +func (s Sprite) Draw(ctx *gg.Context) { + if s.State() == gone { + return + } + + ctx.Push() + ctx.DrawCircle(s.X, s.Y, s.radius) + ctx.SetHexColor(s.color) + ctx.Fill() + ctx.Pop() +} + +func (s Sprite) State() SpriteState { + const precision = 2 + x := roundFloat(s.X, precision) + tX := roundFloat(s.TargetX, precision) + y := roundFloat(s.Y, precision) + tY := roundFloat(s.TargetY, precision) + r := roundFloat(s.radius, precision) + tR := roundFloat(s.TargetRadius, precision) + + if r == 0.0 { + return gone + } + if s.TargetRadius == 0 { + return hiding + } + if x == tX && y == tY && r == tR { + return stopped + } + return moving +} + +func (s *Sprite) computeSpring() { + // Calculate spring coefficients + s.spring = harmonica.NewSpring(s.game.deltaTime, s.game.frequency, s.game.damping) +} + +func (s *Sprite) randomRadius() { + s.TargetRadius = rand.Float64()*100.0 + 100.0 +} + +func (s *Sprite) Show() { + s.X, s.Y = s.game.MousePosition() + if s.radius < 0.1 { + s.radius = 0.1 + } + s.computeSpring() + s.randomRadius() +} + +func (s *Sprite) Hide() { + // Fixed frequency and damping when hiding + s.spring = harmonica.NewSpring(s.game.deltaTime, 3.0, 8.0) + s.TargetRadius = 0 +} + +func roundFloat(input float64, decimalPlaces int) float64 { + pow := math.Pow(10, float64(decimalPlaces)) + return math.Round(pow*input) / pow +}