Help us shape the future of Go! Take the 2026 Go Developer Survey

webcanvas

package module
v0.7.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 30, 2026 License: BSD-3-Clause Imports: 4 Imported by: 2

README

webcanvas

CI Go Reference Go Report Card

Runs a go-widgets scene in a browser tab, on a plain <canvas>: no compositor, no SharedArrayBuffer, no cross-origin isolation, nothing on the far end. It blits an RGBA framebuffer into the canvas and routes DOM pointer and keyboard events back into the scene.

It carries no widget logic of its own. An application implements a small interface and hands it to Run:

//go:build js && wasm

package main

import "github.com/go-widgets/webcanvas"

func main() { webcanvas.Run("screen", myScene()) }

App is Size, Draw(buf []byte), and one method per kind of event, each reporting whether the scene changed so a repaint can be skipped when nothing did. A scene may also implement Ticker, Animator, Resizer or Scroller to be told about time, animation, a resized canvas or the wheel, and ModifierAware to be told the state of Ctrl, Shift, Alt and Meta before every pointer, keyboard and wheel event -- without it Ctrl+A, Cmd+A and Alt+A all reach KeyDown as "a", and a Shift-click is a click.

HiDPI. A Resizer scene that also implements PixelRatioAware renders at the display's device pixels: the canvas backing store becomes its CSS box × window.devicePixelRatio, the scene is told the ratio (a go-widgets scene forwards it to toolkit.SetMetricScale) and gets device pixels in Resize, and both follow browser zoom and moves between displays (a matchMedia resolution query, as MDN's Window.devicePixelRatio shows). Without it a Retina display upscales a 1× bitmap: blurred, stair-stepped text. The page gives the canvas its CSS size (width: 100vw; height: 100vh).

A scene whose content changes while nobody touches it -- a fetch that completes, a message that arrives -- implements RepaintWith(request func()) (RepaintAware). request asks for one frame, from any goroutine, coalesced into the next animation frame. Without it such a scene shows its first frame until the next click. browsertest/ checks this in headless Chrome, and fails when the hook is removed.

Key positions and releases. A scene that implements KeyCode(code string) (KeyCodeAware) is told each keyboard event's physical key, the DOM's KeyboardEvent.code ("KeyA", "ArrowUp", "ShiftLeft"), just before the event, the way ModifierAware is told the modifiers. The key ("a", "A", "é") is what was typed. Its code is where the key is, "not affected by the current keyboard layout or modifier state" (W3C UI Events), which is what a game, or a guest that expects key positions, needs. A code longer than 32 bytes is passed as "". A scene that implements KeyUp(key string) bool (KeyUpper) also gets key releases, with the same key values and the code and modifiers first. Run installs the keyup listener only for such a scene. Scenes that implement neither see exactly the events they did before. browsertest/ types Shift+A and checks char A KeyA shift=true and up A KeyA shift=true.

Images the browser already holds. A scene can show frames a GPU rendered into an ImageBitmap, a video, or another canvas without copying their pixels through Draw's buffer: it implements Layers() []Layer (Layered). Each layer is an opaque Image (NewImage(jsValue)), a rectangle in canvas pixels (device pixels for a PixelRatioAware scene) and an optional alpha. After every Draw, Run calls drawImage for each layer, in order, scaled into its rectangle with high-quality smoothing.

Bounds and safety:

  • At most 64 layers are drawn per frame.
  • A layer with no image, an empty rectangle, a rectangle entirely off the canvas, or an alpha that is not in [0, 1] is skipped.
  • Each layer is drawn under its own recover net, so a closed ImageBitmap, which makes drawImage throw, costs only that layer.
  • The images remain the scene's: Run never closes or keeps them.

browsertest/ draws a closed bitmap, then a 2×2 green bitmap scaled into a 4×4 corner, and reads the pixels back.

Where this came from

It was internal/webcanvas inside go-widgets/gallery, where nothing outside that one module could import it. It is the same code, in a place a second application can reach — which is the point: the DOM plumbing exists once.

Testing

go test -covermode=set ./...

CI gates on exact 100% statement coverage, go vet, a cross-compile across the fleet's targets, and a js/wasm build.

What that coverage does and does not say. The interface, the scene contract and the event dispatch are covered. run_js.go — the DOM loop itself — is behind a js && wasm build tag and so is not part of the measured build at all: it compiles for the browser and is exercised by the applications that use it, not by a test here. A gate that reports 100% while a file is invisible to it is worth saying out loud.

License

BSD-3-Clause — see LICENSE. Copyright the go-widgets/webcanvas authors.

Documentation

Index

Constants

View Source
const MaxLayers = 64

MaxLayers bounds the layers drawn per frame.

Variables

View Source
var PanicReporter = func(r any, stack []byte) {
	fmt.Fprintf(os.Stderr, "webcanvas: recovered panic in a handler: %v\n%s\n", r, stack)
}

PanicReporter logs a handler panic the [guard] net recovered. The wasm host swaps in a console.error reporter carrying the JS stack; the default writes to standard error so a native run — and the recover test — still surfaces it. It is a package var, not a const, precisely so run_js.go can replace it.

