v0.1首个正式支持的版本化规范查看版本范围

canvas

canvas 让 guest 在宿主拥有的真实绘图面上呈现内容。它提供受限 2D 指令和完整软件像素帧两条路径,但不把 DOM、浏览器 Canvas context 或像素读回能力交给 guest。

适用场景

场景 建议路径
图表、表单、棋盘和简单 2D UI draw 批量提交绘图指令
模拟器、像素编辑器和软件渲染器 present 提交一帧 RGBA 像素
自己做文本布局但使用宿主字体 measureText
跟随容器尺寸或屏幕密度变化 监听 canvas.resize

不适合 3D、自定义着色器、大规模场景树或需要读回渲染结果的工作流。这些需求应使用未来 GPU 能力或 guest 库,而不是继续扩张 canvas

L1 边界

宿主独占的是应用壳层中的真实绘图面、系统字体后端和把结果提交给显示系统的能力。guest 只靠内存与计算不能让像素出现在该绘图面上;场景树、布局、命中测试和动画仍可由 guest 基于本模块实现,因此不进入 L1。

能力声明

应用只声明实际使用的方法:

{
  "canvases": {
    "main": { "mode": "draw" },
    "software": { "mode": "pixels" }
  },
  "capabilities": [
    "host:canvas.getInfo",
    "host:canvas.draw",
    "host:canvas.present",
    "host:canvas.measureText"
  ],
  "events": ["canvas.frame", "canvas.resize"]
}

声明 canvas.draw 不会自动获得 canvas.present。事件随对应方法和绘图面的生命周期送达,不另设通配订阅能力。

Reference

数据类型

CanvasInfo {
  width: u32          // 逻辑像素
  height: u32
  dpr: f32            // 以 1/8 为步长量化
  mode: "draw" | "pixels"
  safeArea: { top: u32, right: u32, bottom: u32, left: u32 }
}

FrameInfo {
  width: u32
  height: u32
  stride: u32
  format: "rgba8"
  sequence: u64
}

Color { r: u8, g: u8, b: u8, a: u8 }
Rect  { x: f32, y: f32, width: f32, height: f32 }
Transform { a: f32, b: f32, c: f32, d: f32, e: f32, f: f32 }

TextStyle {
  font?: Handle<font, use, app-context, runtime>
  size: f32
  align: "start" | "center" | "end"
  direction: "ltr" | "rtl"
}

TextMetrics { advance: f32 }

PathCommand =
  | { kind: "moveTo", x: f32, y: f32 }
  | { kind: "lineTo", x: f32, y: f32 }
  | { kind: "quadraticTo", cpx: f32, cpy: f32, x: f32, y: f32 }
  | { kind: "cubicTo", cp1x: f32, cp1y: f32, cp2x: f32, cp2y: f32, x: f32, y: f32 }
  | { kind: "close" }

DrawOperation =
  | { op: "save" }
  | { op: "restore" }
  | { op: "setTransform", transform: Transform }
  | { op: "clipRect", rect: Rect }
  | { op: "clear", color: Color }
  | { op: "fillRect", rect: Rect, color: Color }
  | { op: "strokeRect", rect: Rect, color: Color, width: f32 }
  | { op: "fillPath", commands: PathCommand[], color: Color, fillRule: "nonzero" | "evenodd" }
  | { op: "strokePath", commands: PathCommand[], color: Color, width: f32,
      lineCap: "butt" | "round" | "square", lineJoin: "miter" | "round" | "bevel" }
  | { op: "fillText", x: f32, y: f32, text: string, style: TextStyle, color: Color }

canvasId 由应用壳层创建并传给 guest。它只标识本应用的绘图面,不是可跨应用复用的资源句柄。

manifest 中每个绘图面固定选择 drawpixels;v0.1 不定义两条路径在同一绘图面上的混合顺序。所有坐标使用逻辑像素,矩形宽高和线宽必须非负,所有浮点数必须有限。dpr 以 1/8 为步长四舍五入并钳制到 [0.5, 8]

每次 draw 从 identity transform、绘图面边界 clip 和空状态栈开始,结束后不保留绘图状态。save/restore 保存并恢复 transform 与 clip;restore 在空栈上是 invalid-operationsetTransform 替换而非追加当前 transform,clipRect 将当前 clip 与变换后的矩形相交。合成模式固定为 source-over;clear 仍受当前 transform 与 clip 影响。fillTexty 是 alphabetic baseline,文本方向由 TextStyle.direction 显式决定。

