WakaCanvas

WakaCanvas is a canvas drawing plugin for wakaPAC. It provides a high-level drawing API modelled after Win32 GDI — selectXYZ() functions write state through to the device context immediately, and draw functions operate on geometry only. There is no implicit save()/restore() wrapping. All drawing functions work transparently against both live canvas contexts and wakaPAC MetaFile targets.

explanation

Getting Started

Include the script after wakaPAC and register the plugin:

<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/wakapac.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/quellabs/wakapac@main/plugins/wakacanvas.min.js"></script>

<script>
    wakaPAC.use(WakaCanvas); // must be called before any wakaPAC() calls
</script>

Once registered, WakaCanvas is available globally.

Paint Cycle

A typical paint cycle selects state into the DC, then calls draw functions. The DC starts from a known default state each frame — beginPaint() applies Win32 DC defaults before any drawing occurs:

wakaPAC('myCanvas', {
    msgProc(event) {
        switch (event.message) {
            case wakaPAC.MSG_PAINT: {
                const dc = wakaPAC.getDC(this.pacId);
                WakaCanvas.beginPaint(dc);

                WakaCanvas.selectBrush(dc, '#3a86ff');
                WakaCanvas.selectPen(dc, '#1a56cf', 2);
                WakaCanvas.drawRect(dc, 10, 10, 200, 50);
                WakaCanvas.addHitRect(dc, 10, 10, 200, 50, 'button');

                WakaCanvas.selectFont(dc, 'heading', 16);
                WakaCanvas.selectBrush(dc, '#fff');
                WakaCanvas.selectPen(dc, null);
                WakaCanvas.drawText(dc, 'Click me', 110, 35);

                WakaCanvas.endPaint(dc);
                wakaPAC.releaseDC(dc);
                break;
            }

            case wakaPAC.MSG_LCLICK: {
                const pos = wakaPAC.MAKEPOINTS(event.lParam);
                const dc  = wakaPAC.getDC(this.pacId);
                const hit = WakaCanvas.hitTest(dc, pos.x, pos.y);
                wakaPAC.releaseDC(dc);

                if (hit === 'button') {
                    // handle click
                }

                break;
            }
        }
    }
});

Usage

DC Defaults

beginPaint() resets the DC to a known state before each frame. The defaults mirror Win32 GetDC():

PropertyDefault
BrushNULL_BRUSH — no fill
PenBLACK_PEN — 1px solid black
Font12px sans-serif
Alpha1
Shadownone
Line dashsolid
Text align'left'
Text baseline'middle'

State set by one select* call persists until the next call for the same property. There is no scoping — every select writes through to the DC immediately and stays until changed.

Brush Types

selectBrush() accepts a CSS color string, a gradient descriptor, a pattern descriptor, or null for NULL_BRUSH. NULL_BRUSH suppresses fill operations — shapes are stroked only.

// Solid color
WakaCanvas.selectBrush(dc, '#3a86ff');

// Linear gradient
const grad = WakaCanvas.linearGradient(0, 0, 0, 100, '#000')
    .addColorStop(0, '#3a86ff')
    .addColorStop(1, '#1a56cf');
WakaCanvas.selectBrush(dc, grad);

// Radial gradient
const radial = WakaCanvas.radialGradient(50, 50, 0, 50, 50, 50, '#000')
    .addColorStop(0, '#fff')
    .addColorStop(1, '#3a86ff');
WakaCanvas.selectBrush(dc, radial);

// Pattern
const pattern = WakaCanvas.patternBrush(imageElement, 'repeat', '#888');
WakaCanvas.selectBrush(dc, pattern);

// NULL_BRUSH — no fill
WakaCanvas.selectBrush(dc, null);

Gradient and pattern descriptors are created once and passed to selectBrush(). The gradient or pattern object is resolved against the DC at selection time. When a MetaFile is the target, gradients and patterns fall back to the solid color passed as the last argument to linearGradient(), radialGradient(), or patternBrush().

Hit Testing

Hit areas are registered during the paint cycle alongside drawing calls and are cleared automatically by beginPaint() at the start of each frame. They are fully decoupled from drawing — registering a hit area produces no visual output. Any value can be stored as hit data.

