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 +102 -0
- package/lib/index.d.ts +34 -0
- package/lib/index.js +143 -0
- package/package.json +39 -0
- package/src/index.tsx +159 -0
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
|
+
|