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.
This commit is contained in:
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 {
checkDown := func(number uint8, target *events.BoolTick) bool {
if t.Button == number {
var eventName string
if t.State == 1 && s.Button1.Now == false {
if t.State == 1 && target.Now == false {
eventName = "DOWN"
} else if t.State == 0 && s.Button1.Now == true {
} else if t.State == 0 && target.Now == true {
eventName = "UP"
}
if eventName != "" {
s.Button1.Push(eventName == "DOWN")
target.Push(eventName == "DOWN")
}
return true
}
return false
}
if checkDown(1, s.Button1) || checkDown(3, s.Button2) {
// 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",

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.
// 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 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,
}
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)
if cursor.Inside(ui.AbsoluteRect(w)) {
return w.loopEditable(ev)
}
}
}
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]
}