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.
This commit is contained in:
Noah 2019-06-26 22:44:08 -07:00
parent 03b4441eaa
commit 27b908e40a
3 changed files with 76 additions and 22 deletions

View File

@ -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),

View File

@ -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.

View File

@ -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)
}
}