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

Elementos

Canvas

Un canvas es una vista que dibujas tú mismo con una API de dibujo 2D, la de la HTML Canvas 2D API: gráficos, indicadores, anillos, firmas, formas personalizadas. El código de dibujo registra operaciones; la plataforma las reproduce de forma nativa (Core Graphics en iOS y Mac Catalyst, android.graphics.Canvas en Android) a la resolución de la pantalla. Rust crea uno con canvas(painter), un Painter que registra el dibujo; TSX, con <Canvas> de nsh y su función draw.

el-canvas.tsx
el-canvas.tsxLive
import { Canvas, mount, signal, Text, View } from "nsh";
import { createAnimatedValue, Easing } from "nsh";
import { platform, windowSize } from "nsh";
import { button, buttonText, tint, ui } from "./palette";

const TAU = Math.PI * 2;

// Twelve months of made-up values, 0–100.
const values = [32, 41, 38, 52, 47, 63, 58, 71, 66, 80, 74, 88];
const months = "JFMAMJJASOND";

function Button(props: { label: string; onPress: () => void }) {
  return (
    <View onPress={props.onPress} {...button()}>
      <Text style={buttonText()}>{props.label}</Text>
    </View>
  );
}

function App() {
  // The month under the finger or pointer: `draw` reads it, so moving redraws.
  const picked = signal(values.length - 1);
  const pick = (x: number, width: number) =>
    picked.set(Math.max(0, Math.min(values.length - 1, Math.round(((x - 20) / (width - 40)) * (values.length - 1)))));
  let chartWidth = 0;

  // The dial's angle is a native animated value: it turns without running `draw`.
  const angle = createAnimatedValue(0);
  const turns = signal(0);
  const spin = () => {
    turns.update((n) => n + 1);
    angle.timing(turns() * TAU, { duration: 900, easing: Easing.out(Easing.cubic) });
  };

  const wide = () => windowSize().width >= 700;

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 20, paddingTop: 24, gap: 14 }}>
      <Text style={{ fontSize: 22, fontWeight: "700", color: ui().text }}>canvas</Text>
      <Text style={{ color: ui().muted }}>
        {platform.desktop ? "Drag across the chart with the mouse" : "Drag your finger across the chart"} · {months[picked()]}{" "}
        = {values[picked()]}
      </Text>

      <View style={{ flex: 1, flexDirection: wide() ? "row" : "column", gap: 14 }}>
        <Canvas
          style={{ ...ui().inset, flex: 1, minHeight: 200, borderRadius: 14 }}
          onLayout={({ bounds }) => (chartWidth = bounds.size.width)}
          onStartShouldSetResponder={() => true}
          onResponderGrant={(e) => {
            pick(e.locationX, chartWidth);
            return true;
          }}
          onResponderMove={(e) => pick(e.locationX, chartWidth)}
          draw={(cx, { width, height }) => {
            const pad = 20;
            const x = (i: number) => pad + (i / (values.length - 1)) * (width - 2 * pad);
            const y = (v: number) => height - pad - 16 - (v / 100) * (height - 2 * pad - 32);
            const line = () => {
              cx.beginPath();
              values.forEach((v, i) => (i === 0 ? cx.moveTo(x(i), y(v)) : cx.lineTo(x(i), y(v))));
            };

            // Grid lines, dashed.
            cx.strokeStyle = ui().border;
            cx.lineWidth = 1;
            cx.setLineDash([4, 4]);
            for (const v of [25, 50, 75]) {
              cx.beginPath();
              cx.moveTo(pad, y(v));
              cx.lineTo(width - pad, y(v));
              cx.stroke();
            }
            cx.setLineDash([]);

            // The area under the line: a path filled with a linear gradient.
            const area = cx.createLinearGradient(0, pad, 0, height - pad);
            area.addColorStop(0, tint(ui().accent, 0.32));
            area.addColorStop(1, tint(ui().accent, 0));
            line();
            cx.lineTo(x(values.length - 1), y(0));
            cx.lineTo(x(0), y(0));
            cx.closePath();
            cx.fillStyle = area;
            cx.fill();

            line();
            cx.strokeStyle = ui().accent;
            cx.lineWidth = 2.5;
            cx.lineJoin = "round";
            cx.stroke();

            // Month labels: text.
            cx.font = "11px system-ui";
            cx.textAlign = "center";
            cx.textBaseline = "alphabetic";
            values.forEach((_, i) => {
              cx.fillStyle = i === picked() ? ui().text : ui().faint;
              cx.fillText(months[i], x(i), height - 10);
            });

            // The picked point, its guide and a bubble with its value.
            const i = picked();
            const px = x(i);
            const py = y(values[i]);
            cx.strokeStyle = tint(ui().text, 0.25);
            cx.beginPath();
            cx.moveTo(px, pad);
            cx.lineTo(px, y(0));
            cx.stroke();
            cx.shadowColor = tint(ui().accent, 0.6);
            cx.shadowBlur = 10;
            cx.fillStyle = ui().accent;
            cx.beginPath();
            cx.arc(px, py, 6, 0, TAU);
            cx.fill();
            cx.shadowBlur = 0;
            const label = String(values[i]);
            cx.font = "600 13px system-ui";
            const w = cx.measureText(label).width + 16;
            const bx = Math.max(pad, Math.min(width - pad - w, px - w / 2));
            cx.fillStyle = ui().text;
            cx.beginPath();
            cx.roundRect(bx, py - 36, w, 22, 6);
            cx.fill();
            cx.fillStyle = ui().page;
            cx.textBaseline = "middle";
            cx.fillText(label, bx + w / 2, py - 25);
          }}
        />

        <View style={{ ...ui().card, borderRadius: 14, padding: 16, gap: 12, alignItems: "center", width: wide() ? 240 : undefined, alignSelf: wide() ? "flex-start" : undefined }}>
          <Canvas
            style={{ width: 120, height: 120 }}
            draw={(cx, { width, height }) => {
              const r = width / 2 - 6;
              const c = cx.createConicGradient(0, width / 2, height / 2);
              c.addColorStop(0, ui().info);
              c.addColorStop(0.5, ui().violet);
              c.addColorStop(1, ui().info);
              cx.fillStyle = c;
              cx.beginPath();
              cx.arc(width / 2, height / 2, r, 0, TAU);
              cx.fill();
              cx.translate(width / 2, height / 2);
              cx.rotate(angle); // animates natively
              cx.fillStyle = ui().page;
              cx.beginPath();
              cx.moveTo(0, -r + 10);
              cx.lineTo(8, 0);
              cx.lineTo(-8, 0);
              cx.closePath();
              cx.fill();
              cx.beginPath();
              cx.arc(0, 0, 10, 0, TAU);
              cx.fill();
            }}
          />
          <Text style={{ color: ui().muted, textAlign: "center" }}>Turns: {turns()} (a native animated value)</Text>
          <Button label="Spin" onPress={spin} />
        </View>
      </View>
    </View>
  );
}

