diff --git a/README.MD b/README.MD index db5d861..f078a49 100644 --- a/README.MD +++ b/README.MD @@ -1,12 +1,29 @@ # React Native "Make It Rain" -*Make It Rain* was adapted from Joel Arvidsson's [MakeItRain example](https://github.com/oblador/react-native-animatable/tree/master/Examples/MakeItRain). This is a self-contained component that may be placed in any component. It takes up the entire parent view and is positioned absolutely, overlaying on top of other content in the parent. +### v1.0.0 breaking changes +[Prop names](#props) have been generalized to represent any type of component can be passed in as the falling pieces. +The `moneyComponent` no longer defaults to an SVG. This allowed for removal of the dependency on `react-native-svg`. Refer to the Example project for how to pass in an SVG `moneyComponent`. + + +## Summary + +This is a self-contained component that may be placed in any component. It takes up the entire parent view and is positioned absolutely, overlaying on top of other content in the parent. + +The default animation was adapted from [Shopify's Arrive confetti example](https://engineering.shopify.com/blogs/engineering/building-arrives-confetti-in-react-native-with-reanimated). + +[![Arrive](https://img.youtube.com/vi/Le8HyE4wwJs/0.jpg)](https://www.youtube.com/watch?v=Le8HyE4wwJs) + +The original *Make It Rain* animation was adapted from Joel Arvidsson's [MakeItRain example](https://github.com/oblador/react-native-animatable/tree/master/Examples/MakeItRain). +The `flavor` [prop](#props) selects between them. + + +## Sample Code ```jsx import React, {Component} from 'react'; -import { View } from 'react-native'; +import { View, Text } from 'react-native'; import MakeItRain from 'react-native-make-it-rain'; class Demo extends Component { @@ -15,8 +32,10 @@ class Demo extends Component { Make It Rain 🤍} + itemTintStrength={0.8} /> ); @@ -28,25 +47,44 @@ class Demo extends Component { * `npm install react-native-make-it-rain --save` -## Dependencies -* [react-native-animatable](https://github.com/oblador/react-native-animatable) -* [react-native-svg](https://github.com/react-native-community/react-native-svg) - ## Props | Prop | Description | Type | Default | | ------------------------------ | ---------------------------------------------------- | -------- | ---------- | -| **`numMoneys`** | How much money falls | Integer | 15 | -| **`duration`** | Money flip duration | Integer | 3000 | -| **`moneyComponent`** | Replaces the built-in money component. Can use any React component (Icon, Image, View, etc) | Component | `CashMoney SVG` | -| **`moneyDimensions`** | Size of money. If `moneyComponent` is supplied, size should be set to match | Object | `{ width: 100, height: 50 }` | -| **`wiggleRoom`** | How much the money can move horizontally. | Integer | 50 | -| **`speed`** | How fast the money falls | Integer | 50 | +| **`numItems`** | How many items fall | Integer | 100 | +| **`itemComponent`** | Replaces the built-in item component. Can use any React component (Icon, Image, View, SVG etc) | Component | `` | +| **`itemDimensions`** | Size of item. If `itemComponent` is supplied, size should be set to match. | Object | `{ width: 20, height: 10 }` | +| **`itemColors`** | Colors of falling items. | Array | ['#00e4b2', '#09aec5', '#107ed5'] | +| **`itemTintStrength`** | Opacity of color overlay on item (0 - 1.0) | Number | 1.0 | +| **`flavor`** | The animation behavior ("arrive" or "rain") | String | "arrive" | +| **`fallSpeed`** | How fast the item falls | Integer | 50 | +| **`flipSpeed`** | Item flip speed | Integer | 3 | +| **`horizSpeed`** | How fast the item moves horizontally | Integer | 50 | +| **`wiggleRoom`** | How much the item can move horizontally (only for "rain") | Integer | 50 | +| **`continuous`** | Rain down continuously | Boolean | true | + + +# Sample Application + +If you'd like to see it in action, run these commands: +```sh +cd Example +npm run cp +npm install +npm start +``` + +The sample app is an Expo project created with `create-react-native-app`. + +## Development + +The source files are copied from the project root directory into `Example/react-native-make-it-rain` using `npm run cp`. If a source file is modified, it must be copied over again with `npm run cp`. + +Ideally the Example project could include the parent's source files using package.json, but that causes a babel interopRequireDefault runtime error. ## Credits -Cash SVG was sourced from [IconFinder](https://www.iconfinder.com/icons/1889190/currency_currency_exchange_dollar_euro_exchange_finance_money_icon) and is used under the Creative Commons license. +Cash SVG in the Example project was sourced from [IconFinder](https://www.iconfinder.com/icons/1889190/currency_currency_exchange_dollar_euro_exchange_finance_money_icon) and is used under the Creative Commons license. ## ToDo -* Example project. -* Conditional usage of react-native-svg (so that it's not needed by the consuming app if a custom moneyComponent is supplied). * Tests +* Refactor the original MakeItRain to use hooks and react-native-reanimated. diff --git a/package.json b/package.json index 7d6bbf3..a3c0973 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "react-native-make-it-rain", - "version": "0.9.1", - "main": "makeItRain.js", + "version": "1.0.0", + "main": "src/index.js", "author": "Peace Chen", "license": "MIT", "scripts": { @@ -21,16 +21,16 @@ "react-native-make-it-rain", "Make It Rain", "animatable", + "confetti", "money" ], "devDependencies": { - "react": "*", - "react-native": "*" + "react": ">=16.8.0", + "react-native": ">=0.60.0" }, "dependencies": { "react-native-animatable": "*", - "react-native-svg": "*" + "react-native-reanimated": "^1.8.0" }, - "peerDependencies": { - } + "peerDependencies": {} } diff --git a/src/confetti.js b/src/confetti.js new file mode 100644 index 0000000..c9ddc19 --- /dev/null +++ b/src/confetti.js @@ -0,0 +1,185 @@ +// Inpsired by +// https://engineering.shopify.com/blogs/engineering/building-arrives-confetti-in-react-native-with-reanimated +// https://dev.to/hrastnik/implementing-gravity-and-collision-detection-in-react-native-2hk5 + +import React, { useEffect, useMemo } from 'react' +import Animated from 'react-native-reanimated' +import {View, Dimensions, StyleSheet} from 'react-native' + +const { + View: ReanimatedView, + Clock, + Value, + useCode, + block, + startClock, + stopClock, + set, + add, + sub, + divide, + diff, + multiply, + cond, + clockRunning, + greaterThan, + lessThan, + lessOrEq, + eq, +} = Animated; + +const Confetti = props => { + const [containerDims, setContainerDims] = React.useState(Dimensions.get('screen')); + const [confetti, setConfetti] = React.useState(createConfetti(containerDims)); + const clock = new Clock(); + + useEffect(() => { + return () => { // func indicates unmount + stopClock(clock); + setConfetti([]); + } + }, []); + + // Update confetti positioning if screen changes (e.g. rotation) + const onLayout = (event => { + setContainerDims({ + width: event.nativeEvent.layout.width, + height: event.nativeEvent.layout.height, + }); + }); + + function createConfetti(dimensions) { + return useMemo(() => { + const { width, height } = dimensions; + // Adapt velocity props + const xVelMax = props.horizSpeed * 8; + const yVelMax = props.fallSpeed * 3; + const angleVelMax = props.flipSpeed; + + return [...new Array(props.numItems)].map((_, index) => { + return { + index, + // Spawn confetti from two different sources, a quarter + // from the left and a quarter from the right edge of the screen. + x: new Value( + width * (index % 2 ? 0.25 : 0.75) - props.itemDimensions.width / 2 + ), + y: new Value(-props.itemDimensions.height * 2), + angle: new Value(0), + xVel: new Value(Math.random() * xVelMax - (xVelMax / 2)), + yVel: new Value(Math.random() * yVelMax + yVelMax), + angleVel: new Value((Math.random() * angleVelMax - (angleVelMax / 2)) * Math.PI), + delay: new Value(Math.floor(index / 10) * 0.3), + elasticity: Math.random() * 0.9 + 0.1, + color: props.itemColors[index % props.itemColors.length], + } + }) + return confetti; + }, [dimensions]); + } + + const useDraw = _confetti => { + const nativeCode = useMemo(() => { + const timeDiff = diff(clock); + const nativeCode = _confetti.map(({ + x, + y, + angle, + xVel, + yVel, + angleVel, + color, + elasticity, + delay, + }) => { + const dt = divide(timeDiff, 1000) + const dy = multiply(dt, yVel) + const dx = multiply(dt, xVel) + const dAngle = multiply(dt, angleVel) + + return [ + cond( + lessOrEq(y, containerDims.height + props.itemDimensions.height), + cond( + greaterThan(delay, 0), + [set(delay, sub(delay, dt))], + [ + set(y, add(y, dy)), + set(x, add(x, dx)), + set(angle, add(angle, dAngle)), + ] + ) + ), + cond(greaterThan(x, containerDims.width - props.itemDimensions.width), [ + set(x, containerDims.width - props.itemDimensions.width), + set(xVel, multiply(xVel, -elasticity)), + ]), + cond(lessThan(x, 0), [ + set(x, 0), + set(xVel, multiply(xVel, -elasticity)), + ]), + cond( + eq(props.continuous, true), + cond( + greaterThan(y, containerDims.height + props.itemDimensions.height), + set(y, -props.itemDimensions.height * 2), + ) + ), + ]; + }); + + nativeCode.push(cond(clockRunning(clock), 0, startClock(clock)), clock); + return block(nativeCode); + }, [_confetti]); + + useCode(() => nativeCode, [nativeCode]); + }; + + useDraw(confetti); + + return ( + + {confetti.map( + ({ index, x, y, angle, color: backgroundColor }) => { + return ( + + { props.itemComponent } + + + ) + } + )} + + ) +} + +const styles = StyleSheet.create({ + animContainer: { + position: 'absolute', + top: 0, + left: 0, + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + confettiContainer: { + position: 'absolute', + top: 0, + left: 0, + } +}) + +export default Confetti diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..a74fa47 --- /dev/null +++ b/src/index.js @@ -0,0 +1,29 @@ +import React from 'react'; +import { View } from 'react-native'; +import MakeItRain from './makeItRain'; +import Confetti from './confetti'; + +function Root(props) { + const itemComponent = props.itemComponent || ; + + return ( props.flavor === "arrive" ? + + : + + ); +} + +Root.defaultProps = { + numItems: 100, + itemDimensions: { width: 20, height: 10 }, + itemColors: ['#00e4b2', '#09aec5', '#107ed5'], + itemTintStrength: 1.0, + flavor: "arrive", + fallSpeed: 50, + flipSpeed: 3, + horizSpeed: 50, + wiggleRoom: 50, + continuous: true, +}; + +export default Root; diff --git a/makeItRain.js b/src/makeItRain.js similarity index 56% rename from makeItRain.js rename to src/makeItRain.js index 54b6be3..ce790de 100644 --- a/makeItRain.js +++ b/src/makeItRain.js @@ -1,7 +1,6 @@ import React, {Component} from 'react'; import { View, StyleSheet } from 'react-native'; import * as Animatable from 'react-native-animatable'; -import CashMoney from './assets/cashMoney'; const randomize = max => Math.random() * max; @@ -20,15 +19,31 @@ class MakeItRain extends Component { this.state = { containerWidth: 0, containerHeight: 0, + iterationCount: props.continuous ? "infinite" : 1, + flipDuration: 9000 / props.flipSpeed, + swingDuration: 35000 / props.horizSpeed, + fallDuration: 150000 / props.fallSpeed, }; } - FlippingView = ({ back = false, delay, duration = 1000, moneyComponent, style = {} }) => { - _moneyComponent = moneyComponent; - if (!moneyComponent) { - // Need to size width & height, so create component here with props passed through. - _moneyComponent = + componentDidUpdate(prevProps) { + if (prevProps.flipSpeed !== this.props.flipSpeed || + prevProps.horizSpeed !== this.props.horizSpeed || + prevProps.fallDuration !== this.props.fallDuration || + prevProps.continuous !== this.props.continuous ) + { + this.setState({ + iterationCount: this.props.continuous ? "infinite" : 1, + flipDuration: 9000 / this.props.flipSpeed, + swingDuration: 35000 / this.props.horizSpeed, + fallDuration: 150000 / this.props.fallSpeed, + }); } + } + + FlippingView = ({ back = false, delay, duration = 1000, itemComponent, style = {}, index }) => { + const backgroundColor = this.props.itemColors[index % this.props.itemColors.length]; + return ( - {_moneyComponent} + {itemComponent} + ); } @@ -83,13 +100,13 @@ class MakeItRain extends Component { Falling = ({ duration, delay, style, children }) => ( Math.pow(t, 1.7)} - iterationCount="infinite" + iterationCount={this.state.iterationCount} useNativeDriver style={style} > @@ -105,31 +122,34 @@ class MakeItRain extends Component { }); render() { - let Falling = this.Falling; - let Swinging = this.Swinging; - let FlippingView = this.FlippingView; + const Falling = this.Falling; + const Swinging = this.Swinging; + const FlippingView = this.FlippingView; + return ( - {range(this.props.numMoneys) + {range(this.props.numItems) .map(i => randomize(1000)) .map((flipDelay, i) => ( - - + + @@ -139,14 +159,6 @@ class MakeItRain extends Component { } } -MakeItRain.defaultProps = { - numMoneys: 15, - duration: 3000, - moneyDimensions: { width: 100, height: 50 }, - wiggleRoom: 50, - speed: 50, -} - export default MakeItRain; @@ -158,5 +170,15 @@ let styles = StyleSheet.create( { bottom: 0, left: 0, right: 0 - } + }, + itemContainer: { + alignItems: 'center', + justifyContent: 'center', + overflow: 'hidden', + }, + itemTintContainer: { + position: 'absolute', + top: 0, + left: 0, + }, });