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

التوثيق

Canvas

الـ canvas هو عنصر عرض ترسم فيه باستخدام عمليات HTML Canvas 2D API: المخططات البيانية، وحلقات التقدم، والمقاييس، والتوقيعات، والأيقونات المخصصة، وأي شيء يكون رسمه أسهل من بنائه من عناصر العرض. في Rust يعرض canvas(..) ما سجّله Painter؛ وفي TSX يأخذ Canvas من nsh دالة draw تسجّل الرسم دفعة واحدة. تعيد المنصة تشغيل التسجيل محليًا (Core Graphics على iOS، وandroid.graphics.Canvas على Android) بدقة الشاشة، فتبقى الرسومات حادة عند أي كثافة بكسلات.

guide-canvas.tsx
guide-canvas.tsxLive
import { Canvas, type GestureResponderEvent, mount, Path2D, signal, Text, View } from "nsh";
import { createAnimatedValue, Easing } from "nsh";
import { button, buttonText, Inspector, Panel, tint, ui, wide } from "./palette";

const TAU = Math.PI * 2;
const heart = new Path2D("M12 21s-7-4.35-9.5-8.5C.5 8.5 3 4 7 4c2 0 3.5 1 5 3 1.5-2 3-3 5-3 4 0 6.5 4.5 4.5 8.5C19 16.65 12 21 12 21z");

function Button(props: { label: string; onPress: () => void }) {
  return (
    <View onPress={props.onPress} {...button()}>
      <Text style={buttonText()}>{props.label}</Text>
    </View>
  );
}

