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.
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():
| Property | Default |
|---|---|
| Brush | NULL_BRUSH — no fill |
| Pen | BLACK_PEN — 1px solid black |
| Font | 12px sans-serif |
| Alpha | 1 |
| Shadow | none |
| Line dash | solid |
| 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.
| Parameter | Type | Description |
|---|---|---|
name | string | Alias used in selectFont() |
url | string | Local font file URL or Google Fonts stylesheet URL |
descriptors.weight | string | number | Font weight. Default: 'normal' |
descriptors.style | string | Font style. Default: 'normal' |
descriptors.display | string | Font display strategy. Default: 'swap' |
descriptors.family | string | Override 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.
| Parameter | Type | Description |
|---|---|---|
name | string | Registered 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.
| Parameter | Type | Description |
|---|---|---|
name | string | Registered alias |
size | number | Font size in pixels |
overrides.weight | string | number | Override the registered weight |
overrides.style | string | Override the registered style |
Returns string | e.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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
name | string | Registered alias or raw CSS family name |
size | number | Font size in pixels |
opts.weight | string | number | Override the registered weight |
opts.style | string | Override 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
brush | string | LinearGradientBrush | RadialGradientBrush | PatternBrush | null | Brush 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
color | string | null | CSS color string, or null for no stroke |
width | number | Stroke width in pixels. Default: 1 |
opts.lineCap | string | 'butt' | 'round' | 'square'. Default: 'butt' |
opts.lineJoin | string | 'miter' | 'round' | 'bevel'. Default: 'miter' |
Returns void | ||
WakaCanvas.selectAlpha(dc, alpha)
Sets global alpha on the DC. Applies to all subsequent drawing operations.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
alpha | number | Opacity 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
color | string | null | CSS color string, or null to clear |
blur | number | Blur radius in pixels. Default: 0 |
offsetX | number | Horizontal shadow offset. Default: 0 |
offsetY | number | Vertical 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
pattern | number[] | null | Dash/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().
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
align | string | 'left' | 'center' | 'right' | 'start' | 'end' |
Returns void | ||
WakaCanvas.selectTextBaseline(dc, baseline)
Sets the vertical text alignment anchor for drawText().
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
baseline | string | '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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
t.a | number | Horizontal scaling. Default: 1 |
t.b | number | Horizontal skewing. Default: 0 |
t.c | number | Vertical skewing. Default: 0 |
t.d | number | Vertical scaling. Default: 1 |
t.e | number | Horizontal translation. Default: 0 |
t.f | number | Vertical 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
x, y | number | Top-left corner |
w, h | number | Width and height |
Returns void | ||
WakaCanvas.drawRoundRect(dc, x, y, w, h, radius)
Draws a rectangle with rounded corners.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
x, y | number | Top-left corner |
w, h | number | Width and height |
radius | number | number[] | Corner radius in pixels, or [tl, tr, br, bl] for individual corners |
Returns void | ||
WakaCanvas.drawCircle(dc, cx, cy, r)
Draws a circle.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
cx, cy | number | Centre point |
r | number | Radius in pixels |
Returns void | ||
WakaCanvas.drawEllipse(dc, cx, cy, rx, ry, rotation?)
Draws an ellipse.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
cx, cy | number | Centre point |
rx, ry | number | Horizontal and vertical radii |
rotation | number | Rotation in radians. Default: 0 |
Returns void | ||
WakaCanvas.drawLine(dc, x1, y1, x2, y2)
Draws a straight line. No-op if NULL_PEN is selected.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
x1, y1 | number | Start point |
x2, y2 | number | End 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
cx, cy | number | Centre point |
r | number | Radius in pixels |
startAngle | number | Start angle in radians |
endAngle | number | End angle in radians |
ccw | boolean | Draw 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
text | string | Text to draw |
x, y | number | Position (affected by selectTextAlign and selectTextBaseline) |
maxWidth | number | Maximum 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | MetaFile | Target DC |
bitmap | CanvasRenderingContext2D | Bitmap handle from wakaPAC.loadBitmap() |
dx, dy | number | Destination position |
dw, dh | number | Destination 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | Target DC (MetaFile not supported) |
text | string | Text 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.
| Parameter | Type | Description |
|---|---|---|
dc | CanvasRenderingContext2D | Target DC |
x, y | number | Point to test, in canvas coordinates |
Returns * | null | Hit 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 anywakaPAC()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_LOADEDto 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()orselectPen()will silently inherit the previous value. Establish the full required state before each shape. - Use NULL_PEN to suppress text outlines —
drawText()strokes the text outline if a pen is selected. CallselectPen(dc, null)before drawing text unless an outline is intentional.