From 27b908e40aee447dba25811992ac802ff62b7435 Mon Sep 17 00:00:00 2001 From: Noah Petherbridge Date: Wed, 26 Jun 2019 22:44:08 -0700 Subject: [PATCH] Texture Caching for WASM Canvas Engine * Add RGBA color blending support in WASM build. * Initial texture caching API for Canvas renderer engine. The WASM build writes the chunk caches as a "data:image/png" base64 URL on the browser's sessionStorage, for access to copy into the Canvas. * Separated the ClickEvent from the MouseEvent (motion) in the WASM event queue system, to allow clicking and dragging. * Added the EscapeKey handler, which will abruptly terminate the WASM application, same as it kills the window in the desktop build. * Optimization fix: I discovered that if the user clicks and holds over a single pixel when drawing a level, repeated Set() operations were firing meaning multiple cache invalidations. Not noticeable on PC but on WebAssembly it crippled the browser. Now if the cursor isn't moving it doesn't do anything. --- canvas/draw.go | 21 +++++++++++++++----- canvas/engine.go | 50 +++++++++++++++++++++++++++++++++++++++++++++--- canvas/events.go | 27 +++++++++++++------------- 3 files changed, 76 insertions(+), 22 deletions(-) diff --git a/canvas/draw.go b/canvas/draw.go index fd8d8d2..1f7b8be 100644 --- a/canvas/draw.go +++ b/canvas/draw.go @@ -1,6 +1,7 @@ package canvas import ( + "fmt" "syscall/js" "git.kirsle.net/apps/doodle/lib/render" @@ -8,9 +9,19 @@ import ( // 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", color.ToHex()) + e.canvas.ctx2d.Set("fillStyle", RGBA(color)) e.canvas.ctx2d.Call("fillRect", 0, 0, e.width, e.height) } @@ -21,7 +32,7 @@ func (e *Engine) SetTitle(title string) { // DrawPoint draws a pixel. func (e *Engine) DrawPoint(color render.Color, point render.Point) { - e.canvas.ctx2d.Set("fillStyle", color.ToHex()) + e.canvas.ctx2d.Set("fillStyle", RGBA(color)) e.canvas.ctx2d.Call("fillRect", int(point.X), int(point.Y), @@ -32,7 +43,7 @@ func (e *Engine) DrawPoint(color render.Color, point render.Point) { // DrawLine draws a line between two points. func (e *Engine) DrawLine(color render.Color, a, b render.Point) { - e.canvas.ctx2d.Set("fillStyle", color.ToHex()) + e.canvas.ctx2d.Set("fillStyle", RGBA(color)) for pt := range render.IterLine2(a, b) { e.canvas.ctx2d.Call("fillRect", int(pt.X), @@ -45,7 +56,7 @@ func (e *Engine) DrawLine(color render.Color, a, b render.Point) { // DrawRect draws a rectangle. func (e *Engine) DrawRect(color render.Color, rect render.Rect) { - e.canvas.ctx2d.Set("strokeStyle", color.ToHex()) + e.canvas.ctx2d.Set("strokeStyle", RGBA(color)) e.canvas.ctx2d.Call("strokeRect", int(rect.X), int(rect.Y), @@ -56,7 +67,7 @@ func (e *Engine) DrawRect(color render.Color, rect render.Rect) { // DrawBox draws a filled rectangle. func (e *Engine) DrawBox(color render.Color, rect render.Rect) { - e.canvas.ctx2d.Set("fillStyle", color.ToHex()) + e.canvas.ctx2d.Set("fillStyle", RGBA(color)) e.canvas.ctx2d.Call("fillRect", int(rect.X), int(rect.Y), diff --git a/canvas/engine.go b/canvas/engine.go index 09b7f83..5368083 100644 --- a/canvas/engine.go +++ b/canvas/engine.go @@ -1,10 +1,13 @@ package canvas import ( + "errors" + "syscall/js" "time" "git.kirsle.net/apps/doodle/lib/events" "git.kirsle.net/apps/doodle/lib/render" + "git.kirsle.net/apps/doodle/pkg/wasm" ) // Engine implements a rendering engine targeting an HTML canvas for @@ -63,12 +66,53 @@ func (e *Engine) Present() error { return nil } -func (e *Engine) NewBitmap(filename string) (render.Texturer, error) { - return nil, nil +// Texture can hold on to cached image textures. +type Texture struct { + data string // data:image/png URI + image js.Value // DOM image element + width int + height int } -func (e *Engine) Copy(t render.Texturer, src, dist render.Rect) { +// Size returns the dimensions of the texture. +func (t *Texture) Size() render.Rect { + return render.NewRect(int32(t.width), int32(t.height)) +} +// NewBitmap initializes a texture from a bitmap image. The image is stored +// in HTML5 Session Storage. +func (e *Engine) NewBitmap(filename string) (render.Texturer, error) { + if data, ok := wasm.GetSession(filename); ok { + img := js.Global().Get("document").Call("createElement", "img") + img.Set("src", data) + return &Texture{ + data: data, + image: img, + width: 60, // TODO + height: 60, + }, nil + } + + return nil, errors.New("no bitmap data stored for " + filename) + +} + +var TODO int + +// Copy a texturer bitmap onto the canvas. +func (e *Engine) Copy(t render.Texturer, src, dist render.Rect) { + tex := t.(*Texture) + + // image := js.Global().Get("document").Call("createElement", "img") + // image.Set("src", tex.data) + + // log.Info("drawing image just this once") + e.canvas.ctx2d.Call("drawImage", tex.image, dist.X, dist.Y) + // TODO++ + // if TODO > 200 { + // log.Info("I exited at engine.Copy for canvas engine") + // os.Exit(0) + // } } // Delay for a moment. diff --git a/canvas/events.go b/canvas/events.go index fc8d5c5..f21dade 100644 --- a/canvas/events.go +++ b/canvas/events.go @@ -4,7 +4,6 @@ import ( "syscall/js" "git.kirsle.net/apps/doodle/lib/events" - "git.kirsle.net/apps/doodle/pkg/log" ) // EventClass to categorize JavaScript events. @@ -13,6 +12,7 @@ type EventClass int // EventClass values. const ( MouseEvent EventClass = iota + ClickEvent KeyEvent ResizeEvent ) @@ -69,8 +69,6 @@ func (e *Engine) AddEventListeners() { which = args[0].Get("which").Int() ) - log.Info("Clicked at %d,%d", x, y) - // Is a mouse button pressed down? checkDown := func(number int) bool { if which == number { @@ -81,7 +79,7 @@ func (e *Engine) AddEventListeners() { e.queue <- Event{ Name: ev, - Class: MouseEvent, + Class: ClickEvent, X: x, Y: y, LeftClick: checkDown(1), @@ -149,22 +147,26 @@ func (e *Engine) PollEvent() *Event { func (e *Engine) Poll() (*events.State, error) { s := e.events - if e.events.EnterKey.Now { - log.Info("saw enter key here, good") - } - if e.events.KeyName.Now == "h" { - log.Info("saw letter h here, good") - } - for event := e.PollEvent(); event != nil; event = e.PollEvent() { switch event.Class { 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 @@ -196,15 +198,12 @@ func (e *Engine) Poll() (*events.State, error) { s.KeyName.Push(`\b`) } default: - log.Info("default handler, push key %s", event.KeyName) if event.State { s.KeyName.Push(event.KeyName) } else { s.KeyName.Push("") } } - - log.Info("event end, stored key=%s", s.KeyName.Now) } }