Skip to content

Commit

Permalink
Added BurstAndMoveEmitter
Browse files Browse the repository at this point in the history
  • Loading branch information
nanndoj committed Aug 19, 2018
1 parent 3210235 commit a58008c
Show file tree
Hide file tree
Showing 7 changed files with 6,138 additions and 0 deletions.
67 changes: 67 additions & 0 deletions .flowconfig
@@ -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
216 changes: 216 additions & 0 deletions BaseEmitter.js
@@ -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;
110 changes: 110 additions & 0 deletions BurstAndMoveEmitter.js
@@ -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();
}
}

0 comments on commit a58008c

Please sign in to comment.