Questa pagina è stata tradotta automaticamente e può contenere errori. Leggi l'originale in inglese

API

Canvas

Canvas disegna con un contesto in stile HTML Canvas 2D. La tua funzione draw registra il disegno; la piattaforma lo riproduce in modo nativo (Core Graphics su iOS, android.graphics.Canvas su Android) alla risoluzione dello schermo. I numeri e i colori di un disegno possono essere valori animati: quando si animano, la piattaforma ridisegna da sola, senza eseguire di nuovo 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";

Consulta Canvas per la guida e <Canvas> per le sue prop.

#Canvas

function Canvas(props: CanvasProps): JSX.Element;

interface CanvasProps extends ViewProps {
  draw: (cx: CanvasRenderingContext2D, size: CanvasSize) => void;
}

interface CanvasSize {
  width: number;   // points
  height: number;
}

Una vista disegnata con un contesto 2D. Accetta ogni prop delle viste (stile, classi, gestori del tocco, accessibilità, ref), oltre a:

PropTipoDescrizione
draw(cx, { width, height }) => voidDisegna il canvas. Viene eseguita non appena la dimensione è nota, di nuovo quando cambiano i signal che legge e di nuovo quando il canvas viene ridimensionato. I valori animati passati come numeri o colori si animano senza eseguirla.

Assegna una dimensione al canvas tramite il suo stile (width / height, flex: 1, aspectRatio, …). Le coordinate sono in punti, con l’origine in alto a sinistra.

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);

#Animare senza ridisegnare

Qualsiasi argomento di tipo CanvasNumber e qualsiasi paint (fillStyle, strokeStyle) accetta un AnimatedNode. Il canvas legge quindi il nodo mentre disegna e ridisegna in modo nativo a ogni frame della sua animazione:

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

Il contesto che riceve draw. Le chiamate si limitano a registrare le operazioni; la piattaforma le disegna dopo che draw ha restituito il controllo.

#Stato e trasformazioni

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;
MembroDescrizione
canvasLa dimensione del canvas in punti.
save() / restore()Inserisce / rimuove dallo stack lo stato di disegno (trasformazione, clip, stili).
translate(x, y)Sposta l’origine.
scale(x, y)Scala gli assi.
rotate(angle)Ruota gli assi; in radianti, in senso orario.
transform(a, b, c, d, e, f)Moltiplica la trasformazione per la matrice [a c e; b d f].
setTransform(a, b, c, d, e, f)Sostituisce la trasformazione.
resetTransform()Torna all’identità.

#Proprietà

ProprietàTipoDescrizione
fillStyleCanvasPaintColore, gradiente o nodo colore nativo usato da fill, fillRect, fillText.
strokeStyleCanvasPaintLo stesso per i contorni.
lineWidthCanvasNumberSpessore del contorno in punti.
lineCap"butt" | "round" | "square"Come terminano i contorni.
lineJoin"miter" | "round" | "bevel"Come si uniscono i segmenti del contorno.
miterLimitnumberLe giunzioni miter più lunghe di questo valore (in spessori di linea) vengono smussate.
lineDashOffsetCanvasNumberIl punto in cui inizia il motivo del tratteggio.
globalAlphaCanvasNumberOpacità di ciò che viene disegnato dopo, da 0 a 1.
globalCompositeOperationstringCome ciò che viene disegnato dopo si combina con il canvas ("source-over", "multiply", …).
shadowColorstringIl colore delle ombre (transparent: nessuna).
shadowBlurnumberSfocatura dell’ombra in punti.
shadowOffsetX, shadowOffsetYnumberOffset dell’ombra in punti.
fontstringNotazione abbreviata CSS font, ad es. "600 14px Inter" (dimensioni in px, pt o rem).
textAlign"start" | "end" | "left" | "right" | "center"Dove si colloca il testo orizzontalmente rispetto alla sua x.
textBaseline"alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom"Dove si colloca il testo verticalmente rispetto alla sua y.

Tratteggi: setLineDash(segments: number[]) e getLineDash(): number[].

#Percorsi

Il contesto ha un percorso corrente, costruito con gli stessi metodi di 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;

Tutte le coordinate sono CanvasNumber. Gli angoli sono in radianti, in senso orario a partire dall’asse x. Il radii di roundRect è un singolo raggio oppure da 2 a 4 valori come in CSS border-radius (prima l’angolo in alto a sinistra).

#Disegno

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;
MetodoDescrizione
fill(path?, rule?)Riempie il percorso corrente (o path) con fillStyle, secondo la regola nonzero (predefinita) o even-odd.
stroke(path?)Traccia il contorno del percorso corrente (o path) con strokeStyle.
clip(path?, rule?)Limita ciò che viene disegnato dopo al percorso.
fillRect, strokeRectUn rettangolo, senza toccare il percorso corrente.
clearRectRende trasparente un rettangolo.

#Testo

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 è sincrono: misura con il motore di testo della piattaforma e il font corrente. Viene riportato solo width.

cx.font = "11px system-ui";
const label = "100";
cx.fillText(label, 40 - cx.measureText(label).width, 20);

#Gradienti

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);

#Immagini

drawImage(image: string | ImageAsset, ...args: CanvasNumber[]): void;

Disegna un’immagine indicata come URL o come asset importato (import logo from "./logo.png"). Viene caricata in modo nativo; il canvas ridisegna una volta completato il caricamento. Un asset importato disegna il file importato così com’è; importa il file @3x per ottenere un disegno nitido su ogni schermo. Tre forme, come in HTML:

  • drawImage(image, dx, dy): alla sua dimensione naturale.
  • drawImage(image, dx, dy, dw, dh): scalata all’interno di un rettangolo.
  • drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh): una sua parte (sorgente in pixel dell’immagine).

#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 percorso riutilizzabile da riempire, tracciare o usare come clip. Il costruttore copia un altro Path2D oppure interpreta in modo nativo i dati di percorso 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 supportato

L’accesso ai pixel (getImageData, putImageData), filter, isPointInPath e le immagini SVG non sono disponibili. Un colore o un font non validi annullano quella singola operazione, come in HTML Canvas.