A terminal with history, a Go-colored code block, a log viewer that follows, a hex viewer, a memory map, a JSON tree, a line diff, a GAME OVER crash screen, an FPS meter, profiler bars, breakpoints, variables, command history, a shortcut list, a debug grid, a pixel editor that exports string art, a palette editor, a sprite sheet, a glyph map and a 6502 register view. layout.ScrollArea takes Follow, and ChatLog now uses it. data.TreeNode takes a Value drawn in its Slot. pixel gains Has, and Sprite.Art writes MustSprite art. pixel.Editor takes the caller's value at each input event, so a value changed between frames is never typed onto.
182 lines
4.0 KiB
Go
182 lines
4.0 KiB
Go
package pixel
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
)
|
||
|
||
// Sprite is hand-placed art in palette slots, in frames.
|
||
type Sprite struct {
|
||
W, H int
|
||
frames [][]int8
|
||
}
|
||
|
||
const (
|
||
cellClear int8 = -1
|
||
cellInk int8 = -2
|
||
)
|
||
|
||
// Art letters naming slots; '.' is clear, '#' the ink.
|
||
const artSlots = "kbflsenwaAigyrcm"
|
||
|
||
// MustSprite parses string art; blank lines split frames.
|
||
func MustSprite(art string) *Sprite {
|
||
var frames [][]string
|
||
var cur []string
|
||
for _, line := range strings.Split(strings.TrimSpace(art), "\n") {
|
||
line = strings.TrimSpace(line)
|
||
if line == "" {
|
||
if cur != nil {
|
||
frames, cur = append(frames, cur), nil
|
||
}
|
||
continue
|
||
}
|
||
cur = append(cur, line)
|
||
}
|
||
if cur == nil {
|
||
panic("pixel: empty sprite")
|
||
}
|
||
frames = append(frames, cur)
|
||
sp := &Sprite{W: len(frames[0][0]), H: len(frames[0])}
|
||
for _, f := range frames {
|
||
if len(f) != sp.H {
|
||
panic(fmt.Sprintf("pixel: sprite frames of %d and %d rows", sp.H, len(f)))
|
||
}
|
||
px := make([]int8, 0, sp.W*sp.H)
|
||
for _, row := range f {
|
||
if len(row) != sp.W {
|
||
panic(fmt.Sprintf("pixel: sprite row %q is not %d wide", row, sp.W))
|
||
}
|
||
for i := range len(row) {
|
||
px = append(px, cell(row[i]))
|
||
}
|
||
}
|
||
sp.frames = append(sp.frames, px)
|
||
}
|
||
return sp
|
||
}
|
||
|
||
func cell(c byte) int8 {
|
||
switch c {
|
||
case '.':
|
||
return cellClear
|
||
case '#':
|
||
return cellInk
|
||
}
|
||
if i := strings.IndexByte(artSlots, c); i >= 0 {
|
||
return int8(i)
|
||
}
|
||
panic(fmt.Sprintf("pixel: sprite character %q", c))
|
||
}
|
||
|
||
func (s *Sprite) Frames() int { return len(s.frames) }
|
||
|
||
// At is frame's slot at x, y; false where clear.
|
||
func (s *Sprite) At(frame, x, y int, ink Slot) (Slot, bool) {
|
||
if x < 0 || y < 0 || x >= s.W || y >= s.H {
|
||
panic(fmt.Sprintf("pixel: sprite dot %d,%d outside %d×%d", x, y, s.W, s.H))
|
||
}
|
||
n := len(s.frames)
|
||
switch v := s.frames[(frame%n+n)%n][y*s.W+x]; v {
|
||
case cellClear:
|
||
return 0, false
|
||
case cellInk:
|
||
return ink, true
|
||
default:
|
||
return Slot(v), true
|
||
}
|
||
}
|
||
|
||
// SpriteOf builds a one-frame sprite; at reports each dot.
|
||
func SpriteOf(w, h int, at func(x, y int) (Slot, bool)) *Sprite {
|
||
if w < 1 || h < 1 {
|
||
panic(fmt.Sprintf("pixel: sprite of %d×%d", w, h))
|
||
}
|
||
px := make([]int8, w*h)
|
||
for y := range h {
|
||
for x := range w {
|
||
px[y*w+x] = cellClear
|
||
if s, ok := at(x, y); ok {
|
||
if s >= Slots {
|
||
panic(fmt.Sprintf("pixel: sprite slot %d at %d,%d", s, x, y))
|
||
}
|
||
px[y*w+x] = int8(s)
|
||
}
|
||
}
|
||
}
|
||
return &Sprite{W: w, H: h, frames: [][]int8{px}}
|
||
}
|
||
|
||
// Art writes s as MustSprite reads it, frames apart.
|
||
func (s *Sprite) Art() string {
|
||
var b strings.Builder
|
||
for f, px := range s.frames {
|
||
if f > 0 {
|
||
b.WriteByte('\n')
|
||
}
|
||
for y := range s.H {
|
||
for _, v := range px[y*s.W : (y+1)*s.W] {
|
||
switch v {
|
||
case cellClear:
|
||
b.WriteByte('.')
|
||
case cellInk:
|
||
b.WriteByte('#')
|
||
default:
|
||
b.WriteByte(artSlots[v])
|
||
}
|
||
}
|
||
b.WriteByte('\n')
|
||
}
|
||
}
|
||
return b.String()
|
||
}
|
||
|
||
// Mirror returns s flipped left to right.
|
||
func (s *Sprite) Mirror() *Sprite {
|
||
m := &Sprite{W: s.W, H: s.H}
|
||
for _, px := range s.frames {
|
||
out := make([]int8, len(px))
|
||
for y := range s.H {
|
||
for x := range s.W {
|
||
out[y*s.W+x] = px[y*s.W+s.W-1-x]
|
||
}
|
||
}
|
||
m.frames = append(m.frames, out)
|
||
}
|
||
return m
|
||
}
|
||
|
||
// Sprite draws frame of sp; # takes ink; frames wrap.
|
||
func (g Canvas) Sprite(x, y int, sp *Sprite, frame int, ink Slot, scale int) {
|
||
g.sprite(x, y, sp, frame, ink, scale, false)
|
||
}
|
||
|
||
// Silhouette draws sp's shape all in one slot.
|
||
func (g Canvas) Silhouette(x, y int, sp *Sprite, frame int, s Slot, scale int) {
|
||
g.sprite(x, y, sp, frame, s, scale, true)
|
||
}
|
||
|
||
func (g Canvas) sprite(x, y int, sp *Sprite, frame int, ink Slot, scale int, mono bool) {
|
||
checkScale(scale)
|
||
n := len(sp.frames)
|
||
px := sp.frames[(frame%n+n)%n]
|
||
for row := range sp.H {
|
||
line := px[row*sp.W : (row+1)*sp.W]
|
||
for col := 0; col < sp.W; {
|
||
v := line[col]
|
||
end := col + 1
|
||
for end < sp.W && line[end] == v {
|
||
end++
|
||
}
|
||
if v != cellClear {
|
||
s := ink
|
||
if v >= 0 && !mono {
|
||
s = Slot(v)
|
||
}
|
||
g.Rect(x+col*scale, y+row*scale, (end-col)*scale, scale, s)
|
||
}
|
||
col = end
|
||
}
|
||
}
|
||
}
|