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:
parent
c5c85330de
commit
48fc40ade4
|
@ -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),
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -105,9 +105,6 @@ func (d *Doodle) Run() error {
|
|||
|
||||
start := time.Now() // Record how long this frame took.
|
||||
d.ticks++
|
||||
if d.ticks%100 == 0 {
|
||||
log.Info("...tick...%d", d.ticks)
|
||||
}
|
||||
|
||||
// Poll for events.
|
||||
ev, err := d.Engine.Poll()
|
||||
|
|
|
@ -6,14 +6,15 @@ import (
|
|||
"image"
|
||||
"math"
|
||||
"os"
|
||||
"runtime"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/pkg/balance"
|
||||
"git.kirsle.net/apps/doodle/pkg/log"
|
||||
"git.kirsle.net/apps/doodle/pkg/userdir"
|
||||
"git.kirsle.net/apps/doodle/pkg/wasm"
|
||||
"github.com/satori/go.uuid"
|
||||
"github.com/vmihailenco/msgpack"
|
||||
"golang.org/x/image/bmp"
|
||||
)
|
||||
|
||||
// Types of chunks.
|
||||
|
@ -80,7 +81,9 @@ func (c *Chunk) Texture(e render.Engine) render.Texturer {
|
|||
if c.texture == nil || c.dirty {
|
||||
// Generate the normal bitmap and one with a color mask if applicable.
|
||||
bitmap := c.toBitmap(render.Invisible)
|
||||
if runtime.GOOS != "js" { // WASM
|
||||
defer os.Remove(bitmap)
|
||||
}
|
||||
tex, err := e.NewBitmap(bitmap)
|
||||
if err != nil {
|
||||
log.Error("Texture: %s", err)
|
||||
|
@ -98,7 +101,9 @@ func (c *Chunk) TextureMasked(e render.Engine, mask render.Color) render.Texture
|
|||
if c.textureMasked == nil || c.textureMaskedColor != mask {
|
||||
// Generate the normal bitmap and one with a color mask if applicable.
|
||||
bitmap := c.toBitmap(mask)
|
||||
if runtime.GOOS != "js" { // WASM
|
||||
defer os.Remove(bitmap)
|
||||
}
|
||||
tex, err := e.NewBitmap(bitmap)
|
||||
if err != nil {
|
||||
log.Error("Texture: %s", err)
|
||||
|
@ -125,8 +130,6 @@ func (c *Chunk) toBitmap(mask render.Color) string {
|
|||
)
|
||||
}
|
||||
|
||||
log.Info("Chunk<%d>.toBitmap() called", c.Size)
|
||||
|
||||
// Get the temp bitmap image.
|
||||
bitmap := userdir.CacheFilename("chunk", filename+".bmp")
|
||||
err := c.ToBitmap(bitmap, mask)
|
||||
|
@ -184,13 +187,8 @@ func (c *Chunk) ToBitmap(filename string, mask render.Color) error {
|
|||
)
|
||||
}
|
||||
|
||||
fh, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
|
||||
return bmp.Encode(fh, img)
|
||||
// Write the image to bitmap file.
|
||||
return wasm.StoreBitmap(filename, img)
|
||||
}
|
||||
|
||||
// Set proxies to the accessor and flags the texture as dirty.
|
||||
|
|
|
@ -35,6 +35,15 @@ func (w *Canvas) loopEditable(ev *events.State) error {
|
|||
Swatch: w.Palette.ActiveSwatch,
|
||||
}
|
||||
|
||||
// If the user is holding the mouse down over one spot and not
|
||||
// moving, don't do anything. The pixel has already been set and
|
||||
// needless writes to the map cause needless cache rewrites etc.
|
||||
if lastPixel != nil {
|
||||
if pixel.X == lastPixel.X && pixel.Y == lastPixel.Y {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Append unique new pixels.
|
||||
if len(w.pixelHistory) == 0 || w.pixelHistory[len(w.pixelHistory)-1] != pixel {
|
||||
if lastPixel != nil {
|
||||
|
|
|
@ -65,7 +65,10 @@ func DoodadPath(filename string) string {
|
|||
func CacheFilename(filename ...string) string {
|
||||
paths := append([]string{CacheDirectory}, filename...)
|
||||
dir := paths[:len(paths)-1]
|
||||
|
||||
if runtime.GOOS != "js" {
|
||||
configdir.MakePath(filepath.Join(dir...))
|
||||
}
|
||||
return filepath.Join(paths[0], filepath.Join(paths[1:]...))
|
||||
}
|
||||
|
||||
|
|
20
pkg/wasm/bitmap.go
Normal file
20
pkg/wasm/bitmap.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// +build !js
|
||||
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"image"
|
||||
"os"
|
||||
|
||||
"golang.org/x/image/bmp"
|
||||
)
|
||||
|
||||
// StoreBitmap stores a bitmap image to disk.
|
||||
func StoreBitmap(filename string, img image.Image) error {
|
||||
fh, err := os.Create(filename)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fh.Close()
|
||||
return bmp.Encode(fh, img)
|
||||
}
|
25
pkg/wasm/bitmap_js.go
Normal file
25
pkg/wasm/bitmap_js.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
// +build js,wasm
|
||||
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"image"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
// StoreBitmap stores a bitmap image to sessionStorage as a data URL for PNG
|
||||
// base64 encoded image.
|
||||
func StoreBitmap(filename string, img image.Image) error {
|
||||
var fh = bytes.NewBuffer([]byte{})
|
||||
|
||||
if err := png.Encode(fh, img); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var dataURI = "data:image/png;base64," + base64.StdEncoding.EncodeToString(fh.Bytes())
|
||||
|
||||
SetSession(filename, dataURI)
|
||||
return nil
|
||||
}
|
14
pkg/wasm/localstorage.go
Normal file
14
pkg/wasm/localstorage.go
Normal file
|
@ -0,0 +1,14 @@
|
|||
// +build !js
|
||||
|
||||
package wasm
|
||||
|
||||
// SetSession sets a binary value on sessionStorage.
|
||||
// This is a no-op when not in wasm.
|
||||
func SetSession(key string, value string) {
|
||||
}
|
||||
|
||||
// GetSession retrieves a binary value from sessionStorage.
|
||||
// This is a no-op when not in wasm.
|
||||
func GetSession(key string) (string, bool) {
|
||||
return "", false
|
||||
}
|
20
pkg/wasm/localstorage_js.go
Normal file
20
pkg/wasm/localstorage_js.go
Normal file
|
@ -0,0 +1,20 @@
|
|||
// +build js,wasm
|
||||
|
||||
package wasm
|
||||
|
||||
import (
|
||||
"syscall/js"
|
||||
)
|
||||
|
||||
// SetSession sets a text value on sessionStorage.
|
||||
func SetSession(key string, value string) {
|
||||
// b64 := base64.StdEncoding.EncodeToString(value)
|
||||
js.Global().Get("sessionStorage").Call("setItem", key, value)
|
||||
}
|
||||
|
||||
// GetSession retrieves a text value from sessionStorage.
|
||||
func GetSession(key string) (string, bool) {
|
||||
var value js.Value
|
||||
value = js.Global().Get("sessionStorage").Call("getItem", key)
|
||||
return value.String(), value.Type() == js.TypeString
|
||||
}
|
|
@ -5,6 +5,8 @@ package main
|
|||
import (
|
||||
"fmt"
|
||||
|
||||
"syscall/js"
|
||||
|
||||
"git.kirsle.net/apps/doodle/lib/render"
|
||||
"git.kirsle.net/apps/doodle/lib/render/canvas"
|
||||
doodle "git.kirsle.net/apps/doodle/pkg"
|
||||
|
@ -19,6 +21,7 @@ func main() {
|
|||
|
||||
// Enable workarounds.
|
||||
balance.DisableChunkTextureCache = true
|
||||
js.Global().Get("sessionStorage").Call("clear")
|
||||
|
||||
// HTML5 Canvas engine.
|
||||
engine, _ := canvas.New("canvas")
|
||||
|
@ -35,7 +38,7 @@ func main() {
|
|||
game.SetWindowSize(w, h)
|
||||
|
||||
// game.Goto(&doodle.GUITestScene{})
|
||||
// game.Goto(&doodle.EditorScene{})
|
||||
game.Goto(&doodle.EditorScene{})
|
||||
|
||||
game.Run()
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user