Color 使用非预乘 sRGB 8-bit 通道;宿主在内部需要时自行预乘。TextStyle.size 和 stroke width 必须为有限正数。零宽或零高矩形不产生像素;负宽高非法。路径必须以 moveTo 开始每个子路径,close 只能关闭活动子路径;填充时开放子路径隐式闭合,描边时不闭合。miter join 的 miter limit 固定为 10;v0.1 不提供 dash、渐变、阴影、滤镜或其他合成模式。

方法

canvas.getInfo

canvas.getInfo(canvasId) -> CanvasInfo | null

返回绘图面的当前尺寸。绘图面不存在或已经销毁时返回 null;guest 不应缓存结果跨越 canvas.resize

canvas.draw

canvas.draw(canvasId, operations: DrawOperation[]) -> void

一次提交上面定义的封闭指令联合,只能用于 manifest 中 mode 为 draw 的绘图面。整批按数组顺序原子接受:任一指令非法或整批超预算时不执行任何指令。宿主不得用成功返回掩盖丢弃。v0.1 的 draw 不绘制图片;guest 可自行解码后在另一个 pixels 绘图面使用 present,图片句柄留待后续版本单独设计。

canvas.present

canvas.present(canvasId, pixels: ByteSource, frame: FrameInfo) -> void

提交一帧完整 rgba8 像素,只能用于 manifest 中 mode 为 pixels 的绘图面。宿主验证 width × 4 <= stridepixels.byteLength == stride × height、绘图面尺寸和序号。每个绘图面成功接受的 guest sequence 必须严格递增;失败调用不消耗 sequence。调用完成只表示宿主接收了帧,不保证显示设备已经扫描输出。

canvas.measureText

canvas.measureText(canvasId, texts: string[], style: TextStyle) -> TextMetrics[]

批量测量文本。返回值只有非负的行内 advance,与输入顺序一一对应;align 只影响 fillText 相对原点的放置,不改变度量结果,direction 参与塑形但返回值始终是正向距离。字体句柄缺失时使用宿主随 v0.1 提供的默认 UI 字体。同一宿主构建、同一 font/size/direction 和字符串的 advance 必须与 fillText 使用的行内推进量在 1/64 逻辑像素内一致。该方法只做单行度量,不进行段落排版、换行、塑形结果暴露或富文本布局。

事件

event canvas.frame {
  canvasId,
  tick: u64,
  timeMs: f64
}

event canvas.resize {
  canvasId,
  width: u32,
  height: u32,
  dpr: f32,
  safeArea: { top: u32, right: u32, bottom: u32, left: u32 }
}

canvas.frame 是下一帧提示,不是高精度计时器。首个 tick 为 0,之后每个产生的提示递增 1;合并会让 guest 观察到间隙。它与 present 的 guest sequence 分属两个序列域。timeMs 从当前应用会话启动时的 0 开始,使用 1 ms 向下量化、相对、非递减时间。宿主可以合并积压帧提示。

绘图面信息变化时,最新 resize 必须先于针对新状态产生的第一个 frame 入队;中间 resize 可以合并为最终状态,但最终事件仍保持这个先后关系。这样 guest 在处理新帧提示前一定能观察到尺寸失效。

JS 工作流示例

下面的计数器每帧只提交一次批量绘制:

const canvasId = 'main';
let count = 0;

host.events.on('canvas.frame', ({ canvasId: id }) => {
  if (id !== canvasId) return;

  host.canvas.draw(canvasId, [
    { op: 'clear', color: { r: 16, g: 20, b: 24, a: 255 } },
    { op: 'fillRect', rect: { x: 24, y: 24, width: 180, height: 56 }, color: { r: 47, g: 129, b: 247, a: 255 } },
    { op: 'fillText', x: 40, y: 60, text: `Frame ${count++}`, style: { size: 16, align: 'start', direction: 'ltr' }, color: { r: 255, g: 255, b: 255, a: 255 } },
  ]);
});

软件渲染器应复用像素缓冲,并按最新 resize 结果调整:

