diff --git a/README.md b/README.md new file mode 100644 index 0000000..fde0e4b --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# Render: Go Graphics Library + +Render is a graphics library written in Go which targets desktop applications on +Windows, MacOS and Linux as well as WebAssembly to run in the browser. + +For desktop systems it uses SDL2 under the hood, and in WebAssembly it interacts +with an HTML Canvas element for drawing. diff --git a/canvas/engine.go b/canvas/engine.go index 5f7f49e..ab458cf 100644 --- a/canvas/engine.go +++ b/canvas/engine.go @@ -4,7 +4,7 @@ import ( "syscall/js" "time" - "git.kirsle.net/apps/doodle/lib/events" + "git.kirsle.net/apps/doodle/lib/render/event" ) // Engine implements a rendering engine targeting an HTML canvas for @@ -17,7 +17,7 @@ type Engine struct { ticks uint32 // Private fields. - events *events.State + events *event.State running bool textures map[string]*Texture // cached texture PNG images @@ -34,7 +34,7 @@ func New(canvasID string) (*Engine, error) { engine := &Engine{ canvas: canvas, startTime: time.Now(), - events: events.New(), + events: event.NewState(), width: canvas.ClientW(), height: canvas.ClientH(), queue: make(chan Event, 1024), diff --git a/canvas/events.go b/canvas/events.go index 4cc4905..7b20f91 100644 --- a/canvas/events.go +++ b/canvas/events.go @@ -3,7 +3,7 @@ package canvas import ( "syscall/js" - "git.kirsle.net/apps/doodle/lib/events" + "git.kirsle.net/apps/doodle/lib/render/event" ) // EventClass to categorize JavaScript events. @@ -158,21 +158,21 @@ func (e *Engine) PollEvent() *Event { } // Poll for events. -func (e *Engine) Poll() (*events.State, error) { +func (e *Engine) Poll() (*event.State, error) { s := e.events for event := e.PollEvent(); event != nil; event = e.PollEvent() { switch event.Class { case WindowEvent: - s.Resized.Push(true) + s.WindowResized = true case MouseEvent: - s.CursorX.Push(int32(event.X)) - s.CursorY.Push(int32(event.Y)) + s.CursorX = event.X + s.CursorY = 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) + s.CursorX = event.X + s.CursorY = event.Y + s.Button1 = event.LeftClick + s.Button2 = event.RightClick case KeyEvent: switch event.KeyName { case "Escape": @@ -180,45 +180,34 @@ func (e *Engine) Poll() (*events.State, error) { continue } - if event.State { - s.EscapeKey.Push(true) - } + s.Escape = event.State case "Enter": if event.Repeat { continue } - if event.State { - s.EnterKey.Push(true) - } + s.Enter = event.State case "F3": - if event.State { - s.KeyName.Push("F3") - } + s.SetKeyDown("F3", event.State) case "ArrowUp": - s.Up.Push(event.State) + s.Up = event.State case "ArrowLeft": - s.Left.Push(event.State) + s.Left = event.State case "ArrowRight": - s.Right.Push(event.State) + s.Right = event.State case "ArrowDown": - s.Down.Push(event.State) + s.Down = event.State case "Shift": - s.ShiftActive.Push(event.State) + s.Shift = event.State continue case "Alt": + s.Alt = event.State case "Control": - continue + s.Ctrl = event.State case "Backspace": - if event.State { - s.KeyName.Push(`\b`) - } + s.SetKeyDown(`\b`, event.State) default: - if event.State { - s.KeyName.Push(event.KeyName) - } else { - s.KeyName.Push("") - } + s.SetKeyDown(event.KeyName, event.State) } } } diff --git a/color.go b/color.go index e6b045d..70de13b 100644 --- a/color.go +++ b/color.go @@ -7,8 +7,6 @@ import ( "image/color" "regexp" "strconv" - - "github.com/vmihailenco/msgpack" ) var ( @@ -165,52 +163,6 @@ func (c *Color) UnmarshalJSON(b []byte) error { 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 diff --git a/event/event.go b/event/event.go new file mode 100644 index 0000000..1787e4c --- /dev/null +++ b/event/event.go @@ -0,0 +1,109 @@ +package event + +import "strings" + +// State holds the current state of key/mouse events. +type State struct { + // Mouse buttons. + Button1 bool // Left + Button2 bool // Middle + Button3 bool // Right + + // Special keys + Escape bool + Enter bool + Shift bool + Ctrl bool + Alt bool + Up bool + Left bool + Right bool + Down bool + + // Pressed keys. + keydown map[string]interface{} + + // Cursor position + CursorX int + CursorY int + + // Window resized + WindowResized bool +} + +// NewState creates a new event.State. +func NewState() *State { + return &State{ + keydown: map[string]interface{}{}, + } +} + +// SetKeyDown sets that a named key is pressed down. +func (s *State) SetKeyDown(name string, down bool) { + if down { + s.keydown[name] = nil + } else { + delete(s.keydown, name) + } +} + +// KeyDown returns whether a named key is currently pressed. +func (s *State) KeyDown(name string) bool { + _, ok := s.keydown[name] + return ok +} + +// KeysDown returns a list of all key names currently pressed down. +// Set shifted to True to return the key symbols correctly shifted +// (uppercase, or symbols on number keys, etc.) +func (s *State) KeysDown(shifted bool) []string { + var ( + result = make([]string, len(s.keydown)) + i = 0 + ) + + for key := range s.keydown { + if shifted && s.Shift { + if symbol, ok := shiftMap[key]; ok { + result[i] = symbol + } else { + result[i] = strings.ToUpper(key) + } + } else { + result[i] = key + } + + i++ + } + return result +} + +// ResetKeyDown clears all key-down states. +func (s *State) ResetKeyDown() { + s.keydown = map[string]interface{}{} +} + +// shiftMap maps keys to their Shift versions. +var shiftMap = map[string]string{ + "`": "~", + "1": "!", + "2": "@", + "3": "#", + "4": "$", + "5": "%", + "6": "^", + "7": "&", + "8": "*", + "9": "(", + "0": ")", + "-": "_", + "=": "+", + "[": "{", + "]": "}", + `\`: "|", + ";": ":", + `'`: `"`, + ",": "<", + ".": ">", + "/": "?", +} diff --git a/interface.go b/interface.go index c30948a..b97e289 100644 --- a/interface.go +++ b/interface.go @@ -4,7 +4,7 @@ import ( "fmt" "image" - "git.kirsle.net/apps/doodle/lib/events" + "git.kirsle.net/apps/doodle/lib/render/event" ) // Engine is the interface for the rendering engine, keeping SDL-specific stuff @@ -13,7 +13,7 @@ type Engine interface { Setup() error // Poll for events like keypresses and mouse clicks. - Poll() (*events.State, error) + Poll() (*event.State, error) GetTicks() uint32 WindowSize() (w, h int) diff --git a/sdl/events.go b/sdl/events.go index 152c906..a5b8014 100644 --- a/sdl/events.go +++ b/sdl/events.go @@ -4,7 +4,7 @@ import ( "errors" "fmt" - "git.kirsle.net/apps/doodle/lib/events" + "git.kirsle.net/apps/doodle/lib/render/event" "github.com/veandco/go-sdl2/sdl" ) @@ -12,19 +12,17 @@ import ( var ( DebugWindowEvents = false DebugMouseEvents = false - DebugClickEvents = false + DebugClickEvents = true DebugKeyEvents = false ) // Poll for events. -func (r *Renderer) Poll() (*events.State, error) { +func (r *Renderer) Poll() (*event.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) - } + s.SetKeyDown(name, state == 1) } for event := sdl.PollEvent(); event != nil; event = sdl.PollEvent() { @@ -42,7 +40,10 @@ func (r *Renderer) Poll() (*events.State, error) { ) } } - s.Resized.Push(true) + + if t.Event == sdl.WINDOWEVENT_RESIZED { + s.WindowResized = 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", @@ -51,42 +52,50 @@ func (r *Renderer) Poll() (*events.State, error) { } // Push the cursor position. - s.CursorX.Push(t.X) - s.CursorY.Push(t.Y) - s.Button1.Push(t.State == 1) + s.CursorX = int(t.X) + s.CursorY = int(t.Y) case *sdl.MouseButtonEvent: if DebugClickEvents { - fmt.Printf("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d", + fmt.Printf("[%d ms] tick:%d MouseButton type:%d id:%d x:%d y:%d button:%d state:%d\n", 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) + s.CursorX = int(t.X) + s.CursorY = int(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) || checkDown(2, s.Button3) { - // Return the event immediately. - return s, nil + // Store the clicked state of the mouse button. + if t.Button == 1 { + s.Button1 = t.State == 1 + } else if t.Button == 2 { + s.Button2 = t.State == 1 + } else if t.Button == 3 { + s.Button3 = t.State == 1 } + // + // // 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) || checkDown(2, s.Button3) { + // // Return the event immediately. + // return s, nil + // } case *sdl.MouseWheelEvent: if DebugMouseEvents { fmt.Printf("[%d ms] tick:%d MouseWheel type:%d id:%d x:%d y:%d", @@ -105,12 +114,12 @@ func (r *Renderer) Poll() (*events.State, error) { if t.Repeat == 1 { continue } - s.EscapeKey.Push(t.State == 1) + s.Escape = t.State == 1 case sdl.SCANCODE_RETURN: if t.Repeat == 1 { continue } - s.EnterKey.Push(t.State == 1) + s.Enter = t.State == 1 case sdl.SCANCODE_F1: pushKey("F1", t.State) case sdl.SCANCODE_F2: @@ -136,33 +145,29 @@ func (r *Renderer) Poll() (*events.State, error) { case sdl.SCANCODE_F12: pushKey("F12", t.State) case sdl.SCANCODE_UP: - s.Up.Push(t.State == 1) + s.Up = t.State == 1 case sdl.SCANCODE_LEFT: - s.Left.Push(t.State == 1) + s.Left = t.State == 1 case sdl.SCANCODE_RIGHT: - s.Right.Push(t.State == 1) + s.Right = t.State == 1 case sdl.SCANCODE_DOWN: - s.Down.Push(t.State == 1) + s.Down = t.State == 1 case sdl.SCANCODE_LSHIFT: case sdl.SCANCODE_RSHIFT: - s.ShiftActive.Push(t.State == 1) + s.Shift = t.State == 1 case sdl.SCANCODE_LALT: case sdl.SCANCODE_RALT: - continue + s.Alt = t.State == 1 case sdl.SCANCODE_LCTRL: - s.ControlActive.Push(t.State == 1) + s.Ctrl = t.State == 1 case sdl.SCANCODE_RCTRL: - s.ControlActive.Push(t.State == 1) + s.Ctrl = t.State == 1 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`) - } + s.SetKeyDown(`\b`, t.State == 1 || t.Repeat == 1) default: // Push the string value of the key. - if t.State == 1 { - s.KeyName.Push(string(t.Keysym.Sym)) - } + s.SetKeyDown(string(t.Keysym.Sym), t.State == 1) } } } diff --git a/sdl/sdl.go b/sdl/sdl.go index c54767f..35799ef 100644 --- a/sdl/sdl.go +++ b/sdl/sdl.go @@ -5,8 +5,8 @@ import ( "fmt" "time" - "git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/lib/render/event" "github.com/veandco/go-sdl2/sdl" "github.com/veandco/go-sdl2/ttf" ) @@ -20,7 +20,7 @@ type Renderer struct { startTime time.Time // Private fields. - events *events.State + events *event.State window *sdl.Window renderer *sdl.Renderer running bool @@ -34,7 +34,7 @@ type Renderer struct { // New creates the SDL renderer. func New(title string, width, height int) *Renderer { return &Renderer{ - events: events.New(), + events: event.NewState(), title: title, width: int32(width), height: int32(height), diff --git a/sdl/text.go b/sdl/text.go index efe59fd..1347254 100644 --- a/sdl/text.go +++ b/sdl/text.go @@ -2,10 +2,8 @@ package sdl import ( "fmt" - "strings" "sync" - "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" @@ -77,20 +75,6 @@ func LoadFont(filename string, size int) (*ttf.Font, error) { 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) {