Browse Source

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.
master
Noah Petherbridge 2 years ago
commit
cddc41465f
15 changed files with 1309 additions and 0 deletions
  1. +192
    -0
      color.go
  2. +69
    -0
      functions.go
  3. +221
    -0
      interface.go
  4. +14
    -0
      log.go
  5. +103
    -0
      point.go
  6. +60
    -0
      point_test.go
  7. +71
    -0
      rect_test.go
  8. +57
    -0
      sdl/canvas.go
  9. +131
    -0
      sdl/events.go
  10. +20
    -0
      sdl/fps.go
  11. +17
    -0
      sdl/log.go
  12. +115
    -0
      sdl/sdl.go
  13. +158
    -0
      sdl/text.go
  14. +55
    -0
      sdl/texture.go
  15. +26
    -0
      sdl/utils.go

+ 192
- 0
color.go View 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
- 0
functions.go 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
}
}

+ 221
- 0
interface.go View 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
- 0
log.go View 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
- 0
point.go View 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
- 0
point_test.go View 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
- 0
rect_test.go View 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
- 0
sdl/canvas.go View 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
- 0
sdl/events.go View 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
- 0
sdl/fps.go View 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
- 0
sdl/log.go View 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
- 0
sdl/sdl.go View 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
- 0
sdl/text.go View 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
- 0
sdl/texture.go View 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
- 0
sdl/utils.go View 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…
Cancel
Save