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

التوثيق

الرسوم المتحركة

تعمل كل رسوم متحركة في NSH على خيط واجهة المستخدم. تصفها مرة واحدة، من Rust أو TSX، ثم تشغّلها المنصة إطارًا تلو آخر (Core Animation وdisplay link على iOS، ومحرّك الرسوم على خيط واجهة المستخدم على Android) دون استدعاء شيفرتك. لذلك لا يمكن لانشغال خيط المحرّك أن يجعل الرسوم تتقطع، ولا تكلّف الرسوم المتحركة شيفرتك شيئًا أثناء تشغيلها.

guide-animation.tsx
guide-animation.tsxLive
import { mount, signal, Text, View } from "nsh";
import { reduceMotion } from "nsh";
import { Animated, createAnimatedValue, Easing, LayoutAnimation } from "nsh";
import { button, buttonText, desktop, focusRing, Inspector, Panel, size, Stage, ui, wide } from "./palette";

const TRACK = 250;
// Read when they render, so they follow the theme.
const palette = () => [ui().down, ui().warn, ui().accent, ui().up, ui().info];
const SWATCHES = 5;

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

function App() {
  // 1. A style transition: change the style, the platform animates it.
  const on = signal(false);

  // 2. Animated values: timing and spring, driven on the UI thread.
  const timed = createAnimatedValue(0);
  const sprung = createAnimatedValue(0);
  let there = false;
  const run = () => {
    there = !there;
    timed.timing(there ? TRACK : 0, { duration: 700, easing: Easing.inOut(Easing.cubic) });
    sprung.spring(there ? TRACK : 0, { stiffness: 180, damping: 9 });
  };

  // 3. A composite: a staggered hop, then all of them spin; runs natively end to end.
  const ys = Array.from({ length: SWATCHES }, () => createAnimatedValue(0));
  const spin = createAnimatedValue(0);
  const status = signal("idle");
  const show = Animated.sequence([
    Animated.stagger(
      70,
      ys.map((y) =>
        Animated.sequence([
          Animated.timing(y, { toValue: -30, duration: 200, easing: Easing.out(Easing.quad) }),
          Animated.spring(y, { toValue: 0, stiffness: 300, damping: 12 }),
        ]),
      ),
    ),
    Animated.timing(spin, { toValue: 1, duration: 600, easing: Easing.inOut(Easing.cubic) }),
  ]);
  const play = () => {
    status.set("running…");
    show.reset();
    show.start(({ finished }) => status.set(finished ? "finished" : "stopped"));
  };

  // 4. Layout animation: the next commit's frame changes animate.
  const stacked = signal(false);

  const heading = (title: string) => <Text style={{ fontWeight: "600", color: ui().text }}>{title}</Text>;
  const transition = () => (
      <View style={{ flexDirection: "row", alignItems: "center", gap: 12 }}>
        <View
          onPress={() => on.set(!on())}
          style={{
            width: 64,
            height: 64,
            borderRadius: on() ? 32 : 12,
            backgroundColor: on() ? ui().info : ui().accent,
            opacity: on() ? 0.6 : 1,
            rotate: on() ? "45deg" : "0deg",
            transitionProperty: "all",
            transitionDuration: reduceMotion() ? 0 : 400,
            transitionTimingFunction: "ease-in-out",
          }}
          hoverStyle={{ opacity: 0.88 }}
          focusStyle={focusRing()}
          activeStyle={{ opacity: 0.7 }}
        />
        <View class="size-10 rounded-full border-4 animate-spin motion-reduce:animate-none" style={{ borderColor: ui().border, borderTopColor: ui().accent }} />
        <View class="size-10 rounded-full animate-pulse motion-reduce:animate-none" style={{ backgroundColor: ui().up }} />
        <Text style={{ flex: 1, color: ui().muted, fontSize: 13 }}>
          {desktop ? "Click the square." : "Tap the square."} animate-spin / animate-pulse run natively.
        </Text>
      </View>
  );
  const tracks = () => (
      <>
        <View style={{ width: 20, height: 20, borderRadius: 10, backgroundColor: ui().accent, translate: [timed, 0] }} />
        <View style={{ width: 20, height: 20, borderRadius: 4, backgroundColor: ui().info, translate: [sprung, 0] }} />
        <View
          style={{
            height: 8,
            borderRadius: 4,
            // One value, interpolated into a color and a width fraction.
            backgroundColor: timed.interpolate({ inputRange: [0, TRACK], outputRange: [ui().border, ui().up] }),
            scale: [timed.interpolate({ inputRange: [0, TRACK], outputRange: [0.1, 1] }), 1],
            transformOrigin: "left",
            width: wide() ? TRACK + 20 : undefined,
          }}
        />
      </>
  );
  const move = () => <Chip label="Move" onPress={run} />;
  const swatches = () => (
      <View style={{ flexDirection: "row", justifyContent: "center", alignItems: "flex-end", gap: 10, height: 64 }}>
        <Each of={palette()}>
          {(color, i) => (
            <View
              style={{
                width: 32,
                height: 32,
                borderRadius: 8,
                backgroundColor: color(),
                translate: [0, ys[i()]],
                rotate: spin.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "180deg"] }),
              }}
            />
          )}
        </Each>
      </View>
  );
  const playback = () => (
      <View style={{ flexDirection: "row", alignItems: "center", gap: 8 }}>
        <Chip label="Play" onPress={play} />
        <Chip label="Stop" onPress={() => show.stop()} />
        <Text style={{ color: ui().muted, fontSize: desktop ? 13 : undefined }}>{status()}</Text>
      </View>
  );
  const bars = () => (
      <View style={{ flexDirection: stacked() ? "column" : "row", gap: 8 }}>
        <Each of={palette()}>
          {(color) => <View style={{ height: stacked() ? 18 : 40, flex: stacked() ? undefined : 1, borderRadius: 6, backgroundColor: color() }} />}
        </Each>
      </View>
  );
  const stack = () => (
      <Chip
        label={stacked() ? "Back to a row" : "Stack them"}
        onPress={() => {
          LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
          stacked.set(!stacked());
        }}
      />
  );

  return (
    <View style={{ flex: 1, backgroundColor: ui().page, padding: 16, paddingTop: 24, gap: 14 }}>
      {() =>
        wide() ? (
          <View style={{ flex: 1, flexDirection: "row", gap: 16 }}>
            <Stage width={520} gap={14}>
              {heading("Transition (style changes animate)")}
              {transition()}
              {heading("timing and spring")}
              <View style={{ gap: 8 }}>{tracks()}</View>
              {heading("Animated.sequence / stagger")}
              {swatches()}
              {heading("LayoutAnimation")}
              {bars()}
            </Stage>
            <Inspector>
              <Panel title="timing · spring">
                <View style={{ paddingTop: 6 }}>{move()}</View>
              </Panel>
              <Panel title="Animated.sequence · stagger">
                <View style={{ paddingTop: 6 }}>{playback()}</View>
              </Panel>
              <Panel title="LayoutAnimation.configureNext">
                <View style={{ paddingTop: 6 }}>{stack()}</View>
              </Panel>
            </Inspector>
          </View>
        ) : (
          <>
            {heading("Transition (style changes animate)")}
            {transition()}
            {heading("timing and spring")}
            <View style={{ gap: 8 }}>
              {tracks()}
              {move()}
            </View>
            {heading("Animated.sequence / stagger")}
            {swatches()}
            {playback()}
            {heading("LayoutAnimation")}
            {bars()}
            {stack()}
          </>
        )
      }
    </View>
  );
}

