Wallpapers and Bounded Levels

Implement the Wallpaper system into the levels and the concept of
Bounded and Unbounded levels.

The first wallpaper image is notepad.png which looks like standard ruled
notebook paper. On bounded levels, the top/left edges of the page look
as you would expect and the blue lines tile indefinitely in the positive
directions. On unbounded levels, you only get the repeating blue lines
but not the edge pieces.

A wallpaper is just a rectangular image file. The image is divided into
four equal quadrants to be the Corner, Top, Left and Repeat textures for
the wallpaper. The Repeat texture is ALWAYS used and fills all the empty
space behind the drawing. (Doodads draw with blank canvases as before
because only levels have wallpapers!)

Levels have four options of a "Page Type":
- Unbounded       (default, infinite space)
- NoNegativeSpace (has a top left edge but can grow infinitely)
- Bounded         (has a top left edge and bounded size)
- Bordered        (bounded with bordered texture; NOT IMPLEMENTED!)

The scrollable viewport of a Canvas will respect the wallpaper and page
type settings of a Level loaded into it. That is, if the level has a top
left edge (not Unbounded) you can NOT scroll to see negative coordinates
below (0,0) -- and if the level has a max dimension set, you can't
scroll to see pixels outside those dimensions.

The Canvas property NoLimitScroll=true will override the scroll locking
and let you see outside the bounds, for debugging.

