تُرجمت هذه الصفحة آليًا وقد تحتوي على أخطاء. اقرأ الأصل الإنجليزي

API

Canvas

Canvas يرسم باستخدام سياق (context) من نمط 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> للاطلاع على خصائصه (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;
}

عرض (view) يُرسم باستخدام سياق ثنائي الأبعاد. يقبل كل خصائص العرض (الأنماط، والفئات، ومعالجات اللمس، وإمكانية الوصول، ref)، بالإضافة إلى:

الخاصيةالنوعالوصف
draw(cx, { width, height }) => voidترسم الـ canvas. تعمل بمجرد معرفة الحجم، وتعمل مرة أخرى عندما تتغير الإشارات (signals) التي تقرؤها، وعندما يتغير حجم الـ canvas. القيم المتحركة الممررة كأرقام أو ألوان تتحرك دون تشغيلها.

حدّد حجم الـ canvas من خلال نمطه (width / height، flex: 1، aspectRatio، …). الإحداثيات بالنقاط (points)، والأصل في أعلى اليسار.

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 العقدة (node) أثناء الرسم، ويعيد الرسم أصليًا في كل إطار من إطارات حركتها:

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الشيء نفسه للخطوط (strokes).
lineWidthCanvasNumberعرض الخط بالنقاط.
lineCap"butt" | "round" | "square"كيفية إنهاء الخطوط.
lineJoin"miter" | "round" | "bevel"كيفية وصل أجزاء الخط.
miterLimitnumberتُشطَّب وصلات miter الأطول من هذه القيمة (بوحدات عرض الخط).
lineDashOffsetCanvasNumberالنقطة التي يبدأ منها نمط الشرطات.
globalAlphaCanvasNumberشفافية ما سيُرسم لاحقًا، من 0 إلى 1.
globalCompositeOperationstringكيفية اندماج ما سيُرسم لاحقًا مع الـ canvas ("source-over"، "multiply"، …).
shadowColorstringلون الظلال (الشفاف: بلا ظل).
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
}

مسار قابل لإعادة الاستخدام للملء أو الرسم أو القص. يقوم المنشئ (constructor) بنسخ 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.