mount(App);

هناك ست أدوات، من الأسهل استخدامًا إلى الأكثر مرونة:

الأداةمتى تستخدمها
الحركة (Motion)عرض (view) يدخل أو يخرج أو يلفت الانتباه أو يتكرر: حركات جاهزة (fadeInUp، shake، pulse، …) أو إطارات مفتاحية من تصميمك، على opacity و transform.
الانتقالات (Transitions)تغيير في النمط ينبغي أن يتحرك بدلًا من أن يقفز: لون، أو شفافية، أو تحويل.
الرسوم المتحركة بالإطارات المفتاحية المدمجةمؤشر دوران، أو عنصر نائب ينبض، أو تلميح يقفز.
الإطارات المفتاحية (Keyframes)أي قيمة متحركة عبر إطارات مفتاحية، كما تُشغَّل في CSS: إزاحات، ومنحنى تسارع لكل مقطع، وتكرارات، واتجاه.
القيم المتحركةكل ما تحرّكه قيمة عبر الزمن: النوابض، والإيماءات، وموضع التمرير، والتسلسلات.
رسوم التخطيط المتحركةتغييرات الحجم والموضع بعد التحديث: عناصر تُدرج أو تُزال أو يُعاد ترتيبها، ولوحة تتمدد.

#الحركة (Motion)

تحرّك الحركة opacity و transform للعرض عبر إطارات مفتاحية، بشكل أصلي. أنشئ واحدة في المكوّن الذي يملك العرض (motion() في Rust، وcreateMotion() من nsh في TSX)، واربطها بالعرض، ثم شغّل عليها حركة جاهزة أو حركة من تصميمك. يبدأ enter العرض من النقطة التي تبدأ منها الحركة ويشغّلها فورًا، فيدخل العرض من إطاره الأول.

#[component]
pub fn App() -> impl IntoElement {
    let card = motion().entering(MotionPreset::FadeInUp);
    view().flex_1().items_center().justify_center().child(
        view()
            .size(px(120.0))
            .rounded(px(16.0))
            .bg(rgb(0x0969da))
            .bind_motion(&card)
            .on_press(move || {
                card.play(MotionPreset::Shake);
            }),
    )
}

للحركة قيمة متحركة لكل قناة: opacity (من 0 إلى 1)، translateX وtranslateY (بالنقاط)، rotate وrotateX وrotateY (بالدرجات)، scale وscaleX وscaleY. تبدأ في حالة السكون (opacity والمقاييس تساوي 1، والباقي 0) وتُطبَّق بهذا الترتيب: الإزاحات، ثم الدورانات، ثم المقاييس، حول نقطة أصل التحويل (transform origin) للعرض. ليس للحركة مظهر خاص بها: إنها تحرّك ما صمّمته.

تحرّك الحركة opacity و transform فقط. يضبط نشر m.style قيمتي opacity وtransform للعرض؛ وللاحتفاظ بـ opacity الخاصة بك اربط الخطوات فقط (transform: m.transform). أما الألوان فتتحرك بقيمة متحركة وinterpolate.

TSXRustالوصف
createMotion(options?)motion()حركة تكون كل قنواتها في حالة السكون.
createMotion({ initial: { opacity: 0 } })m.value(MotionChannel::Opacity).set(0.0)المواضع التي تبدأ منها القنوات؛ وتبدأ الباقية في حالة السكون.
createMotion({ enter, delay, duration, ... })motion().entering(spec)تبدأ من حيث يبدأ enter وتشغّله فورًا؛ وتغيّر الخيارات الأخرى توقيته. يحدد enter القنوات التي يحرّكها، عبر initial.
style={m.style}، m.transform.bind_motion(&m)يربط القنوات: m.style هو { opacity, transform }، وtransform فيه قائمة من سبع خطوات (يُدمج scale في scaleX وscaleY). تُطبَّق بعد translate وrotate وscale الخاصة بالعرض؛ ويمنح المنظور (perspective) الموضوع قبلها القلبَ ثلاثي الأبعاد عمقه: transform: [{ perspective: 800 }, ...m.transform]. في Rust يُطبَّق تحويل الحركة بعد تحويل العرض نفسه.
m.opacity، m.translateX، …m.value(MotionChannel::Opacity)، …القيمة المتحركة لقناة واحدة، لتستكمل بينها بالاستيفاء أو تحرّكها يدويًا.
m.play(motion, options?)m.play(spec)يشغّل حركة جاهزة أو حركة من تصميمك؛ والحركة الجديدة تقاطع الحركة الجارية. يُحَل الوعد بالقيمة { finished }، true عند انتهائها (AnimationEnd في Rust).
m.animation(motion, options?)m.animation(spec)الحركة نفسها بوصفها رسمًا متحركًا مركّبًا، غير مبدوءة، لتسلسلها أو توزّعها زمنيًا أو تكرّرها مع غيرها.
play(motion, { ignoreReduceMotion: true })m.play_ignoring_reduce_motion(spec)يشغّلها حتى عندما يطلب المستخدم حركة أقل.
m.stop()m.stop()يوقفها حيث هي.
m.reset()m.reset()يوقفها ويعيد كل قناة إلى حالة السكون.

#الحركات الجاهزة

الحركات الجاهزة بيانات: motions من nsh (motions.fadeInUp هو ما يشغّله play("fadeInUp")) وMotionPreset في Rust (MotionPreset::FadeInUp.spec()). المسافات بالنقاط، ومقاسة للعناصر الصغيرة والبطاقات؛ وللعروض الأكبر حجمًا غيّر مقياس قيم الحركة أو اكتب إطاراتك المفتاحية. يدور swing وبقية الدورانات حول نقطة أصل التحويل للعرض: امنح اللافتة المتأرجحة transformOrigin: "top" (.origin_top()).

الدخول: تبدأ مخفية أو بعيدة وتنتهي في حالة السكون.

