Noah Petherbridge
77297fd60d
Two new tools added to the Level Editor: * Pan Tool: left-click to scroll the level around safely. * Text Tool: write text onto your level. Features of the Text Tool: * Can choose from the game's built-in fonts, size and enter the message you want to write. * The mouse cursor previews the text when hovered over the level. * Click to "stamp" the text onto your level. The currently selected color swatch will be used to color the text in. * Adds two new fonts: Azulian.ttf and Rive.ttf that can be selected in the Text Tool. Some implementation notes: * Added package native/engine_sdl.go that handles the lower-level SDL2_TTF logic to rasterize the text into a black&white image. * WASM not supported yet (if the game even still built for WASM); native/engine_wasm.go stubs out the TextToImage() call with a "not supported" error just in case. Other changes: * New Toolbar icons: they are 24x24 instead of 32x32 to make more room for more tools. * The toolbar now shows two buttons per row for a more densely packed layout. For very narrow screen widths (< 600px) the default Vertical Toolbar layout will use one-button-per-row to not eat too much screen real estate. * In the Horizontal Toolbars layout there are 2 buttons per column.
342 lines
9.4 KiB
Go
342 lines
9.4 KiB
Go
package uix
|
|
|
|
import (
|
|
"git.kirsle.net/apps/doodle/pkg/balance"
|
|
"git.kirsle.net/apps/doodle/pkg/drawtool"
|
|
"git.kirsle.net/apps/doodle/pkg/level"
|
|
"git.kirsle.net/apps/doodle/pkg/log"
|
|
"git.kirsle.net/apps/doodle/pkg/pattern"
|
|
"git.kirsle.net/apps/doodle/pkg/shmem"
|
|
"git.kirsle.net/go/render"
|
|
"git.kirsle.net/go/ui"
|
|
)
|
|
|
|
// canvas_strokes.go: functions related to drawtool.Stroke and the Canvas.
|
|
|
|
// AddStroke installs a new Stroke to be superimposed over drawing data
|
|
// in the canvas.
|
|
//
|
|
// The stroke is added to the canvas's map by its ID so it can be removed later.
|
|
// The stroke must have a non-zero ID value set or this function will panic.
|
|
// drawtool.NewStroke() creates an initialized Stroke object to use here.
|
|
func (w *Canvas) AddStroke(stroke *drawtool.Stroke) {
|
|
if stroke.ID == 0 {
|
|
panic("Canvas.AddStroke: the Stroke is missing an ID; was it initialized properly?")
|
|
}
|
|
|
|
w.strokes[stroke.ID] = stroke
|
|
}
|
|
|
|
// RemoveStroke uninstalls a Stroke from the canvas using its ID.
|
|
//
|
|
// Returns true if the stroke existed to begin with, false if not.
|
|
func (w *Canvas) RemoveStroke(stroke *drawtool.Stroke) bool {
|
|
if _, ok := w.strokes[stroke.ID]; ok {
|
|
delete(w.strokes, stroke.ID)
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// UndoStroke rolls back the level's UndoHistory and deletes the pixels last
|
|
// added to the level. Returns false and emits a warning to the log if the
|
|
// canvas has no level loaded properly.
|
|
func (w *Canvas) UndoStroke() bool {
|
|
var undoer *drawtool.History
|
|
if w.level != nil {
|
|
undoer = w.level.UndoHistory
|
|
} else if w.doodad != nil {
|
|
undoer = w.doodad.UndoHistory
|
|
} else {
|
|
log.Error("Canvas.UndoStroke: no Level or Doodad currently available to the canvas")
|
|
return false
|
|
}
|
|
|
|
latest := undoer.Latest()
|
|
if latest != nil {
|
|
// 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 undoer.Undo()
|
|
}
|
|
|
|
// RedoStroke rolls the level's UndoHistory forwards again and replays the
|
|
// recently undone changes.
|
|
func (w *Canvas) RedoStroke() bool {
|
|
var undoer *drawtool.History
|
|
if w.level != nil {
|
|
undoer = w.level.UndoHistory
|
|
} else if w.doodad != nil {
|
|
undoer = w.doodad.UndoHistory
|
|
} else {
|
|
log.Error("Canvas.UndoStroke: no Level or Doodad currently available to the canvas")
|
|
return false
|
|
}
|
|
|
|
ok := undoer.Redo()
|
|
if !ok {
|
|
return false
|
|
}
|
|
|
|
latest := undoer.Latest()
|
|
|
|
// We stored the ActiveSwatch on this stroke as we drew it. Recover it
|
|
// and place the pixels back down.
|
|
w.currentStroke = latest
|
|
w.commitStroke(w.Tool, false)
|
|
|
|
return ok
|
|
}
|
|
|
|
// presentStrokes is called as part of Present() and draws the strokes whose
|
|
// pixels are currently visible within the viewport.
|
|
func (w *Canvas) presentStrokes(e render.Engine) {
|
|
// Turn stroke map into a list.
|
|
var strokes []*drawtool.Stroke
|
|
for _, stroke := range w.strokes {
|
|
strokes = append(strokes, stroke)
|
|
}
|
|
w.DrawStrokes(e, strokes)
|
|
|
|
// Dynamic actor links visible in the ActorTool and LinkTool.
|
|
if w.Tool == drawtool.ActorTool || w.Tool == drawtool.LinkTool {
|
|
w.presentActorLinks(e)
|
|
}
|
|
|
|
// Text Tool preview.
|
|
if w.Tool == drawtool.TextTool && drawtool.TT.Label != nil {
|
|
drawtool.TT.Label.Present(e, shmem.Cursor)
|
|
}
|
|
}
|
|
|
|
// presentActorLinks draws strokes connecting actors together by their links.
|
|
// TODO: the strokes are computed dynamically every tick in here, might be a
|
|
// way to better optimize later.
|
|
func (w *Canvas) presentActorLinks(e render.Engine) {
|
|
var (
|
|
strokes = []*drawtool.Stroke{}
|
|
actorMap = map[string]*Actor{}
|
|
)
|
|
|
|
// Loop over actors and collect linked ones into the map.
|
|
for _, actor := range w.actors {
|
|
if len(actor.Actor.Links) > 0 {
|
|
actorMap[actor.ID()] = actor
|
|
}
|
|
}
|
|
|
|
// If no links, stop.
|
|
if len(actorMap) == 0 {
|
|
return
|
|
}
|
|
|
|
// The glow colored line. Huge hacky block of code but makes for some
|
|
// basic visualization for now.
|
|
var color = balance.LinkLineColor
|
|
var lightenStep = float64(balance.LinkLighten) / 16
|
|
var step = shmem.Tick % balance.LinkAnimSpeed
|
|
if step < 32 {
|
|
for i := uint64(0); i < step; i++ {
|
|
color = color.Lighten(int(lightenStep))
|
|
}
|
|
if step > 16 {
|
|
for i := uint64(0); i < step-16; i++ {
|
|
color = color.Darken(int(lightenStep))
|
|
}
|
|
}
|
|
}
|
|
|
|
// Loop over the linked actors and draw stroke lines.
|
|
for _, actor := range actorMap {
|
|
for _, linkID := range actor.Actor.Links {
|
|
if _, ok := actorMap[linkID]; !ok {
|
|
continue
|
|
}
|
|
|
|
var (
|
|
aP = actor.Position()
|
|
aS = actor.Size()
|
|
bP = actorMap[linkID].Position()
|
|
bS = actorMap[linkID].Size()
|
|
)
|
|
|
|
// Adapt actor positions per the zoom level.
|
|
aP.X = w.ZoomMultiply(aP.X)
|
|
aP.Y = w.ZoomMultiply(aP.Y)
|
|
bP.X = w.ZoomMultiply(bP.X)
|
|
bP.Y = w.ZoomMultiply(bP.Y)
|
|
|
|
// Draw a line connecting the centers of each actor together.
|
|
stroke := drawtool.NewStroke(drawtool.Line, color)
|
|
stroke.PointA = render.Point{
|
|
X: aP.X + (aS.W / 2),
|
|
Y: aP.Y + (aS.H / 2),
|
|
}
|
|
stroke.PointB = render.Point{
|
|
X: bP.X + (bS.W / 2),
|
|
Y: bP.Y + (bS.H / 2),
|
|
}
|
|
|
|
strokes = append(strokes, stroke)
|
|
}
|
|
}
|
|
|
|
w.DrawStrokes(e, strokes)
|
|
}
|
|
|
|
// DrawStrokes is the common base function behind presentStrokes and
|
|
// presentActorLinks to actually draw the lines to the canvas.
|
|
func (w *Canvas) DrawStrokes(e render.Engine, strokes []*drawtool.Stroke) {
|
|
var (
|
|
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 || stroke.Shape == drawtool.Eraser {
|
|
if len(stroke.Points) >= 2 {
|
|
if !stroke.Points[0].Inside(VP) && !stroke.Points[len(stroke.Points)-1].Inside(VP) {
|
|
continue
|
|
}
|
|
}
|
|
} else {
|
|
// TODO: a very long line that starts and ends outside the viewport
|
|
// but passes thru it would disappear when both ends are out of
|
|
// view.
|
|
if !stroke.PointA.Inside(VP) && !stroke.PointB.Inside(VP) {
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Iter the points and draw what's visible.
|
|
if stroke.Thickness > 0 {
|
|
for rect := range stroke.IterThickPoints() {
|
|
if !rect.Intersects(VP) {
|
|
continue
|
|
}
|
|
|
|
// Does the swatch have a pattern to sample?
|
|
color := stroke.Color
|
|
if stroke.Pattern != "" {
|
|
color = pattern.SampleColor(stroke.Pattern, color, rect.Point())
|
|
}
|
|
|
|
// 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,
|
|
}
|
|
|
|
// 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(color, dest)
|
|
}
|
|
}
|
|
} else {
|
|
var color = stroke.Color
|
|
if balance.DebugCanvasStrokeColor != render.Invisible {
|
|
color = balance.DebugCanvasStrokeColor
|
|
}
|
|
|
|
// TODO: iterating all the points is very costly!
|
|
// Strokes don't properly render your Pattern anyway (glitchy mess),
|
|
// so let them just be simple DrawLine etc. calls.
|
|
// This optimization specifically speeds up the Actor Link Lines.
|
|
if stroke.Shape == drawtool.Line {
|
|
var (
|
|
pointA = render.Point{
|
|
X: P.X + w.Scroll.X + w.BoxThickness(1) + stroke.PointA.X,
|
|
Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + stroke.PointA.Y,
|
|
}
|
|
pointB = render.Point{
|
|
X: P.X + w.Scroll.X + w.BoxThickness(1) + stroke.PointB.X,
|
|
Y: P.Y + w.Scroll.Y + w.BoxThickness(1) + stroke.PointB.Y,
|
|
}
|
|
)
|
|
e.DrawLine(color, pointA, pointB)
|
|
continue
|
|
}
|
|
|
|
for point := range stroke.IterPoints() {
|
|
if !point.Inside(VP) {
|
|
continue
|
|
}
|
|
|
|
// Does the swatch have a pattern to sample?
|
|
if stroke.Pattern != "" {
|
|
color = pattern.SampleColor(stroke.Pattern, color, point)
|
|
}
|
|
|
|
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,
|
|
}
|
|
|
|
e.DrawPoint(color, dest)
|
|
}
|
|
}
|
|
}
|
|
}
|