function App() {
  // The ring's sweep is an animated value: animating it redraws without running `draw`.
  const progress = createAnimatedValue(0.35);
  const target = signal(0.35);
  const go = () => {
    const to = Math.round(Math.random() * 100) / 100;
    target.set(to);
    progress.timing(to, { duration: 800, easing: Easing.inOut(Easing.cubic) });
  };

  // The chart reads a signal: changing it runs `draw` again.
  const data = signal([12, 30, 22, 48, 36, 60, 52, 75]);

  // Finger drawing through the responder system.
  const strokes = signal<[number, number][][]>([]);
  const point = (e: GestureResponderEvent): [number, number] => [e.locationX, e.locationY];

  const ring = () => (
    <Canvas
      style={{ width: 110, height: 110 }}
      draw={(cx, { width, height }) => {
        const centerX = width / 2;
        const cy = height / 2;
        const r = width / 2 - 8;
        cx.lineWidth = 10;
        cx.lineCap = "round";
        cx.strokeStyle = ui().border;
        cx.beginPath();
        cx.arc(centerX, cy, r, 0, TAU);
        cx.stroke();
        const g = cx.createConicGradient(-Math.PI / 2, centerX, cy);
        g.addColorStop(0, ui().up);
        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);
      }}
    />
  );
  const heartCanvas = () => (
    <Canvas
      style={{ width: 48, height: 48 }}
      draw={(cx) => {
        cx.scale(2, 2);
        cx.shadowColor = tint(ui().down, 0.45);
        cx.shadowBlur = 4;
        cx.fillStyle = ui().down;
        cx.fill(heart);
      }}
    />
  );
  const chart = (height: number) => (
    <Canvas
      style={{ ...ui().inset, height, borderRadius: 12 }}
      draw={(cx, { width, height }) => {
        const values = data();
        const pad = 16;
        const x = (i: number) => pad + (i / (values.length - 1)) * (width - 2 * pad);
        const y = (v: number) => height - pad - (v / 100) * (height - 2 * pad);
        const area = cx.createLinearGradient(0, pad, 0, height - pad);
        area.addColorStop(0, tint(ui().accent, 0.28));
        area.addColorStop(1, tint(ui().accent, 0));
        cx.beginPath();
        values.forEach((v, i) => (i === 0 ? cx.moveTo(x(i), y(v)) : cx.lineTo(x(i), y(v))));
        cx.lineTo(x(values.length - 1), height - pad);
        cx.lineTo(x(0), height - pad);
        cx.closePath();
        cx.fillStyle = area;
        cx.fill();
        cx.beginPath();
        values.forEach((v, i) => (i === 0 ? cx.moveTo(x(i), y(v)) : cx.lineTo(x(i), y(v))));
        cx.strokeStyle = ui().accent;
        cx.lineWidth = 2.5;
        cx.lineJoin = "round";
        cx.stroke();
      }}
    />
  );
  const newData = () => <Button label="New data" onPress={() => data.set(data().map(() => Math.round(10 + Math.random() * 85)))} />;
  const pad = () => (
    <Canvas
      style={{ ...ui().inset, flex: 1, borderRadius: 12 }}
      onStartShouldSetResponder={() => true}
      onResponderGrant={(e) => {
        strokes.set([...strokes(), [point(e)]]);
        return true;
      }}
      onResponderMove={(e) => {
        const all = strokes();
        strokes.set([...all.slice(0, -1), [...all[all.length - 1], point(e)]]);
      }}
      draw={(cx, { width }) => {
        cx.fillStyle = ui().faint;
        cx.font = "13px system-ui";
        cx.textAlign = "center";
        cx.fillText("Draw here", width / 2, 20);
        cx.strokeStyle = ui().text;
        cx.lineWidth = 3;
        cx.lineCap = "round";
        cx.lineJoin = "round";
        for (const s of strokes()) {
          cx.beginPath();
          s.forEach(([px, py], i) => (i === 0 ? cx.moveTo(px, py) : cx.lineTo(px, py)));
          cx.stroke();
        }
      }}
    />
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 14 }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <View style={{ flex: 1, minWidth: 0, gap: 14 }}>
              {chart(200)}
              {pad()}
            </View>
            <Inspector>
              <Panel title="An animated value in draw">
                <View style={{ gap: 10 }}>
                  <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
                    {ring()}
                    {heartCanvas()}
                  </View>
                  <Button label="Animate the ring" onPress={go} />
                </View>
              </Panel>
              <Panel title="A signal in draw">
                <View style={{ gap: 10 }}>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>[{data().join(", ")}]</Text>
                  {newData()}
                </View>
              </Panel>
              <Panel title="Responder events">
                <View style={{ gap: 10 }}>
                  <Text style={{ fontSize: 13, color: ui().text, fontVariant: ["tabular-nums"] }}>
                    {strokes().length} strokes · {strokes().reduce((n, s) => n + s.length, 0)} points
                  </Text>
                  <Button label="Clear" onPress={() => strokes.set([])} />
                </View>
              </Panel>
            </Inspector>
          </View>
        ) : (
          <>
            <View style={{ flexDirection: "row", alignItems: "center", gap: 16 }}>
              {ring()}
              <View style={{ gap: 10 }}>
                <Button label="Animate the ring" onPress={go} />
                {heartCanvas()}
              </View>
            </View>
            {chart(150)}
            {newData()}
            {pad()}
          </>
        )
      }
    </View>
  );
}

mount(App);
use std::f32::consts::TAU;

use nsh_core::prelude::*;

#[component]
pub fn App() -> impl IntoElement {
    let area = signal((0.0_f32, 0.0_f32));
    view().flex_1().p_6().pt(px(60.0)).child(
        canvas(move || {
            let (w, h) = area.get();
            Painter::new()
                .fill_color(rgb(0x0969da))
                .begin_path()
                .arc(w / 2.0, h / 2.0, 60.0, 0.0, TAU)
                .fill()
                .stroke_color(rgb(0x1f2328))
                .line_width(4.0)
                .stroke_rect(bounds(point(px(20.0), px(20.0)), size(px(w - 40.0), px(h - 40.0))))
        })
        .h(px(200.0))
        .rounded(px(12.0))
        .bg(white())
        .on_layout(move |frame: &Bounds<Points>| area.set((frame.size.width.0, frame.size.height.0))),
    )
}

يقبل الـ canvas كل ما يقبله العرض (الأنماط، والفئات، ومعالجات اللمس، وإمكانية الوصول، والمراجع) إضافةً إلى الرسم الخاص به. ويحتاج إلى حجم يأتي من نمطه أو من التخطيط؛ ومحتواه هو الرسم. الإحداثيات بالنقاط (points)، والأصل في أعلى اليسار.

#متى يُنفَّذ الرسم

