react-native-pixel-launch 1.0.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/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # react-native-pixel-launch
2
+
3
+ Pixel Launcher-style scale-from-origin overlay animation for React Native and Expo.
4
+
5
+ Opens a full-screen overlay that scales up from any element on screen (like Android's Pixel Launcher app-open animation), and collapses back on close.
6
+
7
+ ## Features
8
+
9
+ - Scales from any screen element to full screen
10
+ - Circular reveal on open, collapses back on close
11
+ - Runs on the native thread (`useNativeDriver: true`) for smooth 60/120 Hz performance
12
+ - Works with both Expo and bare React Native
13
+ - TypeScript support built-in
14
+ - Zero dependencies (only `react` and `react-native`)
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install react-native-pixel-launch
20
+ # or
21
+ yarn add react-native-pixel-launch
22
+ ```
23
+
24
+ ## Usage
25
+
26
+ ```tsx
27
+ import { useState, useRef } from "react";
28
+ import { View, TouchableOpacity, Text } from "react-native";
29
+ import {
30
+ PixelLaunchContainer,
31
+ type LaunchOrigin,
32
+ } from "react-native-pixel-launch";
33
+
34
+ export default function App() {
35
+ const [visible, setVisible] = useState(false);
36
+ const [origin, setOrigin] = useState<LaunchOrigin | null>(null);
37
+ const btnRef = useRef<View>(null);
38
+
39
+ const handleOpen = () => {
40
+ btnRef.current?.measure((_x, _y, width, height, pageX, pageY) => {
41
+ setOrigin({ x: pageX, y: pageY, width, height });
42
+ setVisible(true);
43
+ });
44
+ };
45
+
46
+ return (
47
+ <View style={{ flex: 1 }}>
48
+ <TouchableOpacity ref={btnRef} onPress={handleOpen}>
49
+ <Text>Open</Text>
50
+ </TouchableOpacity>
51
+
52
+ <PixelLaunchContainer
53
+ visible={visible}
54
+ origin={origin}
55
+ onClose={() => setVisible(false)}
56
+ onDismissed={() => console.log("fully closed")}
57
+ backgroundColor="#FFFFFF"
58
+ >
59
+ <View style={{ flex: 1, alignItems: "center", justifyContent: "center" }}>
60
+ <Text>Your screen content here</Text>
61
+ <TouchableOpacity onPress={() => setVisible(false)}>
62
+ <Text>Close</Text>
63
+ </TouchableOpacity>
64
+ </View>
65
+ </PixelLaunchContainer>
66
+ </View>
67
+ );
68
+ }
69
+ ```
70
+
71
+ ## Props
72
+
73
+ | Prop | Type | Required | Default | Description |
74
+ |------|------|----------|---------|-------------|
75
+ | `visible` | `boolean` | Yes | — | Controls open/close state |
76
+ | `origin` | `LaunchOrigin \| null` | Yes | — | Screen-absolute rect of the trigger element |
77
+ | `onClose` | `() => void` | Yes | — | Called when user wants to close |
78
+ | `onDismissed` | `() => void` | No | — | Called after close animation completes |
79
+ | `backgroundColor` | `string` | No | `"#FFFFFF"` | Overlay background colour |
80
+ | `children` | `ReactNode` | Yes | — | Content rendered inside the overlay |
81
+
82
+ ## LaunchOrigin type
83
+
84
+ ```ts
85
+ type LaunchOrigin = {
86
+ x: number; // pageX from ref.measure()
87
+ y: number; // pageY from ref.measure()
88
+ width: number;
89
+ height: number;
90
+ };
91
+ ```
92
+
93
+ Get these values using `ref.measure()` on the trigger element — see the usage example above.
94
+
95
+ ## License
96
+
97
+ MIT — made by [Sourabh Patidar](https://github.com/Saurabh0904)
98
+ # react-native-pixel-launch-
99
+ # sourabh0904-react-native-pixel-launch
100
+ # sourabh0904-react-native-pixel-launch
101
+ # sourabh0904-react-native-pixel-launch
102
+ # react-native-pixel-launch
package/lib/index.d.ts ADDED
@@ -0,0 +1,34 @@
1
+ import React from "react";
2
+ export type LaunchOrigin = {
3
+ x: number;
4
+ y: number;
5
+ width: number;
6
+ height: number;
7
+ };
8
+ export interface PixelLaunchContainerProps {
9
+ /** Controls visibility — drives open/close animation. */
10
+ visible: boolean;
11
+ /** Screen-absolute rect of the element this overlay expands from. */
12
+ origin: LaunchOrigin | null;
13
+ /** Called when the user wants to close (e.g. back button). */
14
+ onClose: () => void;
15
+ /** Called after the close animation fully completes — safe to navigate here. */
16
+ onDismissed?: () => void;
17
+ /** Background colour of the overlay. Defaults to "#FFFFFF". */
18
+ backgroundColor?: string;
19
+ children: React.ReactNode;
20
+ }
21
+ /**
22
+ * Pixel-Launcher-style overlay: scales up from an origin rect to fill the
23
+ * screen, and collapses back on close.
24
+ *
25
+ * Transform math (true "scale from origin"):
26
+ * Every point p transforms as: p' = s·p + (OX, OY)·(1−s)
27
+ * At s=0 → all points collapse to the origin center.
28
+ * At s=1 → overlay fills the screen.
29
+ *
30
+ * transform + opacity → native thread (useNativeDriver: true, 60/120 Hz).
31
+ * borderRadius → JS thread, starts at SCREEN_W/2 (circle) and
32
+ * collapses to 0 as the overlay opens.
33
+ */
34
+ export declare function PixelLaunchContainer({ visible, origin, onClose: _onClose, onDismissed, backgroundColor, children, }: PixelLaunchContainerProps): React.JSX.Element | null;
package/lib/index.js ADDED
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.PixelLaunchContainer = PixelLaunchContainer;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const { width: SCREEN_W, height: SCREEN_H } = react_native_1.Dimensions.get("window");
40
+ /**
41
+ * Pixel-Launcher-style overlay: scales up from an origin rect to fill the
42
+ * screen, and collapses back on close.
43
+ *
44
+ * Transform math (true "scale from origin"):
45
+ * Every point p transforms as: p' = s·p + (OX, OY)·(1−s)
46
+ * At s=0 → all points collapse to the origin center.
47
+ * At s=1 → overlay fills the screen.
48
+ *
49
+ * transform + opacity → native thread (useNativeDriver: true, 60/120 Hz).
50
+ * borderRadius → JS thread, starts at SCREEN_W/2 (circle) and
51
+ * collapses to 0 as the overlay opens.
52
+ */
53
+ function PixelLaunchContainer({ visible, origin, onClose: _onClose, onDismissed, backgroundColor = "#FFFFFF", children, }) {
54
+ const progress = (0, react_1.useRef)(new react_native_1.Animated.Value(0)).current;
55
+ const radiusAnim = (0, react_1.useRef)(new react_native_1.Animated.Value(SCREEN_W / 2)).current;
56
+ const [mounted, setMounted] = (0, react_1.useState)(false);
57
+ const [activeOrigin, setActiveOrigin] = (0, react_1.useState)(null);
58
+ const hasOpenedRef = (0, react_1.useRef)(false);
59
+ (0, react_1.useEffect)(() => {
60
+ if (visible && origin) {
61
+ hasOpenedRef.current = true;
62
+ setActiveOrigin(origin);
63
+ setMounted(true);
64
+ progress.setValue(0);
65
+ radiusAnim.setValue(SCREEN_W / 2);
66
+ react_native_1.Animated.parallel([
67
+ // Open — underdamped spring (~380 ms), gentle elastic overshoot
68
+ react_native_1.Animated.spring(progress, {
69
+ toValue: 1,
70
+ tension: 200,
71
+ friction: 16,
72
+ useNativeDriver: true,
73
+ }),
74
+ // Radius trails scale slightly for a natural circle→flat reveal
75
+ react_native_1.Animated.spring(radiusAnim, {
76
+ toValue: 0,
77
+ tension: 160,
78
+ friction: 18,
79
+ useNativeDriver: false,
80
+ }),
81
+ ]).start();
82
+ }
83
+ else if (!visible && hasOpenedRef.current) {
84
+ react_native_1.Animated.parallel([
85
+ // Close — overdamped, no bounce (~280 ms)
86
+ react_native_1.Animated.spring(progress, {
87
+ toValue: 0,
88
+ tension: 280,
89
+ friction: 28,
90
+ useNativeDriver: true,
91
+ }),
92
+ // Radius matches close cadence
93
+ react_native_1.Animated.spring(radiusAnim, {
94
+ toValue: SCREEN_W / 2,
95
+ tension: 240,
96
+ friction: 28,
97
+ useNativeDriver: false,
98
+ }),
99
+ ]).start(({ finished }) => {
100
+ if (finished) {
101
+ hasOpenedRef.current = false;
102
+ setMounted(false);
103
+ onDismissed === null || onDismissed === void 0 ? void 0 : onDismissed();
104
+ }
105
+ });
106
+ }
107
+ }, [visible]);
108
+ if (!mounted || !activeOrigin)
109
+ return null;
110
+ // ── Animation math ───────────────────────────────────────────────────────────
111
+ const originCX = activeOrigin.x + activeOrigin.width / 2;
112
+ const originCY = activeOrigin.y + activeOrigin.height / 2;
113
+ const OX = originCX - SCREEN_W / 2;
114
+ const OY = originCY - SCREEN_H / 2;
115
+ // Scale 0 → 1 so the overlay fully disappears (size 0) on close
116
+ const scale = progress.interpolate({
117
+ inputRange: [0, 1],
118
+ outputRange: [0, 1],
119
+ });
120
+ // offset = OX * (1 − scale): at scale=0 overlay centre sits on origin centre
121
+ const translateX = progress.interpolate({
122
+ inputRange: [0, 1],
123
+ outputRange: [OX, 0],
124
+ });
125
+ const translateY = progress.interpolate({
126
+ inputRange: [0, 1],
127
+ outputRange: [OY, 0],
128
+ });
129
+ // Appear near-instantly on open; scale→0 on close eliminates the box visually
130
+ const opacity = progress.interpolate({
131
+ inputRange: [0, 0.05, 1],
132
+ outputRange: [0, 1, 1],
133
+ extrapolate: "clamp",
134
+ });
135
+ return (react_1.default.createElement(react_native_1.Animated.View, { style: [{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }, { zIndex: 200, elevation: 200, opacity }] },
136
+ react_1.default.createElement(react_native_1.Animated.View, { style: { flex: 1, transform: [{ translateX }, { translateY }, { scale }] } },
137
+ react_1.default.createElement(react_native_1.Animated.View, { style: {
138
+ flex: 1,
139
+ backgroundColor,
140
+ borderRadius: radiusAnim,
141
+ overflow: "hidden",
142
+ } }, children))));
143
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "react-native-pixel-launch",
3
+ "version": "1.0.0",
4
+ "description": "Pixel Launcher-style scale-from-origin overlay animation for React Native",
5
+ "main": "lib/index.js",
6
+ "module": "lib/index.js",
7
+ "types": "lib/index.d.ts",
8
+ "files": [
9
+ "lib",
10
+ "src"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "prepare": "npm run build"
15
+ },
16
+ "keywords": [
17
+ "react-native",
18
+ "animation",
19
+ "pixel-launcher",
20
+ "overlay",
21
+ "scale",
22
+ "transition",
23
+ "expo"
24
+ ],
25
+ "author": "Sourabh Patidar",
26
+ "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "https://github.com/Saurabh0904/react-native-pixel-launch.git"
30
+ },
31
+ "peerDependencies": {
32
+ "react": ">=17.0.0",
33
+ "react-native": ">=0.68.0"
34
+ },
35
+ "devDependencies": {
36
+ "@types/react": "^19.0.0",
37
+ "typescript": "^5.0.0"
38
+ }
39
+ }
package/src/index.tsx ADDED
@@ -0,0 +1,159 @@
1
+ import React, { useEffect, useRef, useState } from "react";
2
+ import { Animated, Dimensions } from "react-native";
3
+
4
+ const { width: SCREEN_W, height: SCREEN_H } = Dimensions.get("window");
5
+
6
+ export type LaunchOrigin = {
7
+ x: number;
8
+ y: number;
9
+ width: number;
10
+ height: number;
11
+ };
12
+
13
+ export interface PixelLaunchContainerProps {
14
+ /** Controls visibility — drives open/close animation. */
15
+ visible: boolean;
16
+ /** Screen-absolute rect of the element this overlay expands from. */
17
+ origin: LaunchOrigin | null;
18
+ /** Called when the user wants to close (e.g. back button). */
19
+ onClose: () => void;
20
+ /** Called after the close animation fully completes — safe to navigate here. */
21
+ onDismissed?: () => void;
22
+ /** Background colour of the overlay. Defaults to "#FFFFFF". */
23
+ backgroundColor?: string;
24
+ children: React.ReactNode;
25
+ }
26
+
27
+ /**
28
+ * Pixel-Launcher-style overlay: scales up from an origin rect to fill the
29
+ * screen, and collapses back on close.
30
+ *
31
+ * Transform math (true "scale from origin"):
32
+ * Every point p transforms as: p' = s·p + (OX, OY)·(1−s)
33
+ * At s=0 → all points collapse to the origin center.
34
+ * At s=1 → overlay fills the screen.
35
+ *
36
+ * transform + opacity → native thread (useNativeDriver: true, 60/120 Hz).
37
+ * borderRadius → JS thread, starts at SCREEN_W/2 (circle) and
38
+ * collapses to 0 as the overlay opens.
39
+ */
40
+ export function PixelLaunchContainer({
41
+ visible,
42
+ origin,
43
+ onClose: _onClose,
44
+ onDismissed,
45
+ backgroundColor = "#FFFFFF",
46
+ children,
47
+ }: PixelLaunchContainerProps) {
48
+ const progress = useRef(new Animated.Value(0)).current;
49
+ const radiusAnim = useRef(new Animated.Value(SCREEN_W / 2)).current;
50
+
51
+ const [mounted, setMounted] = useState(false);
52
+ const [activeOrigin, setActiveOrigin] = useState<LaunchOrigin | null>(null);
53
+ const hasOpenedRef = useRef(false);
54
+
55
+ useEffect(() => {
56
+ if (visible && origin) {
57
+ hasOpenedRef.current = true;
58
+ setActiveOrigin(origin);
59
+ setMounted(true);
60
+ progress.setValue(0);
61
+ radiusAnim.setValue(SCREEN_W / 2);
62
+
63
+ Animated.parallel([
64
+ // Open — underdamped spring (~380 ms), gentle elastic overshoot
65
+ Animated.spring(progress, {
66
+ toValue: 1,
67
+ tension: 200,
68
+ friction: 16,
69
+ useNativeDriver: true,
70
+ }),
71
+ // Radius trails scale slightly for a natural circle→flat reveal
72
+ Animated.spring(radiusAnim, {
73
+ toValue: 0,
74
+ tension: 160,
75
+ friction: 18,
76
+ useNativeDriver: false,
77
+ }),
78
+ ]).start();
79
+
80
+ } else if (!visible && hasOpenedRef.current) {
81
+ Animated.parallel([
82
+ // Close — overdamped, no bounce (~280 ms)
83
+ Animated.spring(progress, {
84
+ toValue: 0,
85
+ tension: 280,
86
+ friction: 28,
87
+ useNativeDriver: true,
88
+ }),
89
+ // Radius matches close cadence
90
+ Animated.spring(radiusAnim, {
91
+ toValue: SCREEN_W / 2,
92
+ tension: 240,
93
+ friction: 28,
94
+ useNativeDriver: false,
95
+ }),
96
+ ]).start(({ finished }) => {
97
+ if (finished) {
98
+ hasOpenedRef.current = false;
99
+ setMounted(false);
100
+ onDismissed?.();
101
+ }
102
+ });
103
+ }
104
+ }, [visible]);
105
+
106
+ if (!mounted || !activeOrigin) return null;
107
+
108
+ // ── Animation math ───────────────────────────────────────────────────────────
109
+ const originCX = activeOrigin.x + activeOrigin.width / 2;
110
+ const originCY = activeOrigin.y + activeOrigin.height / 2;
111
+ const OX = originCX - SCREEN_W / 2;
112
+ const OY = originCY - SCREEN_H / 2;
113
+
114
+ // Scale 0 → 1 so the overlay fully disappears (size 0) on close
115
+ const scale = progress.interpolate({
116
+ inputRange: [0, 1],
117
+ outputRange: [0, 1],
118
+ });
119
+
120
+ // offset = OX * (1 − scale): at scale=0 overlay centre sits on origin centre
121
+ const translateX = progress.interpolate({
122
+ inputRange: [0, 1],
123
+ outputRange: [OX, 0],
124
+ });
125
+ const translateY = progress.interpolate({
126
+ inputRange: [0, 1],
127
+ outputRange: [OY, 0],
128
+ });
129
+
130
+ // Appear near-instantly on open; scale→0 on close eliminates the box visually
131
+ const opacity = progress.interpolate({
132
+ inputRange: [0, 0.05, 1],
133
+ outputRange: [0, 1, 1],
134
+ extrapolate: "clamp",
135
+ });
136
+
137
+ return (
138
+ <Animated.View
139
+ style={[{ position: "absolute", top: 0, left: 0, right: 0, bottom: 0 }, { zIndex: 200, elevation: 200, opacity }]}
140
+ >
141
+ <Animated.View
142
+ style={{ flex: 1, transform: [{ translateX }, { translateY }, { scale }] }}
143
+ >
144
+ <Animated.View
145
+ style={{
146
+ flex: 1,
147
+ backgroundColor,
148
+ borderRadius: radiusAnim,
149
+ overflow: "hidden",
150
+ }}
151
+ >
152
+ {children}
153
+ </Animated.View>
154
+ </Animated.View>
155
+ </Animated.View>
156
+ );
157
+ }
158
+
159
+