Functions

This section is empty.

Types

type Animator

type Animator interface {
	// AnimationStep advances the scene's animation by dt seconds of real elapsed
	// time and reports whether the scene now needs a repaint.
	AnimationStep(dt float64) (repaint bool)
}

Animator is an optional companion to App: a scene with time-varying content driven by a REAL wall clock (procedurally animated icons, say) implements it, and [Run] installs a requestAnimationFrame loop that hands it the elapsed dt between frames — in seconds — through AnimationStep. Unlike Ticker (a fixed cadence that always repaints), an Animator advances by the true frame delta and reports whether the frame changed anything, so Run repaints only when a pixel actually moved. A scene that implements neither installs no clock and repaints on input alone. The phase-advance logic lives in the scene (natively testable); only the rAF wiring is browser-side.

type App

type App interface {
	// Size returns the fixed pixel dimensions of the scene's surface. The host
	// sizes the canvas and allocates the framebuffer from it; it is read once,
	// at startup, so it must not change over the App's life.
	Size() (w, h int)

	// Draw paints the whole scene into buf, a width*height*4 RGBA byte slice
	// laid out exactly like an image.RGBA's Pix (row-major, 4 bytes/pixel).
	Draw(buf []byte)

	// Click delivers a primary (left) button press at (x, y). It begins a
	// gesture — a selection, a drag, a placement — that later Move/Release
	// calls advance and commit.
	Click(x, y int) bool

	// Move delivers a pointer move at (x, y). While a Click gesture is in
	// flight it is a drag tick; otherwise it is a hover.
	Move(x, y int) bool

	// Release delivers the primary button release at (x, y), committing any
	// in-flight gesture Click began.
	Release(x, y int) bool

	// Context delivers a secondary (right) button press at (x, y), typically
	// opening a context menu. The host suppresses the browser's own menu.
	Context(x, y int) bool

	// Char delivers a single printable character typed with no Ctrl/Meta/Alt
	// modifier — text input for a focused field.
	Char(s string) bool

	// KeyDown delivers a named key (Enter, Backspace, Delete, Arrow*, …) or a
	// modified key press, routed to the focused widget.
	KeyDown(s string) bool
}

App is a self-contained canvas scene. A host (the wasm [Run] loop, or a native test) owns the pixel buffer and the event source; the App owns the widgets. Every coordinate is in canvas-local pixels (top-left origin), the same space [Run] derives from the pointer event's position within the canvas' bounding rectangle.

Each event method reports whether the scene changed and therefore needs a repaint, so the host can skip a redraw when nothing moved. Draw is expected to fully paint the buffer (it is never given a dirty region).

type Image added in v0.6.0

type Image struct {
	// contains filtered or unexported fields
}

Image is a browser image the host can draw over the scene: an ImageBitmap, a <canvas>, an OffscreenCanvas, an <img> -- anything CanvasRenderingContext2D drawImage accepts. The scene holds it as an opaque handle: a syscall/js Value in the browser; in a native build or test any value, which the host never draws (there is no canvas to draw it on).

func NewImage added in v0.6.0

func NewImage(handle any) Image

NewImage wraps handle (a js.Value in the browser) as an Image.

func (Image) Handle added in v0.6.0

func (i Image) Handle() any

Handle is the value NewImage was given.

func (Image) IsZero added in v0.6.0

func (i Image) IsZero() bool

IsZero reports whether i holds nothing.

type KeyCodeAware added in v0.7.0

type KeyCodeAware interface {
	KeyCode(code string)
}

KeyCodeAware is an optional companion to App: a scene that implements it is told the physical key of every keyboard event -- the DOM's KeyboardEvent.code ("KeyA", "ArrowUp", "ShiftLeft") -- immediately before the event is delivered (Char, KeyDown, or KeyUp for a KeyUpper). The event's key ("a", "A", "é") is what was typed; its code is where on the keyboard it was, "not affected by the current keyboard layout or modifier state" (W3C UI Events, KeyboardEvent.code) -- what a game or a remote guest that expects key positions needs. A scene that does not implement it sees exactly the events it always did.

type KeyUpper added in v0.7.0

type KeyUpper interface {
	KeyUp(key string) bool
}