في Rust يسجّل canvas(move || ..) داخل ربط (binding): يعمل الإغلاق (closure) مرة واحدة، ثم يعمل مجددًا عندما تتغير إشارة قرأها. الرسم الذي يعتمد على حجم الـ canvas يحتفظ بالحجم القادم من .on_layout(..) في إشارة ويقرؤه، كما في المثال أعلاه. وفي TSX يعمل draw(cx, { width, height }):

  • بمجرد أن يصبح حجم الـ canvas معروفًا (بعد أول تخطيط له)؛
  • مجددًا عند تغيير حجم الـ canvas؛
  • مجددًا عندما تتغير إشارة قرأها.

كل تشغيل يستبدل الرسم كاملًا. وقراءة الإشارات أثناء الرسم هي الطريقة التي يتبع بها الـ canvas بياناتك:

let values = signal(vec![12.0_f32, 30.0, 22.0, 48.0, 36.0, 60.0]);
let width = signal(0.0_f32);
let height = 160.0;

canvas(move || {
    let w = width.get();
    values.with(|v| {
        let x = |i: usize| i as f32 / (v.len() - 1) as f32 * w;
        let y = |n: f32| height - n / 100.0 * height;
        let mut p = Painter::new().begin_path().move_to(x(0), y(v[0]));
        for (i, &n) in v.iter().enumerate().skip(1) {
            p = p.line_to(x(i), y(n));
        }
        p.stroke_color(rgb(0x3b82f6)).line_width(2.5).line_join(LineJoin::Round).stroke()
    })
})
.h(px(height))
.on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))

استدعاءات الرسم تسجّل فقط: الرسم تنفّذه المنصة بعد انتهاء التسجيل، لذلك لا تُقرأ البكسلات مرة أخرى. ويُرسل التسجيل إلى المنصة كعملية واحدة.

#عمليات 2D

العمليات هي مجموعة فرعية من HTML Canvas 2D API: CanvasRenderingContext2D في TSX، وطرق Painter في Rust بأسماء Rust (begin_path، fill_rect، line_width، set_transform، draw_image، …؛ والألوان عبر fill_color / stroke_color، والتدرجات عبر fill_style / stroke_style). المدعوم هو:

الحالة

العضوRustالوصف
save()، restore().save()، .restore()حفظ حالة الرسم (الأنماط، والتحويل، والقص) واستعادتها.
fillStyle، strokeStyle.fill_color(..)، .fill_style(..)، .stroke_color(..)، .stroke_style(..)لون CSS، أو تدرج، أو قيمة متحركة تقوم بالاستيفاء بين الألوان.
lineWidth، lineCap، lineJoin، miterLimit.line_width(..)، .line_cap(..)، .line_join(..)، .miter_limit(..)شكل الخط.
setLineDash(segments)، getLineDash()، lineDashOffset.line_dash(..)، .line_dash_offset(..)الخطوط المتقطعة.
globalAlpha.global_alpha(..)شفافية ما يُرسم لاحقًا، من 0 إلى 1.
globalCompositeOperation.composite(Composite::DestinationOut)طريقة الدمج: "source-over"، "destination-out"، "multiply"، "screen"، …
shadowColor، shadowBlur، shadowOffsetX، shadowOffsetY.shadow(color, blur, offset)الظلال.
font.font(..)الصيغة المختصرة font في CSS: "600 14px Inter" (الأحجام بوحدات px أو pt أو rem)؛ ويمكن أن يأتي small-caps وصيغ الأرقام tabular-nums، oldstyle-nums، … قبل الحجم.
fontVariantNumeric.font_variant_numeric(..)"tabular-nums" يجعل كل الأرقام بالعرض نفسه، فلا تهتز الأرقام المتغيرة (العدادات، والأسعار، وتسميات المحاور)؛ وكذلك "proportional-nums"، "oldstyle-nums"، "lining-nums"، "normal". إضافة من NSH (يقابل font-variant-numeric في CSS)؛ وضبط font يُبقي عليه. تتجاهله المتصفحات.
textAlign، textBaseline.text_align(..)، .text_baseline(..)موضع النص.

التحويلات: translate(x, y)، scale(x, y)، rotate(angle) (بالراديان، باتجاه عقارب الساعة)، transform(a, b, c, d, e, f)، setTransform(a, b, c, d, e, f)، resetTransform().

