API
Canvas
Canvas يرسم باستخدام سياق (context) من نمط HTML Canvas 2D. تقوم الدالة draw لديك بتسجيل الرسم، ثم تعيد المنصة تشغيله أصليًا (Core Graphics على iOS، وandroid.graphics.Canvas على Android) بدقة الشاشة. يمكن أن تكون الأرقام والألوان في الرسم قيمًا متحركة: وعندما تتحرك، تعيد المنصة الرسم من تلقاء نفسها، دون تشغيل draw مرة أخرى.
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() | يعود إلى مصفوفة الوحدة. |
#الخصائص
| الخاصية | النوع | الوصف |
|---|---|---|
fillStyle | CanvasPaint | لون أو تدرج لوني أو عقدة لون أصلية تستخدمها fill وfillRect وfillText. |
strokeStyle | CanvasPaint | الشيء نفسه للخطوط (strokes). |
lineWidth | CanvasNumber | عرض الخط بالنقاط. |
lineCap | "butt" | "round" | "square" | كيفية إنهاء الخطوط. |
lineJoin | "miter" | "round" | "bevel" | كيفية وصل أجزاء الخط. |
miterLimit | number | تُشطَّب وصلات miter الأطول من هذه القيمة (بوحدات عرض الخط). |
lineDashOffset | CanvasNumber | النقطة التي يبدأ منها نمط الشرطات. |
globalAlpha | CanvasNumber | شفافية ما سيُرسم لاحقًا، من 0 إلى 1. |
globalCompositeOperation | string | كيفية اندماج ما سيُرسم لاحقًا مع الـ canvas ("source-over"، "multiply"، …). |
shadowColor | string | لون الظلال (الشفاف: بلا ظل). |
shadowBlur | number | تمويه الظل بالنقاط. |
shadowOffsetX، shadowOffsetY | number | إزاحة الظل بالنقاط. |
font | string | صيغة 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.