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

العناصر

Canvas

الـ canvas هو عرض ترسمه بنفسك باستخدام واجهة رسم ثنائية الأبعاد، هي واجهة HTML Canvas 2D API: مخططات، ومقاييس، وحلقات، وتوقيعات، وأشكال مخصصة. تسجّل شيفرة الرسم العمليات، وتعيد المنصة تشغيلها أصليًا (Core Graphics على iOS وMac Catalyst، وandroid.graphics.Canvas على Android) بدقة الشاشة. يُنشئ Rust واحدًا باستخدام canvas(painter)، وهو Painter يسجّل الرسم؛ أما TSX فيستخدم <Canvas> من nsh ودالته 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))
}

في TSX، ينشئ Canvas عرض canvas الأصلي، ويقيسه، ثم يشغّل الدالة draw مع سياق رسم وحجم الـ canvas (راجع Canvas API). وفي Rust، يقبل canvas(..) قيمة Painter أو closure تُرجعها: يعيد الـ closure الرسم عندما يتغير ما يقرؤه، ويأتي الحجم من on_layout عندما يعتمد الرسم عليه.

#الخصائص

يقبل الـ canvas خصائص العرض (الأنماط، وخصائص الضغط والإيماءات، وonLayout، وإمكانية الوصول، وref، …) بالإضافة إلى:

الخاصيةRustالنوعالوصف
drawcanvas(painter)(cx: CanvasRenderingContext2D, size: { width: number; height: number }) => voidيرسم الـ canvas. يعمل بمجرد معرفة الحجم، ويعمل مجددًا عند تغيّر حجم الـ canvas، وكذلك عند تغيّر الإشارات (signals) التي يقرؤها.

حدّد حجمًا للـ canvas عبر أنماطه (العرض / الارتفاع، وflex، ونسبة الأبعاد): حجمه هو الحجم الذي تحدده أنماطه.

#متى يُعاد الرسم

يُتتبَّع الرسم كما يُتتبَّع الـ effect: قراءة إشارة (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))

يمكن أن تكون الأرقام والألوان أيضًا قيمًا متحركة أصلية (CanvasNumber، CanvasPaint؛ وفي Rust تُستخدم &AnimatedValue في أي موضع يُوضع فيه رقم): عندها تعيد المنصة الرسم من تلقاء نفسها أثناء حركتها، دون أي عمل في شيفرة التطبيق لكل إطار.

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

#السياق

CanvasRenderingContext2D مجموعة فرعية من سياق المتصفح، ولدى Painter العمليات نفسها كدوال بصيغة snake case (move_to، line_to، fill_rect، fill_text، stroke_color، …): مكدّس الحالة (save، restore)، والتحويلات، والمسارات (moveTo، lineTo، arc، arcTo، ellipse، rect، roundRect، ومنحنيات Bézier)، وfill / stroke / clip (ولـ Path2D أيضًا، الذي يقبل بيانات مسار SVG؛ وfill_svg / stroke_svg في Rust)، والمستطيلات، والنص (fillText، strokeText، measureText)، والتدرجات الخطية / الشعاعية / المخروطية، وأنماط الخطوط المتقطعة، والظلال، وglobalAlpha، وglobalCompositeOperation وdrawImage (الصور عبر URL أو asset). كل عضو مدرج في مرجع Canvas API، ويشرح دليل canvas رسم المخططات والرسومات المتحركة خطوة بخطوة.

الإحداثيات بالنقاط (points) والأصل في أعلى اليسار. measureText متزامنة وتُرجع width. أما Painter في Rust فيجيب أيضًا عن اختبارات الإصابة (hit tests) (is_point_in_path، is_point_in_stroke).

يرسم السياق العمليات المتجهية والصور: الوصول إلى البكسلات (getImageData / putImageData) وfilter وصور SVG خارج نطاقه.

#الأحداث

أحداث العرض: onPress، onLayout، والإيماءات واللمسات. للتعامل مع النقرات بالنسبة إلى الرسم، اقرأ locationX / locationY في onTouchEnd، أو تتبّع السحب باستخدام onResponderMove (.on_responder_move(..) في Rust، مع e.location).

#دوال Ref

لا توجد دوال خاصة به.