commit
a92ceac058
20 changed files with 2262 additions and 0 deletions
@ -0,0 +1,39 @@ |
|||
package canvas |
|||
|
|||
import ( |
|||
"syscall/js" |
|||
) |
|||
|
|||
// Canvas represents an HTML5 Canvas object.
|
|||
type Canvas struct { |
|||
Value js.Value |
|||
ctx2d js.Value |
|||
} |
|||
|
|||
// GetCanvas gets an HTML5 Canvas object from the DOM.
|
|||
func GetCanvas(id string) Canvas { |
|||
canvasEl := js.Global().Get("document").Call("getElementById", id) |
|||
canvas2d := canvasEl.Call("getContext", "2d") |
|||
|
|||
c := Canvas{ |
|||
Value: canvasEl, |
|||
ctx2d: canvas2d, |
|||
} |
|||
|
|||
canvasEl.Set("width", c.ClientW()) |
|||
canvasEl.Set("height", c.ClientH()) |
|||
|
|||
return c |
|||
} |
|||
|
|||
// ClientW returns the client width.
|
|||
func (c Canvas) ClientW() int { |
|||
return js.Global().Get("window").Get("innerWidth").Int() |
|||
// return c.Value.Get("clientWidth").Int()
|
|||
} |
|||
|
|||
// ClientH returns the client height.
|
|||
func (c Canvas) ClientH() int { |
|||
return js.Global().Get("window").Get("innerHeight").Int() |
|||
// return c.Value.Get("clientHeight").Int()
|
|||
} |
@ -0,0 +1,77 @@ |
|||
package canvas |
|||
|
|||
import ( |
|||
"fmt" |
|||
"syscall/js" |
|||
|
|||
"git.kirsle.net/apps/doodle/lib/render" |
|||
) |
|||
|
|||
// Methods here implement the drawing functions of the render.Engine
|
|||
|
|||
// RGBA turns a color into CSS RGBA string.
|
|||
func RGBA(c render.Color) string { |
|||
return fmt.Sprintf("rgba(%d,%d,%d,%f)", |
|||
c.Red, |
|||
c.Green, |
|||
c.Blue, |
|||
float64(c.Alpha)/255, |
|||
) |
|||
} |
|||
|
|||
// Clear the canvas to a certain color.
|
|||
func (e *Engine) Clear(color render.Color) { |
|||
e.canvas.ctx2d.Set("fillStyle", RGBA(color)) |
|||
e.canvas.ctx2d.Call("fillRect", 0, 0, e.width, e.height) |
|||
} |
|||
|
|||
// SetTitle sets the window title.
|
|||
func (e *Engine) SetTitle(title string) { |
|||
js.Global().Get("document").Set("title", title) |
|||
} |
|||
|
|||
// DrawPoint draws a pixel.
|
|||
func (e *Engine) DrawPoint(color render.Color, point render.Point) { |
|||
e.canvas.ctx2d.Set("fillStyle", RGBA(color)) |
|||
e.canvas.ctx2d.Call("fillRect", |
|||
int(point.X), |
|||
int(point.Y), |
|||
1, |
|||
1, |
|||
) |
|||
} |
|||
|
|||
// DrawLine draws a line between two points.
|
|||
func (e *Engine) DrawLine(color render.Color, a, b render.Point) { |
|||
e.canvas.ctx2d.Set("fillStyle", RGBA(color)) |
|||
for pt := range render.IterLine(a, b) { |
|||
e.canvas.ctx2d.Call("fillRect", |
|||
int(pt.X), |
|||
int(pt.Y), |
|||
1, |
|||
1, |
|||
) |
|||
} |
|||
} |
|||
|
|||
// DrawRect draws a rectangle.
|
|||
func (e *Engine) DrawRect(color render.Color, rect render.Rect) { |
|||
e.canvas.ctx2d.Set("strokeStyle", RGBA(color)) |
|||
e.canvas.ctx2d.Call("strokeRect", |
|||
int(rect.X), |
|||
int(rect.Y), |
|||
int(rect.W), |
|||
int(rect.H), |
|||
) |
|||
} |
|||
|
|||
// DrawBox draws a filled rectangle.
|
|||
func (e *Engine) DrawBox(color render.Color, rect render.Rect) { |
|||
e.canvas.ctx2d.Set("fillStyle", RGBA(color)) |
|||
e.canvas.ctx2d.Call("fillRect", |
|||
int(rect.X), |
|||
int(rect.Y), |
|||
int(rect.W), |
|||
int(rect.H), |
|||
) |
|||
} |
@ -0,0 +1,86 @@ |
|||
package canvas |
|||
|
|||
import ( |
|||
"syscall/js" |
|||
"time" |
|||
|
|||
"git.kirsle.net/apps/doodle/lib/events" |
|||
) |
|||
|
|||
// Engine implements a rendering engine targeting an HTML canvas for
|
|||
// WebAssembly targets.
|
|||
type Engine struct { |
|||
canvas Canvas |
|||
startTime time.Time |
|||
width int |
|||
height int |
|||
ticks uint32 |
|||
|
|||
// Private fields.
|
|||
events *events.State |
|||
running bool |
|||
textures map[string]*Texture // cached texture PNG images
|
|||
|
|||
// Event channel. WASM subscribes to events asynchronously using the
|
|||
// JavaScript APIs, whereas SDL2 polls the event queue which orders them
|
|||
// all up for processing. This channel will order and queue the events.
|
|||
queue chan Event |
|||
} |
|||
|
|||
// New creates the Canvas Engine.
|
|||
func New(canvasID string) (*Engine, error) { |
|||
canvas := GetCanvas(canvasID) |
|||
|
|||
engine := &Engine{ |
|||
canvas: canvas, |
|||
startTime: time.Now(), |
|||
events: events.New(), |
|||
width: canvas.ClientW(), |
|||
height: canvas.ClientH(), |
|||
queue: make(chan Event, 1024), |
|||
textures: map[string]*Texture{}, |
|||
} |
|||
|
|||
return engine, nil |
|||
} |
|||
|
|||
// WindowSize returns the size of the canvas window.
|
|||
func (e *Engine) WindowSize() (w, h int) { |
|||
// Good time to recompute it first?
|
|||
var ( |
|||
window = js.Global().Get("window") |
|||
width = window.Get("innerWidth").Int() |
|||
height = window.Get("innerHeight").Int() |
|||
) |
|||
e.canvas.Value.Set("width", width) |
|||
e.canvas.Value.Set("height", height) |
|||
return e.canvas.ClientW(), e.canvas.ClientH() |
|||
} |
|||
|
|||
// GetTicks returns the number of milliseconds since the engine started.
|
|||
func (e *Engine) GetTicks() uint32 { |
|||
ms := time.Since(e.startTime) * time.Millisecond |
|||
return uint32(ms) |
|||
} |
|||
|
|||
// TO BE IMPLEMENTED...
|
|||
|
|||
func (e *Engine) Setup() error { |
|||
return nil |
|||
} |
|||
|
|||
func (e *Engine) Present() error { |
|||
return nil |
|||
} |
|||
|
|||
// Delay for a moment.
|
|||
func (e *Engine) Delay(delay uint32) { |
|||
time.Sleep(time.Duration(delay) * time.Millisecond) |
|||
} |
|||
|
|||
// Teardown tasks.
|
|||
func (e *Engine) Teardown() {} |
|||
|
|||
func (e *Engine) Loop() error { |
|||
return nil |
|||
} |
@ -0,0 +1,227 @@ |
|||
package canvas |
|||
|
|||
import ( |
|||
"syscall/js" |
|||
|
|||
"git.kirsle.net/apps/doodle/lib/events" |
|||
) |
|||
|
|||
// EventClass to categorize JavaScript events.
|
|||
type EventClass int |
|||
|
|||
// EventClass values.
|
|||
const ( |
|||
MouseEvent EventClass = iota |
|||
ClickEvent |
|||
KeyEvent |
|||
ResizeEvent |
|||
WindowEvent |
|||
) |
|||
|
|||
// Event object queues up asynchronous JavaScript events to be processed linearly.
|
|||
type Event struct { |
|||
Name string // mouseup, keydown, etc.
|
|||
Class EventClass |
|||
|
|||
// Mouse events.
|
|||
X int |
|||
Y int |
|||
LeftClick bool |
|||
RightClick bool |
|||
|
|||
// Key events.
|
|||
KeyName string |
|||
State bool |
|||
Repeat bool |
|||
} |
|||
|
|||
// AddEventListeners sets up bindings to collect events from the browser.
|
|||
func (e *Engine) AddEventListeners() { |
|||
// Window resize.
|
|||
js.Global().Get("window").Call( |
|||
"addEventListener", |
|||
"resize", |
|||
js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|||
e.queue <- Event{ |
|||
Name: "resize", |
|||
Class: WindowEvent, |
|||
} |
|||
return nil |
|||
}), |
|||
) |
|||
|
|||
// Mouse movement.
|
|||
e.canvas.Value.Call( |
|||
"addEventListener", |
|||
"mousemove", |
|||
js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|||
var ( |
|||
x = args[0].Get("pageX").Int() |
|||
y = args[0].Get("pageY").Int() |
|||
) |
|||
|
|||
e.queue <- Event{ |
|||
Name: "mousemove", |
|||
Class: MouseEvent, |
|||
X: x, |
|||
Y: y, |
|||
} |
|||
return nil |
|||
}), |
|||
) |
|||
|
|||
// Mouse clicks.
|
|||
for _, ev := range []string{"mouseup", "mousedown"} { |
|||
ev := ev |
|||
e.canvas.Value.Call( |
|||
"addEventListener", |
|||
ev, |
|||
js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|||
var ( |
|||
x = args[0].Get("pageX").Int() |
|||
y = args[0].Get("pageY").Int() |
|||
which = args[0].Get("which").Int() |
|||
) |
|||
|
|||
// Is a mouse button pressed down?
|
|||
checkDown := func(number int) bool { |
|||
if which == number { |
|||
return ev == "mousedown" |
|||
} |
|||
return false |
|||
} |
|||
|
|||
e.queue <- Event{ |
|||
Name: ev, |
|||
Class: ClickEvent, |
|||
X: x, |
|||
Y: y, |
|||
LeftClick: checkDown(1), |
|||
RightClick: checkDown(3), |
|||
} |
|||
return false |
|||
}), |
|||
) |
|||
} |
|||
|
|||
// Supress context menu.
|
|||
e.canvas.Value.Call( |
|||
"addEventListener", |
|||
"contextmenu", |
|||
js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|||
args[0].Call("preventDefault") |
|||
return false |
|||
}), |
|||
) |
|||
|
|||
// Keyboard keys
|
|||
for _, ev := range []string{"keydown", "keyup"} { |
|||
ev := ev |
|||
js.Global().Get("document").Call( |
|||
"addEventListener", |
|||
ev, |
|||
js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|||
var ( |
|||
event = args[0] |
|||
key = event.Get("key").String() |
|||
repeat = event.Get("repeat").Bool() |
|||
|
|||
pressed = ev == "keydown" |
|||
) |
|||
|
|||
if key == "F3" { |
|||
args[0].Call("preventDefault") |
|||
} |
|||
|
|||
e.queue <- Event{ |
|||
Name: ev, |
|||
Class: KeyEvent, |
|||
KeyName: key, |
|||
Repeat: repeat, |
|||
State: pressed, |
|||
} |
|||
return nil |
|||
}), |
|||
) |
|||
} |
|||
} |
|||
|
|||
// PollEvent returns the next event in the queue, or null.
|
|||
func (e *Engine) PollEvent() *Event { |
|||
select { |
|||
case event := <-e.queue: |
|||
return &event |
|||
default: |
|||
return nil |
|||
} |
|||
return nil |
|||
} |
|||
|
|||
// Poll for events.
|
|||
func (e *Engine) Poll() (*events.State, error) { |
|||
s := e.events |
|||
|
|||
for event := e.PollEvent(); event != nil; event = e.PollEvent() { |
|||
switch event.Class { |
|||
case WindowEvent: |
|||
s.Resized.Push(true) |
|||
case MouseEvent: |
|||
s.CursorX.Push(int32(event.X)) |
|||
s.CursorY.Push(int32(event.Y)) |
|||
case ClickEvent: |
|||
s.CursorX.Push(int32(event.X)) |
|||
s.CursorY.Push(int32(event.Y)) |
|||
s.Button1.Push(event.LeftClick) |
|||
s.Button2.Push(event.RightClick) |
|||
case KeyEvent: |
|||
switch event.KeyName { |
|||
case "Escape": |
|||
if event.Repeat { |
|||
continue |
|||
} |
|||
|
|||
if event.State { |
|||
s.EscapeKey.Push(true) |
|||
} |
|||
case "Enter": |
|||
if event.Repeat { |
|||
continue |
|||
} |
|||
|
|||
if event.State { |
|||
s.EnterKey.Push(true) |
|||
} |
|||
case "F3": |
|||
if event.State { |
|||
s.KeyName.Push("F3") |
|||
} |
|||
case "ArrowUp": |
|||
s.Up.Push(event.State) |
|||
case "ArrowLeft": |
|||
s.Left.Push(event.State) |
|||
case "ArrowRight": |
|||
s.Right.Push(event.State) |
|||
case "ArrowDown": |
|||
s.Down.Push(event.State) |
|||
case "Shift": |
|||
s.ShiftActive.Push(event.State) |
|||
continue |
|||
case "Alt": |
|||
case "Control": |
|||
continue |
|||
case "Backspace": |
|||
if event.State { |
|||
s.KeyName.Push(`\b`) |
|||
} |
|||
default: |
|||
if event.State { |
|||
s.KeyName.Push(event.KeyName) |
|||
} else { |
|||
s.KeyName.Push("") |
|||
} |
|||
} |
|||
} |
|||
} |
|||
|
|||
return e.events, nil |
|||
} |
@ -0,0 +1,88 @@ |
|||
package canvas |
|||
|
|||
// Text rendering functions using the HTML 5 canvas.
|
|||
|
|||
import ( |
|||
"fmt" |
|||
"path/filepath" |
|||
"strings" |
|||
|
|||
"git.kirsle.net/apps/doodle/lib/render" |
|||
) |
|||
|
|||
// FontFilenameToName converts a FontFilename to its CSS font name.
|
|||
//
|
|||
// The CSS font name is set to the base of the filename, without the .ttf
|
|||
// file extension. For example, "fonts/DejaVuSans.ttf" uses the CSS font
|
|||
// family name "DejaVuSans" and that's what this function returns.
|
|||
//
|
|||
// Fonts must be defined in the index.html style sheet when serving the
|
|||
// wasm build of Doodle.
|
|||
//
|
|||
// If filename is "", returns "serif" as a sensible default.
|
|||
func FontFilenameToName(filename string) string { |
|||
if filename == "" { |
|||
return "DejaVuSans,serif" |
|||
} |
|||
return strings.TrimSuffix(filepath.Base(filename), filepath.Ext(filename)) |
|||
} |
|||
|
|||
// DrawText draws text on the canvas.
|
|||
func (e *Engine) DrawText(text render.Text, point render.Point) error { |
|||
font := FontFilenameToName(text.FontFilename) |
|||
e.canvas.ctx2d.Set("font", |
|||
fmt.Sprintf("%dpx %s,serif", text.Size, font), |
|||
) |
|||
|
|||
e.canvas.ctx2d.Set("textBaseline", "top") |
|||
|
|||
write := func(dx, dy int, color render.Color) { |
|||
e.canvas.ctx2d.Set("fillStyle", color.ToHex()) |
|||
e.canvas.ctx2d.Call("fillText", |
|||
text.Text, |
|||
int(point.X)+dx, |
|||
int(point.Y)+dy, |
|||
) |
|||
} |
|||
|
|||
// Does the text have a stroke around it?
|
|||
if text.Stroke != render.Invisible { |
|||
e.canvas.ctx2d.Set("fillStyle", text.Stroke.ToHex()) |
|||
write(-1, -1, text.Stroke) |
|||
write(-1, 0, text.Stroke) |
|||
write(-1, 1, text.Stroke) |
|||
write(1, -1, text.Stroke) |
|||
write(1, 0, text.Stroke) |
|||
write(1, 1, text.Stroke) |
|||
write(0, -1, text.Stroke) |
|||
write(0, 1, text.Stroke) |
|||
} |
|||
|
|||
// Does it have a drop shadow?
|
|||
if text.Shadow != render.Invisible { |
|||
write(1, 1, text.Shadow) |
|||
} |
|||
|
|||
// Draw the text itself.
|
|||
write(0, 0, text.Color) |
|||
|
|||
return nil |
|||
} |
|||
|
|||
// ComputeTextRect computes and returns a Rect for how large the text would
|
|||
// appear if rendered.
|
|||
func (e *Engine) ComputeTextRect(text render.Text) (render.Rect, error) { |
|||
font := FontFilenameToName(text.FontFilename) |
|||
e.canvas.ctx2d.Set("font", |
|||
fmt.Sprintf("%dpx %s,serif", text.Size, font), |
|||
) |
|||
|
|||
measure := e.canvas.ctx2d.Call("measureText", text.Text) |
|||
rect := render.Rect{ |
|||
// TODO: the only TextMetrics widely supported in browsers is
|
|||
// the width. For height, use the text size for now.
|
|||
W: int32(measure.Get("width").Int()), |
|||
H: int32(text.Size), |
|||
} |
|||
return rect, nil |
|||
} |
@ -0,0 +1,91 @@ |
|||
package canvas |
|||
|
|||
import ( |
|||
"bytes" |
|||
"encoding/base64" |
|||
"errors" |
|||
"image" |
|||
"image/png" |
|||
"syscall/js" |
|||
|
|||
"git.kirsle.net/apps/doodle/lib/render" |
|||
) |
|||
|
|||
// Texture can hold on to cached image textures.
|
|||
type Texture struct { |
|||
data string // data:image/png URI
|
|||
image js.Value // DOM image element
|
|||
canvas js.Value // Warmed up canvas element
|
|||
ctx2d js.Value // 2D drawing context for the canvas.
|
|||
width int |
|||
height int |
|||
} |
|||
|
|||
// StoreTexture caches a texture from a bitmap.
|
|||
func (e *Engine) StoreTexture(name string, img image.Image) (render.Texturer, error) { |
|||
var ( |
|||
fh = bytes.NewBuffer([]byte{}) |
|||
imageSize = img.Bounds().Size() |
|||
width = imageSize.X |
|||
height = imageSize.Y |
|||
) |
|||
|
|||
// Encode to PNG format.
|
|||
if err := png.Encode(fh, img); err != nil { |
|||
return nil, err |
|||
} |
|||
|
|||
var dataURI = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fh.Bytes()) |
|||
|
|||
tex := &Texture{ |
|||
data: dataURI, |
|||
width: width, |
|||
height: height, |
|||
} |
|||
|
|||
// Preheat a cached Canvas object.
|
|||
canvas := js.Global().Get("document").Call("createElement", "canvas") |
|||
canvas.Set("width", width) |
|||
canvas.Set("height", height) |
|||
tex.canvas = canvas |
|||
|
|||
ctx2d := canvas.Call("getContext", "2d") |
|||
tex.ctx2d = ctx2d |
|||
|
|||
// Load as a JS Image object.
|
|||
image := js.Global().Call("eval", "new Image()") |
|||
image.Call("addEventListener", "load", js.FuncOf(func(this js.Value, args []js.Value) interface{} { |
|||
ctx2d.Call("drawImage", image, 0, 0) |
|||
return nil |
|||
})) |
|||
image.Set("src", tex.data) |
|||
tex.image = image |
|||
|
|||
// Cache the texture in memory.
|
|||
e.textures[name] = tex |
|||
|
|||
return tex, nil |
|||
} |
|||
|
|||
// Size returns the dimensions of the texture.
|
|||
func (t *Texture) Size() render.Rect { |
|||
return render.NewRect(int32(t.width), int32(t.height)) |
|||
} |
|||
|
|||
// LoadTexture recalls a cached texture image.
|
|||
func (e *Engine) LoadTexture(name string) (render.Texturer, error) { |
|||
if tex, ok := e.textures[name]; ok { |
|||
return tex, nil |
|||
} |
|||
|
|||
return nil, errors.New("no bitmap data stored for " + name) |
|||
} |
|||
|
|||
// Copy a texturer bitmap onto the canvas.
|
|||
func (e *Engine) Copy(t render.Texturer, src, dist render.Rect) { |
|||
tex := t.(*Texture) |
|||
|
|||
// e.canvas.ctx2d.Call("drawImage", tex.image, dist.X, dist.Y)
|
|||
e.canvas.ctx2d.Call("drawImage", tex.canvas, dist.X, dist.Y) |
|||
|
|||
} |
@ -0,0 +1,274 @@ |
|||
package render |
|||
|
|||
import ( |
|||
"encoding/json" |
|||
"errors" |
|||
"fmt" |
|||
"image/color" |
|||
"regexp" |
|||
"strconv" |
|||
|
|||
"github.com/vmihailenco/msgpack" |
|||
) |
|||
|
|||
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, |
|||
) |
|||
} |
|||
|
|||
// ToHex converts a render.Color to standard #RRGGBB hexadecimal format.
|
|||
func (c Color) ToHex() string { |
|||
return fmt.Sprintf( |
|||
"#%02x%02x%02x", |
|||
c.Red, c.Green, c.Blue, |
|||
) |
|||
} |
|||
|
|||
// 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 |
|||
} |
|||
|
|||
func (c Color) EncodeMsgpack(enc *msgpack.Encoder) error { |
|||
return enc.EncodeString(fmt.Sprintf( |
|||
`"#%02x%02x%02x"`, |
|||
c.Red, c.Green, c.Blue, |
|||
)) |
|||
} |
|||
|
|||
func (c Color) DecodeMsgpack(dec *msgpack.Decoder) error { |
|||
hex, err := dec.DecodeString() |
|||
if err != nil { |
|||
return fmt.Errorf("Color.DecodeMsgpack: %s", err) |
|||
} |
|||
|
|||
parsed, err := HexColor(hex) |
|||
if err != nil { |
|||
return fmt.Errorf("Color.DecodeMsgpack: HexColor: %s", err) |
|||
} |
|||
|
|||
c.Red = parsed.Red |
|||
c.Blue = parsed.Blue |
|||
c.Green = parsed.Green |
|||
c.Alpha = parsed.Alpha |
|||
return nil |
|||
} |
|||
|
|||
// // MarshalMsgpack serializes the Color for msgpack.
|
|||
// func (c Color) MarshalMsgpack() ([]byte, error) {
|
|||
// data := []uint8{
|
|||
// c.Red, c.Green, c.Blue, c.Alpha,
|
|||
// }
|
|||
// return msgpack.Marshal(data)
|
|||
// }
|
|||
//
|
|||
// // UnmarshalMsgpack decodes a Color from msgpack format.
|
|||
// func (c *Color) UnmarshalMsgpack(b []byte) error {
|
|||
// var data []uint8
|
|||
// if err := msgpack.Unmarshal(data, b); err != nil {
|
|||
// return err
|
|||
// }
|
|||
// c.Red = 255
|
|||
// c.Green = data[1]
|
|||
// c.Blue = data[2]
|
|||
// c.Alpha = data[3]
|
|||
// return nil
|
|||
// }
|
|||
|
|||
// IsZero returns if the color is all zeroes (invisible).
|
|||
func (c Color) IsZero() bool { |
|||
return c.Red+c.Green+c.Blue+c.Alpha == 0 |
|||
} |
|||
|
|||
// Add a relative color value to the color.
|
|||
func (c Color) Add(r, g, b, a int) Color { |
|||
var ( |
|||
R = int(c.Red) + r |
|||
G = int(c.Green) + g |
|||
B = int(c.Blue) + b |
|||
A = int(c.Alpha) + a |
|||
) |
|||
|
|||
cap8 := func(v int) 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), |
|||
} |
|||
} |
|||
|
|||
// AddColor adds another Color to your Color.
|
|||
func (c Color) AddColor(other Color) Color { |
|||
return c.Add( |
|||
int(other.Red), |
|||
int(other.Green), |
|||
int(other.Blue), |
|||
int(other.Alpha), |
|||
) |
|||
} |
|||
|
|||
// Lighten a color value.
|
|||
func (c Color) Lighten(v int) Color { |
|||
return c.Add(v, v, v, 0) |
|||
} |
|||
|
|||
// Darken a color value.
|
|||
func (c Color) Darken(v int) Color { |
|||
return c.Add(-v, -v, -v, 0) |
|||
} |
|||
|
|||
// Transparentize adjusts the alpha value.
|
|||
func (c Color) Transparentize(v int) Color { |
|||
return c.Add(0, 0, 0, v) |
|||
} |
|||
|
|||
// SetAlpha sets the alpha value to a specific setting.
|
|||
func (c Color) SetAlpha(v uint8) Color { |
|||
c.Alpha = v |
|||
return c |
|||
} |
@ -0,0 +1,64 @@ |
|||
package render |
|||
|
|||
// MidpointEllipse implements an ellipse plotting algorithm.
|
|||
func MidpointEllipse(center, radius Point) chan Point { |
|||
yield := make(chan Point) |
|||
go func() { |
|||
|
|||
var ( |
|||
pos = NewPoint(radius.X, 0) |
|||
delta = NewPoint( |
|||
2*radius.Y*radius.Y*pos.X, |
|||
2*radius.X*radius.X*pos.Y, |
|||
) |
|||
err = radius.X*radius.X - |
|||
radius.Y*radius.Y*radius.X + |
|||
(radius.Y*radius.Y)/4 |
|||
) |
|||
|
|||
for delta.Y < delta.X { |
|||
yield <- NewPoint(center.X+pos.X, center.Y+pos.Y) |
|||
yield <- NewPoint(center.X+pos.X, center.Y-pos.Y) |
|||
yield <- NewPoint(center.X-pos.X, center.Y+pos.Y) |
|||
yield <- NewPoint(center.X-pos.X, center.Y-pos.Y) |
|||
|
|||
pos.Y++ |
|||
|
|||
if err < 0 { |
|||
delta.Y += 2 * radius.X * radius.X |
|||
err += delta.Y + radius.X*radius.X |
|||
} else { |
|||
pos.X-- |
|||
delta.Y += 2 * radius.X * radius.X |
|||
delta.X -= 2 * radius.Y * radius.Y |
|||
err += delta.Y - delta.X + radius.X*radius.X |
|||
} |
|||
} |
|||
|
|||
err = radius.X*radius.X*(pos.Y*pos.Y+pos.Y) + |
|||
radius.Y*radius.Y*(pos.X-1)*(pos.X-1) - |
|||
radius.Y*radius.Y*radius.X*radius.X |
|||
|
|||
for pos.X >= 0 { |
|||
yield <- NewPoint(center.X+pos.X, center.Y+pos.Y) |
|||
yield <- NewPoint(center.X+pos.X, center.Y-pos.Y) |
|||
yield <- NewPoint(center.X-pos.X, center.Y+pos.Y) |
|||
yield <- NewPoint(center.X-pos.X, center.Y-pos.Y) |
|||
|
|||
pos.X-- |
|||
|
|||
if err > 0 { |
|||
delta.X -= 2 * radius.Y * radius.Y |
|||
err += radius.Y*radius.Y - delta.X |
|||
} else { |
|||
pos.Y++ |
|||
delta.Y += 2 * radius.X * radius.X |
|||
delta.X -= 2 * radius.Y * radius.Y |
|||
err += delta.Y - delta.X + radius.Y*radius.Y |
|||
} |
|||
} |
|||
|
|||
close(yield) |
|||
}() |
|||
return yield |
|||
} |
@ -0,0 +1,108 @@ |
|||
package render |
|||
|
|||
import ( |
|||
"fmt" |
|||
"regexp" |
|||
"strconv" |
|||
) |
|||
|
|||
var regexpResolution = regexp.MustCompile(`^(\d+)x(\d+)$`) |
|||
|
|||
// ParseResolution turns a resolution string like "1024x768" and returns the
|
|||
// width and height values.
|
|||
func ParseResolution(resi string) (int, int, error) { |
|||
m := regexpResolution.FindStringSubmatch(resi) |
|||
if m == nil { |
|||
return 0, 0, fmt.Errorf("invalid resolution format, should be %s", |
|||
regexpResolution.String(), |
|||
) |
|||
} |
|||
|
|||
width, err := strconv.Atoi(m[1]) |
|||
if err != nil { |
|||
return 0, 0, err |
|||
} |
|||
|
|||
height, err := strconv.Atoi(m[2]) |
|||
if err != nil { |
|||
return 0, 0, err |
|||
} |
|||
|
|||
return width, height, nil |
|||
} |
|||
|
|||
// 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 |
|||
} |
|||
} |
|||
|
|||
// AbsInt32 returns the absolute value of an int32.
|
|||
func AbsInt32(v int32) int32 { |
|||
if v < 0 { |
|||
return -v |
|||
} |
|||
return v |
|||
} |
@ -0,0 +1,196 @@ |
|||
package render |
|||
|
|||
import ( |
|||
"fmt" |
|||
"image" |
|||
|
|||
"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) |
|||
SetTitle(string) |
|||
DrawPoint(Color, Point) |
|||
DrawLine(Color, Point, Point) |
|||
DrawRect(Color, Rect) |
|||
DrawBox(Color, Rect) |
|||
DrawText(Text, Point) error |
|||
ComputeTextRect(Text) (Rect, error) |
|||
|
|||
// Texture caching.
|
|||
StoreTexture(name string, img image.Image) (Texturer, error) |
|||
LoadTexture(name 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, |
|||
} |
|||
} |
|||
|
|||
// SubtractPoint is the inverse of AddPoint. Use this only if you need to invert
|
|||
// the Point being added.
|
|||
//
|
|||
// This does r.X - other.X, r.Y - other.Y and keeps the width/height the same.
|
|||
func (r Rect) SubtractPoint(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) |
|||
Orange = RGBA(255, 153, 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) |
|||
) |
@ -0,0 +1,109 @@ |
|||
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 |
|||
} |
|||
|
|||
// Subtract the other point from your current point.
|
|||
func (p *Point) Subtract(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,81 @@ |
|||
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
|
|||
}, |
|||
{ |
|||
A: newRect(0, 30, 9, 62), |
|||
B: newRect(16, 0, 32, 64), |
|||
Expect: false, |
|||
}, |
|||
{ |
|||
A: newRect(0, 30, 11, 62), |
|||
B: newRect(7, 4, 17, 28), |
|||
Expect: false, |
|||
}, |
|||
} |
|||
|
|||
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,171 @@ |
|||
package sdl |
|||
|
|||
import ( |
|||
"errors" |
|||
"fmt" |
|||
|
|||
"git.kirsle.net/apps/doodle/lib/events" |
|||
"github.com/veandco/go-sdl2/sdl" |
|||
) |
|||
|
|||
// Debug certain SDL events
|
|||
var ( |
|||
DebugWindowEvents = false |
|||
DebugMouseEvents = false |
|||
DebugClickEvents = false |
|||
DebugKeyEvents = false |
|||
) |
|||
|
|||
// Poll for events.
|
|||
func (r *Renderer) Poll() (*events.State, error) { |
|||
s := r.events |
|||
|
|||
// helper function to push keyboard key names on keyDown events only.
|
|||
pushKey := func(name string, state uint8) { |
|||
if state == 1 { |
|||
s.KeyName.Push(name) |
|||
} |
|||
} |
|||
|
|||
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 { |
|||
fmt.Printf("[%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 { |
|||
fmt.Printf("[%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 == |