المسارات: beginPath()، moveTo، lineTo، quadraticCurveTo، bezierCurveTo، arc، arcTo، ellipse، rect، roundRect(x, y, w, h, radii)، closePath()؛ ثم fill(rule?)، stroke() وclip(rule?)، وكلٌّ منها يقبل أيضًا مسارًا (fill_path، stroke_path، clip_path في Rust). قواعد التعبئة هي "nonzero" و"evenodd".

المستطيلات: fillRect، strokeRect، clearRect.

النص: fillText(text, x, y, maxWidth?)، strokeText(...)، measureText(text) (يقيسها محرك النص في المنصة بشكل متزامن باستخدام font الحالي؛ فقط width). في Rust يعطي measure_text(font, text) العرض.

اختبار الإصابة (Hit testing): isPointInPath(path?, x, y, rule?) وisPointInStroke(path?, x, y) (path_contains، stroke_contains، is_point_in_path وis_point_in_stroke على Painter) تجيب بشكل متزامن، وتُحسب من الهندسة المسجلة (التحويل الحالي، والمسار، وlineWidth وlineCap). احتفظ بالرسم وبالمسارات التي رسمتها لتجد الشكل الواقع تحت نقرة:

let slices: Rc<Vec<Path>> = Rc::new(vec![svg_path("M50 50 L100 50 A50 50 0 0 1 50 100 Z") /* , ... */]);
let draw = {
    let slices = slices.clone();
    move || slices.iter().fold(Painter::new(), |p, s| p.fill_path(s, FillRule::NonZero))
};

canvas(draw.clone())
    .size(px(100.0))
    .claim_touches(TouchClaim::on_start())
    .on_responder_release(move |e: &ResponderEvent| {
        let drawing = draw();
        if let Some(hit) = slices.iter().position(|s| drawing.path_contains(s, e.location, FillRule::NonZero)) {
            selected.set(Some(hit));
        }
    })

تُختبر الخطوط بوصلات مستديرة ودون تقطيع؛ والإحداثيات المعطاة كقيم متحركة تُحتسب 0.

التدرجات: createLinearGradient، createRadialGradient، createConicGradient، وكلٌّ منها مع addColorStop(offset, color) (CanvasGradient::linear، radial، conic وadd_color_stop في Rust).

الصور: drawImage(image, dx, dy)، drawImage(image, dx, dy, dw, dh)، drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh) (draw_image(url, dest) وdraw_image_part(..) في Rust).

cx.canvas.width وcx.canvas.height يعطيان حجم الـ canvas بالنقاط في TSX.

المجموعة المدعومة: عمليات الرسم. تكتب المنصة البكسلات، لذلك فإن getImageData / putImageData وأي وصول آخر إلى البكسلات خارجها، وكذلك filter وصور SVG في drawImage. اللون أو الخط غير الصالح يُسقط تلك العملية وحدها، كما في HTML Canvas.

#المسارات وبيانات مسار SVG

يمكن ملء مسار بُني مرة واحدة أو رسم حدوده أو استخدامه للقص مرات عديدة: svg_path(d) في Rust، وPath2D في TSX. كلاهما يقبل بيانات مسار SVG ويحللها محليًا، مما يسهّل التعامل مع الأيقونات:

use nsh_core::canvas::{FillRule, svg_path};

const HEART: &str = "M12 21s-7-4.35-9.5-8.5C.5 8.5 3 4 7 4c2 0 3.5 1 5 3 1.5-2 3-3 5-3 4 0 6.5 4.5 4.5 8.5C19 16.65 12 21 12 21z";

fn heart(size: f32, color: Rgba) -> impl IntoElement {
    let path = svg_path(HEART);
    canvas(
        Painter::new()
            .scale(size / 24.0, size / 24.0) // the path is drawn on a 24-point grid
            .fill_color(color)
            .fill_path(&path, FillRule::NonZero),
    )
    .size(px(size))
}

يملك Path2D طرق المسار نفسها الموجودة في السياق (moveTo، arc، roundRect، …) إضافةً إلى addPath(other). ويقوم new Path2D(other) بنسخ مسار.

#التدرجات