mount(App);
use std::f32::consts::{FRAC_PI_2, TAU};

use nsh_core::{canvas::LineCap, prelude::*};

/// A progress ring, 120 points square.
fn ring(progress: f32) -> impl IntoElement {
    let (center, radius) = (60.0, 50.0);
    canvas(
        Painter::new()
            .line_width(12.0)
            .line_cap(LineCap::Round)
            .stroke_color(rgb(0xe5e7eb))
            .begin_path()
            .arc(center, center, radius, 0.0, TAU)
            .stroke()
            .stroke_color(rgb(0x6366f1))
            .begin_path()
            .arc(center, center, radius, -FRAC_PI_2, -FRAC_PI_2 + progress * TAU)
            .stroke(),
    )
    .size(px(120.0))
}

En TSX, Canvas crea la vista nativa del canvas, la mide y ejecuta tu función draw con un contexto y el tamaño del canvas (consulta la Canvas API). En Rust, canvas(..) recibe un Painter, o una closure que devuelve uno: la closure vuelve a dibujar cuando cambia lo que lee, y el tamaño proviene de on_layout cuando el dibujo depende de él.

#Props

Un canvas acepta las props de una vista (estilos, props de pulsación y de gestos, onLayout, accesibilidad, ref, …) además de:

Prop Rust Tipo Descripción
draw canvas(painter) (cx: CanvasRenderingContext2D, size: { width: number; height: number }) => void Dibuja el canvas. Se ejecuta una vez que se conoce el tamaño, de nuevo cuando el canvas cambia de tamaño y de nuevo cuando cambian las señales que lee.

