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 中每个绘图面固定选择 draw 或 pixels;v0.1 不定义两条路径在同一绘图面上的混合顺序。所有坐标使用逻辑像素,矩形宽高和线宽必须非负,所有浮点数必须有限。dpr 以 1/8 为步长四舍五入并钳制到 [0.5, 8]。
每次 draw 从 identity transform、绘图面边界 clip 和空状态栈开始,结束后不保留绘图状态。save/restore 保存并恢复 transform 与 clip;restore 在空栈上是 invalid-operation。setTransform 替换而非追加当前 transform,clipRect 将当前 clip 与变换后的矩形相交。合成模式固定为 source-over;clear 仍受当前 transform 与 clip 影响。fillText 的 y 是 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 <= stride、pixels.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.maxBatchOperations、canvas.maxBatchBytes、canvas.maxFrameBytes、canvas.maxTextItems、canvas.callsPerMinute;值均为正整数,字节按编码后计算。 - 状态栈深度、单路径命令数和单文本 UTF-8 字节数由
canvas.maxBatchBytes与 schema 上限共同约束。 - 字体只能来自本应用包内类型化句柄。
- 宿主必须拒绝非有限浮点数,并用溢出安全的中间表示、绘图面裁剪和路径预算隔离极端但有限的坐标;不能另设未公布、因实现而异的数值拒绝阈值。
不属于本 API
场景树、动画系统、材质、灯光、物理、布局、命中测试和游戏对象属于 L2 guest 库。GPU 命令、着色器和显存资源属于尚未准入 v0.1 的独立 L1 提议。
一致性测试
宿主至少验证:批量指令顺序、尺寸变化竞态、无效浮点数、超大路径、错误字节长度、帧替换、隐藏页面节流、字体句柄失效,以及 JS/原生 guest 对同一输入得到等价结果。