Skip to content

Commit

Permalink
Introduced support for sprites (#8)
Browse files Browse the repository at this point in the history
* Introduced support for sprites

* Updated readme

* cleanup

* Added unit test

* Refactoring
  • Loading branch information
msoap authored May 5, 2023
1 parent 0a00d67 commit 81e6996
Show file tree
Hide file tree
Showing 6 changed files with 260 additions and 10 deletions.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ Go Graphics library for use in a text terminal. Only 1bit graphics can be used w
* [Fill](https://pkg.go.dev/github.com/msoap/tcg#Buffer.Fill) area with a different [options](https://pkg.go.dev/github.com/msoap/tcg#FillOpt), for example fill with [patterns](https://pkg.go.dev/github.com/msoap/tcg#WithPattern)
* Buffer manipulating: [cut](https://pkg.go.dev/github.com/msoap/tcg#Buffer.Cut), [clone](https://pkg.go.dev/github.com/msoap/tcg#Buffer.Clone), convert to/from stdlib [Image](https://pkg.go.dev/github.com/msoap/tcg#Buffer.ToImage) or text
* Buffer transform: [BitBlt](https://pkg.go.dev/github.com/msoap/tcg#Buffer.BitBlt) with [options](https://pkg.go.dev/github.com/msoap/tcg#BitBltOpt), [clear](https://pkg.go.dev/github.com/msoap/tcg#Buffer.Clear), [flip](https://pkg.go.dev/github.com/msoap/tcg#Buffer.HFlip), [invert](https://pkg.go.dev/github.com/msoap/tcg#Buffer.Invert), scroll ([vertical](https://pkg.go.dev/github.com/msoap/tcg#Buffer.VScroll), [horizontal](https://pkg.go.dev/github.com/msoap/tcg#Buffer.HScroll))
* Sub-package for [turtle graphics](https://pkg.go.dev/github.com/msoap/tcg/turtle), also available [drawing](https://pkg.go.dev/github.com/msoap/[email protected]/turtle#Turtle.DrawScript) by text script
* Sub-package for [turtle graphics](https://pkg.go.dev/github.com/msoap/tcg/turtle), also available [drawing](https://pkg.go.dev/github.com/msoap/tcg/turtle#Turtle.DrawScript) by text script
* [Sprite](https://pkg.go.dev/github.com/msoap/tcg/sprite) support with [Put](https://pkg.go.dev/github.com/msoap/tcg/sprite#Sprite.Put), [Withdraw](https://pkg.go.dev/github.com/msoap/tcg/sprite#Sprite.Withdraw), [Move](https://pkg.go.dev/github.com/msoap/tcg/sprite#Sprite.Move), etc methods

## Install

Expand Down Expand Up @@ -69,7 +70,8 @@ See more [screenshots](https://github.com/msoap/tcg/wiki/Screenshots).
## TODO

* [ ] fonts support
* [ ] sprites, maybe with animation
* [x] sprites
* [ ] animation in sprite

## See also

Expand Down
2 changes: 2 additions & 0 deletions bitblt.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ func (b *Buffer) BitBltAll(x, y int, from Buffer, opts ...BitBltOpt) {
}

// BitBlt - copy part of buffer into this buffer
// xd, yd - destination coordinates
// xs, ys - source coordinates
func (b *Buffer) BitBlt(xd, yd, width, height int, from Buffer, xs, ys int, opts ...BitBltOpt) {
if len(opts) == 0 {
for i := 0; i+ys < from.Height && i < height && i+yd < b.Height; i++ {
Expand Down
3 changes: 2 additions & 1 deletion examples/game_of_life/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Features:

- predefined maps from image files
- save map as screenshot to image file, later you can load it
- edit map with keyboard, by one pixel or with pen mode
- edit map with keyboard, by one pixel or with pen mode, can show/hide cursor
- infinite map (wrap around edges)
- mouse support (scroll up/down to step forward/backward)
- different screen modes (1x1, 1x2, 2x2, 2x3, 2x4Braille)
Expand Down Expand Up @@ -61,6 +61,7 @@ game_of_life -in examples/game_of_life/penta_decathlon.png
* `h` - step to previous state, or mouse scroll down
* `Space` - toggle pixel under cursor
* `a` - toggle pen mode, when pen mode is on, you can draw with arrows keys
* `c` - toggle cursor visibility
* ``, ``, ``, `` - move cursor

## Screenshots
Expand Down
89 changes: 82 additions & 7 deletions examples/game_of_life/game_of_life.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ import (

"github.com/gdamore/tcell/v2"
"github.com/msoap/tcg"
"github.com/msoap/tcg/sprite"
)

type (
cmds int
mode int
game struct {
tg *tcg.Tcg
mode mode // play or pause/edit
generation int // current generation number
cursorX, cursorY int // cursor position for edit/pen mode
pen bool // continue drawing with arrows keys
mode mode // play or pause/edit
generation int // current generation number
cursorX, cursorY int // cursor position for edit/pen mode
pen bool // continue drawing with arrows keys
showCursor bool // show cursor in edit mode
curs *sprite.Sprite
scrH int // screen height in characters
infMap bool // is map infinite?
history *list.List // history of last generations
Expand All @@ -36,6 +39,7 @@ const (
defaultInitFillFactor = 0.2
historySize = 1000
maxFPS = 9999
curHalf = 3

modePlay mode = iota
modePause
Expand All @@ -44,15 +48,37 @@ const (
cmdPause
cmdNext
cmdPrev
cmdPixel // toggle one pixel in current position
cmdPen // toggle pen mode, when pen mode is on, you can draw with arrows keys
cmdPixel // toggle one pixel in current position
cmdPen // toggle pen mode, when pen mode is on, you can draw with arrows keys
cmdToggleCursor // toggle showing cursor in edit mode
cmdUp
cmdDown
cmdLeft
cmdRight
cmdScreenshot
)

var (
cursorImage = tcg.MustNewBufferFromStrings([]string{
"...*...",
"...*...",
".......",
"**...**",
".......",
"...*...",
"...*...",
})
cursorMask = tcg.MustNewBufferFromStrings([]string{
"..***..",
"..***..",
"**...**",
"**...**",
"**...**",
"..***..",
"..***..",
})
)

func main() {
delay := flag.Duration("delay", defaultDelay, "delay between steps")
size := flag.String("size", "", "screen size in chars, in 'width x height' format, example: '80x25'")
Expand Down Expand Up @@ -121,7 +147,7 @@ func main() {

tg.Buf.Rect(0, 0, tg.Width, tg.Height, tcg.Black) // coordinates in pixels
tg.PrintStr(4, 1, " Game of Life ") // coordinates in chars, not pixels
tg.PrintStr(24, scrH-1, `| <q> - Quit | <p> - Pause | <l>/<h> - Next/Prev step | <Space>/<a> pixel/pen | <s> - Screenshot `)
tg.PrintStr(24, scrH-1, `| <q> - Quit | <p> - Pause | <l>/<h> - Next/Prev step | <Space>/<a> pixel/pen | <c> show cursor | <s> - Screenshot `)
tg.Show()

if err := tg.SetClipCenter(width-2, height-2); err != nil {
Expand Down Expand Up @@ -189,6 +215,10 @@ LOOP:
if game.mode != modePlay {
game.pen = !game.pen
}
case cmdToggleCursor:
if game.mode != modePlay {
game.toggleCursor()
}
case cmdScreenshot:
if err := saveScreenshot(*screenshotName, tg.Buf); err != nil {
tg.PrintStr(0, 0, fmt.Sprintf("save: %s", err))
Expand All @@ -203,9 +233,13 @@ LOOP:

func newGame(tg *tcg.Tcg, mode mode) *game {
_, scrH := tg.ScreenSize()

cursorSprite := sprite.New(cursorImage).WithMask(cursorMask)

return &game{
mode: mode,
tg: tg,
curs: cursorSprite,
scrH: scrH,
history: list.New(),
}
Expand Down Expand Up @@ -249,19 +283,29 @@ func (g *game) initFromImage(img image.Image) {
func (g *game) doPause() {
if g.mode == modePlay {
g.mode = modePause
if g.showCursor {
g.curs.MoveAbs(g.tg.Buf, g.cursorX-curHalf, g.cursorY-curHalf).Put(g.tg.Buf)
}
} else {
g.mode = modePlay
if g.showCursor {
g.curs.Withdraw(g.tg.Buf)
}
}
g.tg.Show()
}

func (g *game) togglePixel() {
defer g.handleCursor()()

color := g.tg.Buf.At(g.cursorX, g.cursorY)
color = color ^ 1
g.tg.Buf.Set(g.cursorX, g.cursorY, color)
g.tg.Show()
}

func (g *game) moveCursor(dx, dy int) {
defer g.handleCursor()()
oldX, oldY := g.cursorX, g.cursorY

g.cursorX += dx
Expand All @@ -287,7 +331,34 @@ func (g *game) moveCursor(dx, dy int) {
g.tg.Show()
}

func (g *game) toggleCursor() {
defer g.handleCursor()()
g.showCursor = !g.showCursor
}

func (g *game) handleCursor() func() {
changed := false
if g.showCursor {
g.curs.Withdraw(g.tg.Buf)
changed = true
}

return func() {
if g.showCursor {
g.curs.MoveAbs(g.tg.Buf, g.cursorX-curHalf, g.cursorY-curHalf).Put(g.tg.Buf)
changed = true
}
if changed {
g.tg.Show()
}
}
}

func (g *game) nextStep() {
if g.showCursor && g.mode == modePause {
defer g.handleCursor()()
}

startedAt := time.Now()
g.generation++

Expand Down Expand Up @@ -378,6 +449,8 @@ func (g *game) prevStep() {
return
}

defer g.handleCursor()()

startedAt := time.Now()
buf := g.history.Remove(g.history.Front()).(*tcg.Buffer)
g.tg.Buf.BitBltAll(0, 0, *buf)
Expand Down Expand Up @@ -407,6 +480,8 @@ func getCommand(tg *tcg.Tcg) chan cmds {
resultCh <- cmdPixel
case ev.Rune() == 'a':
resultCh <- cmdPen
case ev.Rune() == 'c':
resultCh <- cmdToggleCursor
case ev.Key() == tcell.KeyRight:
resultCh <- cmdRight
case ev.Key() == tcell.KeyLeft:
Expand Down
89 changes: 89 additions & 0 deletions sprite/sprite.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package sprite

import (
"github.com/msoap/tcg"
)

// Sprite - sprite object
type Sprite struct {
Buf tcg.Buffer // sprite image
x, y int // position on buffer, can be negative
width int
height int
mask *tcg.Buffer // mask for sprite
bg tcg.Buffer // saved
isDrawn bool // is sprite drawn on buffer
}

// New - get new sprite object from tcg.Buffer
func New(buf tcg.Buffer) *Sprite {
return &Sprite{
width: buf.Width,
height: buf.Height,
Buf: buf,
bg: tcg.NewBuffer(buf.Width, buf.Height),
}
}

// WithMask - add mask to sprite
func (s *Sprite) WithMask(mask tcg.Buffer) *Sprite {
s.mask = &mask
return s
}

// Put - put sprite on buffer, save background under sprite, change state of sprite to drawn
func (s *Sprite) Put(buf tcg.Buffer) *Sprite {
s.draw(buf)
s.isDrawn = true

return s
}

// draw - draw sprite on buffer, save background under sprite
func (s *Sprite) draw(buf tcg.Buffer) {
// copy background
s.bg.BitBlt(0, 0, s.width, s.height, buf, s.x, s.y)

// draw sprite
var opts []tcg.BitBltOpt
if s.mask != nil {
opts = append(opts, tcg.BBMask(s.mask))
}
buf.BitBlt(s.x, s.y, s.width, s.height, s.Buf, 0, 0, opts...)
}

// Withdraw - withdraw sprite from buffer
func (s *Sprite) Withdraw(buf tcg.Buffer) *Sprite {
if s.isDrawn {
s.clear(buf)
s.isDrawn = false
}

return s
}

// clear sprite on buffer
func (s *Sprite) clear(buf tcg.Buffer) {
buf.BitBlt(s.x, s.y, s.width, s.height, s.bg, 0, 0)
}

// MoveAbs - move sprite on buffer to absolute position, coordinates can be negative
func (s *Sprite) MoveAbs(buf tcg.Buffer, x, y int) *Sprite {
if s.isDrawn {
s.clear(buf)
}

s.x = x
s.y = y

if s.isDrawn {
s.draw(buf)
}

return s
}

// Move - move sprite on buffer to relative position
func (s *Sprite) Move(buf tcg.Buffer, x, y int) *Sprite {
return s.MoveAbs(buf, s.x+x, s.y+y)
}
Loading

0 comments on commit 81e6996

Please sign in to comment.