* All private Doodle source code into the pkg/ folder. * Potentially public code into the lib/ folder. * Centralize the logger into a subpackage.master
commit
cddc41465f
15 changed files with 1309 additions and 0 deletions
@ -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) |
||||
} |
@ -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 |
||||
} |
||||
} |
@ -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) |
||||
} |
@ -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, |
||||
}) |
||||
} |
@ -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 |
||||
} |
@ -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), |
||||
) |
||||
} |
||||
} |
||||
} |
@ -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), |
||||
) |
||||
} |
||||
} |
||||
} |
@ -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, |
||||
}) |
||||
} |
@ -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 |
||||
} |
@ -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 |
@ -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") |
||||
} |
@ -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 |
||||
} |
@ -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": ")", |
||||
"-": "_", |
||||
"=": "+", |
||||
"[": "{", |
||||
"]": "}", |
||||
`\`: "|", |
||||
";": ":", |
||||
`'`: `"`, |
||||
",": "<", |
||||
".": ">", |
||||
"/": "?", |
||||
} |
@ -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 |
||||
} |
@ -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