API
nsh/canvas
nsh/canvas 提供与 HTML Canvas 2D 风格一致的绘图上下文。你的 draw 函数记录绘图操作,平台随后以屏幕分辨率在原生侧回放(iOS 上是 Core Graphics,Android 上是 android.graphics.Canvas)。绘图中的数字和颜色可以是原生值:它们做动画时,平台会自行重绘,无需再次运行 draw。
import { mount, signal, Text, View } from "nsh";
import { createNativeValue, Easing } from "nsh/animated";
import { Canvas, Path2D } from "nsh/canvas";
const TAU = Math.PI * 2;
const heart = new Path2D("M12 21s-7-4.5-9.5-9A5.5 5.5 0 0 1 12 6a5.5 5.5 0 0 1 9.5 6c-2.5 4.5-9.5 9-9.5 9z");
function Chip(props: { label: string; onPress: () => void }) {
return (
<Text
onPress={props.onPress}
activeOpacity={0.6}
style={{ paddingHorizontal: 14, paddingVertical: 8, borderRadius: 16, overflow: "hidden", backgroundColor: "#e0e7ff", color: "#4338ca", fontWeight: "600" }}
>
{props.label}
</Text>
);
}
function App() {
const data = signal([12, 30, 22, 48, 36, 60, 52, 75, 64, 88]);
const progress = createNativeValue(0.3);
const target = signal(0.3);
const go = () => {
const to = Math.round(Math.random() * 100) / 100;
target.set(to);
progress.timing(to, { duration: 900, easing: Easing.inOut(Easing.cubic) });
};
return (
<View style={{ flex: 1, backgroundColor: "#f8fafc", padding: 20, paddingTop: 24, gap: 16 }}>
<Text style={{ fontSize: 22, fontWeight: "700" }}>Canvas</Text>
<Canvas
style={{ height: 170, borderRadius: 12, backgroundColor: "#fff" }}
draw={(ctx, { width, height }) => {
const values = data();
const pad = 16;
const w = width - pad * 2, h = height - pad * 2;
const x = (i: number) => pad + (i / (values.length - 1)) * w;
const y = (v: number) => pad + h - (v / 100) * h;
const area = ctx.createLinearGradient(0, pad, 0, pad + h);
area.addColorStop(0, "rgba(99, 102, 241, 0.35)");
area.addColorStop(1, "rgba(99, 102, 241, 0)");
ctx.beginPath();
ctx.moveTo(x(0), pad + h);
values.forEach((v, i) => ctx.lineTo(x(i), y(v)));
ctx.lineTo(x(values.length - 1), pad + h);
ctx.closePath();
ctx.fillStyle = area;
ctx.fill();
ctx.beginPath();
values.forEach((v, i) => (i ? ctx.lineTo(x(i), y(v)) : ctx.moveTo(x(i), y(v))));
ctx.strokeStyle = "#6366f1";
ctx.lineWidth = 2.5;
ctx.lineJoin = "round";
ctx.stroke();
}}
/>
<View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
<Canvas
style={{ width: 110, height: 110 }}
draw={(ctx, { width, height }) => {
const cx = width / 2, cy = height / 2, r = width / 2 - 10;
ctx.lineWidth = 12;
ctx.lineCap = "round";
ctx.strokeStyle = "#e2e8f0";
ctx.beginPath();
ctx.arc(cx, cy, r, 0, TAU);
ctx.stroke();
const g = ctx.createConicGradient(-Math.PI / 2, cx, cy);
g.addColorStop(0, "#22d3ee");
g.addColorStop(1, "#6366f1");
ctx.strokeStyle = g;
ctx.beginPath();
ctx.arc(cx, cy, r, -Math.PI / 2, progress.mul(TAU).sub(Math.PI / 2));
ctx.stroke();
ctx.fillStyle = "#0f172a";
ctx.font = "600 18px system-ui";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
ctx.fillText(`${Math.round(target() * 100)}%`, cx, cy);
}}
/>
<Canvas
style={{ width: 96, height: 96 }}
draw={(ctx) => {
ctx.save();
ctx.scale(4, 4);
ctx.fillStyle = "#ef4444";
ctx.fill(heart);
ctx.restore();
}}
/>
</View>
<View style={{ flexDirection: "row", gap: 10 }}>
<Chip label="New point" onPress={() => data.update((v) => [...v.slice(1), Math.round(Math.random() * 90) + 5])} />
<Chip label="Animate ring" onPress={go} />
</View>
</View>
);
}
mount(App);import { Canvas, CanvasGradient, CanvasRenderingContext2D, Path2D } from "nsh/canvas";
#Canvas
function Canvas(props: CanvasProps): YNode;
interface CanvasProps extends ViewProps {
draw: (ctx: CanvasRenderingContext2D, size: CanvasSize) => void;
}
interface CanvasSize {
width: number; // 点
height: number;
}
用 2D 上下文绘制的视图。它接受所有视图属性(样式、class、触摸处理、无障碍、ref),另外还有:
| 属性 | 类型 | 说明 |
|---|---|---|
draw | (ctx, { width, height }) => void | 绘制画布。尺寸确定后运行一次,之后在它读取的 signal 变化时、以及画布尺寸改变时再次运行。作为数字或颜色传入的原生值做动画时不会触发它。 |
通过样式给画布设置尺寸(width / height、flex: 1、aspectRatio 等)。坐标单位为点,原点在左上角。
import { mount, signal, Text, View } from "nsh";
import { Canvas } from "nsh/canvas";
function App() {
const values = signal([12, 30, 22, 48, 36, 60, 52, 75]);
return (
<View style={{ flex: 1, padding: 24 }}>
<Canvas
style={{ height: 160 }}
draw={(ctx, { width, height }) => {
const v = values();
const step = width / (v.length - 1);
ctx.strokeStyle = "#6366f1";
ctx.lineWidth = 3;
ctx.lineJoin = "round";
ctx.beginPath();
v.forEach((n, i) => (i ? ctx.lineTo(i * step, height - n * 2) : ctx.moveTo(0, height - n * 2)));
ctx.stroke();
}}
/>
<Text onPress={() => values.update((v) => [...v.slice(1), Math.random() * 80])}>Next</Text>
</View>
);
}
mount(App);
#不重绘的动画
所有类型为 CanvasNumber 的参数和所有填充/描边样式(fillStyle、strokeStyle)都接受 NativeNode。画布在绘制时读取该节点,并在其动画的每一帧在原生侧重绘:
import { createNativeValue, Easing } from "nsh/animated";
import { Canvas } from "nsh/canvas";
const progress = createNativeValue(0);
<Canvas
style={{ width: 120, height: 120 }}
draw={(ctx, { width }) => {
const c = width / 2;
ctx.lineWidth = 12;
ctx.lineCap = "round";
ctx.strokeStyle = "#6366f1";
ctx.beginPath();
// 终止角是原生节点:`progress` 做动画时在原生侧重绘。
ctx.arc(c, c, c - 10, -Math.PI / 2, progress.mul(Math.PI * 2).sub(Math.PI / 2));
ctx.stroke();
}}
/>;
progress.timing(0.75, { duration: 900, easing: Easing.inOut(Easing.cubic) });
type CanvasNumber = number | NativeNode;
type CanvasPaint = string | CanvasGradient | NativeNode;
#CanvasRenderingContext2D
draw 收到的上下文。调用只会记录操作;draw 返回后由平台绘制。
#状态与变换
readonly canvas: { readonly width: number; readonly height: number };
save(): void;
restore(): void;
translate(x: CanvasNumber, y: CanvasNumber): void;
scale(x: CanvasNumber, y: CanvasNumber): void;
rotate(angle: CanvasNumber): void;
transform(a: CanvasNumber, b: CanvasNumber, c: CanvasNumber, d: CanvasNumber, e: CanvasNumber, f: CanvasNumber): void;
setTransform(a: CanvasNumber, b: CanvasNumber, c: CanvasNumber, d: CanvasNumber, e: CanvasNumber, f: CanvasNumber): void;
resetTransform(): void;
| 成员 | 说明 |
|---|---|
canvas | 画布尺寸(点)。 |
save() / restore() | 保存 / 恢复绘图状态(变换、裁剪、样式)。 |
translate(x, y) | 移动原点。 |
scale(x, y) | 缩放坐标轴。 |
rotate(angle) | 旋转坐标轴;弧度,顺时针。 |
transform(a, b, c, d, e, f) | 用矩阵 [a c e; b d f] 乘以当前变换。 |
setTransform(a, b, c, d, e, f) | 替换当前变换。 |
resetTransform() | 恢复为单位矩阵。 |
#属性
| 属性 | 类型 | 说明 |
|---|---|---|
fillStyle | CanvasPaint | fill、fillRect、fillText 使用的颜色、渐变或原生颜色节点。 |
strokeStyle | CanvasPaint | 描边使用的同类样式。 |
lineWidth | CanvasNumber | 线宽(点)。 |
lineCap | "butt" | "round" | "square" | 线条端点样式。 |
lineJoin | "miter" | "round" | "bevel" | 线段连接样式。 |
miterLimit | number | 尖角连接超过该长度(以线宽计)时改为斜角。 |
lineDashOffset | CanvasNumber | 虚线模式的起始偏移。 |
globalAlpha | CanvasNumber | 接下来绘制内容的不透明度,0..1。 |
globalCompositeOperation | string | 接下来绘制的内容如何与画布合成("source-over"、"multiply" 等)。 |
shadowColor | string | 阴影颜色(透明即无阴影)。 |
shadowBlur | number | 阴影模糊半径(点)。 |
shadowOffsetX、shadowOffsetY | number | 阴影偏移(点)。 |
font | string | CSS font 简写,例如 "600 14px Inter"(尺寸单位可以是 px、pt 或 rem)。 |
textAlign | "start" | "end" | "left" | "right" | "center" | 文本相对于 x 的水平位置。 |
textBaseline | "alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom" | 文本相对于 y 的垂直位置。 |
虚线:setLineDash(segments: number[]) 和 getLineDash(): number[]。
#路径
上下文有一条当前路径,用与 Path2D 相同的方法构建:
beginPath(): void;
moveTo(x, y): void;
lineTo(x, y): void;
quadraticCurveTo(cpx, cpy, x, y): void;
bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y): void;
arc(x, y, radius, startAngle, endAngle, counterclockwise?: boolean): void;
arcTo(x1, y1, x2, y2, radius): void;
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, counterclockwise?: boolean): void;
rect(x, y, w, h): void;
roundRect(x, y, w, h, radii?: CanvasNumber | CanvasNumber[]): void;
closePath(): void;
所有坐标都是 CanvasNumber。角度以弧度为单位,从 x 轴顺时针计算。roundRect 的 radii 是一个半径,或像 CSS border-radius 那样的 2–4 个半径(从左上角开始)。
#绘制
fill(path?: Path2D | "nonzero" | "evenodd", rule?: "nonzero" | "evenodd"): void;
stroke(path?: Path2D): void;
clip(path?: Path2D | "nonzero" | "evenodd", rule?: "nonzero" | "evenodd"): void;
fillRect(x, y, w, h): void;
strokeRect(x, y, w, h): void;
clearRect(x, y, w, h): void;
| 方法 | 说明 |
|---|---|
fill(path?, rule?) | 用 fillStyle 填充当前路径(或 path),按非零(默认)或奇偶规则。 |
stroke(path?) | 用 strokeStyle 描边当前路径(或 path)。 |
clip(path?, rule?) | 把之后的绘制限制在路径范围内。 |
fillRect、strokeRect | 绘制矩形,不影响当前路径。 |
clearRect | 把矩形区域变为透明。 |
#文本
fillText(text: string, x: CanvasNumber, y: CanvasNumber, maxWidth?: number): void;
strokeText(text: string, x: CanvasNumber, y: CanvasNumber, maxWidth?: number): void;
measureText(text: string): TextMetrics; // { width: number }
measureText 是同步的:它用平台的文本引擎和当前 font 测量。只返回 width。
ctx.font = "11px system-ui";
const label = "100";
ctx.fillText(label, 40 - ctx.measureText(label).width, 20);
#渐变
createLinearGradient(x0: number, y0: number, x1: number, y1: number): CanvasGradient;
createRadialGradient(x0: number, y0: number, r0: number, x1: number, y1: number, r1: number): CanvasGradient;
createConicGradient(startAngle: number, x: number, y: number): CanvasGradient;
class CanvasGradient {
addColorStop(offset: number, color: string): void; // offset 0..1,任意 CSS 颜色
}
const g = ctx.createLinearGradient(0, 0, 0, height);
g.addColorStop(0, "rgba(99, 102, 241, 0.4)");
g.addColorStop(1, "rgba(99, 102, 241, 0)");
ctx.fillStyle = g;
ctx.fillRect(0, 0, width, height);
#图片
drawImage(image: string | ImageAsset | { uri: string }, ...args: CanvasNumber[]): void;
绘制一张图片,可以是 URL、导入的资源(import logo from "./logo.png")或 { uri }。图片在原生侧加载,加载完成后画布会重新绘制。与 HTML 一样有三种形式:
drawImage(image, dx, dy):按原始尺寸绘制。drawImage(image, dx, dy, dw, dh):缩放到矩形内。drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh):绘制图片的一部分(源区域以图片像素计)。
#Path2D
class Path2D {
constructor(path?: Path2D | string);
addPath(path: Path2D): void;
// 以及所有路径方法:moveTo、lineTo、quadraticCurveTo、bezierCurveTo、
// arc、arcTo、ellipse、rect、roundRect、closePath
}
可复用的路径,用于填充、描边或裁剪。构造函数可以复制另一个 Path2D,或在原生侧解析 SVG 路径数据:
const heart = new Path2D("M12 21s-7-4.5-9.5-9A5.5 5.5 0 0 1 12 6a5.5 5.5 0 0 1 9.5 6c-2.5 4.5-9.5 9-9.5 9z");
ctx.save();
ctx.scale(4, 4);
ctx.fillStyle = "#ef4444";
ctx.fill(heart);
ctx.restore();
#不支持的功能
不支持像素访问(getImageData、putImageData)、filter、isPointInPath 和 SVG 图片。与 HTML Canvas 一样,无效的颜色或字体只会让那一个操作被丢弃。