Introduce Drawing Tools Concept, Pencil and Actor

The uix.Canvas widget now maintains a selected Tool which configures how
the mouse interacts with the (editable) Canvas widget.

The default Tool is the PencilTool and implements the old behavior: it
draws pixels when clicked and dragged based on your currently selected
Color Swatch. This tool automatically becomes active when you toggle the
Palette tab in the editor mode.

A new Tool is the ActorTool which becomes active when you select the
Doodads tab. In the ActorTool you can't draw pixels on the level, but
when you mouse over a Doodad instance (Actor) in your level, you may
pick it up and drag it someplace else.

Left-click an Actor to pick it up and drag it somewhere else.
Right-click to delete it completely.

You can also delete an Actor by dragging it OFF of the Canvas, like back
onto the palette drawer or onto the menu bar.
pull/1/head
Noah 2018-10-20 17:08:20 -07:00
parent 0044b72943
commit b4a366baa9
8 changed files with 216 additions and 88 deletions

View File

@ -6,9 +6,11 @@ import (
"strconv"
"git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/doodads"
"git.kirsle.net/apps/doodle/enum"
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
"git.kirsle.net/apps/doodle/uix"
@ -167,8 +169,8 @@ func (u *EditorUI) Loop(ev *events.State) error {
ev.CursorY.Now,
debugWorldIndex,
)
u.StatusPaletteText = fmt.Sprintf("Swatch: %s",
u.Canvas.Palette.ActiveSwatch,
u.StatusPaletteText = fmt.Sprintf("%s Tool",
u.Canvas.Tool,
)
u.StatusScrollText = fmt.Sprintf("Scroll: %s Viewport: %s",
u.Canvas.Scroll,
@ -245,6 +247,27 @@ func (u *EditorUI) SetupCanvas(d *Doodle) *uix.Canvas {
drawing.SetSwatch(drawing.Palette.Swatches[0])
}
// Handle the Canvas deleting our actors in edit mode.
drawing.OnDeleteActors = func(actors []*level.Actor) {
if u.Scene.Level != nil {
for _, actor := range actors {
u.Scene.Level.Actors.Remove(actor)
}
drawing.InstallActors(u.Scene.Level.Actors)
}
}
// A drag event initiated inside the Canvas. This happens in the ActorTool
// mode when you click an existing Doodad and it "pops" out of the canvas
// and onto the cursor to be repositioned.
drawing.OnDragStart = func(filename string) {
doodad, err := doodads.LoadJSON(userdir.DoodadPath(filename))
if err != nil {
log.Error("drawing.OnDragStart: %s", err.Error())
}
u.startDragActor(doodad)
}
// Set up the drop handler for draggable doodads.
// NOTE: The drag event begins at editor_ui_doodad.go when configuring the
// Doodad Palette buttons.
@ -415,10 +438,6 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
// Frame that holds the tab buttons in Level Edit mode.
tabFrame := ui.NewFrame("Palette Tabs")
if u.Scene.DrawingType != enum.LevelDrawing {
// Don't show the tab bar except in Level Edit mode.
tabFrame.Hide()
}
for _, name := range []string{"Palette", "Doodads"} {
if u.paletteTab == "" {
u.paletteTab = name
@ -429,9 +448,11 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
}))
tab.Handle(ui.Click, func(p render.Point) {
if u.paletteTab == "Palette" {
u.Canvas.Tool = uix.PencilTool
u.PaletteTab.Show()
u.DoodadTab.Hide()
} else {
u.Canvas.Tool = uix.ActorTool
u.PaletteTab.Hide()
u.DoodadTab.Show()
}
@ -450,6 +471,11 @@ func (u *EditorUI) SetupPalette(d *Doodle) *ui.Window {
PadY: 4,
})
// Only show the tab frame in Level drawing mode!
if u.Scene.DrawingType != enum.LevelDrawing {
tabFrame.Hide()
}
// Doodad frame.
{
frame, err := u.setupDoodadFrame(d.Engine, window)

View File

@ -21,6 +21,22 @@ type DraggableActor struct {
doodad *doodads.Doodad
}
// startDragActor begins the drag event for a Doodad onto a level.
func (u *EditorUI) startDragActor(doodad *doodads.Doodad) {
u.Supervisor.DragStart()
// Create the canvas to render on the mouse cursor.
drawing := uix.NewCanvas(doodad.Layers[0].Chunker.Size, false)
drawing.LoadDoodad(doodad)
drawing.Resize(doodad.Rect())
drawing.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO: invisible becomes white
drawing.MaskColor = balance.DragColor // blueprint effect
u.DraggableActor = &DraggableActor{
canvas: drawing,
doodad: doodad,
}
}
// setupDoodadFrame configures the Doodad Palette tab for Edit Mode.
// This is a subroutine of editor_ui.go#SetupPalette()
//
@ -84,18 +100,7 @@ func (u *EditorUI) setupDoodadFrame(e render.Engine, window *ui.Window) (*ui.Fra
// NOTE: The drag target is the EditorUI.Canvas in
// editor_ui.go#SetupCanvas()
btn.Handle(ui.MouseDown, func(e render.Point) {
u.Supervisor.DragStart()
// Create the canvas to render on the mouse cursor.
drawing := uix.NewCanvas(doodad.Layers[0].Chunker.Size, false)
drawing.LoadDoodad(doodad)
drawing.Resize(doodad.Rect())
drawing.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO: invisible becomes white
drawing.MaskColor = balance.DragColor // blueprint effect
u.DraggableActor = &DraggableActor{
canvas: drawing,
doodad: doodad,
}
u.startDragActor(doodad)
})
u.Supervisor.Add(btn)

View File

@ -176,16 +176,6 @@ func (s *GUITestScene) Setup(d *Doodle) error {
})
cb.Supervise(s.Supervisor)
// Put an image in.
img, err := ui.OpenImage(d.Engine, "exit.bmp")
if err != nil {
log.Error(err.Error())
}
frame.Pack(img, ui.Pack{
Anchor: ui.NE,
Padding: 4,
})
frame.Pack(ui.NewLabel(ui.Label{
Text: "Like Tk!",
Font: render.Text{

View File

@ -24,6 +24,16 @@ func (m ActorMap) Add(a *Actor) {
m[a.id] = a
}
// Remove an Actor from the map. The ID must be set at the very least, so to
// remove by ID just create an Actor{id: x}
func (m ActorMap) Remove(a *Actor) bool {
if _, ok := m[a.id]; ok {
delete(m, a.id)
return true
}
return false
}
// Actor is an instance of a Doodad in the level.
type Actor struct {
id string // NOTE: read only, use ID() to access.

View File

@ -49,23 +49,27 @@ func (r *Renderer) Poll() (*events.State, error) {
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"
}
checkDown := func(number uint8, target *events.BoolTick) bool {
if t.Button == number {
var eventName string
if t.State == 1 && target.Now == false {
eventName = "DOWN"
} else if t.State == 0 && target.Now == true {
eventName = "UP"
}
if eventName != "" {
s.Button1.Push(eventName == "DOWN")
// Return the event immediately.
return s, nil
if eventName != "" {
target.Push(eventName == "DOWN")
}
return true
}
return false
}
// s.Button2.Push(t.Button == 3 && t.State == 1)
if checkDown(1, s.Button1) || checkDown(3, s.Button2) {
// Return the event immediately.
return s, nil
}
case *sdl.MouseWheelEvent:
if DebugMouseEvents {
log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d",

View File

@ -23,6 +23,9 @@ type Canvas struct {
Editable bool // Clicking will edit pixels of this canvas.
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
// Selected draw tool/mode, default Pencil, for editable canvases.
Tool Tool
// MaskColor will force every pixel to render as this color regardless of
// the palette index of that pixel. Otherwise pixels behave the same and
// the palette does work as normal. Set to render.Invisible (zero value)
@ -36,6 +39,12 @@ type Canvas struct {
actor *level.Actor // if this canvas IS an actor
actors []*Actor
// When the Canvas wants to delete Actors, but ultimately it is upstream
// that controls the actors. Upstream should delete them and then reinstall
// the actor list from scratch.
OnDeleteActors func([]*level.Actor)
OnDragStart func(filename string)
// Tracking pixels while editing. TODO: get rid of pixelHistory?
pixelHistory []*level.Pixel
lastPixel *level.Pixel
@ -151,10 +160,6 @@ func (w *Canvas) setup() {
// 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 {
// Get the absolute position of the canvas on screen to accurately match
// it up to mouse clicks.
var P = ui.AbsolutePosition(w)
if w.Scrollable {
// Arrow keys to scroll the view.
scrollBy := render.Point{}
@ -173,51 +178,13 @@ func (w *Canvas) Loop(ev *events.State) error {
}
}
// Only care if the cursor is over our space.
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
if !cursor.Inside(ui.AbsoluteRect(w)) {
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 {
lastPixel := w.lastPixel
cursor := render.Point{
X: ev.CursorX.Now - P.X - w.Scroll.X,
Y: ev.CursorY.Now - P.Y - w.Scroll.Y,
// If the canvas is editable, only care if it's over our space.
if w.Editable {
cursor := render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
if cursor.Inside(ui.AbsoluteRect(w)) {
return w.loopEditable(ev)
}
pixel := &level.Pixel{
X: cursor.X,
Y: cursor.Y,
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) {
w.chunks.Set(point, lastPixel.Swatch)
}
}
}
w.lastPixel = pixel
w.pixelHistory = append(w.pixelHistory, pixel)
// Save in the pixel canvas map.
w.chunks.Set(cursor, pixel.Swatch)
}
} else {
w.lastPixel = nil
}
return nil
}

107
uix/canvas_editable.go Normal file
View File

@ -0,0 +1,107 @@
package uix
import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui"
)
// loopEditable handles the Loop() part for editable canvases.
func (w *Canvas) loopEditable(ev *events.State) error {
// Get the absolute position of the canvas on screen to accurately match
// it up to mouse clicks.
var (
P = ui.AbsolutePosition(w)
cursor = render.Point{
X: ev.CursorX.Now - P.X - w.Scroll.X,
Y: ev.CursorY.Now - P.Y - w.Scroll.Y,
}
)
switch w.Tool {
case PencilTool:
// 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 {
lastPixel := w.lastPixel
pixel := &level.Pixel{
X: cursor.X,
Y: cursor.Y,
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) {
w.chunks.Set(point, lastPixel.Swatch)
}
}
}
w.lastPixel = pixel
w.pixelHistory = append(w.pixelHistory, pixel)
// Save in the pixel canvas map.
w.chunks.Set(cursor, pixel.Swatch)
}
} else {
w.lastPixel = nil
}
case ActorTool:
// See if any of the actors are below the mouse cursor.
var WP = w.WorldIndexAt(cursor)
_ = WP
var deleteActors = []*level.Actor{}
for _, actor := range w.actors {
box := render.Rect{
X: actor.Actor.Point.X - P.X - w.Scroll.X,
Y: actor.Actor.Point.Y - P.Y - w.Scroll.Y,
W: actor.Canvas.Size().W,
H: actor.Canvas.Size().H,
}
if WP.Inside(box) {
actor.Canvas.Configure(ui.Config{
BorderSize: 1,
BorderColor: render.RGBA(255, 153, 0, 255),
BorderStyle: ui.BorderSolid,
Background: render.White, // TODO: cuz the border draws a bgcolor
})
// Check for a mouse down event to begin dragging this
// canvas around.
if ev.Button1.Read() {
// Pop this canvas out for the drag/drop.
if w.OnDragStart != nil {
deleteActors = append(deleteActors, actor.Actor)
w.OnDragStart(actor.Actor.Filename)
}
break
} else if ev.Button2.Read() {
// Right click to delete an actor.
deleteActors = append(deleteActors, actor.Actor)
}
} else {
actor.Canvas.SetBorderSize(0)
actor.Canvas.SetBackground(render.RGBA(0, 0, 1, 0)) // TODO
}
}
// Change in actor count?
if len(deleteActors) > 0 && w.OnDeleteActors != nil {
w.OnDeleteActors(deleteActors)
}
}
return nil
}

19
uix/draw_modes.go Normal file
View File

@ -0,0 +1,19 @@
package uix
// Tool is a draw mode for an editable Canvas.
type Tool int
// Draw modes for editable Canvas.
const (
PencilTool Tool = iota // draw pixels where the mouse clicks
ActorTool // drag and move actors
)
var toolNames = []string{
"Pencil",
"Doodad", // readable name for ActorTool
}
func (t Tool) String() string {
return toolNames[t]
}