TSXRustالمدةالوصف
fadeInMotionPreset::FadeIn300 msيظهر تدريجيًا.
fadeInUpMotionPreset::FadeInUp450 msيظهر تدريجيًا، صاعدًا إلى مكانه.
fadeInDownMotionPreset::FadeInDown450 msيظهر تدريجيًا، نازلًا إلى مكانه.
fadeInLeftMotionPreset::FadeInLeft450 msيظهر تدريجيًا، قادمًا من اليسار.
fadeInRightMotionPreset::FadeInRight450 msيظهر تدريجيًا، قادمًا من اليمين.
zoomInMotionPreset::ZoomIn350 msيظهر تدريجيًا، ويكبر من 90 %.
popInMotionPreset::PopIn450 msيظهر بانفجار: يكبر من نصف حجمه إلى ما يتجاوز حجمه ثم يستقر.
bounceInMotionPreset::BounceIn650 msيدخل مرتدًّا، متجاوزًا حجمه مرتين.
slideInUpMotionPreset::SlideInUp400 msينزلق صاعدًا من الأسفل، دون تلاشٍ (الصفحات السفلية، والإشعارات المنبثقة).
slideInDownMotionPreset::SlideInDown400 msينزلق نازلًا من الأعلى، دون تلاشٍ.
flipInXMotionPreset::FlipInX600 msينقلب داخلًا حول محوره الأفقي.
flipInYMotionPreset::FlipInY600 msينقلب داخلًا حول محوره العمودي.

الخروج: تبدأ في حالة السكون وتنتهي مخفية أو بعيدة.

TSXRustالمدةالوصف
fadeOutMotionPreset::FadeOut250 msيتلاشى.
fadeOutUpMotionPreset::FadeOutUp300 msيتلاشى، صاعدًا بعيدًا.
fadeOutDownMotionPreset::FadeOutDown300 msيتلاشى، نازلًا بعيدًا.
fadeOutLeftMotionPreset::FadeOutLeft300 msيتلاشى نحو اليسار.
fadeOutRightMotionPreset::FadeOutRight300 msيتلاشى نحو اليمين.
zoomOutMotionPreset::ZoomOut250 msيتلاشى، ويتقلص إلى 90 %.
popOutMotionPreset::PopOut300 msيخرج بانفجار: ينتفخ قليلًا ثم يتقلص ويختفي.
slideOutDownMotionPreset::SlideOutDown300 msينزلق نازلًا خارج الرؤية، دون تلاشٍ.
slideOutUpMotionPreset::SlideOutUp300 msينزلق صاعدًا خارج الرؤية، دون تلاشٍ.

لفت الانتباه: تبدأ وتنتهي في حالة السكون.

TSXRustالمدةالوصف
shakeMotionPreset::Shake500 msيهتز جانبيًا: كلمة مرور خاطئة، أو حقل غير صالح.
headShakeMotionPreset::HeadShake800 msيهزّ رأسه: رفض ألطف.
wiggleMotionPreset::Wiggle600 msيتمايل حول مركزه: شيء يستحق الانتباه.
swingMotionPreset::Swing900 msيتأرجح من حافته العليا، مثل لافتة.
pulseMotionPreset::Pulse500 msيكبر قليلًا ثم يعود: تأكيد خفيف.
heartbeatMotionPreset::Heartbeat900 msينبض مرتين، مثل قلب: إعجاب، أو عنصر مفضّل.
rubberBandMotionPreset::RubberBand800 msينضغط ويتمدد مثل المطاط.
jelloMotionPreset::Jello800 msيهتز مثل الجيلي.
tadaMotionPreset::Tada900 msيتقلص ثم يكبر مع تمايل: مكافأة.
jumpMotionPreset::Jump700 msيقفز ويهبط مع انضغاط.
flashMotionPreset::Flash800 msيومض اختفاءً وظهورًا مرتين.

التكرار: تتكرر حتى تُوقَف.

TSXRustالمدةالوصف
spinMotionPreset::Spin1000 msيدور دورة كاملة، إلى الأبد.
floatMotionPreset::Float1500 msيطفو صعودًا ونزولًا، إلى الأبد.
breatheMotionPreset::Breathe1800 msيتنفس: يكبر ويشحب قليلًا، إلى الأبد.
swayMotionPreset::Sway1600 msيتمايل حول مركزه، إلى الأبد.
bobMotionPreset::Bob1000 msيرتفع وينخفض مثل كرة مرتدة، إلى الأبد.
glowMotionPreset::Glow2000 msيتوهج ويخفت، إلى الأبد.

يقبل play خيارات التوقيت الخاصة بحركة جاهزة فوقها (m.play("shake", { duration: 300 }))، ويمكن نشر الحركة الجاهزة داخل حركة من تصميمك (m.play({ ...motions.fadeInUp, duration: 600 })). في Rust غيّر مواصفات الحركة الجاهزة: MotionPreset::FadeInUp.spec().timing(|t| t.delay(100.0)).

#حركات من تصميمك

الحركة من تصميمك هي إطارات مفتاحية لكل قناة وتوقيت واحد (خيارات الإطارات المفتاحية). في TSX اكتب الإطارات المفتاحية لكل قناة ({ opacity: [0, 1] })، أو قائمة إطارات كما يكتبها Web Animations API ([{ opacity: 0, translateY: 24 }, { opacity: 1, translateY: 0 }])، لكل إطار offset وeasing. في قائمة الإطارات، يقع الإطار الذي بلا إزاحة (offset) حيث يضعه موضعه في القائمة، وتتبع القناةُ الإطاراتِ التي تذكرها. يمكن أن تكون الزوايا سلاسل نصية ("45deg"، "0.5turn"). في Rust ابنِ MotionSpec.

let rise = MotionSpec::new(KeyframeTiming::new(400.0))
    .values(MotionChannel::Opacity, [0.0, 1.0])
    .track(
        MotionChannel::TranslateY,
        [Keyframe::at(0.0, 24.0).ease(Easing::EaseOut), Keyframe::at(1.0, 0.0)],
    );
card.play(rise);

let flip = MotionSpec::new(KeyframeTiming::new(600.0).infinite())
    .values(MotionChannel::RotateY, [0.0, 180.0, 360.0]);
card.play(flip);

#الدخول والقوائم المتتابعة

امنح كل صف حركة دخول، بتأخير يزداد مع فهرسه: فالصفوف التي تظهر معًا تدخل واحدًا تلو الآخر. حدّد سقفًا للتأخير حتى لا ينتظر صف أُضيف في أسفل القائمة طويلًا.

each(items, |item: &Item| item.id, |item, index| {
    let delay = index.get_untracked().min(10) as f32 * 60.0;
    let row = motion().entering(MotionPreset::FadeInUp.spec().timing(|t| t.delay(delay)));
    view()
        .rounded_xl()
        .bg(ZINC.shade(100))
        .p_4()
        .bind_motion(&row)
        .child(text(move || item.get().title))
})

