Port over code from old collision dev PR
This commit is contained in:
parent
8fc4f39da0
commit
5c08577214
34
Ideas.md
34
Ideas.md
|
@ -222,11 +222,14 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
|
|||
* The texture file will be a square (rectangular maybe ok) with four quadrants
|
||||
from which the textures will be extracted. For example if the overall image
|
||||
size was 100x100 pixels, it will be divided into the four 50x50 quadrants.
|
||||
1. `TL`: Top left corner is the top left edge of the "page" the level is on
|
||||
2. `TR`: Top right corner is the repeated "top of page" texture.
|
||||
3. `BL`: Bottom left corner is the repeated "left of page" texture.
|
||||
4. `BR`: Bottom right corner is the repeated background texture that extends
|
||||
1. `Corner`: Top left corner is the top left edge of the "page" the level is on
|
||||
2. `Top`: Top right corner is the repeated "top of page" texture.
|
||||
3. `Left`: Bottom left corner is the repeated "left of page" texture.
|
||||
4. `Repeat`: Bottom right corner is the repeated background texture that extends
|
||||
infinitely in all directions.
|
||||
* The Repeat texture is used all the time, and the other three are used when the
|
||||
level type has boundaries (on the top and left edges in particular) to draw
|
||||
decorative borders instead of the Repeat texture.
|
||||
* Levels will be able to choose a "page type" which controls how the wallpaper
|
||||
will be drawn and how the level boundaries may be constrained. There will be
|
||||
four options:
|
||||
|
@ -241,8 +244,9 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
|
|||
wall.
|
||||
3. **Bounded:** The map has a fixed width and height and is bounded on all
|
||||
four edges.
|
||||
4. **Bounded, Mirrored Wallpaper:** same as Bounded but with a different
|
||||
wallpaper behavior.
|
||||
4. **Bordered:** same as Bounded but with a different wallpaper behavior.
|
||||
The bottom and right edges are covered with mirror images of the top and
|
||||
left edges.
|
||||
* The page types will have their own behaviors with how wallpapers are drawn:
|
||||
* **Unbounded:** only the `BR` texture from the wallpaper is used, repeated
|
||||
infinitely in the X and Y directions. The top-left, top, and left edge
|
||||
|
@ -264,6 +268,24 @@ Probably mostly DRM free. Will want some sort of account server early-on though.
|
|||
* The map author can also attach their own custom texture that will be included
|
||||
inside the map file.
|
||||
|
||||
### Default Wallpapers
|
||||
|
||||
**notebook**: standard ruled notebook paper with a red line alone the Left
|
||||
dge and a blank margin along the Top, with a Corner and the blue lines
|
||||
aking up the Repeat in all directions.
|
||||
|
||||
![notebook.png](../assets/wallpapers/notebook.png)
|
||||
|
||||
**graph**: graph paper made up of a grid of light grey or blue lines.
|
||||
|
||||
**dots**: graph paper made of dots at the intersections but not the lines in
|
||||
between.
|
||||
|
||||
**legal**: yellow lined notebook paper (legal pad).
|
||||
|
||||
**placemat**: a placemat texture with a wavy outline that emborders the map
|
||||
on all four sides. To be used with the Bordered level type.
|
||||
|
||||
# Text Console
|
||||
|
||||
* Create a rudimentary dev console for entering text commands in-game. It
|
||||
|
|
3
Makefile
3
Makefile
|
@ -2,10 +2,11 @@ SHELL := /bin/bash
|
|||
|
||||
VERSION=$(shell grep -e 'Version =' pkg/doodle.go | head -n 1 | cut -d '"' -f 2)
|
||||
BUILD=$(shell git describe --always)
|
||||
BUILD_DATE=$(shell date -Iseconds)
|
||||
CURDIR=$(shell curdir)
|
||||
|
||||
# Inject the build version (commit hash) into the executable.
|
||||
LDFLAGS := -ldflags "-X main.Build=$(BUILD)"
|
||||
LDFLAGS := -ldflags "-X main.Build=$(BUILD) -X main.BuildDate=$(BUILD_DATE)"
|
||||
|
||||
# `make setup` to set up a new environment, pull dependencies, etc.
|
||||
.PHONY: setup
|
||||
|
|
|
@ -2,22 +2,38 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/cmd/doodad/commands"
|
||||
doodle "git.kirsle.net/apps/doodle/pkg"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var Build = "N/A"
|
||||
// Build variables.
|
||||
var (
|
||||
Build = "N/A"
|
||||
BuildDate string
|
||||
)
|
||||
|
||||
func init() {
|
||||
if BuildDate == "" {
|
||||
BuildDate = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "doodad"
|
||||
app.Usage = "command line interface for Doodle"
|
||||
app.Version = doodle.Version + " build " + Build
|
||||
app.Version = fmt.Sprintf("%s build %s. Built on %s",
|
||||
doodle.Version,
|
||||
Build,
|
||||
BuildDate,
|
||||
)
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
|
|
|
@ -1,40 +1,64 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
_ "image/png"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render/sdl"
|
||||
doodle "git.kirsle.net/apps/doodle/pkg"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
_ "image/png"
|
||||
)
|
||||
|
||||
// Build number is the git commit hash.
|
||||
var Build string
|
||||
|
||||
// Command line args
|
||||
var (
|
||||
debug bool
|
||||
edit bool
|
||||
guitest bool
|
||||
Build = "<dynamic>"
|
||||
BuildDate string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&debug, "debug", false, "Debug mode")
|
||||
flag.BoolVar(&edit, "edit", false, "Edit the map given on the command line. Default is to play the map.")
|
||||
flag.BoolVar(&guitest, "guitest", false, "Enter the GUI Test scene.")
|
||||
if BuildDate == "" {
|
||||
BuildDate = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
runtime.LockOSThread()
|
||||
flag.Parse()
|
||||
|
||||
args := flag.Args()
|
||||
app := cli.NewApp()
|
||||
app.Name = "doodle"
|
||||
app.Usage = "command line interface for Doodle"
|
||||
app.Version = fmt.Sprintf("%s build %s. Built on %s",
|
||||
doodle.Version,
|
||||
Build,
|
||||
BuildDate,
|
||||
)
|
||||
|
||||
app.Flags = []cli.Flag{
|
||||
cli.BoolFlag{
|
||||
Name: "debug, d",
|
||||
Usage: "enable debug level logging",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "edit, e",
|
||||
Usage: "edit the map given on the command line (instead of play it)",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "guitest",
|
||||
Usage: "enter the GUI Test scene on startup",
|
||||
},
|
||||
}
|
||||
|
||||
app.Action = func(c *cli.Context) error {
|
||||
var filename string
|
||||
if len(args) > 0 {
|
||||
filename = args[0]
|
||||
if c.NArg() > 0 {
|
||||
filename = c.Args().Get(0)
|
||||
}
|
||||
|
||||
// SDL engine.
|
||||
|
@ -44,16 +68,26 @@ func main() {
|
|||
balance.Height,
|
||||
)
|
||||
|
||||
app := doodle.New(debug, engine)
|
||||
app.SetupEngine()
|
||||
if guitest {
|
||||
app.Goto(&doodle.GUITestScene{})
|
||||
game := doodle.New(c.Bool("debug"), engine)
|
||||
game.SetupEngine()
|
||||
if c.Bool("guitest") {
|
||||
game.Goto(&doodle.GUITestScene{})
|
||||
} else if filename != "" {
|
||||
if edit {
|
||||
app.EditFile(filename)
|
||||
if c.Bool("edit") {
|
||||
game.EditFile(filename)
|
||||
} else {
|
||||
app.PlayLevel(filename)
|
||||
game.PlayLevel(filename)
|
||||
}
|
||||
}
|
||||
app.Run()
|
||||
game.Run()
|
||||
return nil
|
||||
}
|
||||
|
||||
sort.Sort(cli.FlagsByName(app.Flags))
|
||||
sort.Sort(cli.CommandsByName(app.Commands))
|
||||
|
||||
err := app.Run(os.Args)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -156,15 +156,15 @@ func (c *Color) UnmarshalJSON(b []byte) error {
|
|||
}
|
||||
|
||||
// Add a relative color value to the color.
|
||||
func (c Color) Add(r, g, b, a int32) Color {
|
||||
func (c Color) Add(r, g, b, a int) Color {
|
||||
var (
|
||||
R = int32(c.Red) + r
|
||||
G = int32(c.Green) + g
|
||||
B = int32(c.Blue) + b
|
||||
A = int32(c.Alpha) + a
|
||||
R = int(c.Red) + r
|
||||
G = int(c.Green) + g
|
||||
B = int(c.Blue) + b
|
||||
A = int(c.Alpha) + a
|
||||
)
|
||||
|
||||
cap8 := func(v int32) uint8 {
|
||||
cap8 := func(v int) uint8 {
|
||||
if v > 255 {
|
||||
v = 255
|
||||
} else if v < 0 {
|
||||
|
@ -182,11 +182,22 @@ func (c Color) Add(r, g, b, a int32) Color {
|
|||
}
|
||||
|
||||
// Lighten a color value.
|
||||
func (c Color) Lighten(v int32) Color {
|
||||
func (c Color) Lighten(v int) Color {
|
||||
return c.Add(v, v, v, 0)
|
||||
}
|
||||
|
||||
// Darken a color value.
|
||||
func (c Color) Darken(v int32) Color {
|
||||
func (c Color) Darken(v int) Color {
|
||||
return c.Add(-v, -v, -v, 0)
|
||||
}
|
||||
|
||||
// Transparentize adjusts the alpha value.
|
||||
func (c Color) Transparentize(v int) Color {
|
||||
return c.Add(0, 0, 0, v)
|
||||
}
|
||||
|
||||
// SetAlpha sets the alpha value to a specific setting.
|
||||
func (c Color) SetAlpha(v uint8) Color {
|
||||
c.Alpha = v
|
||||
return c
|
||||
}
|
||||
|
|
|
@ -1,16 +0,0 @@
|
|||
package sdl
|
||||
|
||||
// Frames to cache for FPS calculation.
|
||||
const (
|
||||
maxSamples = 100
|
||||
TargetFPS = 1000 / 60
|
||||
)
|
||||
|
||||
var (
|
||||
fpsCurrentTicks uint32 // current time we get sdl.GetTicks()
|
||||
fpsLastTime uint32 // last time we printed the fpsCurrentTicks
|
||||
fpsCurrent int
|
||||
fpsFrames int
|
||||
fpsSkipped uint32
|
||||
fpsInterval uint32 = 1000
|
||||
)
|
|
@ -8,5 +8,5 @@ var (
|
|||
ButtonHoverColor = render.RGBA(200, 255, 255, 255)
|
||||
ButtonOutlineColor = render.Black
|
||||
|
||||
BorderColorOffset int32 = 40
|
||||
BorderColorOffset = 40
|
||||
)
|
||||
|
|
|
@ -17,10 +17,10 @@ var (
|
|||
|
||||
// Debug overlay (FPS etc.) settings.
|
||||
DebugFontFilename = "./fonts/DejaVuSans-Bold.ttf"
|
||||
DebugFontSize = 15
|
||||
DebugFontSize = 16
|
||||
DebugLabelColor = render.MustHexColor("#FF9900")
|
||||
DebugValueColor = render.MustHexColor("#00CCFF")
|
||||
DebugStrokeDarken int32 = 80
|
||||
DebugStrokeDarken = 80
|
||||
|
||||
// Background color to use when exporting a drawing Chunk as a bitmap image
|
||||
// on disk. Default is white. Setting this to translucent yellow is a great
|
||||
|
|
|
@ -15,7 +15,7 @@ var (
|
|||
ScrollMaxVelocity = 24
|
||||
|
||||
// Player speeds
|
||||
PlayerMaxVelocity = 12
|
||||
PlayerMaxVelocity = 8
|
||||
PlayerAcceleration = 2
|
||||
Gravity = 2
|
||||
|
||||
|
|
|
@ -20,9 +20,6 @@ type Actor interface {
|
|||
// Movement commands.
|
||||
MoveBy(render.Point) // Add {X,Y} to current Position.
|
||||
MoveTo(render.Point) // Set current Position to {X,Y}.
|
||||
|
||||
// Implement the Draw function.
|
||||
Draw(render.Engine)
|
||||
}
|
||||
|
||||
// GetBoundingRect computes the full pairs of points for the collision box
|
||||
|
|
91
pkg/doodads/drawing.go
Normal file
91
pkg/doodads/drawing.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package doodads
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// Drawing is a Doodad Actor that is based on drawings made inside the game.
|
||||
type Drawing struct {
|
||||
Doodad *Doodad
|
||||
|
||||
id string
|
||||
point render.Point
|
||||
velocity render.Point
|
||||
accel int
|
||||
size render.Rect
|
||||
grounded bool
|
||||
}
|
||||
|
||||
// NewDrawing creates a Drawing actor based on a Doodad drawing. If you pass
|
||||
// an empty ID string, it will make a random UUIDv4 ID.
|
||||
func NewDrawing(id string, doodad *Doodad) Drawing {
|
||||
if id == "" {
|
||||
id = uuid.Must(uuid.NewV4()).String()
|
||||
}
|
||||
return Drawing{
|
||||
id: id,
|
||||
Doodad: doodad,
|
||||
size: doodad.Rect(),
|
||||
}
|
||||
}
|
||||
|
||||
// ID to get the Drawing ID.
|
||||
func (d *Drawing) ID() string {
|
||||
return d.id
|
||||
}
|
||||
|
||||
// Position returns the Drawing's position.
|
||||
func (d *Drawing) Position() render.Point {
|
||||
return d.point
|
||||
}
|
||||
|
||||
// Velocity returns the Drawing's velocity.
|
||||
func (d *Drawing) Velocity() render.Point {
|
||||
return d.velocity
|
||||
}
|
||||
|
||||
// SetVelocity to set the speed.
|
||||
func (d *Drawing) SetVelocity(v render.Point) {
|
||||
d.velocity = v
|
||||
}
|
||||
|
||||
// Acceleration returns the Drawing's acceleration.
|
||||
func (d *Drawing) Acceleration() int {
|
||||
return d.accel
|
||||
}
|
||||
|
||||
// SetAcceleration to set the acceleration.
|
||||
func (d *Drawing) SetAcceleration(v int) {
|
||||
d.accel = v
|
||||
}
|
||||
|
||||
// Size returns the Drawing's size.
|
||||
func (d *Drawing) Size() render.Rect {
|
||||
return d.size
|
||||
}
|
||||
|
||||
// Grounded returns whether the Drawing is standing on solid ground.
|
||||
func (d *Drawing) Grounded() bool {
|
||||
return d.grounded
|
||||
}
|
||||
|
||||
// SetGrounded sets the grounded state.
|
||||
func (d *Drawing) SetGrounded(v bool) {
|
||||
d.grounded = v
|
||||
}
|
||||
|
||||
// MoveBy a relative value.
|
||||
func (d *Drawing) MoveBy(by render.Point) {
|
||||
d.point.Add(by)
|
||||
}
|
||||
|
||||
// MoveTo an absolute world value.
|
||||
func (d *Drawing) MoveTo(to render.Point) {
|
||||
d.point = to
|
||||
}
|
||||
|
||||
// Draw the drawing.
|
||||
func (d *Drawing) Draw(e render.Engine) {
|
||||
|
||||
}
|
16
pkg/doodads/dummy/dummy.go
Normal file
16
pkg/doodads/dummy/dummy.go
Normal file
|
@ -0,0 +1,16 @@
|
|||
// Package dummy implements a dummy doodads.Drawing.
|
||||
package dummy
|
||||
|
||||
import "git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
|
||||
// Drawing is a dummy doodads.Drawing that has no data.
|
||||
type Drawing struct {
|
||||
doodads.Drawing
|
||||
}
|
||||
|
||||
// NewDrawing creates a new dummy drawing.
|
||||
func NewDrawing(id string, doodad *doodads.Doodad) *Drawing {
|
||||
return &Drawing{
|
||||
Drawing: doodads.NewDrawing(id, doodad),
|
||||
}
|
||||
}
|
11
pkg/doodads/dummy/player.go
Normal file
11
pkg/doodads/dummy/player.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
// Package dummy implements a dummy doodads.Drawing.
|
||||
package dummy
|
||||
|
||||
import "git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
|
||||
// NewPlayer creates a dummy player object.
|
||||
func NewPlayer() *Drawing {
|
||||
return &Drawing{
|
||||
Drawing: doodads.NewDrawing("PLAYER", doodads.New(32)),
|
||||
}
|
||||
}
|
|
@ -15,7 +15,7 @@ import (
|
|||
|
||||
const (
|
||||
// Version number.
|
||||
Version = "0.0.1-alpha"
|
||||
Version = "0.0.7-alpha"
|
||||
|
||||
// TargetFPS is the frame rate to cap the game to.
|
||||
TargetFPS = 1000 / 60 // 60 FPS
|
||||
|
|
|
@ -7,6 +7,7 @@ import (
|
|||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads/dummy"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/uix"
|
||||
|
@ -29,7 +30,7 @@ type PlayScene struct {
|
|||
debWorldIndex *string
|
||||
|
||||
// Player character
|
||||
Player doodads.Actor
|
||||
Player *uix.Actor
|
||||
}
|
||||
|
||||
// Name of the scene.
|
||||
|
@ -63,19 +64,25 @@ func (s *PlayScene) Setup(d *Doodle) error {
|
|||
if s.Level != nil {
|
||||
log.Debug("PlayScene.Setup: received level from scene caller")
|
||||
s.drawing.LoadLevel(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)
|
||||
s.LoadLevel(s.Filename)
|
||||
}
|
||||
|
||||
s.Player = doodads.NewPlayer()
|
||||
|
||||
if s.Level == nil {
|
||||
log.Debug("PlayScene.Setup: no grid given, initializing empty grid")
|
||||
s.Level = level.New()
|
||||
s.drawing.LoadLevel(d.Engine, s.Level)
|
||||
s.drawing.InstallActors(s.Level.Actors)
|
||||
}
|
||||
|
||||
player := dummy.NewPlayer()
|
||||
s.Player = uix.NewActor(player.ID(), &level.Actor{}, player.Doodad)
|
||||
s.Player.MoveTo(render.NewPoint(128, 128))
|
||||
s.drawing.AddActor(s.Player)
|
||||
s.drawing.FollowActor = s.Player.ID()
|
||||
|
||||
d.Flash("Entered Play Mode. Press 'E' to edit this map.")
|
||||
|
||||
return nil
|
||||
|
@ -112,6 +119,10 @@ func (s *PlayScene) Loop(d *Doodle, ev *events.State) error {
|
|||
|
||||
// s.drawing.Loop(ev)
|
||||
s.movePlayer(ev)
|
||||
if err := s.drawing.Loop(ev); err != nil {
|
||||
log.Error("Drawing loop error: %s", err.Error())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -124,7 +135,12 @@ func (s *PlayScene) Draw(d *Doodle) error {
|
|||
s.drawing.Present(d.Engine, s.drawing.Point())
|
||||
|
||||
// Draw our hero.
|
||||
s.Player.Draw(d.Engine)
|
||||
d.Engine.DrawBox(render.RGBA(255, 255, 153, 255), render.Rect{
|
||||
X: s.Player.Position().X,
|
||||
Y: s.Player.Position().Y,
|
||||
W: s.Player.Size().W,
|
||||
H: s.Player.Size().H,
|
||||
})
|
||||
|
||||
// Draw out bounding boxes.
|
||||
d.DrawCollisionBox(s.Player)
|
||||
|
@ -135,20 +151,26 @@ func (s *PlayScene) Draw(d *Doodle) error {
|
|||
// movePlayer updates the player's X,Y coordinate based on key pressed.
|
||||
func (s *PlayScene) movePlayer(ev *events.State) {
|
||||
delta := s.Player.Position()
|
||||
var playerSpeed int32 = 8
|
||||
var gravity int32 = 2
|
||||
var playerSpeed = int32(balance.PlayerMaxVelocity)
|
||||
var gravity = int32(balance.Gravity)
|
||||
|
||||
var velocity render.Point
|
||||
|
||||
if ev.Down.Now {
|
||||
delta.Y += playerSpeed
|
||||
velocity.Y = playerSpeed
|
||||
}
|
||||
if ev.Left.Now {
|
||||
delta.X -= playerSpeed
|
||||
velocity.X = -playerSpeed
|
||||
}
|
||||
if ev.Right.Now {
|
||||
delta.X += playerSpeed
|
||||
velocity.X = playerSpeed
|
||||
}
|
||||
if ev.Up.Now {
|
||||
delta.Y -= playerSpeed
|
||||
velocity.Y = -playerSpeed
|
||||
}
|
||||
|
||||
// Apply gravity.
|
||||
|
@ -165,8 +187,10 @@ func (s *PlayScene) movePlayer(ev *events.State) {
|
|||
// Gravity has to pipe through the collision checker, too, so it
|
||||
// can't give us a cheated downward boost.
|
||||
delta.Y += gravity
|
||||
velocity.Y += gravity
|
||||
}
|
||||
|
||||
// s.Player.SetVelocity(velocity)
|
||||
s.Player.MoveTo(delta)
|
||||
}
|
||||
|
||||
|
@ -181,6 +205,7 @@ func (s *PlayScene) LoadLevel(filename string) error {
|
|||
|
||||
s.Level = level
|
||||
s.drawing.LoadLevel(s.d.Engine, s.Level)
|
||||
s.drawing.InstallActors(s.Level.Actors)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -23,6 +23,9 @@ type Scene interface {
|
|||
|
||||
// Goto a scene. First it unloads the current scene.
|
||||
func (d *Doodle) Goto(scene Scene) error {
|
||||
// Clear any debug labels.
|
||||
customDebugLabels = []debugLabel{}
|
||||
|
||||
// Teardown existing scene.
|
||||
if d.Scene != nil {
|
||||
d.Scene.Destroy()
|
||||
|
|
48
pkg/uix/actor.go
Normal file
48
pkg/uix/actor.go
Normal file
|
@ -0,0 +1,48 @@
|
|||
package uix
|
||||
|
||||
import (
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
uuid "github.com/satori/go.uuid"
|
||||
)
|
||||
|
||||
// Actor is an object that marries together the three things that make a
|
||||
// Doodad instance "tick" while inside a Canvas:
|
||||
//
|
||||
// - uix.Actor is a doodads.Drawing so it fulfills doodads.Actor to be a
|
||||
// dynamic object during gameplay.
|
||||
// - It has a pointer to the level.Actor indicating its static level data
|
||||
// as defined in the map: its spawn coordinate and configuration.
|
||||
// - A uix.Canvas that can present the actor's graphics to the screen.
|
||||
type Actor struct {
|
||||
doodads.Drawing
|
||||
Actor *level.Actor
|
||||
Canvas *Canvas
|
||||
}
|
||||
|
||||
// NewActor sets up a uix.Actor.
|
||||
// If the id is blank, a new UUIDv4 is generated.
|
||||
func NewActor(id string, levelActor *level.Actor, doodad *doodads.Doodad) *Actor {
|
||||
if id == "" {
|
||||
id = uuid.Must(uuid.NewV4()).String()
|
||||
}
|
||||
|
||||
size := int32(doodad.Layers[0].Chunker.Size)
|
||||
can := NewCanvas(int(size), false)
|
||||
can.Name = id
|
||||
can.actor = levelActor
|
||||
|
||||
// TODO: if the Background is render.Invisible it gets defaulted to
|
||||
// White somewhere and the Doodad masks the level drawing behind it.
|
||||
can.SetBackground(render.RGBA(0, 0, 1, 0))
|
||||
|
||||
can.LoadDoodad(doodad)
|
||||
can.Resize(render.NewRect(size, size))
|
||||
|
||||
return &Actor{
|
||||
Drawing: doodads.NewDrawing(id, doodad),
|
||||
Actor: levelActor,
|
||||
Canvas: can,
|
||||
}
|
||||
}
|
|
@ -12,7 +12,6 @@ import (
|
|||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"git.kirsle.net/apps/doodle/pkg/wallpaper"
|
||||
)
|
||||
|
||||
|
@ -35,6 +34,9 @@ type Canvas struct {
|
|||
// to remove the mask.
|
||||
MaskColor render.Color
|
||||
|
||||
// Actor ID to follow the camera on automatically, i.e. the main player.
|
||||
FollowActor string
|
||||
|
||||
// Debug tools
|
||||
// NoLimitScroll suppresses the scroll limit for bounded levels.
|
||||
NoLimitScroll bool
|
||||
|
@ -63,12 +65,6 @@ type Canvas struct {
|
|||
Scroll render.Point // Scroll offset for which parts of canvas are visible.
|
||||
}
|
||||
|
||||
// Actor is an instance of an actor with a Canvas attached.
|
||||
type Actor struct {
|
||||
Actor *level.Actor
|
||||
Canvas *Canvas
|
||||
}
|
||||
|
||||
// NewCanvas initializes a Canvas widget.
|
||||
//
|
||||
// If editable is true, Scrollable is also set to true, which means the arrow
|
||||
|
@ -141,35 +137,6 @@ func (w *Canvas) LoadDoodad(d *doodads.Doodad) {
|
|||
w.Load(d.Palette, d.Layers[0].Chunker)
|
||||
}
|
||||
|
||||
// InstallActors adds external Actors to the canvas to be superimposed on top
|
||||
// of the drawing.
|
||||
func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
||||
w.actors = make([]*Actor, 0)
|
||||
for id, actor := range actors {
|
||||
log.Info("InstallActors: %s", id)
|
||||
|
||||
doodad, err := doodads.LoadJSON(userdir.DoodadPath(actor.Filename))
|
||||
if err != nil {
|
||||
return fmt.Errorf("InstallActors: %s", err)
|
||||
}
|
||||
|
||||
size := int32(doodad.Layers[0].Chunker.Size)
|
||||
can := NewCanvas(int(size), false)
|
||||
can.Name = id
|
||||
can.actor = actor
|
||||
// TODO: if the Background is render.Invisible it gets defaulted to
|
||||
// White somewhere and the Doodad masks the level drawing behind it.
|
||||
can.SetBackground(render.RGBA(0, 0, 1, 0))
|
||||
can.LoadDoodad(doodad)
|
||||
can.Resize(render.NewRect(size, size))
|
||||
w.actors = append(w.actors, &Actor{
|
||||
Actor: actor,
|
||||
Canvas: can,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetSwatch changes the currently selected swatch for editing.
|
||||
func (w *Canvas) SetSwatch(s *level.Swatch) {
|
||||
w.Palette.ActiveSwatch = s
|
||||
|
@ -190,21 +157,54 @@ func (w *Canvas) setup() {
|
|||
// Loop is called on the scene's event loop to handle mouse interaction with
|
||||
// the canvas, i.e. to edit it.
|
||||
func (w *Canvas) Loop(ev *events.State) error {
|
||||
if w.Scrollable {
|
||||
// Arrow keys to scroll the view.
|
||||
scrollBy := render.Point{}
|
||||
if ev.Right.Now {
|
||||
scrollBy.X -= balance.CanvasScrollSpeed
|
||||
} else if ev.Left.Now {
|
||||
scrollBy.X += balance.CanvasScrollSpeed
|
||||
// Process the arrow keys scrolling the level in Edit Mode.
|
||||
// canvas_scrolling.go
|
||||
w.loopEditorScroll(ev)
|
||||
if err := w.loopFollowActor(ev); err != nil {
|
||||
log.Error("Follow actor: %s", err) // not fatal but nice to know
|
||||
}
|
||||
w.loopConstrainScroll()
|
||||
|
||||
// Move any actors.
|
||||
for _, a := range w.actors {
|
||||
if v := a.Velocity(); v != render.Origin {
|
||||
// orig := a.Drawing.Position()
|
||||
a.MoveBy(v)
|
||||
|
||||
// Keep them contained inside the level.
|
||||
if w.wallpaper.pageType > level.Unbounded {
|
||||
var (
|
||||
orig = w.WorldIndexAt(a.Drawing.Position())
|
||||
moveBy render.Point
|
||||
size = a.Canvas.Size()
|
||||
)
|
||||
|
||||
// Bound it on the top left edges.
|
||||
if orig.X < 0 {
|
||||
moveBy.X = -orig.X
|
||||
}
|
||||
if orig.Y < 0 {
|
||||
moveBy.Y = -orig.Y
|
||||
}
|
||||
|
||||
// Bound it on the right bottom edges. XXX: downcast from int64!
|
||||
if w.wallpaper.maxWidth > 0 {
|
||||
if int64(orig.X+size.W) > w.wallpaper.maxWidth {
|
||||
var delta = int32(w.wallpaper.maxWidth - int64(orig.X+size.W))
|
||||
moveBy.X = delta
|
||||
}
|
||||
}
|
||||
if w.wallpaper.maxHeight > 0 {
|
||||
if int64(orig.Y+size.H) > w.wallpaper.maxHeight {
|
||||
var delta = int32(w.wallpaper.maxHeight - int64(orig.Y+size.H))
|
||||
moveBy.Y = delta
|
||||
}
|
||||
}
|
||||
|
||||
if !moveBy.IsZero() {
|
||||
a.MoveBy(moveBy)
|
||||
}
|
||||
if ev.Down.Now {
|
||||
scrollBy.Y -= balance.CanvasScrollSpeed
|
||||
} else if ev.Up.Now {
|
||||
scrollBy.Y += balance.CanvasScrollSpeed
|
||||
}
|
||||
if !scrollBy.IsZero() {
|
||||
w.ScrollBy(scrollBy)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -288,273 +288,3 @@ func (w *Canvas) ScrollBy(by render.Point) {
|
|||
func (w *Canvas) Compute(e render.Engine) {
|
||||
|
||||
}
|
||||
|
||||
// Present the canvas.
|
||||
func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||
var (
|
||||
S = w.Size()
|
||||
Viewport = w.Viewport()
|
||||
)
|
||||
// w.MoveTo(p) // TODO: when uncommented the canvas will creep down the Workspace frame in EditorMode
|
||||
w.DrawBox(e, p)
|
||||
e.DrawBox(w.Background(), render.Rect{
|
||||
X: p.X + w.BoxThickness(1),
|
||||
Y: p.Y + w.BoxThickness(1),
|
||||
W: S.W - 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.
|
||||
for coord := range w.chunks.IterViewportChunks(Viewport) {
|
||||
if chunk, ok := w.chunks.GetChunk(coord); ok {
|
||||
var tex render.Texturer
|
||||
if w.MaskColor != render.Invisible {
|
||||
tex = chunk.TextureMasked(e, w.MaskColor)
|
||||
} else {
|
||||
tex = chunk.Texture(e)
|
||||
}
|
||||
src := render.Rect{
|
||||
W: tex.Size().W,
|
||||
H: tex.Size().H,
|
||||
}
|
||||
|
||||
// If the source bitmap is already bigger than the Canvas widget
|
||||
// into which it will render, cap the source width and height.
|
||||
// This is especially useful for Doodad buttons because the drawing
|
||||
// is bigger than the button.
|
||||
if src.W > S.W {
|
||||
src.W = S.W
|
||||
}
|
||||
if src.H > S.H {
|
||||
src.H = S.H
|
||||
}
|
||||
|
||||
dst := render.Rect{
|
||||
X: p.X + w.Scroll.X + w.BoxThickness(1) + (coord.X * int32(chunk.Size)),
|
||||
Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + (coord.Y * int32(chunk.Size)),
|
||||
|
||||
// src.W and src.H will be AT MOST the full width and height of
|
||||
// a Canvas widget. Subtract the scroll offset to keep it bounded
|
||||
// visually on its right and bottom sides.
|
||||
W: src.W,
|
||||
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.
|
||||
//
|
||||
// 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 - w.BoxThickness(1)) - (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 - w.BoxThickness(1)) - (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 + w.BoxThickness(1)
|
||||
dst.W -= delta
|
||||
src.X += delta
|
||||
}
|
||||
if dst.Y < p.Y {
|
||||
delta := p.Y - dst.Y
|
||||
dst.Y = p.Y + w.BoxThickness(1)
|
||||
dst.H -= delta
|
||||
src.Y += delta
|
||||
}
|
||||
|
||||
// Trim the destination width so it doesn't overlap the Canvas border.
|
||||
if dst.W >= S.W-w.BoxThickness(1) {
|
||||
dst.W = S.W - w.BoxThickness(1)
|
||||
}
|
||||
|
||||
e.Copy(tex, src, dst)
|
||||
}
|
||||
}
|
||||
|
||||
w.drawActors(e, p)
|
||||
|
||||
// XXX: Debug, show label in canvas corner.
|
||||
if balance.DebugCanvasLabel {
|
||||
rows := []string{
|
||||
w.Name,
|
||||
|
||||
// XXX: debug options, uncomment for more details
|
||||
|
||||
// Size of the canvas
|
||||
// fmt.Sprintf("S=%d,%d", S.W, S.H),
|
||||
|
||||
// Viewport of the canvas
|
||||
// fmt.Sprintf("V=%d,%d:%d,%d",
|
||||
// Viewport.X, Viewport.Y,
|
||||
// Viewport.W, Viewport.H,
|
||||
// ),
|
||||
}
|
||||
if w.actor != nil {
|
||||
rows = append(rows,
|
||||
fmt.Sprintf("WP=%s", w.actor.Point),
|
||||
)
|
||||
}
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: strings.Join(rows, "\n"),
|
||||
Font: render.Text{
|
||||
FontFilename: balance.ShellFontFilename,
|
||||
Size: balance.ShellFontSizeSmall,
|
||||
Color: render.White,
|
||||
},
|
||||
})
|
||||
label.SetBackground(render.RGBA(0, 0, 50, 150))
|
||||
label.Compute(e)
|
||||
label.Present(e, render.Point{
|
||||
X: p.X + S.W - label.Size().W - w.BoxThickness(1),
|
||||
Y: p.Y + w.BoxThickness(1),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// drawActors superimposes the actors on top of the drawing.
|
||||
func (w *Canvas) drawActors(e render.Engine, p render.Point) {
|
||||
var (
|
||||
Viewport = w.ViewportRelative()
|
||||
S = w.Size()
|
||||
)
|
||||
|
||||
// See if each Actor is in range of the Viewport.
|
||||
for _, a := range w.actors {
|
||||
var (
|
||||
actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE
|
||||
can = a.Canvas // Canvas widget that draws the actor
|
||||
actorPoint = actor.Point // XXX TODO: DO NOT CHANGE
|
||||
actorSize = can.Size()
|
||||
)
|
||||
|
||||
// Create a box of World Coordinates that this actor occupies. The
|
||||
// Actor X,Y from level data is already a World Coordinate;
|
||||
// accomodate for the size of the Actor.
|
||||
actorBox := render.Rect{
|
||||
X: actorPoint.X,
|
||||
Y: actorPoint.Y,
|
||||
W: actorSize.W,
|
||||
H: actorSize.H,
|
||||
}
|
||||
|
||||
// Is any part of the actor visible?
|
||||
if !Viewport.Intersects(actorBox) {
|
||||
continue // not visible on screen
|
||||
}
|
||||
|
||||
drawAt := render.Point{
|
||||
X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1),
|
||||
Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1),
|
||||
}
|
||||
resizeTo := actorSize
|
||||
|
||||
// XXX TODO: when an Actor hits the left or top edge and shrinks,
|
||||
// scrolling to offset that shrink is currently hard to solve.
|
||||
scrollTo := render.Origin
|
||||
|
||||
// Handle cropping and scaling if this Actor's canvas can't be
|
||||
// completely visible within the parent.
|
||||
if drawAt.X+resizeTo.W > p.X+S.W {
|
||||
// Hitting the right edge, shrunk the width now.
|
||||
delta := (drawAt.X + resizeTo.W) - (p.X + S.W)
|
||||
resizeTo.W -= delta
|
||||
} else if drawAt.X < p.X {
|
||||
// Hitting the left edge. Cap the X coord and shrink the width.
|
||||
delta := p.X - drawAt.X // positive number
|
||||
drawAt.X = p.X
|
||||
// scrollTo.X -= delta // TODO
|
||||
resizeTo.W -= delta
|
||||
}
|
||||
|
||||
if drawAt.Y+resizeTo.H > p.Y+S.H {
|
||||
// Hitting the bottom edge, shrink the height.
|
||||
delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H)
|
||||
resizeTo.H -= delta
|
||||
} else if drawAt.Y < p.Y {
|
||||
// Hitting the top edge. Cap the Y coord and shrink the height.
|
||||
delta := p.Y - drawAt.Y
|
||||
drawAt.Y = p.Y
|
||||
// scrollTo.Y -= delta // TODO
|
||||
resizeTo.H -= delta
|
||||
}
|
||||
|
||||
if resizeTo != actorSize {
|
||||
can.Resize(resizeTo)
|
||||
can.ScrollTo(scrollTo)
|
||||
}
|
||||
can.Present(e, drawAt)
|
||||
|
||||
// Clean up the canvas size and offset.
|
||||
can.Resize(actorSize) // restore original size in case cropped
|
||||
can.ScrollTo(render.Origin)
|
||||
}
|
||||
}
|
||||
|
|
116
pkg/uix/canvas_actors.go
Normal file
116
pkg/uix/canvas_actors.go
Normal file
|
@ -0,0 +1,116 @@
|
|||
package uix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/pkg/doodads"
|
||||
"git.kirsle.net/apps/doodle/pkg/level"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
)
|
||||
|
||||
// InstallActors adds external Actors to the canvas to be superimposed on top
|
||||
// of the drawing.
|
||||
func (w *Canvas) InstallActors(actors level.ActorMap) error {
|
||||
w.actors = make([]*Actor, 0)
|
||||
for id, actor := range actors {
|
||||
doodad, err := doodads.LoadJSON(userdir.DoodadPath(actor.Filename))
|
||||
if err != nil {
|
||||
return fmt.Errorf("InstallActors: %s", err)
|
||||
}
|
||||
|
||||
w.actors = append(w.actors, NewActor(id, actor, doodad))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddActor injects additional actors into the canvas, such as a Player doodad.
|
||||
func (w *Canvas) AddActor(actor *Actor) error {
|
||||
w.actors = append(w.actors, actor)
|
||||
return nil
|
||||
}
|
||||
|
||||
// drawActors is a subroutine of Present() that superimposes the actors on top
|
||||
// of the level drawing.
|
||||
func (w *Canvas) drawActors(e render.Engine, p render.Point) {
|
||||
var (
|
||||
Viewport = w.ViewportRelative()
|
||||
S = w.Size()
|
||||
)
|
||||
|
||||
// See if each Actor is in range of the Viewport.
|
||||
for i, a := range w.actors {
|
||||
if a == nil {
|
||||
log.Error("Canvas.drawActors: null actor at index %d (of %d actors)", i, len(w.actors))
|
||||
continue
|
||||
}
|
||||
var (
|
||||
actor = a.Actor // Static Actor instance from Level file, DO NOT CHANGE
|
||||
can = a.Canvas // Canvas widget that draws the actor
|
||||
actorPoint = actor.Point // XXX TODO: DO NOT CHANGE
|
||||
actorSize = can.Size()
|
||||
)
|
||||
|
||||
// Create a box of World Coordinates that this actor occupies. The
|
||||
// Actor X,Y from level data is already a World Coordinate;
|
||||
// accomodate for the size of the Actor.
|
||||
actorBox := render.Rect{
|
||||
X: actorPoint.X,
|
||||
Y: actorPoint.Y,
|
||||
W: actorSize.W,
|
||||
H: actorSize.H,
|
||||
}
|
||||
|
||||
// Is any part of the actor visible?
|
||||
if !Viewport.Intersects(actorBox) {
|
||||
continue // not visible on screen
|
||||
}
|
||||
|
||||
drawAt := render.Point{
|
||||
X: p.X + w.Scroll.X + actorPoint.X + w.BoxThickness(1),
|
||||
Y: p.Y + w.Scroll.Y + actorPoint.Y + w.BoxThickness(1),
|
||||
}
|
||||
resizeTo := actorSize
|
||||
|
||||
// XXX TODO: when an Actor hits the left or top edge and shrinks,
|
||||
// scrolling to offset that shrink is currently hard to solve.
|
||||
scrollTo := render.Origin
|
||||
|
||||
// Handle cropping and scaling if this Actor's canvas can't be
|
||||
// completely visible within the parent.
|
||||
if drawAt.X+resizeTo.W > p.X+S.W {
|
||||
// Hitting the right edge, shrunk the width now.
|
||||
delta := (drawAt.X + resizeTo.W) - (p.X + S.W)
|
||||
resizeTo.W -= delta
|
||||
} else if drawAt.X < p.X {
|
||||
// Hitting the left edge. Cap the X coord and shrink the width.
|
||||
delta := p.X - drawAt.X // positive number
|
||||
drawAt.X = p.X
|
||||
// scrollTo.X -= delta // TODO
|
||||
resizeTo.W -= delta
|
||||
}
|
||||
|
||||
if drawAt.Y+resizeTo.H > p.Y+S.H {
|
||||
// Hitting the bottom edge, shrink the height.
|
||||
delta := (drawAt.Y + resizeTo.H) - (p.Y + S.H)
|
||||
resizeTo.H -= delta
|
||||
} else if drawAt.Y < p.Y {
|
||||
// Hitting the top edge. Cap the Y coord and shrink the height.
|
||||
delta := p.Y - drawAt.Y
|
||||
drawAt.Y = p.Y
|
||||
// scrollTo.Y -= delta // TODO
|
||||
resizeTo.H -= delta
|
||||
}
|
||||
|
||||
if resizeTo != actorSize {
|
||||
can.Resize(resizeTo)
|
||||
can.ScrollTo(scrollTo)
|
||||
}
|
||||
can.Present(e, drawAt)
|
||||
|
||||
// Clean up the canvas size and offset.
|
||||
can.Resize(actorSize) // restore original size in case cropped
|
||||
can.ScrollTo(render.Origin)
|
||||
}
|
||||
}
|
172
pkg/uix/canvas_present.go
Normal file
172
pkg/uix/canvas_present.go
Normal file
|
@ -0,0 +1,172 @@
|
|||
package uix
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"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/log"
|
||||
)
|
||||
|
||||
// Present the canvas.
|
||||
func (w *Canvas) Present(e render.Engine, p render.Point) {
|
||||
var (
|
||||
S = w.Size()
|
||||
Viewport = w.Viewport()
|
||||
)
|
||||
// w.MoveTo(p) // TODO: when uncommented the canvas will creep down the Workspace frame in EditorMode
|
||||
w.DrawBox(e, p)
|
||||
e.DrawBox(w.Background(), render.Rect{
|
||||
X: p.X + w.BoxThickness(1),
|
||||
Y: p.Y + w.BoxThickness(1),
|
||||
W: S.W - w.BoxThickness(2),
|
||||
H: S.H - w.BoxThickness(2),
|
||||
})
|
||||
|
||||
// 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 {
|
||||
var tex render.Texturer
|
||||
if w.MaskColor != render.Invisible {
|
||||
tex = chunk.TextureMasked(e, w.MaskColor)
|
||||
} else {
|
||||
tex = chunk.Texture(e)
|
||||
}
|
||||
src := render.Rect{
|
||||
W: tex.Size().W,
|
||||
H: tex.Size().H,
|
||||
}
|
||||
|
||||
// If the source bitmap is already bigger than the Canvas widget
|
||||
// into which it will render, cap the source width and height.
|
||||
// This is especially useful for Doodad buttons because the drawing
|
||||
// is bigger than the button.
|
||||
if src.W > S.W {
|
||||
src.W = S.W
|
||||
}
|
||||
if src.H > S.H {
|
||||
src.H = S.H
|
||||
}
|
||||
|
||||
dst := render.Rect{
|
||||
X: p.X + w.Scroll.X + w.BoxThickness(1) + (coord.X * int32(chunk.Size)),
|
||||
Y: p.Y + w.Scroll.Y + w.BoxThickness(1) + (coord.Y * int32(chunk.Size)),
|
||||
|
||||
// src.W and src.H will be AT MOST the full width and height of
|
||||
// a Canvas widget. Subtract the scroll offset to keep it bounded
|
||||
// visually on its right and bottom sides.
|
||||
W: src.W,
|
||||
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.
|
||||
//
|
||||
// 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 - w.BoxThickness(1)) - (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 - w.BoxThickness(1)) - (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 + w.BoxThickness(1)
|
||||
dst.W -= delta
|
||||
src.X += delta
|
||||
}
|
||||
if dst.Y < p.Y {
|
||||
delta := p.Y - dst.Y
|
||||
dst.Y = p.Y + w.BoxThickness(1)
|
||||
dst.H -= delta
|
||||
src.Y += delta
|
||||
}
|
||||
|
||||
// Trim the destination width so it doesn't overlap the Canvas border.
|
||||
if dst.W >= S.W-w.BoxThickness(1) {
|
||||
dst.W = S.W - w.BoxThickness(1)
|
||||
}
|
||||
|
||||
e.Copy(tex, src, dst)
|
||||
}
|
||||
}
|
||||
|
||||
w.drawActors(e, p)
|
||||
|
||||
// XXX: Debug, show label in canvas corner.
|
||||
if balance.DebugCanvasLabel {
|
||||
rows := []string{
|
||||
w.Name,
|
||||
|
||||
// XXX: debug options, uncomment for more details
|
||||
|
||||
// Size of the canvas
|
||||
// fmt.Sprintf("S=%d,%d", S.W, S.H),
|
||||
|
||||
// Viewport of the canvas
|
||||
// fmt.Sprintf("V=%d,%d:%d,%d",
|
||||
// Viewport.X, Viewport.Y,
|
||||
// Viewport.W, Viewport.H,
|
||||
// ),
|
||||
}
|
||||
if w.actor != nil {
|
||||
rows = append(rows,
|
||||
fmt.Sprintf("WP=%s", w.actor.Point),
|
||||
)
|
||||
}
|
||||
label := ui.NewLabel(ui.Label{
|
||||
Text: strings.Join(rows, "\n"),
|
||||
Font: render.Text{
|
||||
FontFilename: balance.ShellFontFilename,
|
||||
Size: balance.ShellFontSizeSmall,
|
||||
Color: render.White,
|
||||
},
|
||||
})
|
||||
label.SetBackground(render.RGBA(0, 0, 50, 150))
|
||||
label.Compute(e)
|
||||
label.Present(e, render.Point{
|
||||
X: p.X + S.W - label.Size().W - w.BoxThickness(1),
|
||||
Y: p.Y + w.BoxThickness(1),
|
||||
})
|
||||
}
|
||||
}
|
187
pkg/uix/canvas_scrolling.go
Normal file
187
pkg/uix/canvas_scrolling.go
Normal file
|
@ -0,0 +1,187 @@
|
|||
package uix
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/events"
|
||||
"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/level"
|
||||
)
|
||||
|
||||
/*
|
||||
Loop() subroutine to scroll the canvas using arrow keys (for edit mode).
|
||||
|
||||
If w.Scrollable is false this function won't do anything.
|
||||
|
||||
Cursor keys will scroll the drawing by balance.CanvasScrollSpeed per tick.
|
||||
If the level pageType is constrained, the scrollable viewport will be
|
||||
constrained to fit the bounds of the level.
|
||||
|
||||
The debug boolean `NoLimitScroll=true` will override the bounded level scroll
|
||||
restriction and allow scrolling into out-of-bounds areas of the level.
|
||||
*/
|
||||
func (w *Canvas) loopEditorScroll(ev *events.State) error {
|
||||
if !w.Scrollable {
|
||||
return errors.New("canvas not scrollable")
|
||||
}
|
||||
|
||||
// Arrow keys to scroll the view.
|
||||
scrollBy := render.Point{}
|
||||
if ev.Right.Now {
|
||||
scrollBy.X -= balance.CanvasScrollSpeed
|
||||
} else if ev.Left.Now {
|
||||
scrollBy.X += balance.CanvasScrollSpeed
|
||||
}
|
||||
if ev.Down.Now {
|
||||
scrollBy.Y -= balance.CanvasScrollSpeed
|
||||
} else if ev.Up.Now {
|
||||
scrollBy.Y += balance.CanvasScrollSpeed
|
||||
}
|
||||
if !scrollBy.IsZero() {
|
||||
w.ScrollBy(scrollBy)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Loop() subroutine to constrain the scrolled view to within a bounded level.
|
||||
*/
|
||||
func (w *Canvas) loopConstrainScroll() error {
|
||||
if w.NoLimitScroll {
|
||||
return errors.New("NoLimitScroll enabled")
|
||||
}
|
||||
|
||||
var capped bool
|
||||
|
||||
// Constrain the top and left edges.
|
||||
if w.wallpaper.pageType > level.Unbounded {
|
||||
if w.Scroll.X > 0 {
|
||||
w.Scroll.X = 0
|
||||
capped = true
|
||||
}
|
||||
if w.Scroll.Y > 0 {
|
||||
w.Scroll.Y = 0
|
||||
capped = true
|
||||
}
|
||||
}
|
||||
|
||||
// Constrain the bottom and right for limited world sizes.
|
||||
if w.wallpaper.maxWidth+w.wallpaper.maxHeight > 0 {
|
||||
var (
|
||||
// TODO: downcast from int64!
|
||||
mw = int32(w.wallpaper.maxWidth)
|
||||
mh = int32(w.wallpaper.maxHeight)
|
||||
Viewport = w.Viewport()
|
||||
)
|
||||
if Viewport.W > mw {
|
||||
delta := Viewport.W - mw
|
||||
w.Scroll.X += delta
|
||||
capped = true
|
||||
}
|
||||
if Viewport.H > mh {
|
||||
delta := Viewport.H - mh
|
||||
w.Scroll.Y += delta
|
||||
capped = true
|
||||
}
|
||||
}
|
||||
|
||||
if capped {
|
||||
return errors.New("scroll limited by level constraint")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
/*
|
||||
Loop() subroutine for Play Mode to follow an actor in the camera's view.
|
||||
|
||||
Does nothing if w.FollowActor is an empty string. Set it to the ID of an Actor
|
||||
to follow. If the actor exists, the Canvas will scroll to keep it on the
|
||||
screen.
|
||||
*/
|
||||
func (w *Canvas) loopFollowActor(ev *events.State) error {
|
||||
// Are we following an actor?
|
||||
if w.FollowActor == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
P = w.Point()
|
||||
S = w.Size()
|
||||
)
|
||||
|
||||
// Find the actor.
|
||||
for _, actor := range w.actors {
|
||||
if actor.ID() != w.FollowActor {
|
||||
continue
|
||||
}
|
||||
|
||||
actor.Canvas.SetBorderSize(2)
|
||||
actor.Canvas.SetBorderColor(render.Cyan)
|
||||
actor.Canvas.SetBorderStyle(ui.BorderSolid)
|
||||
|
||||
var (
|
||||
APosition = actor.Position() // relative to screen
|
||||
APoint = actor.Drawing.Position()
|
||||
ASize = actor.Drawing.Size()
|
||||
scrollBy render.Point
|
||||
)
|
||||
|
||||
// Scroll left
|
||||
if APosition.X-P.X <= int32(balance.ScrollboxHoz) {
|
||||
var delta = APoint.X - P.X
|
||||
if delta > int32(balance.ScrollMaxVelocity) {
|
||||
delta = int32(balance.ScrollMaxVelocity)
|
||||
}
|
||||
|
||||
if delta < 0 {
|
||||
// constrain in case they're FAR OFF SCREEN so we don't flip back around
|
||||
delta = -delta
|
||||
}
|
||||
scrollBy.X = delta
|
||||
}
|
||||
|
||||
// Scroll right
|
||||
if APosition.X >= S.W-ASize.W-int32(balance.ScrollboxHoz) {
|
||||
var delta = S.W - ASize.W - int32(balance.ScrollboxHoz)
|
||||
if delta > int32(balance.ScrollMaxVelocity) {
|
||||
delta = int32(balance.ScrollMaxVelocity)
|
||||
}
|
||||
scrollBy.X = -delta
|
||||
}
|
||||
|
||||
// Scroll up
|
||||
if APosition.Y-P.Y <= int32(balance.ScrollboxVert) {
|
||||
var delta = APoint.Y - P.Y
|
||||
if delta > int32(balance.ScrollMaxVelocity) {
|
||||
delta = int32(balance.ScrollMaxVelocity)
|
||||
}
|
||||
|
||||
if delta < 0 {
|
||||
delta = -delta
|
||||
}
|
||||
scrollBy.Y = delta
|
||||
}
|
||||
|
||||
// Scroll down
|
||||
if APosition.Y >= S.H-ASize.H-int32(balance.ScrollboxVert) {
|
||||
var delta = S.H - ASize.H - int32(balance.ScrollboxVert)
|
||||
if delta > int32(balance.ScrollMaxVelocity) {
|
||||
delta = int32(balance.ScrollMaxVelocity)
|
||||
}
|
||||
scrollBy.Y = -delta
|
||||
}
|
||||
|
||||
if scrollBy != render.Origin {
|
||||
w.ScrollBy(scrollBy)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("actor ID '%s' not found in level", w.FollowActor)
|
||||
}
|
Loading…
Reference in New Issue
Block a user