Doodad Edit Mode: Saving and Loading From Disk

Adds the first features to Edit Mode to support creation of Doodad
files! The "New Doodad" button pops up a prompt for a Doodad size
(default 100px) and configures the Canvas widget and makes a Doodad
struct instead of a Level to manage.

* Move the custom Canvas widget from `level.Canvas` to `uix.Canvas`
  (the uix package is for our custom UI widgets now)
* Rename the `doodads.Doodad` interface (for runtime instances of
  Doodads) to `doodads.Actor` and make `doodads.Doodad` describe the
  file format and JSON schema instead.
* Rename the `EditLevel()` method to `EditDrawing()` and it inspects the
  file extension to know whether to launch the Edit Mode for a Level or
  for a Doodad drawing.
* Doodads can be edited by using the `-edit` CLI flag or using the
  in-game file open features (including `edit` command of dev console).
* Add a `Scrollable` boolean to uix.Canvas to restrict the keyboard
  being able to scroll the level, for editing Doodads which have a fixed
  size.
This commit is contained in:
Noah 2018-09-26 10:04:46 -07:00
parent e25869644c
commit a7fd3aa1ca
17 changed files with 489 additions and 218 deletions

View File

@ -7,4 +7,7 @@ var (
// Default chunk size for canvases.
ChunkSize = 1000
// Default size for a new Doodad.
DoodadSize = 100
)

View File

@ -43,7 +43,7 @@ func main() {
app.SetupEngine()
if filename != "" {
if edit {
app.EditLevel(filename)
app.EditDrawing(filename)
} else {
app.PlayLevel(filename)
}

View File

@ -132,7 +132,7 @@ func (c Command) Edit(d *Doodle) error {
filename := c.Args[0]
d.shell.Write("Editing level: " + filename)
d.EditLevel(filename)
d.EditDrawing(filename)
return nil
}

78
doodads/actor.go Normal file
View File

@ -0,0 +1,78 @@
package doodads
import (
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
)
// Actor is a reusable run-time drawing component used in Doodle. Actors are an
// active instance of a Doodad which have a position, velocity, etc.
type Actor interface {
ID() string
// Position and velocity, not saved to disk.
Position() render.Point
Velocity() render.Point
Size() render.Rect
Grounded() bool
SetGrounded(bool)
// Movement commands.
MoveBy(render.Point) // Add {X,Y} to current Position.
MoveTo(render.Point) // Set current Position to {X,Y}.
// Implement the Draw function.
Draw(render.Engine)
}
// GetBoundingRect computes the full pairs of points for the collision box
// around a doodad actor.
func GetBoundingRect(d Actor) render.Rect {
var (
P = d.Position()
S = d.Size()
)
return render.Rect{
X: P.X,
Y: P.Y,
W: S.W,
H: S.H,
}
}
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and
// returns if any of them intersect with level geometry.
func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
col := GetCollisionBox(box)
c.ScanGridLine(col.Top[0], col.Top[1], grid, Top)
c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom)
c.ScanGridLine(col.Left[0], col.Left[1], grid, Left)
c.ScanGridLine(col.Right[0], col.Right[1], grid, Right)
return c.IsColliding()
}
// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests
// for any pixels to be set, implying a collision between level geometry and the
// bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
for point := range render.IterLine2(p1, p2) {
if _, err := grid.Get(point); err == nil {
// A hit!
switch side {
case Top:
c.Top = true
c.TopPoint = point
case Bottom:
c.Bottom = true
c.BottomPoint = point
case Left:
c.Left = true
c.LeftPoint = point
case Right:
c.Right = true
c.RightPoint = point
}
}
}
}

View File

