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 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
+ ![npm](https://img.shields.io/npm/v/react-native-glitter)
8
+ ![license](https://img.shields.io/npm/l/react-native-glitter)
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;