Esta página foi traduzida automaticamente e pode conter erros. Ler o original em inglês

API

Canvas

Canvas desenha com um contexto no estilo do HTML Canvas 2D. Sua função draw grava o desenho; a plataforma o reproduz nativamente (Core Graphics no iOS, android.graphics.Canvas no Android) na resolução da tela. Números e cores em um desenho podem ser valores animados: quando animam, a plataforma redesenha por conta própria, sem executar draw novamente.

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

Veja Canvas para o guia e <Canvas> para suas 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;
}

Uma view desenhada com um contexto 2D. Ela aceita todas as props de view (style, classes, handlers de toque, acessibilidade, ref), além de:

Prop Tipo Descrição
draw (cx, { width, height }) => void Desenha o canvas. Executa assim que o tamanho é conhecido, novamente quando os signals que ela lê mudam e novamente quando o canvas é redimensionado. Valores animados passados como números ou cores animam sem executá-la.

Defina o tamanho do canvas por meio do style (width / height, flex: 1, aspectRatio, …). As coordenadas são em pontos, com a origem no canto superior esquerdo.

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

#Animando sem redesenhar

Qualquer argumento do tipo CanvasNumber e qualquer paint (fillStyle, strokeStyle) aceita um AnimatedNode. O canvas então lê o nó enquanto desenha e redesenha nativamente a cada frame da animação:

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

O contexto que draw recebe. As chamadas apenas gravam operações; a plataforma as desenha depois que draw retorna.

#Estado e transformações

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;
Membro Descrição
canvas O tamanho do canvas em pontos.
save() / restore() Empilha / desempilha o estado de desenho (transformação, clip, estilos).
translate(x, y) Move a origem.
scale(x, y) Escala os eixos.
rotate(angle) Rotaciona os eixos; em radianos, sentido horário.
transform(a, b, c, d, e, f) Multiplica a transformação pela matriz [a c e; b d f].
setTransform(a, b, c, d, e, f) Substitui a transformação.
resetTransform() Volta à identidade.

#Propriedades

Propriedade Tipo Descrição
fillStyle CanvasPaint Cor, gradiente ou nó de cor nativo usado por fill, fillRect, fillText.
strokeStyle CanvasPaint O mesmo para os traços.
lineWidth CanvasNumber Largura do traço em pontos.
lineCap "butt" | "round" | "square" Como os traços terminam.
lineJoin "miter" | "round" | "bevel" Como os segmentos do traço se unem.
miterLimit number Junções miter mais longas que isso (em larguras de linha) são chanfradas.
lineDashOffset CanvasNumber Onde o padrão de tracejado começa.
globalAlpha CanvasNumber Opacidade do que for desenhado a seguir, de 0 a 1.
globalCompositeOperation string Como o que for desenhado a seguir se combina com o canvas ("source-over", "multiply", …).
shadowColor string A cor das sombras (transparent: nenhuma).
shadowBlur number Desfoque da sombra em pontos.
shadowOffsetX, shadowOffsetY number Deslocamento da sombra em pontos.
font string Forma abreviada font do CSS, por exemplo "600 14px Inter" (tamanhos em px, pt ou rem).
textAlign "start" | "end" | "left" | "right" | "center" Onde o texto fica horizontalmente em relação ao seu x.
textBaseline "alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom" Onde o texto fica verticalmente em relação ao seu y.

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

#Caminhos

O contexto tem um caminho atual, construído com os mesmos métodos de 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 as coordenadas são do tipo CanvasNumber. Os ângulos são em radianos, no sentido horário a partir do eixo x. O radii de roundRect é um único raio ou 2 a 4 raios, como no border-radius do CSS (canto superior esquerdo primeiro).

#Desenho

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 Descrição
fill(path?, rule?) Preenche o caminho atual (ou path) com fillStyle, pela regra nonzero (padrão) ou even-odd.
stroke(path?) Traça o caminho atual (ou path) com strokeStyle.
clip(path?, rule?) Restringe o que for desenhado a seguir ao caminho.
fillRect, strokeRect Um retângulo, sem alterar o caminho atual.
clearRect Torna um retângulo transparente.

#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 é síncrono: ele mede com o mecanismo de texto da plataforma e o font atual. Apenas width é informado.

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

#Gradientes

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

#Imagens

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

Desenha uma imagem informada como URL ou como asset importado (import logo from "./logo.png"). Ela é carregada nativamente; o canvas desenha novamente assim que ela for carregada. Um asset importado desenha o próprio arquivo importado; importe o arquivo @3x para desenhar com nitidez em todas as telas. Três formas, como no HTML:

  • drawImage(image, dx, dy): no tamanho natural.
  • drawImage(image, dx, dy, dw, dh): redimensionada para caber em um retângulo.
  • drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh): uma parte dela (origem em pixels da imagem).

#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
}

Um caminho reutilizável para preencher, traçar ou recortar. O construtor copia outro Path2D ou interpreta dados de caminho SVG nativamente:

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

#Não suportado

Acesso a pixels (getImageData, putImageData), filter, isPointInPath e imagens SVG não estão disponíveis. Uma cor ou fonte inválida descarta apenas aquela operação, como no HTML Canvas.