Merge branch 'master' into dev

pull/1/head
Noah 2018-10-28 12:50:48 -07:00
commit 93bdaa0c43
17 changed files with 820 additions and 47 deletions

1
.gitignore vendored
View File

@ -3,3 +3,4 @@ maps/
bin/
screenshot-*.png
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
in the Editor Mode, in pixels per tick.
* `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/balance"
"git.kirsle.net/apps/doodle/render/sdl"
_ "image/png"
)
// 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
UI *EditorUI
d *Doodle
// The current level or doodad object being edited, based on the
// DrawingType.
@ -43,6 +44,7 @@ func (s *EditorScene) Name() string {
func (s *EditorScene) Setup(d *Doodle) error {
// Initialize the user interface. It references the palette and such so it
// must be initialized after those things.
s.d = d
s.UI = NewEditorUI(d, s)
// Were we given configuration data?
@ -57,7 +59,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
case enum.LevelDrawing:
if s.Level != nil {
log.Debug("EditorScene.Setup: received level from scene caller")
s.UI.Canvas.LoadLevel(s.Level)
s.UI.Canvas.LoadLevel(d.Engine, s.Level)
s.UI.Canvas.InstallActors(s.Level.Actors)
} else if s.filename != "" && s.OpenFile {
log.Debug("EditorScene.Setup: Loading map from filename at %s", s.filename)
@ -71,7 +73,7 @@ func (s *EditorScene) Setup(d *Doodle) error {
log.Debug("EditorScene.Setup: initializing a new Level")
s.Level = level.New()
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.Scrollable = true
}
@ -154,7 +156,7 @@ func (s *EditorScene) LoadLevel(filename string) error {
s.DrawingType = enum.LevelDrawing
s.Level = level
s.UI.Canvas.LoadLevel(s.Level)
s.UI.Canvas.LoadLevel(s.d.Engine, s.Level)
// TODO: debug
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)
}
// Fill in defaults.
if m.Wallpaper == "" {
m.Wallpaper = DefaultWallpaper
}
// Inflate the chunk metadata to map the pixels to their palette indexes.
m.Chunker.Inflate(m.Palette)
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"
)
// Useful variables.
var (
DefaultWallpaper = "notebook.png"
)
// Base provides the common struct keys that are shared between Levels and
// Doodads.
type Base struct {
@ -33,6 +38,12 @@ type Level struct {
// properties (solid, fire, slippery, etc.)
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 ActorMap `json:"actors"`
}
@ -46,6 +57,11 @@ func New() *Level {
Chunker: NewChunker(balance.ChunkSize),
Palette: &Palette{},
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

@ -19,6 +19,7 @@ type PlayScene struct {
Level *level.Level
// Private variables.
d *Doodle
drawing *uix.Canvas
// Player character
@ -32,6 +33,7 @@ func (s *PlayScene) Name() string {
// Setup the play scene.
func (s *PlayScene) Setup(d *Doodle) error {
s.d = d
s.drawing = uix.NewCanvas(balance.ChunkSize, false)
s.drawing.MoveTo(render.Origin)
s.drawing.Resize(render.NewRect(int32(d.width), int32(d.height)))
@ -40,7 +42,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
// Given a filename or map data to play?
if s.Level != nil {
log.Debug("PlayScene.Setup: received level from scene caller")
s.drawing.LoadLevel(s.Level)
s.drawing.LoadLevel(s.d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors)
} else if s.Filename != "" {
log.Debug("PlayScene.Setup: loading map from file %s", s.Filename)
@ -53,7 +55,7 @@ func (s *PlayScene) Setup(d *Doodle) error {
if s.Level == nil {
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
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.")
@ -161,7 +163,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
}
s.Level = level
s.drawing.LoadLevel(s.Level)
s.drawing.LoadLevel(s.d.Engine, s.Level)
s.drawing.InstallActors(s.Level.Actors)
s.drawing.AddActor(s.Player)

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,51 +206,51 @@ func (s *Shell) Parse(input string) Command {
// Draw the shell.
func (s *Shell) Draw(d *Doodle, ev *events.State) error {
if ev.EscapeKey.Read() {
s.Close()
return nil
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
s.Execute(s.Text)
// Auto-close the console unless in REPL mode.
if !s.Repl {
s.Close()
}
return nil
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
// Paging through history.
if !s.historyPaging {
s.historyPaging = true
s.historyIndex = len(s.History)
}
// Consume the inputs and make convenient variables.
ev.Down.Read()
isUp := ev.Up.Read()
// Scroll through the input history.
if isUp {
s.historyIndex--
if s.historyIndex < 0 {
s.historyIndex = 0
}
} else {
s.historyIndex++
if s.historyIndex >= len(s.History) {
s.historyIndex = len(s.History) - 1
}
}
s.Text = s.History[s.historyIndex]
}
// 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() {
s.Close()
return nil
} else if ev.EnterKey.Read() || ev.EscapeKey.Read() {
s.Execute(s.Text)
// Auto-close the console unless in REPL mode.
if !s.Repl {
s.Close()
}
return nil
} else if (ev.Up.Now || ev.Down.Now) && len(s.History) > 0 {
// Paging through history.
if !s.historyPaging {
s.historyPaging = true
s.historyIndex = len(s.History)
}
// Consume the inputs and make convenient variables.
ev.Down.Read()
isUp := ev.Up.Read()
// Scroll through the input history.
if isUp {
s.historyIndex--
if s.historyIndex < 0 {
s.historyIndex = 0
}
} else {
s.historyIndex++
if s.historyIndex >= len(s.History) {
s.historyIndex = len(s.History) - 1
}
}
s.Text = s.History[s.historyIndex]
}
// Cursor flip?
if d.ticks > s.cursorFlip {
s.cursorFlip = d.ticks + s.cursorRate

View File

@ -2,6 +2,7 @@ package uix
import (
"fmt"
"os"
"strings"
"git.kirsle.net/apps/doodle/balance"
@ -9,6 +10,7 @@ import (
"git.kirsle.net/apps/doodle/events"
"git.kirsle.net/apps/doodle/level"
"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/ui"
)
@ -32,6 +34,10 @@ type Canvas struct {
// to remove the mask.
MaskColor render.Color
// Debug tools
// NoLimitScroll suppresses the scroll limit for bounded levels.
NoLimitScroll bool
// Underlying chunk data for the drawing.
chunks *level.Chunker
@ -39,6 +45,9 @@ type Canvas struct {
actor *level.Actor // if this canvas IS an actor
actors []*Actor
// Wallpaper settings.
wallpaper *Wallpaper
// When the Canvas wants to delete Actors, but ultimately it is upstream
// that controls the actors. Upstream should delete them and then reinstall
// the actor list from scratch.
@ -64,6 +73,7 @@ func NewCanvas(size int, editable bool) *Canvas {
Palette: level.NewPalette(),
chunks: level.NewChunker(size),
actors: make([]*Actor, 0),
wallpaper: &Wallpaper{},
}
w.setup()
w.IDFunc(func() string {
@ -95,8 +105,27 @@ func (w *Canvas) Load(p *level.Palette, g *level.Chunker) {
}
// 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)
// 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.
@ -262,6 +291,44 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
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.
for coord := range w.chunks.IterViewportChunks(Viewport) {
if chunk, ok := w.chunks.GetChunk(coord); ok {
@ -298,6 +365,8 @@ func (w *Canvas) Present(e render.Engine, p render.Point) {
H: src.H,
}
// TODO: all this shit is in TrimBox(), make it DRY
// If the destination width will cause it to overflow the widget
// 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
}