كما يمكن تركيب الحركات: m.animation(..) هو حركة بوصفها رسمًا متحركًا مركّبًا، غير مبدوء، فيشغّل Animated.stagger مجموعة منها بشكل أصلي، بالعدّ من العنصر الأول أو الأخير أو الوسط أو أي فهرس. امنحها initial مخفيًا (أو ابدأها من حيث تبدأ الحركة) حتى لا يظهر شيء قبل دوره.

let cards: Vec<Motion> = (0..5)
    .map(|_| {
        let m = motion();
        m.value(MotionChannel::Opacity).set(0.0);
        m
    })
    .collect();
Animation::stagger_from(60.0, StaggerFrom::Center, cards.iter().map(|m| m.animation(MotionPreset::FadeInUp))).start();

#الخروج

شغّل حركة الخروج، وانتظر انتهاءها، ثم أزل العرض. وتتيح رسوم التخطيط المتحركة للعروض المحيطة به أن تسدّ الفراغ.

let remove = move || {
    spawn(async move {
        row.play(MotionPreset::FadeOutLeft).await;
        animate_layout(LayoutAnimation::EASE_IN_EASE_OUT);
        items.update(|items| items.retain(|i| i.id != id));
    });
};

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

#الانتقالات (Transitions)

سمِّ ما يجب تحريكه ومدة التحريك (.transition_all().duration(400.0) في Rust، وtransitionProperty وtransitionDuration في TSX)، فتتحرك التغييرات اللاحقة على تلك الخصائص بشكل أصلي من القيمة الظاهرة على الشاشة إلى القيمة الجديدة.

#[component]
pub fn App() -> impl IntoElement {
    let on = signal(false);
    view().flex_1().items_center().justify_center().child(
        view()
            .on_press(move || on.update(|on| *on = !*on))
            .size(px(100.0))
            .rounded(move || px(if on.get() { 50.0 } else { 16.0 }))
            .bg(move || if on.get() { rgb(0x8250df) } else { rgb(0x0969da) })
            .rotate(move || deg(if on.get() { 45.0 } else { 0.0 }))
            .scale(move || if on.get() { 1.2_f32 } else { 1.0 })
            .transition_all()
            .duration(400.0)
            .ease_in_out(),
    )
}
النمطRustالنوعالوصف
transitionProperty.transition()، .transition_all()، .transition_colors()، .transition_opacity()، .transition_transform()، .transition_shadow()stringما الذي يتحرك، مفصولًا بفواصل: "all"، "colors"، "opacity"، "transform"، "box-shadow"، "border-radius"، …
transitionDuration.duration(ms)number | "150ms" | "0.2s"بالمللي ثانية كرقم، أو زمن بصيغة CSS.
transitionDelay.delay(ms)number | "150ms" | "0.2s"قبل أن تبدأ.
transitionTimingFunction.ease_linear()، .ease_in()، .ease_out()، .ease_in_out()، .ease_bezier(..)"linear" | "ease" | "ease-in" | "ease-out" | "ease-in-out" | "cubic-bezier(…)"المنحنى.

الخصائص التي تنتقل، حسب المجموعة: الألوان (ألوان الخلفية والحدود والإطار الخارجي والظل والنص، ونقاط التدرج)، والشفافية، والتحويل، والظلال (box-shadow)، والشكل (نصف قطر الحدود وعرضها). تعمل أسماء CSS وصيغها بنمط camel-case معًا (background-color، backgroundColor، transform، scale، …). أما تغييرات التخطيط فتتحرك عبر رسوم التخطيط المتحركة.

مع Tailwind، الأمر نفسه هو transition (الألوان والشفافية والظلال والتحويلات)، transition-all، transition-colors، transition-opacity، transition-transform، transition-shadow، مع duration-* وdelay-* وease-*. وبدمجها مع حالة الضغط (.active(..)، active:) تحصل على حالات ضغط متحركة:

view()
    .on_press(save)
    .rounded_xl()
    .bg(BLUE.shade(600))
    .px_5()
    .py_3()
    .transition()
    .duration(150.0)
    .active(|s| s.scale(0.95).bg(BLUE.shade(800)))
    .child(text("Save").font_weight(FontWeight::SEMIBOLD).text_color(white()))

يبدأ الانتقال من القيمة الظاهرة حاليًا على الشاشة، فإذا غيّرت الهدف مرة أخرى في منتصف الطريق انعطف الانتقال بسلاسة.

#الرسوم المتحركة بالإطارات المفتاحية المدمجة

تعمل أربعة رسوم مدمجة بالإطارات المفتاحية بشكل أصلي، وتتكرر إلى الأبد افتراضيًا: spin (دورة واحدة باتجاه عقارب الساعة)، ping (يكبر إلى ضعفين ويتلاشى)، pulse (يشحب إلى نصف الشفافية ثم يعود) وbounce (يقفز بمقدار ربع ارتفاعه ويهبط).

view()
    .size_8()
    .rounded_full()
    .border(px(4.0))
    .border_color(ZINC.shade(300))
    .border_t_color(BLUE.shade(600))
    .animate_spin()
view().h_4().w_40().rounded(px(4.0)).bg(ZINC.shade(200)).animate_pulse()
النمطRustالنوعالوصف
animationName.animate_spin()، .animate_ping()، .animate_pulse()، .animate_bounce()، .animate_none()، أو .animation(KeyframeAnimation::new(Keyframes::Spin))"none" | "spin" | "ping" | "pulse" | "bounce"الرسم المتحرك.
animationDurationKeyframeAnimation::duration(ms)number | "1s" | …تكرار واحد.
animationDelayKeyframeAnimation::delay(ms)number | "200ms" | …قبل أن يبدأ.
animationTimingFunctionKeyframeAnimation::easing([x1, y1, x2, y2])مثل transitionTimingFunctionالمنحنى.
animationIterationCountKeyframeAnimation::iterations(n)number | "infinite"القيمة الافتراضية "infinite".

Tailwind: animate-spin، animate-ping، animate-pulse، animate-bounce، animate-none.

#القيم المتحركة

القيمة المتحركة رقم يعيش على خيط واجهة المستخدم. أنشئ واحدة (animated_value(..) في Rust، وcreateAnimatedValue من nsh في TSX)، واربطها بالأنماط، وحرّكها؛ وتتبعها العروض المرتبطة بها في كل إطار دون تحديث.

