Code Layout Refactor
* All private Doodle source code into the pkg/ folder. * Potentially public code into the lib/ folder. * Centralize the logger into a subpackage.
This commit is contained in:
commit
cddc41465f
192
color.go
Normal file
192
color.go
Normal file
|
@ -0,0 +1,192 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"image/color"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// Regexps to parse hex color codes. Three formats are supported:
|
||||||
|
// * reHexColor3 uses only 3 hex characters, like #F90
|
||||||
|
// * reHexColor6 uses standard 6 characters, like #FF9900
|
||||||
|
// * reHexColor8 is the standard 6 plus alpha channel, like #FF9900FF
|
||||||
|
reHexColor3 = regexp.MustCompile(`^([A-Fa-f0-9])([A-Fa-f0-9])([A-Fa-f0-9])$`)
|
||||||
|
reHexColor6 = regexp.MustCompile(`^([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})$`)
|
||||||
|
reHexColor8 = regexp.MustCompile(`^([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})([A-Fa-f0-9]{2})$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Color holds an RGBA color value.
|
||||||
|
type Color struct {
|
||||||
|
Red uint8
|
||||||
|
Green uint8
|
||||||
|
Blue uint8
|
||||||
|
Alpha uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
// RGBA creates a new Color.
|
||||||
|
func RGBA(r, g, b, a uint8) Color {
|
||||||
|
return Color{
|
||||||
|
Red: r,
|
||||||
|
Green: g,
|
||||||
|
Blue: b,
|
||||||
|
Alpha: a,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FromColor creates a render.Color from a Go color.Color
|
||||||
|
func FromColor(from color.Color) Color {
|
||||||
|
// downscale a 16-bit color value to 8-bit. input range 0x0000..0xffff
|
||||||
|
downscale := func(in uint32) uint8 {
|
||||||
|
var scale = float64(in) / 0xffff
|
||||||
|
return uint8(scale * 0xff)
|
||||||
|
}
|
||||||
|
r, g, b, a := from.RGBA()
|
||||||
|
return RGBA(
|
||||||
|
downscale(r),
|
||||||
|
downscale(g),
|
||||||
|
downscale(b),
|
||||||
|
downscale(a),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MustHexColor parses a color from hex code or panics.
|
||||||
|
func MustHexColor(hex string) Color {
|
||||||
|
color, err := HexColor(hex)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
|
||||||
|
// HexColor parses a color from hexadecimal code.
|
||||||
|
func HexColor(hex string) (Color, error) {
|
||||||
|
c := Black // default color
|
||||||
|
|
||||||
|
if len(hex) > 0 && hex[0] == '#' {
|
||||||
|
hex = hex[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
var m []string
|
||||||
|
if len(hex) == 3 {
|
||||||
|
m = reHexColor3.FindStringSubmatch(hex)
|
||||||
|
} else if len(hex) == 6 {
|
||||||
|
m = reHexColor6.FindStringSubmatch(hex)
|
||||||
|
} else if len(hex) == 8 {
|
||||||
|
m = reHexColor8.FindStringSubmatch(hex)
|
||||||
|
} else {
|
||||||
|
return c, errors.New("not a valid length for color code; only 3, 6 and 8 supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Any luck?
|
||||||
|
if m == nil {
|
||||||
|
return c, errors.New("not a valid hex color code")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the color values. 16=base, 8=bit size
|
||||||
|
red, _ := strconv.ParseUint(m[1], 16, 8)
|
||||||
|
green, _ := strconv.ParseUint(m[2], 16, 8)
|
||||||
|
blue, _ := strconv.ParseUint(m[3], 16, 8)
|
||||||
|
|
||||||
|
// Alpha channel available?
|
||||||
|
var alpha uint64 = 255
|
||||||
|
if len(m) == 5 {
|
||||||
|
alpha, _ = strconv.ParseUint(m[4], 16, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Red = uint8(red)
|
||||||
|
c.Green = uint8(green)
|
||||||
|
c.Blue = uint8(blue)
|
||||||
|
c.Alpha = uint8(alpha)
|
||||||
|
return c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c Color) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"Color<#%02x%02x%02x+%02x>",
|
||||||
|
c.Red, c.Green, c.Blue, c.Alpha,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToColor converts a render.Color into a Go standard color.Color
|
||||||
|
func (c Color) ToColor() color.RGBA {
|
||||||
|
return color.RGBA{
|
||||||
|
R: c.Red,
|
||||||
|
G: c.Green,
|
||||||
|
B: c.Blue,
|
||||||
|
A: c.Alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transparent returns whether the alpha channel is zeroed out and the pixel
|
||||||
|
// won't appear as anything when rendered.
|
||||||
|
func (c Color) Transparent() bool {
|
||||||
|
return c.Alpha == 0x00
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON serializes the Color for JSON.
|
||||||
|
func (c Color) MarshalJSON() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf(
|
||||||
|
`"#%02x%02x%02x"`,
|
||||||
|
c.Red, c.Green, c.Blue,
|
||||||
|
)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalJSON reloads the Color from JSON.
|
||||||
|
func (c *Color) UnmarshalJSON(b []byte) error {
|
||||||
|
var hex string
|
||||||
|
err := json.Unmarshal(b, &hex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := HexColor(hex)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Red = parsed.Red
|
||||||
|
c.Blue = parsed.Blue
|
||||||
|
c.Green = parsed.Green
|
||||||
|
c.Alpha = parsed.Alpha
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a relative color value to the color.
|
||||||
|
func (c Color) Add(r, g, b, a int32) Color {
|
||||||
|
var (
|
||||||
|
R = int32(c.Red) + r
|
||||||
|
G = int32(c.Green) + g
|
||||||
|
B = int32(c.Blue) + b
|
||||||
|
A = int32(c.Alpha) + a
|
||||||
|
)
|
||||||
|
|
||||||
|
cap8 := func(v int32) uint8 {
|
||||||
|
if v > 255 {
|
||||||
|
v = 255
|
||||||
|
} else if v < 0 {
|
||||||
|
v = 0
|
||||||
|
}
|
||||||
|
return uint8(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Color{
|
||||||
|
Red: cap8(R),
|
||||||
|
Green: cap8(G),
|
||||||
|
Blue: cap8(B),
|
||||||
|
Alpha: cap8(A),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lighten a color value.
|
||||||
|
func (c Color) Lighten(v int32) Color {
|
||||||
|
return c.Add(v, v, v, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Darken a color value.
|
||||||
|
func (c Color) Darken(v int32) Color {
|
||||||
|
return c.Add(-v, -v, -v, 0)
|
||||||
|
}
|
69
functions.go
Normal file
69
functions.go
Normal 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
|
||||||
|
}
|
||||||
|
}
|
221
interface.go
Normal file
221
interface.go
Normal file
|
@ -0,0 +1,221 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/events"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Engine is the interface for the rendering engine, keeping SDL-specific stuff
|
||||||
|
// far away from the core of Doodle.
|
||||||
|
type Engine interface {
|
||||||
|
Setup() error
|
||||||
|
|
||||||
|
// Poll for events like keypresses and mouse clicks.
|
||||||
|
Poll() (*events.State, error)
|
||||||
|
GetTicks() uint32
|
||||||
|
WindowSize() (w, h int)
|
||||||
|
|
||||||
|
// Present presents the current state to the screen.
|
||||||
|
Present() error
|
||||||
|
|
||||||
|
// Clear the full canvas and set this color.
|
||||||
|
Clear(Color)
|
||||||
|
DrawPoint(Color, Point)
|
||||||
|
DrawLine(Color, Point, Point)
|
||||||
|
DrawRect(Color, Rect)
|
||||||
|
DrawBox(Color, Rect)
|
||||||
|
DrawText(Text, Point) error
|
||||||
|
ComputeTextRect(Text) (Rect, error)
|
||||||
|
|
||||||
|
// Texture caching.
|
||||||
|
NewBitmap(filename string) (Texturer, error)
|
||||||
|
Copy(t Texturer, src, dst Rect)
|
||||||
|
|
||||||
|
// Delay for a moment using the render engine's delay method,
|
||||||
|
// implemented by sdl.Delay(uint32)
|
||||||
|
Delay(uint32)
|
||||||
|
|
||||||
|
// Tasks that the Setup function should defer until tear-down.
|
||||||
|
Teardown()
|
||||||
|
|
||||||
|
Loop() error // maybe?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texturer is a stored image texture used by the rendering engine while
|
||||||
|
// abstracting away its inner workings.
|
||||||
|
type Texturer interface {
|
||||||
|
Size() Rect
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rect has a coordinate and a width and height.
|
||||||
|
type Rect struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
W int32
|
||||||
|
H int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRect creates a rectangle of size `width` and `height`. The X,Y values
|
||||||
|
// are initialized to zero.
|
||||||
|
func NewRect(width, height int32) Rect {
|
||||||
|
return Rect{
|
||||||
|
W: width,
|
||||||
|
H: height,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r Rect) String() string {
|
||||||
|
return fmt.Sprintf("Rect<%d,%d,%d,%d>",
|
||||||
|
r.X, r.Y, r.W, r.H,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Point returns the rectangle's X,Y values as a Point.
|
||||||
|
func (r Rect) Point() Point {
|
||||||
|
return Point{
|
||||||
|
X: r.X,
|
||||||
|
Y: r.Y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bigger returns if the given rect is larger than the current one.
|
||||||
|
func (r Rect) Bigger(other Rect) bool {
|
||||||
|
// TODO: don't know why this is !
|
||||||
|
return !(other.X < r.X || // Lefter
|
||||||
|
other.Y < r.Y || // Higher
|
||||||
|
other.W > r.W || // Wider
|
||||||
|
other.H > r.H) // Taller
|
||||||
|
}
|
||||||
|
|
||||||
|
// Intersects with the other rectangle in any way.
|
||||||
|
func (r Rect) Intersects(other Rect) bool {
|
||||||
|
// Do a bidirectional compare.
|
||||||
|
compare := func(a, b Rect) bool {
|
||||||
|
var corners = []Point{
|
||||||
|
NewPoint(b.X, b.Y),
|
||||||
|
NewPoint(b.X, b.Y+b.H),
|
||||||
|
NewPoint(b.X+b.W, b.Y),
|
||||||
|
NewPoint(b.X+b.W, b.Y+b.H),
|
||||||
|
}
|
||||||
|
for _, pt := range corners {
|
||||||
|
if pt.Inside(a) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return compare(r, other) || compare(other, r) || false
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the Rect is uninitialized.
|
||||||
|
func (r Rect) IsZero() bool {
|
||||||
|
return r.X == 0 && r.Y == 0 && r.W == 0 && r.H == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add another rect.
|
||||||
|
func (r Rect) Add(other Rect) Rect {
|
||||||
|
return Rect{
|
||||||
|
X: r.X + other.X,
|
||||||
|
Y: r.Y + other.Y,
|
||||||
|
W: r.W + other.W,
|
||||||
|
H: r.H + other.H,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a point to move the rect.
|
||||||
|
func (r Rect) AddPoint(other Point) Rect {
|
||||||
|
return Rect{
|
||||||
|
X: r.X + other.X,
|
||||||
|
Y: r.Y + other.Y,
|
||||||
|
W: r.W,
|
||||||
|
H: r.H,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Text holds information for drawing text.
|
||||||
|
type Text struct {
|
||||||
|
Text string
|
||||||
|
Size int
|
||||||
|
Color Color
|
||||||
|
Padding int32
|
||||||
|
PadX int32
|
||||||
|
PadY int32
|
||||||
|
Stroke Color // Stroke color (if not zero)
|
||||||
|
Shadow Color // Drop shadow color (if not zero)
|
||||||
|
FontFilename string // Path to *.ttf file on disk
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t Text) String() string {
|
||||||
|
return fmt.Sprintf(`Text<"%s" %dpx %s>`, t.Text, t.Size, t.Color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the Text is the zero value.
|
||||||
|
func (t Text) IsZero() bool {
|
||||||
|
return t.Text == "" && t.Size == 0 && t.Color == Invisible && t.Padding == 0 && t.Stroke == Invisible && t.Shadow == Invisible
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common color names.
|
||||||
|
var (
|
||||||
|
Invisible = Color{}
|
||||||
|
White = RGBA(255, 255, 255, 255)
|
||||||
|
Grey = RGBA(153, 153, 153, 255)
|
||||||
|
Black = RGBA(0, 0, 0, 255)
|
||||||
|
SkyBlue = RGBA(0, 153, 255, 255)
|
||||||
|
Blue = RGBA(0, 0, 255, 255)
|
||||||
|
DarkBlue = RGBA(0, 0, 153, 255)
|
||||||
|
Red = RGBA(255, 0, 0, 255)
|
||||||
|
DarkRed = RGBA(153, 0, 0, 255)
|
||||||
|
Green = RGBA(0, 255, 0, 255)
|
||||||
|
DarkGreen = RGBA(0, 153, 0, 255)
|
||||||
|
Cyan = RGBA(0, 255, 255, 255)
|
||||||
|
DarkCyan = RGBA(0, 153, 153, 255)
|
||||||
|
Yellow = RGBA(255, 255, 0, 255)
|
||||||
|
DarkYellow = RGBA(153, 153, 0, 255)
|
||||||
|
Magenta = RGBA(255, 0, 255, 255)
|
||||||
|
Purple = RGBA(153, 0, 153, 255)
|
||||||
|
Pink = RGBA(255, 153, 255, 255)
|
||||||
|
)
|
||||||
|
|
||||||
|
// IterLine is a generator that returns the X,Y coordinates to draw a line.
|
||||||
|
// https://en.wikipedia.org/wiki/Digital_differential_analyzer_(graphics_algorithm)
|
||||||
|
func IterLine(x1, y1, x2, y2 int32) chan Point {
|
||||||
|
generator := make(chan Point)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
var (
|
||||||
|
dx = float64(x2 - x1)
|
||||||
|
dy = float64(y2 - y1)
|
||||||
|
)
|
||||||
|
var step float64
|
||||||
|
if math.Abs(dx) >= math.Abs(dy) {
|
||||||
|
step = math.Abs(dx)
|
||||||
|
} else {
|
||||||
|
step = math.Abs(dy)
|
||||||
|
}
|
||||||
|
|
||||||
|
dx = dx / step
|
||||||
|
dy = dy / step
|
||||||
|
x := float64(x1)
|
||||||
|
y := float64(y1)
|
||||||
|
for i := 0; i <= int(step); i++ {
|
||||||
|
generator <- Point{
|
||||||
|
X: int32(x),
|
||||||
|
Y: int32(y),
|
||||||
|
}
|
||||||
|
x += dx
|
||||||
|
y += dy
|
||||||
|
}
|
||||||
|
|
||||||
|
close(generator)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return generator
|
||||||
|
}
|
||||||
|
|
||||||
|
// IterLine2 works with two Points rather than four coordinates.
|
||||||
|
func IterLine2(p1 Point, p2 Point) chan Point {
|
||||||
|
return IterLine(p1.X, p1.Y, p2.X, p2.Y)
|
||||||
|
}
|
14
log.go
Normal file
14
log.go
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import "github.com/kirsle/golog"
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("doodle")
|
||||||
|
log.Configure(&golog.Config{
|
||||||
|
Level: golog.DebugLevel,
|
||||||
|
Theme: golog.DarkTheme,
|
||||||
|
Colors: golog.ExtendedColor,
|
||||||
|
})
|
||||||
|
}
|
103
point.go
Normal file
103
point.go
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
package render
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Point holds an X,Y coordinate value.
|
||||||
|
type Point struct {
|
||||||
|
X int32
|
||||||
|
Y int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Common points.
|
||||||
|
var (
|
||||||
|
Origin Point
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewPoint makes a new Point at an X,Y coordinate.
|
||||||
|
func NewPoint(x, y int32) Point {
|
||||||
|
return Point{
|
||||||
|
X: x,
|
||||||
|
Y: y,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p Point) String() string {
|
||||||
|
return fmt.Sprintf("%d,%d", p.X, p.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePoint to parse a point from its string representation.
|
||||||
|
func ParsePoint(v string) (Point, error) {
|
||||||
|
halves := strings.Split(v, ",")
|
||||||
|
if len(halves) != 2 {
|
||||||
|
return Point{}, fmt.Errorf("'%s': not a valid coordinate string", v)
|
||||||
|
}
|
||||||
|
x, errX := strconv.Atoi(halves[0])
|
||||||
|
y, errY := strconv.Atoi(halves[1])
|
||||||
|
if errX != nil || errY != nil {
|
||||||
|
return Point{}, fmt.Errorf("invalid coordinate string (X: %v; Y: %v)",
|
||||||
|
errX,
|
||||||
|
errY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Point{
|
||||||
|
X: int32(x),
|
||||||
|
Y: int32(y),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsZero returns if the point is the zero value.
|
||||||
|
func (p Point) IsZero() bool {
|
||||||
|
return p.X == 0 && p.Y == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inside returns whether the Point falls inside the rect.
|
||||||
|
//
|
||||||
|
// NOTICE: the W and H are zero-relative, so a 100x100 box at coordinate
|
||||||
|
// X,Y would still have W,H of 100.
|
||||||
|
func (p Point) Inside(r Rect) bool {
|
||||||
|
var (
|
||||||
|
x1 = r.X
|
||||||
|
y1 = r.Y
|
||||||
|
x2 = r.X + r.W
|
||||||
|
y2 = r.Y + r.H
|
||||||
|
)
|
||||||
|
return ((p.X >= x1 && p.X <= x2) &&
|
||||||
|
(p.Y >= y1 && p.Y <= y2))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add (or subtract) the other point to your current point.
|
||||||
|
func (p *Point) Add(other Point) {
|
||||||
|
p.X += other.X
|
||||||
|
p.Y += other.Y
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalText to convert the point into text so that a render.Point may be used
|
||||||
|
// as a map key and serialized to JSON.
|
||||||
|
func (p *Point) MarshalText() ([]byte, error) {
|
||||||
|
return []byte(fmt.Sprintf("%d,%d", p.X, p.Y)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnmarshalText to restore it from text.
|
||||||
|
func (p *Point) UnmarshalText(b []byte) error {
|
||||||
|
halves := strings.Split(strings.Trim(string(b), `"`), ",")
|
||||||
|
if len(halves) != 2 {
|
||||||
|
return fmt.Errorf("'%s': not a valid coordinate string", b)
|
||||||
|
}
|
||||||
|
|
||||||
|
x, errX := strconv.Atoi(halves[0])
|
||||||
|
y, errY := strconv.Atoi(halves[1])
|
||||||
|
if errX != nil || errY != nil {
|
||||||
|
return fmt.Errorf("Point.UnmarshalJSON: Atoi errors (X=%s Y=%s)",
|
||||||
|
errX,
|
||||||
|
errY,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.X = int32(x)
|
||||||
|
p.Y = int32(y)
|
||||||
|
return nil
|
||||||
|
}
|
60
point_test.go
Normal file
60
point_test.go
Normal file
|
@ -0,0 +1,60 @@
|
||||||
|
package render_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPointInside(t *testing.T) {
|
||||||
|
type testCase struct {
|
||||||
|
rect render.Rect
|
||||||
|
p render.Point
|
||||||
|
shouldPass bool
|
||||||
|
}
|
||||||
|
tests := []testCase{
|
||||||
|
testCase{
|
||||||
|
rect: render.Rect{
|
||||||
|
X: 0,
|
||||||
|
Y: 0,
|
||||||
|
W: 500,
|
||||||
|
H: 500,
|
||||||
|
},
|
||||||
|
p: render.NewPoint(128, 256),
|
||||||
|
shouldPass: true,
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
rect: render.Rect{
|
||||||
|
X: 100,
|
||||||
|
Y: 80,
|
||||||
|
W: 40,
|
||||||
|
H: 60,
|
||||||
|
},
|
||||||
|
p: render.NewPoint(128, 256),
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
testCase{
|
||||||
|
// true values when debugging why Doodads weren't
|
||||||
|
// considered inside the viewport.
|
||||||
|
rect: render.Rect{
|
||||||
|
X: 0,
|
||||||
|
Y: -232,
|
||||||
|
H: 874,
|
||||||
|
W: 490,
|
||||||
|
},
|
||||||
|
p: render.NewPoint(509, 260),
|
||||||
|
shouldPass: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
if test.p.Inside(test.rect) != test.shouldPass {
|
||||||
|
t.Errorf("Failed: %s inside %s should be %s",
|
||||||
|
test.p,
|
||||||
|
test.rect,
|
||||||
|
strconv.FormatBool(test.shouldPass),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
71
rect_test.go
Normal file
71
rect_test.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
package render_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strconv"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestIntersection(t *testing.T) {
|
||||||
|
newRect := func(x, y, w, h int) render.Rect {
|
||||||
|
return render.Rect{
|
||||||
|
X: int32(x),
|
||||||
|
Y: int32(y),
|
||||||
|
W: int32(w),
|
||||||
|
H: int32(h),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type TestCase struct {
|
||||||
|
A render.Rect
|
||||||
|
B render.Rect
|
||||||
|
Expect bool
|
||||||
|
}
|
||||||
|
var tests = []TestCase{
|
||||||
|
{
|
||||||
|
A: newRect(0, 0, 1000, 1000),
|
||||||
|
B: newRect(200, 200, 100, 100),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(200, 200, 100, 100),
|
||||||
|
B: newRect(0, 0, 1000, 1000),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(0, 0, 100, 100),
|
||||||
|
B: newRect(100, 0, 100, 100),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(0, 0, 99, 99),
|
||||||
|
B: newRect(100, 0, 99, 99),
|
||||||
|
Expect: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Real coords of a test doodad!
|
||||||
|
A: newRect(183, 256, 283, 356),
|
||||||
|
B: newRect(0, -232, 874, 490),
|
||||||
|
Expect: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
A: newRect(183, 256, 283, 356),
|
||||||
|
B: newRect(0, -240, 874, 490),
|
||||||
|
Expect: false, // XXX: must be true
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, test := range tests {
|
||||||
|
actual := test.A.Intersects(test.B)
|
||||||
|
if actual != test.Expect {
|
||||||
|
t.Errorf(
|
||||||
|
"%s collision with %s: expected %s, got %s",
|
||||||
|
test.A,
|
||||||
|
test.B,
|
||||||
|
strconv.FormatBool(test.Expect),
|
||||||
|
strconv.FormatBool(actual),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
57
sdl/canvas.go
Normal file
57
sdl/canvas.go
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
// Package sdl provides an SDL2 renderer for Doodle.
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Clear the canvas and set this color.
|
||||||
|
func (r *Renderer) Clear(color render.Color) {
|
||||||
|
if color != r.lastColor {
|
||||||
|
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||||
|
}
|
||||||
|
r.renderer.Clear()
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawPoint puts a color at a pixel.
|
||||||
|
func (r *Renderer) DrawPoint(color render.Color, point render.Point) {
|
||||||
|
if color != r.lastColor {
|
||||||
|
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||||
|
}
|
||||||
|
r.renderer.DrawPoint(point.X, point.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawLine draws a line between two points.
|
||||||
|
func (r *Renderer) DrawLine(color render.Color, a, b render.Point) {
|
||||||
|
if color != r.lastColor {
|
||||||
|
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||||
|
}
|
||||||
|
r.renderer.DrawLine(a.X, a.Y, b.X, b.Y)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawRect draws a rectangle.
|
||||||
|
func (r *Renderer) DrawRect(color render.Color, rect render.Rect) {
|
||||||
|
if color != r.lastColor {
|
||||||
|
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||||
|
}
|
||||||
|
r.renderer.DrawRect(&sdl.Rect{
|
||||||
|
X: rect.X,
|
||||||
|
Y: rect.Y,
|
||||||
|
W: rect.W,
|
||||||
|
H: rect.H,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawBox draws a filled rectangle.
|
||||||
|
func (r *Renderer) DrawBox(color render.Color, rect render.Rect) {
|
||||||
|
if color != r.lastColor {
|
||||||
|
r.renderer.SetDrawColor(color.Red, color.Green, color.Blue, color.Alpha)
|
||||||
|
}
|
||||||
|
r.renderer.FillRect(&sdl.Rect{
|
||||||
|
X: rect.X,
|
||||||
|
Y: rect.Y,
|
||||||
|
W: rect.W,
|
||||||
|
H: rect.H,
|
||||||
|
})
|
||||||
|
}
|
131
sdl/events.go
Normal file
131
sdl/events.go
Normal file
|
@ -0,0 +1,131 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/events"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Poll for events.
|
||||||
|
func (r *Renderer) Poll() (*events.State, error) {
|
||||||
|
s := r.events
|
||||||
|
for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() {
|
||||||
|
switch t := event.(type) {
|
||||||
|
case *sdl.QuitEvent:
|
||||||
|
return s, errors.New("quit")
|
||||||
|
case *sdl.WindowEvent:
|
||||||
|
if DebugWindowEvents {
|
||||||
|
if t.Event == sdl.WINDOWEVENT_RESIZED {
|
||||||
|
log.Debug("[%d ms] tick:%d Window Resized to %dx%d",
|
||||||
|
t.Timestamp,
|
||||||
|
r.ticks,
|
||||||
|
t.Data1,
|
||||||
|
t.Data2,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.Resized.Push(true)
|
||||||
|
case *sdl.MouseMotionEvent:
|
||||||
|
if DebugMouseEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d MouseMotion type:%d id:%d x:%d y:%d xrel:%d yrel:%d",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.XRel, t.YRel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the cursor position.
|
||||||
|
s.CursorX.Push(t.X)
|
||||||
|
s.CursorY.Push(t.Y)
|
||||||
|
s.Button1.Push(t.State == 1)
|
||||||
|
case *sdl.MouseButtonEvent:
|
||||||
|
if DebugClickEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y, t.Button, t.State,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Push the cursor position.
|
||||||
|
s.CursorX.Push(t.X)
|
||||||
|
s.CursorY.Push(t.Y)
|
||||||
|
|
||||||
|
// Is a mouse button pressed down?
|
||||||
|
checkDown := func(number uint8, target *events.BoolTick) bool {
|
||||||
|
if t.Button == number {
|
||||||
|
var eventName string
|
||||||
|
if t.State == 1 && target.Now == false {
|
||||||
|
eventName = "DOWN"
|
||||||
|
} else if t.State == 0 && target.Now == true {
|
||||||
|
eventName = "UP"
|
||||||
|
}
|
||||||
|
|
||||||
|
if eventName != "" {
|
||||||
|
target.Push(eventName == "DOWN")
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if checkDown(1, s.Button1) || checkDown(3, s.Button2) {
|
||||||
|
// Return the event immediately.
|
||||||
|
return s, nil
|
||||||
|
}
|
||||||
|
case *sdl.MouseWheelEvent:
|
||||||
|
if DebugMouseEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Which, t.X, t.Y,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case *sdl.KeyboardEvent:
|
||||||
|
if DebugKeyEvents {
|
||||||
|
log.Debug("[%d ms] tick:%d Keyboard type:%d sym:%c modifiers:%d state:%d repeat:%d\n",
|
||||||
|
t.Timestamp, r.ticks, t.Type, t.Keysym.Sym, t.Keysym.Mod, t.State, t.Repeat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch t.Keysym.Scancode {
|
||||||
|
case sdl.SCANCODE_ESCAPE:
|
||||||
|
if t.Repeat == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.EscapeKey.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_RETURN:
|
||||||
|
if t.Repeat == 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
s.EnterKey.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_F12:
|
||||||
|
s.ScreenshotKey.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_UP:
|
||||||
|
s.Up.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_LEFT:
|
||||||
|
s.Left.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_RIGHT:
|
||||||
|
s.Right.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_DOWN:
|
||||||
|
s.Down.Push(t.State == 1)
|
||||||
|
case sdl.SCANCODE_LSHIFT:
|
||||||
|
case sdl.SCANCODE_RSHIFT:
|
||||||
|
s.ShiftActive.Push(t.State == 1)
|
||||||
|
continue
|
||||||
|
case sdl.SCANCODE_LALT:
|
||||||
|
case sdl.SCANCODE_RALT:
|
||||||
|
case sdl.SCANCODE_LCTRL:
|
||||||
|
case sdl.SCANCODE_RCTRL:
|
||||||
|
continue
|
||||||
|
case sdl.SCANCODE_BACKSPACE:
|
||||||
|
// Make it a key event with "\b" as the sequence.
|
||||||
|
if t.State == 1 || t.Repeat == 1 {
|
||||||
|
s.KeyName.Push(`\b`)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
// Push the string value of the key.
|
||||||
|
if t.State == 1 {
|
||||||
|
s.KeyName.Push(string(t.Keysym.Sym))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return s, nil
|
||||||
|
}
|
20
sdl/fps.go
Normal file
20
sdl/fps.go
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import "git.kirsle.net/apps/doodle/pkg/level"
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
|
||||||
|
var pixelHistory []level.Pixel
|
17
sdl/log.go
Normal file
17
sdl/log.go
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import "github.com/kirsle/golog"
|
||||||
|
|
||||||
|
var log *golog.Logger
|
||||||
|
|
||||||
|
// Verbose debug logging.
|
||||||
|
var (
|
||||||
|
DebugMouseEvents = false
|
||||||
|
DebugClickEvents = false
|
||||||
|
DebugKeyEvents = false
|
||||||
|
DebugWindowEvents = false
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
log = golog.GetLogger("doodle")
|
||||||
|
}
|
115
sdl/sdl.go
Normal file
115
sdl/sdl.go
Normal file
|
@ -0,0 +1,115 @@
|
||||||
|
// Package sdl provides an SDL2 renderer for Doodle.
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/events"
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
"github.com/veandco/go-sdl2/ttf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Renderer manages the SDL state.
|
||||||
|
type Renderer struct {
|
||||||
|
// Configurable fields.
|
||||||
|
title string
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
startTime time.Time
|
||||||
|
|
||||||
|
// Private fields.
|
||||||
|
events *events.State
|
||||||
|
window *sdl.Window
|
||||||
|
renderer *sdl.Renderer
|
||||||
|
running bool
|
||||||
|
ticks uint64
|
||||||
|
|
||||||
|
// Optimizations to minimize SDL calls.
|
||||||
|
lastColor render.Color
|
||||||
|
}
|
||||||
|
|
||||||
|
// New creates the SDL renderer.
|
||||||
|
func New(title string, width, height int) *Renderer {
|
||||||
|
return &Renderer{
|
||||||
|
events: events.New(),
|
||||||
|
title: title,
|
||||||
|
width: int32(width),
|
||||||
|
height: int32(height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Teardown tasks when exiting the program.
|
||||||
|
func (r *Renderer) Teardown() {
|
||||||
|
r.renderer.Destroy()
|
||||||
|
r.window.Destroy()
|
||||||
|
sdl.Quit()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the renderer.
|
||||||
|
func (r *Renderer) Setup() error {
|
||||||
|
// Initialize SDL.
|
||||||
|
log.Info("Initializing SDL")
|
||||||
|
if err := sdl.Init(sdl.INIT_EVERYTHING); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize SDL_TTF.
|
||||||
|
log.Info("Initializing SDL_TTF")
|
||||||
|
if err := ttf.Init(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create our window.
|
||||||
|
log.Info("Creating the Main Window")
|
||||||
|
window, err := sdl.CreateWindow(
|
||||||
|
r.title,
|
||||||
|
sdl.WINDOWPOS_CENTERED,
|
||||||
|
sdl.WINDOWPOS_CENTERED,
|
||||||
|
r.width,
|
||||||
|
r.height,
|
||||||
|
sdl.WINDOW_SHOWN|sdl.WINDOW_RESIZABLE,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
r.window = window
|
||||||
|
|
||||||
|
// Blank out the window in white.
|
||||||
|
log.Info("Creating the SDL Renderer")
|
||||||
|
renderer, err := sdl.CreateRenderer(window, -1, sdl.RENDERER_ACCELERATED)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
renderer.SetDrawBlendMode(sdl.BLENDMODE_BLEND)
|
||||||
|
r.renderer = renderer
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTicks gets SDL's current tick count.
|
||||||
|
func (r *Renderer) GetTicks() uint32 {
|
||||||
|
return sdl.GetTicks()
|
||||||
|
}
|
||||||
|
|
||||||
|
// WindowSize returns the SDL window size.
|
||||||
|
func (r *Renderer) WindowSize() (int, int) {
|
||||||
|
w, h := r.window.GetSize()
|
||||||
|
return int(w), int(h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Present the current frame.
|
||||||
|
func (r *Renderer) Present() error {
|
||||||
|
r.renderer.Present()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delay using sdl.Delay
|
||||||
|
func (r *Renderer) Delay(time uint32) {
|
||||||
|
sdl.Delay(time)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loop is the main loop.
|
||||||
|
func (r *Renderer) Loop() error {
|
||||||
|
return nil
|
||||||
|
}
|
158
sdl/text.go
Normal file
158
sdl/text.go
Normal file
|
@ -0,0 +1,158 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/events"
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
"github.com/veandco/go-sdl2/ttf"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO: font filenames
|
||||||
|
var defaultFontFilename = "./fonts/DejaVuSans.ttf"
|
||||||
|
|
||||||
|
var fonts = map[string]*ttf.Font{}
|
||||||
|
|
||||||
|
// LoadFont loads and caches the font at a given size.
|
||||||
|
func LoadFont(filename string, size int) (*ttf.Font, error) {
|
||||||
|
if filename == "" {
|
||||||
|
filename = defaultFontFilename
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cached font available?
|
||||||
|
keyName := fmt.Sprintf("%s@%d", filename, size)
|
||||||
|
if font, ok := fonts[keyName]; ok {
|
||||||
|
return font, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
font, err := ttf.OpenFont(filename, size)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
fonts[keyName] = font
|
||||||
|
|
||||||
|
return font, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Keysym returns the current key pressed, taking into account the Shift
|
||||||
|
// key modifier.
|
||||||
|
func (r *Renderer) Keysym(ev *events.State) string {
|
||||||
|
if key := ev.KeyName.Read(); key != "" {
|
||||||
|
if ev.ShiftActive.Pressed() {
|
||||||
|
if symbol, ok := shiftMap[key]; ok {
|
||||||
|
return symbol
|
||||||
|
}
|
||||||
|
return strings.ToUpper(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// ComputeTextRect computes and returns a Rect for how large the text would
|
||||||
|
// appear if rendered.
|
||||||
|
func (r *Renderer) ComputeTextRect(text render.Text) (render.Rect, error) {
|
||||||
|
var (
|
||||||
|
rect render.Rect
|
||||||
|
font *ttf.Font
|
||||||
|
surface *sdl.Surface
|
||||||
|
color = ColorToSDL(text.Color)
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if font, err = LoadFont(text.FontFilename, text.Size); err != nil {
|
||||||
|
return rect, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if surface, err = font.RenderUTF8Blended(text.Text, color); err != nil {
|
||||||
|
return rect, err
|
||||||
|
}
|
||||||
|
defer surface.Free()
|
||||||
|
|
||||||
|
rect.W = surface.W
|
||||||
|
rect.H = surface.H
|
||||||
|
return rect, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// DrawText draws text on the canvas.
|
||||||
|
func (r *Renderer) DrawText(text render.Text, point render.Point) error {
|
||||||
|
var (
|
||||||
|
font *ttf.Font
|
||||||
|
surface *sdl.Surface
|
||||||
|
tex *sdl.Texture
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
|
||||||
|
if font, err = LoadFont(text.FontFilename, text.Size); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
write := func(dx, dy int32, color sdl.Color) {
|
||||||
|
if surface, err = font.RenderUTF8Blended(text.Text, color); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer surface.Free()
|
||||||
|
|
||||||
|
if tex, err = r.renderer.CreateTextureFromSurface(surface); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer tex.Destroy()
|
||||||
|
|
||||||
|
tmp := &sdl.Rect{
|
||||||
|
X: point.X + dx,
|
||||||
|
Y: point.Y + dy,
|
||||||
|
W: surface.W,
|
||||||
|
H: surface.H,
|
||||||
|
}
|
||||||
|
r.renderer.Copy(tex, nil, tmp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does the text have a stroke around it?
|
||||||
|
if text.Stroke != render.Invisible {
|
||||||
|
color := ColorToSDL(text.Stroke)
|
||||||
|
write(-1, -1, color)
|
||||||
|
write(-1, 0, color)
|
||||||
|
write(-1, 1, color)
|
||||||
|
write(1, -1, color)
|
||||||
|
write(1, 0, color)
|
||||||
|
write(1, 1, color)
|
||||||
|
write(0, -1, color)
|
||||||
|
write(0, 1, color)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Does it have a drop shadow?
|
||||||
|
if text.Shadow != render.Invisible {
|
||||||
|
write(1, 1, ColorToSDL(text.Shadow))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw the text itself.
|
||||||
|
write(0, 0, ColorToSDL(text.Color))
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// shiftMap maps keys to their Shift versions.
|
||||||
|
var shiftMap = map[string]string{
|
||||||
|
"`": "~",
|
||||||
|
"1": "!",
|
||||||
|
"2": "@",
|
||||||
|
"3": "#",
|
||||||
|
"4": "$",
|
||||||
|
"5": "%",
|
||||||
|
"6": "^",
|
||||||
|
"7": "&",
|
||||||
|
"8": "*",
|
||||||
|
"9": "(",
|
||||||
|
"0": ")",
|
||||||
|
"-": "_",
|
||||||
|
"=": "+",
|
||||||
|
"[": "{",
|
||||||
|
"]": "}",
|
||||||
|
`\`: "|",
|
||||||
|
";": ":",
|
||||||
|
`'`: `"`,
|
||||||
|
",": "<",
|
||||||
|
".": ">",
|
||||||
|
"/": "?",
|
||||||
|
}
|
55
sdl/texture.go
Normal file
55
sdl/texture.go
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copy a texture into the renderer.
|
||||||
|
func (r *Renderer) Copy(t render.Texturer, src, dst render.Rect) {
|
||||||
|
if tex, ok := t.(*Texture); ok {
|
||||||
|
var (
|
||||||
|
a = RectToSDL(src)
|
||||||
|
b = RectToSDL(dst)
|
||||||
|
)
|
||||||
|
r.renderer.Copy(tex.tex, &a, &b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Texture can hold on to SDL textures for caching and optimization.
|
||||||
|
type Texture struct {
|
||||||
|
tex *sdl.Texture
|
||||||
|
width int32
|
||||||
|
height int32
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the dimensions of the texture.
|
||||||
|
func (t *Texture) Size() render.Rect {
|
||||||
|
return render.NewRect(t.width, t.height)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBitmap initializes a texture from a bitmap image.
|
||||||
|
func (r *Renderer) NewBitmap(filename string) (render.Texturer, error) {
|
||||||
|
surface, err := sdl.LoadBMP(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewBitmap: LoadBMP: %s", err)
|
||||||
|
}
|
||||||
|
defer surface.Free()
|
||||||
|
|
||||||
|
// TODO: chroma key color hardcoded to white here
|
||||||
|
key := sdl.MapRGB(surface.Format, 255, 255, 255)
|
||||||
|
surface.SetColorKey(true, key)
|
||||||
|
|
||||||
|
tex, err := r.renderer.CreateTextureFromSurface(surface)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("NewBitmap: create texture: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Texture{
|
||||||
|
width: surface.W,
|
||||||
|
height: surface.H,
|
||||||
|
tex: tex,
|
||||||
|
}, nil
|
||||||
|
}
|
26
sdl/utils.go
Normal file
26
sdl/utils.go
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package sdl
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.kirsle.net/apps/doodle/lib/render"
|
||||||
|
"github.com/veandco/go-sdl2/sdl"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ColorToSDL converts Doodle's Color type to an sdl.Color.
|
||||||
|
func ColorToSDL(c render.Color) sdl.Color {
|
||||||
|
return sdl.Color{
|
||||||
|
R: c.Red,
|
||||||
|
G: c.Green,
|
||||||
|
B: c.Blue,
|
||||||
|
A: c.Alpha,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// RectToSDL converts Doodle's Rect type to an sdl.Rect.
|
||||||
|
func RectToSDL(r render.Rect) sdl.Rect {
|
||||||
|
return sdl.Rect{
|
||||||
|
X: r.X,
|
||||||
|
Y: r.Y,
|
||||||
|
W: r.W,
|
||||||
|
H: r.H,
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user