KeyUpper is an optional companion to App: a scene that implements it is told when a key is released (the DOM's keyup, which a user agent "MUST dispatch ... when a key is released"), with the same key value KeyDown and Char get. [Run] installs the keyup listener only for such a scene. KeyUp reports whether the scene changed.

type Layer added in v0.6.0

type Layer struct {
	Image      Image
	X, Y, W, H int
	// Alpha is the opacity in (0, 1]; 0 (the zero value) means opaque.
	Alpha float64
}

Layer is one image drawn over the scene after App.Draw, scaled into its rectangle. Coordinates are canvas pixels, the same space Draw paints: device pixels for a PixelRatioAware scene, so an image lands sharp at any ratio.

type Layered added in v0.6.0

type Layered interface {
	Layers() []Layer
}

Layered is an optional companion to App: a scene that shows images the browser already holds -- frames a GPU rendered into an ImageBitmap, a video -- implements it, rather than copying their pixels through Draw's buffer. After every Draw, [Run] calls Layers and draws each one, in order, with drawImage scaled into its rectangle (smoothing on, quality high). Each layer is drawn under its own recover net, so one unusable image (a closed ImageBitmap throws InvalidStateError) costs that layer only.

The images are the scene's: Run never closes or keeps them past the call. A scene that omits the interface draws exactly as before.

type ModifierAware added in v0.3.0

type ModifierAware interface {
	Modifiers(m Modifiers)
}

ModifierAware is an optional companion to App: a scene that implements it is told the modifier state immediately before every pointer, keyboard and wheel event is delivered. Without it a modified key reaches KeyDown as its bare name -- "a" for Ctrl+A, Cmd+A and Alt+A alike -- and a Shift-click is a click: a scene could not tell copy from select-all, nor extend a selection. A scene that does not implement it sees exactly the events it always did.

type Modifiers added in v0.3.0

type Modifiers struct {
	Ctrl, Shift, Alt, Meta bool
}

Modifiers is the state of the modifier keys when an event happened, as the DOM event reports it (ctrlKey, shiftKey, altKey, metaKey).

type PixelRatioAware added in v0.5.0

type PixelRatioAware interface {
	SetPixelRatio(ratio float64)
}

PixelRatioAware is an optional companion to Resizer: a scene that can render at the display's device pixels implements it. [Run] then sizes the canvas backing store to the canvas' CSS box × window.devicePixelRatio, tells the scene the ratio before each Resize (which is handed device pixels), and re-does both when the ratio changes (browser zoom, a window moved to a display of another density). On a ratio-2 display the scene draws twice the pixels in each direction instead of a 1× bitmap the browser upscales into blocky, blurred text. A go-widgets scene typically forwards the ratio to toolkit.SetMetricScale, which scales every metric and re-renders the OpenType face.

The page must give the canvas its CSS size (e.g. width: 100vw; height: 100vh): the backing store no longer equals it. A scene that omits the interface keeps rendering at CSS pixels, unchanged.

type RepaintAware added in v0.4.0

type RepaintAware interface {
	RepaintWith(request func())
}

RepaintAware is an optional companion to App: a scene whose content can change while nobody touches it -- a fetch that completes, a message that arrives -- and that therefore needs a frame no event will cause.

Before the first frame, [Run] hands it request. Calling request asks for one frame at the next animation frame; it is safe from any goroutine, returns at once, and coalesces: any number of calls before that frame is drawn give one frame. The scene does its pending work at the start of App.Draw -- for a go-widgets tree, draining an mvvm.Queue -- so the frame shows it.

type Resizer

type Resizer interface {
	// Resize relays out the scene to fit w×h device pixels and returns the pixel
	// size (rw, rh) it will render at — the size the host sizes the canvas and
	// framebuffer to.
	Resize(w, h int) (rw, rh int)
}

Resizer is an optional companion to App: a scene that can adapt its layout to a NEW surface size implements it, and [Run] installs a window "resize" listener (and fits the canvas to the viewport once at startup) that re-sizes the canvas and framebuffer, calls Resize, and repaints. A scene that omits it keeps the fixed App.Size forever — the pre-resize behaviour every existing demo relies on — so Run never installs the listener and the surface never changes.

Resize is handed the target pixel size (the canvas' laid-out client box) and returns the size it will actually render at: a scene may clamp to a sane minimum, and the host allocates the framebuffer from the RETURNED size, so the scene and the buffer can never disagree. The relayout logic lives in the scene (natively testable); only the DOM resize wiring is browser-side.

type Scroller

type Scroller interface {
	// Scroll delivers a wheel / trackpad scroll of dy vertical and dx horizontal
	// ROWS at canvas-local (x, y), and reports whether the scene needs a repaint.
	Scroll(x, y, dx, dy int) (repaint bool)
}

Scroller is an optional companion to App: a scene with a scrollable region (a docked list, an icon palette, an overflowing panel) implements it, and [Run] installs a "wheel" listener that translates the browser's WheelEvent into toolkit scroll ROWS and routes them — with the canvas-local pointer position, so the scene can hit-test which region the wheel is over — through Scroll. A scene that omits it installs no wheel listener, so the page keeps its default wheel behaviour and every existing demo (the widget gallery) is unchanged.

dx / dy are the horizontal / vertical scroll amounts in toolkit ROWS (already normalised from the event's deltaMode by [scrollRows]): positive dy scrolls down / forward, positive dx scrolls right. A scene typically forwards them to the widget under (x, y) as a [toolkit.Event] of kind EventScroll (Delta = dy, DeltaX = dx), which scrollable widgets clamp at both ends. Scroll reports whether the scene changed and therefore needs a repaint.

type Ticker

type Ticker interface {
	// Tick advances one animation frame. Run repaints after every Tick.
	Tick()
}

Ticker is an optional companion to App: a scene that needs a steady animation clock (a toast countdown, a blinking caret) implements it, and [Run] installs a 60-Hz timer that calls Tick and repaints. A scene with no time-varying state omits it, and Run installs no timer, so it never repaints except in response to input.

Directories

Path Synopsis
browsertest module

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL