Esta página se tradujo automáticamente y puede contener errores. Leer el original en inglés

API

Canvas

Canvas dibuja con un contexto al estilo del Canvas 2D de HTML. Tu función draw registra el dibujo; la plataforma lo reproduce de forma nativa (Core Graphics en iOS, android.graphics.Canvas en Android) a la resolución de la pantalla. Los números y colores de un dibujo pueden ser valores animados: cuando se animan, la plataforma vuelve a dibujar por sí sola, sin ejecutar draw de nuevo.

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 para la guía y <Canvas> para sus 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;
}

Una vista dibujada con un contexto 2D. Acepta todas las props de vista (estilo, clases, manejadores táctiles, accesibilidad, ref), además de:

Prop Tipo Descripción
draw (cx, { width, height }) => void Dibuja el canvas. Se ejecuta cuando se conoce el tamaño, de nuevo cuando cambian las señales que lee y de nuevo cuando el canvas cambia de tamaño. Los valores animados pasados como números o colores se animan sin ejecutarla.

Define el tamaño del canvas mediante su estilo (width / height, flex: 1, aspectRatio, …). Las coordenadas están en puntos, con el origen en la esquina superior izquierda.

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

#Animar sin volver a dibujar

Cualquier argumento de tipo CanvasNumber y cualquier pintura (fillStyle, strokeStyle) acepta un AnimatedNode. El canvas lee entonces el nodo mientras dibuja y vuelve a dibujar de forma nativa en cada fotograma de su animación:

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

El contexto que recibe draw. Las llamadas solo registran operaciones; la plataforma las dibuja después de que draw retorne.

#Estado y transformaciones

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;
Miembro Descripción
canvas El tamaño del canvas en puntos.
save() / restore() Apila / desapila el estado de dibujo (transformación, recorte, estilos).
translate(x, y) Desplaza el origen.
scale(x, y) Escala los ejes.
rotate(angle) Rota los ejes; en radianes, en sentido horario.
transform(a, b, c, d, e, f) Multiplica la transformación por la matriz [a c e; b d f].
setTransform(a, b, c, d, e, f) Reemplaza la transformación.
resetTransform() Vuelve a la identidad.

#Propiedades

Propiedad Tipo Descripción
fillStyle CanvasPaint Color, degradado o nodo de color nativo usado por fill, fillRect, fillText.
strokeStyle CanvasPaint Lo mismo para los trazos.
lineWidth CanvasNumber Grosor del trazo en puntos.
lineCap "butt" | "round" | "square" Cómo terminan los trazos.
lineJoin "miter" | "round" | "bevel" Cómo se unen los segmentos del trazo.
miterLimit number Las uniones de tipo miter más largas que este valor (en anchos de línea) se biselan.
lineDashOffset CanvasNumber Dónde empieza el patrón de guiones.
globalAlpha CanvasNumber Opacidad de lo que se dibuja a continuación, de 0 a 1.
globalCompositeOperation string Cómo se combina lo que se dibuja a continuación con el canvas ("source-over", "multiply", …).
shadowColor string El color de las sombras (transparente: ninguna).
shadowBlur number Desenfoque de la sombra en puntos.
shadowOffsetX, shadowOffsetY number Desplazamiento de la sombra en puntos.
font string Forma abreviada font de CSS, p. ej. "600 14px Inter" (tamaños en px, pt o rem).
textAlign "start" | "end" | "left" | "right" | "center" Dónde se coloca el texto horizontalmente respecto a su x.
textBaseline "alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom" Dónde se coloca el texto verticalmente respecto a su y.

Guiones: setLineDash(segments: number[]) y getLineDash(): number[].

#Trazados

El contexto tiene un trazado actual, construido con los mismos métodos 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;

Todas las coordenadas son CanvasNumber. Los ángulos están en radianes, en sentido horario desde el eje x. El radii de roundRect es un solo radio o de 2 a 4 como en border-radius de CSS (empezando por la esquina superior izquierda).

#Dibujo

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étodo Descripción
fill(path?, rule?) Rellena el trazado actual (o path) con fillStyle, según la regla nonzero (por defecto) o even-odd.
stroke(path?) Traza el contorno del trazado actual (o path) con strokeStyle.
clip(path?, rule?) Restringe lo que se dibuja a continuación al trazado.
fillRect, strokeRect Un rectángulo, sin tocar el trazado actual.
clearRect Hace transparente un rectángulo.

#Texto

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 es síncrono: mide con el motor de texto de la plataforma y el font actual. Solo se informa de width.

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

#Degradados

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

#Imágenes

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

Dibuja una imagen indicada como URL o como recurso importado (import logo from "./logo.png"). Se carga de forma nativa; el canvas vuelve a dibujar cuando termina de cargarse. Un recurso importado dibuja el propio archivo importado; importa el archivo @3x para dibujar con nitidez en todas las pantallas. Hay tres formas, como en HTML:

  • drawImage(image, dx, dy): a su tamaño natural.
  • drawImage(image, dx, dy, dw, dh): escalada dentro de un rectángulo.
  • drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh): una parte de ella (origen en píxeles de la imagen).

#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 trazado reutilizable para rellenar, trazar o recortar. El constructor copia otro Path2D o interpreta datos de trazado SVG de forma nativa:

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

#No compatible

El acceso a píxeles (getImageData, putImageData), filter, isPointInPath y las imágenes SVG no están disponibles. Un color o una fuente no válidos descartan esa operación concreta, como en HTML Canvas.