canvas(move || {
    let (w, h) = (width.get(), 120.0);
    let linear = CanvasGradient::linear(point(px(0.0), px(0.0)), point(px(w), px(0.0)))
        .add_color_stop(percentage(0.0), rgb(0x22d3ee))
        .add_color_stop(percentage(1.0), rgb(0x6366f1));
    let center = point(px(w / 2.0), px(h * 0.75));
    let radial = CanvasGradient::radial(center, px(4.0), center, px(40.0))
        .add_color_stop(percentage(0.0), rgb(0xfde68a))
        .add_color_stop(percentage(1.0), rgb(0xf97316));
    Painter::new()
        .fill_style(linear)
        .fill_rect(0.0, 0.0, w, h / 2.0)
        .fill_style(radial)
        .begin_path()
        .arc(w / 2.0, h * 0.75, 28.0, 0.0, TAU)
        .fill()
})
.h(px(120.0))
.on_layout(move |frame: &Bounds<Points>| width.set(frame.size.width.0))

التدرج المخروطي (createConicGradient(startAngle, x, y)، CanvasGradient::conic(start_angle, center)) يمرّر الألوان حول نقطة؛ وزاوية بدايته بالراديان، باتجاه عقارب الساعة من المحور x. والتدرج المخروطي على قوس مرسوم الحدود ينتج حلقة تقدم متدرجة.

#النص

Painter::new()
    .font("600 20px system-ui")
    .fill_color(rgb(0x1f2328))
    .text_align(CanvasTextAlign::Center)
    .text_baseline(CanvasTextBaseline::Middle)
    .fill_text("72%", width / 2.0, height / 2.0)
    .font("11px system-ui")
    .fill_text("100", 32.0 - 6.0 - measure_text("11px system-ui", "100").0, 16.0)

عائلات الخطوط هي نفسها المستخدمة في الأنماط: عائلات النظام وأسماء الخطوط التي يشحنها تطبيقك (انظر الخطوط).

#الصور

يأخذ رسم الصورة عنوان URL في Rust (وعنوان أصل asset هو LOGO.url)، وعنوان URL أو أصلًا مستوردًا (import logo from "./logo.png") في TSX. تُحمَّل الصورة محليًا، ويعيد الـ canvas الرسم بعد اكتمال التحميل، فلا تحتاج إلى تتبع التحميل بنفسك. تخضع عناوين URL البعيدة لـإمكانات الشبكة الخاصة بالتطبيق.

تعرض الـ canvas على iOS وmacOS وAndroid وWindows وLinux وGPUI الصور النقطية (PNG وJPEG وGIF وWebP، …)؛ وعليها يُعدّ مصدر SVG صورة يفشل تحميلها، ويذكر السجل اسمه. اعرض صور SVG باستخدام <Image>، أو ارسم أشكالها كمسارات Path2D. أما على الويب فيرسم الـ canvas ما يرسمه المتصفح، بما في ذلك SVG.

const LOGO: ImageAsset = asset!("logo.png");

canvas(
    Painter::new()
        .save()
        .begin_path()
        .arc(60.0, 60.0, 56.0, 0.0, TAU)
        .clip() // a round avatar
        .draw_image(LOGO.url, bounds(point(px(4.0), px(4.0)), size(px(112.0), px(112.0))))
        .restore(),
)
.size(px(120.0))

الصيغة ذات الوسائط التسعة (draw_image_part) تأخذ المستطيل المصدر بوحدات بكسلات الصورة.

#التحكم بالرسومات عبر القيم المتحركة

يمكن أن يكون أي وسيط رقمي في استدعاء رسم، وكذلك طلاءات التعبئة والخط، قيمة متحركة أو عقدة مشتقة منها. تُقرأ هذه الوسائط أثناء رسم المنصة، وعندما تتحرك القيمة تعيد المنصة الرسم من تلقاء نفسها، في كل إطار، دون تسجيل الرسم مجددًا ودون تشغيل أي من شيفرتك.

use std::f32::consts::{FRAC_PI_2, TAU};

use nsh_core::{
    canvas::{CanvasTextAlign, CanvasTextBaseline, LineCap},
    prelude::*,
};