Dale un tamaño al canvas mediante sus estilos (width / height, flex, una relación de aspecto): su tamaño es el que le dan sus estilos.

#Cuándo se vuelve a dibujar

El dibujo se rastrea como un efecto: leer una señal dentro de él suscribe el dibujo a esa señal.

let values = signal(vec![12.0_f32, 40.0, 28.0, 64.0]);
let width = signal(0.0_f32);
let height = 160.0;

canvas(move || {
    let data = values.get(); // a change of values redraws
    let step = width.get() / data.len() as f32;
    data.iter().enumerate().fold(Painter::new().fill_color(rgb(0x0969da)), |p, (i, v)| {
        p.fill_rect(i as f32 * step + 4.0, height - v, step - 8.0, *v)
    })
})
.h(px(height))
.on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))

Los números y los colores también pueden ser valores animados nativos (CanvasNumber, CanvasPaint; en Rust, un &AnimatedValue en cualquier lugar donde vaya un número): la plataforma vuelve a dibujar por sí sola mientras se animan, sin trabajo en el código de la app por cada fotograma.

use std::f32::consts::TAU;

let angle = animated_value(0.0);
angle.timing(TAU, 1000.0, Easing::Linear);

canvas(
    Painter::new()
        .translate(40.0, 40.0)
        .rotate(&angle) // animates natively
        .fill_color(rgb(0xcf222e))
        .fill_rect(-20.0, -4.0, 40.0, 8.0),
)
.size(px(80.0))

#El contexto

CanvasRenderingContext2D es un subconjunto del del navegador, y Painter tiene las mismas operaciones como métodos en snake case (move_to, line_to, fill_rect, fill_text, stroke_color, …): la pila de estados (save, restore), transformaciones, trazados (moveTo, lineTo, arc, arcTo, ellipse, rect, roundRect, curvas de Bézier), fill / stroke / clip (también de un Path2D, que acepta datos de trazado SVG; fill_svg / stroke_svg en Rust), rectángulos, texto (fillText, strokeText, measureText), degradados lineales / radiales / cónicos, guiones de línea, sombras, globalAlpha, globalCompositeOperation y drawImage (imágenes por URL o desde un asset). Cada miembro figura en la referencia de la Canvas API; la guía de canvas explica paso a paso cómo dibujar gráficos y animaciones.

Las coordenadas están en puntos, con el origen en la esquina superior izquierda. measureText es síncrono y devuelve width. El Painter de Rust también responde a pruebas de impacto (is_point_in_path, is_point_in_stroke).

El contexto dibuja operaciones vectoriales e imágenes: el acceso a píxeles (getImageData / putImageData), filter y las imágenes SVG quedan fuera.

#Eventos

Los eventos de vista: onPress, onLayout, gestos y toques. Para gestionar toques relativos al dibujo, lee locationX / locationY en onTouchEnd, o sigue un arrastre con onResponderMove (.on_responder_move(..) en Rust, con e.location).

#Métodos de ref

Ninguno propio.