// Registration — call during MSG_PAINT alongside draw calls
WakaCanvas.addHitRect(dc, x, y, w, h, data);
WakaCanvas.addHitCircle(dc, cx, cy, r, data);
WakaCanvas.addHitPolygon(dc, points, data);     // points: [{x, y}, ...]

// Query — call from a pointer event handler
const hit = WakaCanvas.hitTest(dc, x, y);       // → data | null

When multiple hit areas overlap, hitTest() returns the last registered area that contains the point — equivalent to painter's algorithm z-order.

Font Loading

await WakaCanvas.loadFont('heading', '/fonts/Inter-Bold.woff2', { weight: 700 });
await WakaCanvas.loadFont('body', 'https://fonts.googleapis.com/css2?family=Inter:wght@400', { weight: 400 });

When a font finishes loading, MSG_FONT_LOADED is broadcast to all components. If loading fails, MSG_FONT_FAILED is broadcast instead. See Messages for details.

If selectFont() is called with an alias that is registered but not yet loaded, the DC falls back to sans-serif for that paint cycle. Text is never suppressed — it always renders, using the fallback until the font arrives and the component repaints.

MetaFile Compilation

All select* and draw* calls work transparently against a wakaPAC MetaFile target. DC defaults are recorded as operations so the display list is self-contained and playable without prior DC setup.

const mf = new wakaPAC.MetaFile();
WakaCanvas.beginPaint(mf);
WakaCanvas.selectBrush(mf, '#3a86ff');
WakaCanvas.selectPen(mf, '#1a56cf', 2);
WakaCanvas.drawRect(mf, 10, 10, 200, 50);
WakaCanvas.endPaint(mf);

const dl = mf.build();
wakaPAC.playMetaFile(ctx, dl);

Hit testing is not available for MetaFile targets. addHitRect(), addHitCircle(), and addHitPolygon() are no-ops when called with a MetaFile DC. measureText() returns null for MetaFile targets.

API

WakaCanvas.loadFont(name, url, descriptors?)

Loads and registers a font under a named alias. Accepts local file URLs and Google Fonts stylesheet URLs. Broadcasts MSG_FONT_LOADED on success, MSG_FONT_FAILED on failure.

ParameterTypeDescription
namestringAlias used in selectFont()
urlstringLocal font file URL or Google Fonts stylesheet URL
descriptors.weightstring | numberFont weight. Default: 'normal'
descriptors.stylestringFont style. Default: 'normal'
descriptors.displaystringFont display strategy. Default: 'swap'
descriptors.familystringOverride the CSS family name derived from the filename (local fonts only)
Returns Promise<boolean>true on success, false on failure

WakaCanvas.isFontReady(name)

Returns true if the named font alias has finished loading.

ParameterTypeDescription
namestringRegistered alias
Returns boolean

WakaCanvas.fontString(name, size, overrides?)

Returns the resolved CSS font shorthand string for a registered alias. Useful for measuring text outside a paint cycle.

ParameterTypeDescription
namestringRegistered alias
sizenumberFont size in pixels
overrides.weightstring | numberOverride the registered weight
overrides.stylestringOverride the registered style
Returns stringe.g. '700 16px Inter'

WakaCanvas.beginPaint(dc)

Begins a paint cycle. Applies DC defaults and clears the hit registry for this frame. Must be called before any select* or draw* calls.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
Returns void

WakaCanvas.selectFont(dc, name, size, opts?)

Selects a font into the DC. Writes through immediately. If the alias is registered but not yet loaded, falls back to sans-serif for this paint cycle.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
namestringRegistered alias or raw CSS family name
sizenumberFont size in pixels
opts.weightstring | numberOverride the registered weight
opts.stylestringOverride the registered style
Returns void

WakaCanvas.selectBrush(dc, brush)

Selects a brush into the DC. Controls fill color for shapes and text. Pass null for NULL_BRUSH — fill operations are suppressed.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
brushstring | LinearGradientBrush | RadialGradientBrush | PatternBrush | nullBrush descriptor or null for no fill
Returns void

WakaCanvas.selectPen(dc, color, width?, opts?)

Selects a pen into the DC. Controls stroke color and width. Pass null for NULL_PEN — stroke operations are suppressed. When a pen is selected, drawText() also strokes the text outline using the pen's color and lineWidth.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
colorstring | nullCSS color string, or null for no stroke
widthnumberStroke width in pixels. Default: 1
opts.lineCapstring'butt' | 'round' | 'square'. Default: 'butt'
opts.lineJoinstring'miter' | 'round' | 'bevel'. Default: 'miter'
Returns void

