Draw Actors Embedded in Levels in Edit Mode
Add the JSON format for embedding Actors (Doodad instances) inside of a Level. I made a test map that manually inserted a couple of actors. Actors are given to the Canvas responsible for the Level via the function `InstallActors()`. So it means you'll call LoadLevel and then InstallActors to hook everything up. The Canvas creates sub-Canvas widgets from each Actor. After drawing the main level geometry from the Canvas.Chunker, it calls the drawActors() function which does the same but for Actors. Levels keep a global map of all Actors that exist. For any Actors that are visible within the Viewport, their sub-Canvas widgets are presented appropriately on top of the parent Canvas. In case their sub-Canvas overlaps the parent's boundaries, their sub-Canvas is resized and moved appropriately. - Allow the MainWindow to be resized at run time, and the UI recalculates its sizing and position. - Made the in-game Shell properties editable via environment variables. The kirsle.env file sets a blue and pink color scheme. - Begin the ground work for Levels and Doodads to embed files inside their data via the level.FileSystem type. - UI: Labels can now contain line break characters. It will appropriately render multiple lines of render.Text and take into account the proper BoxSize to contain them all. - Add environment variable DOODLE_DEBUG_ALL=true that will turn on ALL debug overlay and visualization options. - Add debug overlay to "tag" each Canvas widget with some of its details, like its Name and World Position. Can be enabled with the environment variable DEBUG_CANVAS_LABEL=true - Improved the FPS debug overlay to show in labeled columns and multiple colors, with easy ability to add new data points to it.
This commit is contained in:
parent
1c5a0842e4
commit
20771fbe13
|
@ -21,12 +21,16 @@ like `#FF00FF99` for 153 ($99) on the alpha channel.
|
||||||
* `D_SHELL_FS=16`: font size for both the shell and on-screen flashed
|
* `D_SHELL_FS=16`: font size for both the shell and on-screen flashed
|
||||||
messages.
|
messages.
|
||||||
* Debug Colors and Hitboxes (default invisible=off):
|
* Debug Colors and Hitboxes (default invisible=off):
|
||||||
|
* `DOODLE_DEBUG_ALL=false`: turn on all debug colors and hitboxes to their
|
||||||
|
default colors and settings.
|
||||||
* `DEBUG_CHUNK_COLOR=#FFFFFF`: background color when caching a
|
* `DEBUG_CHUNK_COLOR=#FFFFFF`: background color when caching a
|
||||||
chunk to bitmap. Helps visualize where the chunks and caching
|
chunk to bitmap. Helps visualize where the chunks and caching
|
||||||
are happening.
|
are happening.
|
||||||
* `DEBUG_CANVAS_BORDER`: draw a border color around every uix.Canvas
|
* `DEBUG_CANVAS_BORDER`: draw a border color around every uix.Canvas
|
||||||
widget. This effectively draws the bounds of every Doodad drawn on top
|
widget. This effectively draws the bounds of every Doodad drawn on top
|
||||||
of a level or inside a button and the bounds of the level space itself.
|
of a level or inside a button and the bounds of the level space itself.
|
||||||
|
* `DEBUG_CANVAS_LABEL=false`: draw a label in the corner of every Canvas
|
||||||
|
with details about the Canvas.
|
||||||
* Tuning constants (may not be available in production builds):
|
* Tuning constants (may not be available in production builds):
|
||||||
* `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows
|
* `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows
|
||||||
in the Editor Mode, in pixels per tick.
|
in the Editor Mode, in pixels per tick.
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
package balance
|
package balance
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
@ -15,13 +15,21 @@ var (
|
||||||
* Visualizers *
|
* Visualizers *
|
||||||
***************/
|
***************/
|
||||||
|
|
||||||
|
// Debug overlay (FPS etc.) settings.
|
||||||
|
DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf"
|
||||||
|
DebugFontSize = 15
|
||||||
|
DebugLabelColor = render.MustHexColor("#FF9900")
|
||||||
|
DebugValueColor = render.MustHexColor("#00CCFF")
|
||||||
|
DebugStrokeDarken int32 = 80
|
||||||
|
|
||||||
// Background color to use when exporting a drawing Chunk as a bitmap image
|
// Background color to use when exporting a drawing Chunk as a bitmap image
|
||||||
// on disk. Default is white. Setting this to translucent yellow is a great
|
// on disk. Default is white. Setting this to translucent yellow is a great
|
||||||
// way to visualize the chunks loaded from cache on your screen.
|
// way to visualize the chunks loaded from cache on your screen.
|
||||||
DebugChunkBitmapBackground = render.White // XXX: export $DEBUG_CHUNK_COLOR
|
DebugChunkBitmapBackground = render.White // XXX: export $DEBUG_CHUNK_COLOR
|
||||||
|
|
||||||
// Put a border around all Canvas widgets.
|
// Put a border around all Canvas widgets.
|
||||||
DebugCanvasBorder = render.Red
|
DebugCanvasBorder = render.Invisible
|
||||||
|
DebugCanvasLabel = false // Tag the canvas with a label.
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
@ -45,24 +53,33 @@ func init() {
|
||||||
// Visualizers
|
// Visualizers
|
||||||
"DEBUG_CHUNK_COLOR": &DebugChunkBitmapBackground,
|
"DEBUG_CHUNK_COLOR": &DebugChunkBitmapBackground,
|
||||||
"DEBUG_CANVAS_BORDER": &DebugCanvasBorder,
|
"DEBUG_CANVAS_BORDER": &DebugCanvasBorder,
|
||||||
|
"DEBUG_CANVAS_LABEL": &DebugCanvasLabel,
|
||||||
}
|
}
|
||||||
for name, value := range config {
|
for name, value := range config {
|
||||||
switch v := value.(type) {
|
switch v := value.(type) {
|
||||||
case *int:
|
case *int:
|
||||||
*v = IntEnv(name, *(v))
|
*v = IntEnv(name, *(v))
|
||||||
|
case *bool:
|
||||||
|
*v = BoolEnv(name, *(v))
|
||||||
case *int32:
|
case *int32:
|
||||||
*v = int32(IntEnv(name, int(*(v))))
|
*v = int32(IntEnv(name, int(*(v))))
|
||||||
case *render.Color:
|
case *render.Color:
|
||||||
*v = ColorEnv(name, *(v))
|
*v = ColorEnv(name, *(v))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Debug all?
|
||||||
|
if BoolEnv("DOODLE_DEBUG_ALL", false) {
|
||||||
|
DebugChunkBitmapBackground = render.RGBA(255, 255, 0, 128)
|
||||||
|
DebugCanvasBorder = render.Red
|
||||||
|
DebugCanvasLabel = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ColorEnv gets a color value from environment variable or returns a default.
|
// ColorEnv gets a color value from environment variable or returns a default.
|
||||||
// This will panic if the color is not valid, so only do this on startup time.
|
// This will panic if the color is not valid, so only do this on startup time.
|
||||||
func ColorEnv(name string, v render.Color) render.Color {
|
func ColorEnv(name string, v render.Color) render.Color {
|
||||||
if color := os.Getenv(name); color != "" {
|
if color := os.Getenv(name); color != "" {
|
||||||
fmt.Printf("set %s to %s\n", name, color)
|
|
||||||
return render.MustHexColor(color)
|
return render.MustHexColor(color)
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
|
@ -79,3 +96,16 @@ func IntEnv(name string, v int) int {
|
||||||
}
|
}
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// BoolEnv gets a bool from the environment with a default.
|
||||||
|
func BoolEnv(name string, v bool) bool {
|
||||||
|
if env := os.Getenv(name); env != "" {
|
||||||
|
switch strings.ToLower(env) {
|
||||||
|
case "true", "t", "1", "on", "yes", "y":
|
||||||
|
return true
|
||||||
|
case "false", "f", "0", "off", "no", "n":
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
|
|
@ -6,13 +6,13 @@ import (
|
||||||
|
|
||||||
// Shell related variables.
|
// Shell related variables.
|
||||||
var (
|
var (
|
||||||
// TODO: why not renders transparent
|
|
||||||
ShellFontFilename = "./fonts/DejaVuSansMono.ttf"
|
ShellFontFilename = "./fonts/DejaVuSansMono.ttf"
|
||||||
ShellBackgroundColor = render.RGBA(0, 20, 40, 200)
|
ShellBackgroundColor = render.RGBA(0, 20, 40, 200)
|
||||||
ShellForegroundColor = render.RGBA(0, 153, 255, 255)
|
ShellForegroundColor = render.RGBA(0, 153, 255, 255)
|
||||||
ShellPromptColor = render.White
|
ShellPromptColor = render.White
|
||||||
ShellPadding int32 = 8
|
ShellPadding int32 = 8
|
||||||
ShellFontSize = 16
|
ShellFontSize = 16
|
||||||
|
ShellFontSizeSmall = 10
|
||||||
ShellCursorBlinkRate uint64 = 20
|
ShellCursorBlinkRate uint64 = 20
|
||||||
ShellHistoryLineCount = 8
|
ShellHistoryLineCount = 8
|
||||||
|
|
||||||
|
|
134
config.go
134
config.go
|
@ -2,106 +2,16 @@ package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"github.com/kirsle/configdir"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// Configuration constants.
|
// Regexp to match simple filenames for maps and doodads.
|
||||||
var (
|
var reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`)
|
||||||
DebugTextPadding int32 = 8
|
|
||||||
DebugTextSize = 24
|
|
||||||
DebugTextColor = render.SkyBlue
|
|
||||||
DebugTextStroke = render.Grey
|
|
||||||
DebugTextShadow = render.Black
|
|
||||||
)
|
|
||||||
|
|
||||||
// Profile Directory settings.
|
|
||||||
var (
|
|
||||||
ConfigDirectoryName = "doodle"
|
|
||||||
|
|
||||||
ProfileDirectory string
|
|
||||||
LevelDirectory string
|
|
||||||
DoodadDirectory string
|
|
||||||
|
|
||||||
CacheDirectory string
|
|
||||||
FontDirectory string
|
|
||||||
|
|
||||||
// Regexp to match simple filenames for maps and doodads.
|
|
||||||
reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// File extensions
|
|
||||||
const (
|
|
||||||
extLevel = ".level"
|
|
||||||
extDoodad = ".doodad"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Profile directory contains the user's levels and doodads.
|
|
||||||
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
|
|
||||||
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
|
||||||
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
|
||||||
|
|
||||||
// Cache directory to extract font files to.
|
|
||||||
CacheDirectory = configdir.LocalCache(ConfigDirectoryName)
|
|
||||||
FontDirectory = configdir.LocalCache(ConfigDirectoryName, "fonts")
|
|
||||||
|
|
||||||
// Ensure all the directories exist.
|
|
||||||
configdir.MakePath(LevelDirectory)
|
|
||||||
configdir.MakePath(DoodadDirectory)
|
|
||||||
configdir.MakePath(FontDirectory)
|
|
||||||
}
|
|
||||||
|
|
||||||
// LevelPath will turn a "simple" filename into an absolute path in the user's
|
|
||||||
// local levels folder. If the filename already contains slashes, it is returned
|
|
||||||
// as-is as an absolute or relative path.
|
|
||||||
func LevelPath(filename string) string {
|
|
||||||
return resolvePath(LevelDirectory, filename, extLevel)
|
|
||||||
}
|
|
||||||
|
|
||||||
// DoodadPath is like LevelPath but for Doodad files.
|
|
||||||
func DoodadPath(filename string) string {
|
|
||||||
return resolvePath(DoodadDirectory, filename, extDoodad)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ListDoodads returns a listing of all available doodads.
|
|
||||||
func ListDoodads() ([]string, error) {
|
|
||||||
var names []string
|
|
||||||
|
|
||||||
files, err := ioutil.ReadDir(DoodadDirectory)
|
|
||||||
if err != nil {
|
|
||||||
return names, err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
name := file.Name()
|
|
||||||
if strings.HasSuffix(strings.ToLower(name), extDoodad) {
|
|
||||||
names = append(names, name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return names, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// resolvePath is the inner logic for LevelPath and DoodadPath.
|
|
||||||
func resolvePath(directory, filename, extension string) string {
|
|
||||||
if strings.Contains(filename, "/") {
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
// Attach the file extension?
|
|
||||||
if strings.ToLower(filepath.Ext(filename)) != extension {
|
|
||||||
filename += extension
|
|
||||||
}
|
|
||||||
|
|
||||||
return filepath.Join(directory, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
EditFile opens a drawing file (Level or Doodad) in the EditorScene.
|
EditFile opens a drawing file (Level or Doodad) in the EditorScene.
|
||||||
|
@ -123,7 +33,7 @@ func (d *Doodle) EditFile(filename string) error {
|
||||||
if m := reSimpleFilename.FindStringSubmatch(filename); len(m) > 0 {
|
if m := reSimpleFilename.FindStringSubmatch(filename); len(m) > 0 {
|
||||||
log.Debug("EditFile: simple filename %s", filename)
|
log.Debug("EditFile: simple filename %s", filename)
|
||||||
extension := strings.ToLower(filepath.Ext(filename))
|
extension := strings.ToLower(filepath.Ext(filename))
|
||||||
if foundFilename := d.ResolvePath(filename, extension, false); foundFilename != "" {
|
if foundFilename := userdir.ResolvePath(filename, extension, false); foundFilename != "" {
|
||||||
log.Info("EditFile: resolved name '%s' to path %s", filename, foundFilename)
|
log.Info("EditFile: resolved name '%s' to path %s", filename, foundFilename)
|
||||||
absPath = foundFilename
|
absPath = foundFilename
|
||||||
} else {
|
} else {
|
||||||
|
@ -141,39 +51,3 @@ func (d *Doodle) EditFile(filename string) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolvePath takes an ambiguous simple filename and searches for a Level or
|
|
||||||
// Doodad that matches. Returns a blank string if no files found.
|
|
||||||
//
|
|
||||||
// Pass a true value for `one` if you are intending to create the file. It will
|
|
||||||
// only test one filepath and return the first one, regardless if the file
|
|
||||||
// existed. So the filename should have a ".level" or ".doodad" extension and
|
|
||||||
// then this path will resolve the ProfileDirectory of the file.
|
|
||||||
func (d *Doodle) ResolvePath(filename, extension string, one bool) string {
|
|
||||||
// If the filename exists outright, return it.
|
|
||||||
if _, err := os.Stat(filename); !os.IsNotExist(err) {
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
var paths []string
|
|
||||||
if extension == extLevel {
|
|
||||||
paths = append(paths, filepath.Join(LevelDirectory, filename))
|
|
||||||
} else if extension == extDoodad {
|
|
||||||
paths = append(paths, filepath.Join(DoodadDirectory, filename))
|
|
||||||
} else {
|
|
||||||
paths = append(paths,
|
|
||||||
filepath.Join(LevelDirectory, filename+".level"),
|
|
||||||
filepath.Join(DoodadDirectory, filename+".doodad"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, test := range paths {
|
|
||||||
log.Debug("findFilename: try to find '%s' as %s", filename, test)
|
|
||||||
if _, err := os.Stat(test); os.IsNotExist(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
return test
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
14
doodle.go
14
doodle.go
|
@ -7,6 +7,7 @@ import (
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/balance"
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
"git.kirsle.net/apps/doodle/enum"
|
||||||
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
"github.com/kirsle/golog"
|
"github.com/kirsle/golog"
|
||||||
)
|
)
|
||||||
|
@ -28,11 +29,15 @@ type Doodle struct {
|
||||||
Engine render.Engine
|
Engine render.Engine
|
||||||
engineReady bool
|
engineReady bool
|
||||||
|
|
||||||
|
// Easy access to the event state, for the debug overlay to use.
|
||||||
|
// Might not be thread safe.
|
||||||
|
event *events.State
|
||||||
|
|
||||||
startTime time.Time
|
startTime time.Time
|
||||||
running bool
|
running bool
|
||||||
ticks uint64
|
ticks uint64
|
||||||
width int32
|
width int
|
||||||
height int32
|
height int
|
||||||
|
|
||||||
// Command line shell options.
|
// Command line shell options.
|
||||||
shell Shell
|
shell Shell
|
||||||
|
@ -47,8 +52,8 @@ func New(debug bool, engine render.Engine) *Doodle {
|
||||||
Engine: engine,
|
Engine: engine,
|
||||||
startTime: time.Now(),
|
startTime: time.Now(),
|
||||||
running: true,
|
running: true,
|
||||||
width: int32(balance.Width),
|
width: balance.Width,
|
||||||
height: int32(balance.Height),
|
height: balance.Height,
|
||||||
}
|
}
|
||||||
d.shell = NewShell(d)
|
d.shell = NewShell(d)
|
||||||
|
|
||||||
|
@ -97,6 +102,7 @@ func (d *Doodle) Run() error {
|
||||||
d.running = false
|
d.running = false
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
d.event = ev
|
||||||
|
|
||||||
// Command line shell.
|
// Command line shell.
|
||||||
if d.shell.Open {
|
if d.shell.Open {
|
||||||
|
|
|
@ -11,6 +11,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
"git.kirsle.net/apps/doodle/enum"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -103,6 +104,18 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
||||||
|
|
||||||
// Loop the editor scene.
|
// Loop the editor scene.
|
||||||
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
||||||
|
// Has the window been resized?
|
||||||
|
if resized := ev.Resized.Read(); resized {
|
||||||
|
w, h := d.Engine.WindowSize()
|
||||||
|
if w != d.width || h != d.height {
|
||||||
|
// Not a false alarm.
|
||||||
|
d.width = w
|
||||||
|
d.height = h
|
||||||
|
s.UI.Resized(d)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
s.UI.Loop(ev)
|
s.UI.Loop(ev)
|
||||||
|
|
||||||
// Switching to Play Mode?
|
// Switching to Play Mode?
|
||||||
|
@ -133,6 +146,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
|
|
||||||
level, err := level.LoadJSON(filename)
|
level, err := level.LoadJSON(filename)
|
||||||
|
fmt.Printf("%+v\n", level)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
|
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
|
||||||
}
|
}
|
||||||
|
@ -140,6 +154,20 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
s.DrawingType = enum.LevelDrawing
|
s.DrawingType = enum.LevelDrawing
|
||||||
s.Level = level
|
s.Level = level
|
||||||
s.UI.Canvas.LoadLevel(s.Level)
|
s.UI.Canvas.LoadLevel(s.Level)
|
||||||
|
|
||||||
|
// TODO: debug
|
||||||
|
for i, actor := range level.Actors {
|
||||||
|
log.Info("Actor %s is a %s", i, actor.ID())
|
||||||
|
}
|
||||||
|
for name, file := range level.Files {
|
||||||
|
log.Info("File %s has: %s", name, file.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Info("Installing %d actors into the drawing", len(level.Actors))
|
||||||
|
if err := s.UI.Canvas.InstallActors(level.Actors); err != nil {
|
||||||
|
return fmt.Errorf("EditorScene.LoadLevel: InstallActors: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,8 +178,8 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
||||||
return errors.New("SaveLevel: current drawing is not a Level type")
|
return errors.New("SaveLevel: current drawing is not a Level type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasSuffix(filename, extLevel) {
|
if !strings.HasSuffix(filename, enum.LevelExt) {
|
||||||
filename += extLevel
|
filename += enum.LevelExt
|
||||||
}
|
}
|
||||||
|
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
|
@ -173,7 +201,7 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save it to their profile directory.
|
// Save it to their profile directory.
|
||||||
filename = LevelPath(filename)
|
filename = userdir.LevelPath(filename)
|
||||||
log.Info("Write Level: %s", filename)
|
log.Info("Write Level: %s", filename)
|
||||||
err = ioutil.WriteFile(filename, json, 0644)
|
err = ioutil.WriteFile(filename, json, 0644)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -205,8 +233,8 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
||||||
return errors.New("SaveDoodad: current drawing is not a Doodad type")
|
return errors.New("SaveDoodad: current drawing is not a Doodad type")
|
||||||
}
|
}
|
||||||
|
|
||||||
if !strings.HasSuffix(filename, extDoodad) {
|
if !strings.HasSuffix(filename, enum.DoodadExt) {
|
||||||
filename += extDoodad
|
filename += enum.DoodadExt
|
||||||
}
|
}
|
||||||
|
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
|
@ -223,7 +251,7 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
||||||
d.Layers[0].Chunker = s.UI.Canvas.Chunker()
|
d.Layers[0].Chunker = s.UI.Canvas.Chunker()
|
||||||
|
|
||||||
// Save it to their profile directory.
|
// Save it to their profile directory.
|
||||||
filename = DoodadPath(filename)
|
filename = userdir.DoodadPath(filename)
|
||||||
log.Info("Write Doodad: %s", filename)
|
log.Info("Write Doodad: %s", filename)
|
||||||
err := d.WriteJSON(filename)
|
err := d.WriteJSON(filename)
|
||||||
return err
|
return err
|
||||||
|
@ -231,5 +259,6 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
||||||
|
|
||||||
// Destroy the scene.
|
// Destroy the scene.
|
||||||
func (s *EditorScene) Destroy() error {
|
func (s *EditorScene) Destroy() error {
|
||||||
|
debugWorldIndex = render.Origin
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
118
editor_ui.go
118
editor_ui.go
|
@ -9,11 +9,15 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/enum"
|
"git.kirsle.net/apps/doodle/enum"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/ui"
|
||||||
"git.kirsle.net/apps/doodle/uix"
|
"git.kirsle.net/apps/doodle/uix"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Width of the panel frame.
|
||||||
|
var paletteWidth int32 = 150
|
||||||
|
|
||||||
// EditorUI manages the user interface for the Editor Scene.
|
// EditorUI manages the user interface for the Editor Scene.
|
||||||
type EditorUI struct {
|
type EditorUI struct {
|
||||||
d *Doodle
|
d *Doodle
|
||||||
|
@ -70,6 +74,8 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
||||||
u.Palette = u.SetupPalette(d)
|
u.Palette = u.SetupPalette(d)
|
||||||
u.Workspace = u.SetupWorkspace(d) // important that this is last!
|
u.Workspace = u.SetupWorkspace(d) // important that this is last!
|
||||||
|
|
||||||
|
u.Resized(d)
|
||||||
|
|
||||||
// Position the Canvas inside the frame.
|
// Position the Canvas inside the frame.
|
||||||
u.Workspace.Pack(u.Canvas, ui.Pack{
|
u.Workspace.Pack(u.Canvas, ui.Pack{
|
||||||
Anchor: ui.N,
|
Anchor: ui.N,
|
||||||
|
@ -84,14 +90,75 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
||||||
return u
|
return u
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resized handles the window being resized so we can recompute the widgets.
|
||||||
|
func (u *EditorUI) Resized(d *Doodle) {
|
||||||
|
// Menu Bar frame.
|
||||||
|
{
|
||||||
|
u.MenuBar.Configure(ui.Config{
|
||||||
|
Width: int32(d.width),
|
||||||
|
Background: render.Black,
|
||||||
|
})
|
||||||
|
u.MenuBar.Compute(d.Engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status Bar.
|
||||||
|
{
|
||||||
|
u.StatusBar.Configure(ui.Config{
|
||||||
|
Width: int32(d.width),
|
||||||
|
})
|
||||||
|
u.StatusBar.MoveTo(render.Point{
|
||||||
|
X: 0,
|
||||||
|
Y: int32(d.height) - u.StatusBar.Size().H,
|
||||||
|
})
|
||||||
|
u.StatusBar.Compute(d.Engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Palette panel.
|
||||||
|
{
|
||||||
|
u.Palette.Configure(ui.Config{
|
||||||
|
Width: paletteWidth,
|
||||||
|
Height: int32(u.d.height) - u.StatusBar.Size().H,
|
||||||
|
})
|
||||||
|
u.Palette.MoveTo(render.NewPoint(
|
||||||
|
int32(u.d.width)-u.Palette.BoxSize().W,
|
||||||
|
u.MenuBar.BoxSize().H,
|
||||||
|
))
|
||||||
|
u.Palette.Compute(d.Engine)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Position the workspace around with the other widgets.
|
||||||
|
{
|
||||||
|
frame := u.Workspace
|
||||||
|
frame.MoveTo(render.NewPoint(
|
||||||
|
0,
|
||||||
|
u.MenuBar.Size().H,
|
||||||
|
))
|
||||||
|
frame.Resize(render.NewRect(
|
||||||
|
int32(d.width)-u.Palette.Size().W,
|
||||||
|
int32(d.height)-u.MenuBar.Size().H-u.StatusBar.Size().H,
|
||||||
|
))
|
||||||
|
frame.Compute(d.Engine)
|
||||||
|
|
||||||
|
u.ExpandCanvas(d.Engine)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Loop to process events and update the UI.
|
// Loop to process events and update the UI.
|
||||||
func (u *EditorUI) Loop(ev *events.State) {
|
func (u *EditorUI) Loop(ev *events.State) {
|
||||||
u.Supervisor.Loop(ev)
|
u.Supervisor.Loop(ev)
|
||||||
|
|
||||||
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d)",
|
{
|
||||||
ev.CursorX.Now,
|
var P = u.Workspace.Point()
|
||||||
ev.CursorY.Now,
|
debugWorldIndex = render.NewPoint(
|
||||||
)
|
ev.CursorX.Now-P.X-u.Canvas.Scroll.X,
|
||||||
|
ev.CursorY.Now-P.Y-u.Canvas.Scroll.Y,
|
||||||
|
)
|
||||||
|
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d) Px: (%s)",
|
||||||
|
ev.CursorX.Now,
|
||||||
|
ev.CursorY.Now,
|
||||||
|
debugWorldIndex,
|
||||||
|
)
|
||||||
|
}
|
||||||
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
|
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
|
||||||
u.Canvas.Palette.ActiveSwatch,
|
u.Canvas.Palette.ActiveSwatch,
|
||||||
)
|
)
|
||||||
|
@ -139,18 +206,6 @@ func (u *EditorUI) Present(e render.Engine) {
|
||||||
// Canvas, so it can easily full-screen it or center it for Doodad editing.
|
// Canvas, so it can easily full-screen it or center it for Doodad editing.
|
||||||
func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame {
|
func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame {
|
||||||
frame := ui.NewFrame("Workspace")
|
frame := ui.NewFrame("Workspace")
|
||||||
|
|
||||||
// Position and size the frame around the other main widgets.
|
|
||||||
frame.MoveTo(render.NewPoint(
|
|
||||||
0,
|
|
||||||
u.MenuBar.Size().H,
|
|
||||||
))
|
|
||||||
frame.Resize(render.NewRect(
|
|
||||||
d.width-u.Palette.Size().W,
|
|
||||||
d.height-u.MenuBar.Size().H-u.StatusBar.Size().H,
|
|
||||||
))
|
|
||||||
frame.Compute(d.Engine)
|
|
||||||
|
|
||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -170,17 +225,17 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
|
||||||
// in its frame, but that would artificially expand the Canvas also when it
|
// in its frame, but that would artificially expand the Canvas also when it
|
||||||
// _wanted_ to be smaller, as in Doodad Editing Mode.
|
// _wanted_ to be smaller, as in Doodad Editing Mode.
|
||||||
func (u *EditorUI) ExpandCanvas(e render.Engine) {
|
func (u *EditorUI) ExpandCanvas(e render.Engine) {
|
||||||
u.Canvas.Resize(u.Workspace.Size())
|
if u.Scene.DrawingType == enum.LevelDrawing {
|
||||||
|
u.Canvas.Resize(u.Workspace.Size())
|
||||||
|
} else {
|
||||||
|
// Size is managed externally.
|
||||||
|
}
|
||||||
u.Workspace.Compute(e)
|
u.Workspace.Compute(e)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SetupMenuBar sets up the menu bar.
|
// SetupMenuBar sets up the menu bar.
|
||||||
func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
||||||
frame := ui.NewFrame("MenuBar")
|
frame := ui.NewFrame("MenuBar")
|
||||||
frame.Configure(ui.Config{
|
|
||||||
Width: d.width,
|
|
||||||
Background: render.Black,
|
|
||||||
})
|
|
||||||
|
|
||||||
type menuButton struct {
|
type menuButton struct {
|
||||||
Text string
|
Text string
|
||||||
|
@ -293,21 +348,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
||||||
|
|
||||||
// SetupPalette sets up the palette panel.
|
// SetupPalette sets up the palette panel.
|
||||||
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
||||||
var paletteWidth int32 = 150
|
|
||||||
|
|
||||||
window := ui.NewWindow("Palette")
|
window := ui.NewWindow("Palette")
|
||||||
window.ConfigureTitle(balance.TitleConfig)
|
window.ConfigureTitle(balance.TitleConfig)
|
||||||
window.TitleBar().Font = balance.TitleFont
|
window.TitleBar().Font = balance.TitleFont
|
||||||
window.Configure(ui.Config{
|
window.Configure(ui.Config{
|
||||||
Width: paletteWidth,
|
|
||||||
Height: u.d.height - u.StatusBar.Size().H,
|
|
||||||
Background: balance.WindowBackground,
|
Background: balance.WindowBackground,
|
||||||
BorderColor: balance.WindowBorder,
|
BorderColor: balance.WindowBorder,
|
||||||
})
|
})
|
||||||
window.MoveTo(render.NewPoint(
|
|
||||||
u.d.width-window.BoxSize().W,
|
|
||||||
u.MenuBar.BoxSize().H,
|
|
||||||
))
|
|
||||||
|
|
||||||
// Frame that holds the tab buttons in Level Edit mode.
|
// Frame that holds the tab buttons in Level Edit mode.
|
||||||
tabFrame := ui.NewFrame("Palette Tabs")
|
tabFrame := ui.NewFrame("Palette Tabs")
|
||||||
|
@ -355,7 +402,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
||||||
Fill: true,
|
Fill: true,
|
||||||
})
|
})
|
||||||
|
|
||||||
doodadsAvailable, err := ListDoodads()
|
doodadsAvailable, err := userdir.ListDoodads()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
d.Flash("ListDoodads: %s", err)
|
d.Flash("ListDoodads: %s", err)
|
||||||
}
|
}
|
||||||
|
@ -376,7 +423,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
doodad, err := doodads.LoadJSON(DoodadPath(filename))
|
doodad, err := doodads.LoadJSON(userdir.DoodadPath(filename))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Error(err.Error())
|
log.Error(err.Error())
|
||||||
doodad = doodads.New(balance.DoodadSize)
|
doodad = doodads.New(balance.DoodadSize)
|
||||||
|
@ -459,7 +506,6 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
||||||
BorderStyle: ui.BorderRaised,
|
BorderStyle: ui.BorderRaised,
|
||||||
Background: render.Grey,
|
Background: render.Grey,
|
||||||
BorderSize: 2,
|
BorderSize: 2,
|
||||||
Width: d.width,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
style := ui.Config{
|
style := ui.Config{
|
||||||
|
@ -503,15 +549,13 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
||||||
Anchor: ui.E,
|
Anchor: ui.E,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Set the initial good frame size to have the height secured,
|
||||||
|
// so when resizing the application window we can just adjust for width.
|
||||||
frame.Resize(render.Rect{
|
frame.Resize(render.Rect{
|
||||||
W: d.width,
|
W: int32(d.width),
|
||||||
H: labelHeight + frame.BoxThickness(1),
|
H: labelHeight + frame.BoxThickness(1),
|
||||||
})
|
})
|
||||||
frame.Compute(d.Engine)
|
frame.Compute(d.Engine)
|
||||||
frame.MoveTo(render.Point{
|
|
||||||
X: 0,
|
|
||||||
Y: d.height - frame.Size().H,
|
|
||||||
})
|
|
||||||
|
|
||||||
return frame
|
return frame
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,3 +10,9 @@ const (
|
||||||
LevelDrawing DrawingType = iota
|
LevelDrawing DrawingType = iota
|
||||||
DoodadDrawing
|
DoodadDrawing
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// File extensions
|
||||||
|
const (
|
||||||
|
LevelExt = ".level"
|
||||||
|
DoodadExt = ".doodad"
|
||||||
|
)
|
||||||
|
|
|
@ -25,6 +25,9 @@ type State struct {
|
||||||
// Cursor positions.
|
// Cursor positions.
|
||||||
CursorX *Int32Tick
|
CursorX *Int32Tick
|
||||||
CursorY *Int32Tick
|
CursorY *Int32Tick
|
||||||
|
|
||||||
|
// Window events: window has changed size.
|
||||||
|
Resized *BoolTick
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a new event state manager.
|
// New creates a new event state manager.
|
||||||
|
@ -43,6 +46,7 @@ func New() *State {
|
||||||
Down: &BoolTick{},
|
Down: &BoolTick{},
|
||||||
CursorX: &Int32Tick{},
|
CursorX: &Int32Tick{},
|
||||||
CursorY: &Int32Tick{},
|
CursorY: &Int32Tick{},
|
||||||
|
Resized: &BoolTick{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
76
fps.go
76
fps.go
|
@ -2,9 +2,12 @@ package doodle
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/balance"
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/doodads"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
"git.kirsle.net/apps/doodle/ui"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Frames to cache for FPS calculation.
|
// Frames to cache for FPS calculation.
|
||||||
|
@ -15,6 +18,12 @@ const maxSamples = 100
|
||||||
var (
|
var (
|
||||||
DebugOverlay = true
|
DebugOverlay = true
|
||||||
DebugCollision = true
|
DebugCollision = true
|
||||||
|
|
||||||
|
DebugTextPadding int32 = 8
|
||||||
|
DebugTextSize = 24
|
||||||
|
DebugTextColor = render.SkyBlue
|
||||||
|
DebugTextStroke = render.Grey
|
||||||
|
DebugTextShadow = render.Black
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -24,6 +33,11 @@ var (
|
||||||
fpsFrames int
|
fpsFrames int
|
||||||
fpsSkipped uint32
|
fpsSkipped uint32
|
||||||
fpsInterval uint32 = 1000
|
fpsInterval uint32 = 1000
|
||||||
|
|
||||||
|
// XXX: some opt-in WorldIndex variables for the debug overlay.
|
||||||
|
// This is the world pixel that the mouse cursor is over,
|
||||||
|
// the Cursor + Scroll position of the canvas.
|
||||||
|
debugWorldIndex render.Point
|
||||||
)
|
)
|
||||||
|
|
||||||
// DrawDebugOverlay draws the debug FPS text on the SDL canvas.
|
// DrawDebugOverlay draws the debug FPS text on the SDL canvas.
|
||||||
|
@ -32,29 +46,53 @@ func (d *Doodle) DrawDebugOverlay() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
label := fmt.Sprintf(
|
var (
|
||||||
"FPS: %d (%dms) S:%s F12=screenshot",
|
darken = balance.DebugStrokeDarken
|
||||||
fpsCurrent,
|
Yoffset int32 = 20 // leave room for the menu bar
|
||||||
fpsSkipped,
|
Xoffset int32 = 5
|
||||||
d.Scene.Name(),
|
keys = []string{
|
||||||
|
" FPS:",
|
||||||
|
"Scene:",
|
||||||
|
"Pixel:",
|
||||||
|
"Mouse:",
|
||||||
|
}
|
||||||
|
values = []string{
|
||||||
|
fmt.Sprintf("%d (skip: %dms)", fpsCurrent, fpsSkipped),
|
||||||
|
d.Scene.Name(),
|
||||||
|
debugWorldIndex.String(),
|
||||||
|
fmt.Sprintf("%d,%d", d.event.CursorX.Now, d.event.CursorY.Now),
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
err := d.Engine.DrawText(
|
key := ui.NewLabel(ui.Label{
|
||||||
render.Text{
|
Text: strings.Join(keys, "\n"),
|
||||||
Text: label,
|
Font: render.Text{
|
||||||
Size: 24,
|
Size: balance.DebugFontSize,
|
||||||
Color: DebugTextColor,
|
FontFilename: balance.ShellFontFilename,
|
||||||
Stroke: DebugTextStroke,
|
Color: balance.DebugLabelColor,
|
||||||
Shadow: DebugTextShadow,
|
Stroke: balance.DebugLabelColor.Darken(darken),
|
||||||
},
|
},
|
||||||
render.Point{
|
})
|
||||||
X: DebugTextPadding,
|
key.Compute(d.Engine)
|
||||||
Y: DebugTextPadding + 32, // extra padding to not overlay menu bars
|
key.Present(d.Engine, render.NewPoint(
|
||||||
|
DebugTextPadding+Xoffset,
|
||||||
|
DebugTextPadding+Yoffset,
|
||||||
|
))
|
||||||
|
|
||||||
|
value := ui.NewLabel(ui.Label{
|
||||||
|
Text: strings.Join(values, "\n"),
|
||||||
|
Font: render.Text{
|
||||||
|
Size: balance.DebugFontSize,
|
||||||
|
FontFilename: balance.DebugFontFilename,
|
||||||
|
Color: balance.DebugValueColor,
|
||||||
|
Stroke: balance.DebugValueColor.Darken(darken),
|
||||||
},
|
},
|
||||||
)
|
})
|
||||||
if err != nil {
|
value.Compute(d.Engine)
|
||||||
log.Error("DrawDebugOverlay: text error: %s", err.Error())
|
value.Present(d.Engine, render.NewPoint(
|
||||||
}
|
DebugTextPadding+Xoffset+key.Size().W+DebugTextPadding,
|
||||||
|
DebugTextPadding+Yoffset, // padding to not overlay menu bar
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
// DrawCollisionBox draws the collision box around a Doodad.
|
// DrawCollisionBox draws the collision box around a Doodad.
|
||||||
|
|
|
@ -275,14 +275,14 @@ func (s *GUITestScene) Draw(d *Doodle) error {
|
||||||
})
|
})
|
||||||
label.Compute(d.Engine)
|
label.Compute(d.Engine)
|
||||||
label.MoveTo(render.Point{
|
label.MoveTo(render.Point{
|
||||||
X: (d.width / 2) - (label.Size().W / 2),
|
X: (int32(d.width) / 2) - (label.Size().W / 2),
|
||||||
Y: 40,
|
Y: 40,
|
||||||
})
|
})
|
||||||
label.Present(d.Engine, label.Point())
|
label.Present(d.Engine, label.Point())
|
||||||
|
|
||||||
s.Window.Compute(d.Engine)
|
s.Window.Compute(d.Engine)
|
||||||
s.Window.MoveTo(render.Point{
|
s.Window.MoveTo(render.Point{
|
||||||
X: (d.width / 2) - (s.Window.Size().W / 2),
|
X: (int32(d.width) / 2) - (s.Window.Size().W / 2),
|
||||||
Y: 100,
|
Y: 100,
|
||||||
})
|
})
|
||||||
s.Window.Present(d.Engine, s.Window.Point())
|
s.Window.Present(d.Engine, s.Window.Point())
|
||||||
|
|
5
kirsle.env
Normal file
5
kirsle.env
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export D_SHELL_BG="#001133DD"
|
||||||
|
export D_SHELL_FG="#FF99FF"
|
||||||
|
export D_SHELL_PC="#FF9900"
|
25
level/actors.go
Normal file
25
level/actors.go
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
import "git.kirsle.net/apps/doodle/render"
|
||||||
|
|
||||||
|
// ActorMap holds the doodad information by their ID in the level data.
|
||||||
|
type ActorMap map[string]*Actor
|
||||||
|
|
||||||
|
// Inflate assigns each actor its ID from the hash map for their self reference.
|
||||||
|
func (m ActorMap) Inflate() {
|
||||||
|
for id, actor := range m {
|
||||||
|
actor.id = id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Actor is an instance of a Doodad in the level.
|
||||||
|
type Actor struct {
|
||||||
|
id string // NOTE: read only, use ID() to access.
|
||||||
|
Filename string `json:"filename"` // like "exit.doodad"
|
||||||
|
Point render.Point `json:"point"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ID returns the actor's ID.
|
||||||
|
func (a *Actor) ID() string {
|
||||||
|
return a.id
|
||||||
|
}
|
9
level/filesystem.go
Normal file
9
level/filesystem.go
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package level
|
||||||
|
|
||||||
|
// FileSystem embeds a map of files inside a parent drawing.
|
||||||
|
type FileSystem map[string]File
|
||||||
|
|
||||||
|
// File holds details about a file in the FileSystem.
|
||||||
|
type File struct {
|
||||||
|
Data []byte `json:"data"`
|
||||||
|
}
|
|
@ -50,6 +50,7 @@ func LoadJSON(filename string) (*Level, error) {
|
||||||
|
|
||||||
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
// Inflate the chunk metadata to map the pixels to their palette indexes.
|
||||||
m.Chunker.Inflate(m.Palette)
|
m.Chunker.Inflate(m.Palette)
|
||||||
|
m.Actors.Inflate()
|
||||||
|
|
||||||
// Inflate the private instance values.
|
// Inflate the private instance values.
|
||||||
m.Palette.Inflate()
|
m.Palette.Inflate()
|
||||||
|
|
|
@ -15,6 +15,9 @@ type Base struct {
|
||||||
GameVersion string `json:"gameVersion"` // Game version that created the level.
|
GameVersion string `json:"gameVersion"` // Game version that created the level.
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Author string `json:"author"`
|
Author string `json:"author"`
|
||||||
|
|
||||||
|
// Every drawing type is able to embed other files inside of itself.
|
||||||
|
Files FileSystem `json:"files"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Level is the container format for Doodle map drawings.
|
// Level is the container format for Doodle map drawings.
|
||||||
|
@ -29,6 +32,9 @@ type Level struct {
|
||||||
// The Palette holds the unique "colors" used in this map file, and their
|
// The Palette holds the unique "colors" used in this map file, and their
|
||||||
// properties (solid, fire, slippery, etc.)
|
// properties (solid, fire, slippery, etc.)
|
||||||
Palette *Palette `json:"palette"`
|
Palette *Palette `json:"palette"`
|
||||||
|
|
||||||
|
// Actors keep a list of the doodad instances in this map.
|
||||||
|
Actors ActorMap `json:"actors"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// New creates a blank level object with all its members initialized.
|
// New creates a blank level object with all its members initialized.
|
||||||
|
|
|
@ -77,14 +77,14 @@ func (s *MainScene) Draw(d *Doodle) error {
|
||||||
})
|
})
|
||||||
label.Compute(d.Engine)
|
label.Compute(d.Engine)
|
||||||
label.MoveTo(render.Point{
|
label.MoveTo(render.Point{
|
||||||
X: (d.width / 2) - (label.Size().W / 2),
|
X: (int32(d.width) / 2) - (label.Size().W / 2),
|
||||||
Y: 120,
|
Y: 120,
|
||||||
})
|
})
|
||||||
label.Present(d.Engine, label.Point())
|
label.Present(d.Engine, label.Point())
|
||||||
|
|
||||||
s.frame.Compute(d.Engine)
|
s.frame.Compute(d.Engine)
|
||||||
s.frame.MoveTo(render.Point{
|
s.frame.MoveTo(render.Point{
|
||||||
X: (d.width / 2) - (s.frame.Size().W / 2),
|
X: (int32(d.width) / 2) - (s.frame.Size().W / 2),
|
||||||
Y: 200,
|
Y: 200,
|
||||||
})
|
})
|
||||||
s.frame.Present(d.Engine, s.frame.Point())
|
s.frame.Present(d.Engine, s.frame.Point())
|
||||||
|
|
124
pkg/userdir/userdir.go
Normal file
124
pkg/userdir/userdir.go
Normal file
|
@ -0,0 +1,124 @@
|
||||||
|
package userdir
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/kirsle/configdir"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Profile Directory settings.
|
||||||
|
var (
|
||||||
|
ConfigDirectoryName = "doodle"
|
||||||
|
|
||||||
|
ProfileDirectory string
|
||||||
|
LevelDirectory string
|
||||||
|
DoodadDirectory string
|
||||||
|
|
||||||
|
CacheDirectory string
|
||||||
|
FontDirectory string
|
||||||
|
)
|
||||||
|
|
||||||
|
// File extensions
|
||||||
|
const (
|
||||||
|
extLevel = ".level"
|
||||||
|
extDoodad = ".doodad"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// Profile directory contains the user's levels and doodads.
|
||||||
|
ProfileDirectory = configdir.LocalConfig(ConfigDirectoryName)
|
||||||
|
LevelDirectory = configdir.LocalConfig(ConfigDirectoryName, "levels")
|
||||||
|
DoodadDirectory = configdir.LocalConfig(ConfigDirectoryName, "doodads")
|
||||||
|
|
||||||
|
// Cache directory to extract font files to.
|
||||||
|
CacheDirectory = configdir.LocalCache(ConfigDirectoryName)
|
||||||
|
FontDirectory = configdir.LocalCache(ConfigDirectoryName, "fonts")
|
||||||
|
|
||||||
|
// Ensure all the directories exist.
|
||||||
|
configdir.MakePath(LevelDirectory)
|
||||||
|
configdir.MakePath(DoodadDirectory)
|
||||||
|
configdir.MakePath(FontDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// LevelPath will turn a "simple" filename into an absolute path in the user's
|
||||||
|
// local levels folder. If the filename already contains slashes, it is returned
|
||||||
|
// as-is as an absolute or relative path.
|
||||||
|
func LevelPath(filename string) string {
|
||||||
|
return resolvePath(LevelDirectory, filename, extLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoodadPath is like LevelPath but for Doodad files.
|
||||||
|
func DoodadPath(filename string) string {
|
||||||
|
return resolvePath(DoodadDirectory, filename, extDoodad)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListDoodads returns a listing of all available doodads.
|
||||||
|
func ListDoodads() ([]string, error) {
|
||||||
|
var names []string
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(DoodadDirectory)
|
||||||
|
if err != nil {
|
||||||
|
return names, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
name := file.Name()
|
||||||
|
if strings.HasSuffix(strings.ToLower(name), extDoodad) {
|
||||||
|
names = append(names, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return names, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolvePath is the inner logic for LevelPath and DoodadPath.
|
||||||
|
func resolvePath(directory, filename, extension string) string {
|
||||||
|
if strings.Contains(filename, "/") {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attach the file extension?
|
||||||
|
if strings.ToLower(filepath.Ext(filename)) != extension {
|
||||||
|
filename += extension
|
||||||
|
}
|
||||||
|
|
||||||
|
return filepath.Join(directory, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResolvePath takes an ambiguous simple filename and searches for a Level or
|
||||||
|
// Doodad that matches. Returns a blank string if no files found.
|
||||||
|
//
|
||||||
|
// Pass a true value for `one` if you are intending to create the file. It will
|
||||||
|
// only test one filepath and return the first one, regardless if the file
|
||||||
|
// existed. So the filename should have a ".level" or ".doodad" extension and
|
||||||
|
// then this path will resolve the ProfileDirectory of the file.
|
||||||
|
func ResolvePath(filename, extension string, one bool) string {
|
||||||
|
// If the filename exists outright, return it.
|
||||||
|
if _, err := os.Stat(filename); !os.IsNotExist(err) {
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
var paths []string
|
||||||
|
if extension == extLevel {
|
||||||
|
paths = append(paths, filepath.Join(LevelDirectory, filename))
|
||||||
|
} else if extension == extDoodad {
|
||||||
|
paths = append(paths, filepath.Join(DoodadDirectory, filename))
|
||||||
|
} else {
|
||||||
|
paths = append(paths,
|
||||||
|
filepath.Join(LevelDirectory, filename+".level"),
|
||||||
|
filepath.Join(DoodadDirectory, filename+".doodad"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range paths {
|
||||||
|
if _, err := os.Stat(test); os.IsNotExist(err) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return test
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
|
@ -33,7 +33,7 @@ func (s *PlayScene) Name() string {
|
||||||
func (s *PlayScene) Setup(d *Doodle) error {
|
func (s *PlayScene) Setup(d *Doodle) error {
|
||||||
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
||||||
s.drawing.MoveTo(render.Origin)
|
s.drawing.MoveTo(render.Origin)
|
||||||
s.drawing.Resize(render.NewRect(d.width, d.height))
|
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
|
||||||
s.drawing.Compute(d.Engine)
|
s.drawing.Compute(d.Engine)
|
||||||
|
|
||||||
// Given a filename or map data to play?
|
// Given a filename or map data to play?
|
||||||
|
|
|
@ -15,6 +15,7 @@ type Engine interface {
|
||||||
// Poll for events like keypresses and mouse clicks.
|
// Poll for events like keypresses and mouse clicks.
|
||||||
Poll() (*events.State, error)
|
Poll() (*events.State, error)
|
||||||
GetTicks() uint32
|
GetTicks() uint32
|
||||||
|
WindowSize() (w, h int)
|
||||||
|
|
||||||
// Present presents the current state to the screen.
|
// Present presents the current state to the screen.
|
||||||
Present() error
|
Present() error
|
||||||
|
@ -88,6 +89,27 @@ func (r Rect) Bigger(other Rect) bool {
|
||||||
other.H > r.H) // Taller
|
other.H > r.H) // Taller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intersects with the other rectangle in any way.
|
||||||
|
func (r Rect) Intersects(other Rect) bool {
|
||||||
|
// Do a bidirectional compare.
|
||||||
|
compare := func(a, b Rect) bool {
|
||||||
|
var corners = []Point{
|
||||||
|
NewPoint(b.X, b.Y),
|
||||||
|
NewPoint(b.X, b.Y+b.H),
|
||||||
|
NewPoint(b.X+b.W, b.Y),
|
||||||
|
NewPoint(b.X+b.W, b.Y+b.H),
|
||||||
|
}
|
||||||
|
for _, pt := range corners {
|
||||||
|
if pt.Inside(a) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return compare(r, other) || compare(other, r) || false
|
||||||
|
}
|
||||||
|
|
||||||
// IsZero returns if the Rect is uninitialized.
|
// IsZero returns if the Rect is uninitialized.
|
||||||
func (r Rect) IsZero() bool {
|
func (r Rect) IsZero() bool {
|
||||||
return r.X == 0 && r.Y == 0 && r.W == 0 && r.H == 0
|
return r.X == 0 && r.Y == 0 && r.W == 0 && r.H == 0
|
||||||
|
|
|
@ -55,6 +55,9 @@ func (p Point) IsZero() bool {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Inside returns whether the Point falls inside the rect.
|
// Inside returns whether the Point falls inside the rect.
|
||||||
|
//
|
||||||
|
// NOTICE: the W and H are zero-relative, so a 100x100 box at coordinate
|
||||||
|
// X,Y would still have W,H of 100.
|
||||||
func (p Point) Inside(r Rect) bool {
|
func (p Point) Inside(r Rect) bool {
|
||||||
var (
|
var (
|
||||||
x1 = r.X
|
x1 = r.X
|
||||||
|
@ -62,7 +65,8 @@ func (p Point) Inside(r Rect) bool {
|
||||||
x2 = r.X + r.W
|
x2 = r.X + r.W
|
||||||
y2 = r.Y + r.H
|
y2 = r.Y + r.H
|
||||||
)
|
)
|
||||||
return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2
|
return ((p.X >= x1 && p.X <= x2) &&
|
||||||
|
(p.Y >= y1 && p.Y <= y2))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add (or subtract) the other point to your current point.
|
// Add (or subtract) the other point to your current point.
|
||||||
|
|
|
@ -8,13 +8,9 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPointInside(t *testing.T) {
|
func TestPointInside(t *testing.T) {
|
||||||
var p = render.Point{
|
|
||||||
X: 128,
|
|
||||||
Y: 256,
|
|
||||||
}
|
|
||||||
|
|
||||||
type testCase struct {
|
type testCase struct {
|
||||||
rect render.Rect
|
rect render.Rect
|
||||||
|
p render.Point
|
||||||
shouldPass bool
|
shouldPass bool
|
||||||
}
|
}
|
||||||
tests := []testCase{
|
tests := []testCase{
|
||||||
|
@ -25,6 +21,7 @@ func TestPointInside(t *testing.T) {
|
||||||
W: 500,
|
W: 500,
|
||||||
H: 500,
|
H: 500,
|
||||||
},
|
},
|
||||||
|
p: render.NewPoint(128, 256),
|
||||||
shouldPass: true,
|
shouldPass: true,
|
||||||
},
|
},
|
||||||
testCase{
|
testCase{
|
||||||
|
@ -34,14 +31,27 @@ func TestPointInside(t *testing.T) {
|
||||||
W: 40,
|
W: 40,
|
||||||
H: 60,
|
H: 60,
|
||||||
},
|
},
|
||||||
|
p: render.NewPoint(128, 256),
|
||||||
shouldPass: false,
|
shouldPass: false,
|
||||||
},
|
},
|
||||||
|
testCase{
|
||||||
|
// true values when debugging why Doodads weren't
|
||||||
|
// considered inside the viewport.
|
||||||
|
rect: render.Rect{
|
||||||
|
X: 0,
|
||||||
|
Y: -232,
|
||||||
|
H: 874,
|
||||||
|
W: 490,
|
||||||
|
},
|
||||||
|
p: render.NewPoint(509, 260),
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
if p.Inside(test.rect) != test.shouldPass {
|
if test.p.Inside(test.rect) != test.shouldPass {
|
||||||
t.Errorf("Failed: %s inside %s should %s",
|
t.Errorf("Failed: %s inside %s should be %s",
|
||||||
p,
|
test.p,
|
||||||
test.rect,
|
test.rect,
|
||||||
strconv.FormatBool(test.shouldPass),
|
strconv.FormatBool(test.shouldPass),
|
||||||
)
|
)
|
||||||
|
|
71
render/rect_test.go
Normal file
71
render/rect_test.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package render_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntersection(t *testing.T) {
|
||||||
|
newRect := func(x, y, w, h int) render.Rect {
|
||||||
|
return render.Rect{
|
||||||
|
X: int32(x),
|
||||||
|
Y: int32(y),
|
||||||
|
W: int32(w),
|
||||||
|
H: int32(h),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCase struct {
|
||||||
|
A render.Rect
|
||||||
|
B render.Rect
|
||||||
|
Expect bool
|
||||||
|
}
|
||||||
|
var tests = []TestCase{
|
||||||
|
{
|
||||||
|
A: newRect(0, 0, 1000, 1000),
|
||||||
|
B: newRect(200, 200, 100, 100),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(200, 200, 100, 100),
|
||||||
|
B: newRect(0, 0, 1000, 1000),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(0, 0, 100, 100),
|
||||||
|
B: newRect(100, 0, 100, 100),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(0, 0, 99, 99),
|
||||||
|
B: newRect(100, 0, 99, 99),
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Real coords of a test doodad!
|
||||||
|
A: newRect(183, 256, 283, 356),
|
||||||
|
B: newRect(0, -232, 874, 490),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(183, 256, 283, 356),
|
||||||
|
B: newRect(0, -240, 874, 490),
|
||||||
|
Expect: false, // XXX: must be true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
actual := test.A.Intersects(test.B)
|
||||||
|
if actual != test.Expect {
|
||||||
|
t.Errorf(
|
||||||
|
"%s collision with %s: expected %s, got %s",
|
||||||
|
test.A,
|
||||||
|
test.B,
|
||||||
|
strconv.FormatBool(test.Expect),
|
||||||
|
strconv.FormatBool(actual),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
127
render/sdl/events.go
Normal file
127
render/sdl/events.go
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/events"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poll for events.
|
||||||
|
func (r *Renderer) Poll() (*events.State, error) {
|
||||||
|
s := r.events
|
||||||
|
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||||
|
switch t := event.(type) {
|
||||||
|
case *sdl.QuitEvent:
|
||||||
|
return s, errors.New("quit")
|
||||||
|
case *sdl.WindowEvent:
|
||||||
|
if DebugWindowEvents {
|
||||||
|
if t.Event == sdl.WINDOWEVENT_RESIZED {
|
||||||
|
log.Debug("[%d ms] tick:%d Window Resized to %dx%d",
|
||||||
|
t.Timestamp,
|
||||||
|
r.ticks,
|
||||||
|
t.Data1,
|
||||||
|
t.Data2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Resized.Push(true)
|
||||||
|
case *sdl.MouseMotionEvent:
|
||||||
|
if DebugMouseEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the cursor position.
|
||||||
|
s.CursorX.Push(t.X)
|
||||||
|
s.CursorY.Push(t.Y)
|
||||||
|
s.Button1.Push(t.State == 1)
|
||||||
|
case *sdl.MouseButtonEvent:
|
||||||
|
if DebugClickEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the cursor position.
|
||||||
|
s.CursorX.Push(t.X)
|
||||||
|
s.CursorY.Push(t.Y)
|
||||||
|
|
||||||
|
// Is a mouse button pressed down?
|
||||||
|
if t.Button == 1 {
|
||||||
|
var eventName string
|
||||||
|
if t.State == 1 && s.Button1.Now == false {
|
||||||
|
eventName = "DOWN"
|
||||||
|
} else if t.State == 0 && s.Button1.Now == true {
|
||||||
|
eventName = "UP"
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventName != "" {
|
||||||
|
s.Button1.Push(eventName == "DOWN")
|
||||||
|
|
||||||
|
// Return the event immediately.
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// s.Button2.Push(t.Button == 3 && t.State == 1)
|
||||||
|
case *sdl.MouseWheelEvent:
|
||||||
|
if DebugMouseEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case *sdl.KeyboardEvent:
|
||||||
|
if DebugKeyEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Keysym.Scancode {
|
||||||
|
case sdl.SCANCODE_ESCAPE:
|
||||||
|
if t.Repeat == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.EscapeKey.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_RETURN:
|
||||||
|
if t.Repeat == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.EnterKey.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_F12:
|
||||||
|
s.ScreenshotKey.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_UP:
|
||||||
|
s.Up.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_LEFT:
|
||||||
|
s.Left.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_RIGHT:
|
||||||
|
s.Right.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_DOWN:
|
||||||
|
s.Down.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_LSHIFT:
|
||||||
|
case sdl.SCANCODE_RSHIFT:
|
||||||
|
s.ShiftActive.Push(t.State == 1)
|
||||||
|
continue
|
||||||
|
case sdl.SCANCODE_LALT:
|
||||||
|
case sdl.SCANCODE_RALT:
|
||||||
|
case sdl.SCANCODE_LCTRL:
|
||||||
|
case sdl.SCANCODE_RCTRL:
|
||||||
|
continue
|
||||||
|
case sdl.SCANCODE_BACKSPACE:
|
||||||
|
// Make it a key event with "\b" as the sequence.
|
||||||
|
if t.State == 1 || t.Repeat == 1 {
|
||||||
|
s.KeyName.Push(`\b`)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Push the string value of the key.
|
||||||
|
if t.State == 1 {
|
||||||
|
s.KeyName.Push(string(t.Keysym.Sym))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
|
@ -6,9 +6,10 @@ var log *golog.Logger
|
||||||
|
|
||||||
// Verbose debug logging.
|
// Verbose debug logging.
|
||||||
var (
|
var (
|
||||||
DebugMouseEvents = false
|
DebugMouseEvents = false
|
||||||
DebugClickEvents = false
|
DebugClickEvents = false
|
||||||
DebugKeyEvents = false
|
DebugKeyEvents = false
|
||||||
|
DebugWindowEvents = false
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
package sdl
|
package sdl
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
|
@ -69,7 +68,7 @@ func (r *Renderer) Setup() error {
|
||||||
sdl.WINDOWPOS_CENTERED,
|
sdl.WINDOWPOS_CENTERED,
|
||||||
r.width,
|
r.width,
|
||||||
r.height,
|
r.height,
|
||||||
sdl.WINDOW_SHOWN,
|
sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -93,111 +92,10 @@ func (r *Renderer) GetTicks() uint32 {
|
||||||
return sdl.GetTicks()
|
return sdl.GetTicks()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Poll for events.
|
// WindowSize returns the SDL window size.
|
||||||
func (r *Renderer) Poll() (*events.State, error) {
|
func (r *Renderer) WindowSize() (int, int) {
|
||||||
s := r.events
|
w, h := r.window.GetSize()
|
||||||
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
return int(w), int(h)
|
||||||
switch t := event.(type) {
|
|
||||||
case *sdl.QuitEvent:
|
|
||||||
return s, errors.New("quit")
|
|
||||||
case *sdl.MouseMotionEvent:
|
|
||||||
if DebugMouseEvents {
|
|
||||||
log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d",
|
|
||||||
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the cursor position.
|
|
||||||
s.CursorX.Push(t.X)
|
|
||||||
s.CursorY.Push(t.Y)
|
|
||||||
s.Button1.Push(t.State == 1)
|
|
||||||
case *sdl.MouseButtonEvent:
|
|
||||||
if DebugClickEvents {
|
|
||||||
log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d",
|
|
||||||
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Push the cursor position.
|
|
||||||
s.CursorX.Push(t.X)
|
|
||||||
s.CursorY.Push(t.Y)
|
|
||||||
|
|
||||||
// Is a mouse button pressed down?
|
|
||||||
if t.Button == 1 {
|
|
||||||
var eventName string
|
|
||||||
if t.State == 1 && s.Button1.Now == false {
|
|
||||||
eventName = "DOWN"
|
|
||||||
} else if t.State == 0 && s.Button1.Now == true {
|
|
||||||
eventName = "UP"
|
|
||||||
}
|
|
||||||
|
|
||||||
if eventName != "" {
|
|
||||||
s.Button1.Push(eventName == "DOWN")
|
|
||||||
|
|
||||||
// Return the event immediately.
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// s.Button2.Push(t.Button == 3 && t.State == 1)
|
|
||||||
case *sdl.MouseWheelEvent:
|
|
||||||
if DebugMouseEvents {
|
|
||||||
log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d",
|
|
||||||
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
case *sdl.KeyboardEvent:
|
|
||||||
if DebugKeyEvents {
|
|
||||||
log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n",
|
|
||||||
t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
switch t.Keysym.Scancode {
|
|
||||||
case sdl.SCANCODE_ESCAPE:
|
|
||||||
if t.Repeat == 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.EscapeKey.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_RETURN:
|
|
||||||
if t.Repeat == 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
s.EnterKey.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_F12:
|
|
||||||
s.ScreenshotKey.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_UP:
|
|
||||||
s.Up.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_LEFT:
|
|
||||||
s.Left.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_RIGHT:
|
|
||||||
s.Right.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_DOWN:
|
|
||||||
s.Down.Push(t.State == 1)
|
|
||||||
case sdl.SCANCODE_LSHIFT:
|
|
||||||
case sdl.SCANCODE_RSHIFT:
|
|
||||||
s.ShiftActive.Push(t.State == 1)
|
|
||||||
continue
|
|
||||||
case sdl.SCANCODE_LALT:
|
|
||||||
case sdl.SCANCODE_RALT:
|
|
||||||
case sdl.SCANCODE_LCTRL:
|
|
||||||
case sdl.SCANCODE_RCTRL:
|
|
||||||
continue
|
|
||||||
case sdl.SCANCODE_BACKSPACE:
|
|
||||||
// Make it a key event with "\b" as the sequence.
|
|
||||||
if t.State == 1 || t.Repeat == 1 {
|
|
||||||
s.KeyName.Push(`\b`)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
// Push the string value of the key.
|
|
||||||
if t.State == 1 {
|
|
||||||
s.KeyName.Push(string(t.Keysym.Sym))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Present the current frame.
|
// Present the current frame.
|
||||||
|
|
10
shell.go
10
shell.go
|
@ -281,14 +281,14 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
|
||||||
balance.ShellBackgroundColor,
|
balance.ShellBackgroundColor,
|
||||||
render.Rect{
|
render.Rect{
|
||||||
X: 0,
|
X: 0,
|
||||||
Y: d.height - boxHeight,
|
Y: int32(d.height) - boxHeight,
|
||||||
W: d.width,
|
W: int32(d.width),
|
||||||
H: boxHeight,
|
H: boxHeight,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
// Draw the recent commands.
|
// Draw the recent commands.
|
||||||
outputY := d.height - int32(lineHeight*2)
|
outputY := int32(d.height - (lineHeight * 2))
|
||||||
for i := 0; i < balance.ShellHistoryLineCount; i++ {
|
for i := 0; i < balance.ShellHistoryLineCount; i++ {
|
||||||
if len(s.Output) > i {
|
if len(s.Output) > i {
|
||||||
line := s.Output[len(s.Output)-1-i]
|
line := s.Output[len(s.Output)-1-i]
|
||||||
|
@ -318,14 +318,14 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
|
||||||
},
|
},
|
||||||
render.Point{
|
render.Point{
|
||||||
X: balance.ShellPadding,
|
X: balance.ShellPadding,
|
||||||
Y: d.height - int32(balance.ShellFontSize) - balance.ShellPadding,
|
Y: int32(d.height-balance.ShellFontSize) - balance.ShellPadding,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
} else if len(s.Flashes) > 0 {
|
} else if len(s.Flashes) > 0 {
|
||||||
// Otherwise, just draw flashed messages.
|
// Otherwise, just draw flashed messages.
|
||||||
valid := false // Did we actually draw any?
|
valid := false // Did we actually draw any?
|
||||||
|
|
||||||
outputY := d.height - int32(lineHeight*2)
|
outputY := int32(d.height - (lineHeight * 2))
|
||||||
for i := len(s.Flashes); i > 0; i-- {
|
for i := len(s.Flashes); i > 0; i-- {
|
||||||
flash := s.Flashes[i-1]
|
flash := s.Flashes[i-1]
|
||||||
if d.ticks >= flash.Expires {
|
if d.ticks >= flash.Expires {
|
||||||
|
|
48
ui/label.go
48
ui/label.go
|
@ -2,6 +2,7 @@ package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
)
|
)
|
||||||
|
@ -21,8 +22,9 @@ type Label struct {
|
||||||
TextVariable *string
|
TextVariable *string
|
||||||
Font render.Text
|
Font render.Text
|
||||||
|
|
||||||
width int32
|
width int32
|
||||||
height int32
|
height int32
|
||||||
|
lineHeight int
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewLabel creates a new label.
|
// NewLabel creates a new label.
|
||||||
|
@ -60,10 +62,24 @@ func (w *Label) Value() string {
|
||||||
|
|
||||||
// Compute the size of the label widget.
|
// Compute the size of the label widget.
|
||||||
func (w *Label) Compute(e render.Engine) {
|
func (w *Label) Compute(e render.Engine) {
|
||||||
rect, err := e.ComputeTextRect(w.text())
|
text := w.text()
|
||||||
if err != nil {
|
lines := strings.Split(text.Text, "\n")
|
||||||
log.Error("%s: failed to compute text rect: %s", w, err)
|
|
||||||
return
|
// Max rect to encompass all lines of text.
|
||||||
|
var maxRect = render.Rect{}
|
||||||
|
for _, line := range lines {
|
||||||
|
text.Text = line // only this line at this time.
|
||||||
|
rect, err := e.ComputeTextRect(text)
|
||||||
|
if err != nil {
|
||||||
|
log.Error("%s: failed to compute text rect: %s", w, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if rect.W > maxRect.W {
|
||||||
|
maxRect.W = rect.W
|
||||||
|
}
|
||||||
|
maxRect.H += rect.H
|
||||||
|
w.lineHeight = int(rect.H)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
@ -73,14 +89,14 @@ func (w *Label) Compute(e render.Engine) {
|
||||||
|
|
||||||
if !w.FixedSize() {
|
if !w.FixedSize() {
|
||||||
w.resizeAuto(render.Rect{
|
w.resizeAuto(render.Rect{
|
||||||
W: rect.W + (padX * 2),
|
W: maxRect.W + (padX * 2),
|
||||||
H: rect.H + (padY * 2),
|
H: maxRect.H + (padY * 2),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
w.MoveTo(render.Point{
|
w.MoveTo(render.Point{
|
||||||
X: rect.X + w.BoxThickness(1),
|
X: maxRect.X + w.BoxThickness(1),
|
||||||
Y: rect.Y + w.BoxThickness(1),
|
Y: maxRect.Y + w.BoxThickness(1),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,13 +109,17 @@ func (w *Label) Present(e render.Engine, P render.Point) {
|
||||||
border := w.BoxThickness(1)
|
border := w.BoxThickness(1)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
text = w.text()
|
||||||
padX = w.Font.Padding + w.Font.PadX
|
padX = w.Font.Padding + w.Font.PadX
|
||||||
padY = w.Font.Padding + w.Font.PadY
|
padY = w.Font.Padding + w.Font.PadY
|
||||||
)
|
)
|
||||||
|
|
||||||
w.DrawBox(e, P)
|
w.DrawBox(e, P)
|
||||||
e.DrawText(w.text(), render.Point{
|
for i, line := range strings.Split(text.Text, "\n") {
|
||||||
X: P.X + border + padX,
|
text.Text = line
|
||||||
Y: P.Y + border + padY,
|
e.DrawText(text, render.Point{
|
||||||
})
|
X: P.X + border + padX,
|
||||||
|
Y: P.Y + border + padY + int32(i*w.lineHeight),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
232
uix/canvas.go
232
uix/canvas.go
|
@ -8,6 +8,7 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/doodads"
|
"git.kirsle.net/apps/doodle/doodads"
|
||||||
"git.kirsle.net/apps/doodle/events"
|
"git.kirsle.net/apps/doodle/events"
|
||||||
"git.kirsle.net/apps/doodle/level"
|
"git.kirsle.net/apps/doodle/level"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||||
"git.kirsle.net/apps/doodle/render"
|
"git.kirsle.net/apps/doodle/render"
|
||||||
"git.kirsle.net/apps/doodle/ui"
|
"git.kirsle.net/apps/doodle/ui"
|
||||||
)
|
)
|
||||||
|
@ -21,7 +22,14 @@ type Canvas struct {
|
||||||
Editable bool
|
Editable bool
|
||||||
Scrollable bool
|
Scrollable bool
|
||||||
|
|
||||||
chunks *level.Chunker
|
// Underlying chunk data for the drawing.
|
||||||
|
chunks *level.Chunker
|
||||||
|
|
||||||
|
// Actors to superimpose on top of the drawing.
|
||||||
|
actor *level.Actor // if this canvas IS an actor
|
||||||
|
actors []*Actor
|
||||||
|
|
||||||
|
// Tracking pixels while editing. TODO: get rid of pixelHistory?
|
||||||
pixelHistory []*level.Pixel
|
pixelHistory []*level.Pixel
|
||||||
lastPixel *level.Pixel
|
lastPixel *level.Pixel
|
||||||
|
|
||||||
|
@ -29,6 +37,12 @@ type Canvas struct {
|
||||||
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Actor is an instance of an actor with a Canvas attached.
|
||||||
|
type Actor struct {
|
||||||
|
Actor *level.Actor
|
||||||
|
Canvas *Canvas
|
||||||
|
}
|
||||||
|
|
||||||
// NewCanvas initializes a Canvas widget.
|
// NewCanvas initializes a Canvas widget.
|
||||||
//
|
//
|
||||||
// If editable is true, Scrollable is also set to true, which means the arrow
|
// If editable is true, Scrollable is also set to true, which means the arrow
|
||||||
|
@ -39,6 +53,7 @@ func NewCanvas(size int, editable bool) *Canvas {
|
||||||
Scrollable: editable,
|
Scrollable: editable,
|
||||||
Palette: level.NewPalette(),
|
Palette: level.NewPalette(),
|
||||||
chunks: level.NewChunker(size),
|
chunks: level.NewChunker(size),
|
||||||
|
actors: make([]*Actor, 0),
|
||||||
}
|
}
|
||||||
w.setup()
|
w.setup()
|
||||||
w.IDFunc(func() string {
|
w.IDFunc(func() string {
|
||||||
|
@ -80,6 +95,32 @@ func (w *Canvas) LoadDoodad(d *doodads.Doodad) {
|
||||||
w.Load(d.Palette, d.Layers[0].Chunker)
|
w.Load(d.Palette, d.Layers[0].Chunker)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// InstallActors adds external Actors to the canvas to be superimposed on top
|
||||||
|
// of the drawing.
|
||||||
|
func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
||||||
|
w.actors = make([]*Actor, 0)
|
||||||
|
for id, actor := range actors {
|
||||||
|
log.Info("InstallActors: %s", id)
|
||||||
|
|
||||||
|
doodad, err := doodads.LoadJSON(userdir.DoodadPath(actor.Filename))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("InstallActors: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
size := int32(doodad.Layers[0].Chunker.Size)
|
||||||
|
can := NewCanvas(int(size), false)
|
||||||
|
can.Name = id
|
||||||
|
can.actor = actor
|
||||||
|
can.LoadDoodad(doodad)
|
||||||
|
can.Resize(render.NewRect(size, size))
|
||||||
|
w.actors = append(w.actors, &Actor{
|
||||||
|
Actor: actor,
|
||||||
|
Canvas: can,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// SetSwatch changes the currently selected swatch for editing.
|
// SetSwatch changes the currently selected swatch for editing.
|
||||||
func (w *Canvas) SetSwatch(s *level.Swatch) {
|
func (w *Canvas) SetSwatch(s *level.Swatch) {
|
||||||
w.Palette.ActiveSwatch = s
|
w.Palette.ActiveSwatch = s
|
||||||
|
@ -88,6 +129,16 @@ func (w *Canvas) SetSwatch(s *level.Swatch) {
|
||||||
// setup common configs between both initializers of the canvas.
|
// setup common configs between both initializers of the canvas.
|
||||||
func (w *Canvas) setup() {
|
func (w *Canvas) setup() {
|
||||||
w.SetBackground(render.White)
|
w.SetBackground(render.White)
|
||||||
|
|
||||||
|
// XXX: Debug code.
|
||||||
|
if balance.DebugCanvasBorder != render.Invisible {
|
||||||
|
w.Configure(ui.Config{
|
||||||
|
BorderColor: balance.DebugCanvasBorder,
|
||||||
|
BorderSize: 2,
|
||||||
|
BorderStyle: ui.BorderSolid,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
w.Handle(ui.MouseOver, func(p render.Point) {
|
w.Handle(ui.MouseOver, func(p render.Point) {
|
||||||
w.SetBackground(render.Yellow)
|
w.SetBackground(render.Yellow)
|
||||||
})
|
})
|
||||||
|
@ -145,14 +196,6 @@ func (w *Canvas) Loop(ev *events.State) error {
|
||||||
Swatch: w.Palette.ActiveSwatch,
|
Swatch: w.Palette.ActiveSwatch,
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Warn(
|
|
||||||
"real cursor: %d,%d translated: %s widget pos: %s scroll: %s",
|
|
||||||
ev.CursorX.Now, ev.CursorY.Now,
|
|
||||||
cursor,
|
|
||||||
P,
|
|
||||||
w.Scroll,
|
|
||||||
)
|
|
||||||
|
|
||||||
// Append unique new pixels.
|
// Append unique new pixels.
|
||||||
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
|
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
|
||||||
if lastPixel != nil {
|
if lastPixel != nil {
|
||||||
|
@ -168,7 +211,6 @@ func (w *Canvas) Loop(ev *events.State) error {
|
||||||
w.pixelHistory = append(w.pixelHistory, pixel)
|
w.pixelHistory = append(w.pixelHistory, pixel)
|
||||||
|
|
||||||
// Save in the pixel canvas map.
|
// Save in the pixel canvas map.
|
||||||
log.Info("Set: %s %s", cursor, pixel.Swatch.Color)
|
|
||||||
w.chunks.Set(cursor, pixel.Swatch)
|
w.chunks.Set(cursor, pixel.Swatch)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -181,6 +223,15 @@ func (w *Canvas) Loop(ev *events.State) error {
|
||||||
// Viewport returns a rect containing the viewable drawing coordinates in this
|
// Viewport returns a rect containing the viewable drawing coordinates in this
|
||||||
// canvas. The X,Y values are the scroll offset (top left) and the W,H values
|
// canvas. The X,Y values are the scroll offset (top left) and the W,H values
|
||||||
// are the scroll offset plus the width/height of the Canvas widget.
|
// are the scroll offset plus the width/height of the Canvas widget.
|
||||||
|
//
|
||||||
|
// The Viewport rect are the Absolute World Coordinates of the drawing that are
|
||||||
|
// visible inside the Canvas. The X,Y is the top left World Coordinate and the
|
||||||
|
// W,H are the bottom right World Coordinate, making this rect an absolute
|
||||||
|
// slice of the world. For a normal rect with a relative width and height,
|
||||||
|
// use ViewportRelative().
|
||||||
|
//
|
||||||
|
// The rect X,Y are the negative Scroll Value.
|
||||||
|
// The rect W,H are the Canvas widget size minus the Scroll Value.
|
||||||
func (w *Canvas) Viewport() render.Rect {
|
func (w *Canvas) Viewport() render.Rect {
|
||||||
var S = w.Size()
|
var S = w.Size()
|
||||||
return render.Rect{
|
return render.Rect{
|
||||||
|
@ -191,6 +242,22 @@ func (w *Canvas) Viewport() render.Rect {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ViewportRelative returns a relative viewport where the Width and Height
|
||||||
|
// values are zero-relative: so you can use it with point.Inside(viewport)
|
||||||
|
// to see if a World Index point should be visible on screen.
|
||||||
|
//
|
||||||
|
// The rect X,Y are the negative Scroll Value
|
||||||
|
// The rect W,H are the Canvas widget size.
|
||||||
|
func (w *Canvas) ViewportRelative() render.Rect {
|
||||||
|
var S = w.Size()
|
||||||
|
return render.Rect{
|
||||||
|
X: -w.Scroll.X,
|
||||||
|
Y: -w.Scroll.Y,
|
||||||
|
W: S.W,
|
||||||
|
H: S.H,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Chunker returns the underlying Chunker object.
|
// Chunker returns the underlying Chunker object.
|
||||||
func (w *Canvas) Chunker() *level.Chunker {
|
func (w *Canvas) Chunker() *level.Chunker {
|
||||||
return w.chunks
|
return w.chunks
|
||||||
|
@ -254,8 +321,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
// src.W and src.H will be AT MOST the full width and height of
|
// src.W and src.H will be AT MOST the full width and height of
|
||||||
// a Canvas widget. Subtract the scroll offset to keep it bounded
|
// a Canvas widget. Subtract the scroll offset to keep it bounded
|
||||||
// visually on its right and bottom sides.
|
// visually on its right and bottom sides.
|
||||||
W: src.W, // - w.Scroll.X,
|
W: src.W,
|
||||||
H: src.H, // - w.Scroll.Y,
|
H: src.H,
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the destination width will cause it to overflow the widget
|
// If the destination width will cause it to overflow the widget
|
||||||
|
@ -273,13 +340,13 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
if dst.X+src.W > p.X+S.W {
|
if dst.X+src.W > p.X+S.W {
|
||||||
// NOTE: delta is a negative number,
|
// NOTE: delta is a negative number,
|
||||||
// so it will subtract from the width.
|
// so it will subtract from the width.
|
||||||
delta := (S.W + p.X) - (dst.W + dst.X)
|
delta := (p.X + S.W - w.BoxThickness(1)) - (dst.W + dst.X)
|
||||||
src.W += delta
|
src.W += delta
|
||||||
dst.W += delta
|
dst.W += delta
|
||||||
}
|
}
|
||||||
if dst.Y+src.H > p.Y+S.H {
|
if dst.Y+src.H > p.Y+S.H {
|
||||||
// NOTE: delta is a negative number
|
// NOTE: delta is a negative number
|
||||||
delta := (S.H + p.Y) - (dst.H + dst.Y)
|
delta := (p.Y + S.H - w.BoxThickness(1)) - (dst.H + dst.Y)
|
||||||
src.H += delta
|
src.H += delta
|
||||||
dst.H += delta
|
dst.H += delta
|
||||||
}
|
}
|
||||||
|
@ -298,30 +365,141 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
// NOTE: delta is a positive number,
|
// NOTE: delta is a positive number,
|
||||||
// so it will add to the destination coordinates.
|
// so it will add to the destination coordinates.
|
||||||
delta := p.X - dst.X
|
delta := p.X - dst.X
|
||||||
dst.X = p.X
|
dst.X = p.X + w.BoxThickness(1)
|
||||||
dst.W -= delta
|
dst.W -= delta
|
||||||
src.X += delta
|
src.X += delta
|
||||||
}
|
}
|
||||||
if dst.Y < p.Y {
|
if dst.Y < p.Y {
|
||||||
delta := p.Y - dst.Y
|
delta := p.Y - dst.Y
|
||||||
dst.Y = p.Y
|
dst.Y = p.Y + w.BoxThickness(1)
|
||||||
dst.H -= delta
|
dst.H -= delta
|
||||||
src.Y += delta
|
src.Y += delta
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Trim the destination width so it doesn't overlap the Canvas border.
|
||||||
|
if dst.W >= S.W-w.BoxThickness(1) {
|
||||||
|
dst.W = S.W - w.BoxThickness(1)
|
||||||
|
}
|
||||||
|
|
||||||
e.Copy(tex, src, dst)
|
e.Copy(tex, src, dst)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// for px := range w.chunks.IterViewport(Viewport) {
|
w.drawActors(e, p)
|
||||||
// // This pixel is visible in the canvas, but offset it by the
|
|
||||||
// // scroll height.
|
// XXX: Debug, show label in canvas corner.
|
||||||
// px.X -= Viewport.X
|
if balance.DebugCanvasLabel {
|
||||||
// px.Y -= Viewport.Y
|
rows := []string{
|
||||||
// color := render.Cyan // px.Swatch.Color
|
w.Name,
|
||||||
// e.DrawPoint(color, render.Point{
|
|
||||||
// X: p.X + w.BoxThickness(1) + px.X,
|
// XXX: debug options, uncomment for more details
|
||||||
// Y: p.Y + w.BoxThickness(1) + px.Y,
|
|
||||||
// })
|
// Size of the canvas
|
||||||
// }
|
// fmt.Sprintf("S=%d,%d", S.W, S.H),
|
||||||
|
|
||||||
|
// Viewport of the canvas
|
||||||
|
// fmt.Sprintf("V=%d,%d:%d,%d",
|
||||||
|
// Viewport.X, Viewport.Y,
|
||||||
|
// Viewport.W, Viewport.H,
|
||||||
|
// ),
|
||||||
|
}
|
||||||
|
if w.actor != nil {
|
||||||
|
rows = append(rows,
|
||||||
|
fmt.Sprintf("WP=%s", w.actor.Point),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
label := ui.NewLabel(ui.Label{
|
||||||
|
Text: strings.Join(rows, "\n"),
|
||||||
|
Font: render.Text{
|
||||||
|
FontFilename: balance.ShellFontFilename,
|
||||||
|
Size: balance.ShellFontSizeSmall,
|
||||||
|
Color: render.White,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
label.SetBackground(render.RGBA(0, 0, 50, 150))
|
||||||
|
label.Compute(e)
|
||||||
|
label.Present(e, render.Point{
|
||||||
|
X: p.X + S.W - label.Size().W - w.BoxThickness(1),
|
||||||
|
Y: p.Y + w.BoxThickness(1),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// drawActors superimposes the actors on top of the drawing.
|
||||||
|
func (w *Canvas) drawActors(e render.Engine, p render.Point) {
|
||||||
|
var (
|
||||||
|
Viewport = w.ViewportRelative()
|
||||||
|
S = w.Size()
|
||||||
|
)
|
||||||
|
|
||||||
|
// See if each Actor is in range of the Viewport.
|
||||||
|
for _, a := range w.actors {
|
||||||
|
var (
|
||||||
|
actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE
|
||||||
|
can = a.Canvas // Canvas widget that draws the actor
|
||||||
|
actorPoint = actor.Point // XXX TODO: DO NOT CHANGE
|
||||||
|
actorSize = can.Size()
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a box of World Coordinates that this actor occupies. The
|
||||||
|
// Actor X,Y from level data is already a World Coordinate;
|
||||||
|
// accomodate for the size of the Actor.
|
||||||
|
actorBox := render.Rect{
|
||||||
|
X: actorPoint.X,
|
||||||
|
Y: actorPoint.Y,
|
||||||
|
W: actorSize.W,
|
||||||
|
H: actorSize.H,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Is any part of the actor visible?
|
||||||
|
if !Viewport.Intersects(actorBox) {
|
||||||
|
continue // not visible on screen
|
||||||
|
}
|
||||||
|
|
||||||
|
drawAt := render.Point{
|
||||||
|
X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1),
|
||||||
|
Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1),
|
||||||
|
}
|
||||||
|
resizeTo := actorSize
|
||||||
|
|
||||||
|
// XXX TODO: when an Actor hits the left or top edge and shrinks,
|
||||||
|
// scrolling to offset that shrink is currently hard to solve.
|
||||||
|
scrollTo := render.Origin
|
||||||
|
|
||||||
|
// Handle cropping and scaling if this Actor's canvas can't be
|
||||||
|
// completely visible within the parent.
|
||||||
|
if drawAt.X+resizeTo.W > p.X+S.W {
|
||||||
|
// Hitting the right edge, shrunk the width now.
|
||||||
|
delta := (drawAt.X + resizeTo.W) - (p.X + S.W)
|
||||||
|
resizeTo.W -= delta
|
||||||
|
} else if drawAt.X < p.X {
|
||||||
|
// Hitting the left edge. Cap the X coord and shrink the width.
|
||||||
|
delta := p.X - drawAt.X // positive number
|
||||||
|
drawAt.X = p.X
|
||||||
|
// scrollTo.X -= delta // TODO
|
||||||
|
resizeTo.W -= delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if drawAt.Y+resizeTo.H > p.Y+S.H {
|
||||||
|
// Hitting the bottom edge, shrink the height.
|
||||||
|
delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H)
|
||||||
|
resizeTo.H -= delta
|
||||||
|
} else if drawAt.Y < p.Y {
|
||||||
|
// Hitting the top edge. Cap the Y coord and shrink the height.
|
||||||
|
delta := p.Y - drawAt.Y
|
||||||
|
drawAt.Y = p.Y
|
||||||
|
// scrollTo.Y -= delta // TODO
|
||||||
|
resizeTo.H -= delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if resizeTo != actorSize {
|
||||||
|
can.Resize(resizeTo)
|
||||||
|
can.ScrollTo(scrollTo)
|
||||||
|
}
|
||||||
|
can.Present(e, drawAt)
|
||||||
|
|
||||||
|
// Clean up the canvas size and offset.
|
||||||
|
can.Resize(actorSize) // restore original size in case cropped
|
||||||
|
can.ScrollTo(render.Origin)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user