@ -5,27 +5,6 @@ import (
"git.kirsle.net/apps/doodle/render"
)
// Doodad is a reusable drawing component used in Doodle. Doodads are buttons,
// doors, switches, the player characters themselves, anything that isn't a part
// of the level geometry.
type Doodad interface {
ID() string
// Position and velocity, not saved to disk.
Position() render.Point
Velocity() render.Point
Size() render.Rect
Grounded() bool
SetGrounded(bool)
// Movement commands.
MoveBy(render.Point) // Add {X,Y} to current Position.
MoveTo(render.Point) // Set current Position to {X,Y}.
// Implement the Draw function.
Draw(render.Engine)
}
// Collide describes how a collision occurred.
type Collide struct {
Top bool
@ -60,6 +39,52 @@ type CollisionBox struct {
Right []render.Point
}
// GetCollisionBox returns a CollisionBox with the four coordinates.
func GetCollisionBox(box render.Rect) CollisionBox {
return CollisionBox{
Top: []render.Point{
{
X: box.X,
Y: box.Y,
},
{
X: box.X + box.W,
Y: box.Y,
},
},
Bottom: []render.Point{
{
X: box.X,
Y: box.Y + box.H,
},
{
X: box.X + box.W,
Y: box.Y + box.H,
},
},
Left: []render.Point{
{
X: box.X,
Y: box.Y + box.H - 1,
},
{
X: box.X,
Y: box.Y + 1,
},
},
Right: []render.Point{
{
X: box.X + box.W,
Y: box.Y + box.H - 1,
},
{
X: box.X + box.W,
Y: box.Y + 1,
},
},
}
}
// Side of the collision box (top, bottom, left, right)
type Side uint8
@ -72,7 +97,7 @@ const (
)
// CollidesWithGrid checks if a Doodad collides with level geometry.
func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Collide, bool) {
func CollidesWithGrid(d Actor, grid *level.Chunker, target render.Point) (*Collide, bool) {
var (
P = d.Position()
S = d.Size()
@ -221,100 +246,3 @@ func CollidesWithGrid(d Doodad, grid *level.Chunker, target render.Point) (*Coll
func (c *Collide) IsColliding() bool {
return c.Top || c.Bottom || c.Left || c.Right
}
// GetBoundingRect computes the full pairs of points for the collision box
// around a doodad.
func GetBoundingRect(d Doodad) render.Rect {
var (
P = d.Position()
S = d.Size()
)
return render.Rect{
X: P.X,
Y: P.Y,
W: S.W,
H: S.H,
}
}
func GetCollisionBox(box render.Rect) CollisionBox {
return CollisionBox{
Top: []render.Point{
{
X: box.X,
Y: box.Y,
},
{
X: box.X + box.W,
Y: box.Y,
},
},
Bottom: []render.Point{
{
X: box.X,
Y: box.Y + box.H,
},
{
X: box.X + box.W,
Y: box.Y + box.H,
},
},
Left: []render.Point{
{
X: box.X,
Y: box.Y + box.H - 1,
},
{
X: box.X,
Y: box.Y + 1,
},
},
Right: []render.Point{
{
X: box.X + box.W,
Y: box.Y + box.H - 1,
},
{
X: box.X + box.W,
Y: box.Y + 1,
},
},
}
}
// ScanBoundingBox scans all of the pixels in a bounding box on the grid and
// returns if any of them intersect with level geometry.
func (c *Collide) ScanBoundingBox(box render.Rect, grid *level.Chunker) bool {
col := GetCollisionBox(box)
c.ScanGridLine(col.Top[0], col.Top[1], grid, Top)
c.ScanGridLine(col.Bottom[0], col.Bottom[1], grid, Bottom)
c.ScanGridLine(col.Left[0], col.Left[1], grid, Left)
c.ScanGridLine(col.Right[0], col.Right[1], grid, Right)
return c.IsColliding()
}
// ScanGridLine scans all of the pixels between p1 and p2 on the grid and tests
// for any pixels to be set, implying a collision between level geometry and the
// bounding boxes of the doodad.
func (c *Collide) ScanGridLine(p1, p2 render.Point, grid *level.Chunker, side Side) {
for point := range render.IterLine2(p1, p2) {
if _, err := grid.Get(point); err == nil {
// A hit!
switch side {
case Top:
c.Top = true
c.TopPoint = point
case Bottom:
c.Bottom = true
c.BottomPoint = point
case Left:
c.Left = true
c.LeftPoint = point
case Right:
c.Right = true
c.RightPoint = point
}
}
}
}

48
doodads/doodad.go Normal file
View File

@ -0,0 +1,48 @@
package doodads
import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/level"
)
// Doodad is a reusable component for Levels that have scripts and graphics.
type Doodad struct {
level.Base
Palette *level.Palette `json:"palette"`
Script string `json:"script"`
Layers []Layer `json:"layers"`
}
// Layer holds a layer of drawing data for a Doodad.
type Layer struct {
Name string `json:"name"`
Chunker *level.Chunker `json:"chunks"`
}
// New creates a new Doodad.
func New(size int) *Doodad {
if size == 0 {
size = balance.DoodadSize
}
return &Doodad{
Base: level.Base{
Version: 1,
},
Palette: level.DefaultPalette(),
Layers: []Layer{
{
Name: "main",
Chunker: level.NewChunker(size),
},
},
}
}
// Inflate attaches the pixels to their swatches after loading from disk.
func (d *Doodad) Inflate() {
d.Palette.Inflate()
for _, layer := range d.Layers {
layer.Chunker.Inflate(d.Palette)
}
}

54
doodads/json.go Normal file
View File

@ -0,0 +1,54 @@
package doodads
import (
"bytes"
"encoding/json"
"fmt"
"io/ioutil"
"os"
)
// ToJSON serializes the doodad as JSON.
func (d *Doodad) ToJSON() ([]byte, error) {
out := bytes.NewBuffer([]byte{})
encoder := json.NewEncoder(out)
encoder.SetIndent("", "\t")
err := encoder.Encode(d)
return out.Bytes(), err
}
// WriteJSON writes a Doodad to JSON on disk.
func (d *Doodad) WriteJSON(filename string) error {
json, err := d.ToJSON()
if err != nil {
return fmt.Errorf("Doodad.WriteJSON: JSON encode error: %s", err)
}
err = ioutil.WriteFile(filename, json, 0755)
if err != nil {
return fmt.Errorf("Doodad.WriteJSON: WriteFile error: %s", err)
}
return nil
}
// LoadJSON loads a map from JSON file.
func LoadJSON(filename string) (*Doodad, error) {
fh, err := os.Open(filename)
if err != nil {
return nil, err
}
defer fh.Close()
// Decode the JSON file from disk.
d := New(0)
decoder := json.NewDecoder(fh)
err = decoder.Decode(&d)
if err != nil {
return d, fmt.Errorf("doodad.LoadJSON: JSON decode error: %s", err)
}
// Inflate the chunk metadata to map the pixels to their palette indexes.
d.Inflate()
return d, err
}

View File

@ -1,8 +1,11 @@
package doodle
import (
"fmt"
"strings"
"time"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/render"
"github.com/kirsle/golog"
)
@ -163,13 +166,41 @@ func (d *Doodle) NewMap() {
d.Goto(scene)
}
// EditLevel loads a map from JSON into the EditorScene.
func (d *Doodle) EditLevel(filename string) error {
log.Info("Loading level from file: %s", filename)
// NewDoodad loads a new Doodad in Edit Mode.
func (d *Doodle) NewDoodad(size int) {
log.Info("Starting a new doodad")
scene := &EditorScene{
DrawingType: enum.DoodadDrawing,
DoodadSize: size,
}
d.Goto(scene)
}
// EditDrawing loads a drawing (Level or Doodad) in Edit Mode.
func (d *Doodle) EditDrawing(filename string) error {
log.Info("Loading drawing from file: %s", filename)
parts := strings.Split(filename, ".")
if len(parts) < 2 {
return fmt.Errorf("filename `%s` has no file extension", filename)
}
ext := strings.ToLower(parts[len(parts)-1])
scene := &EditorScene{
Filename: filename,
OpenFile: true,
}
switch ext {
case "level":
case "map":
log.Info("is a LEvel type")
scene.DrawingType = enum.LevelDrawing
case "doodad":
scene.DrawingType = enum.DoodadDrawing
default:
return fmt.Errorf("file extension '%s' doesn't indicate its drawing type", ext)
}
d.Goto(scene)
return nil
}

View File

@ -1,32 +1,38 @@
package doodle
import (
"errors"
"fmt"
"io/ioutil"
"os"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/uix"
)
// EditorScene manages the "Edit Level" game mode.
type EditorScene struct {
// Configuration for the scene initializer.
DrawingType enum.DrawingType
OpenFile bool
Filename string
DoodadSize int
UI *EditorUI
// The current level being edited.
DrawingType enum.DrawingType
// The current level or doodad object being edited, based on the
// DrawingType.
Level *level.Level
Doodad *doodads.Doodad
// The canvas widget that contains the map we're working on.
// XXX: in dev builds this is available at $ d.Scene.GetDrawing()
drawing *level.Canvas
drawing *uix.Canvas
// Last saved filename by the user.
filename string
@ -39,7 +45,7 @@ func (s *EditorScene) Name() string {
// Setup the editor scene.
func (s *EditorScene) Setup(d *Doodle) error {
s.drawing = level.NewCanvas(balance.ChunkSize, true)
s.drawing = uix.NewCanvas(balance.ChunkSize, true)
if len(s.drawing.Palette.Swatches) > 0 {
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0])
}
@ -49,12 +55,16 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.drawing.Resize(render.NewRect(d.width-150, d.height-44))
s.drawing.Compute(d.Engine)
// // Were we given configuration data?
// Were we given configuration data?
if s.Filename != "" {
log.Debug("EditorScene.Setup: Set filename to %s", s.Filename)
s.filename = s.Filename
s.Filename = ""
}
// Loading a Level or a Doodad?
switch s.DrawingType {
case enum.LevelDrawing:
if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller")
s.drawing.LoadLevel(s.Level)
@ -71,6 +81,31 @@ func (s *EditorScene) Setup(d *Doodle) error {
s.Level = level.New()
s.Level.Palette = level.DefaultPalette()
s.drawing.LoadLevel(s.Level)
s.drawing.ScrollTo(render.Origin)
s.drawing.Scrollable = true
}
case enum.DoodadDrawing:
// No Doodad?
if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading doodad from filename at %s", s.filename)
if err := s.LoadDoodad(s.filename); err != nil {
d.Flash("LoadDoodad error: %s", err)
}
}
// No Doodad?
if s.Doodad == nil {
log.Debug("EditorScene.Setup: initializing a new Doodad")
s.Doodad = doodads.New(s.DoodadSize)
s.drawing.LoadDoodad(s.Doodad)
}
// TODO: move inside the UI. Just an approximate position for now.
s.drawing.MoveTo(render.NewPoint(200, 200))
s.drawing.Resize(render.NewRect(int32(s.DoodadSize), int32(s.DoodadSize)))
s.drawing.ScrollTo(render.Origin)
s.drawing.Scrollable = false
s.drawing.Compute(d.Engine)
}
// Initialize the user interface. It references the palette and such so it
@ -127,7 +162,11 @@ func (s *EditorScene) LoadLevel(filename string) error {
// SaveLevel saves the level to disk.
// TODO: move this into the Canvas?
func (s *EditorScene) SaveLevel(filename string) {
func (s *EditorScene) SaveLevel(filename string) error {
if s.DrawingType != enum.LevelDrawing {
return errors.New("SaveLevel: current drawing is not a Level type")
}
s.filename = filename
m := s.Level
@ -143,15 +182,54 @@ func (s *EditorScene) SaveLevel(filename string) {
json, err := m.ToJSON()
if err != nil {
log.Error("SaveLevel error: %s", err)
return
return fmt.Errorf("SaveLevel error: %s", err)
}
err = ioutil.WriteFile(filename, json, 0644)
if err != nil {
log.Error("Create map file error: %s", err)
return
return fmt.Errorf("Create map file error: %s", err)
}
return nil
}
// LoadDoodad loads a doodad from disk.
func (s *EditorScene) LoadDoodad(filename string) error {
s.filename = filename
doodad, err := doodads.LoadJSON(filename)
if err != nil {
return fmt.Errorf("EditorScene.LoadDoodad(%s): %s", filename, err)
}
s.DrawingType = enum.DoodadDrawing
s.Doodad = doodad
s.DoodadSize = doodad.Layers[0].Chunker.Size
s.drawing.LoadDoodad(s.Doodad)
return nil
}
// SaveDoodad saves the doodad to disk.
func (s *EditorScene) SaveDoodad(filename string) error {
if s.DrawingType != enum.DoodadDrawing {
return errors.New("SaveDoodad: current drawing is not a Doodad type")
}
s.filename = filename
d := s.Doodad
if d.Title == "" {
d.Title = "Untitled Doodad"
}
if d.Author == "" {
d.Author = os.Getenv("USER")
}
// TODO: is this copying necessary?
d.Palette = s.drawing.Palette
d.Layers[0].Chunker = s.drawing.Chunker()
err := d.WriteJSON(s.filename)
return err
}
// Destroy the scene.

View File

@ -1,11 +1,11 @@
package doodle
import "git.kirsle.net/apps/doodle/level"
import "git.kirsle.net/apps/doodle/uix"
// TODO: build flags to not include this in production builds.
// This adds accessors for private variables from the dev console.
// GetDrawing returns the level.Canvas
func (w *EditorScene) GetDrawing() *level.Canvas {
// GetDrawing returns the uix.Canvas
func (w *EditorScene) GetDrawing() *uix.Canvas {
return w.drawing
}

View File

@ -2,8 +2,10 @@ package doodle
import (
"fmt"
"strconv"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
@ -63,11 +65,16 @@ func (u *EditorUI) Loop(ev *events.State) {
// Statusbar filename label.
filename := "untitled.map"
fileType := "Level"
if u.Scene.filename != "" {
filename = u.Scene.filename
}
u.StatusFilenameText = fmt.Sprintf("Filename: %s",
if u.Scene.DrawingType == enum.DoodadDrawing {
fileType = "Doodad"
}
u.StatusFilenameText = fmt.Sprintf("Filename: %s (%s)",
filename,
fileType,
)
u.MenuBar.Compute(u.d.Engine)
@ -110,20 +117,52 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
menuButton{
Text: "New Doodad",
Click: func(render.Point) {
d.NewMap()
d.Prompt("Doodad size [100]>", func(answer string) {
size := balance.DoodadSize
if answer != "" {
i, err := strconv.Atoi(answer)
if err != nil {
d.Flash("Error: Doodad size must be a number.")
return
}
size = i
}
d.NewDoodad(size)
})
},
},
menuButton{
Text: "Save",
Click: func(render.Point) {
var saveFunc func(filename string)
switch u.Scene.DrawingType {
case enum.LevelDrawing:
saveFunc = func(filename string) {
if err := u.Scene.SaveLevel(filename); err != nil {
d.Flash("Error: %s", err)
} else {
d.Flash("Saved level: %s", filename)
}
}
case enum.DoodadDrawing:
saveFunc = func(filename string) {
if err := u.Scene.SaveDoodad(filename); err != nil {
d.Flash("Error: %s", err)
} else {
d.Flash("Saved doodad: %s", filename)
}
}
default:
d.Flash("Error: Scene.DrawingType is not a valid type")
}
if u.Scene.filename != "" {
u.Scene.SaveLevel(u.Scene.filename)
d.Flash("Saved: %s", u.Scene.filename)
saveFunc(u.Scene.filename)
} else {
d.Prompt("Save filename>", func(answer string) {
if answer != "" {
u.Scene.SaveLevel("./maps/" + answer) // TODO: maps path
d.Flash("Saved: %s", answer)
saveFunc(answer)
}
})
}
@ -145,7 +184,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
Click: func(render.Point) {
d.Prompt("Open filename>", func(answer string) {
if answer != "" {
u.d.EditLevel("./maps/" + answer) // TODO: maps path
u.d.EditDrawing("./maps/" + answer) // TODO: maps path
}
})
},

2
fps.go
View File

@ -58,7 +58,7 @@ func (d *Doodle) DrawDebugOverlay() {
}
// DrawCollisionBox draws the collision box around a Doodad.
func (d *Doodle) DrawCollisionBox(actor doodads.Doodad) {
func (d *Doodle) DrawCollisionBox(actor doodads.Actor) {
if !d.Debug || !DebugCollision {
return
}

View File

@ -221,7 +221,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
}))
button2.Handle(ui.Click, func(p render.Point) {
d.Prompt("Map name>", func(name string) {
d.EditLevel(name)
d.EditDrawing(name)
})
})

View File

@ -50,14 +50,5 @@ func LoadJSON(filename string) (*Level, error) {
// Inflate the private instance values.
m.Palette.Inflate()
for _, px := range m.Pixels {
if int(px.PaletteIndex) > len(m.Palette.Swatches) {
return nil, fmt.Errorf(
"pixel %s references palette index %d but there are only %d swatches in the palette",
px, px.PaletteIndex, len(m.Palette.Swatches),
)
}
px.Swatch = m.Palette.Swatches[px.PaletteIndex]
}
return m, err
}

View File

@ -8,37 +8,36 @@ import (
"git.kirsle.net/apps/doodle/render"
)
// Level is the container format for Doodle map drawings.
type Level struct {
// Base provides the common struct keys that are shared between Levels and
// Doodads.
type Base struct {
Version int `json:"version"` // File format version spec.
GameVersion string `json:"gameVersion"` // Game version that created the level.
Title string `json:"title"`
Author string `json:"author"`
}
// Level is the container format for Doodle map drawings.
type Level struct {
Base
Password string `json:"passwd"`
Locked bool `json:"locked"`
// Chunked pixel data.
Chunker *Chunker `json:"chunks"`
// XXX: deprecated?
Width int32 `json:"w"`
Height int32 `json:"h"`
// The Palette holds the unique "colors" used in this map file, and their
// properties (solid, fire, slippery, etc.)
Palette *Palette `json:"palette"`
// Pixels is a 2D array indexed by [X][Y]. The cell values are indexes into
// the Palette.
Pixels []*Pixel `json:"pixels"`
}
// New creates a blank level object with all its members initialized.
func New() *Level {
return &Level{
Base: Base{
Version: 1,
},
Chunker: NewChunker(balance.ChunkSize),
Pixels: []*Pixel{},
Palette: &Palette{},
}
}

View File

@ -8,6 +8,7 @@ import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/uix"
)
// PlayScene manages the "Edit Level" game mode.
@ -17,10 +18,10 @@ type PlayScene struct {
Level *level.Level
// Private variables.
drawing *level.Canvas
drawing *uix.Canvas
// Player character
Player doodads.Doodad
Player doodads.Actor
}
// Name of the scene.
@ -30,7 +31,7 @@ func (s *PlayScene) Name() string {
// Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error {
s.drawing = level.NewCanvas(balance.ChunkSize, false)
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
s.drawing.MoveTo(render.Origin)
s.drawing.Resize(render.NewRect(d.width, d.height))
s.drawing.Compute(d.Engine)

View File

@ -1,8 +1,10 @@
package level
package uix
import (
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
@ -10,32 +12,37 @@ import (
// Canvas is a custom ui.Widget that manages a single drawing.
type Canvas struct {
ui.Frame
Palette *Palette
Palette *level.Palette
// Set to true to allow clicking to edit this canvas.
Editable bool
Scrollable bool
chunks *Chunker
pixelHistory []*Pixel
lastPixel *Pixel
chunks *level.Chunker
pixelHistory []*level.Pixel
lastPixel *level.Pixel
// We inherit the ui.Widget which manages the width and height.
Scroll render.Point // Scroll offset for which parts of canvas are visible.
}
// NewCanvas initializes a Canvas widget.
//
// If editable is true, Scrollable is also set to true, which means the arrow
// keys will scroll the canvas viewport which is desirable in Edit Mode.
func NewCanvas(size int, editable bool) *Canvas {
w := &Canvas{
Editable: editable,
Palette: NewPalette(),
chunks: NewChunker(size),
Scrollable: editable,
Palette: level.NewPalette(),
chunks: level.NewChunker(size),
}
w.setup()
return w
}
// Load initializes the Canvas using an existing Palette and Grid.
func (w *Canvas) Load(p *Palette, g *Chunker) {
func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
w.Palette = p
w.chunks = g
@ -45,12 +52,18 @@ func (w *Canvas) Load(p *Palette, g *Chunker) {
}
// LoadLevel initializes a Canvas from a Level object.
func (w *Canvas) LoadLevel(level *Level) {
func (w *Canvas) LoadLevel(level *level.Level) {
w.Load(level.Palette, level.Chunker)
}
// LoadDoodad initializes a Canvas from a Doodad object.
func (w *Canvas) LoadDoodad(d *doodads.Doodad) {
// TODO more safe
w.Load(d.Palette, d.Layers[0].Chunker)
}
// SetSwatch changes the currently selected swatch for editing.
func (w *Canvas) SetSwatch(s *Swatch) {
func (w *Canvas) SetSwatch(s *level.Swatch) {
w.Palette.ActiveSwatch = s
}
@ -73,6 +86,7 @@ func (w *Canvas) Loop(ev *events.State) error {
_ = P
)
if w.Scrollable {
// Arrow keys to scroll the view.
scrollBy := render.Point{}
if ev.Right.Now {
@ -88,6 +102,7 @@ func (w *Canvas) Loop(ev *events.State) error {
if !scrollBy.IsZero() {
w.ScrollBy(scrollBy)
}
}
// Only care if the cursor is over our space.
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
@ -108,7 +123,7 @@ func (w *Canvas) Loop(ev *events.State) error {
X: ev.CursorX.Now - P.X + w.Scroll.X,
Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
}
pixel := &Pixel{
pixel := &level.Pixel{
X: cursor.X,
Y: cursor.Y,
Swatch: w.Palette.ActiveSwatch,
@ -152,10 +167,16 @@ func (w *Canvas) Viewport() render.Rect {
}
// Chunker returns the underlying Chunker object.
func (w *Canvas) Chunker() *Chunker {
func (w *Canvas) Chunker() *level.Chunker {
return w.chunks
}
// ScrollTo sets the viewport scroll position.
func (w *Canvas) ScrollTo(to render.Point) {
w.Scroll.X = to.X
w.Scroll.Y = to.Y
}
// ScrollBy adjusts the viewport scroll position.
func (w *Canvas) ScrollBy(by render.Point) {
w.Scroll.Add(by)