Эта страница переведена машинным способом и может содержать ошибки. Читать оригинал на английском

API

Canvas

Canvas рисует с помощью контекста в стиле HTML Canvas 2D. Ваша функция draw записывает рисунок, а платформа воспроизводит его нативно (Core Graphics на iOS, android.graphics.Canvas на Android) в разрешении экрана. Числа и цвета в рисунке могут быть анимируемыми значениями: когда они анимируются, платформа перерисовывает сама, не запуская 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";

Руководство см. в разделе Canvas, а свойства компонента описаны в <Canvas>.

#Canvas

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

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

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

Представление, которое рисуется с помощью 2D-контекста. Принимает все свойства представлений (стиль, классы, обработчики касаний, доступность, ref), а также:

СвойствоТипОписание
draw(cx, { width, height }) => voidРисует canvas. Запускается, когда становится известен размер, снова при изменении сигналов, которые она читает, и снова при изменении размера canvas. Анимируемые значения, переданные как числа или цвета, анимируются без её запуска.

Задайте размер canvas через стиль (width / height, flex: 1, aspectRatio, …). Координаты задаются в пунктах, начало координат находится в левом верхнем углу.

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

#Анимация без перерисовки

Любой аргумент типа CanvasNumber и любая заливка (fillStyle, strokeStyle) принимает AnimatedNode. Тогда canvas читает узел во время рисования и перерисовывается нативно на каждом кадре его анимации:

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

Контекст, который получает draw. Вызовы только записывают операции; платформа рисует их после возврата из draw.

#Состояние и трансформации

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;
ЧленОписание
canvasРазмер canvas в пунктах.
save() / restore()Помещает в стек / извлекает из стека состояние рисования (трансформацию, обрезку, стили).
translate(x, y)Сдвигает начало координат.
scale(x, y)Масштабирует оси.
rotate(angle)Поворачивает оси; в радианах, по часовой стрелке.
transform(a, b, c, d, e, f)Умножает трансформацию на матрицу [a c e; b d f].
setTransform(a, b, c, d, e, f)Заменяет трансформацию.
resetTransform()Возвращает единичную трансформацию.

#Свойства

СвойствоТипОписание
fillStyleCanvasPaintЦвет, градиент или нативный узел цвета, используемый в fill, fillRect, fillText.
strokeStyleCanvasPaintТо же для обводки.
lineWidthCanvasNumberТолщина обводки в пунктах.
lineCap"butt" | "round" | "square"Как заканчиваются линии обводки.
lineJoin"miter" | "round" | "bevel"Как соединяются сегменты обводки.
miterLimitnumberОстрые соединения длиннее этого значения (в толщинах линии) срезаются.
lineDashOffsetCanvasNumberОткуда начинается штриховой узор.
globalAlphaCanvasNumberНепрозрачность того, что рисуется далее, от 0 до 1.
globalCompositeOperationstringКак то, что рисуется далее, сочетается с содержимым canvas ("source-over", "multiply", …).
shadowColorstringЦвет теней (transparent: без тени).
shadowBlurnumberРазмытие тени в пунктах.
shadowOffsetX, shadowOffsetYnumberСмещение тени в пунктах.
fontstringКраткая запись CSS font, например "600 14px Inter" (размеры в px, pt или rem).
textAlign"start" | "end" | "left" | "right" | "center"Положение текста по горизонтали относительно его x.
textBaseline"alphabetic" | "top" | "hanging" | "middle" | "ideographic" | "bottom"Положение текста по вертикали относительно его y.

Штрихи: setLineDash(segments: number[]) и getLineDash(): number[].

#Контуры

У контекста есть текущий контур, который строится теми же методами, что и у 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;

Все координаты — числа (CanvasNumber). Углы задаются в радианах, по часовой стрелке от оси x. Параметр radii у roundRect — это один радиус или от 2 до 4 радиусов, как в CSS border-radius (сначала левый верхний).

#Рисование

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;
МетодОписание
fill(path?, rule?)Заливает текущий контур (или path) с помощью fillStyle по правилу nonzero (по умолчанию) или even-odd.
stroke(path?)Обводит текущий контур (или path) с помощью strokeStyle.
clip(path?, rule?)Ограничивает область, в которой рисуется следующее, этим контуром.
fillRect, strokeRectПрямоугольник, не затрагивающий текущий контур.
clearRectДелает прямоугольную область прозрачной.

#Текст

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 работает синхронно: она измеряет текст с помощью текстового движка платформы и текущего font. Возвращается только width.

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

#Градиенты

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

#Изображения

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

Рисует изображение, заданное как URL или импортированный ресурс (import logo from "./logo.png"). Оно загружается нативно; canvas перерисовывается после завершения загрузки. Импортированный ресурс рисуется как сам импортированный файл; чтобы изображение было чётким на любом экране, импортируйте файл @3x. Три формы, как в HTML:

  • drawImage(image, dx, dy): в натуральном размере.
  • drawImage(image, dx, dy, dw, dh): с масштабированием в прямоугольник.
  • drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh): часть изображения (источник задаётся в пикселях изображения).

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

Переиспользуемый контур для заливки, обводки или обрезки. Конструктор копирует другой Path2D или нативно разбирает данные 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();

#Не поддерживается

Доступ к пикселям (getImageData, putImageData), filter, isPointInPath и SVG-изображения недоступны. Недопустимый цвет или шрифт отбрасывает только эту операцию, как в HTML Canvas.