WIP Zoom Tool

* Added Feature Flag support, run doodle with --experimental to enable
  all flags. Eraser Tool is behind a feature flag now.
* + and - on the top row of keyboard keys will zoom the drawing in and
  out in Edit Mode. The wallpaper zooms nicely enough, but level
  chunkers need work.
* A View menu is added with Zoom in/out, reset zoom, and scroll to
  origin options. The whole menu is behind the Zoom feature flag.
* Update README with lots of details for fun debug mode options to play
  around with.
loading-screen
Noah 2020-11-19 20:08:38 -08:00
parent 24aef28a0d
commit 6e40d58010
12 changed files with 426 additions and 40 deletions

View File

@ -142,7 +142,9 @@ A brief introduction to the built-in doodads available so far:
# Developer Console
Press `Enter` at any time to open the developer console.
Press `Enter` at any time to open the developer console. The console
provides commands and advanced functionality, and is also where cheat
codes can be entered.
Commands supported:
@ -154,8 +156,8 @@ new
Show the "New Level" screen to start editing a new map.
save [filename]
Save the current map in Edit Mode. The filename is required if the map has
not been saved yet.
Save the current map in Edit Mode. The filename is required
if the map has not been saved yet.
edit [filename]
Open a map or doodad in Edit Mode.
@ -166,14 +168,39 @@ play [filename]
echo <text>
Flash a message to the console.
alert <text>
Test an alert box modal with a custom message.
clear
Clear the console output history.
exit
quit
Close the developer console.
boolProp <property> <true/false>
Toggle certain boolean settings in the game. Most of these
are debugging related. `boolProp list` shows the available
props.
eval <expression>
$ <expression>
Execute a line of JavaScript code in the console. Several
of the game's core data types are available here; `d` is
the master game struct; d.Scene is the pointer to the
current scene. d.Scene.UI.Canvas may point to the level edit
canvas in Editor Mode. Object.keys() can enumerate public
functions and variables.
repl
Enters an interactive JavaScript shell, where the console
stays open and pre-fills a $ prompt for subsequent commands.
```
The JavaScript console is a feature for advanced users and was
used while developing the game. Cool things you can do with it
may be documented elsewhere.
## Cheat Codes
The following cheats can be entered into the developer console.
@ -205,6 +232,64 @@ Experimental:
The player character must always remain on screen though so you can't
scroll too far away.
Unsupported shell commands (here be dragons):
* `reload`: reloads the current 'scene' within the game engine, using the
existing scene's data. If playing a level this will start the level over.
If editing a level this will reload the editor, but your recent unsaved
changes _should_ be left intact.
* `guitest`: loads the GUI Test scene within the game. This was where I
was testing UI widgets early on; not well maintained; the `close`
command can get you out of it.
## Environment Variables
To enable certain debug features or customize some aspects of the game,
run it with environment variables like the following:
```bash
# Draw a semi-transparent yellow background over all level chunks
$ DEBUG_CHUNK_COLOR=FFFF0066 ./doodle
# Set a window size for the application
# (equivalent to: doodle --window 1024x768)
$ DOODLE_W=1024 DOODLE_H=768 ./doodle
# Turn on lots of fun debug features.
$ DEBUG_CANVAS_LABEL=1 DEBUG_CHUNK_COLOR=FFFF00AA \
DEBUG_CANVAS_BORDER=FF0 ./doodle
```
Supported variables include:
* `DOODLE_W` and `DOODLE_H` set the width and height of the application
window. Equivalent to the `--window` command-line option.
* `D_SCROLL_SPEED` (int): tune the canvas scrolling speed. Default might
be around 8 or so.
* `D_DOODAD_SIZE` (int): default size for newly created doodads
* `D_SHELL_BG` (color): set the background color of the developer console
* `D_SHELL_FG` (color): text color for the developer console
* `D_SHELL_PC` (color): color for the shell prompt text
* `D_SHELL_LN` (int): set the number of lines of output history the
console will show. This dictates how 'tall' it rises from the bottom
of the screen. Large values will cover the entire screen with console
whenever the shell is open.
* `D_SHELL_FS` (int): set the font size for the developer shell. Default
is about 16. This also affects the size of "flashed" text that appears
at the bottom of the screen.
* `DEBUG_CHUNK_COLOR` (color): set a background color over each chunk
of drawing (level or doodad). A solid color will completely block out
the wallpaper; semitransparent is best.
* `DEBUG_CANVAS_BORDER` (color): the game will draw an insert colored
border around every "Canvas" widget (drawing) on the screen. The level
itself is a Canvas and every individual Doodad or actor in the level is
its own Canvas.
* `DEBUG_CANVAS_LABEL` (bool): draws a text label over every Canvas
widget on the screen, showing its name or Actor ID and some properties,
such as Level Position (LP) and World Position (WP) of actors within
a level. LP is their placement in the level file and WP is their
actual position now (in case it moves).
# Author
Copyright (C) 2020 Noah Petherbridge. All rights reserved.

View File

@ -77,6 +77,10 @@ func main() {
Name: "guitest",
Usage: "enter the GUI Test scene on startup",
},
&cli.BoolFlag{
Name: "experimental",
Usage: "enable experimental Feature Flags",
},
}
app.Action = func(c *cli.Context) error {
@ -92,6 +96,11 @@ func main() {
}
}
// Enable feature flags?
if c.Bool("experimental") {
balance.FeaturesOn()
}
// SDL engine.
engine := sdl.New(
fmt.Sprintf("%s v%s", branding.AppName, branding.Version),

View File

@ -0,0 +1,15 @@
package balance
// Feature Flags to turn on/off experimental content.
var Feature = feature{
Zoom: false,
}
// FeaturesOn turns on all feature flags, from CLI --experimental option.
func FeaturesOn() {
Feature.Zoom = true
}
type feature struct {
Zoom bool
}

View File

@ -137,7 +137,11 @@ func (d *Doodle) Run() error {
} else {
// Global event handlers.
if keybind.Shutdown(ev) {
d.ConfirmExit()
if d.Debug { // fast exit in -debug mode.
d.running = false
} else {
d.ConfirmExit()
}
continue
}

View File

@ -202,6 +202,23 @@ func (s *EditorScene) Loop(d *Doodle, ev *event.State) error {
s.UI.Canvas.RedoStroke()
}
// Zoom in/out.
if balance.Feature.Zoom {
if keybind.ZoomIn(ev) {
d.Flash("Zoom in")
s.UI.Canvas.Zoom++
} else if keybind.ZoomOut(ev) {
d.Flash("Zoom out")
s.UI.Canvas.Zoom--
} else if keybind.ZoomReset(ev) {
d.Flash("Reset zoom")
s.UI.Canvas.Zoom = 0
} else if keybind.Origin(ev) {
d.Flash("Scrolled back to level origin (0,0)")
s.UI.Canvas.ScrollTo(render.Origin)
}
}
s.UI.Loop(ev)
// Switching to Play Mode?

View File

@ -547,26 +547,43 @@ func (u *EditorUI) SetupMenuBar(d *Doodle) *ui.MenuBar {
editMenu.AddItemAccel("Redo", "Ctrl-Y", func() {
u.Canvas.RedoStroke()
})
editMenu.AddSeparator()
editMenu.AddItem("Level options", func() {
log.Info("Opening the window")
// Open the New Level window in edit-settings mode.
u.levelSettingsWindow.Hide()
u.levelSettingsWindow = nil
u.SetupPopups(u.d)
u.levelSettingsWindow.Show()
})
////////
// Level menu
if u.Scene.DrawingType == enum.LevelDrawing {
levelMenu := menu.AddMenu("Level")
levelMenu.AddItem("Page settings", func() {
log.Info("Opening the window")
// Open the New Level window in edit-settings mode.
u.levelSettingsWindow.Hide()
u.levelSettingsWindow = nil
u.SetupPopups(u.d)
u.levelSettingsWindow.Show()
})
levelMenu.AddItemAccel("Playtest", "P", func() {
u.Scene.Playtest()
})
}
////////
// View menu
if balance.Feature.Zoom {
viewMenu := menu.AddMenu("View")
viewMenu.AddItemAccel("Zoom in", "+", func() {
u.Canvas.Zoom++
})
viewMenu.AddItemAccel("Zoom out", "-", func() {
u.Canvas.Zoom--
})
viewMenu.AddItemAccel("Reset zoom", "1", func() {
u.Canvas.Zoom = 0
})
viewMenu.AddItemAccel("Scroll drawing to origin", "0", func() {
u.Canvas.ScrollTo(render.Origin)
})
}
////////
// Tools menu
toolMenu := menu.AddMenu("Tools")

View File

@ -45,6 +45,26 @@ func Redo(ev *event.State) bool {
return ev.Ctrl && ev.KeyDown("y")
}
// ZoomIn (+)
func ZoomIn(ev *event.State) bool {
return ev.KeyDown("=") || ev.KeyDown("+")
}
// ZoomOut (-)
func ZoomOut(ev *event.State) bool {
return ev.KeyDown("-")
}
// ZoomReset (1)
func ZoomReset(ev *event.State) bool {
return ev.KeyDown("1")
}
// Origin (0) -- scrolls the canvas back to 0,0 in Editor Mode.
func Origin(ev *event.State) bool {
return ev.KeyDown("0")
}
// GotoPlay (P) play tests the current level in the editor.
func GotoPlay(ev *event.State) bool {
return ev.KeyDown("p")

View File

@ -29,6 +29,7 @@ type Canvas struct {
// NewCanvas() with editable=true, they are both enabled.
Editable bool // Clicking will edit pixels of this canvas.
Scrollable bool // Cursor keys will scroll the viewport of this canvas.
Zoom int // Zoom level on the canvas.
// Selected draw tool/mode, default Pencil, for editable canvases.
Tool drawtool.Tool
@ -289,10 +290,17 @@ func (w *Canvas) ViewportRelative() render.Rect {
// the mouse cursor.
func (w *Canvas) WorldIndexAt(screenPixel render.Point) render.Point {
var P = ui.AbsolutePosition(w)
return render.Point{
world := render.Point{
X: screenPixel.X - P.X - w.Scroll.X,
Y: screenPixel.Y - P.Y - w.Scroll.Y,
}
// Handle Zoomies
if w.Zoom != 0 {
world.X = w.ZoomMultiply(world.X)
world.Y = w.ZoomMultiply(world.Y)
}
return world
}
// Chunker returns the underlying Chunker object.

View File

@ -28,6 +28,10 @@ func (w *Canvas) commitStroke(tool drawtool.Tool, addHistory bool) {
return
}
// Zoom the stroke coordinates (this modifies the pointer)
zStroke := w.ZoomStroke(w.currentStroke)
_ = zStroke
// Mark the canvas as modified.
w.modified = true

View File

@ -42,9 +42,16 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
} else {
tex = chunk.Texture(e)
}
// Zoom in the texture.
texSize := tex.Size()
if w.Zoom != 0 {
texSize.W = w.ZoomMultiply(texSize.W)
texSize.H = w.ZoomMultiply(texSize.H)
}
src := render.Rect{
W: tex.Size().W,
H: tex.Size().H,
W: texSize.W,
H: texSize.H,
}
// If the source bitmap is already bigger than the Canvas widget
@ -69,6 +76,16 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
H: src.H,
}
// Zoom the destination rect.
if w.Zoom != 0 {
// dst.X += int(w.GetZoomMultiplier())
// dst.Y += int(w.GetZoomMultiplier())
// dst.X = w.ZoomMultiply(dst.X)
// dst.Y = w.ZoomMultiply(dst.Y)
// dst.W = w.ZoomMultiply(dst.W)
// dst.H = w.ZoomMultiply(dst.H)
}
// TODO: all this shit is in TrimBox(), make it DRY
// If the destination width will cause it to overflow the widget

View File

@ -2,6 +2,7 @@ package uix
import (
"git.kirsle.net/apps/doodle/pkg/level"
"git.kirsle.net/apps/doodle/pkg/log"
"git.kirsle.net/apps/doodle/pkg/wallpaper"
"git.kirsle.net/go/render"
)
@ -70,45 +71,96 @@ func (w *Canvas) loopContainActorsInsideLevel(a *Actor) {
}
// PresentWallpaper draws the wallpaper.
// Point p is the one given to Canvas.Present(), i.e., the position of the
// top-left corner of the Canvas widget relative to the application window.
func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
var (
wp = w.wallpaper
S = w.Size()
size = wp.corner.Size()
wp = w.wallpaper
S = w.Size()
size = wp.corner.Size()
// Get the relative viewport of world coordinates looked at by the canvas.
// The X,Y values are the negative Scroll value
// The W,H values are the Canvas size same as var S above.
Viewport = w.ViewportRelative()
origin = render.Point{
X: p.X + w.Scroll.X + w.BoxThickness(1),
Y: p.Y + w.Scroll.Y + w.BoxThickness(1),
}
limit = render.Point{
// NOTE: we add + the texture size so we would actually draw one
// full extra texture out-of-bounds for the repeating backgrounds.
// This is cuz for scrolling we offset the draw spot on a loop.
X: origin.X + S.W - w.BoxThickness(1) + size.W,
Y: origin.Y + S.H - w.BoxThickness(1) + size.H,
// origin and limit seem to be the boundaries of where on screen
// we are rendering inside.
origin = render.Point{
X: p.X + w.Scroll.X, // + w.BoxThickness(1),
Y: p.Y + w.Scroll.Y, // + w.BoxThickness(1),
}
limit render.Point // TBD later
)
// Grow or shrink the render limit if we're zoomed.
if w.Zoom != 0 {
// I was surprised to discover that just zooming the texture
// quadrant size handled most of the problem! For reference, the
// Blueprint wallpaper has a size of 120x120 for the tiling pattern.
size.H = w.ZoomMultiply(size.H)
size.W = w.ZoomMultiply(size.W)
}
// SCRATCH
// at bootup, scroll position 0,0:
// origin=44,20 p=44,20 p=relative to application window
// scroll right and down to -60,-60:
// origin=-16,-40 p=44,20 and looks good in that direction
// scroll left and up to 60,60:
// origin=104,80 p=44,20
// becomes origin=44,20 p=44,20 d=-16,-40
// the latter case is handled below. walking thru:
// if o(104) > p(44):
// while o(104) > p(44):
// o -= size(120) of texture block
// o is now -16,-40
// while o(-16) > p(44): it's not; break
// dx = o(-16)
// origin.X = p.X
// (becomes origin=44,20 p=44,20 d=-16,-40)
//
// The visual bug is: if you scroll left or up on an Unbounded level from
// the origin (0, 0), the tiling of the wallpaper jumps to the right and
// down by an offset of 44x20 pixels.
//
// what is meant to happen:
// -
// For tiled textures, compute the offset amount. If we are scrolled away
// from the Origin (0,0) we find out by how far (subtract full tile sizes)
// and use the remainder as an offset for drawing the tiles.
// p = position on screen of the Canvas widget
// origin = p.X + Scroll.X, p.Y + scroll.Y
// note: negative Scroll values means to the right and down
var dx, dy int
if origin.X > p.X {
for origin.X > p.X && origin.X > size.W {
origin.X -= size.W
}
// View is scrolled leftward (into negative world coordinates)
dx = origin.X
origin.X = p.X
for dx > p.X {
dx -= size.W
}
origin.X = 0 // note: origin 0,0 will be the corner of the app window
}
if origin.Y > p.Y {
for origin.Y > p.Y && origin.Y > size.H {
origin.Y -= size.H
}
// View is scrolled upward (into negative world coordinates)
dy = origin.Y
origin.Y = p.Y
for dy > p.Y {
dy -= size.H
}
origin.Y = 0
}
// And capping the scroll delta in the other direction.
limit = render.Point{
// NOTE: we add + the texture size so we would actually draw one
// full extra texture out-of-bounds for the repeating backgrounds.
// This is cuz for scrolling we offset the draw spot on a loop.
X: origin.X + S.W + size.W,
Y: origin.Y + S.H + size.H,
}
// And capping the scroll delta in the other direction. Always draw
// pixels until the Canvas size is covered.
if limit.X < S.W {
limit.X = S.W
}
@ -117,10 +169,12 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
limit.Y = S.H
}
// TODO: was still getting some slight flicker on the right and bottom
// when scrolling.. add a bit extra margin.
limit.X += size.W
limit.Y += size.H
// Tile the repeat texture.
// Tile the repeat texture. Start from 1 full wallpaper tile out of bounds
for x := origin.X - size.W; x < limit.X; x += size.W {
for y := origin.Y - size.H; y < limit.Y; y += size.H {
src := render.Rect{
@ -134,8 +188,20 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
H: src.H,
}
// Zoom the output texture.
if w.Zoom != 0 {
// dst.X = w.ZoomMultiply(dst.X - p.X)
// dst.Y = w.ZoomMultiply(dst.Y - p.Y)
// dst.W = w.ZoomMultiply(dst.W)
// dst.H = w.ZoomMultiply(dst.H)
}
// Trim the edges of the destination box, like in canvas.go#Present
odst := dst
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
if dst.W == 0 {
log.Error("TrimBoxed! %s => %s", odst, dst)
}
e.Copy(wp.repeat, src, dst)
}
@ -154,6 +220,15 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
W: src.W,
H: src.H,
}
// Zoom the output texture.
if w.Zoom != 0 {
// dst.X = w.ZoomMultiply(dst.X - origin.X)
// dst.Y = w.ZoomMultiply(dst.Y - origin.Y)
// dst.W = w.ZoomMultiply(dst.W)
// dst.H = w.ZoomMultiply(dst.H)
}
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.left, src, dst)
}
@ -170,6 +245,15 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
W: src.W,
H: src.H,
}
// Zoom the output texture.
if w.Zoom != 0 {
// dst.X = w.ZoomMultiply(dst.X - origin.X)
// dst.Y = w.ZoomMultiply(dst.Y - origin.Y)
// dst.W = w.ZoomMultiply(dst.W)
// dst.H = w.ZoomMultiply(dst.H)
}
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.top, src, dst)
}
@ -186,6 +270,15 @@ func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
W: src.W,
H: src.H,
}
// Zoom the output texture.
if w.Zoom != 0 {
// dst.X = w.ZoomMultiply(dst.X - origin.X)
// dst.Y = w.ZoomMultiply(dst.Y - origin.Y)
// dst.W = w.ZoomMultiply(dst.W)
// dst.H = w.ZoomMultiply(dst.H)
}
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.corner, src, dst)
}

97
pkg/uix/canvas_zoom.go Normal file
View File

@ -0,0 +1,97 @@
package uix
import (
"git.kirsle.net/apps/doodle/pkg/drawtool"
"git.kirsle.net/go/render"
)
// Functions related to the Zoom Tool to magnify the size of the canvas.
/*
GetZoomMultiplier parses the .Zoom integer and returns a multiplier.
Examples:
Zoom = 0: neutral (100% scale, 1x)
Zoom = 1: 2x zoom
Zoom = 2: 4x zoom
Zoom = 3: 8x zoom
Zoom = -1: 0.5x zoom
Zoom = -2: 0.25x zoom
*/
func (w *Canvas) GetZoomMultiplier() float64 {
// Get and bounds cap the zoom setting.
if w.Zoom < -2 {
w.Zoom = -2
} else if w.Zoom > 3 {
w.Zoom = 3
}
// Return the multipliers.
switch w.Zoom {
case -2:
return 0.25
case -1:
return 0.5
case 0:
return 1
case 1:
return 1.5
case 2:
return 2
case 3:
return 2.5
default:
return 1
}
}
/*
ZoomMultiply multiplies a width or height value by the Zoom Multiplier and
returns the modified integer.
Usage is like:
// when building a render.Rect destination box.
dest.W *= ZoomMultiply(dest.W)
dest.H *= ZoomMultiply(dest.H)
*/
func (w *Canvas) ZoomMultiply(value int) int {
return int(float64(value) * w.GetZoomMultiplier())
}
/*
ZoomStroke adjusts a drawn stroke on the canvas to account for the zoom level.
Returns a copy Stroke value without changing the original.
*/
func (w *Canvas) ZoomStroke(stroke *drawtool.Stroke) drawtool.Stroke {
copy := drawtool.Stroke{
ID: stroke.ID,
Shape: stroke.Shape,
Color: stroke.Color,
Thickness: stroke.Thickness,
ExtraData: stroke.ExtraData,
PointA: stroke.PointA,
PointB: stroke.PointB,
Points: stroke.Points,
OriginalPoints: stroke.OriginalPoints,
}
return copy
// Multiply all coordinates in this stroke, which should be World
// Coordinates in the level data, by the zoom multiplier.
adjust := func(p render.Point) render.Point {
p.X = w.ZoomMultiply(p.X)
p.Y = w.ZoomMultiply(p.Y)
return p
}
copy.PointA = adjust(copy.PointA)
copy.PointB = adjust(copy.PointB)
for i := range copy.Points {
copy.Points[i] = adjust(copy.Points[i])
}
return copy
}