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:
Noah 2019-07-11 19:07:46 -07:00
parent 7317615318
commit cc1e441232
20 changed files with 548 additions and 114 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 709 B

View File

@ -254,6 +254,7 @@ func IterRect(p1, p2 Point) chan Point {
X: TopLeft.X,
Y: BottomRight.Y,
}
dedupe = map[Point]interface{}{}
)
// Trace all four edges and yield it.
@ -268,7 +269,10 @@ func IterRect(p1, p2 Point) chan Point {
}
for _, edge := range edges {
for pt := range IterLine2(edge.A, edge.B) {
generator <- pt
if _, ok := dedupe[pt]; !ok {
generator <- pt
dedupe[pt] = nil
}
}
}

View File

@ -20,6 +20,7 @@ type Label struct {
// Configurable fields for the constructor.
Text string
TextVariable *string
IntVariable *int
Font render.Text
width int32
@ -32,6 +33,7 @@ func NewLabel(c Label) *Label {
w := &Label{
Text: c.Text,
TextVariable: c.TextVariable,
IntVariable: c.IntVariable,
Font: DefaultFont,
}
if !c.Font.IsZero() {
@ -49,6 +51,9 @@ func (w *Label) text() render.Text {
if w.TextVariable != nil {
w.Font.Text = *w.TextVariable
return w.Font
} else if w.IntVariable != nil {
w.Font.Text = fmt.Sprintf("%d", *w.IntVariable)
return w.Font
}
w.Font.Text = w.Text
return w.Font

View File

@ -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.
hovering = append(hovering, child)
} else {

View File

@ -27,6 +27,22 @@ var (
// Size of Undo/Redo history for map editor.
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

View File

@ -52,6 +52,14 @@ var (
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.
DragColor = render.MustHexColor("#0099FF")

View File

@ -113,6 +113,7 @@ func (d *Doodle) Run() error {
// Poll for events.
ev, err := d.Engine.Poll()
shmem.Cursor = render.NewPoint(ev.CursorX.Now, ev.CursorY.Now)
if ev.EnterKey.Now {
log.Info("MainLoop sees enter key now")
}

View File

@ -8,4 +8,5 @@ const (
Freehand Shape = iota
Line
Rectangle
Eraser // not really a shape but communicates the intention
)

View File

@ -18,6 +18,7 @@ type Stroke struct {
ID int // Unique ID per each stroke
Shape Shape
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
// Start and end points for Lines, Rectangles, etc.
@ -27,6 +28,16 @@ type Stroke struct {
// Array of points for Freehand shapes.
Points []render.Point
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
@ -42,6 +53,8 @@ func NewStroke(shape Shape, color render.Color) *Stroke {
// Initialize data structures.
Points: []render.Point{},
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 {
nextStrokeID++
return &Stroke{
ID: nextStrokeID,
Shape: s.Shape,
Color: s.Color,
ID: nextStrokeID,
Shape: s.Shape,
Color: s.Color,
Thickness: s.Thickness,
ExtraData: s.ExtraData,
Points: []render.Point{},
uniqPoint: map[render.Point]interface{}{},
@ -66,6 +81,8 @@ func (s *Stroke) IterPoints() chan render.Point {
ch := make(chan render.Point)
go func() {
switch s.Shape {
case Eraser:
fallthrough
case Freehand:
for _, point := range s.Points {
ch <- point
@ -84,6 +101,23 @@ func (s *Stroke) IterPoints() chan render.Point {
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.
func (s *Stroke) AddPoint(p render.Point) {
if _, ok := s.uniqPoint[p]; ok {

View File

@ -10,6 +10,7 @@ const (
RectTool
ActorTool // drag and move actors
LinkTool
EraserTool
)
var toolNames = []string{
@ -18,6 +19,7 @@ var toolNames = []string{
"Rectangle",
"Doodad", // readable name for ActorTool
"Link",
"Eraser",
}
func (t Tool) String() string {

View File

@ -227,7 +227,6 @@ func (s *EditorScene) LoadLevel(filename string) error {
s.filename = filename
level, err := level.LoadFile(filename)
fmt.Printf("%+v\n", level)
if err != nil {
return fmt.Errorf("EditorScene.LoadLevel(%s): %s", filename, err)
}

View File

@ -216,8 +216,6 @@ func (u *EditorUI) scrollDoodadFrame(rows int) {
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.
var (
maxVisibleHeight = int32(u.d.height - 86)
@ -233,8 +231,6 @@ func (u *EditorUI) scrollDoodadFrame(rows int) {
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 {
u.doodadSkip = maxSkip
}
@ -269,6 +265,5 @@ func (u *EditorUI) scrollDoodadFrame(rows int) {
u.doodadScroller.Configure(ui.Config{
Width: int32(float64(paletteWidth-50) * viewPercent), // TODO: hacky magic number
})
log.Info("v%% = (%d + %d) / %d = %f", rowsBefore, rowsVisible, len(u.doodadRows), viewPercent)
}

View File

@ -3,8 +3,8 @@ package doodle
import (
"git.kirsle.net/apps/doodle/lib/render"
"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/log"
"git.kirsle.net/apps/doodle/pkg/sprites"
)
@ -27,6 +27,18 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
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.
var buttons = []struct {
Value string
@ -38,8 +50,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Icon: "assets/sprites/pencil-tool.png",
Click: func() {
u.Canvas.Tool = drawtool.PencilTool
u.DoodadTab.Hide()
u.PaletteTab.Show()
showSwatchPalette()
d.Flash("Pencil Tool selected.")
},
},
@ -49,8 +60,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Icon: "assets/sprites/line-tool.png",
Click: func() {
u.Canvas.Tool = drawtool.LineTool
u.DoodadTab.Hide()
u.PaletteTab.Show()
showSwatchPalette()
d.Flash("Line Tool selected.")
},
},
@ -60,8 +70,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Icon: "assets/sprites/rect-tool.png",
Click: func() {
u.Canvas.Tool = drawtool.RectTool
u.DoodadTab.Hide()
u.PaletteTab.Show()
showSwatchPalette()
d.Flash("Rectangle Tool selected.")
},
},
@ -71,8 +80,7 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
Icon: "assets/sprites/actor-tool.png",
Click: func() {
u.Canvas.Tool = drawtool.ActorTool
u.PaletteTab.Hide()
u.DoodadTab.Show()
showDoodadPalette()
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",
Click: func() {
u.Canvas.Tool = drawtool.LinkTool
u.PaletteTab.Hide()
u.DoodadTab.Show()
showDoodadPalette()
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 {
button := button
@ -103,7 +128,6 @@ func (u *EditorUI) SetupToolbar(d *Doodle) *ui.Frame {
)
var btnSize int32 = btn.BoxThickness(2) + toolbarSpriteSize
log.Info("BtnSize: %d", btnSize)
btn.Resize(render.NewRect(btnSize, btnSize))
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)
return frame

View File

@ -204,6 +204,23 @@ func (c *Chunker) Set(p render.Point, sw *Swatch) error {
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.
func (c *Chunker) Delete(p render.Point) error {
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)
}
// 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.
func (c *Chunker) ChunkCoordinate(abs render.Point) render.Point {
if c.Size == 0 {

View File

@ -12,6 +12,9 @@ var (
// Tick is incremented by the main game loop each frame.
Tick uint64
// Current position of the cursor relative to the window.
Cursor render.Point
// Current render engine (i.e. SDL2 or HTML5 Canvas)
// The level.Chunk.ToBitmap() uses this to cache a texture image.
CurrentRenderEngine render.Engine

View File

@ -31,7 +31,8 @@ type Canvas struct {
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
// 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
// the palette index of that pixel. Otherwise pixels behave the same and
@ -86,10 +87,7 @@ type Canvas struct {
// mousedown-and-dragging event.
currentStroke *drawtool.Stroke
strokes map[int]*drawtool.Stroke // active stroke mapped by ID
// Tracking pixels while editing. TODO: get rid of pixelHistory?
pixelHistory []*level.Pixel
lastPixel *level.Pixel
lastPixel *level.Pixel
// We inherit the ui.Widget which manages the width and height.
Scroll render.Point // Scroll offset for which parts of canvas are visible.

50
pkg/uix/canvas_cursor.go Normal file
View 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)
}
}

View File

@ -8,6 +8,93 @@ import (
"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.
func (w *Canvas) loopEditable(ev *events.State) error {
// 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 {
case drawtool.PencilTool:
// 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?
if w.currentStroke == nil {
w.currentStroke = drawtool.NewStroke(drawtool.Freehand, w.Palette.ActiveSwatch.Color)
w.currentStroke.Thickness = w.BrushSize
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
w.AddStroke(w.currentStroke)
}
@ -53,18 +149,15 @@ func (w *Canvas) loopEditable(ev *events.State) error {
}
// 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.currentStroke.AddPoint(point)
}
if lastPixel != nil || lastPixel != pixel {
// Draw the pixels in between.
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.pixelHistory = append(w.pixelHistory, pixel)
// Save the pixel in the current stroke.
w.currentStroke.AddPoint(render.Point{
@ -73,22 +166,7 @@ func (w *Canvas) loopEditable(ev *events.State) error {
})
}
} else {
// Mouse released, commit the points to the drawing.
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
w.commitStroke(w.Tool, true)
}
case drawtool.LineTool:
// 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?
if w.currentStroke == nil {
w.currentStroke = drawtool.NewStroke(drawtool.Line, w.Palette.ActiveSwatch.Color)
w.currentStroke.Thickness = w.BrushSize
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
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)
} else {
// Mouse released, commit the points to the drawing.
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
}
w.commitStroke(w.Tool, true)
}
case drawtool.RectTool:
// 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?
if w.currentStroke == nil {
w.currentStroke = drawtool.NewStroke(drawtool.Rectangle, w.Palette.ActiveSwatch.Color)
w.currentStroke.Thickness = w.BrushSize
w.currentStroke.ExtraData = w.Palette.ActiveSwatch
w.currentStroke.PointA = render.NewPoint(cursor.X, cursor.Y)
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)
} else {
// Mouse released, commit the points to the drawing.
if w.currentStroke != nil {
for pt := range render.IterRect(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
w.commitStroke(w.Tool, true)
}
case drawtool.EraserTool:
// Clicking? Log all the pixels while doing so.
if ev.Button1.Now {
// Initialize a new Stroke for this atomic drawing operation?
if w.currentStroke == nil {
// The color is white, will look like white-out that covers the
// wallpaper during the stroke.
w.currentStroke = drawtool.NewStroke(drawtool.Eraser, render.White)
w.currentStroke.Thickness = w.BrushSize
w.AddStroke(w.currentStroke)
}
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:
// See if any of the actors are below the mouse cursor.

View File

@ -132,8 +132,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
}
w.drawActors(e, p)
w.presentStrokes(e)
w.presentCursor(e)
// XXX: Debug, show label in canvas corner.
if balance.DebugCanvasLabel {

View File

@ -48,8 +48,48 @@ func (w *Canvas) UndoStroke() bool {
latest := w.level.UndoHistory.Latest()
if latest != nil {
for point := range latest.IterPoints() {
w.chunks.Delete(point)
// TODO: only single-thickness lines will restore the original color;
// 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()
@ -72,14 +112,8 @@ func (w *Canvas) RedoStroke() bool {
// We stored the ActiveSwatch on this stroke as we drew it. Recover it
// and place the pixels back down.
if swatch, ok := latest.ExtraData.(*level.Swatch); ok {
for point := range latest.IterPoints() {
w.chunks.Set(point, swatch)
}
return true
}
log.Error("Canvas.UndoStroke: undo was successful but no Swatch was stored on the Stroke.ExtraData!")
w.currentStroke = latest
w.commitStroke(w.Tool, false)
return ok
}
@ -153,6 +187,7 @@ func (w *Canvas) presentActorLinks(e render.Engine) {
// Draw a line connecting the centers of each actor together.
stroke := drawtool.NewStroke(drawtool.Line, color)
stroke.Thickness = 1
stroke.PointA = render.Point{
X: aP.X + (aS.W / 2),
Y: aP.Y + (aS.H / 2),
@ -163,16 +198,6 @@ func (w *Canvas) presentActorLinks(e render.Engine) {
}
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.
func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
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
)
for _, stroke := range strokes {
// If none of this stroke is in our viewport, don't waste time
// looping through it.
if stroke.Shape == drawtool.Freehand {
if stroke.Shape == drawtool.Freehand || stroke.Shape == drawtool.Eraser {
if len(stroke.Points) >= 2 {
if !stroke.Points[0].Inside(VP) && !stroke.Points[len(stroke.Points)-1].Inside(VP) {
continue
@ -206,20 +231,58 @@ func (w *Canvas) drawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
}
// Iter the points and draw what's visible.
for point := range stroke.IterPoints() {
if !point.Inside(VP) {
continue
}
if stroke.Thickness > 0 {
for rect := range stroke.IterThickPoints() {
if !rect.Intersects(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,
}
// Destination rectangle to draw to screen, taking into account
// the position of the Canvas itself.
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 {
e.DrawPoint(balance.DebugCanvasStrokeColor, dest)
} else {
e.DrawPoint(stroke.Color, dest)
// Cap the render square so it doesn't leave the Canvas and
// overlap other UI elements!
if dest.X < P.X {
// 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)
}
}
}
}