#[component]
pub fn App() -> impl IntoElement {
    let x = animated_value(0.0);
    let opacity = animated_value(1.0);
    let play = move || {
        spawn(async move {
            x.timing(240.0, 600.0, Easing::Curve(Curve::Cubic, EaseMode::Out)).await;
            opacity.timing(0.3, 200.0, Easing::EaseInOut).await;
            x.spring(0.0, Spring::default());
            opacity.timing_with(1.0, Timing::default());
        });
    };
    view()
        .flex_1()
        .justify_center()
        .p_6()
        .gap_6()
        .child(
            view()
                .size(px(60.0))
                .rounded(px(12.0))
                .bg(rgb(0x0969da))
                .bind_opacity(&opacity)
                .bind_translate_x(&x),
        )
        .child(view().on_press(play).child(text("Play").text_color(rgb(0x0969da))))
}

ما يمكن ربطه: opacity (.bind_opacity(..))؛ وbackgroundColor وborderColor (كل الجوانب) وcolor للنص (النص كله)، عبر استيفاء إلى الألوان (.bind_background(..)، .bind_border_color(..)، .bind_color(..))؛ وtranslate ([x, y]، بقيمة لكل محور)، وrotate (بالدرجات)، وscale ([x, y] لكل محور، أو قيمة واحدة للمحورين)؛ وخطوات قائمة transform: translateX، translateY، scale، scaleX، scaleY، rotate، rotateX، rotateY وrotateZ (.bind_translate_x(..)، .bind_translate_y(..)، .bind_scale(..)، .bind_rotate(..) بالدرجات، .bind_transform(kind, ..) في Rust). تتغير الظلال عبر الانتقالات. وحلقة التركيز أو السعر الذي يومض بالأخضر هو استيفاء لوني مربوط بلون الحدود أو النص:

let flash = animated_value(0.0);
let color = flash.interpolate_colors(&[0.0, 1.0], &[rgb(0x1f2328), rgb(0x1a7f37)]);

text(move || price.get().to_string()).bind_color(&color)
// On a new price: flash.set(1.0); flash.timing(0.0, 600.0, Easing::EaseInOut);

تقود القيم المتحركة أيضًا رسومات canvas، وتستقبل إزاحات التمرير (scrollValue) والإيماءات (pan، pinch، rotation).

تبقى القيمة حيّة بفضل العروض المرتبطة بها والمكوّن الذي أنشأها، فيظل الربط متصلًا مهما جاءت متغيراتك وذهبت. أنشئ القيم المتحركة في المكوّن الذي يملكها (وليس على مستوى الوحدة (module) حين يحتاج كل نسخة إلى قيمتها الخاصة).

#دوال AnimatedValue

TSXRustالوصف
set(value)set(value)ينتقل فورًا إلى value ويوقف أي رسم متحرك.
stop()stop()يوقف الرسم المتحرك الجاري عند موضعه الحالي؛ وفي TSX يُحَلّ الوعد بالقيمة { value }، أي القيمة عند ذلك الموضع.
getValue()value()القيمة الحالية، وتُقرأ على خيط واجهة المستخدم (UI thread) حيث تتحرك (وتستقر خلال إطار واحد): وعد (promise) في TSX، ونتيجة قابلة للانتظار في Rust.
setOffset(offset)set_offset(offset)يضيف offset إلى ما تعرضه القيمة: فترى الارتباطات (bindings) القيمة + الإزاحة، بينما set والرسوم المتحركة وعمليات السحب تحرّك القيمة نفسها.
flattenOffset()flatten_offset()يضيف الإزاحة إلى القيمة ويجعل الإزاحة 0.
extractOffset()extract_offset()ينقل القيمة إلى الإزاحة ويجعل القيمة 0: فيبدأ السحب التالي من 0، فوق الموضع الذي تركه السحب السابق.
timing(to, { duration?, easing?, delay? })timing(to, ms, easing)، timing_with(to, Timing { .. })يحرّك القيمة إلى to خلال duration مللي ثانية (الافتراضي 300، ومنحنى التسهيل "easeInOut").
spring(to, { stiffness?, damping?, mass?, velocity? })spring(to, Spring { .. })ينتقل بنمط الزنبرك إلى to (القيم الافتراضية: الصلابة 170، والتخميد 26، والكتلة 1).
decay({ velocity?, deceleration?, clamp?, bounce? })decay(velocity, deceleration)، decay_within(velocity, deceleration, (min, max), bounce)ينساب بدءًا من velocity (وحدات في الثانية) ثم يتباطأ (التباطؤ الافتراضي 0.998 لكل مللي ثانية). مع القيد [min, max] يتوقف عند حد، أو يرتد عنه محتفظًا بنسبة bounce (من 0 إلى 1) من سرعته.

يعطي كل رسم متحرك نتيجة يمكنك انتظارها تبيّن هل انتهى: true إذا اكتمل حتى النهاية، وfalse إذا أوقفه شيء ما (stop() أو set() أو رسم متحرك جديد على القيمة نفسها). وهي Promise<{ finished: boolean }> في TSX وAnimationEnd في Rust، وتنتظرها المهمة (task) باستخدام bool. يؤدي بدء رسم متحرك جديد على قيمة متحركة إلى مقاطعة القديم بسلاسة: فالزنبرك أو الانزلاق دون سرعة ابتدائية يتابع بالسرعة الحالية للقيمة.

// Spring settings: lower damping overshoots more, higher stiffness is faster.
x.spring(200.0, Spring { stiffness: 180.0, damping: 8.0, ..Spring::default() });   // wobbly
x.spring(200.0, Spring { stiffness: 400.0, damping: 30.0, ..Spring::default() });  // stiff, no overshoot

// A fling that coasts to a stop, from a pan's release velocity.
view().pan_values(Some(&x), None).on_pan_end(move |e: &PanEvent| {
    x.decay(Some(e.velocity_x), 0.998);
})

// A fling that stays on its track and bounces softly off its ends.
x.decay_within(Some(e.velocity_x), 0.998, (0.0, 300.0), 0.3);

// Where a value is now (it moves on the UI thread, so the answer is async).
let at = x.value().await;

#الإطارات المفتاحية

يشغّل keyframes القيمة عبر إطارات مفتاحية (keyframes) كما تعمل إطارات CSS المفتاحية: value.keyframes(frames, options) في TSX، وvalue.keyframes(frames, KeyframeTiming) في Rust (Animated.keyframes(value, { keyframes, ... }) وAnimation::keyframes(..) بوصفها خطوة في رسم متحرك مركّب). الإطار المفتاحي إما رقم (أو سلسلة زاوية مثل "90deg") يُوزَّع بالتساوي مع بقية الإطارات، أو { offset, value, easing } (Keyframe::new(value) وKeyframe::at(offset, value) و.ease(easing) في Rust).

let y = animated_value(0.0);
y.keyframes(
    [Keyframe::new(0.0), Keyframe::at(0.3, -20.0).ease(Easing::EaseIn), Keyframe::new(0.0)],
    KeyframeTiming::new(600.0).easing(Easing::EaseOut).iterations(Some(3)),
);

