Eraser Tool, Brush Sizes
* Implement Brush Sizes for drawtool.Stroke and add a UI to the tools panel to control the brush size. * Brush sizes: 1, 2, 4, 8, 16, 24, 32, 48, 64 * Add the Eraser Tool to editor mode. It uses a default brush size of 16 and a max size of 32 due to some performance issues. * The Undo/Redo system now remembers the original color of pixels when you change them, so that Undo will set them back how they were instead of deleting the pixel entirely. Due to performance issues, this only happens when your Brush Size is 0 (drawing single-pixel shapes). * UI: Add an IntVariable option to ui.Label to bind showing the value of an int reference. Aforementioned performance issues: * When we try to remember whole rects of pixels for drawing thick shapes, it requires a ton of scanning for each step of the shape. Even de-duplicating pixel checks, tons of extra reads are constantly checked. * The Eraser is the only tool that absolutely needs to be able to remember wiped pixels AND have large brush sizes. The performance sucks and lags a bit if you erase a lot all at once, but it's a trade-off for now. * So pixels aren't remembered when drawing lines in your level with thick brushes, so the Undo action will simply delete your pixels and not reset them. Only the Eraser can bring back pixels.
This commit is contained in:
parent
7317615318
commit
cc1e441232
BIN
assets/sprites/eraser-tool.png
Normal file
BIN
assets/sprites/eraser-tool.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 709 B |
|
@ -254,6 +254,7 @@ func IterRect(p1, p2 Point) chan Point {
|
||||||
X: TopLeft.X,
|
X: TopLeft.X,
|
||||||
Y: BottomRight.Y,
|
Y: BottomRight.Y,
|
||||||
}
|
}
|
||||||
|
dedupe = map[Point]interface{}{}
|
||||||
)
|
)
|
||||||
|
|
||||||
// Trace all four edges and yield it.
|
// Trace all four edges and yield it.
|
||||||
|
@ -268,7 +269,10 @@ func IterRect(p1, p2 Point) chan Point {
|
||||||
}
|
}
|
||||||
for _, edge := range edges {
|
for _, edge := range edges {
|
||||||
for pt := range IterLine2(edge.A, edge.B) {
|
for pt := range IterLine2(edge.A, edge.B) {
|
||||||
generator <- pt
|
if _, ok := dedupe[pt]; !ok {
|
||||||
|
generator <- pt
|
||||||
|
dedupe[pt] = nil
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,7 @@ type Label struct {
|
||||||
// Configurable fields for the constructor.
|
// Configurable fields for the constructor.
|
||||||
Text string
|
Text string
|
||||||
TextVariable *string
|
TextVariable *string
|
||||||
|
IntVariable *int
|
||||||
Font render.Text
|
Font render.Text
|
||||||
|
|
||||||
width int32
|
width int32
|
||||||
|
@ -32,6 +33,7 @@ func NewLabel(c Label) *Label {
|
||||||
w := &Label{
|
w := &Label{
|
||||||
Text: c.Text,
|
Text: c.Text,
|
||||||
TextVariable: c.TextVariable,
|
TextVariable: c.TextVariable,
|
||||||
|
IntVariable: c.IntVariable,
|
||||||
Font: DefaultFont,
|
Font: DefaultFont,
|
||||||
}
|
}
|
||||||
if !c.Font.IsZero() {
|
if !c.Font.IsZero() {
|
||||||
|
@ -49,6 +51,9 @@ func (w *Label) text() render.Text {
|
||||||
if w.TextVariable != nil {
|
if w.TextVariable != nil {
|
||||||
w.Font.Text = *w.TextVariable
|
w.Font.Text = *w.TextVariable
|
||||||
return w.Font
|
return w.Font
|
||||||
|
} else if w.IntVariable != nil {
|
||||||
|
w.Font.Text = fmt.Sprintf("%d", *w.IntVariable)
|
||||||
|
return w.Font
|
||||||
}
|
}
|
||||||
w.Font.Text = w.Text
|
w.Font.Text = w.Text
|
||||||
return w.Font
|
return w.Font
|
||||||
|
|
|
@ -173,7 +173,7 @@ func (s *Supervisor) Hovering(cursor render.Point) (hovering, outside []WidgetSl
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if XY.X >= P.X && XY.X <= P2.X && XY.Y >= P.Y && XY.Y <= P2.Y {
|
if XY.X >= P.X && XY.X < P2.X && XY.Y >= P.Y && XY.Y < P2.Y {
|
||||||
// Cursor intersects the widget.
|
// Cursor intersects the widget.
|
||||||
hovering = append(hovering, child)
|
hovering = append(hovering, child)
|
||||||
} else {
|
} else {
|
||||||
|
|
|
@ -27,6 +27,22 @@ var (
|
||||||
|
|
||||||
// Size of Undo/Redo history for map editor.
|
// Size of Undo/Redo history for map editor.
|
||||||
UndoHistory = 20
|
UndoHistory = 20
|
||||||
|
|
||||||
|
// Options for brush size.
|
||||||
|
BrushSizeOptions = []int{
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
4,
|
||||||
|
8,
|
||||||
|
16,
|
||||||
|
24,
|
||||||
|
32,
|
||||||
|
48,
|
||||||
|
64,
|
||||||
|
}
|
||||||
|
DefaultEraserBrushSize = 8
|
||||||
|
MaxEraserBrushSize = 32 // the bigger, the slower
|
||||||
)
|
)
|
||||||
|
|
||||||
// Edit Mode Values
|
// Edit Mode Values
|
||||||
|
|
|
@ -52,6 +52,14 @@ var (
|
||||||
Color: render.Black,
|
Color: render.Black,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SmallMonoFont for cramped spaces like the +/- buttons on Toolbar.
|
||||||
|
SmallMonoFont = render.Text{
|
||||||
|
Size: 14,
|
||||||
|
PadX: 3,
|
||||||
|
FontFilename: "DejaVuSansMono.ttf",
|
||||||
|
Color: render.Black,
|
||||||
|
}
|
||||||
|
|
||||||
// Color for draggable doodad.
|
// Color for draggable doodad.
|
||||||
DragColor = render.MustHexColor("#0099FF")
|
DragColor = render.MustHexColor("#0099FF")
|
||||||
|
|
||||||
|
|
|
@ -113,6 +113,7 @@ func (d *Doodle) Run() error {
|
||||||
|
|
||||||
// Poll for events.
|
// Poll for events.
|
||||||
ev, err := d.Engine.Poll()
|
ev, err := d.Engine.Poll()
|
||||||
|
shmem.Cursor = render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
|
||||||
if ev.EnterKey.Now {
|
if ev.EnterKey.Now {
|
||||||
log.Info("MainLoop sees enter key now")
|
log.Info("MainLoop sees enter key now")
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,4 +8,5 @@ const (
|
||||||
Freehand Shape = iota
|
Freehand Shape = iota
|
||||||
Line
|
Line
|
||||||
Rectangle
|
Rectangle
|
||||||
|
Eraser // not really a shape but communicates the intention
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,6 +18,7 @@ type Stroke struct {
|
||||||
ID int // Unique ID per each stroke
|
ID int // Unique ID per each stroke
|
||||||
Shape Shape
|
Shape Shape
|
||||||
Color render.Color
|
Color render.Color
|
||||||
|
Thickness int // 0 = 1px; thickness creates a box N pixels away from each point
|
||||||
ExtraData interface{} // arbitrary storage for extra data to attach
|
ExtraData interface{} // arbitrary storage for extra data to attach
|
||||||
|
|
||||||
// Start and end points for Lines, Rectangles, etc.
|
// Start and end points for Lines, Rectangles, etc.
|
||||||
|
@ -27,6 +28,16 @@ type Stroke struct {
|
||||||
// Array of points for Freehand shapes.
|
// Array of points for Freehand shapes.
|
||||||
Points []render.Point
|
Points []render.Point
|
||||||
uniqPoint map[render.Point]interface{} // deduplicate points added
|
uniqPoint map[render.Point]interface{} // deduplicate points added
|
||||||
|
|
||||||
|
// Storage space to recall the previous values of points that were replaced,
|
||||||
|
// especially for the Undo/Redo History tool. When the uix.Canvas commits a
|
||||||
|
// Stroke to the level data, any pixel that has replaced an existing color
|
||||||
|
// will cache its color here, so we can easily page forwards and backwards
|
||||||
|
// in history and not lose data.
|
||||||
|
//
|
||||||
|
// The data is implementation defined and controlled by the caller. This
|
||||||
|
// package does not modify OriginalPoints or do anything with it.
|
||||||
|
OriginalPoints map[render.Point]interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var nextStrokeID int
|
var nextStrokeID int
|
||||||
|
@ -42,6 +53,8 @@ func NewStroke(shape Shape, color render.Color) *Stroke {
|
||||||
// Initialize data structures.
|
// Initialize data structures.
|
||||||
Points: []render.Point{},
|
Points: []render.Point{},
|
||||||
uniqPoint: map[render.Point]interface{}{},
|
uniqPoint: map[render.Point]interface{}{},
|
||||||
|
|
||||||
|
OriginalPoints: map[render.Point]interface{}{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,9 +62,11 @@ func NewStroke(shape Shape, color render.Color) *Stroke {
|
||||||
func (s *Stroke) Copy() *Stroke {
|
func (s *Stroke) Copy() *Stroke {
|
||||||
nextStrokeID++
|
nextStrokeID++
|
||||||
return &Stroke{
|
return &Stroke{
|
||||||
ID: nextStrokeID,
|
ID: nextStrokeID,
|
||||||
Shape: s.Shape,
|
Shape: s.Shape,
|
||||||
Color: s.Color,
|
Color: s.Color,
|
||||||
|
Thickness: s.Thickness,
|
||||||
|
ExtraData: s.ExtraData,
|
||||||
|
|
||||||
Points: []render.Point{},
|
Points: []render.Point{},
|
||||||
uniqPoint: map[render.Point]interface{}{},
|
uniqPoint: map[render.Point]interface{}{},
|
||||||
|
@ -66,6 +81,8 @@ func (s *Stroke) IterPoints() chan render.Point {
|
||||||
ch := make(chan render.Point)
|
ch := make(chan render.Point)
|
||||||
go func() {
|
go func() {
|
||||||
switch s.Shape {
|
switch s.Shape {
|
||||||
|
case Eraser:
|
||||||
|
fallthrough
|
||||||
case Freehand:
|
case Freehand:
|
||||||
for _, point := range s.Points {
|
for _, point := range s.Points {
|
||||||
ch <- point
|
ch <- point
|
||||||
|
@ -84,6 +101,23 @@ func (s *Stroke) IterPoints() chan render.Point {
|
||||||
return ch
|
return ch
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IterThickPoints iterates over the points and yield Rects of each one.
|
||||||
|
func (s *Stroke) IterThickPoints() chan render.Rect {
|
||||||
|
ch := make(chan render.Rect)
|
||||||
|
go func() {
|
||||||
|
for pt := range s.IterPoints() {
|
||||||
|
ch <- render.Rect{
|
||||||
|
X: pt.X - int32(s.Thickness),
|
||||||
|
Y: pt.Y - int32(s.Thickness),
|
||||||
|
W: int32(s.Thickness) * 2,
|
||||||
|
H: int32(s.Thickness) * 2,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
|
||||||
// AddPoint adds a point to the stroke, for freehand shapes.
|
// AddPoint adds a point to the stroke, for freehand shapes.
|
||||||
func (s *Stroke) AddPoint(p render.Point) {
|
func (s *Stroke) AddPoint(p render.Point) {
|
||||||
if _, ok := s.uniqPoint[p]; ok {
|
if _, ok := s.uniqPoint[p]; ok {
|
||||||
|
|
|
@ -10,6 +10,7 @@ const (
|
||||||
RectTool
|
RectTool
|
||||||
ActorTool // drag and move actors
|
ActorTool // drag and move actors
|
||||||
LinkTool
|
LinkTool
|
||||||
|
EraserTool
|
||||||
)
|
)
|
||||||
|
|
||||||
var toolNames = []string{
|
var toolNames = []string{
|
||||||
|
@ -18,6 +19,7 @@ var toolNames = []string{
|
||||||
"Rectangle",
|
"Rectangle",
|
||||||
"Doodad", // readable name for ActorTool
|
"Doodad", // readable name for ActorTool
|
||||||
"Link",
|
"Link",
|
||||||
|
"Eraser",
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t Tool) String() string {
|
func (t Tool) String() string {
|
||||||
|
|
|
@ -227,7 +227,6 @@ func (s *EditorScene) LoadLevel(filename string) error {
|
||||||
s.filename = filename
|
s.filename = filename
|
||||||
|
|
||||||
level, err := level.LoadFile(filename)
|
level, err := level.LoadFile(filename)
|
||||||
fmt.Printf("%+v\n", level)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
|
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
|
||||||
}
|
}
|
||||||
|
|
|
@ -216,8 +216,6 @@ func (u *EditorUI) scrollDoodadFrame(rows int) {
|
||||||
u.doodadSkip = 0
|
u.doodadSkip = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("scrollDoodadFrame(%d): skip=%d", rows, u.doodadSkip)
|
|
||||||
|
|
||||||
// Calculate about how many rows we can see given our current window size.
|
// Calculate about how many rows we can see given our current window size.
|
||||||
var (
|
var (
|
||||||
maxVisibleHeight = int32(u.d.height - 86)
|
maxVisibleHeight = int32(u.d.height - 86)
|
||||||
|
@ -233,8 +231,6 @@ func (u *EditorUI) scrollDoodadFrame(rows int) {
|
||||||
maxSkip = 0
|
maxSkip = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Info("maxSkip = (%d * %d) - (%d * %d) = %d", len(u.doodadRows), u.doodadButtonSize, u.doodadButtonSize, rowsEstimated, maxSkip)
|
|
||||||
// log.Info("maxSkip: estimate=%d rows=%d - visible=%d => %d", rowsEstimated, len(u.doodadRows), rowsVisible, maxSkip)
|
|
||||||
if u.doodadSkip > maxSkip {
|
if u.doodadSkip > maxSkip {
|
||||||
u.doodadSkip = maxSkip
|
u.doodadSkip = maxSkip
|
||||||
}
|
}
|
||||||
|
@ -269,6 +265,5 @@ func (u *EditorUI) scrollDoodadFrame(rows int) {
|
||||||
u.doodadScroller.Configure(ui.Config{
|
u.doodadScroller.Configure(ui.Config{
|
||||||
Width: int32(float64(paletteWidth-50) * viewPercent), // TODO: hacky magic number
|
Width: int32(float64(paletteWidth-50) * viewPercent), // TODO: hacky magic number
|
||||||
})
|
})
|
||||||
log.Info("v%% = (%d + %d) / %d = %f", rowsBefore, rowsVisible, len(u.doodadRows), viewPercent)
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,8 +3,8 @@ package doodle
|
||||||
import (
|
import (
|
||||||
"git.kirsle.net/apps/doodle/lib/render"
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
"git.kirsle.net/apps/doodle/lib/ui"
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||||
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
||||||
"git.kirsle.net/apps/doodle/pkg/log"
|
|
||||||
"git.kirsle.net/apps/doodle/pkg/sprites"
|
"git.kirsle.net/apps/doodle/pkg/sprites"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -27,6 +27,18 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Anchor: ui.N,
|
Anchor: ui.N,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Helper functions to toggle the correct palette panel.
|
||||||
|
var (
|
||||||
|
showSwatchPalette = func() {
|
||||||
|
u.DoodadTab.Hide()
|
||||||
|
u.PaletteTab.Show()
|
||||||
|
}
|
||||||
|
showDoodadPalette = func() {
|
||||||
|
u.PaletteTab.Hide()
|
||||||
|
u.DoodadTab.Show()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
// Buttons.
|
// Buttons.
|
||||||
var buttons = []struct {
|
var buttons = []struct {
|
||||||
Value string
|
Value string
|
||||||
|
@ -38,8 +50,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Icon: "assets/sprites/pencil-tool.png",
|
Icon: "assets/sprites/pencil-tool.png",
|
||||||
Click: func() {
|
Click: func() {
|
||||||
u.Canvas.Tool = drawtool.PencilTool
|
u.Canvas.Tool = drawtool.PencilTool
|
||||||
u.DoodadTab.Hide()
|
showSwatchPalette()
|
||||||
u.PaletteTab.Show()
|
|
||||||
d.Flash("Pencil Tool selected.")
|
d.Flash("Pencil Tool selected.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -49,8 +60,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Icon: "assets/sprites/line-tool.png",
|
Icon: "assets/sprites/line-tool.png",
|
||||||
Click: func() {
|
Click: func() {
|
||||||
u.Canvas.Tool = drawtool.LineTool
|
u.Canvas.Tool = drawtool.LineTool
|
||||||
u.DoodadTab.Hide()
|
showSwatchPalette()
|
||||||
u.PaletteTab.Show()
|
|
||||||
d.Flash("Line Tool selected.")
|
d.Flash("Line Tool selected.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -60,8 +70,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Icon: "assets/sprites/rect-tool.png",
|
Icon: "assets/sprites/rect-tool.png",
|
||||||
Click: func() {
|
Click: func() {
|
||||||
u.Canvas.Tool = drawtool.RectTool
|
u.Canvas.Tool = drawtool.RectTool
|
||||||
u.DoodadTab.Hide()
|
showSwatchPalette()
|
||||||
u.PaletteTab.Show()
|
|
||||||
d.Flash("Rectangle Tool selected.")
|
d.Flash("Rectangle Tool selected.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -71,8 +80,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Icon: "assets/sprites/actor-tool.png",
|
Icon: "assets/sprites/actor-tool.png",
|
||||||
Click: func() {
|
Click: func() {
|
||||||
u.Canvas.Tool = drawtool.ActorTool
|
u.Canvas.Tool = drawtool.ActorTool
|
||||||
u.PaletteTab.Hide()
|
showDoodadPalette()
|
||||||
u.DoodadTab.Show()
|
|
||||||
d.Flash("Actor Tool selected. Drag a Doodad from the drawer into your level.")
|
d.Flash("Actor Tool selected. Drag a Doodad from the drawer into your level.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -82,11 +90,28 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
Icon: "assets/sprites/link-tool.png",
|
Icon: "assets/sprites/link-tool.png",
|
||||||
Click: func() {
|
Click: func() {
|
||||||
u.Canvas.Tool = drawtool.LinkTool
|
u.Canvas.Tool = drawtool.LinkTool
|
||||||
u.PaletteTab.Hide()
|
showDoodadPalette()
|
||||||
u.DoodadTab.Show()
|
|
||||||
d.Flash("Link Tool selected. Click a doodad in your level to link it to another.")
|
d.Flash("Link Tool selected. Click a doodad in your level to link it to another.")
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
Value: drawtool.EraserTool.String(),
|
||||||
|
Icon: "assets/sprites/eraser-tool.png",
|
||||||
|
Click: func() {
|
||||||
|
u.Canvas.Tool = drawtool.EraserTool
|
||||||
|
|
||||||
|
// Set the brush size within range for the eraser.
|
||||||
|
if u.Canvas.BrushSize < balance.DefaultEraserBrushSize {
|
||||||
|
u.Canvas.BrushSize = balance.DefaultEraserBrushSize
|
||||||
|
} else if u.Canvas.BrushSize > balance.MaxEraserBrushSize {
|
||||||
|
u.Canvas.BrushSize = balance.MaxEraserBrushSize
|
||||||
|
}
|
||||||
|
|
||||||
|
showSwatchPalette()
|
||||||
|
d.Flash("Eraser Tool selected.")
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, button := range buttons {
|
for _, button := range buttons {
|
||||||
button := button
|
button := button
|
||||||
|
@ -103,7 +128,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
)
|
)
|
||||||
|
|
||||||
var btnSize int32 = btn.BoxThickness(2) + toolbarSpriteSize
|
var btnSize int32 = btn.BoxThickness(2) + toolbarSpriteSize
|
||||||
log.Info("BtnSize: %d", btnSize)
|
|
||||||
btn.Resize(render.NewRect(btnSize, btnSize))
|
btn.Resize(render.NewRect(btnSize, btnSize))
|
||||||
|
|
||||||
btn.Handle(ui.Click, func(p render.Point) {
|
btn.Handle(ui.Click, func(p render.Point) {
|
||||||
|
@ -117,6 +141,102 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Spacer frame.
|
||||||
|
frame.Pack(ui.NewFrame("spacer"), ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
PadY: 8,
|
||||||
|
})
|
||||||
|
|
||||||
|
// "Brush Size" label
|
||||||
|
bsLabel := ui.NewLabel(ui.Label{
|
||||||
|
Text: "Size:",
|
||||||
|
Font: balance.LabelFont,
|
||||||
|
})
|
||||||
|
frame.Pack(bsLabel, ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
})
|
||||||
|
|
||||||
|
// Brush Size widget
|
||||||
|
{
|
||||||
|
sizeFrame := ui.NewFrame("Brush Size Frame")
|
||||||
|
frame.Pack(sizeFrame, ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
PadY: 0,
|
||||||
|
})
|
||||||
|
|
||||||
|
sizeLabel := ui.NewLabel(ui.Label{
|
||||||
|
IntVariable: &u.Canvas.BrushSize,
|
||||||
|
Font: balance.SmallMonoFont,
|
||||||
|
})
|
||||||
|
sizeLabel.Configure(ui.Config{
|
||||||
|
BorderSize: 1,
|
||||||
|
BorderStyle: ui.BorderSunken,
|
||||||
|
Background: render.Grey,
|
||||||
|
})
|
||||||
|
sizeFrame.Pack(sizeLabel, ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
FillX: true,
|
||||||
|
PadY: 2,
|
||||||
|
})
|
||||||
|
|
||||||
|
sizeBtnFrame := ui.NewFrame("Size Increment Button Frame")
|
||||||
|
sizeFrame.Pack(sizeBtnFrame, ui.Pack{
|
||||||
|
Anchor: ui.N,
|
||||||
|
FillX: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
var incButtons = []struct {
|
||||||
|
Label string
|
||||||
|
F func()
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Label: "-",
|
||||||
|
F: func() {
|
||||||
|
// Select next smaller brush size.
|
||||||
|
for i := len(balance.BrushSizeOptions) - 1; i >= 0; i-- {
|
||||||
|
if balance.BrushSizeOptions[i] < u.Canvas.BrushSize {
|
||||||
|
u.Canvas.BrushSize = balance.BrushSizeOptions[i]
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Label: "+",
|
||||||
|
F: func() {
|
||||||
|
// Select next bigger brush size.
|
||||||
|
for _, size := range balance.BrushSizeOptions {
|
||||||
|
if size > u.Canvas.BrushSize {
|
||||||
|
u.Canvas.BrushSize = size
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit the eraser brush size, too big and it's slow because
|
||||||
|
// the eraser has to scan and remember pixels to be able to
|
||||||
|
// Undo the erase and restore them.
|
||||||
|
if u.Canvas.Tool == drawtool.EraserTool && u.Canvas.BrushSize > balance.MaxEraserBrushSize {
|
||||||
|
u.Canvas.BrushSize = balance.MaxEraserBrushSize
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, button := range incButtons {
|
||||||
|
button := button
|
||||||
|
btn := ui.NewButton("BrushSize"+button.Label, ui.NewLabel(ui.Label{
|
||||||
|
Text: button.Label,
|
||||||
|
Font: balance.SmallMonoFont,
|
||||||
|
}))
|
||||||
|
btn.Handle(ui.Click, func(p render.Point) {
|
||||||
|
button.F()
|
||||||
|
})
|
||||||
|
u.Supervisor.Add(btn)
|
||||||
|
sizeBtnFrame.Pack(btn, ui.Pack{
|
||||||
|
Anchor: ui.W,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
frame.Compute(d.Engine)
|
frame.Compute(d.Engine)
|
||||||
|
|
||||||
return frame
|
return frame
|
||||||
|
|
|
@ -204,6 +204,23 @@ func (c *Chunker) Set(p render.Point, sw *Swatch) error {
|
||||||
return chunk.Set(p, sw)
|
return chunk.Set(p, sw)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SetRect sets a rectangle of pixels to a color all at once.
|
||||||
|
func (c *Chunker) SetRect(r render.Rect, sw *Swatch) error {
|
||||||
|
var (
|
||||||
|
xMin = r.X
|
||||||
|
yMin = r.Y
|
||||||
|
xMax = r.X + r.W
|
||||||
|
yMax = r.Y + r.H
|
||||||
|
)
|
||||||
|
for x := xMin; x < xMax; x++ {
|
||||||
|
for y := yMin; y < yMax; y++ {
|
||||||
|
c.Set(render.NewPoint(x, y), sw)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Delete a pixel at the given coordinate.
|
// Delete a pixel at the given coordinate.
|
||||||
func (c *Chunker) Delete(p render.Point) error {
|
func (c *Chunker) Delete(p render.Point) error {
|
||||||
coord := c.ChunkCoordinate(p)
|
coord := c.ChunkCoordinate(p)
|
||||||
|
@ -213,6 +230,25 @@ func (c *Chunker) Delete(p render.Point) error {
|
||||||
return fmt.Errorf("no chunk %s exists for point %s", coord, p)
|
return fmt.Errorf("no chunk %s exists for point %s", coord, p)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteRect deletes a rectangle of pixels between two points.
|
||||||
|
// The rect is a relative one with a width and height, and the X,Y values are
|
||||||
|
// an absolute world coordinate.
|
||||||
|
func (c *Chunker) DeleteRect(r render.Rect) error {
|
||||||
|
var (
|
||||||
|
xMin = r.X
|
||||||
|
yMin = r.Y
|
||||||
|
xMax = r.X + r.W
|
||||||
|
yMax = r.Y + r.H
|
||||||
|
)
|
||||||
|
for x := xMin; x < xMax; x++ {
|
||||||
|
for y := yMin; y < yMax; y++ {
|
||||||
|
c.Delete(render.NewPoint(x, y))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ChunkCoordinate computes a chunk coordinate from an absolute coordinate.
|
// ChunkCoordinate computes a chunk coordinate from an absolute coordinate.
|
||||||
func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point {
|
func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point {
|
||||||
if c.Size == 0 {
|
if c.Size == 0 {
|
||||||
|
|
|
@ -12,6 +12,9 @@ var (
|
||||||
// Tick is incremented by the main game loop each frame.
|
// Tick is incremented by the main game loop each frame.
|
||||||
Tick uint64
|
Tick uint64
|
||||||
|
|
||||||
|
// Current position of the cursor relative to the window.
|
||||||
|
Cursor render.Point
|
||||||
|
|
||||||
// Current render engine (i.e. SDL2 or HTML5 Canvas)
|
// Current render engine (i.e. SDL2 or HTML5 Canvas)
|
||||||
// The level.Chunk.ToBitmap() uses this to cache a texture image.
|
// The level.Chunk.ToBitmap() uses this to cache a texture image.
|
||||||
CurrentRenderEngine render.Engine
|
CurrentRenderEngine render.Engine
|
||||||
|
|
|
@ -31,7 +31,8 @@ type Canvas struct {
|
||||||
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
|
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
|
||||||
|
|
||||||
// Selected draw tool/mode, default Pencil, for editable canvases.
|
// Selected draw tool/mode, default Pencil, for editable canvases.
|
||||||
Tool drawtool.Tool
|
Tool drawtool.Tool
|
||||||
|
BrushSize int // thickness of selected brush
|
||||||
|
|
||||||
// MaskColor will force every pixel to render as this color regardless of
|
// 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 index of that pixel. Otherwise pixels behave the same and
|
||||||
|
@ -86,10 +87,7 @@ type Canvas struct {
|
||||||
// mousedown-and-dragging event.
|
// mousedown-and-dragging event.
|
||||||
currentStroke *drawtool.Stroke
|
currentStroke *drawtool.Stroke
|
||||||
strokes map[int]*drawtool.Stroke // active stroke mapped by ID
|
strokes map[int]*drawtool.Stroke // active stroke mapped by ID
|
||||||
|
lastPixel *level.Pixel
|
||||||
// Tracking pixels while editing. TODO: get rid of pixelHistory?
|
|
||||||
pixelHistory []*level.Pixel
|
|
||||||
lastPixel *level.Pixel
|
|
||||||
|
|
||||||
// We inherit the ui.Widget which manages the width and height.
|
// We inherit the ui.Widget which manages the width and height.
|
||||||
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
||||||
|
|
50
pkg/uix/canvas_cursor.go
Normal file
50
pkg/uix/canvas_cursor.go
Normal file
|
@ -0,0 +1,50 @@
|
||||||
|
package uix
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"git.kirsle.net/apps/doodle/lib/ui"
|
||||||
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
||||||
|
)
|
||||||
|
|
||||||
|
// IsCursorOver returns true if the mouse cursor is physically over top
|
||||||
|
// of the canvas's widget space.
|
||||||
|
func (w *Canvas) IsCursorOver() bool {
|
||||||
|
var (
|
||||||
|
P = ui.AbsolutePosition(w)
|
||||||
|
S = w.Size()
|
||||||
|
)
|
||||||
|
return shmem.Cursor.Inside(render.Rect{
|
||||||
|
X: P.X,
|
||||||
|
Y: P.Y,
|
||||||
|
W: S.W,
|
||||||
|
H: S.H,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// presentCursor draws something at the mouse cursor on the Canvas.
|
||||||
|
//
|
||||||
|
// This is currently used in Edit Mode when you're drawing a shape with a thick
|
||||||
|
// brush size, and draws a "preview rect" under the cursor of how big a click
|
||||||
|
// will be at that size.
|
||||||
|
func (w *Canvas) presentCursor(e render.Engine) {
|
||||||
|
if !w.IsCursorOver() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Are we editing with a thick brush?
|
||||||
|
if w.BrushSize > 0 {
|
||||||
|
var r = int32(w.BrushSize)
|
||||||
|
rect := render.Rect{
|
||||||
|
X: shmem.Cursor.X - r,
|
||||||
|
Y: shmem.Cursor.Y - r,
|
||||||
|
W: r * 2,
|
||||||
|
H: r * 2,
|
||||||
|
}
|
||||||
|
e.DrawRect(render.Black, rect)
|
||||||
|
rect.X++
|
||||||
|
rect.Y++
|
||||||
|
rect.W -= 2
|
||||||
|
rect.H -= 2
|
||||||
|
e.DrawRect(render.RGBA(153, 153, 153, 153), rect)
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,93 @@ import (
|
||||||
"git.kirsle.net/apps/doodle/pkg/level"
|
"git.kirsle.net/apps/doodle/pkg/level"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// commitStroke is the common function that applies a stroke the user is
|
||||||
|
// actively drawing onto the canvas. This is for Edit Mode.
|
||||||
|
func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) {
|
||||||
|
if w.currentStroke == nil {
|
||||||
|
// nothing to commit
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
deleting = w.currentStroke.Shape == drawtool.Eraser
|
||||||
|
dedupe = map[render.Point]interface{}{} // don't revisit the same point twice
|
||||||
|
|
||||||
|
// Helper functions to set pixels on the level while storing the original
|
||||||
|
// value of any pixel being replaced.
|
||||||
|
set = func(pt render.Point, sw *level.Swatch) {
|
||||||
|
// Take note of what pixel was originally here before we change it.
|
||||||
|
if swatch, err := w.chunks.Get(pt); err == nil {
|
||||||
|
if _, ok := dedupe[pt]; !ok {
|
||||||
|
w.currentStroke.OriginalPoints[pt] = swatch
|
||||||
|
dedupe[pt] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if deleting {
|
||||||
|
w.chunks.Delete(pt)
|
||||||
|
} else if sw != nil {
|
||||||
|
w.chunks.Set(pt, sw)
|
||||||
|
} else {
|
||||||
|
panic("Canvas.commitStroke.set: current stroke has no level.Swatch in ExtraData")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rects: read existing pixels first, then write new pixels
|
||||||
|
readRect = func(rect render.Rect) {
|
||||||
|
for pt := range w.chunks.IterViewport(rect) {
|
||||||
|
point := pt.Point()
|
||||||
|
if _, ok := dedupe[point]; !ok {
|
||||||
|
w.currentStroke.OriginalPoints[pt.Point()] = pt.Swatch
|
||||||
|
dedupe[point] = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setRect = func(rect render.Rect, sw *level.Swatch) {
|
||||||
|
if deleting {
|
||||||
|
w.chunks.DeleteRect(rect)
|
||||||
|
} else if sw != nil {
|
||||||
|
w.chunks.SetRect(rect, sw)
|
||||||
|
} else {
|
||||||
|
panic("Canvas.commitStroke.setRect: current stroke has no level.Swatch in ExtraData")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
var swatch *level.Swatch
|
||||||
|
if v, ok := w.currentStroke.ExtraData.(*level.Swatch); ok {
|
||||||
|
swatch = v
|
||||||
|
}
|
||||||
|
|
||||||
|
if w.currentStroke.Thickness > 0 {
|
||||||
|
// Eraser Tool only: record which pixels will be blown away by this.
|
||||||
|
// This is SLOW for thick (rect-based) lines, but eraser tool must have it.
|
||||||
|
if deleting {
|
||||||
|
for rect := range w.currentStroke.IterThickPoints() {
|
||||||
|
readRect(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for rect := range w.currentStroke.IterThickPoints() {
|
||||||
|
setRect(rect, swatch)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for pt := range w.currentStroke.IterPoints() {
|
||||||
|
// note: set already records the original pixel if changing it.
|
||||||
|
set(pt, swatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the stroke to level history.
|
||||||
|
if w.level != nil && addHistory {
|
||||||
|
w.level.UndoHistory.AddStroke(w.currentStroke)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.RemoveStroke(w.currentStroke)
|
||||||
|
w.currentStroke = nil
|
||||||
|
|
||||||
|
w.lastPixel = nil
|
||||||
|
}
|
||||||
|
|
||||||
// loopEditable handles the Loop() part for editable canvases.
|
// loopEditable handles the Loop() part for editable canvases.
|
||||||
func (w *Canvas) loopEditable(ev *events.State) error {
|
func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
// Get the absolute position of the canvas on screen to accurately match
|
// Get the absolute position of the canvas on screen to accurately match
|
||||||
|
@ -20,6 +107,14 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If the actual cursor is not over the actual Canvas UI element, don't
|
||||||
|
// pay any attention to clicks. I added this when I saw you were able to
|
||||||
|
// accidentally draw (with large brush size) when clicking on the Palette
|
||||||
|
// panel and not the drawing itself.
|
||||||
|
if !w.IsCursorOver() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
switch w.Tool {
|
switch w.Tool {
|
||||||
case drawtool.PencilTool:
|
case drawtool.PencilTool:
|
||||||
// If no swatch is active, do nothing with mouse clicks.
|
// If no swatch is active, do nothing with mouse clicks.
|
||||||
|
@ -32,6 +127,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
// Initialize a new Stroke for this atomic drawing operation?
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.AddStroke(w.currentStroke)
|
w.AddStroke(w.currentStroke)
|
||||||
}
|
}
|
||||||
|
@ -53,18 +149,15 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Append unique new pixels.
|
// Append unique new pixels.
|
||||||
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
|
if lastPixel != nil || lastPixel != pixel {
|
||||||
if lastPixel != nil {
|
// Draw the pixels in between.
|
||||||
// Draw the pixels in between.
|
if lastPixel != nil && lastPixel != pixel {
|
||||||
if lastPixel != pixel {
|
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||||
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
w.currentStroke.AddPoint(point)
|
||||||
w.currentStroke.AddPoint(point)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.lastPixel = pixel
|
w.lastPixel = pixel
|
||||||
w.pixelHistory = append(w.pixelHistory, pixel)
|
|
||||||
|
|
||||||
// Save the pixel in the current stroke.
|
// Save the pixel in the current stroke.
|
||||||
w.currentStroke.AddPoint(render.Point{
|
w.currentStroke.AddPoint(render.Point{
|
||||||
|
@ -73,22 +166,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Mouse released, commit the points to the drawing.
|
w.commitStroke(w.Tool, true)
|
||||||
if w.currentStroke != nil {
|
|
||||||
for _, pt := range w.currentStroke.Points {
|
|
||||||
w.chunks.Set(pt, w.Palette.ActiveSwatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the stroke to level history.
|
|
||||||
if w.level != nil {
|
|
||||||
w.level.UndoHistory.AddStroke(w.currentStroke)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.RemoveStroke(w.currentStroke)
|
|
||||||
w.currentStroke = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
w.lastPixel = nil
|
|
||||||
}
|
}
|
||||||
case drawtool.LineTool:
|
case drawtool.LineTool:
|
||||||
// If no swatch is active, do nothing with mouse clicks.
|
// If no swatch is active, do nothing with mouse clicks.
|
||||||
|
@ -101,6 +179,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
// Initialize a new Stroke for this atomic drawing operation?
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
||||||
w.AddStroke(w.currentStroke)
|
w.AddStroke(w.currentStroke)
|
||||||
|
@ -108,20 +187,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
|
|
||||||
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
||||||
} else {
|
} else {
|
||||||
// Mouse released, commit the points to the drawing.
|
w.commitStroke(w.Tool, true)
|
||||||
if w.currentStroke != nil {
|
|
||||||
for pt := range render.IterLine2(w.currentStroke.PointA, w.currentStroke.PointB) {
|
|
||||||
w.chunks.Set(pt, w.Palette.ActiveSwatch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add the stroke to level history.
|
|
||||||
if w.level != nil {
|
|
||||||
w.level.UndoHistory.AddStroke(w.currentStroke)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.RemoveStroke(w.currentStroke)
|
|
||||||
w.currentStroke = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
case drawtool.RectTool:
|
case drawtool.RectTool:
|
||||||
// If no swatch is active, do nothing with mouse clicks.
|
// If no swatch is active, do nothing with mouse clicks.
|
||||||
|
@ -134,6 +200,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
// Initialize a new Stroke for this atomic drawing operation?
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
if w.currentStroke == nil {
|
if w.currentStroke == nil {
|
||||||
w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color)
|
w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color)
|
||||||
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
|
||||||
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
|
||||||
w.AddStroke(w.currentStroke)
|
w.AddStroke(w.currentStroke)
|
||||||
|
@ -141,20 +208,52 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
||||||
|
|
||||||
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
w.currentStroke.PointB = render.NewPoint(cursor.X, cursor.Y)
|
||||||
} else {
|
} else {
|
||||||
// Mouse released, commit the points to the drawing.
|
w.commitStroke(w.Tool, true)
|
||||||
if w.currentStroke != nil {
|
}
|
||||||
for pt := range render.IterRect(w.currentStroke.PointA, w.currentStroke.PointB) {
|
case drawtool.EraserTool:
|
||||||
w.chunks.Set(pt, w.Palette.ActiveSwatch)
|
// Clicking? Log all the pixels while doing so.
|
||||||
}
|
if ev.Button1.Now {
|
||||||
|
// Initialize a new Stroke for this atomic drawing operation?
|
||||||
// Add the stroke to level history.
|
if w.currentStroke == nil {
|
||||||
if w.level != nil {
|
// The color is white, will look like white-out that covers the
|
||||||
w.level.UndoHistory.AddStroke(w.currentStroke)
|
// wallpaper during the stroke.
|
||||||
}
|
w.currentStroke = drawtool.NewStroke(drawtool.Eraser, render.White)
|
||||||
|
w.currentStroke.Thickness = w.BrushSize
|
||||||
w.RemoveStroke(w.currentStroke)
|
w.AddStroke(w.currentStroke)
|
||||||
w.currentStroke = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastPixel := w.lastPixel
|
||||||
|
pixel := &level.Pixel{
|
||||||
|
X: cursor.X,
|
||||||
|
Y: cursor.Y,
|
||||||
|
Swatch: w.Palette.ActiveSwatch,
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the user is holding the mouse down over one spot and not
|
||||||
|
// moving, don't do anything. The pixel has already been set and
|
||||||
|
// needless writes to the map cause needless cache rewrites etc.
|
||||||
|
if lastPixel != nil {
|
||||||
|
if pixel.X == lastPixel.X && pixel.Y == lastPixel.Y {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append unique new pixels.
|
||||||
|
if lastPixel == nil || lastPixel != pixel {
|
||||||
|
if lastPixel != nil && lastPixel != pixel {
|
||||||
|
for point := range render.IterLine(lastPixel.X, lastPixel.Y, pixel.X, pixel.Y) {
|
||||||
|
w.currentStroke.AddPoint(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.lastPixel = pixel
|
||||||
|
w.currentStroke.AddPoint(render.Point{
|
||||||
|
X: cursor.X,
|
||||||
|
Y: cursor.Y,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
w.commitStroke(w.Tool, true)
|
||||||
}
|
}
|
||||||
case drawtool.ActorTool:
|
case drawtool.ActorTool:
|
||||||
// See if any of the actors are below the mouse cursor.
|
// See if any of the actors are below the mouse cursor.
|
||||||
|
|
|
@ -132,8 +132,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||||
}
|
}
|
||||||
|
|
||||||
w.drawActors(e, p)
|
w.drawActors(e, p)
|
||||||
|
|
||||||
w.presentStrokes(e)
|
w.presentStrokes(e)
|
||||||
|
w.presentCursor(e)
|
||||||
|
|
||||||
// XXX: Debug, show label in canvas corner.
|
// XXX: Debug, show label in canvas corner.
|
||||||
if balance.DebugCanvasLabel {
|
if balance.DebugCanvasLabel {
|
||||||
|
|
|
@ -48,8 +48,48 @@ func (w *Canvas) UndoStroke() bool {
|
||||||
|
|
||||||
latest := w.level.UndoHistory.Latest()
|
latest := w.level.UndoHistory.Latest()
|
||||||
if latest != nil {
|
if latest != nil {
|
||||||
for point := range latest.IterPoints() {
|
// TODO: only single-thickness lines will restore the original color;
|
||||||
w.chunks.Delete(point)
|
// thick lines just delete their pixels from the world due to performance.
|
||||||
|
// But the Eraser Tool is always thick, which always should restore its
|
||||||
|
// pixels. Can't do anything about that, so the inefficient thick rect
|
||||||
|
// restore is used only for Eraser at least.
|
||||||
|
if latest.Thickness > 0 {
|
||||||
|
if latest.Shape == drawtool.Eraser {
|
||||||
|
for rect := range latest.IterThickPoints() {
|
||||||
|
var (
|
||||||
|
xMin = rect.X
|
||||||
|
xMax = rect.X + rect.W
|
||||||
|
yMin = rect.Y
|
||||||
|
yMax = rect.Y + rect.H
|
||||||
|
)
|
||||||
|
for x := xMin; x < xMax; x++ {
|
||||||
|
for y := yMin; y < yMax; y++ {
|
||||||
|
if v, ok := latest.OriginalPoints[render.NewPoint(x, y)]; ok {
|
||||||
|
if swatch, ok := v.(*level.Swatch); ok {
|
||||||
|
w.chunks.Set(render.NewPoint(x, y), swatch)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for rect := range latest.IterThickPoints() {
|
||||||
|
w.chunks.DeleteRect(rect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for point := range latest.IterPoints() {
|
||||||
|
// Was there a previous swatch at this point to restore?
|
||||||
|
if v, ok := latest.OriginalPoints[point]; ok {
|
||||||
|
if swatch, ok := v.(*level.Swatch); ok {
|
||||||
|
w.chunks.Set(point, swatch)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
w.chunks.Delete(point)
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return w.level.UndoHistory.Undo()
|
return w.level.UndoHistory.Undo()
|
||||||
|
@ -72,14 +112,8 @@ func (w *Canvas) RedoStroke() bool {
|
||||||
|
|
||||||
// We stored the ActiveSwatch on this stroke as we drew it. Recover it
|
// We stored the ActiveSwatch on this stroke as we drew it. Recover it
|
||||||
// and place the pixels back down.
|
// and place the pixels back down.
|
||||||
if swatch, ok := latest.ExtraData.(*level.Swatch); ok {
|
w.currentStroke = latest
|
||||||
for point := range latest.IterPoints() {
|
w.commitStroke(w.Tool, false)
|
||||||
w.chunks.Set(point, swatch)
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Error("Canvas.UndoStroke: undo was successful but no Swatch was stored on the Stroke.ExtraData!")
|
|
||||||
|
|
||||||
return ok
|
return ok
|
||||||
}
|
}
|
||||||
|
@ -153,6 +187,7 @@ func (w *Canvas) presentActorLinks(e render.Engine) {
|
||||||
|
|
||||||
// Draw a line connecting the centers of each actor together.
|
// Draw a line connecting the centers of each actor together.
|
||||||
stroke := drawtool.NewStroke(drawtool.Line, color)
|
stroke := drawtool.NewStroke(drawtool.Line, color)
|
||||||
|
stroke.Thickness = 1
|
||||||
stroke.PointA = render.Point{
|
stroke.PointA = render.Point{
|
||||||
X: aP.X + (aS.W / 2),
|
X: aP.X + (aS.W / 2),
|
||||||
Y: aP.Y + (aS.H / 2),
|
Y: aP.Y + (aS.H / 2),
|
||||||
|
@ -163,16 +198,6 @@ func (w *Canvas) presentActorLinks(e render.Engine) {
|
||||||
}
|
}
|
||||||
|
|
||||||
strokes = append(strokes, stroke)
|
strokes = append(strokes, stroke)
|
||||||
|
|
||||||
// Make it double thick.
|
|
||||||
double := stroke.Copy()
|
|
||||||
double.PointA = render.NewPoint(stroke.PointA.X, stroke.PointA.Y+1)
|
|
||||||
double.PointB = render.NewPoint(stroke.PointB.X, stroke.PointB.Y+1)
|
|
||||||
strokes = append(strokes, double)
|
|
||||||
double = stroke.Copy()
|
|
||||||
double.PointA = render.NewPoint(stroke.PointA.X+1, stroke.PointA.Y)
|
|
||||||
double.PointB = render.NewPoint(stroke.PointB.X+1, stroke.PointB.Y)
|
|
||||||
strokes = append(strokes, double)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -183,14 +208,14 @@ func (w *Canvas) presentActorLinks(e render.Engine) {
|
||||||
// presentActorLinks to actually draw the lines to the canvas.
|
// presentActorLinks to actually draw the lines to the canvas.
|
||||||
func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
||||||
var (
|
var (
|
||||||
P = ui.AbsolutePosition(w) // w.Point() // Canvas point in UI
|
P = ui.AbsolutePosition(w) // Canvas point in UI
|
||||||
VP = w.ViewportRelative() // Canvas scroll viewport
|
VP = w.ViewportRelative() // Canvas scroll viewport
|
||||||
)
|
)
|
||||||
|
|
||||||
for _, stroke := range strokes {
|
for _, stroke := range strokes {
|
||||||
// If none of this stroke is in our viewport, don't waste time
|
// If none of this stroke is in our viewport, don't waste time
|
||||||
// looping through it.
|
// looping through it.
|
||||||
if stroke.Shape == drawtool.Freehand {
|
if stroke.Shape == drawtool.Freehand || stroke.Shape == drawtool.Eraser {
|
||||||
if len(stroke.Points) >= 2 {
|
if len(stroke.Points) >= 2 {
|
||||||
if !stroke.Points[0].Inside(VP) && !stroke.Points[len(stroke.Points)-1].Inside(VP) {
|
if !stroke.Points[0].Inside(VP) && !stroke.Points[len(stroke.Points)-1].Inside(VP) {
|
||||||
continue
|
continue
|
||||||
|
@ -206,20 +231,58 @@ func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iter the points and draw what's visible.
|
// Iter the points and draw what's visible.
|
||||||
for point := range stroke.IterPoints() {
|
if stroke.Thickness > 0 {
|
||||||
if !point.Inside(VP) {
|
for rect := range stroke.IterThickPoints() {
|
||||||
continue
|
if !rect.Intersects(VP) {
|
||||||
}
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
dest := render.Point{
|
// Destination rectangle to draw to screen, taking into account
|
||||||
X: P.X + w.Scroll.X + w.BoxThickness(1) + point.X,
|
// the position of the Canvas itself.
|
||||||
Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + point.Y,
|
dest := render.Rect{
|
||||||
}
|
X: rect.X + P.X + w.Scroll.X + w.BoxThickness(1),
|
||||||
|
Y: rect.Y + P.Y + w.Scroll.Y + w.BoxThickness(1),
|
||||||
|
W: rect.W,
|
||||||
|
H: rect.H,
|
||||||
|
}
|
||||||
|
|
||||||
if balance.DebugCanvasStrokeColor != render.Invisible {
|
// Cap the render square so it doesn't leave the Canvas and
|
||||||
e.DrawPoint(balance.DebugCanvasStrokeColor, dest)
|
// overlap other UI elements!
|
||||||
} else {
|
if dest.X < P.X {
|
||||||
e.DrawPoint(stroke.Color, dest)
|
// Left edge. TODO: right edge
|
||||||
|
delta := P.X - dest.X
|
||||||
|
dest.X = P.X
|
||||||
|
dest.W -= delta
|
||||||
|
}
|
||||||
|
if dest.Y < P.Y {
|
||||||
|
// Top edge. TODO: bottom edge
|
||||||
|
delta := P.Y - dest.Y
|
||||||
|
dest.Y = P.Y
|
||||||
|
dest.H -= delta
|
||||||
|
}
|
||||||
|
|
||||||
|
if balance.DebugCanvasStrokeColor != render.Invisible {
|
||||||
|
e.DrawBox(balance.DebugCanvasStrokeColor, dest)
|
||||||
|
} else {
|
||||||
|
e.DrawBox(stroke.Color, dest)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for point := range stroke.IterPoints() {
|
||||||
|
if !point.Inside(VP) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
dest := render.Point{
|
||||||
|
X: P.X + w.Scroll.X + w.BoxThickness(1) + point.X,
|
||||||
|
Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + point.Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
if balance.DebugCanvasStrokeColor != render.Invisible {
|
||||||
|
e.DrawPoint(balance.DebugCanvasStrokeColor, dest)
|
||||||
|
} else {
|
||||||
|
e.DrawPoint(stroke.Color, dest)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user