API
Canvas
Canvas dessine avec un contexte de style HTML Canvas 2D. Votre fonction draw enregistre le dessin ; la plateforme le rejoue nativement (Core Graphics sur iOS, android.graphics.Canvas sur Android) à la résolution de l’écran. Les nombres et les couleurs d’un dessin peuvent être des valeurs animées : lorsqu’elles s’animent, la plateforme redessine d’elle-même, sans réexécuter draw.
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";
Voir Canvas pour le guide et <Canvas> pour ses 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;
}
Une vue dessinée avec un contexte 2D. Elle accepte toutes les props de vue (style, classes, gestionnaires tactiles, accessibilité, ref), ainsi que :
| Prop | Type | Description |
|---|---|---|
draw | (cx, { width, height }) => void | Dessine le canvas. S’exécute une fois la taille connue, de nouveau lorsque les signaux qu’il lit changent, et de nouveau lorsque le canvas est redimensionné. Les valeurs animées passées en tant que nombres ou couleurs s’animent sans l’exécuter. |
Donnez une taille au canvas via son style (width / height, flex: 1, aspectRatio, …). Les coordonnées sont en points, avec l’origine en haut à gauche.
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);
#Animer sans redessiner
Tout argument typé CanvasNumber et tout style de remplissage (fillStyle, strokeStyle) accepte un AnimatedNode. Le canvas lit alors le nœud pendant qu’il dessine, et redessine nativement à chaque image de son animation :
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
Le contexte que reçoit draw. Les appels ne font qu’enregistrer des opérations ; la plateforme les dessine après le retour de draw.
#État et transformations
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;
| Membre | Description |
|---|---|
canvas | La taille du canvas en points. |
save() / restore() | Empile / dépile l’état de dessin (transformation, découpe, styles). |
translate(x, y) | Déplace l’origine. |
scale(x, y) | Met les axes à l’échelle. |
rotate(angle) | Fait pivoter les axes ; en radians, dans le sens horaire. |
transform(a, b, c, d, e, f) | Multiplie la transformation par la matrice [a c e; b d f]. |
setTransform(a, b, c, d, e, f) | Remplace la transformation. |
resetTransform() | Retour à l’identité. |
#Propriétés
| Propriété | Type | Description |
|---|---|---|
fillStyle | CanvasPaint | Couleur, dégradé ou nœud de couleur natif utilisé par fill, fillRect, fillText. |
strokeStyle | CanvasPaint | Idem pour les contours. |
lineWidth | CanvasNumber | Épaisseur du contour en points. |
lineCap | "butt" | "round" | "square" | Forme des extrémités des contours. |
lineJoin | "miter" | "round" | "bevel" | Forme de la jonction des segments d’un contour. |
miterLimit | number | Les jonctions en onglet plus longues que cette valeur (en épaisseurs de ligne) sont biseautées. |
lineDashOffset | CanvasNumber | Point de départ du motif de tirets. |
globalAlpha | CanvasNumber | Opacité de ce qui est dessiné ensuite, de 0 à 1. |
globalCompositeOperation | string | Manière dont ce qui est dessiné ensuite se combine avec le canvas ("source-over", "multiply", …). |
shadowColor | string | La couleur des ombres (transparent : aucune). |
shadowBlur | number | Flou de l’ombre en points. |
shadowOffsetX, shadowOffsetY | number | Décalage de l’ombre en points. |
font | string | Notation abrégée CSS font, par exemple "600 14px Inter" (tailles en px, pt ou rem). |
textAlign | "start" | "end" | "left" | "right" | "center" | Position horizontale du texte par rapport à son x. |
textBaseline | "alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom" | Position verticale du texte par rapport à son y. |
Tirets : setLineDash(segments: number[]) et getLineDash(): number[].
#Chemins
Le contexte possède un chemin courant, construit avec les mêmes méthodes que 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;
Toutes les coordonnées sont des CanvasNumber. Les angles sont en radians, dans le sens horaire à partir de l’axe x. Le radii de roundRect est un rayon unique ou 2 à 4 rayons, comme pour border-radius en CSS (haut gauche en premier).
#Dessin
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;
| Méthode | Description |
|---|---|
fill(path?, rule?) | Remplit le chemin courant (ou path) avec fillStyle, selon la règle non nulle (par défaut) ou pair-impair. |
stroke(path?) | Trace le contour du chemin courant (ou path) avec strokeStyle. |
clip(path?, rule?) | Restreint ce qui est dessiné ensuite au chemin. |
fillRect, strokeRect | Un rectangle, sans toucher au chemin courant. |
clearRect | Rend un rectangle transparent. |
#Texte
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 est synchrone : il mesure avec le moteur de texte de la plateforme et le font courant. Seul width est renvoyé.
cx.font = "11px system-ui";
const label = "100";
cx.fillText(label, 40 - cx.measureText(label).width, 20);
#Dégradés
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);
#Images
drawImage(image: string | ImageAsset, ...args: CanvasNumber[]): void;
Dessine une image donnée sous forme d’URL ou de ressource importée (import logo from "./logo.png"). Elle est chargée nativement ; le canvas se redessine une fois l’image chargée. Une ressource importée dessine le fichier importé tel quel ; importez le fichier @3x pour obtenir un rendu net sur tous les écrans. Trois formes, comme en HTML :
drawImage(image, dx, dy): à sa taille naturelle.drawImage(image, dx, dy, dw, dh): mise à l’échelle dans un rectangle.drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh): une partie de l’image (source en pixels de l’image).
#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
}
Un chemin réutilisable à remplir, à tracer ou à utiliser pour découper. Le constructeur copie un autre Path2D ou analyse nativement des données de chemin 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");
cx.save();
cx.scale(4, 4);
cx.fillStyle = "#ef4444";
cx.fill(heart);
cx.restore();
#Non pris en charge
L’accès aux pixels (getImageData, putImageData), filter, isPointInPath et les images SVG ne sont pas disponibles. Une couleur ou une police invalide annule cette seule opération, comme dans HTML Canvas.