react-native-glitter 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +20 -0
- package/README.md +256 -0
- package/lib/module/index.js +194 -0
- package/lib/typescript/src/index.d.ts +19 -0
- package/package.json +173 -0
- package/src/index.tsx +309 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 liveforownhappiness
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# react-native-glitter
|
|
2
|
+
|
|
3
|
+
✨ A beautiful shimmer/glitter effect component for React Native. Add a sparkling diagonal shine animation to any component!
|
|
4
|
+
|
|
5
|
+
Works with both **React Native CLI** and **Expo** projects - no native dependencies required!
|
|
6
|
+
|
|
7
|
+

|
|
8
|
+

|
|
9
|
+
|
|
10
|
+
## Demo
|
|
11
|
+
|
|
12
|
+
<p align="center">
|
|
13
|
+
<img src="./assets/demo.gif" alt="React Native Glitter Demo" width="320" />
|
|
14
|
+
</p>
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- 🚀 **Zero native dependencies** - Pure JavaScript/TypeScript implementation
|
|
19
|
+
- 📱 **Cross-platform** - Works on iOS, Android, and Web
|
|
20
|
+
- 🎨 **Customizable** - Control color, speed, angle, and more
|
|
21
|
+
- ⚡ **Performant** - Uses native driver for smooth 60fps animations
|
|
22
|
+
- 🔧 **TypeScript** - Full TypeScript support with type definitions
|
|
23
|
+
- ✨ **Animation Modes** - Normal, expand, and shrink effects
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Using npm
|
|
29
|
+
npm install react-native-glitter
|
|
30
|
+
|
|
31
|
+
# Using yarn
|
|
32
|
+
yarn add react-native-glitter
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Usage
|
|
36
|
+
|
|
37
|
+
### Basic Usage
|
|
38
|
+
|
|
39
|
+
Wrap any component with `<Glitter>` to add a shimmer effect:
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
import { Glitter } from 'react-native-glitter';
|
|
43
|
+
|
|
44
|
+
function MyComponent() {
|
|
45
|
+
return (
|
|
46
|
+
<Glitter>
|
|
47
|
+
<View style={styles.card}>
|
|
48
|
+
<Text>This content will shimmer!</Text>
|
|
49
|
+
</View>
|
|
50
|
+
</Glitter>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### Animation Modes
|
|
56
|
+
|
|
57
|
+
Control how the shimmer line behaves during animation:
|
|
58
|
+
|
|
59
|
+
```tsx
|
|
60
|
+
// Normal - constant size (default)
|
|
61
|
+
<Glitter mode="normal">
|
|
62
|
+
<View style={styles.box} />
|
|
63
|
+
</Glitter>
|
|
64
|
+
|
|
65
|
+
// Expand - starts small and grows
|
|
66
|
+
<Glitter mode="expand">
|
|
67
|
+
<View style={styles.box} />
|
|
68
|
+
</Glitter>
|
|
69
|
+
|
|
70
|
+
// Shrink - starts full size and shrinks
|
|
71
|
+
<Glitter mode="shrink">
|
|
72
|
+
<View style={styles.box} />
|
|
73
|
+
</Glitter>
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Shrink/Expand Positions
|
|
77
|
+
|
|
78
|
+
For `shrink` and `expand` modes, control where the line shrinks to or expands from:
|
|
79
|
+
|
|
80
|
+
```tsx
|
|
81
|
+
// Shrink to top
|
|
82
|
+
<Glitter mode="shrink" position="top">
|
|
83
|
+
<View style={styles.box} />
|
|
84
|
+
</Glitter>
|
|
85
|
+
|
|
86
|
+
// Shrink to center (default)
|
|
87
|
+
<Glitter mode="shrink" position="center">
|
|
88
|
+
<View style={styles.box} />
|
|
89
|
+
</Glitter>
|
|
90
|
+
|
|
91
|
+
// Shrink to bottom
|
|
92
|
+
<Glitter mode="shrink" position="bottom">
|
|
93
|
+
<View style={styles.box} />
|
|
94
|
+
</Glitter>
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Skeleton Loading
|
|
98
|
+
|
|
99
|
+
Create beautiful skeleton loading states:
|
|
100
|
+
|
|
101
|
+
```tsx
|
|
102
|
+
import { Glitter } from 'react-native-glitter';
|
|
103
|
+
|
|
104
|
+
function SkeletonLoader() {
|
|
105
|
+
return (
|
|
106
|
+
<Glitter duration={1200} delay={300}>
|
|
107
|
+
<View style={styles.skeletonBox} />
|
|
108
|
+
</Glitter>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### Premium Button
|
|
114
|
+
|
|
115
|
+
Add a luxurious shimmer to buttons:
|
|
116
|
+
|
|
117
|
+
```tsx
|
|
118
|
+
import { Glitter } from 'react-native-glitter';
|
|
119
|
+
|
|
120
|
+
function PremiumButton() {
|
|
121
|
+
return (
|
|
122
|
+
<Glitter color="rgba(255, 215, 0, 0.5)" angle={25}>
|
|
123
|
+
<TouchableOpacity style={styles.button}>
|
|
124
|
+
<Text>✨ Premium Feature</Text>
|
|
125
|
+
</TouchableOpacity>
|
|
126
|
+
</Glitter>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### Controlled Animation
|
|
132
|
+
|
|
133
|
+
Control when the animation runs:
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
import { Glitter } from 'react-native-glitter';
|
|
137
|
+
|
|
138
|
+
function ControlledGlitter() {
|
|
139
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<Glitter active={isLoading}>
|
|
143
|
+
<View style={styles.content}>
|
|
144
|
+
<Text>Loading...</Text>
|
|
145
|
+
</View>
|
|
146
|
+
</Glitter>
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## Props
|
|
152
|
+
|
|
153
|
+
| Prop | Type | Default | Description |
|
|
154
|
+
|------|------|---------|-------------|
|
|
155
|
+
| `children` | `ReactNode` | **required** | The content to apply the shimmer effect to |
|
|
156
|
+
| `duration` | `number` | `1500` | Duration of one shimmer animation cycle in milliseconds |
|
|
157
|
+
| `delay` | `number` | `400` | Delay between animation cycles in milliseconds |
|
|
158
|
+
| `color` | `string` | `'rgba(255, 255, 255, 0.8)'` | Color of the shimmer effect |
|
|
159
|
+
| `angle` | `number` | `20` | Angle of the shimmer in degrees |
|
|
160
|
+
| `shimmerWidth` | `number` | `60` | Width of the shimmer band in pixels |
|
|
161
|
+
| `active` | `boolean` | `true` | Whether the animation is active |
|
|
162
|
+
| `style` | `ViewStyle` | - | Additional styles for the container |
|
|
163
|
+
| `easing` | `(value: number) => number` | - | Custom easing function for the animation |
|
|
164
|
+
| `mode` | `'normal' \| 'expand' \| 'shrink'` | `'normal'` | Animation mode for the shimmer line |
|
|
165
|
+
| `position` | `'top' \| 'center' \| 'bottom'` | `'center'` | Position where the line shrinks/expands (for shrink/expand modes) |
|
|
166
|
+
|
|
167
|
+
## Examples
|
|
168
|
+
|
|
169
|
+
### Different Speeds
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
// Fast shimmer
|
|
173
|
+
<Glitter duration={800} delay={200}>
|
|
174
|
+
<View style={styles.box} />
|
|
175
|
+
</Glitter>
|
|
176
|
+
|
|
177
|
+
// Slow shimmer
|
|
178
|
+
<Glitter duration={3000} delay={1000}>
|
|
179
|
+
<View style={styles.box} />
|
|
180
|
+
</Glitter>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Different Colors
|
|
184
|
+
|
|
185
|
+
```tsx
|
|
186
|
+
// Gold shimmer
|
|
187
|
+
<Glitter color="rgba(255, 215, 0, 0.5)">
|
|
188
|
+
<View style={styles.box} />
|
|
189
|
+
</Glitter>
|
|
190
|
+
|
|
191
|
+
// Blue shimmer
|
|
192
|
+
<Glitter color="rgba(100, 149, 237, 0.5)">
|
|
193
|
+
<View style={styles.box} />
|
|
194
|
+
</Glitter>
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Different Angles
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
// Horizontal shimmer
|
|
201
|
+
<Glitter angle={0}>
|
|
202
|
+
<View style={styles.box} />
|
|
203
|
+
</Glitter>
|
|
204
|
+
|
|
205
|
+
// Diagonal shimmer
|
|
206
|
+
<Glitter angle={45}>
|
|
207
|
+
<View style={styles.box} />
|
|
208
|
+
</Glitter>
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### Animation Modes
|
|
212
|
+
|
|
213
|
+
```tsx
|
|
214
|
+
// Expand mode - line grows as it moves
|
|
215
|
+
<Glitter mode="expand">
|
|
216
|
+
<View style={styles.box} />
|
|
217
|
+
</Glitter>
|
|
218
|
+
|
|
219
|
+
// Shrink mode with position - line shrinks to bottom
|
|
220
|
+
<Glitter mode="shrink" position="bottom">
|
|
221
|
+
<View style={styles.box} />
|
|
222
|
+
</Glitter>
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
## TypeScript
|
|
226
|
+
|
|
227
|
+
This library is written in TypeScript and includes type definitions:
|
|
228
|
+
|
|
229
|
+
```tsx
|
|
230
|
+
import {
|
|
231
|
+
Glitter,
|
|
232
|
+
type GlitterProps,
|
|
233
|
+
type GlitterMode,
|
|
234
|
+
type GlitterPosition,
|
|
235
|
+
} from 'react-native-glitter';
|
|
236
|
+
|
|
237
|
+
const customProps: GlitterProps = {
|
|
238
|
+
children: <View />,
|
|
239
|
+
duration: 2000,
|
|
240
|
+
color: 'rgba(255, 255, 255, 0.3)',
|
|
241
|
+
mode: 'shrink',
|
|
242
|
+
position: 'center',
|
|
243
|
+
};
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
## Contributing
|
|
247
|
+
|
|
248
|
+
See the [contributing guide](CONTRIBUTING.md) to learn how to contribute to the repository and the development workflow.
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
MIT
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
Made with ❤️ by [liveforownhappiness](https://github.com/liveforownhappiness)
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useEffect, useRef, useState, useCallback, } from 'react';
|
|
3
|
+
import { View, Animated, StyleSheet, Easing, } from 'react-native';
|
|
4
|
+
function generateGlitterOpacities(count, peak = 1) {
|
|
5
|
+
const opacities = [];
|
|
6
|
+
const center = (count - 1) / 2;
|
|
7
|
+
for (let i = 0; i < count; i++) {
|
|
8
|
+
const distance = Math.abs(i - center);
|
|
9
|
+
const normalizedDistance = distance / center;
|
|
10
|
+
let opacity;
|
|
11
|
+
if (normalizedDistance < 0.15) {
|
|
12
|
+
opacity = peak;
|
|
13
|
+
}
|
|
14
|
+
else if (normalizedDistance < 0.3) {
|
|
15
|
+
const t = (normalizedDistance - 0.15) / 0.15;
|
|
16
|
+
opacity = peak * (1 - t * 0.6);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
const t = (normalizedDistance - 0.3) / 0.7;
|
|
20
|
+
opacity = peak * 0.4 * Math.pow(1 - t, 2);
|
|
21
|
+
}
|
|
22
|
+
opacities.push(Math.max(0, opacity));
|
|
23
|
+
}
|
|
24
|
+
return opacities;
|
|
25
|
+
}
|
|
26
|
+
function generateVerticalSegments(fadeRatioParam) {
|
|
27
|
+
const fadeRatio = fadeRatioParam ?? 0.25;
|
|
28
|
+
const solidRatio = 1 - fadeRatio * 2;
|
|
29
|
+
const fadeSegments = 5;
|
|
30
|
+
const segments = [];
|
|
31
|
+
for (let i = 0; i < fadeSegments; i++) {
|
|
32
|
+
const opacity = (i + 1) / fadeSegments;
|
|
33
|
+
segments.push({
|
|
34
|
+
heightRatio: fadeRatio / fadeSegments,
|
|
35
|
+
opacity: Math.pow(opacity, 2),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
segments.push({
|
|
39
|
+
heightRatio: solidRatio,
|
|
40
|
+
opacity: 1,
|
|
41
|
+
});
|
|
42
|
+
for (let i = fadeSegments - 1; i >= 0; i--) {
|
|
43
|
+
const opacity = (i + 1) / fadeSegments;
|
|
44
|
+
segments.push({
|
|
45
|
+
heightRatio: fadeRatio / fadeSegments,
|
|
46
|
+
opacity: Math.pow(opacity, 2),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
return segments;
|
|
50
|
+
}
|
|
51
|
+
export function Glitter({ children, duration = 1500, delay = 400, color = 'rgba(255, 255, 255, 0.8)', angle = 20, shimmerWidth = 60, active = true, style, easing, mode = 'normal', position = 'center', }) {
|
|
52
|
+
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
53
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
54
|
+
const [containerHeight, setContainerHeight] = useState(0);
|
|
55
|
+
const animationRef = useRef(null);
|
|
56
|
+
const defaultEasing = Easing.bezier(0.4, 0, 0.2, 1);
|
|
57
|
+
const startAnimation = useCallback(() => {
|
|
58
|
+
if (!active || containerWidth === 0)
|
|
59
|
+
return;
|
|
60
|
+
animatedValue.setValue(0);
|
|
61
|
+
const timing = Animated.timing(animatedValue, {
|
|
62
|
+
toValue: 1,
|
|
63
|
+
duration,
|
|
64
|
+
useNativeDriver: true,
|
|
65
|
+
easing: easing ?? defaultEasing,
|
|
66
|
+
});
|
|
67
|
+
animationRef.current = Animated.loop(Animated.sequence([timing, Animated.delay(delay)]));
|
|
68
|
+
animationRef.current.start();
|
|
69
|
+
return () => {
|
|
70
|
+
animationRef.current?.stop();
|
|
71
|
+
};
|
|
72
|
+
}, [
|
|
73
|
+
active,
|
|
74
|
+
containerWidth,
|
|
75
|
+
duration,
|
|
76
|
+
delay,
|
|
77
|
+
animatedValue,
|
|
78
|
+
easing,
|
|
79
|
+
defaultEasing,
|
|
80
|
+
]);
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
const cleanup = startAnimation();
|
|
83
|
+
return cleanup;
|
|
84
|
+
}, [startAnimation]);
|
|
85
|
+
const onLayout = useCallback((event) => {
|
|
86
|
+
const { width, height } = event.nativeEvent.layout;
|
|
87
|
+
setContainerWidth(width);
|
|
88
|
+
setContainerHeight(height);
|
|
89
|
+
}, []);
|
|
90
|
+
const extraWidth = Math.tan((angle * Math.PI) / 180) * 200;
|
|
91
|
+
const translateX = animatedValue.interpolate({
|
|
92
|
+
inputRange: [0, 1],
|
|
93
|
+
outputRange: [-shimmerWidth - extraWidth, containerWidth + shimmerWidth],
|
|
94
|
+
});
|
|
95
|
+
const heightMultiplier = 1.5;
|
|
96
|
+
const lineHeight = containerHeight * heightMultiplier;
|
|
97
|
+
const getScaleY = () => {
|
|
98
|
+
if (mode === 'normal') {
|
|
99
|
+
return 1;
|
|
100
|
+
}
|
|
101
|
+
if (mode === 'expand') {
|
|
102
|
+
return animatedValue.interpolate({
|
|
103
|
+
inputRange: [0, 1],
|
|
104
|
+
outputRange: [0.01, 1],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return animatedValue.interpolate({
|
|
108
|
+
inputRange: [0, 1],
|
|
109
|
+
outputRange: [1, 0.01],
|
|
110
|
+
});
|
|
111
|
+
};
|
|
112
|
+
const halfHeight = lineHeight / 2;
|
|
113
|
+
const startOffset = 0;
|
|
114
|
+
const getTransformOriginOffset = () => {
|
|
115
|
+
if (mode === 'normal' || position === 'center') {
|
|
116
|
+
return 0;
|
|
117
|
+
}
|
|
118
|
+
if (position === 'top') {
|
|
119
|
+
return -halfHeight;
|
|
120
|
+
}
|
|
121
|
+
else {
|
|
122
|
+
return halfHeight;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
const layerCount = Math.max(11, Math.round(shimmerWidth / 3));
|
|
126
|
+
const horizontalOpacities = generateGlitterOpacities(layerCount, 1);
|
|
127
|
+
const layerWidth = shimmerWidth / layerCount;
|
|
128
|
+
const normalFadeRatio = (heightMultiplier - 1) / heightMultiplier / 2;
|
|
129
|
+
const normalSegments = generateVerticalSegments(normalFadeRatio);
|
|
130
|
+
const animatedSegments = generateVerticalSegments(0.25);
|
|
131
|
+
const shimmerLayers = horizontalOpacities.map((opacity, index) => ({
|
|
132
|
+
opacity,
|
|
133
|
+
position: index * layerWidth - shimmerWidth / 2 + layerWidth / 2,
|
|
134
|
+
}));
|
|
135
|
+
const scaleY = getScaleY();
|
|
136
|
+
const transformOriginOffset = getTransformOriginOffset();
|
|
137
|
+
const isAnimated = mode !== 'normal';
|
|
138
|
+
return (_jsxs(View, { style: [styles.container, style], onLayout: onLayout, children: [children, active && containerWidth > 0 && containerHeight > 0 && (_jsx(Animated.View, { style: [
|
|
139
|
+
styles.shimmerContainer,
|
|
140
|
+
{
|
|
141
|
+
transform: [{ translateX }],
|
|
142
|
+
},
|
|
143
|
+
], pointerEvents: "none", children: _jsx(View, { style: [
|
|
144
|
+
styles.rotationWrapper,
|
|
145
|
+
{
|
|
146
|
+
width: shimmerWidth,
|
|
147
|
+
height: lineHeight,
|
|
148
|
+
transform: [{ skewX: `${-angle}deg` }],
|
|
149
|
+
},
|
|
150
|
+
], children: shimmerLayers.map((layer, layerIndex) => (_jsx(Animated.View, { style: [
|
|
151
|
+
styles.shimmerLine,
|
|
152
|
+
{
|
|
153
|
+
width: layerWidth + 0.5,
|
|
154
|
+
height: lineHeight,
|
|
155
|
+
left: layer.position,
|
|
156
|
+
transform: isAnimated
|
|
157
|
+
? [
|
|
158
|
+
{ translateY: startOffset + transformOriginOffset },
|
|
159
|
+
{
|
|
160
|
+
scaleY: scaleY,
|
|
161
|
+
},
|
|
162
|
+
{ translateY: -transformOriginOffset },
|
|
163
|
+
]
|
|
164
|
+
: [{ translateY: startOffset }],
|
|
165
|
+
},
|
|
166
|
+
], children: (isAnimated ? animatedSegments : normalSegments).map((segment, vIndex) => (_jsx(View, { style: {
|
|
167
|
+
width: '100%',
|
|
168
|
+
height: lineHeight * segment.heightRatio,
|
|
169
|
+
backgroundColor: color,
|
|
170
|
+
opacity: layer.opacity * segment.opacity,
|
|
171
|
+
} }, vIndex))) }, layerIndex))) }) }))] }));
|
|
172
|
+
}
|
|
173
|
+
const styles = StyleSheet.create({
|
|
174
|
+
container: {
|
|
175
|
+
position: 'relative',
|
|
176
|
+
overflow: 'hidden',
|
|
177
|
+
},
|
|
178
|
+
shimmerContainer: {
|
|
179
|
+
...StyleSheet.absoluteFillObject,
|
|
180
|
+
flexDirection: 'row',
|
|
181
|
+
justifyContent: 'center',
|
|
182
|
+
alignItems: 'center',
|
|
183
|
+
},
|
|
184
|
+
rotationWrapper: {
|
|
185
|
+
flexDirection: 'row',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'center',
|
|
188
|
+
},
|
|
189
|
+
shimmerLine: {
|
|
190
|
+
position: 'absolute',
|
|
191
|
+
flexDirection: 'column',
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
export default Glitter;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type ReactNode, type ReactElement } from 'react';
|
|
2
|
+
import { type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
export type GlitterMode = 'normal' | 'expand' | 'shrink';
|
|
4
|
+
export type GlitterPosition = 'top' | 'center' | 'bottom';
|
|
5
|
+
export interface GlitterProps {
|
|
6
|
+
children: ReactNode;
|
|
7
|
+
duration?: number;
|
|
8
|
+
delay?: number;
|
|
9
|
+
color?: string;
|
|
10
|
+
angle?: number;
|
|
11
|
+
shimmerWidth?: number;
|
|
12
|
+
active?: boolean;
|
|
13
|
+
style?: StyleProp<ViewStyle>;
|
|
14
|
+
easing?: (value: number) => number;
|
|
15
|
+
mode?: GlitterMode;
|
|
16
|
+
position?: GlitterPosition;
|
|
17
|
+
}
|
|
18
|
+
export declare function Glitter({ children, duration, delay, color, angle, shimmerWidth, active, style, easing, mode, position, }: GlitterProps): ReactElement;
|
|
19
|
+
export default Glitter;
|
package/package.json
ADDED
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "react-native-glitter",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "A beautiful shimmer/glitter effect component for React Native. Add a sparkling diagonal shine animation to any component!",
|
|
5
|
+
"main": "./lib/module/index.js",
|
|
6
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"source": "./src/index.tsx",
|
|
10
|
+
"types": "./lib/typescript/src/index.d.ts",
|
|
11
|
+
"default": "./lib/module/index.js"
|
|
12
|
+
},
|
|
13
|
+
"./package.json": "./package.json"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"src",
|
|
17
|
+
"lib",
|
|
18
|
+
"android",
|
|
19
|
+
"ios",
|
|
20
|
+
"cpp",
|
|
21
|
+
"*.podspec",
|
|
22
|
+
"react-native.config.js",
|
|
23
|
+
"!ios/build",
|
|
24
|
+
"!android/build",
|
|
25
|
+
"!android/gradle",
|
|
26
|
+
"!android/gradlew",
|
|
27
|
+
"!android/gradlew.bat",
|
|
28
|
+
"!android/local.properties",
|
|
29
|
+
"!**/__tests__",
|
|
30
|
+
"!**/__fixtures__",
|
|
31
|
+
"!**/__mocks__",
|
|
32
|
+
"!**/.*"
|
|
33
|
+
],
|
|
34
|
+
"scripts": {
|
|
35
|
+
"example": "yarn workspace react-native-glitter-example",
|
|
36
|
+
"ios": "yarn workspace react-native-glitter-example ios",
|
|
37
|
+
"example:ios": "yarn workspace react-native-glitter-example ios",
|
|
38
|
+
"example:android": "yarn workspace react-native-glitter-example android",
|
|
39
|
+
"example:web": "yarn workspace react-native-glitter-example web",
|
|
40
|
+
"clean": "del-cli lib",
|
|
41
|
+
"prepare": "yarn clean && tsc -p tsconfig.build.json",
|
|
42
|
+
"typecheck": "tsc",
|
|
43
|
+
"lint": "eslint \"**/*.{js,ts,tsx}\"",
|
|
44
|
+
"test": "jest",
|
|
45
|
+
"release": "release-it --only-version"
|
|
46
|
+
},
|
|
47
|
+
"keywords": [
|
|
48
|
+
"react-native",
|
|
49
|
+
"ios",
|
|
50
|
+
"android",
|
|
51
|
+
"expo",
|
|
52
|
+
"glitter",
|
|
53
|
+
"shimmer",
|
|
54
|
+
"shine",
|
|
55
|
+
"skeleton",
|
|
56
|
+
"loading",
|
|
57
|
+
"animation",
|
|
58
|
+
"effect",
|
|
59
|
+
"sparkle"
|
|
60
|
+
],
|
|
61
|
+
"repository": {
|
|
62
|
+
"type": "git",
|
|
63
|
+
"url": "git+https://github.com/liveforownhappiness/react-native-glitter.git"
|
|
64
|
+
},
|
|
65
|
+
"author": "liveforownhappiness <liveforownhappiness@gmail.com> (https://github.com/liveforownhappiness)",
|
|
66
|
+
"license": "MIT",
|
|
67
|
+
"bugs": {
|
|
68
|
+
"url": "https://github.com/liveforownhappiness/react-native-glitter/issues"
|
|
69
|
+
},
|
|
70
|
+
"homepage": "https://github.com/liveforownhappiness/react-native-glitter#readme",
|
|
71
|
+
"publishConfig": {
|
|
72
|
+
"registry": "https://registry.npmjs.org/"
|
|
73
|
+
},
|
|
74
|
+
"devDependencies": {
|
|
75
|
+
"@commitlint/config-conventional": "^19.8.1",
|
|
76
|
+
"@eslint/compat": "^1.3.2",
|
|
77
|
+
"@eslint/eslintrc": "^3.3.1",
|
|
78
|
+
"@eslint/js": "^9.35.0",
|
|
79
|
+
"@react-native/babel-preset": "0.83.0",
|
|
80
|
+
"@react-native/eslint-config": "0.83.0",
|
|
81
|
+
"@release-it/conventional-changelog": "^10.0.1",
|
|
82
|
+
"@types/jest": "^29.5.14",
|
|
83
|
+
"@types/react": "^19.1.12",
|
|
84
|
+
"commitlint": "^19.8.1",
|
|
85
|
+
"del-cli": "^6.0.0",
|
|
86
|
+
"eslint": "^9.35.0",
|
|
87
|
+
"eslint-config-prettier": "^10.1.8",
|
|
88
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
89
|
+
"jest": "^29.7.0",
|
|
90
|
+
"lefthook": "^2.0.3",
|
|
91
|
+
"prettier": "^2.8.8",
|
|
92
|
+
"react": "19.1.0",
|
|
93
|
+
"react-native": "0.81.5",
|
|
94
|
+
"react-native-builder-bob": "^0.40.17",
|
|
95
|
+
"release-it": "^19.0.4",
|
|
96
|
+
"typescript": "^5.9.2"
|
|
97
|
+
},
|
|
98
|
+
"peerDependencies": {
|
|
99
|
+
"react": "*",
|
|
100
|
+
"react-native": "*"
|
|
101
|
+
},
|
|
102
|
+
"workspaces": [
|
|
103
|
+
"example"
|
|
104
|
+
],
|
|
105
|
+
"packageManager": "yarn@4.11.0",
|
|
106
|
+
"react-native-builder-bob": {
|
|
107
|
+
"source": "src",
|
|
108
|
+
"output": "lib",
|
|
109
|
+
"targets": [
|
|
110
|
+
[
|
|
111
|
+
"module",
|
|
112
|
+
{
|
|
113
|
+
"esm": true
|
|
114
|
+
}
|
|
115
|
+
],
|
|
116
|
+
[
|
|
117
|
+
"typescript",
|
|
118
|
+
{
|
|
119
|
+
"project": "tsconfig.build.json"
|
|
120
|
+
}
|
|
121
|
+
]
|
|
122
|
+
]
|
|
123
|
+
},
|
|
124
|
+
"prettier": {
|
|
125
|
+
"quoteProps": "consistent",
|
|
126
|
+
"singleQuote": true,
|
|
127
|
+
"tabWidth": 2,
|
|
128
|
+
"trailingComma": "es5",
|
|
129
|
+
"useTabs": false
|
|
130
|
+
},
|
|
131
|
+
"jest": {
|
|
132
|
+
"preset": "react-native",
|
|
133
|
+
"modulePathIgnorePatterns": [
|
|
134
|
+
"<rootDir>/example/node_modules",
|
|
135
|
+
"<rootDir>/lib/"
|
|
136
|
+
]
|
|
137
|
+
},
|
|
138
|
+
"commitlint": {
|
|
139
|
+
"extends": [
|
|
140
|
+
"@commitlint/config-conventional"
|
|
141
|
+
]
|
|
142
|
+
},
|
|
143
|
+
"release-it": {
|
|
144
|
+
"git": {
|
|
145
|
+
"commitMessage": "chore: release ${version}",
|
|
146
|
+
"tagName": "v${version}"
|
|
147
|
+
},
|
|
148
|
+
"npm": {
|
|
149
|
+
"publish": true
|
|
150
|
+
},
|
|
151
|
+
"github": {
|
|
152
|
+
"release": true
|
|
153
|
+
},
|
|
154
|
+
"plugins": {
|
|
155
|
+
"@release-it/conventional-changelog": {
|
|
156
|
+
"preset": {
|
|
157
|
+
"name": "angular"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
},
|
|
162
|
+
"create-react-native-library": {
|
|
163
|
+
"type": "library",
|
|
164
|
+
"languages": "js",
|
|
165
|
+
"tools": [
|
|
166
|
+
"eslint",
|
|
167
|
+
"jest",
|
|
168
|
+
"lefthook",
|
|
169
|
+
"release-it"
|
|
170
|
+
],
|
|
171
|
+
"version": "0.56.0"
|
|
172
|
+
}
|
|
173
|
+
}
|
package/src/index.tsx
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import {
|
|
2
|
+
useEffect,
|
|
3
|
+
useRef,
|
|
4
|
+
useState,
|
|
5
|
+
useCallback,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
type ReactElement,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import {
|
|
10
|
+
View,
|
|
11
|
+
Animated,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
Easing,
|
|
14
|
+
type LayoutChangeEvent,
|
|
15
|
+
type StyleProp,
|
|
16
|
+
type ViewStyle,
|
|
17
|
+
} from 'react-native';
|
|
18
|
+
|
|
19
|
+
export type GlitterMode = 'normal' | 'expand' | 'shrink';
|
|
20
|
+
|
|
21
|
+
export type GlitterPosition = 'top' | 'center' | 'bottom';
|
|
22
|
+
|
|
23
|
+
export interface GlitterProps {
|
|
24
|
+
children: ReactNode;
|
|
25
|
+
duration?: number;
|
|
26
|
+
delay?: number;
|
|
27
|
+
color?: string;
|
|
28
|
+
angle?: number;
|
|
29
|
+
shimmerWidth?: number;
|
|
30
|
+
active?: boolean;
|
|
31
|
+
style?: StyleProp<ViewStyle>;
|
|
32
|
+
easing?: (value: number) => number;
|
|
33
|
+
mode?: GlitterMode;
|
|
34
|
+
position?: GlitterPosition;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function generateGlitterOpacities(count: number, peak: number = 1): number[] {
|
|
38
|
+
const opacities: number[] = [];
|
|
39
|
+
const center = (count - 1) / 2;
|
|
40
|
+
|
|
41
|
+
for (let i = 0; i < count; i++) {
|
|
42
|
+
const distance = Math.abs(i - center);
|
|
43
|
+
const normalizedDistance = distance / center;
|
|
44
|
+
|
|
45
|
+
let opacity: number;
|
|
46
|
+
if (normalizedDistance < 0.15) {
|
|
47
|
+
opacity = peak;
|
|
48
|
+
} else if (normalizedDistance < 0.3) {
|
|
49
|
+
const t = (normalizedDistance - 0.15) / 0.15;
|
|
50
|
+
opacity = peak * (1 - t * 0.6);
|
|
51
|
+
} else {
|
|
52
|
+
const t = (normalizedDistance - 0.3) / 0.7;
|
|
53
|
+
opacity = peak * 0.4 * Math.pow(1 - t, 2);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
opacities.push(Math.max(0, opacity));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return opacities;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface VerticalSegment {
|
|
63
|
+
heightRatio: number;
|
|
64
|
+
opacity: number;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function generateVerticalSegments(fadeRatioParam?: number): VerticalSegment[] {
|
|
68
|
+
const fadeRatio = fadeRatioParam ?? 0.25;
|
|
69
|
+
const solidRatio = 1 - fadeRatio * 2;
|
|
70
|
+
const fadeSegments = 5;
|
|
71
|
+
|
|
72
|
+
const segments: VerticalSegment[] = [];
|
|
73
|
+
|
|
74
|
+
for (let i = 0; i < fadeSegments; i++) {
|
|
75
|
+
const opacity = (i + 1) / fadeSegments;
|
|
76
|
+
segments.push({
|
|
77
|
+
heightRatio: fadeRatio / fadeSegments,
|
|
78
|
+
opacity: Math.pow(opacity, 2),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
segments.push({
|
|
83
|
+
heightRatio: solidRatio,
|
|
84
|
+
opacity: 1,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
for (let i = fadeSegments - 1; i >= 0; i--) {
|
|
88
|
+
const opacity = (i + 1) / fadeSegments;
|
|
89
|
+
segments.push({
|
|
90
|
+
heightRatio: fadeRatio / fadeSegments,
|
|
91
|
+
opacity: Math.pow(opacity, 2),
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return segments;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function Glitter({
|
|
99
|
+
children,
|
|
100
|
+
duration = 1500,
|
|
101
|
+
delay = 400,
|
|
102
|
+
color = 'rgba(255, 255, 255, 0.8)',
|
|
103
|
+
angle = 20,
|
|
104
|
+
shimmerWidth = 60,
|
|
105
|
+
active = true,
|
|
106
|
+
style,
|
|
107
|
+
easing,
|
|
108
|
+
mode = 'normal',
|
|
109
|
+
position = 'center',
|
|
110
|
+
}: GlitterProps): ReactElement {
|
|
111
|
+
const animatedValue = useRef(new Animated.Value(0)).current;
|
|
112
|
+
const [containerWidth, setContainerWidth] = useState(0);
|
|
113
|
+
const [containerHeight, setContainerHeight] = useState(0);
|
|
114
|
+
const animationRef = useRef<ReturnType<typeof Animated.loop> | null>(null);
|
|
115
|
+
|
|
116
|
+
const defaultEasing = Easing.bezier(0.4, 0, 0.2, 1);
|
|
117
|
+
|
|
118
|
+
const startAnimation = useCallback(() => {
|
|
119
|
+
if (!active || containerWidth === 0) return;
|
|
120
|
+
|
|
121
|
+
animatedValue.setValue(0);
|
|
122
|
+
|
|
123
|
+
const timing = Animated.timing(animatedValue, {
|
|
124
|
+
toValue: 1,
|
|
125
|
+
duration,
|
|
126
|
+
useNativeDriver: true,
|
|
127
|
+
easing: easing ?? defaultEasing,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
animationRef.current = Animated.loop(
|
|
131
|
+
Animated.sequence([timing, Animated.delay(delay)])
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
animationRef.current.start();
|
|
135
|
+
|
|
136
|
+
return () => {
|
|
137
|
+
animationRef.current?.stop();
|
|
138
|
+
};
|
|
139
|
+
}, [
|
|
140
|
+
active,
|
|
141
|
+
containerWidth,
|
|
142
|
+
duration,
|
|
143
|
+
delay,
|
|
144
|
+
animatedValue,
|
|
145
|
+
easing,
|
|
146
|
+
defaultEasing,
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
useEffect(() => {
|
|
150
|
+
const cleanup = startAnimation();
|
|
151
|
+
return cleanup;
|
|
152
|
+
}, [startAnimation]);
|
|
153
|
+
|
|
154
|
+
const onLayout = useCallback((event: LayoutChangeEvent) => {
|
|
155
|
+
const { width, height } = event.nativeEvent.layout;
|
|
156
|
+
setContainerWidth(width);
|
|
157
|
+
setContainerHeight(height);
|
|
158
|
+
}, []);
|
|
159
|
+
|
|
160
|
+
const extraWidth = Math.tan((angle * Math.PI) / 180) * 200;
|
|
161
|
+
|
|
162
|
+
const translateX = animatedValue.interpolate({
|
|
163
|
+
inputRange: [0, 1],
|
|
164
|
+
outputRange: [-shimmerWidth - extraWidth, containerWidth + shimmerWidth],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
const heightMultiplier = 1.5;
|
|
168
|
+
const lineHeight = containerHeight * heightMultiplier;
|
|
169
|
+
|
|
170
|
+
const getScaleY = (): Animated.AnimatedInterpolation<number> | number => {
|
|
171
|
+
if (mode === 'normal') {
|
|
172
|
+
return 1;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (mode === 'expand') {
|
|
176
|
+
return animatedValue.interpolate({
|
|
177
|
+
inputRange: [0, 1],
|
|
178
|
+
outputRange: [0.01, 1],
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return animatedValue.interpolate({
|
|
183
|
+
inputRange: [0, 1],
|
|
184
|
+
outputRange: [1, 0.01],
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const halfHeight = lineHeight / 2;
|
|
189
|
+
const startOffset = 0;
|
|
190
|
+
|
|
191
|
+
const getTransformOriginOffset = (): number => {
|
|
192
|
+
if (mode === 'normal' || position === 'center') {
|
|
193
|
+
return 0;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (position === 'top') {
|
|
197
|
+
return -halfHeight;
|
|
198
|
+
} else {
|
|
199
|
+
return halfHeight;
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
const layerCount = Math.max(11, Math.round(shimmerWidth / 3));
|
|
204
|
+
const horizontalOpacities = generateGlitterOpacities(layerCount, 1);
|
|
205
|
+
const layerWidth = shimmerWidth / layerCount;
|
|
206
|
+
|
|
207
|
+
const normalFadeRatio = (heightMultiplier - 1) / heightMultiplier / 2;
|
|
208
|
+
const normalSegments = generateVerticalSegments(normalFadeRatio);
|
|
209
|
+
const animatedSegments = generateVerticalSegments(0.25);
|
|
210
|
+
|
|
211
|
+
const shimmerLayers = horizontalOpacities.map((opacity, index) => ({
|
|
212
|
+
opacity,
|
|
213
|
+
position: index * layerWidth - shimmerWidth / 2 + layerWidth / 2,
|
|
214
|
+
}));
|
|
215
|
+
|
|
216
|
+
const scaleY = getScaleY();
|
|
217
|
+
const transformOriginOffset = getTransformOriginOffset();
|
|
218
|
+
const isAnimated = mode !== 'normal';
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<View style={[styles.container, style]} onLayout={onLayout}>
|
|
222
|
+
{children}
|
|
223
|
+
{active && containerWidth > 0 && containerHeight > 0 && (
|
|
224
|
+
<Animated.View
|
|
225
|
+
style={[
|
|
226
|
+
styles.shimmerContainer,
|
|
227
|
+
{
|
|
228
|
+
transform: [{ translateX }],
|
|
229
|
+
},
|
|
230
|
+
]}
|
|
231
|
+
pointerEvents="none"
|
|
232
|
+
>
|
|
233
|
+
<View
|
|
234
|
+
style={[
|
|
235
|
+
styles.rotationWrapper,
|
|
236
|
+
{
|
|
237
|
+
width: shimmerWidth,
|
|
238
|
+
height: lineHeight,
|
|
239
|
+
transform: [{ skewX: `${-angle}deg` }],
|
|
240
|
+
},
|
|
241
|
+
]}
|
|
242
|
+
>
|
|
243
|
+
{shimmerLayers.map((layer, layerIndex) => (
|
|
244
|
+
<Animated.View
|
|
245
|
+
key={layerIndex}
|
|
246
|
+
style={[
|
|
247
|
+
styles.shimmerLine,
|
|
248
|
+
{
|
|
249
|
+
width: layerWidth + 0.5,
|
|
250
|
+
height: lineHeight,
|
|
251
|
+
left: layer.position,
|
|
252
|
+
transform: isAnimated
|
|
253
|
+
? [
|
|
254
|
+
{ translateY: startOffset + transformOriginOffset },
|
|
255
|
+
{
|
|
256
|
+
scaleY:
|
|
257
|
+
scaleY as Animated.AnimatedInterpolation<number>,
|
|
258
|
+
},
|
|
259
|
+
{ translateY: -transformOriginOffset },
|
|
260
|
+
]
|
|
261
|
+
: [{ translateY: startOffset }],
|
|
262
|
+
},
|
|
263
|
+
]}
|
|
264
|
+
>
|
|
265
|
+
{(isAnimated ? animatedSegments : normalSegments).map(
|
|
266
|
+
(segment, vIndex) => (
|
|
267
|
+
<View
|
|
268
|
+
key={vIndex}
|
|
269
|
+
style={{
|
|
270
|
+
width: '100%',
|
|
271
|
+
height: lineHeight * segment.heightRatio,
|
|
272
|
+
backgroundColor: color,
|
|
273
|
+
opacity: layer.opacity * segment.opacity,
|
|
274
|
+
}}
|
|
275
|
+
/>
|
|
276
|
+
)
|
|
277
|
+
)}
|
|
278
|
+
</Animated.View>
|
|
279
|
+
))}
|
|
280
|
+
</View>
|
|
281
|
+
</Animated.View>
|
|
282
|
+
)}
|
|
283
|
+
</View>
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const styles = StyleSheet.create({
|
|
288
|
+
container: {
|
|
289
|
+
position: 'relative',
|
|
290
|
+
overflow: 'hidden',
|
|
291
|
+
},
|
|
292
|
+
shimmerContainer: {
|
|
293
|
+
...StyleSheet.absoluteFillObject,
|
|
294
|
+
flexDirection: 'row',
|
|
295
|
+
justifyContent: 'center',
|
|
296
|
+
alignItems: 'center',
|
|
297
|
+
},
|
|
298
|
+
rotationWrapper: {
|
|
299
|
+
flexDirection: 'row',
|
|
300
|
+
alignItems: 'center',
|
|
301
|
+
justifyContent: 'center',
|
|
302
|
+
},
|
|
303
|
+
shimmerLine: {
|
|
304
|
+
position: 'absolute',
|
|
305
|
+
flexDirection: 'column',
|
|
306
|
+
},
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
export default Glitter;
|