Abstract Drawing Canvas into Reusable Widget
The `level.Canvas` is a widget that holds onto its Palette and Grid and has interactions to allow scrolling and editing the grid using the swatches available on the palette. Thus all of the logic in the Editor Mode for drawing directly onto the root SDL surface are now handled inside a level.Canvas instance. The `level.Canvas` widget has the following properties: * Like any widget it has an X,Y position and a width/height. * It has a Scroll position to control which slice of its drawing will be visible inside its bounding box. * It supports levels having negative coordinates for their pixels. It doesn't care. The default Scroll position is (0,0) at the top left corner of the widget but you can scroll into the negatives and see the negative pixels. * Keyboard keys will scroll the viewport inside the canvas. * The canvas draws only the pixels that are visible inside its bounding box. This feature will eventually pave the way toward: * Doodads being dropped on top of your map, each Doodad being its own Canvas widget. * Using drawings as button icons for the user interface, as the Canvas is a normal widget.
This commit is contained in:
parent
5956863996
commit
5434484b6e
30
Ideas.md
30
Ideas.md
|
@ -3,6 +3,7 @@
|
|||
## Table of Contents
|
||||
|
||||
* [Major Milestones](#major-milestones)
|
||||
* [Release Modes](#release-modes)
|
||||
* [File Formats](#file-formats)
|
||||
* [Text Console](#text-console)
|
||||
* [Doodads](#doodads)
|
||||
|
@ -105,6 +106,35 @@ For creating Doodads in particular:
|
|||
your window). This will use a Canvas widget in the UI toolkit as an abstraction
|
||||
layer. Small canvases will be useful for drawing doodads of a fixed size.
|
||||
|
||||
# Release Modes
|
||||
|
||||
## Shareware/Demo Version
|
||||
|
||||
This would be a free version with some limitations. Early public alpha releases
|
||||
would be built with this release mode.
|
||||
|
||||
* Optional expiration date after which the game WILL NOT run.
|
||||
* Can play the built-in maps and create your own custom maps.
|
||||
* No support for Custom Doodads. The game will have the code to read Doodads from
|
||||
disk dummied out/not compiled in, and any third-party map that embeds or
|
||||
references custom Doodads will not be allowed to run.
|
||||
* Custom maps created in a demo version will have some feature limitations:
|
||||
* Infinite map sizes not allowed, only bounded ones with a fixed size.
|
||||
* No custom wallpaper images, only built-in ones.
|
||||
* No custom palette for new maps, only the default standard palette.
|
||||
* No features for drawing doodad graphics (multiple frames, etc.)
|
||||
|
||||
As an end user, it means basically:
|
||||
|
||||
* You are limited to built-in doodads but you can make (and share) and play
|
||||
other users' custom maps that only use the built-in doodads.
|
||||
|
||||
## Release Version
|
||||
|
||||
TBD.
|
||||
|
||||
Probably mostly DRM free. Will want some sort of account server early-on though.
|
||||
|
||||
# File Formats
|
||||
|
||||
* The file formats should eventually have a **Protocol Buffers** binary
|
||||
|
|
7
balance/numbers.go
Normal file
7
balance/numbers.go
Normal file
|
@ -0,0 +1,7 @@
|
|||
package balance
|
||||
|
||||
// Numbers.
|
||||
var (
|
||||
// Speed to scroll a canvas with arrow keys in Edit Mode.
|
||||
CanvasScrollSpeed int32 = 8
|
||||
)
|
161
editor_scene.go
161
editor_scene.go
|
@ -1,12 +1,8 @@
|
|||
package doodle
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/png"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
"git.kirsle.net/apps/doodle/level"
|
||||
|
@ -18,18 +14,16 @@ type EditorScene struct {
|
|||
// Configuration for the scene initializer.
|
||||
OpenFile bool
|
||||
Filename string
|
||||
Canvas level.Grid
|
||||
Canvas *level.Grid
|
||||
|
||||
UI *EditorUI
|
||||
|
||||
Palette *level.Palette // Full palette of swatches for this level
|
||||
Swatch *level.Swatch // actively selected painting swatch
|
||||
// The canvas widget that contains the map we're working on.
|
||||
// XXX: in dev builds this is available at $ d.Scene.GetDrawing()
|
||||
drawing *level.Canvas
|
||||
|
||||
// History of all the pixels placed by the user.
|
||||
pixelHistory []*level.Pixel
|
||||
lastPixel *level.Pixel // last pixel placed while mouse down and dragging
|
||||
canvas level.Grid
|
||||
filename string // Last saved filename.
|
||||
filename string // Last saved filename.
|
||||
|
||||
// Canvas size
|
||||
width int32
|
||||
|
@ -43,7 +37,11 @@ func (s *EditorScene) Name() string {
|
|||
|
||||
// Setup the editor scene.
|
||||
func (s *EditorScene) Setup(d *Doodle) error {
|
||||
s.Palette = level.DefaultPalette()
|
||||
s.drawing = level.NewCanvas(true)
|
||||
s.drawing.Palette = level.DefaultPalette()
|
||||
if len(s.drawing.Palette.Swatches) > 0 {
|
||||
s.drawing.SetSwatch(s.drawing.Palette.Swatches[0])
|
||||
}
|
||||
|
||||
// Were we given configuration data?
|
||||
if s.Filename != "" {
|
||||
|
@ -59,28 +57,15 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
}
|
||||
if s.Canvas != nil {
|
||||
log.Debug("EditorScene: Received Canvas from caller")
|
||||
s.canvas = s.Canvas
|
||||
s.drawing.Load(s.drawing.Palette, s.Canvas)
|
||||
s.Canvas = nil
|
||||
}
|
||||
|
||||
// Select the first swatch in the palette.
|
||||
if len(s.Palette.Swatches) > 0 {
|
||||
s.Swatch = s.Palette.Swatches[0]
|
||||
s.Palette.ActiveSwatch = s.Swatch.Name
|
||||
}
|
||||
|
||||
// Initialize the user interface. It references the palette and such so it
|
||||
// must be initialized after those things.
|
||||
s.UI = NewEditorUI(d, s)
|
||||
d.Flash("Editor Mode. Press 'P' to play this map.")
|
||||
|
||||
if s.pixelHistory == nil {
|
||||
s.pixelHistory = []*level.Pixel{}
|
||||
}
|
||||
if s.canvas == nil {
|
||||
log.Debug("EditorScene: Setting default canvas to an empty grid")
|
||||
s.canvas = level.Grid{}
|
||||
}
|
||||
s.width = d.width // TODO: canvas width = copy the window size
|
||||
s.height = d.height
|
||||
return nil
|
||||
|
@ -89,99 +74,45 @@ func (s *EditorScene) Setup(d *Doodle) error {
|
|||
// Loop the editor scene.
|
||||
func (s *EditorScene) Loop(d *Doodle, ev *events.State) error {
|
||||
s.UI.Loop(ev)
|
||||
|
||||
// Taking a screenshot?
|
||||
if ev.ScreenshotKey.Pressed() {
|
||||
log.Info("Taking a screenshot")
|
||||
s.Screenshot()
|
||||
}
|
||||
s.drawing.Loop(ev)
|
||||
|
||||
// Switching to Play Mode?
|
||||
if ev.KeyName.Read() == "p" {
|
||||
log.Info("Play Mode, Go!")
|
||||
d.Goto(&PlayScene{
|
||||
Canvas: s.canvas,
|
||||
Canvas: s.drawing.Grid(),
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clicking? Log all the pixels while doing so.
|
||||
if ev.Button1.Now {
|
||||
// log.Warn("Button1: %+v", ev.Button1)
|
||||
lastPixel := s.lastPixel
|
||||
pixel := &level.Pixel{
|
||||
X: ev.CursorX.Now,
|
||||
Y: ev.CursorY.Now,
|
||||
Palette: s.Palette,
|
||||
Swatch: s.Swatch,
|
||||
}
|
||||
|
||||
// Append unique new pixels.
|
||||
if len(s.pixelHistory) == 0 || s.pixelHistory[len(s.pixelHistory)-1] != pixel {
|
||||
if lastPixel != nil {
|
||||
// Draw the pixels in between.
|
||||
if lastPixel != pixel {
|
||||
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||
dot := &level.Pixel{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
Palette: lastPixel.Palette,
|
||||
Swatch: lastPixel.Swatch,
|
||||
}
|
||||
s.canvas[dot] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.lastPixel = pixel
|
||||
s.pixelHistory = append(s.pixelHistory, pixel)
|
||||
|
||||
// Save in the pixel canvas map.
|
||||
s.canvas[pixel] = nil
|
||||
}
|
||||
} else {
|
||||
s.lastPixel = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Draw the current frame.
|
||||
func (s *EditorScene) Draw(d *Doodle) error {
|
||||
// Clear the canvas and fill it with white.
|
||||
d.Engine.Clear(render.White)
|
||||
// Clear the canvas and fill it with magenta so it's clear if any spots are missed.
|
||||
d.Engine.Clear(render.Magenta)
|
||||
|
||||
s.canvas.Draw(d.Engine)
|
||||
s.UI.Present(d.Engine)
|
||||
|
||||
// TODO: move inside the UI. Just an approximate position for now.
|
||||
s.drawing.MoveTo(render.NewPoint(0, 19))
|
||||
s.drawing.Resize(render.NewRect(d.width-150, d.height-44))
|
||||
s.drawing.Compute(d.Engine)
|
||||
s.drawing.Present(d.Engine, s.drawing.Point())
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LoadLevel loads a level from disk.
|
||||
func (s *EditorScene) LoadLevel(filename string) error {
|
||||
s.filename = filename
|
||||
s.pixelHistory = []*level.Pixel{}
|
||||
s.canvas = level.Grid{}
|
||||
return s.drawing.LoadFilename(filename)
|
||||
|
||||
m, err := level.LoadJSON(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.Palette = m.Palette
|
||||
if len(s.Palette.Swatches) > 0 {
|
||||
s.Swatch = m.Palette.Swatches[0]
|
||||
}
|
||||
|
||||
for _, pixel := range m.Pixels {
|
||||
s.pixelHistory = append(s.pixelHistory, pixel)
|
||||
s.canvas[pixel] = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SaveLevel saves the level to disk.
|
||||
// TODO: move this into the Canvas?
|
||||
func (s *EditorScene) SaveLevel(filename string) {
|
||||
s.filename = filename
|
||||
|
||||
|
@ -190,9 +121,9 @@ func (s *EditorScene) SaveLevel(filename string) {
|
|||
m.Author = os.Getenv("USER")
|
||||
m.Width = s.width
|
||||
m.Height = s.height
|
||||
m.Palette = s.Palette
|
||||
m.Palette = s.drawing.Palette
|
||||
|
||||
for pixel := range s.canvas {
|
||||
for pixel := range *s.drawing.Grid() {
|
||||
m.Pixels = append(m.Pixels, &level.Pixel{
|
||||
X: pixel.X,
|
||||
Y: pixel.Y,
|
||||
|
@ -213,48 +144,6 @@ func (s *EditorScene) SaveLevel(filename string) {
|
|||
}
|
||||
}
|
||||
|
||||
// Screenshot saves the level canvas to disk as a PNG image.
|
||||
func (s *EditorScene) Screenshot() {
|
||||
screenshot := image.NewRGBA(image.Rect(0, 0, int(s.width), int(s.height)))
|
||||
|
||||
// White-out the image.
|
||||
for x := 0; x < int(s.width); x++ {
|
||||
for y := 0; y < int(s.height); y++ {
|
||||
screenshot.Set(x, y, image.White)
|
||||
}
|
||||
}
|
||||
|
||||
// Fill in the dots we drew.
|
||||
for pixel := range s.canvas {
|
||||
screenshot.Set(int(pixel.X), int(pixel.Y), image.Black)
|
||||
}
|
||||
|
||||
// Create the screenshot directory.
|
||||
if _, err := os.Stat("./screenshots"); os.IsNotExist(err) {
|
||||
log.Info("Creating directory: ./screenshots")
|
||||
err = os.Mkdir("./screenshots", 0755)
|
||||
if err != nil {
|
||||
log.Error("Can't create ./screenshots: %s", err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("./screenshots/screenshot-%s.png",
|
||||
time.Now().Format("2006-01-02T15-04-05"),
|
||||
)
|
||||
fh, err := os.Create(filename)
|
||||
if err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
if err := png.Encode(fh, screenshot); err != nil {
|
||||
log.Error(err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Destroy the scene.
|
||||
func (s *EditorScene) Destroy() error {
|
||||
return nil
|
||||
|
|
11
editor_scene_debug.go
Normal file
11
editor_scene_debug.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package doodle
|
||||
|
||||
import "git.kirsle.net/apps/doodle/level"
|
||||
|
||||
// TODO: build flags to not include this in production builds.
|
||||
// This adds accessors for private variables from the dev console.
|
||||
|
||||
// GetDrawing returns the level.Canvas
|
||||
func (w *EditorScene) GetDrawing() *level.Canvas {
|
||||
return w.drawing
|
||||
}
|
24
editor_ui.go
24
editor_ui.go
|
@ -18,6 +18,7 @@ type EditorUI struct {
|
|||
StatusMouseText string
|
||||
StatusPaletteText string
|
||||
StatusFilenameText string
|
||||
selectedSwatch string // name of selected swatch in palette
|
||||
|
||||
// Widgets
|
||||
Supervisor *ui.Supervisor
|
||||
|
@ -36,6 +37,12 @@ func NewEditorUI(d *Doodle, s *EditorScene) *EditorUI {
|
|||
StatusPaletteText: "Swatch: <none>",
|
||||
StatusFilenameText: "Filename: <none>",
|
||||
}
|
||||
|
||||
// Select the first swatch of the palette.
|
||||
if u.Scene.drawing.Palette.ActiveSwatch != nil {
|
||||
u.selectedSwatch = u.Scene.drawing.Palette.ActiveSwatch.Name
|
||||
}
|
||||
|
||||
u.MenuBar = u.SetupMenuBar(d)
|
||||
u.StatusBar = u.SetupStatusBar(d)
|
||||
u.Palette = u.SetupPalette(d)
|
||||
|
@ -51,7 +58,7 @@ func (u *EditorUI) Loop(ev *events.State) {
|
|||
ev.CursorY.Now,
|
||||
)
|
||||
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
|
||||
u.Scene.Swatch,
|
||||
u.Scene.drawing.Palette.ActiveSwatch,
|
||||
)
|
||||
|
||||
// Statusbar filename label.
|
||||
|
@ -154,7 +161,7 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.Frame {
|
|||
BorderSize: 1,
|
||||
OutlineSize: 0,
|
||||
})
|
||||
w.Handle("MouseUp", btn.Click)
|
||||
w.Handle(ui.MouseUp, btn.Click)
|
||||
u.Supervisor.Add(w)
|
||||
frame.Pack(w, ui.Pack{
|
||||
Anchor: ui.W,
|
||||
|
@ -185,25 +192,26 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
|
|||
|
||||
// Handler function for the radio buttons being clicked.
|
||||
onClick := func(p render.Point) {
|
||||
name := u.Scene.Palette.ActiveSwatch
|
||||
swatch, ok := u.Scene.Palette.Get(name)
|
||||
name := u.selectedSwatch
|
||||
swatch, ok := u.Scene.drawing.Palette.Get(name)
|
||||
if !ok {
|
||||
log.Error("Palette onClick: couldn't get swatch named '%s' from palette", name)
|
||||
return
|
||||
}
|
||||
u.Scene.Swatch = swatch
|
||||
log.Info("Set swatch: %s", swatch)
|
||||
u.Scene.drawing.SetSwatch(swatch)
|
||||
}
|
||||
|
||||
// Draw the radio buttons for the palette.
|
||||
for _, swatch := range u.Scene.Palette.Swatches {
|
||||
for _, swatch := range u.Scene.drawing.Palette.Swatches {
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: swatch.Name,
|
||||
Font: balance.StatusFont,
|
||||
})
|
||||
label.Font.Color = swatch.Color.Darken(40)
|
||||
|
||||
btn := ui.NewRadioButton("palette", &u.Scene.Palette.ActiveSwatch, swatch.Name, label)
|
||||
btn.Handle("MouseUp", onClick)
|
||||
btn := ui.NewRadioButton("palette", &u.selectedSwatch, swatch.Name, label)
|
||||
btn.Handle(ui.Click, onClick)
|
||||
u.Supervisor.Add(btn)
|
||||
|
||||
window.Pack(btn, ui.Pack{
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
// Package events manages mouse and keyboard SDL events for Doodle.
|
||||
package events
|
||||
|
||||
import "strings"
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// State keeps track of event states.
|
||||
type State struct {
|
||||
|
|
|
@ -83,7 +83,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
|||
Text: label,
|
||||
Font: balance.StatusFont,
|
||||
}))
|
||||
btn.Handle("Click", func(p render.Point) {
|
||||
btn.Handle(ui.Click, func(p render.Point) {
|
||||
d.Flash("%s clicked", btn)
|
||||
})
|
||||
s.Supervisor.Add(btn)
|
||||
|
@ -134,7 +134,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
|||
Height: 20,
|
||||
BorderStyle: ui.BorderRaised,
|
||||
})
|
||||
btn.Handle("Click", func(p render.Point) {
|
||||
btn.Handle(ui.Click, func(p render.Point) {
|
||||
d.Flash("%s clicked", btn)
|
||||
})
|
||||
rowFrame.Pack(btn, ui.Pack{
|
||||
|
@ -209,7 +209,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
|||
Font: balance.StatusFont,
|
||||
}))
|
||||
button1.SetBackground(render.Blue)
|
||||
button1.Handle("Click", func(p render.Point) {
|
||||
button1.Handle(ui.Click, func(p render.Point) {
|
||||
d.NewMap()
|
||||
})
|
||||
|
||||
|
@ -219,7 +219,7 @@ func (s *GUITestScene) Setup(d *Doodle) error {
|
|||
Text: "Load Map",
|
||||
Font: balance.StatusFont,
|
||||
}))
|
||||
button2.Handle("Click", func(p render.Point) {
|
||||
button2.Handle(ui.Click, func(p render.Point) {
|
||||
d.Prompt("Map name>", func(name string) {
|
||||
d.EditLevel(name)
|
||||
})
|
||||
|
|
216
level/canvas.go
Normal file
216
level/canvas.go
Normal file
|
@ -0,0 +1,216 @@
|
|||
package level
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/balance"
|
||||
"git.kirsle.net/apps/doodle/events"
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
"git.kirsle.net/apps/doodle/ui"
|
||||
)
|
||||
|
||||
// Canvas is a custom ui.Widget that manages a single drawing.
|
||||
type Canvas struct {
|
||||
ui.Frame
|
||||
Palette *Palette
|
||||
|
||||
// Set to true to allow clicking to edit this canvas.
|
||||
Editable bool
|
||||
|
||||
grid Grid
|
||||
pixelHistory []*Pixel
|
||||
lastPixel *Pixel
|
||||
|
||||
// We inherit the ui.Widget which manages the width and height.
|
||||
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
||||
}
|
||||
|
||||
// NewCanvas initializes a Canvas widget.
|
||||
func NewCanvas(editable bool) *Canvas {
|
||||
w := &Canvas{
|
||||
Editable: editable,
|
||||
Palette: NewPalette(),
|
||||
grid: Grid{},
|
||||
}
|
||||
w.setup()
|
||||
return w
|
||||
}
|
||||
|
||||
// Load initializes the Canvas using an existing Palette and Grid.
|
||||
func (w *Canvas) Load(p *Palette, g *Grid) {
|
||||
w.Palette = p
|
||||
w.grid = *g
|
||||
}
|
||||
|
||||
// LoadFilename initializes the Canvas using a file on disk.
|
||||
func (w *Canvas) LoadFilename(filename string) error {
|
||||
w.grid = Grid{}
|
||||
|
||||
m, err := LoadJSON(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, pixel := range m.Pixels {
|
||||
w.grid[pixel] = nil
|
||||
}
|
||||
w.Palette = m.Palette
|
||||
|
||||
if len(w.Palette.Swatches) > 0 {
|
||||
w.SetSwatch(w.Palette.Swatches[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSwatch changes the currently selected swatch for editing.
|
||||
func (w *Canvas) SetSwatch(s *Swatch) {
|
||||
w.Palette.ActiveSwatch = s
|
||||
}
|
||||
|
||||
// setup common configs between both initializers of the canvas.
|
||||
func (w *Canvas) setup() {
|
||||
w.SetBackground(render.White)
|
||||
w.Handle(ui.MouseOver, func(p render.Point) {
|
||||
w.SetBackground(render.Yellow)
|
||||
})
|
||||
w.Handle(ui.MouseOut, func(p render.Point) {
|
||||
w.SetBackground(render.SkyBlue)
|
||||
})
|
||||
}
|
||||
|
||||
// Loop is called on the scene's event loop to handle mouse interaction with
|
||||
// the canvas, i.e. to edit it.
|
||||
func (w *Canvas) Loop(ev *events.State) error {
|
||||
log.Info("my territory")
|
||||
var (
|
||||
P = w.Point()
|
||||
_ = P
|
||||
)
|
||||
|
||||
// Arrow keys to scroll the view.
|
||||
scrollBy := render.Point{}
|
||||
if ev.Right.Now {
|
||||
scrollBy.X += balance.CanvasScrollSpeed
|
||||
} else if ev.Left.Now {
|
||||
scrollBy.X -= balance.CanvasScrollSpeed
|
||||
}
|
||||
if ev.Down.Now {
|
||||
scrollBy.Y += balance.CanvasScrollSpeed
|
||||
} else if ev.Up.Now {
|
||||
scrollBy.Y -= balance.CanvasScrollSpeed
|
||||
}
|
||||
if !scrollBy.IsZero() {
|
||||
w.ScrollBy(scrollBy)
|
||||
}
|
||||
|
||||
// Only care if the cursor is over our space.
|
||||
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
|
||||
if !cursor.Inside(w.Rect()) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If no swatch is active, do nothing with mouse clicks.
|
||||
if w.Palette.ActiveSwatch == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Clicking? Log all the pixels while doing so.
|
||||
if ev.Button1.Now {
|
||||
// log.Warn("Button1: %+v", ev.Button1)
|
||||
lastPixel := w.lastPixel
|
||||
pixel := &Pixel{
|
||||
X: ev.CursorX.Now - P.X + w.Scroll.X,
|
||||
Y: ev.CursorY.Now - P.Y + w.Scroll.Y,
|
||||
Palette: w.Palette,
|
||||
Swatch: w.Palette.ActiveSwatch,
|
||||
}
|
||||
|
||||
// Append unique new pixels.
|
||||
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
|
||||
if lastPixel != nil {
|
||||
// Draw the pixels in between.
|
||||
if lastPixel != pixel {
|
||||
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||
dot := &Pixel{
|
||||
X: point.X,
|
||||
Y: point.Y,
|
||||
Palette: lastPixel.Palette,
|
||||
Swatch: lastPixel.Swatch,
|
||||
}
|
||||
w.grid[dot] = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.lastPixel = pixel
|
||||
w.pixelHistory = append(w.pixelHistory, pixel)
|
||||
|
||||
// Save in the pixel canvas map.
|
||||
w.grid[pixel] = nil
|
||||
}
|
||||
} else {
|
||||
w.lastPixel = nil
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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.
|
||||
func (w *Canvas) Viewport() render.Rect {
|
||||
var S = w.Size()
|
||||
return render.Rect{
|
||||
X: w.Scroll.X,
|
||||
Y: w.Scroll.Y,
|
||||
W: S.W - w.BoxThickness(2),
|
||||
H: S.H - w.BoxThickness(2),
|
||||
}
|
||||
}
|
||||
|
||||
// Grid returns the underlying grid object.
|
||||
func (w *Canvas) Grid() *Grid {
|
||||
return &w.grid
|
||||
}
|
||||
|
||||
// ScrollBy adjusts the viewport scroll position.
|
||||
func (w *Canvas) ScrollBy(by render.Point) {
|
||||
w.Scroll.Add(by)
|
||||
}
|
||||
|
||||
// Compute the canvas.
|
||||
func (w *Canvas) Compute(e render.Engine) {
|
||||
|
||||
}
|
||||
|
||||
// Present the canvas.
|
||||
func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||
var (
|
||||
S = w.Size()
|
||||
Viewport = w.Viewport()
|
||||
)
|
||||
w.MoveTo(p)
|
||||
w.DrawBox(e, p)
|
||||
e.DrawBox(w.Background(), render.Rect{
|
||||
X: p.X + w.BoxThickness(1),
|
||||
Y: p.Y + w.BoxThickness(1),
|
||||
W: S.W - w.BoxThickness(2),
|
||||
H: S.H - w.BoxThickness(2),
|
||||
})
|
||||
|
||||
for pixel := range w.grid {
|
||||
point := render.NewPoint(pixel.X, pixel.Y)
|
||||
if point.Inside(Viewport) {
|
||||
// This pixel is visible in the canvas, but offset it by the
|
||||
// scroll height.
|
||||
point.Add(render.Point{
|
||||
X: -Viewport.X,
|
||||
Y: -Viewport.Y,
|
||||
})
|
||||
color := pixel.Swatch.Color
|
||||
e.DrawPoint(color, render.Point{
|
||||
X: p.X + w.BoxThickness(1) + point.X,
|
||||
Y: p.Y + w.BoxThickness(1) + point.Y,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
9
level/log.go
Normal file
9
level/log.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package level
|
||||
|
||||
import "github.com/kirsle/golog"
|
||||
|
||||
var log *golog.Logger
|
||||
|
||||
func init() {
|
||||
log = golog.GetLogger("doodle")
|
||||
}
|
|
@ -33,12 +33,20 @@ func DefaultPalette() *Palette {
|
|||
}
|
||||
}
|
||||
|
||||
// NewPalette initializes a blank palette.
|
||||
func NewPalette() *Palette {
|
||||
return &Palette{
|
||||
Swatches: []*Swatch{},
|
||||
byName: map[string]int{},
|
||||
}
|
||||
}
|
||||
|
||||
// Palette holds an index of colors used in a drawing.
|
||||
type Palette struct {
|
||||
Swatches []*Swatch `json:"swatches"`
|
||||
|
||||
// Private runtime values
|
||||
ActiveSwatch string `json:"-"` // name of the actively selected color
|
||||
ActiveSwatch *Swatch `json:"-"` // name of the actively selected color
|
||||
byName map[string]int // Cache map of swatches by name
|
||||
}
|
||||
|
||||
|
|
|
@ -29,7 +29,7 @@ func (s *MainScene) Setup(d *Doodle) error {
|
|||
Text: "New Map",
|
||||
Font: balance.StatusFont,
|
||||
}))
|
||||
button1.Handle("Click", func(p render.Point) {
|
||||
button1.Handle(ui.Click, func(p render.Point) {
|
||||
d.NewMap()
|
||||
})
|
||||
|
||||
|
|
|
@ -11,10 +11,10 @@ import (
|
|||
type PlayScene struct {
|
||||
// Configuration attributes.
|
||||
Filename string
|
||||
Canvas level.Grid
|
||||
Canvas *level.Grid
|
||||
|
||||
// Private variables.
|
||||
canvas level.Grid
|
||||
canvas *level.Grid
|
||||
|
||||
// Canvas size
|
||||
width int32
|
||||
|
@ -46,7 +46,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
|
||||
if s.canvas == nil {
|
||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
||||
s.canvas = level.Grid{}
|
||||
s.canvas = &level.Grid{}
|
||||
}
|
||||
|
||||
s.width = d.width // TODO: canvas width = copy the window size
|
||||
|
@ -110,7 +110,7 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
|||
// Apply gravity.
|
||||
// var onFloor bool
|
||||
|
||||
info, ok := doodads.CollidesWithGrid(s.Player, &s.canvas, delta)
|
||||
info, ok := doodads.CollidesWithGrid(s.Player, s.canvas, delta)
|
||||
if ok {
|
||||
// Collision happened with world.
|
||||
}
|
||||
|
@ -128,16 +128,16 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
|||
|
||||
// LoadLevel loads a level from disk.
|
||||
func (s *PlayScene) LoadLevel(filename string) error {
|
||||
s.canvas = level.Grid{}
|
||||
s.canvas = &level.Grid{}
|
||||
|
||||
m, err := level.LoadJSON(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// m, err := level.LoadJSON(filename)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
|
||||
for _, pixel := range m.Pixels {
|
||||
s.canvas[pixel] = nil
|
||||
}
|
||||
// for _, pixel := range m.Pixels {
|
||||
// // *s.canvas[pixel] = nil
|
||||
// }
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -56,6 +56,28 @@ func (p Point) String() string {
|
|||
return fmt.Sprintf("Point<%d,%d>", p.X, p.Y)
|
||||
}
|
||||
|
||||
// IsZero returns if the point is the zero value.
|
||||
func (p Point) IsZero() bool {
|
||||
return p.X == 0 && p.Y == 0
|
||||
}
|
||||
|
||||
// Inside returns whether the Point falls inside the rect.
|
||||
func (p Point) Inside(r Rect) bool {
|
||||
var (
|
||||
x1 = r.X
|
||||
y1 = r.Y
|
||||
x2 = r.X + r.W
|
||||
y2 = r.Y + r.H
|
||||
)
|
||||
return p.X >= x1 && p.X <= x2 && p.Y >= y1 && p.Y <= y2
|
||||
}
|
||||
|
||||
// Add (or subtract) the other point to your current point.
|
||||
func (p *Point) Add(other Point) {
|
||||
p.X += other.X
|
||||
p.Y += other.Y
|
||||
}
|
||||
|
||||
// Rect has a coordinate and a width and height.
|
||||
type Rect struct {
|
||||
X int32
|
||||
|
@ -79,6 +101,14 @@ func (r Rect) String() string {
|
|||
)
|
||||
}
|
||||
|
||||
// Point returns the rectangle's X,Y values as a Point.
|
||||
func (r Rect) Point() Point {
|
||||
return Point{
|
||||
X: r.X,
|
||||
Y: r.Y,
|
||||
}
|
||||
}
|
||||
|
||||
// Bigger returns if the given rect is larger than the current one.
|
||||
func (r Rect) Bigger(other Rect) bool {
|
||||
// TODO: don't know why this is !
|
||||
|
|
50
render/point_test.go
Normal file
50
render/point_test.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package render_test
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
func TestPointInside(t *testing.T) {
|
||||
var p = render.Point{
|
||||
X: 128,
|
||||
Y: 256,
|
||||
}
|
||||
|
||||
type testCase struct {
|
||||
rect render.Rect
|
||||
shouldPass bool
|
||||
}
|
||||
tests := []testCase{
|
||||
testCase{
|
||||
rect: render.Rect{
|
||||
X: 0,
|
||||
Y: 0,
|
||||
W: 500,
|
||||
H: 500,
|
||||
},
|
||||
shouldPass: true,
|
||||
},
|
||||
testCase{
|
||||
rect: render.Rect{
|
||||
X: 100,
|
||||
Y: 80,
|
||||
W: 40,
|
||||
H: 60,
|
||||
},
|
||||
shouldPass: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
if p.Inside(test.rect) != test.shouldPass {
|
||||
t.Errorf("Failed: %s inside %s should %s",
|
||||
p,
|
||||
test.rect,
|
||||
strconv.FormatBool(test.shouldPass),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,7 +9,7 @@ import (
|
|||
// Clear the canvas and set this color.
|
||||
func (r *Renderer) Clear(color render.Color) {
|
||||
if color != r.lastColor {
|
||||
r.renderer.SetDrawColor(color.Red, color.Blue, color.Green, color.Alpha)
|
||||
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||
}
|
||||
r.renderer.Clear()
|
||||
}
|
||||
|
|
|
@ -35,20 +35,20 @@ func NewButton(name string, child Widget) *Button {
|
|||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
|
||||
w.Handle("MouseOver", func(p render.Point) {
|
||||
w.Handle(MouseOver, func(p render.Point) {
|
||||
w.hovering = true
|
||||
w.SetBackground(theme.ButtonHoverColor)
|
||||
})
|
||||
w.Handle("MouseOut", func(p render.Point) {
|
||||
w.Handle(MouseOut, func(p render.Point) {
|
||||
w.hovering = false
|
||||
w.SetBackground(theme.ButtonBackgroundColor)
|
||||
})
|
||||
|
||||
w.Handle("MouseDown", func(p render.Point) {
|
||||
w.Handle(MouseDown, func(p render.Point) {
|
||||
w.clicked = true
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
})
|
||||
w.Handle("MouseUp", func(p render.Point) {
|
||||
w.Handle(MouseUp, func(p render.Point) {
|
||||
w.clicked = false
|
||||
w.SetBorderStyle(BorderRaised)
|
||||
})
|
||||
|
|
|
@ -78,24 +78,24 @@ func (w *CheckButton) setup() {
|
|||
Background: theme.ButtonBackgroundColor,
|
||||
})
|
||||
|
||||
w.Handle("MouseOver", func(p render.Point) {
|
||||
w.Handle(MouseOver, func(p render.Point) {
|
||||
w.hovering = true
|
||||
w.SetBackground(theme.ButtonHoverColor)
|
||||
})
|
||||
w.Handle("MouseOut", func(p render.Point) {
|
||||
w.Handle(MouseOut, func(p render.Point) {
|
||||
w.hovering = false
|
||||
w.SetBackground(theme.ButtonBackgroundColor)
|
||||
})
|
||||
|
||||
w.Handle("MouseDown", func(p render.Point) {
|
||||
w.Handle(MouseDown, func(p render.Point) {
|
||||
w.clicked = true
|
||||
w.SetBorderStyle(BorderSunken)
|
||||
})
|
||||
w.Handle("MouseUp", func(p render.Point) {
|
||||
w.Handle(MouseUp, func(p render.Point) {
|
||||
w.clicked = false
|
||||
})
|
||||
|
||||
w.Handle("MouseDown", func(p render.Point) {
|
||||
w.Handle(Click, func(p render.Point) {
|
||||
var sunken bool
|
||||
if w.BoolVar != nil {
|
||||
if *w.BoolVar {
|
||||
|
|
|
@ -35,8 +35,8 @@ func makeCheckbox(name string, boolVar *bool, stringVar *string, value string, c
|
|||
w.Frame.Setup()
|
||||
|
||||
// Forward clicks on the child widget to the CheckButton.
|
||||
for _, e := range []string{"MouseOver", "MouseOut", "MouseUp", "MouseDown"} {
|
||||
func(e string) {
|
||||
for _, e := range []Event{MouseOver, MouseOut, MouseUp, MouseDown} {
|
||||
func(e Event) {
|
||||
w.child.Handle(e, func(p render.Point) {
|
||||
w.button.Event(e, p)
|
||||
})
|
||||
|
|
|
@ -7,6 +7,22 @@ import (
|
|||
"git.kirsle.net/apps/doodle/render"
|
||||
)
|
||||
|
||||
// Event is a named event that the supervisor will send.
|
||||
type Event int
|
||||
|
||||
// Events.
|
||||
const (
|
||||
NullEvent Event = iota
|
||||
MouseOver
|
||||
MouseOut
|
||||
MouseDown
|
||||
MouseUp
|
||||
Click
|
||||
KeyDown
|
||||
KeyUp
|
||||
KeyPress
|
||||
)
|
||||
|
||||
// Supervisor keeps track of widgets of interest to notify them about
|
||||
// interaction events such as mouse hovers and clicks in their general
|
||||
// vicinity.
|
||||
|
@ -49,30 +65,30 @@ func (s *Supervisor) Loop(ev *events.State) {
|
|||
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
|
||||
// Cursor has intersected the widget.
|
||||
if _, ok := s.hovering[id]; !ok {
|
||||
w.Event("MouseOver", XY)
|
||||
w.Event(MouseOver, XY)
|
||||
s.hovering[id] = nil
|
||||
}
|
||||
|
||||
_, isClicked := s.clicked[id]
|
||||
if ev.Button1.Now {
|
||||
if !isClicked {
|
||||
w.Event("MouseDown", XY)
|
||||
w.Event(MouseDown, XY)
|
||||
s.clicked[id] = nil
|
||||
}
|
||||
} else if isClicked {
|
||||
w.Event("MouseUp", XY)
|
||||
w.Event("Click", XY)
|
||||
w.Event(MouseUp, XY)
|
||||
w.Event(Click, XY)
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
} else {
|
||||
// Cursor is not intersecting the widget.
|
||||
if _, ok := s.hovering[id]; ok {
|
||||
w.Event("MouseOut", XY)
|
||||
w.Event(MouseOut, XY)
|
||||
delete(s.hovering, id)
|
||||
}
|
||||
|
||||
if _, ok := s.clicked[id]; ok {
|
||||
w.Event("MouseUp", XY)
|
||||
w.Event(MouseUp, XY)
|
||||
delete(s.clicked, id)
|
||||
}
|
||||
}
|
||||
|
|
31
ui/widget.go
31
ui/widget.go
|
@ -29,9 +29,10 @@ type Widget interface {
|
|||
BoxSize() render.Rect // Return the full size including the border and outline.
|
||||
Resize(render.Rect)
|
||||
ResizeBy(render.Rect)
|
||||
Rect() render.Rect // Return the full absolute rect combining the Size() and Point()
|
||||
|
||||
Handle(string, func(render.Point))
|
||||
Event(string, render.Point) // called internally to trigger an event
|
||||
Handle(Event, func(render.Point))
|
||||
Event(Event, render.Point) // called internally to trigger an event
|
||||
|
||||
// Thickness of the padding + border + outline.
|
||||
BoxThickness(multiplier int32) int32
|
||||
|
@ -103,7 +104,7 @@ type BaseWidget struct {
|
|||
borderSize int32
|
||||
outlineColor render.Color
|
||||
outlineSize int32
|
||||
handlers map[string][]func(render.Point)
|
||||
handlers map[Event][]func(render.Point)
|
||||
}
|
||||
|
||||
// SetID sets a string name for your widget, helpful for debugging purposes.
|
||||
|
@ -170,6 +171,16 @@ func (w *BaseWidget) Configure(c Config) {
|
|||
}
|
||||
}
|
||||
|
||||
// Rect returns the widget's absolute rectangle, the combined Size and Point.
|
||||
func (w *BaseWidget) Rect() render.Rect {
|
||||
return render.Rect{
|
||||
X: w.point.X,
|
||||
Y: w.point.Y,
|
||||
W: w.width,
|
||||
H: w.height,
|
||||
}
|
||||
}
|
||||
|
||||
// Point returns the X,Y position of the widget on the window.
|
||||
func (w *BaseWidget) Point() render.Point {
|
||||
return w.point
|
||||
|
@ -395,8 +406,8 @@ func (w *BaseWidget) SetOutlineSize(v int32) {
|
|||
}
|
||||
|
||||
// Event is called internally by Doodle to trigger an event.
|
||||
func (w *BaseWidget) Event(name string, p render.Point) {
|
||||
if handlers, ok := w.handlers[name]; ok {
|
||||
func (w *BaseWidget) Event(event Event, p render.Point) {
|
||||
if handlers, ok := w.handlers[event]; ok {
|
||||
for _, fn := range handlers {
|
||||
fn(p)
|
||||
}
|
||||
|
@ -404,16 +415,16 @@ func (w *BaseWidget) Event(name string, p render.Point) {
|
|||
}
|
||||
|
||||
// Handle an event in the widget.
|
||||
func (w *BaseWidget) Handle(name string, fn func(render.Point)) {
|
||||
func (w *BaseWidget) Handle(event Event, fn func(render.Point)) {
|
||||
if w.handlers == nil {
|
||||
w.handlers = map[string][]func(render.Point){}
|
||||
w.handlers = map[Event][]func(render.Point){}
|
||||
}
|
||||
|
||||
if _, ok := w.handlers[name]; !ok {
|
||||
w.handlers[name] = []func(render.Point){}
|
||||
if _, ok := w.handlers[event]; !ok {
|
||||
w.handlers[event] = []func(render.Point){}
|
||||
}
|
||||
|
||||
w.handlers[name] = append(w.handlers[name], fn)
|
||||
w.handlers[event] = append(w.handlers[event], fn)
|
||||
}
|
||||
|
||||
// OnMouseOut should be overridden on widgets who want this event.
|
||||
|
|
Loading…
Reference in New Issue
Block a user