v0.1首个版本化设计基线查看版本范围
Developer

canvas

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

适用场景

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

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

能力声明

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

{
  "capabilities": [
    "host:canvas.getInfo",
    "host:canvas.draw",
    "host:canvas.measureText"
  ]
}

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

Reference

数据类型

CanvasInfo {
  width: u32          // 逻辑像素
  height: u32
  dpr: f32            // 已按信任档位限制精度
  mode: "draw" | "pixels" | "hybrid"
}

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

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

方法

canvas.getInfo

canvas.getInfo(canvasId) -> CanvasInfo | null

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

canvas.draw

canvas.draw(canvasId, operations[]) -> { accepted: u32, dropped: u32 }

一次提交一批 v0.1 白名单 2D 指令。指令至少覆盖变换栈、矩形、路径、裁剪、纯色、包内图片和文本。未知指令、非有限数值、越界资源引用和超预算批次必须被拒绝,不能透传给浏览器绘图对象。

资源参数使用 manifest 中的 assetIdHandle<font, use, app-context, runtime>,不接受 URL、路径或文件 token。

canvas.present

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

提交一帧完整 rgba8 像素。宿主验证 width × 4 <= stride、总字节数、绘图面尺寸和序号。调用完成只表示宿主接收了帧,不保证显示设备已经扫描输出。

canvas.measureText

canvas.measureText(canvasId, texts[], font?) -> TextMetrics[]

批量测量文本。返回结果与输入顺序一一对应;字体句柄缺失时使用绘图面的默认字体。该方法只做度量,不进行段落排版、换行或富文本布局。

事件

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

event canvas.resize {
  canvasId,
  width: u32,
  height: u32,
  dpr: f32
}

canvas.frame 是下一帧提示,不是高精度计时器。timeMs 使用平台统一的粗化、相对、非递减时间。宿主可以合并积压帧提示。

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: '#101418' },
    { op: 'fillRect', x: 24, y: 24, width: 180, height: 56, color: '#2f81f7' },
    { op: 'fillText', x: 40, y: 60, text: `Frame ${count++}`, color: '#ffffff' },
  ]);
});

软件渲染器应复用像素缓冲,并按最新 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 返回的 dropped 使被预算丢弃的指令可观察。
  • guest 暂停或不可见时,宿主可以停止发送 frame

错误

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

安全与预算

  • 信任档位:green。
  • 不提供像素读回、屏幕内容、DOM 节点或真实 context。
  • 单次预算至少覆盖指令数、路径点数、文本总长度和像素字节数。
  • 时间窗预算覆盖 draw/present 次数和总提交字节。
  • 图像和字体只能来自本应用包内资源或类型化句柄。
  • 宿主必须把非有限浮点数、极端变换矩阵和病态路径挡在渲染后端之前。

不属于本 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事件ime API输入ime.openSession()方法ime.updateState()方法ime.updateGeometry()方法ime.closeSession()方法ime.textUpdate event事件ime.composition event事件ime.formatUpdate event事件ime.characterBoundsRequest 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()方法clipboard.readImage()方法clipboard.writeImage()方法net API网络net.fetch()方法identity API身份identity.status()方法identity.login()方法identity.logout()方法env API平台集成env.snapshot()方法env.change event事件print API平台集成print.open()方法开始调用 Host API指南构建交互式绘图应用指南构建自绘文本编辑器指南管理媒体采集与播放指南组织本地数据与同步指南