Cette page a été traduite automatiquement et peut contenir des erreurs. Lire l'original en anglais

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.

mod-canvas.tsx
mod-canvas.tsxLive
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 :

PropTypeDescription
draw(cx, { width, height }) => voidDessine 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;
MembreDescription
canvasLa 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éTypeDescription
fillStyleCanvasPaintCouleur, dégradé ou nœud de couleur natif utilisé par fill, fillRect, fillText.
strokeStyleCanvasPaintIdem pour les contours.
lineWidthCanvasNumberÉ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.
miterLimitnumberLes jonctions en onglet plus longues que cette valeur (en épaisseurs de ligne) sont biseautées.
lineDashOffsetCanvasNumberPoint de départ du motif de tirets.
globalAlphaCanvasNumberOpacité de ce qui est dessiné ensuite, de 0 à 1.
globalCompositeOperationstringManière dont ce qui est dessiné ensuite se combine avec le canvas ("source-over", "multiply", …).
shadowColorstringLa couleur des ombres (transparent : aucune).
shadowBlurnumberFlou de l’ombre en points.
shadowOffsetX, shadowOffsetYnumberDécalage de l’ombre en points.
fontstringNotation 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éthodeDescription
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, strokeRectUn rectangle, sans toucher au chemin courant.
clearRectRend 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.