API
Canvas
Canvas zeichnet mit einem Kontext im Stil von HTML Canvas 2D. Deine draw-Funktion zeichnet die Zeichenbefehle auf; die Plattform gibt sie anschließend nativ in der Auflösung des Bildschirms wieder (Core Graphics unter iOS, android.graphics.Canvas unter Android). Zahlen und Farben in einer Zeichnung können animierte Werte sein: Wenn sie sich animieren, zeichnet die Plattform selbstständig neu, ohne draw erneut auszuführen.
import { Canvas, mount, Path2D, signal, Text, View } from "nsh";
import { createAnimatedValue, Easing } from "nsh";
import { button, buttonText, Panel, Split, tint, ui, wide } from "./palette";
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 (
<View onPress={props.onPress} {...button()}>
<Text style={buttonText()}>{props.label}</Text>
</View>
);
}
function App() {
const data = signal([12, 30, 22, 48, 36, 60, 52, 75, 64, 88]);
const progress = createAnimatedValue(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: ui().page, padding: 20, paddingTop: 24, gap: 16 }}>
<Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>Canvas</Text>
<Split
gap={16}
width={560}
inspector={
wide() ? (
<>
<Panel title="draw reads a signal">
<View style={{ gap: 10 }}>
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>[{data().join(", ")}]</Text>
<Chip label="New point" onPress={() => data.update((v) => [...v.slice(1), Math.round(Math.random() * 90) + 5])} />
</View>
</Panel>
<Panel title="An animated value in draw">
<View style={{ gap: 10 }}>
<Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>{Math.round(target() * 100)}%</Text>
<Chip label="Animate ring" onPress={go} />
</View>
</Panel>
</>
) : (
<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>
)
}
>
<Canvas
style={{ height: 170, borderRadius: 12, ...ui().inset }}
draw={(cx, { 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 = cx.createLinearGradient(0, pad, 0, pad + h);
area.addColorStop(0, tint(ui().accent, 0.28));
area.addColorStop(1, tint(ui().accent, 0));
cx.beginPath();
cx.moveTo(x(0), pad + h);
values.forEach((v, i) => cx.lineTo(x(i), y(v)));
cx.lineTo(x(values.length - 1), pad + h);
cx.closePath();
cx.fillStyle = area;
cx.fill();
cx.beginPath();
values.forEach((v, i) => (i ? cx.lineTo(x(i), y(v)) : cx.moveTo(x(i), y(v))));
cx.strokeStyle = ui().accent;
cx.lineWidth = 2.5;
cx.lineJoin = "round";
cx.stroke();
}}
/>
<View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
<Canvas
style={{ width: 110, height: 110 }}
draw={(cx, { width, height }) => {
const centerX = width / 2, cy = height / 2, r = width / 2 - 10;
cx.lineWidth = 12;
cx.lineCap = "round";
cx.strokeStyle = ui().inset.backgroundColor;
cx.beginPath();
cx.arc(centerX, cy, r, 0, TAU);
cx.stroke();
const g = cx.createConicGradient(-Math.PI / 2, centerX, cy);
g.addColorStop(0, ui().info);
g.addColorStop(1, ui().accent);
cx.strokeStyle = g;
cx.beginPath();
cx.arc(centerX, cy, r, -Math.PI / 2, progress.mul(TAU).sub(Math.PI / 2));
cx.stroke();
cx.fillStyle = ui().text;
cx.font = "600 18px system-ui";
cx.textAlign = "center";
cx.textBaseline = "middle";
cx.fillText(`${Math.round(target() * 100)}%`, centerX, cy);
}}
/>
<Canvas
style={{ width: 96, height: 96 }}
draw={(cx) => {
cx.save();
cx.scale(4, 4);
cx.fillStyle = ui().down;
cx.fill(heart);
cx.restore();
}}
/>
</View>
</Split>
</View>
);
}
mount(App);import { Canvas, CanvasGradient, CanvasRenderingContext2D, Path2D } from "nsh";
Siehe Canvas für die Anleitung und <Canvas> für die zugehörigen Props.
#Canvas
function Canvas(props: CanvasProps): JSX.Element;
interface CanvasProps extends ViewProps {
draw: (cx: CanvasRenderingContext2D, size: CanvasSize) => void;
}
interface CanvasSize {
width: number; // points
height: number;
}
Eine View, die mit einem 2D-Kontext gezeichnet wird. Sie akzeptiert alle View-Props (Style, Klassen, Touch-Handler, Accessibility, ref) sowie:
| Prop | Typ | Beschreibung |
|---|---|---|
draw |
(cx, { width, height }) => void |
Zeichnet den Canvas. Wird ausgeführt, sobald die Größe bekannt ist, erneut, wenn sich die gelesenen Signals ändern, und noch einmal, wenn sich die Größe des Canvas ändert. Animierte Werte, die als Zahlen oder Farben übergeben werden, animieren, ohne dass die Funktion erneut ausgeführt wird. |
Gib dem Canvas über seinen Style eine Größe (width / height, flex: 1, aspectRatio, …). Koordinaten sind in Punkten angegeben, der Ursprung liegt oben links.
import { Canvas, mount, signal, Text, View } from "nsh";
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={(cx, { width, height }) => {
const v = values();
const step = width / (v.length - 1);
cx.strokeStyle = "#6366f1";
cx.lineWidth = 3;
cx.lineJoin = "round";
cx.beginPath();
v.forEach((n, i) => (i ? cx.lineTo(i * step, height - n * 2) : cx.moveTo(0, height - n * 2)));
cx.stroke();
}}
/>
<Text onPress={() => values.update((v) => [...v.slice(1), Math.random() * 80])}>Next</Text>
</View>
);
}
mount(App);
#Animieren ohne Neuzeichnen
Jedes Argument vom Typ CanvasNumber und jede Füllung bzw. Linienfarbe (fillStyle, strokeStyle) akzeptiert einen AnimatedNode. Der Canvas liest den Node dann beim Zeichnen und zeichnet bei jedem Frame der Animation nativ neu:
import { createAnimatedValue, Easing } from "nsh";
import { Canvas } from "nsh";
const progress = createAnimatedValue(0);
<Canvas
style={{ width: 120, height: 120 }}
draw={(cx, { width }) => {
const c = width / 2;
cx.lineWidth = 12;
cx.lineCap = "round";
cx.strokeStyle = "#6366f1";
cx.beginPath();
// The end angle is an animated node: animating `progress` redraws natively.
cx.arc(c, c, c - 10, -Math.PI / 2, progress.mul(Math.PI * 2).sub(Math.PI / 2));
cx.stroke();
}}
/>;
progress.timing(0.75, { duration: 900, easing: Easing.inOut(Easing.cubic) });
type CanvasNumber = number | AnimatedNode;
type CanvasPaint = string | CanvasGradient | AnimatedNode;
#CanvasRenderingContext2D
Der Kontext, den draw erhält. Aufrufe zeichnen nur Operationen auf; die Plattform zeichnet sie, nachdem draw zurückgekehrt ist.
#Zustand und Transformationen
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;
| Member | Beschreibung |
|---|---|
canvas |
Die Größe des Canvas in Punkten. |
save() / restore() |
Legt den Zeichenzustand (Transformation, Clip, Styles) auf den Stack / holt ihn wieder davon. |
translate(x, y) |
Verschiebt den Ursprung. |
scale(x, y) |
Skaliert die Achsen. |
rotate(angle) |
Rotiert die Achsen; Bogenmaß, im Uhrzeigersinn. |
transform(a, b, c, d, e, f) |
Multipliziert die Transformation mit der Matrix [a c e; b d f]. |
setTransform(a, b, c, d, e, f) |
Ersetzt die Transformation. |
resetTransform() |
Zurück zur Identität. |
#Eigenschaften
| Eigenschaft | Typ | Beschreibung |
|---|---|---|
fillStyle |
CanvasPaint |
Farbe, Verlauf oder nativer Farb-Node, verwendet von fill, fillRect, fillText. |
strokeStyle |
CanvasPaint |
Dasselbe für Linien. |
lineWidth |
CanvasNumber |
Linienbreite in Punkten. |
lineCap |
"butt" | "round" | "square" |
Wie Linien enden. |
lineJoin |
"miter" | "round" | "bevel" |
Wie Liniensegmente verbunden werden. |
miterLimit |
number |
Miter-Verbindungen, die länger als dieser Wert (in Linienbreiten) sind, werden abgeschrägt. |
lineDashOffset |
CanvasNumber |
Wo das Strichmuster beginnt. |
globalAlpha |
CanvasNumber |
Deckkraft dessen, was als Nächstes gezeichnet wird, 0..1. |
globalCompositeOperation |
string |
Wie sich das, was als Nächstes gezeichnet wird, mit dem Canvas verbindet ("source-over", "multiply", …). |
shadowColor |
string |
Die Farbe von Schatten (transparent: kein Schatten). |
shadowBlur |
number |
Schattenunschärfe in Punkten. |
shadowOffsetX, shadowOffsetY |
number |
Schattenversatz in Punkten. |
font |
string |
CSS-Kurzschreibweise für font, z. B. "600 14px Inter" (Größen in px, pt oder rem). |
textAlign |
"start" | "end" | "left" | "right" | "center" |
Wo der Text horizontal relativ zu seinem x liegt. |
textBaseline |
"alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom" |
Wo der Text vertikal relativ zu seinem y liegt. |
Strichmuster: setLineDash(segments: number[]) und getLineDash(): number[].
#Pfade
Der Kontext hat einen aktuellen Pfad, der mit denselben Methoden aufgebaut wird wie 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;
Alle Koordinaten sind CanvasNumbers. Winkel sind im Bogenmaß angegeben, im Uhrzeigersinn ab der x-Achse. Der Parameter radii von roundRect ist ein einzelner Radius oder 2 bis 4 Werte wie bei CSS border-radius (zuerst oben links).
#Zeichnen
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;
| Methode | Beschreibung |
|---|---|
fill(path?, rule?) |
Füllt den aktuellen Pfad (oder path) mit fillStyle, nach der Nonzero-Regel (Standard) oder der Even-Odd-Regel. |
stroke(path?) |
Zeichnet die Kontur des aktuellen Pfads (oder path) mit strokeStyle. |
clip(path?, rule?) |
Beschränkt das, was als Nächstes gezeichnet wird, auf den Pfad. |
fillRect, strokeRect |
Ein Rechteck, ohne den aktuellen Pfad zu verändern. |
clearRect |
Macht ein Rechteck transparent. |
#Text
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 arbeitet synchron: Es misst mit der Text-Engine der Plattform und dem aktuellen font. Es wird nur width zurückgegeben.
cx.font = "11px system-ui";
const label = "100";
cx.fillText(label, 40 - cx.measureText(label).width, 20);
#Verläufe
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, any CSS color
}
const g = cx.createLinearGradient(0, 0, 0, height);
g.addColorStop(0, "rgba(99, 102, 241, 0.4)");
g.addColorStop(1, "rgba(99, 102, 241, 0)");
cx.fillStyle = g;
cx.fillRect(0, 0, width, height);
#Bilder
drawImage(image: string | ImageAsset, ...args: CanvasNumber[]): void;
Zeichnet ein Bild, das als URL oder als importiertes Asset (import logo from "./logo.png") angegeben wird. Es wird nativ geladen; der Canvas zeichnet erneut, sobald es geladen ist. Ein importiertes Asset zeichnet die importierte Datei selbst; importiere die Datei @3x, um auf jedem Bildschirm scharf zu zeichnen. Drei Formen, wie in HTML:
drawImage(image, dx, dy): in seiner natürlichen Größe.drawImage(image, dx, dy, dw, dh): in ein Rechteck skaliert.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh): ein Ausschnitt davon (Quelle in Bildpixeln).
#Path2D
class Path2D {
constructor(path?: Path2D | string);
addPath(path: Path2D): void;
// and every path method: moveTo, lineTo, quadraticCurveTo, bezierCurveTo,
// arc, arcTo, ellipse, rect, roundRect, closePath
}
Ein wiederverwendbarer Pfad zum Füllen, Konturieren oder Clippen. Der Konstruktor kopiert einen anderen Path2D oder parst SVG-Pfaddaten nativ:
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");
cx.save();
cx.scale(4, 4);
cx.fillStyle = "#ef4444";
cx.fill(heart);
cx.restore();
#Nicht unterstützt
Pixelzugriff (getImageData, putImageData), filter, isPointInPath und SVG-Bilder sind nicht verfügbar. Eine ungültige Farbe oder Schrift lässt genau diese eine Operation entfallen, wie bei HTML Canvas.