Skip to content

Commit

Permalink
Merge pull request #8 from AlirezaHadjar/feat/rounded-flakes
Browse files Browse the repository at this point in the history
Feat/rounded flakes
  • Loading branch information
AlirezaHadjar authored Jan 17, 2025
2 parents 2013320 + 9aca6fe commit e5d9930
Show file tree
Hide file tree
Showing 16 changed files with 2,985 additions and 2,805 deletions.
925 changes: 0 additions & 925 deletions .yarn/releases/yarn-4.5.0.cjs

This file was deleted.

934 changes: 934 additions & 0 deletions .yarn/releases/yarn-4.6.0.cjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion .yarnrc.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ nmHoistingLimits: workspaces

nodeLinker: node-modules

yarnPath: .yarn/releases/yarn-4.5.0.cjs
yarnPath: .yarn/releases/yarn-4.6.0.cjs
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ yarn add react-native-fast-confetti

## Usage
### `<Confetti />`
This animation creates a basic confetti effect where pieces fall from the top in a straight line.
This animation creates a basic confetti effect where pieces fall from the top in a straight line.

https://github.com/user-attachments/assets/d89ef248-6b27-435e-a322-fb62a3550343

Expand Down Expand Up @@ -77,7 +77,8 @@ return (
| `fadeOutOnEnd` | No | N/A | Should the confetti flakes fade out as they reach the bottom. |
| `onAnimationStart` | No | N/A | Callback function triggered when the falling animation starts. |
| `onAnimationEnd` | No | N/A | Callback function triggered when the falling animation ends. |
| `sizeVariation` | No | 0 | Controls the random size variation of confetti flakes. Value between 0 and 1. A value of 0.1 means flakes can randomly vary between 10% smaller to 10% larger than the base size (`flakeSize`). Recommended value is between 0 and 0.2 |
| `sizeVariation` | No | 0 | A value of 0.1 means flakes can vary up to 10% smaller than the base (`flakeSize`), with more flakes clustering towards the original size and fewer towards the minimum size. Recommended value is between 0 and 0.5 |
| `radiusRange` | No | [0, 0] | The range of the radius of the confetti flakes. A tuple of [min, max] values from which a random radius will be selected for each flake. |

## `<PIConfetti />` Props

Expand All @@ -95,7 +96,8 @@ return (
| `fadeOutOnEnd` | No | N/A | Should the confetti flakes fade out as they reach the bottom. |
| `onAnimationStart` | No | N/A | Callback function triggered when the falling animation starts. |
| `onAnimationEnd` | No | N/A | Callback function triggered when the falling animation ends. |
| `sizeVariation` | No | 0 | Controls the random size variation of confetti flakes. Value between 0 and 1. A value of 0.1 means flakes can randomly vary between 10% smaller to 10% larger than the base size (`flakeSize`). Recommended value is between 0 and 0.2 |
| `sizeVariation` | No | 0 | A value of 0.1 means flakes can vary up to 10% smaller than the base (`flakeSize`), with more flakes clustering towards the original size and fewer towards the minimum size. Recommended value is between 0 and 0.5 |
| `radiusRange` | No | [0, 0] | The range of the radius of the confetti flakes. A tuple of [min, max] values from which a random radius will be selected for each flake.


## Methods
Expand Down
3 changes: 2 additions & 1 deletion example/app.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
},
"web": {
"favicon": "./assets/favicon.png"
}
},
"newArchEnabled": true
}
}
18 changes: 9 additions & 9 deletions example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@
"web": "expo start --web"
},
"dependencies": {
"@expo/metro-runtime": "~3.2.3",
"@shopify/react-native-skia": "1.2.3",
"expo": "~51.0.28",
"expo-status-bar": "~1.12.1",
"react": "18.2.0",
"react-dom": "18.2.0",
"react-native": "0.74.5",
"react-native-reanimated": "~3.10.1",
"react-native-web": "~0.19.10"
"@expo/metro-runtime": "~4.0.0",
"@shopify/react-native-skia": "1.5.0",
"expo": "^52.0.25",
"expo-status-bar": "~2.0.1",
"react": "18.3.1",
"react-dom": "18.3.1",
"react-native": "0.76.6",
"react-native-reanimated": "~3.16.1",
"react-native-web": "~0.19.13"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
76 changes: 62 additions & 14 deletions example/src/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { useRef } from 'react';
import { StyleSheet, View, Button, useWindowDimensions } from 'react-native';
import { Confetti } from 'react-native-fast-confetti';
import { useRef, useState } from 'react';
import {
StyleSheet,
View,
Button,
useWindowDimensions,
Switch,
Text,
} from 'react-native';
import { Confetti, PIConfetti } from 'react-native-fast-confetti';
import type {
ConfettiMethods,
ConfettiProps,
Expand All @@ -9,25 +16,60 @@ import type {
export default function App() {
const confettiRef = useRef<ConfettiMethods>(null);
const { height, width } = useWindowDimensions();
const [isPIConfetti, setIsPIConfetti] = useState(false);
const [cannons, setCannons] = useState(false);

const cannonPositions: ConfettiProps['cannonsPositions'] = [
{ x: -30, y: 600 },
{ x: -30, y: height },
{ x: width + 30, y: 600 },
{ x: width + 30, y: height },
];

return (
<View style={styles.container}>
<Confetti
ref={confettiRef}
autoplay={true}
fallDuration={8000}
verticalSpacing={20}
cannonsPositions={cannonPositions}
sizeVariation={0.1}
fadeOutOnEnd
/>
<View style={styles.switchContainer}>
<Text>Regular Confetti</Text>
<Switch
value={isPIConfetti}
onValueChange={setIsPIConfetti}
trackColor={{ false: '#767577', true: '#81b0ff' }}
/>
<Text>PI Confetti</Text>
</View>
{!isPIConfetti && (
<View style={styles.switchContainer}>
<Text>Regular Confetti</Text>
<Switch
value={cannons}
onValueChange={setCannons}
trackColor={{ false: '#767577', true: '#81b0ff' }}
/>
<Text>Cannons Confetti</Text>
</View>
)}

{isPIConfetti ? (
<PIConfetti
ref={confettiRef}
autoplay={true}
fallDuration={2000}
blastDuration={250}
sizeVariation={0.3}
radiusRange={[0, 15]}
flakeSize={{ width: 18, height: 12 }}
/>
) : (
<Confetti
ref={confettiRef}
autoplay={true}
verticalSpacing={cannons ? 0 : 20}
cannonsPositions={cannons ? cannonPositions : undefined}
radiusRange={[0, 15]}
sizeVariation={0.5}
flakeSize={{ width: 15, height: 10 }}
count={500}
/>
)}

<Button title="Resume" onPress={() => confettiRef.current?.resume()} />
<Button title="Pause" onPress={() => confettiRef.current?.pause()} />
<Button title="Restart" onPress={() => confettiRef.current?.restart()} />
Expand All @@ -43,4 +85,10 @@ const styles = StyleSheet.create({
backgroundColor: 'white',
justifyContent: 'center',
},
switchContainer: {
flexDirection: 'row',
alignItems: 'center',
marginBottom: 20,
gap: 8,
},
});
10 changes: 5 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-native-fast-confetti",
"version": "0.7.0",
"version": "0.8.1",
"description": "The fastest confetti animation library in react native",
"source": "./src/index.tsx",
"main": "./lib/commonjs/index.js",
Expand Down Expand Up @@ -66,10 +66,10 @@
"eslint-plugin-prettier": "^5.0.1",
"jest": "^29.7.0",
"prettier": "^3.0.3",
"react": "18.2.0",
"react-native": "0.74.5",
"react": "18.3.1",
"react-native": "0.76.6",
"react-native-builder-bob": "^0.30.2",
"react-native-reanimated": "^3.15.5",
"react-native-reanimated": "~3.16.1",
"release-it": "^15.0.0",
"typescript": "^5.2.2"
},
Expand All @@ -85,7 +85,7 @@
"workspaces": [
"example"
],
"packageManager": "yarn@4.5.0",
"packageManager": "yarn@4.6.0",
"jest": {
"preset": "react-native",
"modulePathIgnorePatterns": [
Expand Down
113 changes: 45 additions & 68 deletions src/Confetti.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,12 @@
import {
useTexture,
Group,
Rect,
rect,
useRSXformBuffer,
Canvas,
Atlas,
} from '@shopify/react-native-skia';
import {
forwardRef,
useCallback,
useEffect,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { useRSXformBuffer, Canvas, Atlas } from '@shopify/react-native-skia';
import { forwardRef, useCallback, useEffect, useImperativeHandle } from 'react';
import { StyleSheet, useWindowDimensions, View } from 'react-native';
import {
cancelAnimation,
Extrapolation,
interpolate,
runOnJS,
runOnUI,
useDerivedValue,
useSharedValue,
withRepeat,
Expand All @@ -39,6 +25,13 @@ import {
RANDOM_INITIAL_Y_JIGGLE,
} from './constants';
import type { ConfettiMethods, ConfettiProps } from './types';
import { useConfettiLogic } from './hooks/useConfettiLogic';
import { useVariations } from './hooks/sizeVariations';
import {
clearAnimatedTimeout,
setAnimatedTimeout,
type AnimatedTimeoutID,
} from './hooks/useAnimatedTimeout';

export const Confetti = forwardRef<ConfettiMethods, ConfettiProps>(
(
Expand All @@ -51,6 +44,7 @@ export const Confetti = forwardRef<ConfettiMethods, ConfettiProps>(
colors = DEFAULT_COLORS,
autoStartDelay = DEFAULT_AUTOSTART_DELAY,
verticalSpacing = DEFAULT_VERTICAL_SPACING,
radiusRange: _radiusRange,
onAnimationEnd,
onAnimationStart,
width: _width,
Expand Down Expand Up @@ -99,25 +93,20 @@ export const Confetti = forwardRef<ConfettiMethods, ConfettiProps>(
-rowsNum * rowHeight * (hasCannons ? 0.2 : 1) +
verticalSpacing -
RANDOM_INITIAL_Y_JIGGLE;

const sizeSteps = 10;
const sizeVariations = useMemo(() => {
const sizeVariations = [];
for (let i = 0; i < sizeSteps; i++) {
const variationScale = -1 + (2 * i) / (sizeSteps - 1);
const multiplier = 1 + sizeVariation * variationScale;

sizeVariations.push({
width: flakeSize.width * multiplier,
height: flakeSize.height * multiplier,
});
}
return sizeVariations;
}, [sizeSteps, sizeVariation, flakeSize]);

const [boxes, setBoxes] = useState(() =>
const sizeVariations = useVariations({
sizeVariation,
flakeSize,
_radiusRange,
});
const boxes = useSharedValue(
generateBoxesArray(count, colors.length, sizeVariations.length)
);
const { texture, sprites } = useConfettiLogic({
sizeVariations,
count,
colors,
boxes,
});

const pause = () => {
running.value = false;
Expand All @@ -137,8 +126,9 @@ export const Confetti = forwardRef<ConfettiMethods, ConfettiProps>(
colors.length,
sizeVariations.length
);
runOnJS(setBoxes)(newBoxes);
}, [count, colors, sizeVariations.length]);
boxes.value = newBoxes;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [count, colors.length, sizeVariations.length]);

const JSOnStart = () => onAnimationStart?.();
const JSOnEnd = () => onAnimationEnd?.();
Expand Down Expand Up @@ -185,10 +175,11 @@ export const Confetti = forwardRef<ConfettiMethods, ConfettiProps>(
};

const restart = () => {
'worklet';
refreshBoxes();
progress.value = initialProgress;
running.value = true;
JSOnStart();
runOnJS(JSOnStart)();

progress.value = runAnimation(
{ infinite: isInfinite, blastDuration, fallDuration },
Expand Down Expand Up @@ -270,42 +261,28 @@ export const Confetti = forwardRef<ConfettiMethods, ConfettiProps>(
return { x, y };
};

const animatedTimeout = useSharedValue<AnimatedTimeoutID>(-1);
useEffect(() => {
if (autoplay && !running.value) setTimeout(restart, autoStartDelay);
runOnUI(() => {
if (autoplay && !running.value) {
if (autoStartDelay > 0)
animatedTimeout.value = setAnimatedTimeout(restart, autoStartDelay);
else restart();
}
})();

return () => {
if (animatedTimeout.value !== -1) {
clearAnimatedTimeout(animatedTimeout.value);
animatedTimeout.value = -1;
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [autoplay]);

const maxWidth = Math.max(...sizeVariations.map((size) => size.width));
const maxHeight = Math.max(...sizeVariations.map((size) => size.height));

const texture = useTexture(
<Group>
{colors.map((color, index) => {
return (
<Rect
key={`${index}`}
rect={rect(0, index * maxHeight, maxWidth, maxHeight)}
color={color}
/>
);
})}
</Group>,
{
width: maxWidth,
height: maxHeight * colors.length,
}
);

const sprites = boxes.map((box) => {
const colorIndex = box.colorIndex;
const sizeIndex = box.sizeIndex;
const size = sizeVariations[sizeIndex]!;
return rect(0, colorIndex * maxHeight, size.width, size.height);
});

const transforms = useRSXformBuffer(boxes.length, (val, i) => {
const transforms = useRSXformBuffer(count, (val, i) => {
'worklet';
const piece = boxes[i];
const piece = boxes.value[i];
if (!piece) return;

let tx = 0,
Expand Down
Loading

0 comments on commit e5d9930

Please sign in to comment.