// A clock hand that ticks once a second.
let hand = animated_value(0.0);
hand.keyframes(
    [Keyframe::new(0.0), Keyframe::new(360.0)],
    KeyframeTiming::new(60_000.0).easing(Easing::Steps(60, StepPosition::JumpEnd)).infinite(),
);
الخيارRust (KeyframeTiming)الافتراضيالوصف
durationKeyframeTiming::new(ms)300مدة تكرار واحد، بالمللي ثانية.
delay.delay(ms)0التأخير قبل التكرار الأول.
easing.easing(easing)قيمة CSS easeمنحنى كل مقطع ليس له تسهيل خاص به.
iterations.iterations(Some(n))، .infinite()1عدد التكرارات، أو "infinite".
direction.direction(PlaybackDirection::Alternate)"normal""normal" أو "reverse" أو "alternate" أو "alternate-reverse".

تعمل كما في CSS وWeb Animations API:

  • يشكّل التسهيل كل مقطع على حدة، لا الحركة كلها: فـeasing الخاص بالإطار المفتاحي يشكّل المقطع الذي يبدأ عنده، وeasing الخاص بالتوقيت يشكّل كل مقطع آخر.
  • تتوزع الإطارات المفتاحية التي بلا إزاحة (offset) بالتساوي بين جاراتها؛ ويكون الأول افتراضيًا عند 0 والأخير عند 1.
  • يبدأ كل تكرار بقفزة إلى إطاره المفتاحي الأول. وإذا لم يوجد إطار مفتاحي عند الإزاحة 0، تنطلق القيمة من موضعها الحالي.
  • أثناء delay تحمل القيمة الإطار المفتاحي الأول فعلًا (animation-fill-mode: backwards في CSS)، فيعرض الدخول المتأخر إطاره الأول بدلًا من العرض في حالة السكون. وينطبق هذا على الحركات (motions) أيضًا.
  • reverse والتكرارات العكسية في alternate تشغّل المقاطع بالعكس، مع عكس منحنى كل منها.
  • تبقى القيمة حيث يتركها التكرار الأخير. وتبيّن النتيجة هل انتهت: true إذا انتهت، وfalse إذا أوقفها شيء ما.

#الاستيفاء

يحوّل interpolate قيمةً عبر مقاطع خطية متعددة إلى أرقام أو ألوان أو زوايا، ويعيد عقدة جديدة يمكنك ربطها. ويمكن لقيمة واحدة أن تتحكم في خصائص كثيرة. في Rust يحوّل interpolate(&input, &output) إلى أرقام، وinterpolate_colors(&input, &colors) إلى ألوان، ويقبل interpolate_with(&input, output, Interpolation { .. }) الاستقراء (extrapolation) والتسهيل؛ أما في TSX فيقبلها value.interpolate({ inputRange, outputRange }) جميعًا.

let t = animated_value(0.0);

view()
    .bind_background(&t.interpolate_colors(&[0.0, 1.0, 2.0], &[rgb(0x54aeff), rgb(0x8250df), rgb(0xcf222e)]))
    .bind_opacity(&t.interpolate(&[0.0, 1.0], &[0.4, 1.0]))
    .bind_rotate(&t.interpolate(&[0.0, 1.0], &[0.0, 180.0]))
    .bind_scale(&t.interpolate_with(
        &[0.0, 1.0],
        AnimationOutput::Numbers(vec![0.6, 1.2]),
        Interpolation {
            easing: Easing::Curve(Curve::Back(1.70158), EaseMode::Out),
            ..Interpolation::default()
        },
    ))
الخيارRustالوصف
inputRangeشريحة المدخلاتنقاط إدخال متزايدة.
outputRangeشريحة المخرجات، أو AnimationOutputمخرج واحد لكل نقطة: أرقام، أو ألوان ("#rrggbb" أو "rgba(…)" أو بالاسم)، أو زوايا ("45deg" أو "1rad").
extrapolateInterpolation::extrapolate(Extrapolate::Extend)خارج نطاق المدخلات: "clamp" (الافتراضي)، أو "extend" (متابعة المقطعين الطرفيين)، أو "identity" (تمرير المدخل كما هو).
extrapolateLeft، extrapolateRightextrapolate_left، extrapolate_rightالأمر نفسه، لجانب واحد.
easingeasingيشكّل كل مقطع.

#العمليات الحسابية وdiffClamp

تُدمج العقد بعمليات حسابية، وبشكل أصلي (native) أيضًا: a.add(b) وa.sub(b) وa.mul(b) وa.div(b) وa.mod(b) (a.modulo(b) في Rust؛ لا يكون الناتج سالبًا أبدًا عندما يكون b موجبًا، مثل الزوايا في النطاق 0..360)، حيث b عقدة أو رقم. وفي TSX، Animated.add وsubtract وmultiply وdivide وmodulo هي نفسها في صورة دوال.

يتتبع diffClamp(min, max) (diff_clamp) تغيّرات العقدة مع البقاء ضمن min..max. وعند تطبيقه على إزاحة التمرير، يصنع ترويسة تختفي عندما يُمرَّر المحتوى لأعلى وتعود بمجرد التمرير لأسفل:

let offset = animated_value(0.0);
let header = offset.diff_clamp(0.0, 56.0);

scroll().flex_1().scroll_value(&offset)
view().absolute().top_0().left_0().right_0().h(px(56.0)).bind_translate_y(&header.mul(-1.0))

مع { snap: true } (diff_clamp_with(min, max, DiffClamp::snapping()))، إذا توقف التمرير والترويسة ظاهرة جزئيًا، فإنها تُكمل الحركة: تستقر ظاهرة بالكامل أو مخفية بالكامل، أيهما أقرب، خلال 200 مللي ثانية على خيط واجهة المستخدم. ولا تختفي إلا بعد أن تُمرَّر الصفحة بمقدار ارتفاعها على الأقل، فالصفحة التي تُمرَّر بأقل من ذلك تُظهر الترويسة كاملة. وإذا عاد التمرير أثناء الحركة، تتولى الترويسة من موضعها الحالي.

let header = offset.diff_clamp_with(0.0, 56.0, DiffClamp::snapping());

#الرسوم المتحركة المركّبة

تُبنى الرسوم المتحركة من خطوات (Animation في Rust، وAnimated من nsh في TSX) وتعمل الشجرة كلها بشكل أصلي: تبدأ كل خطوة على خيط واجهة المستخدم لحظة انتهاء سابقتها، دون أي فجوة ودون أن تتدخل أي شيفرة من عندك بينهما.

let ys: Vec<AnimatedValue> = (0..5).map(|_| animated_value(0.0)).collect();
let fade = animated_value(1.0);

let hop = Animation::stagger(
    80.0,
    ys.iter().map(|y| {
        Animation::sequence([
            Animation::timing(y, -30.0, Timing::new(200.0, Easing::Curve(Curve::Quad, EaseMode::Out))),
            Animation::spring(y, 0.0, Spring { stiffness: 300.0, damping: 12.0, ..Spring::default() }),
        ])
    }),
);