const info = await host.canvas.getInfo('main');
if (!info) throw new Error('canvas unavailable');

const pixels = renderFrame(info.width, info.height);
await host.canvas.present('main', pixels, {
  width: info.width,
  height: info.height,
  stride: info.width * 4,
  format: 'rgba8',
  sequence: 1n,
});

原生 WASM 绑定示例

原生 guest 把已分配线性内存区域绑定为 ByteSource,但指针不进入逻辑 API:

CanvasInfo info = host_canvas_get_info(MAIN_CANVAS);
uint32_t byte_len = info.width * info.height * 4;
uint8_t *pixels = guest_alloc(byte_len);
render_rgba8(pixels, info.width, info.height);

host_canvas_present(
  MAIN_CANVAS,
  byte_source(pixels, byte_len),
  (FrameInfo){ info.width, info.height, info.width * 4, RGBA8, 1 }
);

绑定层必须先验证内存范围,再复制或借用字节;宿主不得在调用返回后保留未经声明的 guest 指针。

生命周期与背压

  • 绘图面由壳层创建,随应用视图销毁。
  • resize 后旧尺寸帧可以被拒绝为 stale-frame
  • 宿主只保留有限数量待呈现帧;新帧可以替换尚未显示的旧帧。
  • draw 全批接受或全批失败,不允许预算压力造成静默丢弃。
  • guest 暂停或不可见时,宿主可以停止发送 frame

错误

code 含义 retryable
not-found canvasId 不存在 false
invalid-operation 指令不在白名单、字段非法或绘图面 mode 不匹配 false
invalid-frame 尺寸、步长、格式或字节数不匹配 false
stale-frame 帧基于过期尺寸 true
limit-exceeded 批次、像素或帧率超过预算 true

安全与预算

  • 信任档位:green。
  • 不提供像素读回、屏幕内容、DOM 节点或真实 context。
  • v0.1 预算键为 canvas.maxBatchOperationscanvas.maxBatchBytescanvas.maxFrameBytescanvas.maxTextItemscanvas.callsPerMinute;值均为正整数,字节按编码后计算。
  • 状态栈深度、单路径命令数和单文本 UTF-8 字节数由 canvas.maxBatchBytes 与 schema 上限共同约束。
  • 字体只能来自本应用包内类型化句柄。
  • 宿主必须拒绝非有限浮点数,并用溢出安全的中间表示、绘图面裁剪和路径预算隔离极端但有限的坐标;不能另设未公布、因实现而异的数值拒绝阈值。

不属于本 API

场景树、动画系统、材质、灯光、物理、布局、命中测试和游戏对象属于 L2 guest 库。GPU 命令、着色器和显存资源属于尚未准入 v0.1 的独立 L1 提议。

一致性测试

宿主至少验证:批量指令顺序、尺寸变化竞态、无效浮点数、超大路径、错误字节长度、帧替换、隐藏页面节流、字体句柄失效,以及 JS/原生 guest 对同一输入得到等价结果。

Host API 版本与分层概览Host API v0.1版本非正式提议提议canvas API呈现canvas.getInfo()方法canvas.draw()方法canvas.present()方法canvas.measureText()方法canvas.frame event事件canvas.resize event事件asset API呈现asset.read()方法asset.loadFont()方法playback API呈现playback.open()方法playback.write()方法playback.stop()方法playback.ready event事件playback.ended event事件input API输入input.focus()方法input.pointer event事件input.key event事件input.wheel event事件input.focusChange event事件ime API输入ime.openSession()方法ime.updateState()方法ime.updateGeometry()方法ime.closeSession()方法ime.textUpdate event事件ime.composition event事件ime.formatUpdate event事件ime.characterBoundsRequest event事件ime.ended event事件capture API输入capture.requestCamera()方法capture.requestMic()方法capture.frame()方法capture.readAudio()方法capture.stop()方法capture.ended event事件store API数据store.get()方法store.put()方法store.delete()方法store.list()方法store.deletePrefix()方法clipboard API数据clipboard.readText()方法clipboard.writeText()方法env API平台集成env.snapshot()方法env.change event事件开始调用 Host API指南构建交互式绘图应用指南构建自绘文本编辑器指南管理媒体采集与播放指南组织本地数据与同步指南