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
|
||||
messages.
|
||||
* 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
|
||||
chunk to bitmap. Helps visualize where the chunks and caching
|
||||
are happening.
|
||||
* `DEBUG_CANVAS_BORDER`: draw a border color around every uix.Canvas
|
||||
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.
|
||||
* `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):
|
||||
* `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows
|
||||
in the Editor Mode, in pixels per tick.
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package balance
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
@ -15,13 +15,21 @@ var (
|
|||
* 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
|
||||
// on disk. Default is white. Setting this to translucent yellow is a great
|
||||
// way to visualize the chunks loaded from cache on your screen.
|
||||
DebugChunkBitmapBackground = render.White // XXX: export $DEBUG_CHUNK_COLOR
|
||||
|
||||
// Put a border around all Canvas widgets.
|
||||
DebugCanvasBorder = render.Red
|
||||
DebugCanvasBorder = render.Invisible
|
||||
DebugCanvasLabel = false // Tag the canvas with a label.
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
@ -45,24 +53,33 @@ func init() {
|
|||
// Visualizers
|
||||
"DEBUG_CHUNK_COLOR": &DebugChunkBitmapBackground,
|
||||
"DEBUG_CANVAS_BORDER": &DebugCanvasBorder,
|
||||
"DEBUG_CANVAS_LABEL": &DebugCanvasLabel,
|
||||
}
|
||||
for name, value := range config {
|
||||
switch v := value.(type) {
|
||||
case *int:
|
||||
*v = IntEnv(name, *(v))
|
||||
case *bool:
|
||||
*v = BoolEnv(name, *(v))
|
||||
case *int32:
|
||||
*v = int32(IntEnv(name, int(*(v))))
|
||||
case *render.Color:
|
||||
*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.
|
||||
// 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 {
|
||||
if color := os.Getenv(name); color != "" {
|
||||
fmt.Printf("set %s to %s\n", name, color)
|
||||
return render.MustHexColor(color)
|
||||
}
|
||||
return v
|
||||
|
@ -79,3 +96,16 @@ func IntEnv(name string, v int) int {
|
|||
}
|
||||
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.
|
||||
var (
|
||||
// TODO: why not renders transparent
|
||||
ShellFontFilename = "./fonts/DejaVuSansMono.ttf"
|
||||
ShellBackgroundColor = render.RGBA(0, 20, 40, 200)
|
||||
ShellForegroundColor = render.RGBA(0, 153, 255, 255)
|
||||
ShellPromptColor = render.White
|
||||
ShellPadding int32 = 8
|
||||
ShellFontSize = 16
|
||||
ShellFontSizeSmall = 10
|
||||
ShellCursorBlinkRate uint64 = 20
|
||||
ShellHistoryLineCount = 8
|
||||
|
||||
|
|
132
config.go
132
config.go
|
@ -2,106 +2,16 @@ package doodle
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"github.com/kirsle/configdir"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
)
|
||||
|
||||
// Configuration constants.
|
||||
var (
|
||||
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)
|
||||
}
|
||||
var reSimpleFilename = regexp.MustCompile(`^([A-Za-z0-9-_.,+ '"\[\](){}]+)$`)
|
||||
|
||||
/*
|
||||
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 {
|
||||
log.Debug("EditFile: simple filename %s", 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)
|
||||
absPath = foundFilename
|
||||
} else {
|
||||
|
@ -141,39 +51,3 @@ func (d *Doodle) EditFile(filename string) error {
|
|||
|
||||
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/enum"
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"github.com/kirsle/golog"
|
||||
)
|
||||
|
@ -28,11 +29,15 @@ type Doodle struct {
|
|||
Engine render.Engine
|
||||
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
|
||||
running bool
|
||||
ticks uint64
|
||||
width int32
|
||||
height int32
|
||||
width int
|
||||
height int
|
||||
|
||||
// Command line shell options.
|
||||
shell Shell
|
||||
|
@ -47,8 +52,8 @@ func New(debug bool, engine render.Engine) *Doodle {
|
|||
Engine: engine,
|
||||
startTime: time.Now(),
|
||||
running: true,
|
||||
width: int32(balance.Width),
|
||||
height: int32(balance.Height),
|
||||
width: balance.Width,
|
||||
height: balance.Height,
|
||||
}
|
||||
d.shell = NewShell(d)
|
||||
|
||||
|
@ -97,6 +102,7 @@ func (d *Doodle) Run() error {
|
|||
d.running = false
|
||||
break
|
||||
}
|
||||
d.event = ev
|
||||
|
||||
// Command line shell.
|
||||
if d.shell.Open {
|
||||
|
|
|
@ -11,6 +11,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/enum"
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
"git.kirsle.net/apps/doodle/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
|
@ -103,6 +104,18 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
|
||||
// Loop the editor scene.
|
||||
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)
|
||||
|
||||
// Switching to Play Mode?
|
||||
|
@ -133,6 +146,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
|||
s.filename = filename
|
||||
|
||||
level, err := level.LoadJSON(filename)
|
||||
fmt.Printf("%+v\n", level)
|
||||
if err != nil {
|
||||
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.Level = 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
|
||||
}
|
||||
|
||||
|
@ -150,8 +178,8 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
|||
return errors.New("SaveLevel: current drawing is not a Level type")
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(filename, extLevel) {
|
||||
filename += extLevel
|
||||
if !strings.HasSuffix(filename, enum.LevelExt) {
|
||||
filename += enum.LevelExt
|
||||
}
|
||||
|
||||
s.filename = filename
|
||||
|
@ -173,7 +201,7 @@ func (s *EditorScene) SaveLevel(filename string) error {
|
|||
}
|
||||
|
||||
// Save it to their profile directory.
|
||||
filename = LevelPath(filename)
|
||||
filename = userdir.LevelPath(filename)
|
||||
log.Info("Write Level: %s", filename)
|
||||
err = ioutil.WriteFile(filename, json, 0644)
|
||||
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")
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(filename, extDoodad) {
|
||||
filename += extDoodad
|
||||
if !strings.HasSuffix(filename, enum.DoodadExt) {
|
||||
filename += enum.DoodadExt
|
||||
}
|
||||
|
||||
s.filename = filename
|
||||
|
@ -223,7 +251,7 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
|||
d.Layers[0].Chunker = s.UI.Canvas.Chunker()
|
||||
|
||||
// Save it to their profile directory.
|
||||
filename = DoodadPath(filename)
|
||||
filename = userdir.DoodadPath(filename)
|
||||
log.Info("Write Doodad: %s", filename)
|
||||
err := d.WriteJSON(filename)
|
||||
return err
|
||||
|
@ -231,5 +259,6 @@ func (s *EditorScene) SaveDoodad(filename string) error {
|
|||
|
||||
// Destroy the scene.
|
||||
func (s *EditorScene) Destroy() error {
|
||||
debugWorldIndex = render.Origin
|
||||
return nil
|
||||
}
|
||||
|
|
110
editor_ui.go
110
editor_ui.go
|
@ -9,11 +9,15 @@ import (
|
|||
"git.kirsle.net/apps/doodle/enum"
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
"git.kirsle.net/apps/doodle/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"git.kirsle.net/apps/doodle/ui"
|
||||
"git.kirsle.net/apps/doodle/uix"
|
||||
)
|
||||
|
||||
// Width of the panel frame.
|
||||
var paletteWidth int32 = 150
|
||||
|
||||
// EditorUI manages the user interface for the Editor Scene.
|
||||
type EditorUI struct {
|
||||
d *Doodle
|
||||
|
@ -70,6 +74,8 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
|||
u.Palette = u.SetupPalette(d)
|
||||
u.Workspace = u.SetupWorkspace(d) // important that this is last!
|
||||
|
||||
u.Resized(d)
|
||||
|
||||
// Position the Canvas inside the frame.
|
||||
u.Workspace.Pack(u.Canvas, ui.Pack{
|
||||
Anchor: ui.N,
|
||||
|
@ -84,14 +90,75 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
|||
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.
|
||||
func (u *EditorUI) Loop(ev *events.State) {
|
||||
u.Supervisor.Loop(ev)
|
||||
|
||||
u.StatusMouseText = fmt.Sprintf("Mouse: (%d,%d)",
|
||||
{
|
||||
var P = u.Workspace.Point()
|
||||
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.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.
|
||||
func (u *EditorUI) SetupWorkspace(d *Doodle) *ui.Frame {
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -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
|
||||
// _wanted_ to be smaller, as in Doodad Editing Mode.
|
||||
func (u *EditorUI) ExpandCanvas(e render.Engine) {
|
||||
if u.Scene.DrawingType == enum.LevelDrawing {
|
||||
u.Canvas.Resize(u.Workspace.Size())
|
||||
} else {
|
||||
// Size is managed externally.
|
||||
}
|
||||
u.Workspace.Compute(e)
|
||||
}
|
||||
|
||||
// SetupMenuBar sets up the menu bar.
|
||||
func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
||||
frame := ui.NewFrame("MenuBar")
|
||||
frame.Configure(ui.Config{
|
||||
Width: d.width,
|
||||
Background: render.Black,
|
||||
})
|
||||
|
||||
type menuButton struct {
|
||||
Text string
|
||||
|
@ -293,21 +348,13 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
|||
|
||||
// SetupPalette sets up the palette panel.
|
||||
func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
||||
var paletteWidth int32 = 150
|
||||
|
||||
window := ui.NewWindow("Palette")
|
||||
window.ConfigureTitle(balance.TitleConfig)
|
||||
window.TitleBar().Font = balance.TitleFont
|
||||
window.Configure(ui.Config{
|
||||
Width: paletteWidth,
|
||||
Height: u.d.height - u.StatusBar.Size().H,
|
||||
Background: balance.WindowBackground,
|
||||
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.
|
||||
tabFrame := ui.NewFrame("Palette Tabs")
|
||||
|
@ -355,7 +402,7 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
|||
Fill: true,
|
||||
})
|
||||
|
||||
doodadsAvailable, err := ListDoodads()
|
||||
doodadsAvailable, err := userdir.ListDoodads()
|
||||
if err != nil {
|
||||
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 {
|
||||
log.Error(err.Error())
|
||||
doodad = doodads.New(balance.DoodadSize)
|
||||
|
@ -459,7 +506,6 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
|||
BorderStyle: ui.BorderRaised,
|
||||
Background: render.Grey,
|
||||
BorderSize: 2,
|
||||
Width: d.width,
|
||||
})
|
||||
|
||||
style := ui.Config{
|
||||
|
@ -503,15 +549,13 @@ func (u *EditorUI) SetupStatusBar(d *Doodle) *ui.Frame {
|
|||
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{
|
||||
W: d.width,
|
||||
W: int32(d.width),
|
||||
H: labelHeight + frame.BoxThickness(1),
|
||||
})
|
||||
frame.Compute(d.Engine)
|
||||
frame.MoveTo(render.Point{
|
||||
X: 0,
|
||||
Y: d.height - frame.Size().H,
|
||||
})
|
||||
|
||||
return frame
|
||||
}
|
||||
|
|
|
@ -10,3 +10,9 @@ const (
|
|||
LevelDrawing DrawingType = iota
|
||||
DoodadDrawing
|
||||
)
|
||||
|
||||
// File extensions
|
||||
const (
|
||||
LevelExt = ".level"
|
||||
DoodadExt = ".doodad"
|
||||
)
|
||||
|
|
|
@ -25,6 +25,9 @@ type State struct {
|
|||
// Cursor positions.
|
||||
CursorX *Int32Tick
|
||||
CursorY *Int32Tick
|
||||
|
||||
// Window events: window has changed size.
|
||||
Resized *BoolTick
|
||||
}
|
||||
|
||||
// New creates a new event state manager.
|
||||
|
@ -43,6 +46,7 @@ func New() *State {
|
|||
Down: &BoolTick{},
|
||||
CursorX: &Int32Tick{},
|
||||
CursorY: &Int32Tick{},
|
||||
Resized: &BoolTick{},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
74
fps.go
74
fps.go
|
@ -2,9 +2,12 @@ package doodle
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/balance"
|
||||
"git.kirsle.net/apps/doodle/doodads"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"git.kirsle.net/apps/doodle/ui"
|
||||
)
|
||||
|
||||
// Frames to cache for FPS calculation.
|
||||
|
@ -15,6 +18,12 @@ const maxSamples = 100
|
|||
var (
|
||||
DebugOverlay = true
|
||||
DebugCollision = true
|
||||
|
||||
DebugTextPadding int32 = 8
|
||||
DebugTextSize = 24
|
||||
DebugTextColor = render.SkyBlue
|
||||
DebugTextStroke = render.Grey
|
||||
DebugTextShadow = render.Black
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -24,6 +33,11 @@ var (
|
|||
fpsFrames int
|
||||
fpsSkipped uint32
|
||||
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.
|
||||
|
@ -32,29 +46,53 @@ func (d *Doodle) DrawDebugOverlay() {
|
|||
return
|
||||
}
|
||||
|
||||
label := fmt.Sprintf(
|
||||
"FPS: %d (%dms) S:%s F12=screenshot",
|
||||
fpsCurrent,
|
||||
fpsSkipped,
|
||||
var (
|
||||
darken = balance.DebugStrokeDarken
|
||||
Yoffset int32 = 20 // leave room for the menu bar
|
||||
Xoffset int32 = 5
|
||||
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(
|
||||
render.Text{
|
||||
Text: label,
|
||||
Size: 24,
|
||||
Color: DebugTextColor,
|
||||
Stroke: DebugTextStroke,
|
||||
Shadow: DebugTextShadow,
|
||||
key := ui.NewLabel(ui.Label{
|
||||
Text: strings.Join(keys, "\n"),
|
||||
Font: render.Text{
|
||||
Size: balance.DebugFontSize,
|
||||
FontFilename: balance.ShellFontFilename,
|
||||
Color: balance.DebugLabelColor,
|
||||
Stroke: balance.DebugLabelColor.Darken(darken),
|
||||
},
|
||||
render.Point{
|
||||
X: DebugTextPadding,
|
||||
Y: DebugTextPadding + 32, // extra padding to not overlay menu bars
|
||||
})
|
||||
key.Compute(d.Engine)
|
||||
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 {
|
||||
log.Error("DrawDebugOverlay: text error: %s", err.Error())
|
||||
}
|
||||
})
|
||||
value.Compute(d.Engine)
|
||||
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.
|
||||
|
|
|
@ -275,14 +275,14 @@ func (s *GUITestScene) Draw(d *Doodle) error {
|
|||
})
|
||||
label.Compute(d.Engine)
|
||||
label.MoveTo(render.Point{
|
||||
X: (d.width / 2) - (label.Size().W / 2),
|
||||
X: (int32(d.width) / 2) - (label.Size().W / 2),
|
||||
Y: 40,
|
||||
})
|
||||
label.Present(d.Engine, label.Point())
|
||||
|
||||
s.Window.Compute(d.Engine)
|
||||
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,
|
||||
})
|
||||
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.
|
||||
m.Chunker.Inflate(m.Palette)
|
||||
m.Actors.Inflate()
|
||||
|
||||
// Inflate the private instance values.
|
||||
m.Palette.Inflate()
|
||||
|
|
|
@ -15,6 +15,9 @@ type Base struct {
|
|||
GameVersion string `json:"gameVersion"` // Game version that created the level.
|
||||
Title string `json:"title"`
|
||||
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.
|
||||
|
@ -29,6 +32,9 @@ type Level struct {
|
|||
// The Palette holds the unique "colors" used in this map file, and their
|
||||
// properties (solid, fire, slippery, etc.)
|
||||
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.
|
||||
|
|
|
@ -77,14 +77,14 @@ func (s *MainScene) Draw(d *Doodle) error {
|
|||
})
|
||||
label.Compute(d.Engine)
|
||||
label.MoveTo(render.Point{
|
||||
X: (d.width / 2) - (label.Size().W / 2),
|
||||
X: (int32(d.width) / 2) - (label.Size().W / 2),
|
||||
Y: 120,
|
||||
})
|
||||
label.Present(d.Engine, label.Point())
|
||||
|
||||
s.frame.Compute(d.Engine)
|
||||
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,
|
||||
})
|
||||
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 {
|
||||
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
|
||||
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)
|
||||
|
||||
// Given a filename or map data to play?
|
||||
|
|
|
@ -15,6 +15,7 @@ type Engine interface {
|
|||
// Poll for events like keypresses and mouse clicks.
|
||||
Poll() (*events.State, error)
|
||||
GetTicks() uint32
|
||||
WindowSize() (w, h int)
|
||||
|
||||
// Present presents the current state to the screen.
|
||||
Present() error
|
||||
|
@ -88,6 +89,27 @@ func (r Rect) Bigger(other Rect) bool {
|
|||
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.
|
||||
func (r Rect) IsZero() bool {
|
||||
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.
|
||||
//
|
||||
// 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 {
|
||||
var (
|
||||
x1 = r.X
|
||||
|
@ -62,7 +65,8 @@ func (p Point) Inside(r Rect) bool {
|
|||
x2 = r.X + r.W
|
||||
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.
|
||||
|
|
|
@ -8,13 +8,9 @@ import (
|
|||
)
|
||||
|
||||
func TestPointInside(t *testing.T) {
|
||||
var p = render.Point{
|
||||
X: 128,
|
||||
Y: 256,
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
rect render.Rect
|
||||
p render.Point
|
||||
shouldPass bool
|
||||
}
|
||||
tests := []testCase{
|
||||
|
@ -25,6 +21,7 @@ func TestPointInside(t *testing.T) {
|
|||
W: 500,
|
||||
H: 500,
|
||||
},
|
||||
p: render.NewPoint(128, 256),
|
||||
shouldPass: true,
|
||||
},
|
||||
testCase{
|
||||
|
@ -34,14 +31,27 @@ func TestPointInside(t *testing.T) {
|
|||
W: 40,
|
||||
H: 60,
|
||||
},
|
||||
p: render.NewPoint(128, 256),
|
||||
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 {
|
||||
if p.Inside(test.rect) != test.shouldPass {
|
||||
t.Errorf("Failed: %s inside %s should %s",
|
||||
p,
|
||||
if test.p.Inside(test.rect) != test.shouldPass {
|
||||
t.Errorf("Failed: %s inside %s should be %s",
|
||||
test.p,
|
||||
test.rect,
|
||||
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
|
||||
}
|
|
@ -9,6 +9,7 @@ var (
|
|||
DebugMouseEvents = false
|
||||
DebugClickEvents = false
|
||||
DebugKeyEvents = false
|
||||
DebugWindowEvents = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
|
|
@ -2,7 +2,6 @@
|
|||
package sdl
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
|
@ -69,7 +68,7 @@ func (r *Renderer) Setup() error {
|
|||
sdl.WINDOWPOS_CENTERED,
|
||||
r.width,
|
||||
r.height,
|
||||
sdl.WINDOW_SHOWN,
|
||||
sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
@ -93,111 +92,10 @@ func (r *Renderer) GetTicks() uint32 {
|
|||
return sdl.GetTicks()
|
||||
}
|
||||
|
||||
// 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.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
|
||||
// WindowSize returns the SDL window size.
|
||||
func (r *Renderer) WindowSize() (int, int) {
|
||||
w, h := r.window.GetSize()
|
||||
return int(w), int(h)
|
||||
}
|
||||
|
||||
// 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,
|
||||
render.Rect{
|
||||
X: 0,
|
||||
Y: d.height - boxHeight,
|
||||
W: d.width,
|
||||
Y: int32(d.height) - boxHeight,
|
||||
W: int32(d.width),
|
||||
H: boxHeight,
|
||||
},
|
||||
)
|
||||
|
||||
// Draw the recent commands.
|
||||
outputY := d.height - int32(lineHeight*2)
|
||||
outputY := int32(d.height - (lineHeight * 2))
|
||||
for i := 0; i < balance.ShellHistoryLineCount; i++ {
|
||||
if len(s.Output) > 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{
|
||||
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 {
|
||||
// Otherwise, just draw flashed messages.
|
||||
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-- {
|
||||
flash := s.Flashes[i-1]
|
||||
if d.ticks >= flash.Expires {
|
||||
|
|
34
ui/label.go
34
ui/label.go
|
@ -2,6 +2,7 @@ package ui
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
@ -23,6 +24,7 @@ type Label struct {
|
|||
|
||||
width int32
|
||||
height int32
|
||||
lineHeight int
|
||||
}
|
||||
|
||||
// NewLabel creates a new label.
|
||||
|
@ -60,12 +62,26 @@ func (w *Label) Value() string {
|
|||
|
||||
// Compute the size of the label widget.
|
||||
func (w *Label) Compute(e render.Engine) {
|
||||
rect, err := e.ComputeTextRect(w.text())
|
||||
text := w.text()
|
||||
lines := strings.Split(text.Text, "\n")
|
||||
|
||||
// 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 (
|
||||
padX = w.Font.Padding + w.Font.PadX
|
||||
padY = w.Font.Padding + w.Font.PadY
|
||||
|
@ -73,14 +89,14 @@ func (w *Label) Compute(e render.Engine) {
|
|||
|
||||
if !w.FixedSize() {
|
||||
w.resizeAuto(render.Rect{
|
||||
W: rect.W + (padX * 2),
|
||||
H: rect.H + (padY * 2),
|
||||
W: maxRect.W + (padX * 2),
|
||||
H: maxRect.H + (padY * 2),
|
||||
})
|
||||
}
|
||||
|
||||
w.MoveTo(render.Point{
|
||||
X: rect.X + w.BoxThickness(1),
|
||||
Y: rect.Y + w.BoxThickness(1),
|
||||
X: maxRect.X + 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)
|
||||
|
||||
var (
|
||||
text = w.text()
|
||||
padX = w.Font.Padding + w.Font.PadX
|
||||
padY = w.Font.Padding + w.Font.PadY
|
||||
)
|
||||
|
||||
w.DrawBox(e, P)
|
||||
e.DrawText(w.text(), render.Point{
|
||||
for i, line := range strings.Split(text.Text, "\n") {
|
||||
text.Text = line
|
||||
e.DrawText(text, render.Point{
|
||||
X: P.X + border + padX,
|
||||
Y: P.Y + border + padY,
|
||||
Y: P.Y + border + padY + int32(i*w.lineHeight),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
230
uix/canvas.go
230
uix/canvas.go
|
@ -8,6 +8,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/doodads"
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
"git.kirsle.net/apps/doodle/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"git.kirsle.net/apps/doodle/ui"
|
||||
)
|
||||
|
@ -21,7 +22,14 @@ type Canvas struct {
|
|||
Editable bool
|
||||
Scrollable bool
|
||||
|
||||
// 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
|
||||
lastPixel *level.Pixel
|
||||
|
||||
|
@ -29,6 +37,12 @@ type Canvas struct {
|
|||
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.
|
||||
//
|
||||
// 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,
|
||||
Palette: level.NewPalette(),
|
||||
chunks: level.NewChunker(size),
|
||||
actors: make([]*Actor, 0),
|
||||
}
|
||||
w.setup()
|
||||
w.IDFunc(func() string {
|
||||
|
@ -80,6 +95,32 @@ func (w *Canvas) LoadDoodad(d *doodads.Doodad) {
|
|||
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.
|
||||
func (w *Canvas) SetSwatch(s *level.Swatch) {
|
||||
w.Palette.ActiveSwatch = s
|
||||
|
@ -88,6 +129,16 @@ func (w *Canvas) SetSwatch(s *level.Swatch) {
|
|||
// setup common configs between both initializers of the canvas.
|
||||
func (w *Canvas) setup() {
|
||||
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.SetBackground(render.Yellow)
|
||||
})
|
||||
|
@ -145,14 +196,6 @@ func (w *Canvas) Loop(ev *events.State) error {
|
|||
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.
|
||||
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
|
||||
if lastPixel != nil {
|
||||
|
@ -168,7 +211,6 @@ func (w *Canvas) Loop(ev *events.State) error {
|
|||
w.pixelHistory = append(w.pixelHistory, pixel)
|
||||
|
||||
// Save in the pixel canvas map.
|
||||
log.Info("Set: %s %s", cursor, pixel.Swatch.Color)
|
||||
w.chunks.Set(cursor, pixel.Swatch)
|
||||
}
|
||||
} else {
|
||||
|
@ -181,6 +223,15 @@ func (w *Canvas) Loop(ev *events.State) error {
|
|||
// 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
|
||||
// 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 {
|
||||
var S = w.Size()
|
||||
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.
|
||||
func (w *Canvas) Chunker() *level.Chunker {
|
||||
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
|
||||
// a Canvas widget. Subtract the scroll offset to keep it bounded
|
||||
// visually on its right and bottom sides.
|
||||
W: src.W, // - w.Scroll.X,
|
||||
H: src.H, // - w.Scroll.Y,
|
||||
W: src.W,
|
||||
H: src.H,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// NOTE: delta is a negative number,
|
||||
// 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
|
||||
dst.W += delta
|
||||
}
|
||||
if dst.Y+src.H > p.Y+S.H {
|
||||
// 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
|
||||
dst.H += delta
|
||||
}
|
||||
|
@ -298,30 +365,141 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
|||
// NOTE: delta is a positive number,
|
||||
// so it will add to the destination coordinates.
|
||||
delta := p.X - dst.X
|
||||
dst.X = p.X
|
||||
dst.X = p.X + w.BoxThickness(1)
|
||||
dst.W -= delta
|
||||
src.X += delta
|
||||
}
|
||||
if dst.Y < p.Y {
|
||||
delta := p.Y - dst.Y
|
||||
dst.Y = p.Y
|
||||
dst.Y = p.Y + w.BoxThickness(1)
|
||||
dst.H -= 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)
|
||||
}
|
||||
}
|
||||
|
||||
// for px := range w.chunks.IterViewport(Viewport) {
|
||||
// // This pixel is visible in the canvas, but offset it by the
|
||||
// // scroll height.
|
||||
// px.X -= Viewport.X
|
||||
// px.Y -= Viewport.Y
|
||||
// color := render.Cyan // px.Swatch.Color
|
||||
// e.DrawPoint(color, render.Point{
|
||||
// X: p.X + w.BoxThickness(1) + px.X,
|
||||
// Y: p.Y + w.BoxThickness(1) + px.Y,
|
||||
// })
|
||||
// }
|
||||
w.drawActors(e, p)
|
||||
|
||||
// XXX: Debug, show label in canvas corner.
|
||||
if balance.DebugCanvasLabel {
|
||||
rows := []string{
|
||||
w.Name,
|
||||
|
||||
// XXX: debug options, uncomment for more details
|
||||
|
||||
// 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