diff --git a/lib/render/canvas/draw.go b/lib/render/canvas/draw.go index fd8d8d2..1f7b8be 100644 --- a/lib/render/canvas/draw.go +++ b/lib/render/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/lib/render/canvas/engine.go b/lib/render/canvas/engine.go index 09b7f83..5368083 100644 --- a/lib/render/canvas/engine.go +++ b/lib/render/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/lib/render/canvas/events.go b/lib/render/canvas/events.go index fc8d5c5..f21dade 100644 --- a/lib/render/canvas/events.go +++ b/lib/render/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) } } diff --git a/pkg/doodle.go b/pkg/doodle.go index 707a308..b7aaf31 100644 --- a/pkg/doodle.go +++ b/pkg/doodle.go @@ -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() diff --git a/pkg/level/chunk.go b/pkg/level/chunk.go index 41f2a71..434c1fe 100644 --- a/pkg/level/chunk.go +++ b/pkg/level/chunk.go @@ -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) - defer os.Remove(bitmap) + 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) - defer os.Remove(bitmap) + 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. diff --git a/pkg/uix/canvas_editable.go b/pkg/uix/canvas_editable.go index e0cee35..6d3de9c 100644 --- a/pkg/uix/canvas_editable.go +++ b/pkg/uix/canvas_editable.go @@ -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 { diff --git a/pkg/userdir/userdir.go b/pkg/userdir/userdir.go index 12ad41e..6df1518 100644 --- a/pkg/userdir/userdir.go +++ b/pkg/userdir/userdir.go @@ -65,7 +65,10 @@ func DoodadPath(filename string) string { func CacheFilename(filename ...string) string { paths := append([]string{CacheDirectory}, filename...) dir := paths[:len(paths)-1] - configdir.MakePath(filepath.Join(dir...)) + + if runtime.GOOS != "js" { + configdir.MakePath(filepath.Join(dir...)) + } return filepath.Join(paths[0], filepath.Join(paths[1:]...)) } diff --git a/pkg/wasm/bitmap.go b/pkg/wasm/bitmap.go new file mode 100644 index 0000000..addd855 --- /dev/null +++ b/pkg/wasm/bitmap.go @@ -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) +} diff --git a/pkg/wasm/bitmap_js.go b/pkg/wasm/bitmap_js.go new file mode 100644 index 0000000..ddd2144 --- /dev/null +++ b/pkg/wasm/bitmap_js.go @@ -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 +} diff --git a/pkg/wasm/localstorage.go b/pkg/wasm/localstorage.go new file mode 100644 index 0000000..584a7d1 --- /dev/null +++ b/pkg/wasm/localstorage.go @@ -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 +} diff --git a/pkg/wasm/localstorage_js.go b/pkg/wasm/localstorage_js.go new file mode 100644 index 0000000..fbb70d4 --- /dev/null +++ b/pkg/wasm/localstorage_js.go @@ -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 +} diff --git a/wasm/main_wasm.go b/wasm/main_wasm.go index c7bcb1e..0ce6181 100644 --- a/wasm/main_wasm.go +++ b/wasm/main_wasm.go @@ -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() }