Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions docs/bindings-roadmap.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
62 changes: 62 additions & 0 deletions lib/codegen_deno.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down
179 changes: 179 additions & 0 deletions stdlib/Canvas.affine
Original file line number Diff line number Diff line change
@@ -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;
66 changes: 66 additions & 0 deletions tests/codegen-deno/canvas_smoke.affine
Original file line number Diff line number Diff line change
@@ -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
}
Loading
Loading