Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
6,138 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,67 @@ | ||
[ignore] | ||
; We fork some components by platform | ||
.*/*[.]android.js | ||
|
||
; Ignore "BUCK" generated dirs | ||
<PROJECT_ROOT>/\.buckd/ | ||
|
||
; Ignore unexpected extra "@providesModule" | ||
.*/node_modules/.*/node_modules/fbjs/.* | ||
|
||
; Ignore duplicate module providers | ||
; For RN Apps installed via npm, "Libraries" folder is inside | ||
; "node_modules/react-native" but in the source repo it is in the root | ||
.*/Libraries/react-native/React.js | ||
|
||
; Ignore polyfills | ||
.*/Libraries/polyfills/.* | ||
|
||
; Ignore metro | ||
.*/node_modules/metro/.* | ||
|
||
[include] | ||
|
||
[libs] | ||
node_modules/react-native/Libraries/react-native/react-native-interface.js | ||
node_modules/react-native/flow/ | ||
node_modules/react-native/flow-github/ | ||
|
||
[options] | ||
emoji=true | ||
|
||
module.system=haste | ||
module.system.haste.use_name_reducers=true | ||
# get basename | ||
module.system.haste.name_reducers='^.*/\([a-zA-Z0-9$_.-]+\.js\(\.flow\)?\)$' -> '\1' | ||
# strip .js or .js.flow suffix | ||
module.system.haste.name_reducers='^\(.*\)\.js\(\.flow\)?$' -> '\1' | ||
# strip .ios suffix | ||
module.system.haste.name_reducers='^\(.*\)\.ios$' -> '\1' | ||
module.system.haste.name_reducers='^\(.*\)\.android$' -> '\1' | ||
module.system.haste.name_reducers='^\(.*\)\.native$' -> '\1' | ||
module.system.haste.paths.blacklist=.*/__tests__/.* | ||
module.system.haste.paths.blacklist=.*/__mocks__/.* | ||
module.system.haste.paths.blacklist=<PROJECT_ROOT>/node_modules/react-native/Libraries/Animated/src/polyfills/.* | ||
module.system.haste.paths.whitelist=<PROJECT_ROOT>/node_modules/react-native/Libraries/.* | ||
|
||
munge_underscores=true | ||
|
||
module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' | ||
|
||
module.file_ext=.js | ||
module.file_ext=.jsx | ||
module.file_ext=.json | ||
module.file_ext=.native.js | ||
|
||
suppress_type=$FlowIssue | ||
suppress_type=$FlowFixMe | ||
suppress_type=$FlowFixMeProps | ||
suppress_type=$FlowFixMeState | ||
|
||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) | ||
suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(<VERSION>\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ | ||
suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy | ||
suppress_comment=\\(.\\|\n\\)*\\$FlowExpectedError | ||
|
||
[version] | ||
^0.75.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,216 @@ | ||
//@flow | ||
import React from 'react'; | ||
import type { Element } from 'react'; | ||
import debounce from 'lodash.debounce'; | ||
import { Vector } from './entities/Vector'; | ||
import AnimatedParticle from './AnimatedParticle'; | ||
import type { VectorType } from './entities/Vector'; | ||
import type { ParticleType } from './entities/Particle'; | ||
import { Dimensions, View, StyleSheet, Animated } from 'react-native'; | ||
|
||
const windowDimensions = Dimensions.get('window'); | ||
|
||
export type BaseEmitterType = { | ||
/** Start emitting particles after initialization */ | ||
autoStart?: boolean, | ||
/** The total of particles to be emitted */ | ||
numberOfParticles: number, | ||
/** Interval between emitting a new batch of particles */ | ||
interval: number, | ||
/** The position from where the particles should be generated */ | ||
fromPosition?: VectorType | (() => VectorType), | ||
/** Number of particles to be be emitted on each cycle */ | ||
emissionRate: number, | ||
/** The particle life time (ms) */ | ||
particleLife: number, | ||
/** Width of the emitter */ | ||
width?: number, | ||
/** Height of the emitter */ | ||
height?: number, | ||
/** Style of the emitter */ | ||
style?: any, | ||
/** The particle content to be rendered */ | ||
children: Element<any>, | ||
/** Reference to the Emiter */ | ||
ref: BaseEmitterType => void, | ||
/** Function to calculate a new bunch of particles */ | ||
onCalculate: () => ParticleConfig[], | ||
/** Function used to animate particles */ | ||
onAnimate: (Animated.Value, Animated.Value) => void | ||
}; | ||
|
||
type BaseEmitterState = { | ||
visibleParticles: ParticleConfig[] | ||
}; | ||
|
||
export type ParticleConfig = { | ||
particle: ParticleType, | ||
path: VectorType[] | ||
}; | ||
|
||
class BaseEmitter extends React.Component<BaseEmitterType, BaseEmitterState> { | ||
// All particles | ||
particles: ParticleConfig[] = []; | ||
// Particles scheduled to be destroyed | ||
particlesToDestroy: number[] = []; | ||
// Number of generated particles | ||
particlesCounter: number = 0; | ||
// Last time a bunch of particles was emitted | ||
lastEmission: number; | ||
// Is emitting particles | ||
isEmitting: boolean = true; | ||
|
||
static defaultProps = { | ||
autoStart: true, | ||
width: windowDimensions.width, | ||
height: windowDimensions.height, | ||
fromPosition: Vector(0, 0) | ||
}; | ||
|
||
constructor(props: BaseEmitterType) { | ||
super(props); | ||
|
||
this.state = { | ||
// List of visible particles | ||
visibleParticles: [] | ||
}; | ||
|
||
(this: any)._loop = debounce(this._loop.bind(this), 100); | ||
} | ||
|
||
render() { | ||
const { particleLife, children, style, onAnimate } = this.props; | ||
const { visibleParticles } = this.state; | ||
|
||
// The job is done | ||
if (!this.isEmitting && !visibleParticles.length) return null; | ||
|
||
const child = React.Children.only(children); | ||
|
||
return ( | ||
<View style={[styles.base, style]}> | ||
{visibleParticles.map((obj, i) => ( | ||
<AnimatedParticle | ||
key={obj.particle.id} | ||
path={obj.path} | ||
lifetime={particleLife} | ||
autoStart={true} | ||
onLifeEnds={this._destroyParticle(obj.particle)} | ||
onAnimate={onAnimate} | ||
> | ||
{child} | ||
</AnimatedParticle> | ||
))} | ||
</View> | ||
); | ||
} | ||
|
||
componentDidMount() { | ||
const { autoStart } = this.props; | ||
autoStart && this.start(); | ||
} | ||
|
||
shouldComponentUpdate(nextProps: BaseEmitterType, nextState: BaseEmitterState) { | ||
return this.state.visibleParticles.length !== nextState.visibleParticles.length; | ||
} | ||
|
||
stopEmitting() { | ||
const { particleLife } = this.props; | ||
this.isEmitting = false; | ||
|
||
// Schedule a final loop for when the last particles are done | ||
setTimeout(this._loop.bind(this), particleLife + 1); | ||
} | ||
|
||
start() { | ||
this.isEmitting = true; | ||
this.particlesCounter = 0; | ||
this.particles = []; | ||
this._loop(); | ||
} | ||
|
||
_loop() { | ||
this._cleanUp(); | ||
this._calculate(); | ||
this._draw(); | ||
this._queue(); | ||
} | ||
|
||
_cleanUp() { | ||
// Remove particles scheduled to be destroyed | ||
this.particles = this.particles.filter(p => !this.particlesToDestroy.includes(p.particle.id)); | ||
this.particlesToDestroy = []; | ||
} | ||
|
||
_calculate() { | ||
const { onCalculate, numberOfParticles, interval } = this.props; | ||
|
||
if (!this.isEmitting) return; | ||
|
||
if (this.particlesCounter >= numberOfParticles) { | ||
// Stop emitting new particles | ||
return this.stopEmitting(); | ||
} | ||
|
||
if (Date.now() - this.lastEmission < interval) return; | ||
|
||
this.lastEmission = Date.now(); | ||
|
||
const newParticles = onCalculate(this._getInitialPosition(), this.particlesCounter); | ||
|
||
// Add the new generated particles | ||
this.particles.push(...newParticles); | ||
this.particlesCounter = this.particles.length; | ||
} | ||
|
||
_draw() { | ||
const { width, height } = this.props; | ||
// Filter the visible particles | ||
this.setState({ | ||
visibleParticles: this.particles | ||
// Remove the particles out of bounds | ||
.filter(p => { | ||
const { x, y } = p.particle.position; | ||
return x >= 0 && x <= width && y >= 0 && y <= height; | ||
}) | ||
}); | ||
} | ||
|
||
_queue() { | ||
if (!this.isEmitting) return; | ||
requestAnimationFrame(() => this._loop()); | ||
} | ||
|
||
_getInitialPosition(): VectorType { | ||
const { fromPosition } = this.props; | ||
|
||
if (!fromPosition) return Vector(0, 0); | ||
|
||
if (typeof fromPosition === 'function') { | ||
return fromPosition(); | ||
} | ||
|
||
if (Object.prototype.toString.apply(fromPosition) === '[object Object]') { | ||
return fromPosition; | ||
} | ||
|
||
return Vector(0, 0); | ||
} | ||
|
||
_destroyParticle = (particle: ParticleType): Function => (): void => { | ||
this.particlesToDestroy.push(particle.id); | ||
if (!this.isEmitting) { | ||
this._loop(); | ||
} | ||
}; | ||
} | ||
|
||
const styles = StyleSheet.create({ | ||
base: { | ||
position: 'absolute', | ||
top: 0, | ||
left: 0 | ||
} | ||
}); | ||
|
||
export default BaseEmitter; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
// @flow | ||
import React, { Component } from 'react'; | ||
import { Animated, Dimensions, Easing } from 'react-native'; | ||
import type { VectorType } from './entities/Vector'; | ||
import { Vector } from './entities/Vector'; | ||
import { fromAngle, toRadians, add } from './utils/vector-helpers'; | ||
import BaseEmitter from './BaseEmitter'; | ||
import { Particle } from './entities/Particle'; | ||
import type { ParticleConfig } from '../../Emitter'; | ||
|
||
const { width, height } = Dimensions.get('window'); | ||
|
||
type IBurstAndMoveEmitter = BaseEmitter & { | ||
finalPoint?: VectorType, | ||
radius: number, | ||
}; | ||
export interface IBurstAndMoveEmitterState { | ||
particles: Array<Vector>[]; | ||
} | ||
|
||
export default class BurstAndMoveEmitter extends Component<IBurstAndMoveEmitter, IBurstAndMoveEmitterState> { | ||
static defaultProps = { | ||
finalPoint: Vector(width, height) | ||
}; | ||
|
||
constructor(props) { | ||
super(props); | ||
|
||
this._calculate = this._calculate.bind(this); | ||
this._animateParticle = this._animateParticle.bind(this); | ||
this._storeEmitterRef = emitter => (this.emitter = emitter); | ||
} | ||
|
||
render() { | ||
const { children, ...props } = this.props; | ||
|
||
return ( | ||
<BaseEmitter | ||
{...props} | ||
onCalculate={this._calculate} | ||
ref={this._storeEmitterRef} | ||
onAnimate={this._animateParticle} | ||
> | ||
{children} | ||
</BaseEmitter> | ||
); | ||
} | ||
|
||
_calculate(initialPosition: VectorType, particlesCounter: number) { | ||
const { numberOfParticles, radius, finalPoint, emissionRate } = this.props; | ||
|
||
const particles: ParticleConfig[] = []; | ||
|
||
const rate = Math.min(numberOfParticles, emissionRate); | ||
|
||
for (let i = 0; i < rate; i++) { | ||
// Generate a random magnitude lower than or equals the radius | ||
const magnitude = Math.round(Math.random() * radius); | ||
|
||
// Generate a random angle between 0 and 360 | ||
const angle = Math.round(Math.random() * 360); | ||
|
||
// Calculate a vector based on the angle and magnitude. | ||
const burstPoint = add(initialPosition, fromAngle(toRadians(angle), magnitude)); | ||
|
||
// first step - Emit new particles | ||
const particle = Particle(Vector(0, 0), Vector(0, 0), particlesCounter + i, initialPosition); | ||
const path = [initialPosition, burstPoint, finalPoint]; | ||
|
||
particles.push({ | ||
particle, | ||
path | ||
}); | ||
} | ||
|
||
return particles; | ||
} | ||
|
||
_animateParticle(path, transformValue, opacityValue) { | ||
const { particleLife } = this.props; | ||
return Animated.parallel([ | ||
Animated.sequence([ | ||
Animated.timing(transformValue, { | ||
toValue: 1, | ||
duration: particleLife * 0.3, | ||
easing: Easing.out(Easing.quad), | ||
useNativeDriver: true | ||
}), | ||
Animated.timing(transformValue, { | ||
toValue: 2, | ||
duration: particleLife * 0.5, | ||
delay: particleLife * 0.2, | ||
easing: Easing.in(Easing.quad), | ||
useNativeDriver: true | ||
}) | ||
]), | ||
Animated.timing(opacityValue, { | ||
toValue: 0, | ||
ease: Easing.inOut(Easing.quad), | ||
delay: particleLife * 0.8, | ||
duration: particleLife * 0.2, | ||
useNativeDriver: true | ||
}) | ||
]); | ||
} | ||
|
||
start() { | ||
this.emitter.start(); | ||
} | ||
} |
Oops, something went wrong.