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,
+ },
});