let breathe = Animation::sequence([
    Animation::timing(&fade, 0.3, Timing::new(400.0, Easing::EaseInOut)),
    Animation::timing(&fade, 1.0, Timing::new(400.0, Easing::EaseInOut)),
])
.looped(None);

let landed = hop.start();
spawn(async move {
    status.set(if landed.await { "landed" } else { "stopped" });
});
TSXRustالوصف
Animated.timing(value, { toValue, duration?, easing?, delay? })Animation::timing(&value, to, Timing { .. })خطوة توقيت.
Animated.spring(value, { toValue, stiffness?, damping?, mass?, velocity? })Animation::spring(&value, to, Spring { .. })خطوة زنبرك.
Animated.decay(value, { velocity?, deceleration?, clamp?, bounce? })Animation::decay(&value, velocity, deceleration)، Animation::decay_within(..)خطوة انزلاق (decay).
Animated.sequence(list)Animation::sequence(list)واحدة بعد أخرى.
Animated.parallel(list, { stopTogether? })Animation::parallel(list)، .stop_together(..)جميعها في وقت واحد.
Animated.stagger(ms, list, { from? })Animation::stagger(ms, list)، Animation::stagger_from(ms, StaggerFrom::Center, list)جميعها في وقت واحد، وتبدأ كل واحدة بعد ms من سابقتها، محسوبةً من from: "first" (الافتراضي)، أو "last"، أو "center" (تموّج يمتد من المنتصف إلى الخارج)، أو فهرس (StaggerFrom::First أو Last أو Center أو Index(i)).
Animated.keyframes(value, { keyframes, ...timing })Animation::keyframes(&value, frames, KeyframeTiming)خطوة إطارات مفتاحية.
Animated.delay(ms)Animation::delay(ms)تنتظر.
Animated.loop(animation, { iterations?, resetBeforeIteration? }).looped(iterations)، .reset_before_iteration(..)تكرّر (إلى الأبد افتراضيًا؛ None في Rust).

للرسم المتحرك المركّب start() (وفي TSX أيضًا مع دالة رد نداء ووعد، وكلاهما يُمرَّر إليه { finished }؛ وفي Rust نتيجة قابلة للانتظار bool؛ واستدعاؤه مرة أخرى يشغّله من جديد)، وstop() (يتوقف عند موضعه)، وreset() (يتوقف ويعيد قيمه إلى ما كانت عليه عند بدئه). وتعمل دالة رد النداء مرة واحدة للشجرة كلها.

لا يُبقي الرسم المتحرك الجاري قيمَه حية: فعندما يزول المكوّن الذي يملكها (بعروضه ومتغيراته)، تتوقف معه أي حلقة لا نهائية، وتكون نتيجتها أنها لم تنتهِ (false).

#التسهيل

منحنيات التسهيل (easing) بيانات تُقيَّم بشكل أصلي: Easing من nsh في TSX، والتعداد Easing في Rust.

TSXRustالوصف
Easing.linear، Easing.easeEasing::Linear، Easing::Bezier(0.25, 0.1, 0.25, 1.0)سرعة ثابتة؛ ما يقابل ease في CSS.
Easing.quad، Easing.cubic، Easing.poly(n)Curve::Quad، Curve::Cubic، Curve::Poly(n)t²، t³، tⁿ.
Easing.sin، Easing.circle، Easing.expCurve::Sin، Curve::Circle، Curve::Expجيبي، ودائري، وأسّي.
Easing.back(s?)Curve::Back(s)يتراجع قبل الانطلاق (التجاوز s، والافتراضي 1.70158).
Easing.elastic(bounciness?)Curve::Elastic(bounciness)تجاوز مرن كالزنبرك.
Easing.bounceCurve::Bounceيرتد عند النهاية.
Easing.bezier(x1, y1, x2, y2)Easing::Bezier(x1, y1, x2, y2)منحنى بيزييه تكعيبي، كما في CSS.
Easing.steps(count, position?)Easing::Steps(count, StepPosition::JumpEnd)يقفز بين count من المستويات بدل أن يتحرك بسلاسة، كما يفعل steps() في CSS: صورة متحركة إطارًا بإطار، أو عقرب يدقّ. وقيمة position هي "jump-end" (الافتراضي، وكذلك "end")، أو "jump-start" ("start")، أو "jump-none" أو "jump-both".
Easing.in(e)، Easing.out(e)، Easing.inOut(e)Easing::Curve(curve, EaseMode::In)، EaseMode::Out، EaseMode::InOutهذه المنحنيات هي منحنيات «الدخول» (in)؛ وout وinOut يعكسانها.

في TSX يمكنك، في أي موضع يُقبل فيه تسهيل، تمرير اسم ("linear" أو "ease" أو "easeIn" أو "easeOut" أو "easeInOut") أو مصفوفة بيزييه [x1, y1, x2, y2]؛ وفي Rust، Easing::EaseIn وEasing::EaseOut وEasing::EaseInOut هي منحنيات CSS.

x.timing(1.0, 300.0, Easing::Curve(Curve::Back(1.70158), EaseMode::Out));
x.timing(1.0, 300.0, Easing::EaseOut);
x.timing(1.0, 300.0, Easing::Bezier(0.2, 0.8, 0.2, 1.0));

يعيد Easing.sample(easing, count) (sample_easing في Rust) المنحنى عند count من النقاط المتباعدة بالتساوي من 0 إلى 1، ويعيد Animated.sampleSpring(config, fps?) / Animated.sampleDecay(config, fps?) (sample_spring، sample_decay) القيم التي يمرّ بها الزنبرك أو الانزلاق، قيمة لكل إطار (60 في الثانية افتراضيًا) بدءًا من config.from (0) حتى النهاية. وتُحسب بالشيفرة الأصلية نفسها التي تعمل بها الرسوم المتحركة، لذا فإن رسم المنحنى البياني يعرض بالضبط ما يفعله الرسم المتحرك:

use nsh_core::element::{sample_easing, sample_spring};

let curve = sample_easing(Easing::Curve(Curve::Back(1.70158), EaseMode::Out), 50); // 50 progress values
let spring = sample_spring(0.0, 1.0, Spring { stiffness: 180.0, damping: 8.0, ..Spring::default() }, 60.0);

#LayoutAnimation

تحرّك الرسوم المتحركة للتخطيط (layout animation) تغيّرات الإطارات في التحديث التالي بشكل أصلي: العروض التي تتحرك أو يتغير حجمها تنزلق إلى إطاراتها الجديدة، والعروض الجديدة تظهر تدريجيًا (أو بتكبير)، والعروض المحذوفة تتلاشى. استدعِها قبل تغيير الحالة مباشرة: animate_layout(..) في Rust، وLayoutAnimation.configureNext(config) في TSX.

