From a694a8dc49a2c6026acb9c6d400cfd8b91bfbee9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sun, 31 May 2026 12:14:42 +0100 Subject: [PATCH] =?UTF-8?q?feat(stdlib):=20Canvas.affine=20=E2=80=94=20HTM?= =?UTF-8?q?L5=20Canvas=202D=20rendering=20(bindings=20#8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New stdlib module covering the Tier-1 #8 Canvas 2D half — idaptik-ums App.res's 1178-LoC DOM-and-canvas rendering surface has been waiting for this. WebGL/WebGL2/WebGPU stays at `○` until a consumer surfaces a concrete need. stdlib/Canvas.affine (+170 lines, new module): 1 extern type + 26 extern fns covering the full idaptik-ums-relevant Canvas 2D surface. | Surface | Externs | |---|---| | Context acquisition | canvasGetContext2D | | Styles | canvasFillStyle / StrokeStyle / LineWidth / GlobalAlpha | | Rectangles | canvasFillRect / StrokeRect / ClearRect | | Paths | canvasBeginPath / ClosePath / MoveTo / LineTo / Arc / Fill / Stroke | | Transform stack | canvasSave / Restore / Translate / Rotate / Scale | | Text | canvasFont / TextAlign / TextBaseline / FillText / StrokeText / MeasureText | | Images | canvasDrawImage / canvasDrawImageScaled | `HTMLCanvasElement` crosses the boundary as opaque `Json` (the canvas-creation entry point is host-dependent — browser `document.createElement("canvas")` vs idaptik's pre-existing DOM tree). Typed wrapper is the natural follow-up once `affinescript-dom` lands runtime support (currently blocked on the wasm-codegen for-in / while gap, issue #255). lib/codegen_deno.ml (+58 lines): 26 `__as_canvas*` prelude helpers + 26 dispatch entries adjacent to the Ipc block. tests/codegen-deno/canvas_smoke.{affine,harness.mjs} (+160 lines combined): 5 distinct smoke functions exercise every shipped extern. MockCtx2D records every method call as a typed-op tuple so the test asserts call order + arguments, not just side-effects. measureText round-trip exercises the Json-return shape. docs/bindings-roadmap.adoc row #8 status promoted `○ → ◑`; deferred items captured (WebGL, gradients/patterns, ImageData, bezierCurveTo / quadraticCurveTo / ellipse, compositing / clip). Refs #446 — Tier 1 #8. --- docs/bindings-roadmap.adoc | 6 +- lib/codegen_deno.ml | 62 +++++++ stdlib/Canvas.affine | 179 ++++++++++++++++++++ tests/codegen-deno/canvas_smoke.affine | 66 ++++++++ tests/codegen-deno/canvas_smoke.harness.mjs | 103 +++++++++++ 5 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 stdlib/Canvas.affine create mode 100644 tests/codegen-deno/canvas_smoke.affine create mode 100644 tests/codegen-deno/canvas_smoke.harness.mjs diff --git a/docs/bindings-roadmap.adoc b/docs/bindings-roadmap.adoc index a44c71a..a63eef5 100644 --- a/docs/bindings-roadmap.adoc +++ b/docs/bindings-roadmap.adoc @@ -91,9 +91,9 @@ no further significant ReScript → AffineScript work is tractable. |8 |*WebGL / Canvas2D context* (HTMLCanvasElement, getContext, drawImage, fillRect, transform stack) -|`○` -|`affinescript-canvas`, or extension to `affinescript-dom` -|idaptik-ums uses raw Canvas2D (App.res: 1178 LoC of DOM + canvas); also needed for any non-Pixi-based UI. +|`◑` partial (Canvas 2D half landed: 26 extern fns covering context acquisition, fill/stroke styles, rectangles, paths, transform stack, text rendering, and `drawImage`; WebGL / WebGL2 / WebGPU + `CanvasGradient`/`CanvasPattern` + `ImageData` pixel-level + curve primitives + compositing/clip deferred) +|`stdlib/Canvas.affine` +|idaptik-ums uses raw Canvas2D (App.res: 1178 LoC of DOM + canvas); also needed for any non-Pixi-based UI. Canvas2D half landed 2026-05-31. Test fixture: `tests/codegen-deno/canvas_smoke.{affine,harness.mjs}` exercises every shipped extern via 5 distinct smoke functions (`smokeRects` / `smokePath` / `smokeTransform` / `smokeText` / `smokeImages`) — full path/transform/text/image round-trips against a mocked `CanvasRenderingContext2D`. WebGL stays at `○` until a consumer surfaces a concrete need (Tier-3 axis when it lands; idaptik-ums is Canvas2D-only). |9 |*IPC / structuredClone* for host↔guest message passing (postMessage, MessageChannel, transfer ownership) diff --git a/lib/codegen_deno.ml b/lib/codegen_deno.ml index 4ef7ac1..22c0cb0 100644 --- a/lib/codegen_deno.ml +++ b/lib/codegen_deno.ml @@ -354,6 +354,39 @@ const __as_messagePortStart = (p) => { p.start(); return 0; }; const __as_messagePortClose = (p) => { p.close(); return 0; }; const __as_targetPostMessage = (t, msg) => { t.postMessage(msg); return 0; }; const __as_structuredCloneValue = (v) => structuredClone(v); +// ---- Canvas (bindings #8): HTML5 Canvas 2D rendering context ---- +// `canvas` arg is the consumer-supplied HTMLCanvasElement; helpers +// dispatch directly to the standard CanvasRenderingContext2D +// methods. Available unmodified in browsers, jsdom-under-Deno, +// idaptik's WebView host, and any DOM emulator. +const __as_canvasGetContext2D = (canvas) => canvas.getContext("2d"); +const __as_canvasFillStyle = (ctx, color) => { ctx.fillStyle = color; return 0; }; +const __as_canvasStrokeStyle = (ctx, color) => { ctx.strokeStyle = color; return 0; }; +const __as_canvasLineWidth = (ctx, w) => { ctx.lineWidth = w; return 0; }; +const __as_canvasGlobalAlpha = (ctx, a) => { ctx.globalAlpha = a; return 0; }; +const __as_canvasFillRect = (ctx, x, y, w, h) => { ctx.fillRect(x, y, w, h); return 0; }; +const __as_canvasStrokeRect = (ctx, x, y, w, h) => { ctx.strokeRect(x, y, w, h); return 0; }; +const __as_canvasClearRect = (ctx, x, y, w, h) => { ctx.clearRect(x, y, w, h); return 0; }; +const __as_canvasBeginPath = (ctx) => { ctx.beginPath(); return 0; }; +const __as_canvasClosePath = (ctx) => { ctx.closePath(); return 0; }; +const __as_canvasMoveTo = (ctx, x, y) => { ctx.moveTo(x, y); return 0; }; +const __as_canvasLineTo = (ctx, x, y) => { ctx.lineTo(x, y); return 0; }; +const __as_canvasArc = (ctx, x, y, r, s, e) => { ctx.arc(x, y, r, s, e); return 0; }; +const __as_canvasFill = (ctx) => { ctx.fill(); return 0; }; +const __as_canvasStroke = (ctx) => { ctx.stroke(); return 0; }; +const __as_canvasSave = (ctx) => { ctx.save(); return 0; }; +const __as_canvasRestore = (ctx) => { ctx.restore(); return 0; }; +const __as_canvasTranslate = (ctx, x, y) => { ctx.translate(x, y); return 0; }; +const __as_canvasRotate = (ctx, rad) => { ctx.rotate(rad); return 0; }; +const __as_canvasScale = (ctx, x, y) => { ctx.scale(x, y); return 0; }; +const __as_canvasFont = (ctx, font) => { ctx.font = font; return 0; }; +const __as_canvasTextAlign = (ctx, align) => { ctx.textAlign = align; return 0; }; +const __as_canvasTextBaseline = (ctx, baseline) => { ctx.textBaseline = baseline; return 0; }; +const __as_canvasFillText = (ctx, text, x, y) => { ctx.fillText(text, x, y); return 0; }; +const __as_canvasStrokeText = (ctx, text, x, y) => { ctx.strokeText(text, x, y); return 0; }; +const __as_canvasMeasureText = (ctx, text) => ctx.measureText(text); +const __as_canvasDrawImage = (ctx, img, x, y) => { ctx.drawImage(img, x, y); return 0; }; +const __as_canvasDrawImageScaled = (ctx, img, x, y, w, h) => { ctx.drawImage(img, x, y, w, h); return 0; }; // `++` is overloaded (string concat / array concat); `a + b` would // stringify arrays. Dispatch on shape so stdlib/string.affine's // `result ++ [x]` and `a ++ b` are both correct. @@ -602,6 +635,35 @@ let () = b "messagePortClose" (fun a -> Printf.sprintf "__as_messagePortClose(%s)" (arg 0 a)); b "targetPostMessage" (fun a -> Printf.sprintf "__as_targetPostMessage(%s, %s)" (arg 0 a) (arg 1 a)); b "structuredCloneValue" (fun a -> Printf.sprintf "__as_structuredCloneValue(%s)" (arg 0 a)); + (* ---- Canvas (bindings #8): HTML5 Canvas 2D rendering context ---- *) + b "canvasGetContext2D" (fun a -> Printf.sprintf "__as_canvasGetContext2D(%s)" (arg 0 a)); + b "canvasFillStyle" (fun a -> Printf.sprintf "__as_canvasFillStyle(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasStrokeStyle" (fun a -> Printf.sprintf "__as_canvasStrokeStyle(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasLineWidth" (fun a -> Printf.sprintf "__as_canvasLineWidth(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasGlobalAlpha" (fun a -> Printf.sprintf "__as_canvasGlobalAlpha(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasFillRect" (fun a -> Printf.sprintf "__as_canvasFillRect(%s, %s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a) (arg 4 a)); + b "canvasStrokeRect" (fun a -> Printf.sprintf "__as_canvasStrokeRect(%s, %s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a) (arg 4 a)); + b "canvasClearRect" (fun a -> Printf.sprintf "__as_canvasClearRect(%s, %s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a) (arg 4 a)); + b "canvasBeginPath" (fun a -> Printf.sprintf "__as_canvasBeginPath(%s)" (arg 0 a)); + b "canvasClosePath" (fun a -> Printf.sprintf "__as_canvasClosePath(%s)" (arg 0 a)); + b "canvasMoveTo" (fun a -> Printf.sprintf "__as_canvasMoveTo(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); + b "canvasLineTo" (fun a -> Printf.sprintf "__as_canvasLineTo(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); + b "canvasArc" (fun a -> Printf.sprintf "__as_canvasArc(%s, %s, %s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a) (arg 4 a) (arg 5 a)); + b "canvasFill" (fun a -> Printf.sprintf "__as_canvasFill(%s)" (arg 0 a)); + b "canvasStroke" (fun a -> Printf.sprintf "__as_canvasStroke(%s)" (arg 0 a)); + b "canvasSave" (fun a -> Printf.sprintf "__as_canvasSave(%s)" (arg 0 a)); + b "canvasRestore" (fun a -> Printf.sprintf "__as_canvasRestore(%s)" (arg 0 a)); + b "canvasTranslate" (fun a -> Printf.sprintf "__as_canvasTranslate(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); + b "canvasRotate" (fun a -> Printf.sprintf "__as_canvasRotate(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasScale" (fun a -> Printf.sprintf "__as_canvasScale(%s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a)); + b "canvasFont" (fun a -> Printf.sprintf "__as_canvasFont(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasTextAlign" (fun a -> Printf.sprintf "__as_canvasTextAlign(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasTextBaseline" (fun a -> Printf.sprintf "__as_canvasTextBaseline(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasFillText" (fun a -> Printf.sprintf "__as_canvasFillText(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a)); + b "canvasStrokeText" (fun a -> Printf.sprintf "__as_canvasStrokeText(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a)); + b "canvasMeasureText" (fun a -> Printf.sprintf "__as_canvasMeasureText(%s, %s)" (arg 0 a) (arg 1 a)); + b "canvasDrawImage" (fun a -> Printf.sprintf "__as_canvasDrawImage(%s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a)); + b "canvasDrawImageScaled" (fun a -> Printf.sprintf "__as_canvasDrawImageScaled(%s, %s, %s, %s, %s, %s)" (arg 0 a) (arg 1 a) (arg 2 a) (arg 3 a) (arg 4 a) (arg 5 a)); (* Generic JS array push helper (returns the array, fluent). *) b "arrayPush" (fun a -> Printf.sprintf "(%s.push(%s), %s)" (arg 0 a) (arg 1 a) (arg 0 a)); (* ---- honest string/number primitives underpinning the diff --git a/stdlib/Canvas.affine b/stdlib/Canvas.affine new file mode 100644 index 0000000..64d4bb6 --- /dev/null +++ b/stdlib/Canvas.affine @@ -0,0 +1,179 @@ +// SPDX-License-Identifier: MPL-2.0 +// SPDX-FileCopyrightText: 2026 hyperpolymath +// +// Canvas.affine — HTML5 Canvas 2D rendering context bindings +// (bindings #8 in docs/bindings-roadmap.adoc). +// +// Initial typed surface covering the Canvas 2D operations idaptik-ums +// (App.res: 1178 LoC of DOM + canvas) actually reaches for: context +// acquisition, fill / stroke styling, rectangle and path drawing, +// transform stack (save/restore/translate/rotate/scale), text +// rendering, and image drawing. +// +// Targets the Deno-ESM backend. `HTMLCanvasElement` and +// `CanvasRenderingContext2D` are standard web-platform types +// available in browser embedders, the Deno+jsdom flow, and any DOM +// emulator. No consumer-side init is required for the context-level +// operations — `canvasGetContext2D(canvasJson)` lowers to a direct +// `.getContext("2d")` call on the host canvas. +// +// `HTMLCanvasElement` itself crosses the boundary as opaque `Json` +// in this binding because the canvas-creation entry point is host- +// dependent (browser: `document.createElement("canvas")`; idaptik: +// already in the DOM tree). A typed wrapper layered on top is the +// natural follow-up once `affinescript-dom` lands runtime support +// (currently blocked on the for-in / while wasm-codegen gap, issue +// #255). +// +// Test fixture: `tests/codegen-deno/canvas_smoke.{affine,harness.mjs}`. +// +// Out of scope for this initial surface (deferred to follow-ups): +// * WebGL / WebGL2 / WebGPU contexts (separate axis — Tier 1 #8 +// scope intentionally split, this is the Canvas2D half) +// * `CanvasGradient` / `CanvasPattern` extern types (typed gradients +// and pattern fills — currently the consumer pre-computes a CSS +// colour string and uses `fillStyle`) +// * `ImageData` (getImageData / putImageData — pixel-level +// manipulation, useful but lower-frequency than the drawing +// surface) +// * `bezierCurveTo` / `quadraticCurveTo` / `ellipse` (curve +// primitives — needed for richer paths) +// * Compositing operations / clip / globalAlpha (deferred — most +// consumers reach for save+restore around the alpha or blend +// mode anyway) + +module Canvas; + +use Deno::{Json}; + +// ── Opaque host types ────────────────────────────────────────────── + +/// The `CanvasRenderingContext2D` returned by `canvas.getContext("2d")`. +/// All stateful rendering operations are methods on this handle. +pub extern type Ctx2D; + +// ── Context acquisition ──────────────────────────────────────────── + +/// `canvas.getContext("2d")` — obtain the 2D rendering context from +/// an `HTMLCanvasElement` (passed as opaque `Json` because the +/// canvas-creation entry point is host-dependent). +pub extern fn canvasGetContext2D(canvas: Json) -> Ctx2D; + +// ── Styles ───────────────────────────────────────────────────────── + +/// `ctx.fillStyle = color` — set the fill-style. `color` is any CSS +/// colour string (`"#ff0000"`, `"red"`, `"rgba(0,0,0,0.5)"`, …). +/// Returns 0. +pub extern fn canvasFillStyle(ctx: Ctx2D, color: String) -> Int; + +/// `ctx.strokeStyle = color` — set the stroke-style. Returns 0. +pub extern fn canvasStrokeStyle(ctx: Ctx2D, color: String) -> Int; + +/// `ctx.lineWidth = width` — stroke line width in pixels. Returns 0. +pub extern fn canvasLineWidth(ctx: Ctx2D, width: Float) -> Int; + +/// `ctx.globalAlpha = a` — global opacity (0.0–1.0) applied to all +/// subsequent rendering. Returns 0. +pub extern fn canvasGlobalAlpha(ctx: Ctx2D, a: Float) -> Int; + +// ── Rectangles ───────────────────────────────────────────────────── + +/// `ctx.fillRect(x, y, w, h)` — paint a solid rectangle using the +/// current `fillStyle`. Returns 0. +pub extern fn canvasFillRect(ctx: Ctx2D, x: Float, y: Float, w: Float, h: Float) -> Int; + +/// `ctx.strokeRect(x, y, w, h)` — outline a rectangle using the +/// current `strokeStyle` and `lineWidth`. Returns 0. +pub extern fn canvasStrokeRect(ctx: Ctx2D, x: Float, y: Float, w: Float, h: Float) -> Int; + +/// `ctx.clearRect(x, y, w, h)` — clear the rectangle to fully +/// transparent. Returns 0. +pub extern fn canvasClearRect(ctx: Ctx2D, x: Float, y: Float, w: Float, h: Float) -> Int; + +// ── Paths ────────────────────────────────────────────────────────── + +/// `ctx.beginPath()` — start a new path. Returns 0. +pub extern fn canvasBeginPath(ctx: Ctx2D) -> Int; + +/// `ctx.closePath()` — close the current sub-path back to its start. +/// Returns 0. +pub extern fn canvasClosePath(ctx: Ctx2D) -> Int; + +/// `ctx.moveTo(x, y)` — move the path pen without drawing. Returns 0. +pub extern fn canvasMoveTo(ctx: Ctx2D, x: Float, y: Float) -> Int; + +/// `ctx.lineTo(x, y)` — line segment from the current pen position. +/// Returns 0. +pub extern fn canvasLineTo(ctx: Ctx2D, x: Float, y: Float) -> Int; + +/// `ctx.arc(x, y, r, startRad, endRad, ccw?)` — circular arc; this +/// binding fixes counter-clockwise to `false` (the common case). +/// Returns 0. +pub extern fn canvasArc(ctx: Ctx2D, x: Float, y: Float, r: Float, startRad: Float, endRad: Float) -> Int; + +/// `ctx.fill()` — fill the current path using `fillStyle`. Returns 0. +pub extern fn canvasFill(ctx: Ctx2D) -> Int; + +/// `ctx.stroke()` — stroke the current path using `strokeStyle` and +/// `lineWidth`. Returns 0. +pub extern fn canvasStroke(ctx: Ctx2D) -> Int; + +// ── Transform stack ──────────────────────────────────────────────── + +/// `ctx.save()` — push the current transform / style state onto the +/// internal stack. Returns 0. +pub extern fn canvasSave(ctx: Ctx2D) -> Int; + +/// `ctx.restore()` — pop the most recently saved transform / style +/// state. Returns 0. +pub extern fn canvasRestore(ctx: Ctx2D) -> Int; + +/// `ctx.translate(x, y)` — translate the current transform. Returns 0. +pub extern fn canvasTranslate(ctx: Ctx2D, x: Float, y: Float) -> Int; + +/// `ctx.rotate(rad)` — rotate the current transform by `rad` +/// radians. Returns 0. +pub extern fn canvasRotate(ctx: Ctx2D, rad: Float) -> Int; + +/// `ctx.scale(x, y)` — scale the current transform. Returns 0. +pub extern fn canvasScale(ctx: Ctx2D, x: Float, y: Float) -> Int; + +// ── Text ─────────────────────────────────────────────────────────── + +/// `ctx.font = font` — set the font string (CSS font shorthand, +/// e.g. `"16px sans-serif"`). Returns 0. +pub extern fn canvasFont(ctx: Ctx2D, font: String) -> Int; + +/// `ctx.textAlign = align` — horizontal text alignment +/// (`"left" | "right" | "center" | "start" | "end"`). Returns 0. +pub extern fn canvasTextAlign(ctx: Ctx2D, align: String) -> Int; + +/// `ctx.textBaseline = baseline` — vertical text alignment +/// (`"top" | "middle" | "bottom" | "alphabetic" | "hanging"`). +/// Returns 0. +pub extern fn canvasTextBaseline(ctx: Ctx2D, baseline: String) -> Int; + +/// `ctx.fillText(text, x, y)` — paint filled text at `(x, y)` using +/// `fillStyle`, `font`, `textAlign`, `textBaseline`. Returns 0. +pub extern fn canvasFillText(ctx: Ctx2D, text: String, x: Float, y: Float) -> Int; + +/// `ctx.strokeText(text, x, y)` — outline text at `(x, y)` using +/// `strokeStyle`. Returns 0. +pub extern fn canvasStrokeText(ctx: Ctx2D, text: String, x: Float, y: Float) -> Int; + +/// `ctx.measureText(text)` — returns a `TextMetrics`-shaped JS object +/// exposed as opaque `Json`. Read `width` and font-baseline metrics +/// via existing Json accessors. +pub extern fn canvasMeasureText(ctx: Ctx2D, text: String) -> Json; + +// ── Images ───────────────────────────────────────────────────────── + +/// `ctx.drawImage(image, x, y)` — draw a host image (`HTMLImageElement`, +/// `HTMLCanvasElement`, `ImageBitmap`, …) at `(x, y)` at its +/// natural size. The image is opaque `Json` since its concrete type +/// depends on context. Returns 0. +pub extern fn canvasDrawImage(ctx: Ctx2D, image: Json, x: Float, y: Float) -> Int; + +/// `ctx.drawImage(image, x, y, w, h)` — same as `canvasDrawImage` +/// but scales the source to fit `(w, h)`. Returns 0. +pub extern fn canvasDrawImageScaled(ctx: Ctx2D, image: Json, x: Float, y: Float, w: Float, h: Float) -> Int; diff --git a/tests/codegen-deno/canvas_smoke.affine b/tests/codegen-deno/canvas_smoke.affine new file mode 100644 index 0000000..4112fdb --- /dev/null +++ b/tests/codegen-deno/canvas_smoke.affine @@ -0,0 +1,66 @@ +// SPDX-License-Identifier: MPL-2.0 +// bindings #8 — HTML5 Canvas 2D smoke test. +// +// Exercises every shipped extern via a single end-to-end drawing +// sequence: acquire context, set styles, fill rect, stroke path, +// save+translate+rotate+scale+restore the transform, draw text, +// measure text, and drawImage. The harness mocks both the canvas +// element and the 2D context. + +use Deno::{Json}; +use Canvas::{Ctx2D, canvasGetContext2D, canvasFillStyle, canvasStrokeStyle, canvasLineWidth, canvasGlobalAlpha, canvasFillRect, canvasStrokeRect, canvasClearRect, canvasBeginPath, canvasClosePath, canvasMoveTo, canvasLineTo, canvasArc, canvasFill, canvasStroke, canvasSave, canvasRestore, canvasTranslate, canvasRotate, canvasScale, canvasFont, canvasTextAlign, canvasTextBaseline, canvasFillText, canvasStrokeText, canvasMeasureText, canvasDrawImage, canvasDrawImageScaled}; + +/// Exercises the styles + rect-drawing surface. +pub fn smokeRects(canvas: Json) -> Ctx2D { + let ctx = canvasGetContext2D(canvas); + canvasFillStyle(ctx, "#ff0000"); + canvasStrokeStyle(ctx, "rgba(0, 0, 0, 0.5)"); + canvasLineWidth(ctx, 2.5); + canvasGlobalAlpha(ctx, 0.75); + canvasClearRect(ctx, 0.0, 0.0, 100.0, 100.0); + canvasFillRect(ctx, 10.0, 20.0, 30.0, 40.0); + canvasStrokeRect(ctx, 50.0, 60.0, 20.0, 20.0); + ctx +} + +/// Exercises the path surface (begin/move/line/arc/close + fill+stroke). +pub fn smokePath(ctx: Ctx2D) -> Int { + canvasBeginPath(ctx); + canvasMoveTo(ctx, 10.0, 10.0); + canvasLineTo(ctx, 20.0, 30.0); + canvasArc(ctx, 50.0, 50.0, 10.0, 0.0, 6.283); + canvasClosePath(ctx); + canvasFill(ctx); + canvasStroke(ctx); + 0 +} + +/// Exercises the transform stack — save / translate / rotate / scale / +/// restore — by applying a transform, drawing inside, and popping. +pub fn smokeTransform(ctx: Ctx2D) -> Int { + canvasSave(ctx); + canvasTranslate(ctx, 100.0, 100.0); + canvasRotate(ctx, 1.5708); + canvasScale(ctx, 2.0, 3.0); + canvasFillRect(ctx, 0.0, 0.0, 5.0, 5.0); + canvasRestore(ctx); + 0 +} + +/// Exercises the text surface — font, align, baseline, fill/stroke +/// text, and measure-text round-trip (returns Json). +pub fn smokeText(ctx: Ctx2D) -> Json { + canvasFont(ctx, "16px sans-serif"); + canvasTextAlign(ctx, "center"); + canvasTextBaseline(ctx, "middle"); + canvasFillText(ctx, "Hello", 50.0, 50.0); + canvasStrokeText(ctx, "Outlined", 50.0, 70.0); + canvasMeasureText(ctx, "Measurement") +} + +/// Exercises both drawImage shapes — natural-size and scaled. +pub fn smokeImages(ctx: Ctx2D, img: Json) -> Int { + canvasDrawImage(ctx, img, 10.0, 20.0); + canvasDrawImageScaled(ctx, img, 30.0, 40.0, 100.0, 50.0); + 0 +} diff --git a/tests/codegen-deno/canvas_smoke.harness.mjs b/tests/codegen-deno/canvas_smoke.harness.mjs new file mode 100644 index 0000000..d0814e2 --- /dev/null +++ b/tests/codegen-deno/canvas_smoke.harness.mjs @@ -0,0 +1,103 @@ +// SPDX-License-Identifier: MPL-2.0 +// bindings #8 — Node ESM harness for the Canvas 2D binding. +// +// Mocks an HTMLCanvasElement + CanvasRenderingContext2D, runs the +// generated smoke functions, and asserts every shipped extern's +// side-effect was observed on the mock context. + +import assert from "node:assert/strict"; + +class MockCtx2D { + constructor() { + this.fillStyle = "#000"; + this.strokeStyle = "#000"; + this.lineWidth = 1; + this.globalAlpha = 1; + this.font = "10px sans-serif"; + this.textAlign = "start"; + this.textBaseline = "alphabetic"; + this.ops = []; + } + fillRect(x, y, w, h) { this.ops.push(["fillRect", x, y, w, h]); } + strokeRect(x, y, w, h) { this.ops.push(["strokeRect", x, y, w, h]); } + clearRect(x, y, w, h) { this.ops.push(["clearRect", x, y, w, h]); } + beginPath() { this.ops.push(["beginPath"]); } + closePath() { this.ops.push(["closePath"]); } + moveTo(x, y) { this.ops.push(["moveTo", x, y]); } + lineTo(x, y) { this.ops.push(["lineTo", x, y]); } + arc(x, y, r, s, e) { this.ops.push(["arc", x, y, r, s, e]); } + fill() { this.ops.push(["fill"]); } + stroke() { this.ops.push(["stroke"]); } + save() { this.ops.push(["save"]); } + restore() { this.ops.push(["restore"]); } + translate(x, y) { this.ops.push(["translate", x, y]); } + rotate(rad) { this.ops.push(["rotate", rad]); } + scale(x, y) { this.ops.push(["scale", x, y]); } + fillText(text, x, y) { this.ops.push(["fillText", text, x, y]); } + strokeText(text, x, y) { this.ops.push(["strokeText", text, x, y]); } + measureText(text) { this.ops.push(["measureText", text]); return { width: text.length * 7.5, fontBoundingBoxAscent: 12, fontBoundingBoxDescent: 4 }; } + drawImage(img, ...rest) { this.ops.push(["drawImage", img, ...rest]); } +} + +const ctx = new MockCtx2D(); +const canvas = { getContext: (kind) => { assert.equal(kind, "2d", "getContext called with '2d'"); return ctx; } }; + +const { smokeRects, smokePath, smokeTransform, smokeText, smokeImages } = await import("./canvas_smoke.deno.js"); + +// Rects: assert each style + each of the three rect ops + clear. +const ctxBack = smokeRects(canvas); +assert.equal(ctxBack, ctx, "smokeRects returns the same ctx instance"); +assert.equal(ctx.fillStyle, "#ff0000"); +assert.equal(ctx.strokeStyle, "rgba(0, 0, 0, 0.5)"); +assert.equal(ctx.lineWidth, 2.5); +assert.equal(ctx.globalAlpha, 0.75); +assert.deepEqual(ctx.ops[0], ["clearRect", 0, 0, 100, 100]); +assert.deepEqual(ctx.ops[1], ["fillRect", 10, 20, 30, 40]); +assert.deepEqual(ctx.ops[2], ["strokeRect", 50, 60, 20, 20]); + +// Path: 7 ops in order (beginPath, moveTo, lineTo, arc, closePath, fill, stroke). +ctx.ops = []; +assert.equal(smokePath(ctx), 0); +assert.deepEqual(ctx.ops, [ + ["beginPath"], + ["moveTo", 10, 10], + ["lineTo", 20, 30], + ["arc", 50, 50, 10, 0, 6.283], + ["closePath"], + ["fill"], + ["stroke"], +]); + +// Transform: save → translate → rotate → scale → fillRect → restore. +ctx.ops = []; +assert.equal(smokeTransform(ctx), 0); +assert.deepEqual(ctx.ops, [ + ["save"], + ["translate", 100, 100], + ["rotate", 1.5708], + ["scale", 2, 3], + ["fillRect", 0, 0, 5, 5], + ["restore"], +]); + +// Text: font/align/baseline are set; fillText + strokeText recorded; +// measureText returns the typed metrics object. +ctx.ops = []; +const metrics = smokeText(ctx); +assert.equal(ctx.font, "16px sans-serif"); +assert.equal(ctx.textAlign, "center"); +assert.equal(ctx.textBaseline, "middle"); +assert.deepEqual(ctx.ops[0], ["fillText", "Hello", 50, 50]); +assert.deepEqual(ctx.ops[1], ["strokeText", "Outlined", 50, 70]); +assert.deepEqual(ctx.ops[2], ["measureText", "Measurement"]); +assert.equal(metrics.width, "Measurement".length * 7.5); +assert.equal(metrics.fontBoundingBoxAscent, 12); + +// Images: drawImage natural-size and scaled variants. +ctx.ops = []; +const img = { __mockImage: true, naturalWidth: 200, naturalHeight: 100 }; +assert.equal(smokeImages(ctx, img), 0); +assert.deepEqual(ctx.ops[0], ["drawImage", img, 10, 20]); +assert.deepEqual(ctx.ops[1], ["drawImage", img, 30, 40, 100, 50]); + +console.log("canvas_smoke.harness.mjs OK");