pub fn ring() -> impl IntoElement {
    let progress = animated_value(0.3);
    let label = signal(30_u32);
    let go = move |to: f32| {
        label.set((to * 100.0).round() as u32);
        progress.timing(to, 900.0, Easing::Curve(Curve::Cubic, EaseMode::InOut));
    };
    let (center_x, cy, r) = (70.0, 70.0, 60.0);
    view()
        .flex_1()
        .items_center()
        .justify_center()
        .gap_4()
        .child(
            canvas(move || {
                // The end angle reads the animated value on every frame.
                let end = progress.mul(TAU).sub(FRAC_PI_2);
                Painter::new()
                    .line_width(12.0)
                    .line_cap(LineCap::Round)
                    .stroke_color(rgb(0xe5e7eb))
                    .begin_path()
                    .arc(center_x, cy, r, 0.0, TAU)
                    .stroke()
                    .stroke_color(rgb(0x6366f1))
                    .begin_path()
                    .arc(center_x, cy, r, -FRAC_PI_2, &end)
                    .stroke()
                    .fill_color(rgb(0x1f2328))
                    .font("600 22px system-ui")
                    .text_align(CanvasTextAlign::Center)
                    .text_baseline(CanvasTextBaseline::Middle)
                    .fill_text(&format!("{}%", label.get()), center_x, cy)
            })
            .size(px(140.0)),
        )
        .child(
            text("Animate")
                .text_color(rgb(0x0969da))
                .on_press(move || go(if label.get_untracked() < 50 { 0.85 } else { 0.2 })),
        )
}

زاوية نهاية القوس هي progress.mul(2π).sub(π/2): عقدة، لذلك تمسح الحلقة محليًا لمدة 900 مللي ثانية بينما سُجّل الرسم مرة واحدة (ومرة أخرى للتسمية التي تقرأ إشارة). ويعمل عقرب الثواني في الساعة بالطريقة نفسها: rotate(seconds.mul(π / 30)) مع seconds متحركة خطيًا.

تتحرك الألوان بالطريقة نفسها: .fill_style(&t.interpolate_colors(&[0.0, 1.0], &[rgb(0x54aeff), rgb(0xcf222e)])) في Rust، وcx.fillStyle = t.interpolate({ inputRange: [0, 1], outputRange: ["#54aeff", "#cf222e"] }) في TSX.

#اللمس على canvas

الـ canvas عنصر عرض، لذلك يقبل الضغطات ونظام المستجيبات (responder system). الرسم بالإصبع يسجّل النقاط في إشارة ويرسمها:

let strokes = signal(Vec::<Vec<(f32, f32)>>::new());

canvas(move || {
    strokes.with(|strokes| {
        let mut p = Painter::new().line_width(3.0).line_cap(LineCap::Round).line_join(LineJoin::Round);
        for stroke in strokes {
            p = p.begin_path();
            for (i, &(x, y)) in stroke.iter().enumerate() {
                p = if i == 0 { p.move_to(x, y) } else { p.line_to(x, y) };
            }
            p = p.stroke();
        }
        p
    })
})
.h(px(240.0))
.claim_touches(TouchClaim::on_start())
.keep_touches(true) // a scroll view around must not take the touches
.on_responder_grant(move |e: &ResponderEvent| {
    strokes.update(|s| s.push(vec![(e.location.x.0, e.location.y.0)]))
})
.on_responder_move(move |e: &ResponderEvent| {
    strokes.update(|s| {
        if let Some(stroke) = s.last_mut() {
            stroke.push((e.location.x.0, e.location.y.0));
        }
    })
})

#الأداء

  • يُسجَّل الرسم في Rust (وفي TSX كاستدعاءات JS على السياق) ويصل إلى المنصة كعملية واحدة؛ فلا يوجد عبور للجسر (bridge) لكل استدعاء.
  • كل رسم يعيد تسجيل كل شيء. اجعل العمل متناسبًا مع ما يُعرض، وأبقِ البيانات التي تتغير في كل إطار في قيم متحركة بدل الإشارات.
  • قسّم الرسم الذي فيه جزء ثابت وجزء يتغير كثيرًا إلى canvas ين متراكبين فوق بعضهما، بحيث يُعاد رسم أحدهما فقط.
  • داخل عرض تمرير يدعم التكبير، يُعاد رسم الـ canvas بدقة التكبير عند انتهاء التكبير.

#مراجع ذات صلة