WakaCanvas.selectAlpha(dc, alpha)

Sets global alpha on the DC. Applies to all subsequent drawing operations.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
alphanumberOpacity from 0 (transparent) to 1 (opaque)
Returns void

WakaCanvas.selectShadow(dc, color, blur?, offsetX?, offsetY?)

Sets a drop shadow on the DC. Pass null as color to clear the shadow.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
colorstring | nullCSS color string, or null to clear
blurnumberBlur radius in pixels. Default: 0
offsetXnumberHorizontal shadow offset. Default: 0
offsetYnumberVertical shadow offset. Default: 0
Returns void

WakaCanvas.selectLineDash(dc, pattern)

Sets the line dash pattern. Pass null or an empty array for a solid line.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
patternnumber[] | nullDash/gap lengths in pixels, e.g. [4, 4]. null or [] for solid.
Returns void

WakaCanvas.selectTextAlign(dc, align)

Sets the horizontal text alignment anchor for drawText().

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
alignstring'left' | 'center' | 'right' | 'start' | 'end'
Returns void

WakaCanvas.selectTextBaseline(dc, baseline)

Sets the vertical text alignment anchor for drawText().

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
baselinestring'top' | 'middle' | 'bottom' | 'alphabetic' | 'hanging' | 'ideographic'
Returns void

WakaCanvas.selectTransform(dc, t)

Sets an absolute transform matrix on the DC. Replaces the current transform entirely — not cumulative. Use resetTransform() to return to the identity matrix.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
t.anumberHorizontal scaling. Default: 1
t.bnumberHorizontal skewing. Default: 0
t.cnumberVertical skewing. Default: 0
t.dnumberVertical scaling. Default: 1
t.enumberHorizontal translation. Default: 0
t.fnumberVertical translation. Default: 0
Returns void

WakaCanvas.drawRect(dc, x, y, w, h)

Draws a rectangle. Fills if a brush is selected, strokes if a pen is selected.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
x, ynumberTop-left corner
w, hnumberWidth and height
Returns void

WakaCanvas.drawRoundRect(dc, x, y, w, h, radius)

Draws a rectangle with rounded corners.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
x, ynumberTop-left corner
w, hnumberWidth and height
radiusnumber | number[]Corner radius in pixels, or [tl, tr, br, bl] for individual corners
Returns void

WakaCanvas.drawCircle(dc, cx, cy, r)

Draws a circle.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
cx, cynumberCentre point
rnumberRadius in pixels
Returns void

WakaCanvas.drawEllipse(dc, cx, cy, rx, ry, rotation?)

Draws an ellipse.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
cx, cynumberCentre point
rx, rynumberHorizontal and vertical radii
rotationnumberRotation in radians. Default: 0
Returns void

WakaCanvas.drawLine(dc, x1, y1, x2, y2)

Draws a straight line. No-op if NULL_PEN is selected.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
x1, y1numberStart point
x2, y2numberEnd point
Returns void

WakaCanvas.drawPolyline(dc, points)

Draws an open polyline through a series of points. No-op if NULL_PEN is selected or fewer than 2 points are provided.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
points{x: number, y: number}[]Array of points
Returns void

WakaCanvas.drawPolygon(dc, points)

Draws a closed polygon. No-op if fewer than 3 points are provided.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
points{x: number, y: number}[]Array of points
Returns void

WakaCanvas.drawArc(dc, cx, cy, r, startAngle, endAngle, ccw?)

Draws an arc. No-op if NULL_PEN is selected. Angles are in radians, where 0 is the 3 o'clock position.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
cx, cynumberCentre point
rnumberRadius in pixels
startAnglenumberStart angle in radians
endAnglenumberEnd angle in radians
ccwbooleanDraw counter-clockwise. Default: false
Returns void

WakaCanvas.drawText(dc, text, x, y, maxWidth?)

Draws text using the selected font and brush. The brush is the fill color — equivalent to Win32 SetTextColor(). No-op if NULL_BRUSH is selected. If a pen is selected, the text outline is also stroked using the pen's color and lineWidth. Select NULL_PEN to suppress the outline.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
textstringText to draw
x, ynumberPosition (affected by selectTextAlign and selectTextBaseline)
maxWidthnumberMaximum render width in pixels — browser scales text down to fit
Returns void

