UNPKG

react-native-animation-scenario

Version:
450 lines (352 loc) โ€ข 13.2 kB
# ๐ŸŽฌ react-native-animation-scenario A lightweight, declarative animation timeline hook for React Native โ€” perfect for scenarios like onboarding, tutorials, interactive demos, and more. Easily control complex UI animations step-by-step or automatically. Works with [React Native](https://reactnative.dev/) and [Expo](https://expo.dev). --- ## โœจ Features - โœ… Declarative animation timeline with step-by-step control - ๐Ÿง  Supports `set`, `move`, `delay`, `parallel`, `callback`, `vibrate`, `hold`, `label`, `goto`, `resume`, `use`, `ifJump`, `ifThen` and `use` steps - ๐ŸŽ› Works in both `"auto"` and `"manual"` step modes - ๐Ÿ” Loopable scenarios - ๐Ÿ“ฆ Minimal dependencies โ€“ just React Native + Animated - ๐Ÿงฉ Drop-in `TimelineView` for debug or dev overlay - ๐Ÿ” Safe callback and ref validation --- ## โšก Try it live [Snack demo on Expo](https://snack.expo.dev/@cadam68/animation-demo) --- ## ๐Ÿ”ฅ What's New in 1.4.3 - โœ… documentation added. ## ๐Ÿ”ฅ What's New in 1.4.2 `Patch Release` - ๐Ÿ›  Fixed an issue where the `stop() step didnโ€™t always correctly interrupt the scenario execution. - โœ… Improved overall code structure and performance with internal refactoring. ## ๐Ÿ”ฅ What's New in 1.4.0 - โœ… `ifThen`, `ifElse`, `ifEnd`: New block-style conditional logic, familiar to developers. Example: ``` ifThen(() => score.current > 5), move("x", 100, 500), ifElse(), move("x", -100, 500), ifEnd() ``` - โœ… Nested conditionals are supported and validated at compile time. - โœ… Improved compiler validation to detect malformed blocks or unclosed conditionals. - โœ… Relative values in `move()` using helper functions like `inc(value)` or `dec(value)`. Example: ``` move("x", inc(50), 500) ``` - โœ… Arguments in `callback()` steps now supported. - โœ… Jump to label in `nextStep(label)`: You can resume or redirect flow from UI or interaction code. ## ๐Ÿ”ฅ What's New in 1.3.0 - โœ… `resume` step: jump back to the point after the last `goto()` call - โœ… `set` step: set a value directly or from a function - โœ… `stop` step: stop and reset the animation - โœ… `ifJump(condition, labelTrue, labelFalse?)` step: jump based on condition - ๐Ÿ›ก Label validation for `goto` and `ifJump` steps now included in scenario compilation - โš™๏ธ Cleaned and centralized all validation logic in `compileScenario()` ## ๐Ÿ”ฅ What's New in 1.2.0 - โœ… `goto("label")` step: jump to a specific label in your scenario - โœ… `label("name")` step: define named labels - โœ… Nested reusable blocks with `use("blockName")` - โœ… Full `reset()` and `stop()` logic - โœ… `TimelineView` for visual debug - โœ… `useScreenLifecycle(onFocus, onBlur)` helper --- ## ๐Ÿ“ฆ Installation ``` npm install react-native-animation-scenario ``` --- ## ๐Ÿ“ฆ Exports ``` useAnimationScenario(); // Main hook defineScenario([]); // Safely define your scenario move(), delay(), callback(), ... // Step helpers useScreenLifecycle(); // Hook for screen focus lifecycle ``` --- ## ๐Ÿ›  Usage ```js import { useAnimationScenario, defineScenario, move, delay, parallel, callback, hold, } from "react-native-animation-scenario"; const scenario = defineScenario([ move("opacity", 1, 500), delay(300), parallel([ move("scale", 1.1, 500), move("translateY", 50, 500), ]), hold("waitForTap"), callback("onFinish"), ]); const MyComponent = () => { const { refs, start, reset, nextStep, TimelineView, } = useAnimationScenario({ scenario, initialValues: { opacity: 0, scale: 1, translateY: 0, }, callbacks: { onFinish: () => console.log("Done!"), }, }); useEffect(() => { start(); }, []); return ( <Animated.View style={{ opacity: refs.opacity, transform: [ { scale: refs.scale }, { translateY: refs.translateY } ] }}> <Text>Hello Animation</Text> <TimelineView /> </Animated.View> ); }; ``` --- ## ๐Ÿ”ง Step Types | Type | Description | Version | |------------|----------------------------------------------|---------| | `move` | Animate a ref value to a target | 1.0 | | `delay` | Pause for a given duration | 1.0 | | `parallel` | Animate multiple `values` simultaneously | 1.0 | | `callback` | Run external logic (sync or async) | 1.0 | | `vibrate` | Trigger haptic feedback | 1.0 | | `hold` | Pause animation until `nextStep()` is called | 1.0 | | `label` | Mark jump targets | 1.2 | | `goto` | Jump to a label | 1.2 | | `use` | Insert a block | 1.2 | | `set` | Set a value directly or from a function | 1.3 | | `stop` | Stop and reset the animation | 1.3 | | `resume` | Continue from the point after last `goto()` | 1.3 | | `ifJump` | Conditionally jump to a label | 1.3 | | `ifThen` | Block-style conditional logic | 1.4 | --- ## ๐Ÿง  Modes - mode: "auto" โ€“ steps play sequentially - mode: "manual" โ€“ steps only run via nextStep() - loop: true โ€“ repeat the scenario forever --- ## ๐Ÿงฉ Reusable Blocks ```js const blocks = { bounce: defineScenario([ move("y", -30, 200), move("y", 0, 200), ]), }; const scenario = defineScenario([ callback("showStart"), use("bounce"), delay(500), use("bounce"), ]); ``` --- ## ๐Ÿค– Conditional Branching with `ifJump()` Use `ifJump()` to dynamically choose the next step at runtime. ```js const MyComponent = () => { const directionRef = useRef(1); const scenario = defineScenario([ hold(), // Wait for user input before branching ifJump(() => directionRef.current === 1, "moveRight", "moveLeft"), label("moveRight"), move("x", 200, 500), goto("resumePoint"), label("moveLeft"), move("x", 0, 500), label("resumePoint"), ]); const { refs, start, nextStep } = useAnimationScenario({ scenario, initialValues: { x: 100 }, }); useEffect(() => { start(); }, []); return ( <View> <Animated.View style={{ width: 50, height: 50, backgroundColor: "blue", transform: [{ translateX: refs.x }], marginBottom: 20, }} /> <Text>Choose Direction:</Text> <View style={{ flexDirection: "row", gap: 10 }}> <Button title="Left" onPress={() => { directionRef.current = 0; nextStep(); }} /> <Button title="Right" onPress={() => { directionRef.current = 1; nextStep(); }} /> </View> </View> ); }; ``` ## ๐Ÿ” `ifJump()` using a named callback You can define reusable branching logic in `callbacks`: ```js const scenario = defineScenario([ hold(), ifJump("checkDirection", "goRight", "goLeft"), label("goRight"), move("x", 100, 300), goto("end"), label("goLeft"), move("x", -100, 300), label("end"), ]); const { refs, nextStep, start } = useAnimationScenario({ scenario, initialValues: { x: 0 }, callbacks: { checkDirection: () => Math.random() > 0.5, }, }); ``` --- ## ๐Ÿงช Advanced Flow Example: `set`, `goto`, `resume`, and `stop` This scenario demonstrates how to use the new set, goto, resume, and stop steps for conditional or non-linear animation flows. ```js import { useAnimationScenario,defineScenario, label, vibrate, move, goto, stop, set, resume } from "react-native-animation-scenario"; const scenario = defineScenario([ label("start"), vibrate(), move("x", 100, 2000), goto("reverse"), // Jump to reverse path move("x", 200, 500, "after-resume"), // Will resume here after `resume` stop(), // Stops the flow here label("reverse"), set("x", 0), // Instantly reset position move("x", -100, 500), resume(), ]); const MyComponent = () => { const {refs,start,TimelineView,} = useAnimationScenario({ scenario, initialValues: { x: 0, direction: 0 }, callbacks: { checkDirection: () => { // Example: dynamically decide to reverse if (Math.random() > 0.5) return goto("reverse"); }, }, }); return ( <Animated.View style={{ transform: [{ translateX: refs.x }] }} > <Button title="Start" onPress={start} /> <TimelineView /> </Animated.View> ); }; ``` --- ## ๐Ÿงช Timeline Debug Add the TimelineView to display step progress: ```js <TimelineView /> <!-- Optional --> ``` Great for development or visual debugging of onboarding flows. --- ## ๐Ÿ›ก Safeguards - ๐Ÿšซ Throws error if an initialValue or callback is missing - ๐Ÿ”’ All animations use Animated.Value and native drivers by default --- ## ๐Ÿงฉ Optional: Screen Lifecycle Hook This helper hook requires `@react-navigation/native` and should be imported separately: ```js import { useScreenLifecycle } from 'react-native-animation-scenario/src/useScreenLifecycle'; ``` It allows you to trigger logic when the screen gains or loses focus. --- ## ๐Ÿ“„ License MIT โ€“ Created by Cyril Adam --- ## ๐Ÿ“ฆ CHANGELOG for `v1.4.2` and `v1.4.1` ### ๐Ÿ”ง Fixes - Fixed a bug where the `stop()` step didn't correctly interrupt scenario execution in all cases. - Improved internal handling of the `shouldStop` flag to prevent race conditions and ensure proper reset behavior. - `reset()` now explicitly clears `holdResolver` and `callingStepIndexRef`, making the scenario restart more predictable and clean. - Minor log improvements under debug mode. ### ๐Ÿ’ก Internal Improvements - Factored out reusable logic into `evalStepValue()` and `evalStepCondition()` to unify async/sync step evaluation across `set`, `ifThen`, and `callback` steps. - Jump logic now centralized via `jumpTo()` utility, ensuring consistent block traversal across `ifThen`, `ifElse`, and `ifEnd`. - Internal state handling made more robust for future scalability and debugging ease. - Scenario validation (`compileScenario`) now checks `parallel` content blocks. ## ๐Ÿ“œ CHANGELOG for `v1.4.0` ### โœจ Added - `ifThen(condition)`, `ifElse()`, `ifEnd()` block structure: familiar conditional logic using nested blocks. - `nextStep(label)`: allows you to jump to a specific label manually, even after a `hold()` step. - `move(...)` now supports relative values using `inc(x)` and `dec(x)`. - Extended support for `callback()` to accept parameters, including dynamic values. - Scenario validation (`compileScenario`) now checks for properly closed `ifThen/ifElse/ifEnd` blocks. - Modular refactoring for better extensibility. ### ๐Ÿž Fixed - Ensured `ifJump()` does not rely on refs directly from `defineScenario()`. - Improved safety and label resolution for nested conditional logic. - Fixed corner cases in `hold()` + jump logic for `nextStep()`. ### ๐Ÿงช Testing - Jest tests added for malformed conditional structures (`ifThen` without `ifEnd`, duplicate `ifElse`, etc.) - Unit tests extended to cover relative moves. ## ๐Ÿ“œ CHANGELOG for `v1.3.1` ### ๐Ÿž Fixed - Move useScreenLifecycle to a separate export to avoid dependency issues on Snack ## ๐Ÿ“œ CHANGELOG for `v1.3.0` ### โœจ Added - `set(target, value)`: set a ref value directly, from a function, or from an async callback - `stop()`: terminates the current animation sequence cleanly - `resume()`: continues execution from the step after the last `goto()` jump - `ifJump(condition, labelTrue, labelFalse?)`: conditional branching based on a sync or async function or a callback name - Scenario validation now checks label existence for `goto` and `ifJump` - Improved label jump tracking with `callingStepIndexRef` for `resume() ### ๐Ÿž Fixed - Ensured `nextStep()` respects the `shouldStop` flag - Cleaned up validation logic (moved to `compileScenario`) - Deduplicated validation errors for clearer debug output ### ๐Ÿงช Testing - New unit test coverage for scenario compilation including nested `use() blocks and label resolution. - Some validation moved from runtime to compile-time, simplifying runtime hook logic. ## ๐Ÿ“œ CHANGELOG for `v1.2.0` ### โœจ Added - `goto("label")` step to jump within the scenario - `label("name")` step to declare jump targets - Support for `use("blockName")` reusable block structure - `__sourceBlock` annotation for debugging block origins - Visual timeline label display (`TimelineView`) - Full `reset()` support restores animated values - `stop()` now halts looping scenarios safely - `useScreenLifecycle(onFocus, onBlur)` hook ### ๐Ÿž Fixed - Scenario would continue running after screen unmount (fixed with `shouldStop`) - `reset()` didn't clear active animation state properly - Label detection and step validation improved ### ๐Ÿงช Testing - Added Jest tests and modular `compileScenario` system - Internal compilation now flattens nested `use()` blocks --- ## ๐Ÿค Contributing - PRs welcome! Especially for: - TypeScript types - New step types (e.g. spring) - Helper tooling