Canvas(Web 2D 绘图上下文)
Canvas 是基于 SwiftUI Canvas 的视图,对外提供一套与 Web Canvas 2D 一致的命令式 API。
JS 端的 CanvasRenderingContext 是指令收集器——每次方法调用或属性赋值都会记录一条
命令;SwiftUI 每次重新评估视图(状态 / 布局变化)时,会由 Swift 端把命令队列回放到真实的
GraphicsContext 上完成绘制。
适用场景
- 你已经熟悉 Web Canvas API,希望以最低成本把脚本迁移过来。
- 需要命令式绘图(自定义图表、走势图、徽章、签名、生成式艺术等),用声明式的
Shape/Rectangle/Chart不好表达的场景。 - 绘制内容依赖于脚本在 render 时计算出的数据,而不是 60fps 的连续动画。
如果需要逐帧动画,请改用 <TimelineCanvas> — 同样的 draw API,但通过 SwiftUI
TimelineView 按 ~60fps tick。<Canvas> 的 closure 触发频率与 React render
同步(state / 布局变化),不是每帧。
基本用法
Props
没有 width / height props——请用 frame / padding / aspectRatio 等通用
修饰符来控制尺寸。实际绘制尺寸通过 draw 的第二个参数 size 传入。
draw 必须是对 React state 纯粹的——不要在里面 setState。返回值会被忽略。
支持的 API
状态栈
save() — 把当前 context 状态(变换、不透明度、裁剪、样式)压入栈。
restore() — 把栈顶状态弹出,还原到当前 context。
变换
路径
beginPath、closePath、moveTo、lineTo、quadraticCurveTo、bezierCurveTo、
arc、arcTo、rect、ellipse。
ellipse(x, y, rx, ry, rotation, startAngle, endAngle, counterclockwise) 完整支持
旋转的部分椭圆弧,所有参数均生效。
绘制
文本
fillText(text, x, y, maxWidth?)、strokeText(text, x, y, maxWidth?)。
font支持数字(14→system(size: 14))、SwiftUI 字体名("caption"、"headline"等)或自定义字体对象{ name, size }——和项目其他地方的 Font 字段一致。textAlign/textBaseline会映射到 SwiftUIcontext.draw(_:at:anchor:)的 anchor。strokeText当前退化为用strokeStyle填充。outline-only 描边文本尚未支持。
measureText
measureText 是同步调用——会立刻往返一次 host 拿到结果,可以用来驱动后续绘制
(文字居中、按测量结果绘制背景胶囊、手动断行等)。它使用当前 ctx.font 值,返回的
尺寸与绘制坐标同单位。
测量底层走 UIKit (NSAttributedString + UIFont)。对于 SwiftUI textStyle 字体
名("headline"、"body" 等),会用 UIFont.preferredFont(forTextStyle:),因此
width 会跟随用户当前 Dynamic Type 设置。SwiftUI 自身的渲染在边角字形上可能与 UIKit
差异不到 1pt。
图片
- 接受
{ systemName }(SF Symbols)、{ filePath }(本地文件路径)或{ image: UIImage }(内存中的 UIImage)。 - 9 参数形态(
sx, sy, sw, sh, dx, dy, dw, dh)会先把源矩形裁剪出来再绘制到目标矩形。 imageSmoothingEnabled = false切换到最近邻插值(适合像素艺术)。- 远程 URL 暂不支持——异步加载请改用
Image组件。
样式属性
与 Web canvas 同名同语义:
fillStyle、strokeStyle— 颜色字符串(见下)、CanvasGradient或CanvasPattern。lineWidth、lineCap、lineJoin、miterLimit、setLineDash([...])/getLineDash()、lineDashOffset。globalAlpha— 映射到 SwiftUI context 的 opacity。font、textAlign、textBaseline。shadowOffsetX、shadowOffsetY、shadowBlur、shadowColor— 阴影状态, 作用于后续fill/stroke/fillText/drawImage。globalCompositeOperation— 后续绘制的 blend mode(见下)。imageSmoothingEnabled— 控制drawImage的图像插值。
颜色字符串
fillStyle / strokeStyle 中的颜色字符串走的是桥层统一的解析器,以下都合法:
- 系统色名:
"systemBlue"、"systemGray6"、"label"、"secondaryLabel"、"accentColor"。 - Hex:
"#0a84ff"、"#fff"。 "rgb(r, g, b)"/"rgba(r, g, b, a)"。"hsl(h, s%, l%)"/"hsla(h, s%, l%, a)"—— hue 是 0-360 的度数, saturation / lightness 是 0-100 的百分比(必须带%),alpha 是 0-1。
渐变
还可用 createRadialGradient(x0, y0, r0, x1, y1, r1)。
createConicGradient(startAngle, x, y)(对应 SwiftUI AngularGradient)也可用 ——
经典 Web Canvas 没有,但映射干净,顺手暴露。
渐变端点使用 canvas 像素坐标,与 Web Canvas 行为一致。
Radial gradient 提示: Web 的
createRadialGradient需要两个圆(焦点 + 外圆); SwiftUI 只接受一个中心 + start/end 半径。桥层使用第二个圆的中心(x1, y1), 把r0/r1作为 start / end 半径。当r0 ≈ 0(常见用法)时视觉一致, 否则焦点偏移会被近似掉。
Pattern 填充
ctx.createPattern(image, repetition) 返回 CanvasPattern,可赋给 fillStyle /
strokeStyle。image 接受跟 drawImage 一样的来源形态。
限制: SwiftUI 的 tiledImage shading 只支持双轴重复。
"repeat-x"、"repeat-y"、"no-repeat"当前被接受但行为等同"repeat"。如果需要单轴控制, 请配合ctx.clip(...)自行裁剪。
Shadow
shadow 状态作用于后续 fill / stroke / fillText / drawImage。把
shadowColor 设为透明色(或把 shadowBlur 和两个 offset 都重置 0)即可关闭。
shadowBlur 跟 Web 同义,是 Gaussian blur 半径而非 standard deviation。
混合模式
支持的值:"source-over"(默认)、"multiply"、"screen"、"overlay"、
"darken"、"lighten"、"color-dodge"、"color-burn"、"hard-light"、
"soft-light"、"difference"、"exclusion"、"hue"、"saturation"、
"color"、"luminosity"、"plus-lighter"、"destination-over"。
不支持的值会 silently fallback 到 "source-over"。Web 的完整 Porter-Duff 子集
("source-in" / "destination-in" / "xor" 等)在 SwiftUI 没有 1:1 映射,暂不暴露。
性能
draw 闭包是从 SwiftUI Canvas closure 同步反向调用 JS 的。Canvas closure 触发
频率与 React render 同步(state / layout 变化),并非每帧——每次调用涉及一次 JSCore
往返加 commands 数组的 JSON 序列化,数百条命令在毫秒级完成。
请保持 draw body 轻量:避免重计算、大对象捕获、上千个 arc 段(用单条 bezierCurveTo
即可代替)。
TimelineCanvas(逐帧动画)
<Canvas> 的 draw 闭包只在 React 重新评估视图时(state / 布局变化)运行;真正的
requestAnimationFrame 式动画(弹球、粒子、扫针表盘、生成式循环)请改用
<TimelineCanvas>。内部组合 SwiftUI 的 Canvas + TimelineView,
draw 闭包按调度器节奏触发(默认 ~60fps)。
与 <Canvas> 的区别
Props
跨帧状态
draw 闭包每次 React render 都会重新创建。需要跨帧保留的状态(粒子数组、位置、累
加器)请放在 useRef 或模块顶层 — 跟经典 Web Canvas + rAF 的写法一致:
不要把每帧状态放在 useState 里 — 那会触发每帧 React re-render,纯浪费。
time 语义
time 是相对 mount 的秒数。两个含义:
- 跑几小时也不会溢出 Number 精度区间,
time * speed % period不会漂移。 - 组件 remount(例如 key 变化)时
time归零。
性能
每帧 = 一次 JSCore 往返 + 一次 commands 数组 JSON 编码。典型场景(几十个图元)
落在毫秒级,稳定 60fps 没问题。重场景(几百个 arc、多个 gradient、每帧
measureText)请盯 FPS 数:跌到 ~50 以下就改 schedule={{ minimumInterval: 1/30 }}。
几条经验:
- 不随帧变化的对象(gradient、颜色、预计算路径)做一次缓存就够。
- 尽量别在
draw内调measureText— 字体 / 文字变化时测一次,后续复用结果。 - 同屏多个
<TimelineCanvas>共享主线程;每个会瓜分帧预算。
视图离开屏幕时(NavigationStack push / 滑出 viewport)SwiftUI 会自动停 tick,
不需要手动清理。但主动暂停时(动画想停但视图还在屏)请用 paused: true。
暂未支持
以下 Web canvas API 已故意延后,若有强需求请反馈:
getImageData/putImageData(collector 模式无法读回像素)。isPointInPath/isPointInStroke(同上)。getTransform(collector 模式无法读回状态)。- outline-only
strokeText— 当前退化为用strokeStyle填充。 - 单轴 pattern 重复模式(
"repeat-x"/"repeat-y"/"no-repeat")。 - 在 SwiftUI 没有干净映射的 Porter-Duff
globalCompositeOperation值 ("source-in"/"destination-in"/"xor"等)。