WakaCanvas.drawImage(dc, bitmap, dx, dy, dw?, dh?)

Draws a bitmap loaded via wakaPAC.loadBitmap(). Respects the selected alpha. If dw and dh are omitted, the bitmap is drawn at its natural size.

ParameterTypeDescription
dcCanvasRenderingContext2D | MetaFileTarget DC
bitmapCanvasRenderingContext2DBitmap handle from wakaPAC.loadBitmap()
dx, dynumberDestination position
dw, dhnumberDestination size. Defaults to the bitmap's natural dimensions.
Returns void

WakaCanvas.measureText(dc, text)

Measures a text string using the DC's current font. Returns null for MetaFile targets.

ParameterTypeDescription
dcCanvasRenderingContext2DTarget DC (MetaFile not supported)
textstringText to measure
Returns { width, ascent, descent, height } | null

WakaCanvas.addHitRect(dc, x, y, w, h, data)

WakaCanvas.addHitCircle(dc, cx, cy, r, data)

WakaCanvas.addHitPolygon(dc, points, data)

Register a hit area for the current frame. No-op for MetaFile targets. All three functions accept any value as data — it is returned verbatim by hitTest() on a match.

WakaCanvas.hitTest(dc, x, y)

Tests a point against all registered hit areas for this DC. Returns the data payload of the last registered matching area, or null if no hit. No-op for MetaFile targets.

ParameterTypeDescription
dcCanvasRenderingContext2DTarget DC
x, ynumberPoint to test, in canvas coordinates
Returns * | nullHit data or null

Messages

WakaCanvas broadcasts two message types relating to font loading. All components receive them regardless of whether they use fonts.

Font Loaded (MSG_FONT_LOADED)

Broadcast to all components when a font finishes loading. Handle it to repaint any component that uses the font so text renders with the correct typeface instead of the fallback.

case WakaCanvas.MSG_FONT_LOADED: {
    wakaPAC.invalidateRect(this.pacId, null);
    break;
}

Message Parameters

Parameter Type Description
wParam string The alias name passed to loadFont(), e.g. 'heading'.
lParam object Always { alias, family } — the registered alias and resolved CSS family name.

Font Failed (MSG_FONT_FAILED)

Broadcast to all components when a font fails to load. The alias is not registered — subsequent selectFont() calls using it will fall back to sans-serif.

case WakaCanvas.MSG_FONT_FAILED: {
    // wParam: alias name — lParam: { alias, url }
    console.warn('Font failed to load:', event.wParam);
    break;
}

Message Parameters

Parameter Type Description
wParam string The alias name passed to loadFont().
lParam object Always { alias, url } — the registered alias and the URL that failed to load.

Notes

WakaCanvas has no implicit state scoping. There is no save()/restore() — each select* call writes through to the DC and remains active until changed. beginPaint() is the only mechanism that resets state to a known baseline. Always call it at the start of a paint cycle.

Hit areas are bound to a DC, not a frame. They are cleared by beginPaint() at the start of each paint cycle. Register hit areas alongside the draw calls they correspond to so position and hit area stay in sync.

Gradient and pattern brushes carry a fallback solid color for MetaFile rendering. When targeting a MetaFile, pass a representative solid color as the last argument to linearGradient(), radialGradient(), or patternBrush().

measureText() returns null for MetaFile targets — there is no rendering context to measure against. Perform text measurement against a live DC before compiling to a MetaFile if layout calculations are needed.

Best Practices

  • Register before creating components — call wakaPAC.use(WakaCanvas) before any wakaPAC() calls.
  • Load fonts early when possible — fonts loaded before first paint avoid a fallback-then-repaint cycle. Fonts loaded later are fine too — handle MSG_FONT_LOADED to repaint any component that uses the font.
  • Always call beginPaint() and endPaint()beginPaint() clears the hit registry and resets state. Skipping it leaves stale hit areas from the previous frame.
  • Set full state before each draw call — because state persists across draw calls, a missing selectBrush() or selectPen() will silently inherit the previous value. Establish the full required state before each shape.
  • Use NULL_PEN to suppress text outlinesdrawText() strokes the text outline if a pen is selected. Call selectPen(dc, null) before drawing text unless an outline is intentional.