use nsh_core::element::LayoutAnimation;

let items = signal(vec![1, 2, 3]);
let next = stored(4);

let add = move || {
    animate_layout(LayoutAnimation::SPRING);
    let n = next.with(|n| {
        *n += 1;
        *n - 1
    });
    items.update(|i| i.insert(0, n));
};
let remove = move || {
    animate_layout(LayoutAnimation::EASE_IN_EASE_OUT);
    items.update(|i| {
        i.remove(0);
    });
};
TSXRustالوصف
configureNext(config, onEnd?)animate_layout(config)يحرّك تغيّرات الالتزام (commit) التالي؛ وفي TSX يعمل onEnd عند الانتهاء.
create(duration, type?, property?)LayoutAnimation::all(LayoutTransition::new(curve, ms))إعداد بمنحنى واحد للمراحل الثلاث.
Presets.easeInEaseOut، Presets.linear، Presets.springLayoutAnimation::EASE_IN_EASE_OUT، LINEAR، SPRINGإعدادات جاهزة.
easeInEaseOut(onEnd?)، linear(onEnd?)، spring(onEnd?)animate_layout مع إعداد مسبقconfigureNext مع إعداد مسبق.
Types، PropertiesLayoutCurve، LayoutPropertyالأسماء، لبناء الإعدادات.

الإعداد هو { duration, create?, update?, delete? } (LayoutAnimation { create, update, delete } في Rust): create للعروض المُدرَجة، وupdate للعروض التي نُقلت أو تغيّر حجمها، وdelete للعروض المحذوفة. وكل مرحلة هي { type?, property?, duration?, delay?, springDamping? } (LayoutTransition { curve, property, duration_ms, delay_ms })، حيث type هو "easeInEaseOut" أو "spring" أو "linear" أو "easeIn" أو "easeOut" أو "keyboard" (منحنى لوحة المفاتيح البرمجية نفسها، ويُحتسب توقيته من لحظة بدء حركة لوحة المفاتيح، ليتحرك معها ما يُفسح لها المكان؛ LayoutCurve::Keyboard)، وproperty هي الخاصية التي تتحرك عند ظهور العروض واختفائها ("opacity" أو "scaleXY" أو "scale")، وspringDamping (من 0 إلى 1، والافتراضي 0.7؛ LayoutCurve::Spring { damping }) يحدد مقدار تجاوز الزنبرك.

use nsh_core::element::{LayoutAnimation, LayoutCurve, LayoutProperty, LayoutTransition};

animate_layout(LayoutAnimation {
    create: Some(LayoutTransition::new(LayoutCurve::EaseOut, 300.0)),
    update: Some(LayoutTransition::new(LayoutCurve::Spring { damping: 0.8 }, 300.0)),
    delete: Some(LayoutTransition {
        property: LayoutProperty::Scale,
        ..LayoutTransition::new(LayoutCurve::EaseIn, 300.0)
    }),
});

تشمل الرسوم المتحركة للتخطيط كل إطار يغيّره التحديث، في أي مكان على الشاشة: فالقائمة التي تتحدث، أو البيانات الحية التي تصل في التحديث نفسه، تتحرك معه. وفي مكوّن Rust، حرّك فقط ما يتحرك مع تغيّر عنصر واحد باستخدام ElementRef::animate_layout(transition): إطار العنصر نفسه وإطار محتواه، وإطارات العروض التي يدفعها حجمه (أشقاؤه وأبناؤهم، وصولًا إلى الشاشة). أما بقية إطارات التحديث فتتغير دفعة واحدة. وهكذا يكشف nsh/base عن محتوى Collapsible وAccordionPanel.

use nsh_core::element::{LayoutCurve, LayoutTransition};

let region = element_ref();
let open = signal(false);
let toggle = move || {
    region.animate_layout(LayoutTransition::new(LayoutCurve::Spring { damping: 1.0 }, 265.0));
    open.update(|o| *o = !*o);
};
view()
    .element_ref(&region)
    .overflow_hidden()
    .style_with(move |s| if open.get() { s } else { s.h(px(0.0)) })

#ما يعمل وأين

يعمل علىشيفرتك في كل إطار
الانتقالات، والرسوم المتحركة بالإطارات المفتاحيةخيط واجهة المستخدملا شيء
الحركات، والإطارات المفتاحية، وtiming / spring / decay، والرسوم المركّبةخيط واجهة المستخدملا شيء؛ تعرف شيفرتك فقط عند انتهائها
interpolate، والعمليات الحسابية، وdiffClampخيط واجهة المستخدملا شيء
scrollValue، وpan / pinch / rotationخيط واجهة المستخدملا شيء؛ تعرف شيفرتك البداية والنهاية
الرسوم المتحركة للتخطيطخيط واجهة المستخدملا شيء
حلقات requestAnimationFrame (request_animation_frame في Rust)، ومعالجات onScroll، وPanResponderشيفرتك، ثم تحديثكل إطار

لا تلجأ إلى requestAnimationFrame (دالة عامة في TSX، وتتبع معدل تحديث الشاشة) إلا لما لا يمكن وصفه مسبقًا، مثل محاكاة فيزيائية في شيفرتك. وكل ما يمكن أن يكون قيمة متحركة ينبغي أن يكون كذلك.

#تقليل الحركة

عندما يطلب المستخدم حركة أقل (iOS «Reduce Motion»، وAndroid «Remove animations»)، تكون قيمة reduce_motion() في Rust وreduceMotion() من nsh في TSX هي true، وتُطبَّق متغيرات Tailwind ‏motion-reduce: / motion-safe:.

الحركات تتبع هذا الإعداد من تلقاء نفسها: فـplay وenter يصلان فورًا إلى حيث تنتهي الحركة، دون أن يتحركا، وتكون النتيجة أنها انتهت (true)، كما في حركة اكتمل تشغيلها. ويظل الخروج ينتهي بالإخفاء، ويظل الدخول ينتهي في حالة السكون. مرّر ignoreReduceMotion: true (play_ignoring_reduce_motion في Rust) للحركة التي تحمل معنى يحتاجه المستخدم، مثل مؤشر التقدم الدوّار.

أما الانتقالات، والرسوم المتحركة بالإطارات المفتاحية، والقيم المتحركة، والإطارات المفتاحية، والرسوم المركّبة فتعمل كما كُتبت؛ استخدم ما يلي لاختيار بديل أهدأ:

let open = move || {
    if reduce_motion() {
        panel.set(0.0);
    } else {
        panel.spring(0.0, Spring::default());
    }
};

view().style_when(|| !reduce_motion(), |s| s.animate_bounce())

راجع إمكانية الوصول.

#ذات صلة