दस्तावेज़
Animation
NSH में हर एनिमेशन UI thread पर चलता है। आप उसे Rust या TSX से एक बार बयान करते हैं, और प्लेटफ़ॉर्म उसे फ़्रेम-दर-फ़्रेम चलाता है (iOS पर Core Animation और display link, Android पर UI-thread animator), आपके कोड को वापस बुलाए बिना। व्यस्त engine thread किसी एनिमेशन को अटकने नहीं दे सकता, और एनिमेशन के चलने के दौरान आपके कोड की कोई लागत नहीं आती।
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 जो आता है, जाता है, ध्यान खींचता है या लूप में चलता है: तैयार motions (fadeInUp, shake, pulse, …) या आपके अपने keyframes, उसकी opacity और transform पर। |
| Transitions | ऐसा style बदलाव जो झटके से बदलने के बजाय एनिमेट होना चाहिए: कोई रंग, opacity, transform। |
| Keyframe animations | स्पिनर, धड़कता हुआ प्लेसहोल्डर, उछलता हुआ संकेत। |
| Keyframes | कोई भी एनिमेटेड मान keyframes के ज़रिए, जैसे CSS उन्हें चलाता है: offsets, हर खंड के लिए easing, iterations और direction। |
| Animated values | समय के साथ किसी मान से चलने वाली कोई भी चीज़: springs, gestures, scroll position, sequences। |
| Layout animations | अपडेट के बाद आकार और स्थिति के बदलाव: items का जुड़ना, हटना, क्रम बदलना, पैनल का फैलना। |
#Motion
Motion किसी view की opacity और transform को keyframes के ज़रिए, native रूप से चलाता है। उसे उस component में बनाएँ जो view का मालिक है (Rust में motion(), TSX में nsh से createMotion()), उसे view से bind करें, और उस पर कोई तैयार motion या अपना motion चलाएँ। enter view को वहाँ से शुरू करता है जहाँ motion शुरू होता है और उसे तुरंत चला देता है, ताकि view अपने पहले फ़्रेम से ही आए।
#[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);
}),
)
}import { mount, View } from "nsh";
import { createMotion } from "nsh";
function App() {
const card = createMotion({ enter: "fadeInUp" });
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View
onPress={() => card.play("shake")}
style={{ width: 120, height: 120, borderRadius: 16, backgroundColor: "#0969da", ...card.style }}
/>
</View>
);
}
mount(App);Motion में हर channel के लिए एक animated value होता है: opacity (0..1), translateX और translateY (points), rotate, rotateX और rotateY (डिग्री), scale, scaleX और scaleY। ये विश्राम अवस्था से शुरू होते हैं (opacity और scales 1, बाकी 0) और इसी क्रम में लागू होते हैं: पहले translations, फिर rotations, फिर scales, view के transform origin के सापेक्ष। Motion का अपना कोई रूप नहीं होता: वह उसी को चलाता है जिसे आपने style किया है।
Motion केवल opacity और transform को चलाता है। m.style को spread करने से view की opacity और transform सेट हो जाती हैं; अपनी opacity रखने के लिए केवल steps को bind करें (transform: m.transform)। रंग किसी animated value और interpolate से चलते हैं।
| TSX | Rust | विवरण |
|---|---|---|
createMotion(options?) | motion() | ऐसा motion जिसका हर channel विश्राम अवस्था में है। |
createMotion({ initial: { opacity: 0 } }) | m.value(MotionChannel::Opacity).set(0.0) | जहाँ से channels शुरू होते हैं; बाकी विश्राम अवस्था से शुरू होते हैं। |
createMotion({ enter, delay, duration, ... }) | motion().entering(spec) | वहीं से शुरू होता है जहाँ enter शुरू होता है और उसे तुरंत चलाता है; अन्य विकल्प उसकी timing बदलते हैं। enter उन channels को सेट करता है जिन्हें वह चलाता है, initial के ऊपर। |
style={m.style}, m.transform | .bind_motion(&m) | channels को bind करता है: m.style है { opacity, transform }, उसका transform सात steps की एक सूची है (scale को scaleX और scaleY में मिला दिया जाता है)। ये view के translate, rotate और scale के बाद लागू होते हैं; इनसे पहले का perspective 3D flip को गहराई देता है: transform: [{ perspective: 800 }, ...m.transform]। Rust में motion का transform view के अपने transform के बाद लागू होता है। |
m.opacity, m.translateX, … | m.value(MotionChannel::Opacity), … | किसी एक channel का animated value, जिसे interpolate किया जा सकता है या हाथ से चलाया जा सकता है। |
m.play(motion, options?) | m.play(spec) | कोई preset या अपना motion चलाता है; नया motion चल रहे motion को बीच में रोक देता है। पूरा होने पर { finished }, true के साथ resolve होता है (Rust में AnimationEnd)। |
m.animation(motion, options?) | m.animation(spec) | वही motion एक composite animation के रूप में, बिना शुरू किए, ताकि दूसरों के साथ क्रम में, stagger करके या लूप में चलाया जा सके। |
play(motion, { ignoreReduceMotion: true }) | m.play_ignoring_reduce_motion(spec) | इसे तब भी चलाता है जब उपयोगकर्ता कम motion माँगता है। |
m.stop() | m.stop() | उसे जहाँ है वहीं रोक देता है। |
m.reset() | m.reset() | उसे रोकता है और हर channel को विश्राम अवस्था में लौटा देता है। |
#Presets
तैयार motions डेटा हैं: nsh से motions (motions.fadeInUp वही है जो play("fadeInUp") चलाता है) और Rust में MotionPreset (MotionPreset::FadeInUp.spec())। दूरियाँ points में हैं, controls और cards के हिसाब से; बड़े views के लिए motion के मानों को scale करें या अपने keyframes लिखें। swing और अन्य rotations view के transform origin के सापेक्ष घूमते हैं: झूलते साइन को transformOrigin: "top" दें (.origin_top())।
Enter: ये छिपे हुए या दूर से शुरू होते हैं और विश्राम अवस्था में खत्म होते हैं।
| TSX | Rust | अवधि | विवरण |
|---|---|---|---|
fadeIn | MotionPreset::FadeIn | 300 ms | धीरे-धीरे दिखाई देता है। |
fadeInUp | MotionPreset::FadeInUp | 450 ms | धीरे-धीरे दिखाई देता है, ऊपर उठकर अपनी जगह आता है। |
fadeInDown | MotionPreset::FadeInDown | 450 ms | धीरे-धीरे दिखाई देता है, नीचे उतरकर अपनी जगह आता है। |
fadeInLeft | MotionPreset::FadeInLeft | 450 ms | धीरे-धीरे दिखाई देता है, बाईं ओर से आता है। |
fadeInRight | MotionPreset::FadeInRight | 450 ms | धीरे-धीरे दिखाई देता है, दाईं ओर से आता है। |
zoomIn | MotionPreset::ZoomIn | 350 ms | धीरे-धीरे दिखाई देता है, 90 % से बढ़ता है। |
popIn | MotionPreset::PopIn | 450 ms | उभरकर आता है: आधे आकार से अपने आकार के पार तक बढ़ता है और फिर टिक जाता है। |
bounceIn | MotionPreset::BounceIn | 650 ms | उछलकर आता है, दो बार आगे निकल जाता है। |
slideInUp | MotionPreset::SlideInUp | 400 ms | नीचे से ऊपर सरकता है, बिना fade के (sheets, toasts)। |
slideInDown | MotionPreset::SlideInDown | 400 ms | ऊपर से नीचे सरकता है, बिना fade के। |
flipInX | MotionPreset::FlipInX | 600 ms | अपनी क्षैतिज धुरी पर पलटकर आता है। |
flipInY | MotionPreset::FlipInY | 600 ms | अपनी ऊर्ध्वाधर धुरी पर पलटकर आता है। |
Exit: ये विश्राम अवस्था से शुरू होते हैं और छिपे हुए या दूर जाकर खत्म होते हैं।
| TSX | Rust | अवधि | विवरण |
|---|---|---|---|
fadeOut | MotionPreset::FadeOut | 250 ms | धीरे-धीरे गायब होता है। |
fadeOutUp | MotionPreset::FadeOutUp | 300 ms | धीरे-धीरे गायब होता है, ऊपर उठकर दूर जाता है। |
fadeOutDown | MotionPreset::FadeOutDown | 300 ms | धीरे-धीरे गायब होता है, नीचे उतरकर दूर जाता है। |
fadeOutLeft | MotionPreset::FadeOutLeft | 300 ms | धीरे-धीरे गायब होता है, बाईं ओर जाता है। |
fadeOutRight | MotionPreset::FadeOutRight | 300 ms | धीरे-धीरे गायब होता है, दाईं ओर जाता है। |
zoomOut | MotionPreset::ZoomOut | 250 ms | धीरे-धीरे गायब होता है, सिकुड़कर 90 % हो जाता है। |
popOut | MotionPreset::PopOut | 300 ms | उभरकर जाता है: थोड़ा फूलता है, फिर सिकुड़कर गायब हो जाता है। |
slideOutDown | MotionPreset::SlideOutDown | 300 ms | नीचे सरककर दृश्य से बाहर जाता है, बिना fade के। |
slideOutUp | MotionPreset::SlideOutUp | 300 ms | ऊपर सरककर दृश्य से बाहर जाता है, बिना fade के। |
Attention: ये विश्राम अवस्था में शुरू और खत्म होते हैं।
| TSX | Rust | अवधि | विवरण |
|---|---|---|---|
shake | MotionPreset::Shake | 500 ms | बगल में हिलता है: गलत पासवर्ड, अमान्य फ़ील्ड। |
headShake | MotionPreset::HeadShake | 800 ms | सिर हिलाता है: एक नरम “नहीं”। |
wiggle | MotionPreset::Wiggle | 600 ms | अपने केंद्र के सापेक्ष डोलता है: कुछ ध्यान देने लायक। |
swing | MotionPreset::Swing | 900 ms | अपने ऊपरी किनारे से साइन की तरह झूलता है। |
pulse | MotionPreset::Pulse | 500 ms | थोड़ा बढ़कर वापस आता है: हल्का ज़ोर। |
heartbeat | MotionPreset::Heartbeat | 900 ms | दिल की तरह दो बार धड़कता है: लाइक, पसंदीदा। |
rubberBand | MotionPreset::RubberBand | 800 ms | रबर की तरह दबता और खिंचता है। |
jello | MotionPreset::Jello | 800 ms | जेली की तरह थरथराता है। |
tada | MotionPreset::Tada | 900 ms | सिकुड़ता है, फिर डोलते हुए बढ़ता है: एक इनाम। |
jump | MotionPreset::Jump | 700 ms | कूदता है और दबकर उतरता है। |
flash | MotionPreset::Flash | 800 ms | दो बार बुझता और जलता है। |
Loop: ये रोके जाने तक दोहराते रहते हैं।
| TSX | Rust | अवधि | विवरण |
|---|---|---|---|
spin | MotionPreset::Spin | 1000 ms | एक बार घूमता है, हमेशा। |
float | MotionPreset::Float | 1500 ms | ऊपर-नीचे तैरता है, हमेशा। |
breathe | MotionPreset::Breathe | 1800 ms | साँस लेता है: थोड़ा बढ़ता और फीका पड़ता है, हमेशा। |
sway | MotionPreset::Sway | 1600 ms | अपने केंद्र के सापेक्ष झूमता है, हमेशा। |
bob | MotionPreset::Bob | 1000 ms | उछलती गेंद की तरह ऊपर-नीचे होता है, हमेशा। |
glow | MotionPreset::Glow | 2000 ms | चमकता और मंद होता है, हमेशा। |
play किसी preset के ऊपर उसके timing विकल्प लेता है (m.play("shake", { duration: 300 })), और preset को अपने motion में spread किया जा सकता है (m.play({ ...motions.fadeInUp, duration: 600 }))। Rust में preset का spec बदलें: MotionPreset::FadeInUp.spec().timing(|t| t.delay(100.0))।
#अपने खुद के motions
अपना motion हर channel के लिए keyframes और एक timing से बनता है (keyframes के विकल्प)। TSX में हर channel के keyframes लिखें ({ opacity: [0, 1] }), या frames की एक सूची जैसी Web Animations API में लिखी जाती है ([{ opacity: 0, translateY: 24 }, { opacity: 1, translateY: 0 }]), जिसमें हर frame के पास अपना offset और easing हो। frames की सूची में, बिना offset वाला frame वहीं बैठता है जहाँ सूची में उसकी स्थिति उसे रखती है, और कोई channel उन्हीं frames का अनुसरण करता है जो उसका नाम लेते हैं। कोण strings हो सकते हैं ("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);// Channels' keyframes.
card.play({
duration: 400,
keyframes: { opacity: [0, 1], translateY: [{ value: 24, easing: "easeOut" }, 0] },
});
// A list of frames.
card.play({
duration: 400,
keyframes: [
{ opacity: 0, translateY: 24, easing: "easeOut" },
{ opacity: 1, translateY: 0 },
],
});
card.play({ duration: 600, iterations: "infinite", keyframes: { rotateY: ["0deg", "180deg", "360deg"] } });#Entrances और staggered सूचियाँ
हर पंक्ति को एक ऐसा motion दें जो अंदर आता है, और ऐसी delay के साथ जो उसके index के साथ बढ़ती है: जो पंक्तियाँ एक साथ दिखती हैं वे एक के बाद एक आती हैं। delay की एक सीमा रखें, ताकि सूची में बहुत नीचे जोड़ी गई पंक्ति को इंतज़ार न करना पड़े।
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))
})function Row(props: { title: string; index: number }) {
const row = createMotion({ enter: "fadeInUp", delay: Math.min(props.index, 10) * 60 });
return (
<View class="rounded-xl bg-zinc-100 p-4" style={row.style}>
<Text>{props.title}</Text>
</View>
);
}
<Each of={items()} key={(item) => item.id}>
{(item, index) => <Row title={item().title} index={index()} />}
</Each>Motions आपस में जुड़ भी सकते हैं: m.animation(..) एक motion है composite animation के रूप में, बिना शुरू किया हुआ, इसलिए Animated.stagger उनके एक समूह को native रूप से चलाता है, गिनती पहले item से, आख़िरी से, केंद्र से या किसी भी index से। उन्हें एक छिपा हुआ initial दें (या उन्हें वहीं से शुरू करें जहाँ motion शुरू होता है) ताकि अपनी बारी से पहले कुछ न दिखे।
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();const cards = [0, 1, 2, 3, 4].map(() => createMotion({ initial: { opacity: 0 } }));
Animated.stagger(60, cards.map((m) => m.animation("fadeInUp")), { from: "center" }).start();#Exits
exit चलाएँ, उसके खत्म होने की प्रतीक्षा करें, फिर view को हटाएँ। layout animation आसपास के views को खाली जगह भरने देता है।
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));
});
};const remove = async () => {
await row.play("fadeOutLeft");
LayoutAnimation.easeInEaseOut();
items.set(items().filter((i) => i.id !== id));
};Motions उपयोगकर्ता की reduced motion सेटिंग का अपने आप पालन करते हैं: जब उपयोगकर्ता कम motion माँगता है, तो motion बिना हिले तुरंत अपने अंतिम स्थान पर पहुँच जाता है।
#Transitions
बताएँ कि क्या एनिमेट करना है और कितनी देर के लिए (Rust में .transition_all().duration(400.0), TSX में transitionProperty और transitionDuration), और उन properties में बाद के बदलाव native रूप से स्क्रीन पर मौजूद मान से नए मान तक एनिमेट होंगे।
#[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(),
)
}import { mount, signal, View } from "nsh";
function App() {
const on = signal(false);
return (
<View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
<View
onPress={() => on.set(!on())}
style={{
width: 100,
height: 100,
borderRadius: on() ? 50 : 16,
backgroundColor: on() ? "#8250df" : "#0969da",
rotate: on() ? "45deg" : "0deg",
scale: on() ? 1.2 : 1,
transitionProperty: "all",
transitionDuration: 400,
transitionTimingFunction: "ease-in-out",
}}
/>
</View>
);
}
mount(App);| Style | 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 time। |
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(…)" | वक्र। |
जो properties transition करती हैं, समूह के अनुसार: रंग (background, border, outline, shadow और text के रंग, gradient stops), opacity, transform, shadows (box-shadow) और आकृति (border radius और width)। CSS नाम और उनके camel-case रूप दोनों काम करते हैं (background-color, backgroundColor, transform, scale, …)। Layout बदलाव layout animations से एनिमेट होते हैं।
Tailwind के साथ वही चीज़ है transition (रंग, opacity, shadows और transforms), transition-all, transition-colors, transition-opacity, transition-transform, transition-shadow, साथ में duration-*, delay-* और ease-*। pressed state (.active(..), active:) के साथ मिलाने पर यह ऐसे pressed states देता है जो एनिमेट होते हैं:
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()))<View onPress={save} class="rounded-xl bg-blue-600 px-5 py-3 transition duration-150 active:scale-95 active:bg-blue-800">
<Text class="font-semibold text-white">Save</Text>
</View>Transition उस मान से शुरू होता है जो इस समय स्क्रीन पर है, इसलिए बीच में लक्ष्य फिर बदलने पर वह सहजता से मुड़ जाता है।
#Keyframe animations
चार अंतर्निहित keyframe animations native रूप से चलते हैं, डिफ़ॉल्ट रूप से हमेशा: spin (दक्षिणावर्त एक चक्कर), ping (2× तक बढ़ता है और फीका पड़ जाता है), pulse (आधी opacity तक फीका होकर वापस आता है) और 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()<View class="size-8 rounded-full border-4 border-zinc-300 border-t-blue-600 animate-spin" />
<View class="h-4 w-40 rounded bg-zinc-200 animate-pulse" />| Style | Rust | प्रकार | विवरण |
|---|---|---|---|
animationName | .animate_spin(), .animate_ping(), .animate_pulse(), .animate_bounce(), .animate_none(), या .animation(KeyframeAnimation::new(Keyframes::Spin)) | "none" | "spin" | "ping" | "pulse" | "bounce" | एनिमेशन। |
animationDuration | KeyframeAnimation::duration(ms) | number | "1s" | … | एक iteration। |
animationDelay | KeyframeAnimation::delay(ms) | number | "200ms" | … | शुरू होने से पहले। |
animationTimingFunction | KeyframeAnimation::easing([x1, y1, x2, y2]) | transitionTimingFunction की तरह | वक्र। |
animationIterationCount | KeyframeAnimation::iterations(n) | number | "infinite" | डिफ़ॉल्ट "infinite"। |
Tailwind: animate-spin, animate-ping, animate-pulse, animate-bounce, animate-none।
#Animated values
Animated value एक संख्या है जो UI thread पर रहती है। उसे बनाएँ (Rust में animated_value(..), TSX में nsh से createAnimatedValue), उसे styles से bind करें, और उसे एनिमेट करें; bind किए गए views हर फ़्रेम में बिना अपडेट के उसका अनुसरण करते हैं।
#[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))))
}import { mount, Text, View } from "nsh";
import { createAnimatedValue, Easing } from "nsh";
function App() {
const x = createAnimatedValue(0);
const opacity = createAnimatedValue(1);
return (
<View style={{ flex: 1, justifyContent: "center", padding: 24, gap: 24 }}>
<View style={{ width: 60, height: 60, borderRadius: 12, backgroundColor: "#0969da", opacity, translate: [x, 0] }} />
<View
onPress={async () => {
await x.timing(240, { duration: 600, easing: Easing.out(Easing.cubic) });
await opacity.timing(0.3, { duration: 200 });
x.spring(0);
opacity.timing(1);
}}
>
<Text style={{ color: "#0969da" }}>Play</Text>
</View>
</View>
);
}
mount(App);क्या bind किया जा सकता है: opacity (.bind_opacity(..)); backgroundColor, borderColor (हर तरफ़) और text का color (पूरा text), रंगों तक interpolation के ज़रिए (.bind_background(..), .bind_border_color(..), .bind_color(..)); translate ([x, y], हर axis के लिए एक मान), rotate (डिग्री) और scale (हर axis के लिए [x, y], या दोनों के लिए एक मान); और transform सूची के steps: translateX, translateY, scale, scaleX, scaleY, rotate, rotateX, rotateY और rotateZ (.bind_translate_x(..), .bind_translate_y(..), .bind_scale(..), .bind_rotate(..) डिग्री में, Rust में .bind_transform(kind, ..))। Shadows transitions से बदलते हैं। focus ring या हरे रंग में चमकने वाली कीमत, border या text के रंग से bind किया गया color interpolation है:
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);const flash = createAnimatedValue(0);
const color = flash.interpolate({ inputRange: [0, 1], outputRange: ["#1f2328", "#1a7f37"] });
<Text style={{ color }}>{price()}</Text>
// On a new price: flash.set(1); flash.timing(0, { duration: 600 });Animated values canvas drawings को भी चलाते हैं, scroll offsets (scrollValue) और gestures (pan, pinch, rotation) प्राप्त करते हैं।
मान को उससे bind किए गए views और उसे बनाने वाला component जीवित रखते हैं, इसलिए आपके variables कैसे भी आएँ-जाएँ, binding जुड़ी रहती है। Animated values को उसी component में बनाएँ जो उनका मालिक है (module स्तर पर नहीं, जब हर instance को अपना चाहिए)।
#AnimatedValue मेथड
| TSX | Rust | विवरण |
|---|---|---|
set(value) | set(value) | value पर सीधे पहुँच जाता है और चल रहे किसी भी animation को रोक देता है। |
stop() | stop() | चल रहे animation को जहाँ है वहीं रोक देता है; TSX में यह { value } के साथ resolve होता है, यानी वहाँ के मान के साथ। |
getValue() | value() | वर्तमान मान, जो UI thread पर पढ़ा जाता है जहाँ वह बदलता है (यह एक फ़्रेम के भीतर स्थिर हो जाता है): TSX में promise, Rust में await किया जा सकने वाला उत्तर। |
setOffset(offset) | set_offset(offset) | मान जो दिखाता है उसमें offset जोड़ता है: bindings को value + offset दिखता है, जबकि set, animations और drags मान को ही बदलते हैं। |
flattenOffset() | flatten_offset() | offset को मान में जोड़ देता है और offset को 0 कर देता है। |
extractOffset() | extract_offset() | मान को offset में ले जाता है और मान को 0 कर देता है: इसके बाद drag 0 से शुरू होता है, पिछले drag ने जहाँ छोड़ा था उसके ऊपर। |
timing(to, { duration?, easing?, delay? }) | timing(to, ms, easing), timing_with(to, Timing { .. }) | duration ms में to तक animate करता है (डिफ़ॉल्ट 300, easing "easeInOut")। |
spring(to, { stiffness?, damping?, mass?, velocity? }) | spring(to, Spring { .. }) | spring की तरह to तक पहुँचता है (डिफ़ॉल्ट: stiffness 170, damping 26, mass 1)। |
decay({ velocity?, deceleration?, clamp?, bounce? }) | decay(velocity, deceleration), decay_within(velocity, deceleration, (min, max), bounce) | velocity (इकाई प्रति सेकंड) की गति से चलना शुरू करता है और धीमा होता जाता है (डिफ़ॉल्ट deceleration 0.998 प्रति ms)। [min, max] के clamp के साथ यह किसी सीमा पर रुक जाता है, या अपनी गति का bounce (0..1) हिस्सा बनाए रखते हुए उससे वापस लौट आता है। |
हर animation एक उत्तर देता है जिसे आप await कर सकते हैं, कि वह पूरा हुआ या नहीं: true जब वह अंत तक चला, false जब किसी ने उसे रोक दिया (stop(), set(), या उसी मान पर नया animation)। यह TSX में Promise<{ finished: boolean }> है और Rust में AnimationEnd, जिसे कोई task bool के लिए await करता है। चलते हुए मान पर नया animation शुरू करने से पुराना animation सहजता से बाधित होता है: velocity के बिना दिया गया spring या decay मान की मौजूदा velocity से जारी रहता है।
// 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;// Spring settings: lower damping overshoots more, higher stiffness is faster.
x.spring(200, { stiffness: 180, damping: 8 }); // wobbly
x.spring(200, { stiffness: 400, damping: 30 }); // stiff, no overshoot
// A fling that coasts to a stop, from a pan's release velocity.
<View pan={{ x }} onPanEnd={(e) => x.decay({ velocity: e.velocity.x })} />
// A fling that stays on its track and bounces softly off its ends.
x.decay({ velocity: e.velocity.x, clamp: [0, 300], bounce: 0.3 });
// Where a value is now (it moves on the UI thread, so the answer is async).
const where = await x.getValue();#Keyframes
keyframes किसी मान को keyframes के ज़रिए वैसे ही चलाता है जैसे CSS keyframes चलते हैं: TSX में value.keyframes(frames, options), Rust में value.keyframes(frames, KeyframeTiming) (composite के एक step के रूप में Animated.keyframes(value, { keyframes, ... }) और Animation::keyframes(..))। keyframe एक संख्या है (या "90deg" जैसी angle string), जो बाकी के साथ समान दूरी पर फैलती है, या { offset, value, easing } (Rust में Keyframe::new(value), Keyframe::at(offset, value), .ease(easing))।
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(),
);const y = createAnimatedValue(0);
y.keyframes([0, { offset: 0.3, value: -20, easing: "easeIn" }, 0], { duration: 600, easing: "easeOut", iterations: 3 });
// A clock hand that ticks once a second.
const hand = createAnimatedValue(0);
hand.keyframes([0, 360], { duration: 60_000, easing: Easing.steps(60), iterations: "infinite" });| विकल्प | Rust (KeyframeTiming) | डिफ़ॉल्ट | विवरण |
|---|---|---|---|
duration | KeyframeTiming::new(ms) | 300 | एक iteration, मिलीसेकंड में। |
delay | .delay(ms) | 0 | पहले iteration से पहले। |
easing | .easing(easing) | CSS ease | हर उस segment का curve जिसका अपना easing नहीं है। |
iterations | .iterations(Some(n)), .infinite() | 1 | कितने iterations, या "infinite"। |
direction | .direction(PlaybackDirection::Alternate) | "normal" | "normal", "reverse", "alternate" या "alternate-reverse"। |
ये CSS और Web Animations API की तरह चलते हैं:
- easing हर segment को आकार देता है, पूरे को नहीं: keyframe का
easingउस segment को आकार देता है जो उससे शुरू होता है, और timing काeasingबाकी हर segment को। - बिना offset वाले keyframes अपने पड़ोसियों के बीच समान दूरी पर फैलते हैं; पहले का डिफ़ॉल्ट 0 और आख़िरी का 1 है।
- हर iteration अपने पहले keyframe पर छलांग के साथ शुरू होता है। offset 0 पर keyframe न हो तो मान वहीं से चलता है जहाँ वह है।
delayके दौरान मान पहले से पहला keyframe धारण किए रहता है (CSSanimation-fill-mode: backwards), इसलिए देर से शुरू होने वाली entrance विश्राम अवस्था वाले view के बजाय अपना पहला फ़्रेम दिखाती है। यह motions पर भी लागू होता है।reverseऔरalternateके पीछे की ओर वाले iterations segments को उलटे क्रम में चलाते हैं, हर एक का curve उलटकर।- मान वहीं रहता है जहाँ आख़िरी iteration उसे छोड़ता है। उत्तर बताता है कि वे पूरे हुए या नहीं: पूरे होने पर
true, किसी के रोकने परfalse।
#Interpolation
interpolate किसी मान को piecewise linear segments के ज़रिए संख्याओं, रंगों या कोणों में map करता है, और एक नया node लौटाता है जिसे आप bind कर सकते हैं। एक मान कई properties चला सकता है। Rust में interpolate(&input, &output) संख्याओं में, interpolate_colors(&input, &colors) रंगों में map करता है, और interpolate_with(&input, output, Interpolation { .. }) extrapolation और easing लेता है; 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()
},
))const t = createAnimatedValue(0);
<View
style={{
backgroundColor: t.interpolate({ inputRange: [0, 1, 2], outputRange: ["#54aeff", "#8250df", "#cf222e"] }),
opacity: t.interpolate({ inputRange: [0, 1], outputRange: [0.4, 1] }),
rotate: t.interpolate({ inputRange: [0, 1], outputRange: ["0deg", "180deg"] }),
scale: t.interpolate({ inputRange: [0, 1], outputRange: [0.6, 1.2], easing: Easing.out(Easing.back()) }),
}}
/>| विकल्प | Rust | विवरण |
|---|---|---|
inputRange | input slice | बढ़ते क्रम में input stops। |
outputRange | output slice, या AnimationOutput | हर stop के लिए एक output: संख्याएँ, रंग ("#rrggbb", "rgba(…)", नामित) या कोण ("45deg", "1rad")। |
extrapolate | Interpolation::extrapolate(Extrapolate::Extend) | input सीमा के बाहर: "clamp" (डिफ़ॉल्ट), "extend" (अंतिम segments को जारी रखता है) या "identity" (input को ज्यों का त्यों पास करता है)। |
extrapolateLeft, extrapolateRight | extrapolate_left, extrapolate_right | वही, एक तरफ़ के लिए। |
easing | easing | हर segment को आकार देता है। |
#अंकगणित और diffClamp
Nodes अंकगणित से भी जुड़ सकते हैं, और यह भी natively चलता है: a.add(b), a.sub(b), a.mul(b), a.div(b), a.mod(b) (Rust में a.modulo(b); धनात्मक b के लिए कभी ऋणात्मक नहीं, जैसे 0..360 में कोण), जहाँ b एक node या संख्या है। TSX में Animated.add, subtract, multiply, divide और modulo functions के रूप में यही काम करते हैं।
diffClamp(min, max) (diff_clamp) node के बदलावों का अनुसरण करता है और min..max के भीतर रहता है। scroll offset पर लगाने पर यह ऐसा header बनाता है जो content के ऊपर scroll होते ही छिप जाता है और नीचे scroll होते ही वापस आ जाता है:
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))const scroll = createAnimatedValue(0);
const header = scroll.diffClamp(0, 56);
<Scroll style={{ flex: 1 }} scrollValue={scroll}>…</Scroll>
<View style={{ position: "absolute", top: 0, left: 0, right: 0, height: 56, translate: [0, header.mul(-1)] }}>…</View>{ snap: true } (diff_clamp_with(min, max, DiffClamp::snapping())) के साथ, scroll रुकने पर आंशिक रूप से दिखता छूटा हुआ header उस गति को पूरा कर देता है: वह UI thread पर 200 ms में पूरी तरह दिखाई देने या पूरी तरह छिपने की स्थिति में आ जाता है, जो भी निकट हो। वह तभी छिपता है जब page कम से कम उसकी ऊँचाई जितना scroll हो चुका हो, इसलिए उससे कम scroll हुए page में पूरा header दिखता है। गति के दौरान फिर से scroll करने पर नियंत्रण वहीं से लिया जाता है जहाँ header है।
let header = offset.diff_clamp_with(0.0, 56.0, DiffClamp::snapping());const header = scroll.diffClamp(0, 56, { snap: true });#संयुक्त (composite) animations
Animations steps से बनते हैं (Rust में Animation, TSX में nsh से Animated) और पूरा tree natively चलता है: हर step UI thread पर उसी क्षण शुरू होता है जब पिछला खत्म होता है, बिना किसी अंतराल के और बीच में आपके किसी code के बिना।
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" });
});import { Animated, createAnimatedValue, Easing } from "nsh";
const ys = [0, 1, 2, 3, 4].map(() => createAnimatedValue(0));
const fade = createAnimatedValue(1);
const hop = Animated.stagger(
80,
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 }),
]),
),
);
const breathe = Animated.loop(
Animated.sequence([
Animated.timing(fade, { toValue: 0.3, duration: 400 }),
Animated.timing(fade, { toValue: 1, duration: 400 }),
]),
);
hop.start(({ finished }) => console.log(finished ? "landed" : "stopped"));| TSX | Rust | विवरण |
|---|---|---|
Animated.timing(value, { toValue, duration?, easing?, delay? }) | Animation::timing(&value, to, Timing { .. }) | timing step। |
Animated.spring(value, { toValue, stiffness?, damping?, mass?, velocity? }) | Animation::spring(&value, to, Spring { .. }) | spring step। |
Animated.decay(value, { velocity?, deceleration?, clamp?, bounce? }) | Animation::decay(&value, velocity, deceleration), Animation::decay_within(..) | decay step। |
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" (बीच से बाहर की ओर लहर) या एक index (StaggerFrom::First, Last, Center, Index(i))। |
Animated.keyframes(value, { keyframes, ...timing }) | Animation::keyframes(&value, frames, KeyframeTiming) | keyframes का एक step। |
Animated.delay(ms) | Animation::delay(ms) | प्रतीक्षा करता है। |
Animated.loop(animation, { iterations?, resetBeforeIteration? }) | .looped(iterations), .reset_before_iteration(..) | दोहराता है (डिफ़ॉल्ट रूप से हमेशा; Rust में None)। |
composite animation में start() होता है (TSX में callback और promise के साथ भी, दोनों को { finished } मिलता है; Rust में await किया जा सकने वाला bool; इसे फिर से कॉल करने पर यह फिर से चलता है), stop() (जहाँ है वहीं रुक जाता है) और reset() (रुक जाता है और अपने मानों को वहाँ लौटा देता है जहाँ वे शुरू होते समय थे)। callback पूरे tree के लिए एक बार चलता है।
चलता हुआ animation अपने मानों को जीवित नहीं रखता: जब उन्हें रखने वाला component हट जाता है (उसके views और variables), तो अनंत loop भी उनके साथ रुक जाता है, और उसका उत्तर होता है कि वह पूरा नहीं हुआ (false)।
#Easing वक्र
Easing curves डेटा हैं, जिनका मूल्यांकन natively होता है: TSX में nsh से Easing, Rust में Easing enum।
| TSX | Rust | विवरण |
|---|---|---|
Easing.linear, Easing.ease | Easing::Linear, Easing::Bezier(0.25, 0.1, 0.25, 1.0) | स्थिर गति; CSS का ease। |
Easing.quad, Easing.cubic, Easing.poly(n) | Curve::Quad, Curve::Cubic, Curve::Poly(n) | t², t³, tⁿ। |
Easing.sin, Easing.circle, Easing.exp | Curve::Sin, Curve::Circle, Curve::Exp | Sinusoidal, वृत्ताकार, घातांकीय। |
Easing.back(s?) | Curve::Back(s) | आगे बढ़ने से पहले पीछे खिंचता है (overshoot s, डिफ़ॉल्ट 1.70158)। |
Easing.elastic(bounciness?) | Curve::Elastic(bounciness) | स्प्रिंग जैसा overshoot। |
Easing.bounce | Curve::Bounce | अंत में उछलता है। |
Easing.bezier(x1, y1, x2, y2) | Easing::Bezier(x1, y1, x2, y2) | CSS की तरह cubic Bézier। |
Easing.steps(count, position?) | Easing::Steps(count, StepPosition::JumpEnd) | सरकने के बजाय count स्तरों के बीच छलांग लगाता है, जैसे CSS steps(): फ़्रेम-दर-फ़्रेम sprite, टिक-टिक करती सुई। 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 | ये curves “in” curves हैं; out और inOut उन्हें पलट देते हैं। |
TSX में जहाँ भी easing स्वीकार किया जाता है, वहाँ आप नाम ("linear", "ease", "easeIn", "easeOut", "easeInOut") या Bézier array [x1, y1, x2, y2] भी दे सकते हैं; Rust में Easing::EaseIn, Easing::EaseOut और Easing::EaseInOut CSS के curves हैं।
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));x.timing(1, { easing: Easing.out(Easing.back()) });
x.timing(1, { easing: "easeOut" });
x.timing(1, { easing: [0.2, 0.8, 0.2, 1] });Easing.sample(easing, count) (Rust में sample_easing) 0 से 1 तक count समान दूरी वाले बिंदुओं पर curve लौटाता है, और Animated.sampleSpring(config, fps?) / Animated.sampleDecay(config, fps?) (sample_spring, sample_decay) वे मान लौटाते हैं जिनसे होकर spring या decay गुज़रता है, हर फ़्रेम पर एक (डिफ़ॉल्ट 60 प्रति सेकंड), config.from (0) से उसके खत्म होने तक। इनकी गणना उसी native code से होती है जिससे animations चलते हैं, इसलिए curve का chart ठीक वही दिखाता है जो animation करता है:
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);const curve = Easing.sample(Easing.out(Easing.back()), 50); // 50 progress values
const spring = Animated.sampleSpring({ toValue: 1, stiffness: 180, damping: 8 });#LayoutAnimation
layout animation अगले update के फ़्रेम बदलावों को natively animate करता है: जो views हिलते या आकार बदलते हैं वे अपने नए फ़्रेम तक सरकते हैं, नए views fade (या scale) होकर आते हैं, हटाए गए views fade होकर चले जाते हैं। इसे state बदलने से ठीक पहले कॉल करें: Rust में animate_layout(..), TSX में LayoutAnimation.configureNext(config)।
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);
});
};import { signal } from "nsh";
import { LayoutAnimation } from "nsh";
const items = signal([1, 2, 3]);
let next = 4;
const add = () => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.spring);
items.set([next++, ...items()]);
};
const remove = () => {
LayoutAnimation.easeInEaseOut();
items.set(items().slice(1));
};| TSX | Rust | विवरण |
|---|---|---|
configureNext(config, onEnd?) | animate_layout(config) | अगले commit के बदलावों को animate करता है; TSX में पूरा होने पर onEnd चलता है। |
create(duration, type?, property?) | LayoutAnimation::all(LayoutTransition::new(curve, ms)) | तीनों चरणों के लिए एक ही curve वाला config। |
Presets.easeInEaseOut, Presets.linear, Presets.spring | LayoutAnimation::EASE_IN_EASE_OUT, LINEAR, SPRING | तैयार configs। |
easeInEaseOut(onEnd?), linear(onEnd?), spring(onEnd?) | preset के साथ animate_layout | preset के साथ configureNext। |
Types, Properties | LayoutCurve, LayoutProperty | नाम, configs बनाने के लिए। |
config है { duration, create?, update?, delete? } (Rust में LayoutAnimation { create, update, delete }): जोड़े गए views के लिए create, हिलाए या आकार बदले गए views के लिए update, हटाए गए views के लिए delete। हर चरण { type?, property?, duration?, delay?, springDamping? } (LayoutTransition { curve, property, duration_ms, delay_ms }) है, जहाँ type है "easeInEaseOut", "spring", "linear", "easeIn", "easeOut" या "keyboard" (software keyboard का अपना curve, जो keyboard के हिलना शुरू करने के समय से गिना जाता है, ताकि जो उसके लिए जगह बनाता है वह उसके साथ चले; LayoutCurve::Keyboard), property वह है जिसे प्रकट और गायब होते views animate करते हैं ("opacity", "scaleXY", "scale"), और springDamping (0..1, डिफ़ॉल्ट 0.7; LayoutCurve::Spring { damping }) spring का overshoot तय करता है।
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)
}),
});LayoutAnimation.configureNext({
duration: 300,
create: { type: "easeOut", property: "opacity" },
update: { type: "spring", springDamping: 0.8 },
delete: { type: "easeIn", property: "scaleXY" },
});layout animation स्क्रीन पर कहीं भी, update द्वारा बदले गए हर फ़्रेम को कवर करता है: जो list refresh होती है या जो live data उसी update में आता है, वह भी साथ में चलता है। Rust component में, केवल उसे animate करें जो किसी एक element के बदलाव से हिलता है, ElementRef::animate_layout(transition) के साथ: element का अपना फ़्रेम और उसके content का, और उन views के फ़्रेम जिन्हें उसका आकार आगे धकेलता है (उसके siblings और उनके, स्क्रीन तक)। update के बाकी फ़्रेम तुरंत बदल जाते हैं। nsh/base के Collapsible और AccordionPanel इसी तरह अपना content प्रकट करते हैं।
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(®ion)
.overflow_hidden()
.style_with(move |s| if open.get() { s } else { s.h(px(0.0)) })
#क्या कहाँ चलता है
| कहाँ चलता है | प्रति फ़्रेम आपका code | |
|---|---|---|
| Transitions, keyframe animations | UI thread | कोई नहीं |
Motions, keyframes, timing / spring / decay, composites | UI thread | कोई नहीं; आपके code को उनके खत्म होने की सूचना मिलती है |
interpolate, अंकगणित, diffClamp | UI thread | कोई नहीं |
scrollValue, pan / pinch / rotation | UI thread | कोई नहीं; आपके code को शुरू और अंत की सूचना मिलती है |
| Layout animations | UI thread | कोई नहीं |
requestAnimationFrame loops (Rust में request_animation_frame), onScroll handlers, PanResponder | आपका code, फिर एक update | हर फ़्रेम |
requestAnimationFrame (TSX में एक global, जो display का अनुसरण करता है) का सहारा केवल उसी के लिए लें जिसे पहले से वर्णित नहीं किया जा सकता, जैसे आपके code में physics simulation। जो कुछ भी animated value हो सकता है, उसे animated value ही होना चाहिए।
#कम गति (Reduced motion)
जब user कम गति माँगता है (iOS में “Reduce Motion”, Android में “Remove animations”), तो Rust में reduce_motion() और TSX में nsh से reduceMotion() true होते हैं, और motion-reduce: / motion-safe: Tailwind variants लागू होते हैं।
Motions इस सेटिंग का अपने आप पालन करते हैं: play और enter बिना हिले तुरंत वहीं पहुँच जाते हैं जहाँ motion खत्म होता, और उत्तर यह होता है कि वह पूरा हुआ (true), ठीक वैसे ही जैसे चले हुए motion के लिए। exit फिर भी छिपे हुए पर खत्म होता है, entrance फिर भी विश्राम अवस्था में खत्म होता है। ऐसे motion के लिए जिसमें user के लिए ज़रूरी अर्थ है, जैसे progress spinner, ignoreReduceMotion: true (Rust में play_ignoring_reduce_motion) दें।
Transitions, keyframe animations, animated values, keyframes और composites जैसा लिखा है वैसा ही चलते हैं; इनका उपयोग करके कोई शांत विकल्प चुनें:
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())import { View } from "nsh";
import { reduceMotion } from "nsh";
const open = () => (reduceMotion() ? panel.set(0) : panel.spring(0));
<View class="motion-safe:animate-bounce" />देखें Accessibility।
#संबंधित
nshका animation API: TSX संदर्भ।- Touch और gestures: उँगली से animated values को चलाना।
- Scrolling:
scrollValue। - Canvas: ड्रॉइंग में animated values।
- Style reference:
transition*औरanimation*keys।