- Default map settings for New Level are now:
  - Page Type: NoNegativeSpace
  - Wallpaper: notepad.png (default)
  - MaxWidth: 2550  (8.5" * 300 ppi)
  - MaxHeight: 3300 ( 11" * 300 ppi)
This commit is contained in:
Noah 2018-10-27 22:22:13 -07:00
parent b4a366baa9
commit bca848d534
17 changed files with 820 additions and 47 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ maps/
bin/ bin/
screenshot-*.png screenshot-*.png
map-*.json map-*.json
pkg/wallpaper/*.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -35,3 +35,7 @@ like `#FF00FF99` for 153 ($99) on the alpha channel.
* `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows * `D_SCROLL_SPEED=8`: Canvas scroll speed when using the keyboard arrows
in the Editor Mode, in pixels per tick. in the Editor Mode, in pixels per tick.
* `D_DOODAD_SIZE=100`: Default size when creating a new Doodad. * `D_DOODAD_SIZE=100`: Default size when creating a new Doodad.
Development booleans for unit tests (set to any non-empty value):
* `T_WALLPAPER_PNG` for pkg/wallpaper to output PNG images.

View File

@ -7,6 +7,8 @@ import (
"git.kirsle.net/apps/doodle" "git.kirsle.net/apps/doodle"
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
"git.kirsle.net/apps/doodle/render/sdl" "git.kirsle.net/apps/doodle/render/sdl"
_ "image/png"
) )
// Build number is the git commit hash. // Build number is the git commit hash.

66
docs/Doodad Scripts.md Normal file
View File

@ -0,0 +1,66 @@
# Doodad Scripting Engine
Some ideas for the scripting engine for Doodads inside your level.
# Architecture
The script will be an "attached file" in the Doodad format as a special file
named "index.js" as the entry point.
Each Doodad will have its `index.js` script loaded into an isolated JS
environment where it can't access any data about other Doodads or anything
user specific. The `main()` function is called so the Doodad script can
set itself up.
The `main()` function should:
* Initialize any state variables the Doodad wants to use in its script.
* Subscribe to callback events that the Doodad is interested in catching.
The script interacts with the Doodle application through an API broker object
(a Go surface area of functions).
# API Broker Interface
```go
type API interface {
// "Self" functions.
SetFrame(frame int) // Set the currently visible frame in this Doodad.
MoveTo(render.Point)
// Game functions.k
EndLevel() // Exit the current level with a victory
/************************************
* Event Handler Callback Functions *
************************************/
// When we become visible on screen or disappear off the screen.
OnVisible()
OnHidden()
// OnEnter: the other Doodad has ENTIRELY entered our box. Or if the other
// doodad is bigger, they have ENTIRELY enveloped ours.
OnEnter(func(other Doodad))
// OnCollide: when we bump into another Doodad.
OnCollide(func(other Doodad))
}
```
## Mockup Script
```javascript
function main() {
console.log("hello world");
// Register event callbacks.
Doodle.OnEnter(onEnter);
}
// onEnter: handle when another Doodad (like the player) completely enters
// the bounding box of our Doodad. Example: a level exit.
function onEnter(other) {
}
```

View File

@ -24,6 +24,7 @@ type EditorScene struct {
DoodadSize int DoodadSize int
UI *EditorUI UI *EditorUI
d *Doodle
// The current level or doodad object being edited, based on the // The current level or doodad object being edited, based on the
// DrawingType. // DrawingType.
@ -43,6 +44,7 @@ func (s *EditorScene) Name() string {
func (s *EditorScene) Setup(d *Doodle) error { func (s *EditorScene) Setup(d *Doodle) error {
// Initialize the user interface. It references the palette and such so it // Initialize the user interface. It references the palette and such so it
// must be initialized after those things. // must be initialized after those things.
s.d = d
s.UI = NewEditorUI(d, s) s.UI = NewEditorUI(d, s)
// Were we given configuration data? // Were we given configuration data?
@ -57,7 +59,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
case enum.LevelDrawing: case enum.LevelDrawing:
if s.Level != nil { if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller") log.Debug("EditorScene.Setup: received level from scene caller")
s.UI.Canvas.LoadLevel(s.Level) s.UI.Canvas.LoadLevel(d.Engine, s.Level)
} else if s.filename != "" && s.OpenFile { } else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename) log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
if err := s.LoadLevel(s.filename); err != nil { if err := s.LoadLevel(s.filename); err != nil {
@ -70,7 +72,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
log.Debug("EditorScene.Setup: initializing a new Level") log.Debug("EditorScene.Setup: initializing a new Level")
s.Level = level.New() s.Level = level.New()
s.Level.Palette = level.DefaultPalette() s.Level.Palette = level.DefaultPalette()
s.UI.Canvas.LoadLevel(s.Level) s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.ScrollTo(render.Origin) s.UI.Canvas.ScrollTo(render.Origin)
s.UI.Canvas.Scrollable = true s.UI.Canvas.Scrollable = true
} }
@ -153,7 +155,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
s.DrawingType = enum.LevelDrawing s.DrawingType = enum.LevelDrawing
s.Level = level s.Level = level
s.UI.Canvas.LoadLevel(s.Level) s.UI.Canvas.LoadLevel(s.d.Engine, s.Level)
// TODO: debug // TODO: debug
for i, actor := range level.Actors { for i, actor := range level.Actors {

View File

@ -48,6 +48,11 @@ func LoadJSON(filename string) (*Level, error) {
return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err) return m, fmt.Errorf("level.LoadJSON: JSON decode error: %s", err)
} }
// Fill in defaults.
if m.Wallpaper == "" {
m.Wallpaper = DefaultWallpaper
}
// Inflate the chunk metadata to map the pixels to their palette indexes. // Inflate the chunk metadata to map the pixels to their palette indexes.
m.Chunker.Inflate(m.Palette) m.Chunker.Inflate(m.Palette)
m.Actors.Inflate() m.Actors.Inflate()

32
level/page_type.go Normal file
View File

@ -0,0 +1,32 @@
package level
// PageType configures the bounds and wallpaper behavior of a Level.
type PageType int
// PageType values.
const (
// Unbounded means the map can grow freely in any direction.
// - Only the repeat texture is used for the wallpaper.
Unbounded PageType = iota
// NoNegativeSpace means the map is bounded at the top left edges.
// - Can't scroll or visit any pixels in negative X,Y coordinates.
// - Wallpaper shows the Corner at 0,0
// - Wallpaper repeats the Top along the Y=0 plane
// - Wallpaper repeats the Left along the X=0 plane
// - The repeat texture fills the rest of the level.
NoNegativeSpace
// Bounded is the same as NoNegativeSpace but the level is imposing a
// maximum cap on the width and height of the level.
// - Can't scroll below X,Y origin at 0,0
// - Can't scroll past the bounded width and height of the level
Bounded
// Bordered is like Bounded except the corner textures are wrapped
// around the other edges of the level too.
// - The wallpaper hoz mirrors Left along the X=Width plane
// - The wallpaper vert mirrors Top along the Y=Width plane
// - The wallpaper 180 rotates the Corner for opposite corners
Bordered
)

View File

@ -8,6 +8,11 @@ import (
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
) )
// Useful variables.
var (
DefaultWallpaper = "notebook.png"
)
// Base provides the common struct keys that are shared between Levels and // Base provides the common struct keys that are shared between Levels and
// Doodads. // Doodads.
type Base struct { type Base struct {
@ -33,6 +38,12 @@ type Level struct {
// properties (solid, fire, slippery, etc.) // properties (solid, fire, slippery, etc.)
Palette *Palette `json:"palette"` Palette *Palette `json:"palette"`
// Page boundaries and wallpaper settings.
PageType PageType `json:"pageType"`
MaxWidth int64 `json:"boundedWidth"` // only if bounded or bordered
MaxHeight int64 `json:"boundedHeight"`
Wallpaper string `json:"wallpaper"`
// Actors keep a list of the doodad instances in this map. // Actors keep a list of the doodad instances in this map.
Actors ActorMap `json:"actors"` Actors ActorMap `json:"actors"`
} }
@ -46,6 +57,11 @@ func New() *Level {
Chunker: NewChunker(balance.ChunkSize), Chunker: NewChunker(balance.ChunkSize),
Palette: &Palette{}, Palette: &Palette{},
Actors: ActorMap{}, Actors: ActorMap{},
PageType: NoNegativeSpace,
Wallpaper: DefaultWallpaper,
MaxWidth: 2550,
MaxHeight: 3300,
} }
} }

76
pkg/wallpaper/texture.go Normal file
View File

@ -0,0 +1,76 @@
package wallpaper
// The methods that deal in cached Textures for Doodle.
import (
"fmt"
"image"
"os"
"git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/render"
"golang.org/x/image/bmp"
)
// CornerTexture returns the Texture.
func (wp *Wallpaper) CornerTexture(e render.Engine) (render.Texturer, error) {
fmt.Println("CornerTex")
if wp.tex.corner == nil {
tex, err := texture(e, wp.corner, wp.Name+"c")
wp.tex.corner = tex
return tex, err
}
return wp.tex.corner, nil
}
// TopTexture returns the Texture.
func (wp *Wallpaper) TopTexture(e render.Engine) (render.Texturer, error) {
if wp.tex.top == nil {
tex, err := texture(e, wp.top, wp.Name+"t")
wp.tex.top = tex
return tex, err
}
return wp.tex.top, nil
}
// LeftTexture returns the Texture.
func (wp *Wallpaper) LeftTexture(e render.Engine) (render.Texturer, error) {
if wp.tex.left == nil {
tex, err := texture(e, wp.left, wp.Name+"l")
wp.tex.left = tex
return tex, err
}
return wp.tex.left, nil
}
// RepeatTexture returns the Texture.
func (wp *Wallpaper) RepeatTexture(e render.Engine) (render.Texturer, error) {
if wp.tex.repeat == nil {
tex, err := texture(e, wp.repeat, wp.Name+"x")
wp.tex.repeat = tex
return tex, err
}
return wp.tex.repeat, nil
}
func texture(e render.Engine, img *image.RGBA, name string) (render.Texturer, error) {
filename := userdir.CacheFilename("wallpaper", name+".bmp")
if _, err := os.Stat(filename); os.IsNotExist(err) {
fh, err := os.Create(filename)
if err != nil {
return nil, fmt.Errorf("CornerTexture: %s", err.Error())
}
defer fh.Close()
err = bmp.Encode(fh, img)
if err != nil {
return nil, fmt.Errorf("CornerTexture: bmp.Encode: %s", err.Error())
}
}
texture, err := e.NewBitmap(filename)
if err != nil {
return nil, fmt.Errorf("CornerTexture: NewBitmap(%s): %s", filename, err.Error())
}
return texture, nil
}

143
pkg/wallpaper/wallpaper.go Normal file
View File

@ -0,0 +1,143 @@
package wallpaper
import (
"image"
"image/draw"
"os"
"path/filepath"
"strings"
"git.kirsle.net/apps/doodle/render"
)
// Wallpaper is a repeatable background image to go behind levels.
type Wallpaper struct {
Name string
Format string // image file format
Image *image.RGBA
// Parsed values.
quarterWidth int
quarterHeight int
// The four parsed images.
corner *image.RGBA // Top Left corner
top *image.RGBA // Top repeating
left *image.RGBA // Left repeating
repeat *image.RGBA // Main repeating
// Cached textures.
tex struct {
corner render.Texturer
top render.Texturer
left render.Texturer
repeat render.Texturer
}
}
// FromImage creates a Wallpaper from an image.Image.
// If the renger.Engine is nil it will compute images but not pre-cache any
// textures yet.
func FromImage(e render.Engine, img *image.RGBA, name string) (*Wallpaper, error) {
wp := &Wallpaper{
Name: name,
Image: img,
}
wp.cache(e)
return wp, nil
}
// FromFile creates a Wallpaper from a file on disk.
// If the renger.Engine is nil it will compute images but not pre-cache any
// textures yet.
func FromFile(e render.Engine, filename string) (*Wallpaper, error) {
fh, err := os.Open(filename)
if err != nil {
return nil, err
}
img, format, err := image.Decode(fh)
if err != nil {
return nil, err
}
// Ugly hack: make it an image.RGBA because the thing we get tends to be
// an image.Paletted, UGH!
var b = img.Bounds()
rgba := image.NewRGBA(b)
for x := b.Min.X; x < b.Max.X; x++ {
for y := b.Min.Y; y < b.Max.Y; y++ {
rgba.Set(x, y, img.At(x, y))
}
}
wp := &Wallpaper{
Name: strings.Split(filepath.Base(filename), ".")[0],
Format: format,
Image: rgba,
}
wp.cache(e)
return wp, nil
}
// cache the bitmap images.
func (wp *Wallpaper) cache(e render.Engine) {
// Zero-bound the rect cuz an image.Rect doesn't necessarily contain 0,0
var rect = wp.Image.Bounds()
if rect.Min.X < 0 {
rect.Max.X += rect.Min.X
rect.Min.X = 0
}
if rect.Min.Y < 0 {
rect.Max.Y += rect.Min.Y
rect.Min.Y = 0
}
// Our quarter rect size.
wp.quarterWidth = int(float64((rect.Max.X - rect.Min.X) / 2))
wp.quarterHeight = int(float64((rect.Max.Y - rect.Min.Y) / 2))
quarter := image.Rect(0, 0, wp.quarterWidth, wp.quarterHeight)
// Slice the image into the four corners.
slice := func(dx, dy int) *image.RGBA {
slice := image.NewRGBA(quarter)
draw.Draw(
slice,
image.Rect(0, 0, wp.quarterWidth, wp.quarterHeight),
wp.Image,
image.Point{dx, dy},
draw.Over,
)
return slice
}
wp.corner = slice(0, 0)
wp.top = slice(wp.quarterWidth, 0)
wp.left = slice(0, wp.quarterHeight)
wp.repeat = slice(wp.quarterWidth, wp.quarterHeight)
}
// QuarterSize returns the width and height of the quarter images.
func (wp *Wallpaper) QuarterSize() (int, int) {
return wp.quarterWidth, wp.quarterHeight
}
// Corner returns the top left corner image.
func (wp *Wallpaper) Corner() *image.RGBA {
return wp.corner
}
// Top returns the top repeating image.
func (wp *Wallpaper) Top() *image.RGBA {
return wp.top
}
// Left returns the left repeating image.
func (wp *Wallpaper) Left() *image.RGBA {
return wp.left
}
// Repeat returns the main repeating image.
func (wp *Wallpaper) Repeat() *image.RGBA {
return wp.repeat
}

View File

@ -0,0 +1,111 @@
package wallpaper
import (
"fmt"
"image"
"image/color"
"image/draw"
"image/png"
"os"
"testing"
)
func TestWallpaper(t *testing.T) {
var testFunc = func(width, height int) {
var (
qWidth = width / 2
qHeight = height / 2
red = color.RGBA{255, 0, 0, 255}
green = color.RGBA{0, 255, 0, 255}
blue = color.RGBA{0, 0, 255, 255}
pink = color.RGBA{255, 0, 255, 255}
)
// Create a dummy image that is width*height and has the four
// quadrants laid out as solid colors:
// Red | Green
// Blue | Pink
img := image.NewRGBA(image.Rect(0, 0, width, height))
draw.Draw(
// Corner: red
img, // dst Image
image.Rect(0, 0, qWidth, qHeight), // r Rectangle
image.NewUniform(red), // src Image
image.Point{0, 0}, // sp Point
draw.Over, // op Op
)
draw.Draw(
// Top: green
img,
image.Rect(qWidth, 0, width, qHeight),
image.NewUniform(green),
image.Point{qWidth, 0},
draw.Over,
)
draw.Draw(
// Left: blue
img,
image.Rect(0, qHeight, qWidth, height),
image.NewUniform(blue),
image.Point{0, qHeight},
draw.Over,
)
draw.Draw(
// Repeat: pink
img,
image.Rect(qWidth, qHeight, width, height),
image.NewUniform(pink),
image.Point{qWidth, qHeight},
draw.Over,
)
// Output as png to disk if you wanna see what's in it.
if os.Getenv("T_WALLPAPER_PNG") != "" {
fn := fmt.Sprintf("test-%dx%d.png", width, height)
if fh, err := os.Create(fn); err == nil {
defer fh.Close()
if err := png.Encode(fh, img); err != nil {
t.Errorf("err: %s", err)
}
}
}
wp, err := FromImage(nil, img, "dummy")
if err != nil {
t.Errorf("Couldn't create FromImage: %s", err)
t.FailNow()
}
// Check the quarter size is what we expected.
w, h := wp.QuarterSize()
if w != qWidth || h != qHeight {
t.Errorf(
"Got wrong quarter size: expected %dx%d but got %dx%d",
qWidth, qHeight,
w, h,
)
}
// Test the colors.
testColor := func(name string, img *image.RGBA, expect color.RGBA) {
if actual := img.At(5, 5); actual != expect {
t.Errorf(
"%s: expected color %v but got %v",
name,
expect,
actual,
)
}
}
testColor("Corner", wp.Corner(), red)
testColor("Top", wp.Top(), green)
testColor("Left", wp.Left(), blue)
testColor("Repeat", wp.Repeat(), pink)
}
testFunc(128, 128)
testFunc(128, 64)
testFunc(64, 128)
testFunc(12, 12)
testFunc(57, 39)
}

View File

@ -18,6 +18,7 @@ type PlayScene struct {
Level *level.Level Level *level.Level
// Private variables. // Private variables.
d *Doodle
drawing *uix.Canvas drawing *uix.Canvas
// Player character // Player character
@ -31,6 +32,7 @@ func (s *PlayScene) Name() string {
// Setup the play scene. // Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error { func (s *PlayScene) Setup(d *Doodle) error {
s.d = d
s.drawing = uix.NewCanvas(balance.ChunkSize, false) s.drawing = uix.NewCanvas(balance.ChunkSize, false)
s.drawing.MoveTo(render.Origin) s.drawing.MoveTo(render.Origin)
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height))) s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
@ -39,7 +41,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
// Given a filename or map data to play? // Given a filename or map data to play?
if s.Level != nil { if s.Level != nil {
log.Debug("PlayScene.Setup: received level from scene caller") log.Debug("PlayScene.Setup: received level from scene caller")
s.drawing.LoadLevel(s.Level) s.drawing.LoadLevel(d.Engine, s.Level)
} else if s.Filename != "" { } else if s.Filename != "" {
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename) log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
s.LoadLevel(s.Filename) s.LoadLevel(s.Filename)
@ -50,7 +52,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
if s.Level == nil { if s.Level == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid") log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
s.Level = level.New() s.Level = level.New()
s.drawing.LoadLevel(s.Level) s.drawing.LoadLevel(d.Engine, s.Level)
} }
d.Flash("Entered Play Mode. Press 'E' to edit this map.") d.Flash("Entered Play Mode. Press 'E' to edit this map.")
@ -140,7 +142,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
} }
s.Level = level s.Level = level
s.drawing.LoadLevel(s.Level) s.drawing.LoadLevel(s.d.Engine, s.Level)
return nil return nil
} }

69
render/functions.go Normal file
View File

@ -0,0 +1,69 @@
package render
// TrimBox helps with Engine.Copy() to trim a destination box so that it
// won't overflow with the parent container.
func TrimBox(src, dst *Rect, p Point, S Rect, thickness int32) {
// Constrain source width to not bigger than Canvas width.
if src.W > S.W {
src.W = S.W
}
if src.H > S.H {
src.H = S.H
}
// If the destination width will cause it to overflow the widget
// box, trim off the right edge of the destination rect.
//
// Keep in mind we're dealing with chunks here, and a chunk is
// a small part of the image. Example:
// - Canvas is 800x600 (S.W=800 S.H=600)
// - Chunk wants to render at 790,0 width 100,100 or whatever
// dst={790, 0, 100, 100}
// - Chunk box would exceed 800px width (X=790 + W=100 == 890)
// - Find the delta how much it exceeds as negative (800 - 890 == -90)
// - Lower the Source and Dest rects by that delta size so they
// stay proportional and don't scale or anything dumb.
if dst.X+src.W > p.X+S.W {
// NOTE: delta is a negative number,
// so it will subtract from the width.
delta := (p.X + S.W - thickness) - (dst.W + dst.X)
src.W += delta
dst.W += delta
}
if dst.Y+src.H > p.Y+S.H {
// NOTE: delta is a negative number
delta := (p.Y + S.H - thickness) - (dst.H + dst.Y)
src.H += delta
dst.H += delta
}
// The same for the top left edge, so the drawings don't overlap
// menu bars or left side toolbars.
// - Canvas was placed 80px from the left of the screen.
// Canvas.MoveTo(80, 0)
// - A texture wants to draw at 60, 0 which would cause it to
// overlap 20 pixels into the left toolbar. It needs to be cropped.
// - The delta is: p.X=80 - dst.X=60 == 20
// - Set destination X to p.X to constrain it there: 20
// - Subtract the delta from destination W so we don't scale it.
// - Add 20 to X of the source: the left edge of source is not visible
if dst.X < p.X {
// NOTE: delta is a positive number,
// so it will add to the destination coordinates.
delta := p.X - dst.X
dst.X = p.X + thickness
dst.W -= delta
src.X += delta
}
if dst.Y < p.Y {
delta := p.Y - dst.Y
dst.Y = p.Y + thickness
dst.H -= delta
src.Y += delta
}
// Trim the destination width so it doesn't overlap the Canvas border.
if dst.W >= S.W-thickness {
dst.W = S.W - thickness
}
}

View File

@ -206,6 +206,11 @@ func (s *Shell) Parse(input string) Command {
// Draw the shell. // Draw the shell.
func (s *Shell) Draw(d *Doodle, ev *events.State) error { func (s *Shell) Draw(d *Doodle, ev *events.State) error {
// Compute the line height we can draw.
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
// If the console is open, draw the console.
if s.Open {
if ev.EscapeKey.Read() { if ev.EscapeKey.Read() {
s.Close() s.Close()
return nil return nil
@ -246,11 +251,6 @@ func (s *Shell) Draw(d *Doodle, ev *events.State) error {
} }
// Compute the line height we can draw.
lineHeight := balance.ShellFontSize + int(balance.ShellPadding)
// If the console is open, draw the console.
if s.Open {
// Cursor flip? // Cursor flip?
if d.ticks > s.cursorFlip { if d.ticks > s.cursorFlip {
s.cursorFlip = d.ticks + s.cursorRate s.cursorFlip = d.ticks + s.cursorRate

View File

@ -2,6 +2,7 @@ package uix
import ( import (
"fmt" "fmt"
"os"
"strings" "strings"
"git.kirsle.net/apps/doodle/balance" "git.kirsle.net/apps/doodle/balance"
@ -9,6 +10,7 @@ import (
"git.kirsle.net/apps/doodle/events" "git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level" "git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/pkg/userdir" "git.kirsle.net/apps/doodle/pkg/userdir"
"git.kirsle.net/apps/doodle/pkg/wallpaper"
"git.kirsle.net/apps/doodle/render" "git.kirsle.net/apps/doodle/render"
"git.kirsle.net/apps/doodle/ui" "git.kirsle.net/apps/doodle/ui"
) )
@ -32,6 +34,10 @@ type Canvas struct {
// to remove the mask. // to remove the mask.
MaskColor render.Color MaskColor render.Color
// Debug tools
// NoLimitScroll suppresses the scroll limit for bounded levels.
NoLimitScroll bool
// Underlying chunk data for the drawing. // Underlying chunk data for the drawing.
chunks *level.Chunker chunks *level.Chunker
@ -39,6 +45,9 @@ type Canvas struct {
actor *level.Actor // if this canvas IS an actor actor *level.Actor // if this canvas IS an actor
actors []*Actor actors []*Actor
// Wallpaper settings.
wallpaper *Wallpaper
// When the Canvas wants to delete Actors, but ultimately it is upstream // When the Canvas wants to delete Actors, but ultimately it is upstream
// that controls the actors. Upstream should delete them and then reinstall // that controls the actors. Upstream should delete them and then reinstall
// the actor list from scratch. // the actor list from scratch.
@ -70,6 +79,7 @@ func NewCanvas(size int, editable bool) *Canvas {
Palette: level.NewPalette(), Palette: level.NewPalette(),
chunks: level.NewChunker(size), chunks: level.NewChunker(size),
actors: make([]*Actor, 0), actors: make([]*Actor, 0),
wallpaper: &Wallpaper{},
} }
w.setup() w.setup()
w.IDFunc(func() string { w.IDFunc(func() string {
@ -101,8 +111,27 @@ func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
} }
// LoadLevel initializes a Canvas from a Level object. // LoadLevel initializes a Canvas from a Level object.
func (w *Canvas) LoadLevel(level *level.Level) { func (w *Canvas) LoadLevel(e render.Engine, level *level.Level) {
w.Load(level.Palette, level.Chunker) w.Load(level.Palette, level.Chunker)
// TODO: wallpaper paths
filename := "assets/wallpapers/" + level.Wallpaper
if _, err := os.Stat(filename); os.IsNotExist(err) {
log.Error("LoadLevel: %s", err)
filename = "assets/wallpapers/notebook.png" // XXX TODO
}
wp, err := wallpaper.FromFile(e, filename)
if err != nil {
log.Error("wallpaper FromFile(%s): %s", filename, err)
}
w.wallpaper.maxWidth = level.MaxWidth
w.wallpaper.maxHeight = level.MaxHeight
err = w.wallpaper.Load(e, level.PageType, wp)
if err != nil {
log.Error("wallpaper Load: %s", err)
}
} }
// LoadDoodad initializes a Canvas from a Doodad object. // LoadDoodad initializes a Canvas from a Doodad object.
@ -274,6 +303,44 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
H: S.H - w.BoxThickness(2), H: S.H - w.BoxThickness(2),
}) })
// Constrain the scroll view if the level is bounded.
if w.Scrollable && !w.NoLimitScroll {
// Constrain the top and left edges.
if w.wallpaper.pageType > level.Unbounded {
if w.Scroll.X > 0 {
w.Scroll.X = 0
}
if w.Scroll.Y > 0 {
w.Scroll.Y = 0
}
}
// Constrain the bottom and right for limited world sizes.
if w.wallpaper.maxWidth > 0 && w.wallpaper.maxHeight > 0 {
var (
// TODO: downcast from int64!
mw = int32(w.wallpaper.maxWidth)
mh = int32(w.wallpaper.maxHeight)
)
if Viewport.W > mw {
delta := Viewport.W - mw
w.Scroll.X += delta
}
if Viewport.H > mh {
delta := Viewport.H - mh
w.Scroll.Y += delta
}
}
}
// Draw the wallpaper.
if w.wallpaper.Valid() {
err := w.PresentWallpaper(e, p)
if err != nil {
log.Error(err.Error())
}
}
// Get the chunks in the viewport and cache their textures. // Get the chunks in the viewport and cache their textures.
for coord := range w.chunks.IterViewportChunks(Viewport) { for coord := range w.chunks.IterViewportChunks(Viewport) {
if chunk, ok := w.chunks.GetChunk(coord); ok { if chunk, ok := w.chunks.GetChunk(coord); ok {
@ -310,6 +377,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
H: src.H, H: src.H,
} }
// TODO: all this shit is in TrimBox(), make it DRY
// If the destination width will cause it to overflow the widget // If the destination width will cause it to overflow the widget
// box, trim off the right edge of the destination rect. // box, trim off the right edge of the destination rect.
// //

175
uix/canvas_wallpaper.go Normal file
View File

@ -0,0 +1,175 @@
package uix
import (
"git.kirsle.net/apps/doodle/level"
"git.kirsle.net/apps/doodle/pkg/wallpaper"
"git.kirsle.net/apps/doodle/render"
)
// Wallpaper configures the wallpaper in a Canvas.
type Wallpaper struct {
pageType level.PageType
maxWidth int64
maxHeight int64
corner render.Texturer
top render.Texturer
left render.Texturer
repeat render.Texturer
}
// Valid returns whether the Wallpaper is configured. Only Levels should
// have wallpapers and Doodads will have nil ones.
func (wp *Wallpaper) Valid() bool {
return wp.repeat != nil
}
// PresentWallpaper draws the wallpaper.
func (w *Canvas) PresentWallpaper(e render.Engine, p render.Point) error {
var (
wp = w.wallpaper
S = w.Size()
size = wp.corner.Size()
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,
}
)
// 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.
var dx, dy int32
if origin.X > 0 {
for origin.X > 0 && origin.X > size.W {
origin.X -= size.W
}
dx = origin.X
origin.X = 0
}
if origin.Y > 0 {
for origin.Y > 0 && origin.Y > size.H {
origin.Y -= size.H
}
dy = origin.Y
origin.Y = 0
}
// And capping the scroll delta in the other direction.
if limit.X < S.W {
limit.X = S.W
}
if limit.Y < S.H {
// TODO: slight flicker on bottom edge when scrolling down
limit.Y = S.H
}
// Tile the repeat texture.
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{
W: size.W,
H: size.H,
}
dst := render.Rect{
X: x + dx,
Y: y + dy,
W: src.W,
H: src.H,
}
// Trim the edges of the destination box, like in canvas.go#Present
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.repeat, src, dst)
}
}
// The left edge corner tiled along the left edge.
if wp.pageType > level.Unbounded {
for y := origin.Y; y < limit.Y; y += size.H {
src := render.Rect{
W: size.W,
H: size.H,
}
dst := render.Rect{
X: origin.X,
Y: y + dy,
W: src.W,
H: src.H,
}
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.left, src, dst)
}
// The top edge tiled along the top edge.
for x := origin.X; x < limit.X; x += size.W {
src := render.Rect{
W: size.W,
H: size.H,
}
dst := render.Rect{
X: x,
Y: origin.Y,
W: src.W,
H: src.H,
}
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.top, src, dst)
}
// The top left corner for all page types except Unbounded.
if Viewport.Intersects(size) {
src := render.Rect{
W: size.W,
H: size.H,
}
dst := render.Rect{
X: origin.X,
Y: origin.Y,
W: src.W,
H: src.H,
}
render.TrimBox(&src, &dst, p, S, w.BoxThickness(1))
e.Copy(wp.corner, src, dst)
}
}
return nil
}
// Load the wallpaper settings from a level.
func (wp *Wallpaper) Load(e render.Engine, pageType level.PageType, v *wallpaper.Wallpaper) error {
wp.pageType = pageType
if tex, err := v.CornerTexture(e); err == nil {
wp.corner = tex
} else {
return err
}
if tex, err := v.TopTexture(e); err == nil {
wp.top = tex
} else {
return err
}
if tex, err := v.LeftTexture(e); err == nil {
wp.left = tex
} else {
return err
}
if tex, err := v.RepeatTexture(e); err == nil {
wp.repeat = tex
} else {
return err
}
return nil
}