Elementos
Canvas
Um canvas é uma view que você mesmo desenha com uma API de desenho 2D, a da HTML Canvas 2D API: gráficos, medidores, anéis, assinaturas, formas personalizadas. O código de desenho registra operações; a plataforma as reproduz nativamente (Core Graphics no iOS e no Mac Catalyst, android.graphics.Canvas no Android) na resolução da tela. Em Rust, cria-se um com canvas(painter), um Painter que registra o desenho; em TSX, com <Canvas> de nsh e sua função draw.
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))
}import { Canvas, mount } from "nsh";
function Ring(props: { progress: number }) {
return (
<Canvas
style={{ width: 120, height: 120 }}
draw={(cx, { width, height }) => {
const centerX = width / 2, cy = height / 2, r = width / 2 - 10;
cx.lineWidth = 12;
cx.lineCap = "round";
cx.strokeStyle = "#e5e7eb";
cx.beginPath();
cx.arc(centerX, cy, r, 0, Math.PI * 2);
cx.stroke();
cx.strokeStyle = "#6366f1";
cx.beginPath();
cx.arc(centerX, cy, r, -Math.PI / 2, -Math.PI / 2 + props.progress * Math.PI * 2);
cx.stroke();
}}
/>
);
}
mount(() => <Ring progress={0.7} />);Em TSX, Canvas cria a view de canvas nativa, mede essa view e executa sua função draw com um contexto e o tamanho do canvas (veja a Canvas API). Em Rust, canvas(..) recebe um Painter, ou um closure que retorna um: o closure desenha novamente quando o que ele lê muda, e o tamanho vem de on_layout quando o desenho depende dele.
#Props
Um canvas aceita as props de uma view (estilos, props de toque e gestos, onLayout, acessibilidade, ref, …) além de:
| Prop | Rust | Tipo | Descrição |
|---|---|---|---|
draw |
canvas(painter) |
(cx: CanvasRenderingContext2D, size: { width: number; height: number }) => void |
Desenha o canvas. Executa assim que o tamanho é conhecido, novamente quando o canvas é redimensionado e novamente quando os signals que ele lê mudam. |
Dê um tamanho ao canvas por meio de seus estilos (width / height, flex, uma proporção): o tamanho dele é o que os estilos definem.
#Quando redesenha
O desenho é rastreado como um effect: ler um signal dentro dele inscreve o desenho nesse signal.
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))const values = signal([12, 40, 28, 64]);
<Canvas
style={{ height: 160 }}
draw={(cx, { width, height }) => {
const data = values(); // a change of values() redraws
const step = width / data.length;
cx.fillStyle = "#0969da";
data.forEach((v, i) => cx.fillRect(i * step + 4, height - v, step - 8, v));
}}
/>;Números e cores também podem ser valores animados nativos (CanvasNumber, CanvasPaint; em Rust, um &AnimatedValue em qualquer lugar onde entra um número): a plataforma então redesenha por conta própria enquanto eles animam, sem nenhum trabalho por frame no código do app.
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))import { Canvas } from "nsh";
import { Animated, createAnimatedValue } from "nsh";
const angle = createAnimatedValue(0);
Animated.timing(angle, { toValue: Math.PI * 2, duration: 1000 }).start();
<Canvas
style={{ width: 80, height: 80 }}
draw={(cx) => {
cx.translate(40, 40);
cx.rotate(angle); // animates natively
cx.fillStyle = "#cf222e";
cx.fillRect(-20, -4, 40, 8);
}}
/>;#O contexto
CanvasRenderingContext2D é um subconjunto do contexto do navegador, e Painter tem as mesmas operações como métodos em snake case (move_to, line_to, fill_rect, fill_text, stroke_color, …): a pilha de estado (save, restore), transformações, caminhos (moveTo, lineTo, arc, arcTo, ellipse, rect, roundRect, curvas de Bézier), fill / stroke / clip (também de um Path2D, que aceita dados de caminho SVG; fill_svg / stroke_svg em Rust), retângulos, texto (fillText, strokeText, measureText), gradientes lineares / radiais / cônicos, tracejados de linha, sombras, globalAlpha, globalCompositeOperation e drawImage (imagens por URL ou asset). Todos os membros estão listados na referência da Canvas API; o guia de canvas mostra passo a passo como desenhar gráficos e elementos animados.
As coordenadas são em pontos, com a origem no canto superior esquerdo. measureText é síncrono e retorna width. O Painter do Rust também responde a hit tests (is_point_in_path, is_point_in_stroke).
O contexto desenha operações vetoriais e imagens: acesso a pixels (getImageData / putImageData), filter e imagens SVG ficam fora dele.
#Eventos
Os eventos de view: onPress, onLayout, gestos e toques. Para tratar toques relativos ao desenho, leia locationX / locationY em onTouchEnd, ou acompanhe um arrasto com onResponderMove (.on_responder_move(..) em Rust, com e.location).
#Métodos de ref
Nenhum próprio.