├── .eslintrc.js ├── .gitignore ├── LICENSE ├── README.md ├── exmaple └── anchor-point-test │ ├── .expo-shared │ └── assets.json │ ├── .gitignore │ ├── AnchorPointDemo.tsx │ ├── App.tsx │ ├── SmallTestCard.tsx │ ├── TestAnimatedCard.tsx │ ├── TestCard.tsx │ ├── app.json │ ├── assets │ ├── adaptive-icon.png │ ├── favicon.png │ ├── icon.png │ └── splash.png │ ├── babel.config.js │ ├── package.json │ └── tsconfig.json ├── index.ts ├── package.json └── res ├── anchor_point.png ├── external_point.png ├── rotate.gif ├── rotateXY.gif └── rotateZ.gif /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true 4 | }, 5 | "extends": [ 6 | "eslint:recommended", 7 | "plugin:react/recommended", 8 | "plugin:@typescript-eslint/eslint-recommended" 9 | ], 10 | "globals": { 11 | "Atomics": "readonly", 12 | "SharedArrayBuffer": "readonly" 13 | }, 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaFeatures": { 17 | "jsx": true 18 | }, 19 | "ecmaVersion": 11, 20 | "sourceType": "module" 21 | }, 22 | "plugins": [ 23 | "react", 24 | "@typescript-eslint" 25 | ], 26 | "rules": { 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | package-lock.json -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 RY Zheng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # react-native-anchor-point 2 | 3 | 4 | Provide a simple, tricky but powerful function, `withAnchorPoint` , like Anchor Point in iOS, Pivot Point in Android, `transform-origin` in css to achieve better 3D transform animation in React-Native. 5 | 6 | > Make the 3D transform easier in React Native 7 | 8 | ![](./res/rotateZ.gif) 9 | ![](./res/rotateXY.gif) 10 | ![](./res/rotate.gif) 11 | 12 | ## Getting Started 13 | 14 | install the `react-native-anchor-point` 15 | 16 | ``` 17 | yarn add react-native-anchor-point 18 | ``` 19 | or 20 | ``` 21 | npm install react-native-anchor-point 22 | ``` 23 | 24 | ## Example 25 | 26 | ```javascript 27 | import { withAnchorPoint } from 'react-native-anchor-point'; 28 | 29 | getTransform = () => { 30 | let transform = { 31 | transform: [{ perspective: 400 }, { rotateX: rotateValue }], 32 | }; 33 | return withAnchorPoint(transform, { x: 0.5, y: 1.5 }, { width: CARD_WIDTH, height: CARD_HEIGHT }); 34 | }; 35 | 36 | 37 | ``` -------------------------------------------------------------------------------- /exmaple/anchor-point-test/.expo-shared/assets.json: -------------------------------------------------------------------------------- 1 | { 2 | "12bb71342c6255bbf50437ec8f4441c083f47cdb74bd89160c15e4f43e52a1cb": true, 3 | "40b842e832070c58deac6aa9e08fa459302ee3f9da492c7e77d93d2fbf4a56fd": true 4 | } 5 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/AnchorPointDemo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, Text, View, ScrollView, Button, Animated } from 'react-native'; 3 | import TestCard from './TestCard'; 4 | import SmallTestCard from './SmallTestCard'; 5 | import TestAnimatedCard from './TestAnimatedCard'; 6 | 7 | const ExternalAnchorPointViews = () => { 8 | let cards = [] 9 | const initialDegree = -45; 10 | for(let i = 0; i < 2; i++) { 11 | const deg = i * 90 + initialDegree; 12 | cards.push( 13 | 14 | ) 15 | } 16 | 17 | return cards; 18 | } 19 | 20 | export default class AnchorPointDemo extends React.Component { 21 | render() { 22 | return ( 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {ExternalAnchorPointViews()} 36 | 37 | 38 | 39 | 43 | 47 | 48 | 49 | 50 | 54 | 58 | 62 | 63 | 64 | 69 | 74 | 79 | 80 | 81 | 82 | 86 | 90 | 94 | 95 | 96 | 100 | 104 | 108 | 109 | 110 | 111 | 115 | 119 | 123 | 124 | 125 | 129 | 133 | 137 | 138 | 139 | 143 | 147 | 151 | 152 | 153 | ) 154 | } 155 | } 156 | 157 | const styles = StyleSheet.create({ 158 | container: { 159 | flex: 1, 160 | backgroundColor: '#fff', 161 | marginTop: 64 162 | }, 163 | row: { 164 | flexDirection: 'row', 165 | paddingHorizontal: 60, 166 | justifyContent: 'space-between' 167 | }, 168 | marginTop: { 169 | marginTop: 300 170 | }, 171 | zoomIn: { 172 | transform: [{ scale: 0.5 }] 173 | } 174 | }); 175 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/App.tsx: -------------------------------------------------------------------------------- 1 | import { StatusBar } from 'expo-status-bar'; 2 | import React from 'react'; 3 | import { StyleSheet, Text, View } from 'react-native'; 4 | import AnchorPointDemo from './AnchorPointDemo'; 5 | 6 | export default function App() { 7 | return ( 8 | 9 | 10 | 11 | ); 12 | } 13 | 14 | const styles = StyleSheet.create({ 15 | container: { 16 | flex: 1, 17 | backgroundColor: '#fff', 18 | alignItems: 'center', 19 | justifyContent: 'center', 20 | }, 21 | }); 22 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/SmallTestCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { 3 | StyleSheet, 4 | View, 5 | Animated, 6 | ViewProps, 7 | Text, 8 | RotateXTransform, 9 | RotateYTransform, 10 | PerpectiveTransform, 11 | RotateTransform, 12 | RotateZTransform, 13 | ScaleTransform, 14 | ScaleXTransform, 15 | ScaleYTransform, 16 | TranslateXTransform, 17 | TranslateYTransform, 18 | SkewXTransform, 19 | SkewYTransform, 20 | } from 'react-native' 21 | import {withAnchorPoint, Point, Size} from 'react-native-anchor-point' 22 | 23 | interface IProps extends ViewProps { 24 | rotateX?: string | undefined 25 | rotateY?: string | undefined 26 | rotateZ?: string | undefined 27 | anchorPoint: Point 28 | } 29 | 30 | const CARD_WIDTH = 60 31 | const CARD_HEIGHT = 80 32 | 33 | export default class TestCard extends React.Component { 34 | getTransform = () => { 35 | // why perspective: 400 useless when put it at last 36 | let transformValues: ( 37 | | PerpectiveTransform 38 | | RotateTransform 39 | | RotateXTransform 40 | | RotateYTransform 41 | | RotateZTransform 42 | | ScaleTransform 43 | | ScaleXTransform 44 | | ScaleYTransform 45 | | TranslateXTransform 46 | | TranslateYTransform 47 | | SkewXTransform 48 | | SkewYTransform 49 | )[] = [{perspective: 400}] 50 | 51 | if (this.props.rotateX) { 52 | transformValues.push({rotateX: this.props.rotateX}) 53 | } 54 | 55 | if (this.props.rotateY) { 56 | transformValues.push({rotateY: this.props.rotateY}) 57 | } 58 | 59 | if (this.props.rotateZ) { 60 | transformValues.push({rotateZ: this.props.rotateZ}) 61 | } 62 | 63 | let transform = { 64 | transform: transformValues, 65 | } 66 | return withAnchorPoint(transform, this.props.anchorPoint, { 67 | width: CARD_WIDTH, 68 | height: CARD_HEIGHT, 69 | }) 70 | } 71 | 72 | render() { 73 | const {anchorPoint} = this.props 74 | const top = anchorPoint.y * CARD_HEIGHT 75 | const left = anchorPoint.x * CARD_WIDTH 76 | 77 | return ( 78 | 79 | 80 | 81 | 82 | {' '} 83 | (x: {anchorPoint.x}, y: {anchorPoint.y} 84 | 85 | 86 | ) 87 | } 88 | } 89 | 90 | const styles = StyleSheet.create({ 91 | background: { 92 | marginLeft: CARD_WIDTH, 93 | width: CARD_WIDTH, 94 | height: CARD_HEIGHT, 95 | backgroundColor: '#a6abaa', 96 | }, 97 | anchorPoint: { 98 | width: 8, 99 | height: 8, 100 | backgroundColor: '#cc3b92', 101 | position: 'absolute', 102 | }, 103 | blockBlue: { 104 | flex: 1, 105 | backgroundColor: '#03fcd3', 106 | }, 107 | text: { 108 | position: 'absolute', 109 | left: 20, 110 | top: 20, 111 | color: '#bd488b', 112 | }, 113 | }) 114 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/TestAnimatedCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { 3 | StyleSheet, 4 | View, 5 | Animated, 6 | ViewProps, 7 | Text, 8 | RotateXTransform, 9 | RotateYTransform, 10 | PerpectiveTransform, 11 | RotateTransform, 12 | RotateZTransform, 13 | ScaleTransform, 14 | ScaleXTransform, 15 | 16 | ScaleYTransform, 17 | TranslateXTransform, 18 | TranslateYTransform, 19 | SkewXTransform, 20 | SkewYTransform, 21 | Easing, 22 | } from 'react-native'; 23 | import { withAnchorPoint, Point, Size } from 'react-native-anchor-point'; 24 | 25 | 26 | interface IProps extends ViewProps { 27 | rotateOnXAxis?: boolean; 28 | rotateOnYAxis?: boolean; 29 | rotateOnZAxis?: boolean; 30 | anchorPoint: Point; 31 | } 32 | 33 | const CARD_WIDTH = 60; 34 | const CARD_HEIGHT = 60; 35 | 36 | export default class TestAnimatedCard extends React.Component { 37 | _rotateAnimatedValue = new Animated.Value(0); 38 | _animation: any 39 | 40 | componentDidMount() { 41 | this._animation = Animated 42 | .loop( 43 | Animated.timing(this._rotateAnimatedValue, { 44 | toValue: 1, 45 | duration: 1500, 46 | useNativeDriver: true, 47 | easing: Easing.cubic 48 | }), 49 | { iterations: 1000 }, 50 | ) 51 | .start(); 52 | } 53 | 54 | componentWillUnmount() { 55 | if(this._animation) { 56 | this._animation.stop(); 57 | this._animation = null 58 | } 59 | } 60 | 61 | getTransform = () => { 62 | // why perspective: 400 useless when put it at last 63 | let transformValues= [{ perspective: 400 }]; 64 | const rotateValue = this._rotateAnimatedValue.interpolate({ 65 | inputRange: [0, 1], 66 | outputRange: ['0deg', '360deg'], 67 | }) 68 | 69 | const {rotateOnXAxis, rotateOnYAxis, rotateOnZAxis} = this.props; 70 | if (rotateOnXAxis) { 71 | transformValues.push({ rotateX: rotateValue }); 72 | } 73 | 74 | if (rotateOnYAxis) { 75 | transformValues.push({ rotateY: rotateValue }); 76 | } 77 | 78 | if (rotateOnZAxis) { 79 | transformValues.push({ rotateZ: rotateValue }); 80 | } 81 | 82 | let transform = { 83 | transform: transformValues, 84 | }; 85 | return withAnchorPoint(transform, this.props.anchorPoint, { width: CARD_WIDTH, height: CARD_HEIGHT }); 86 | }; 87 | 88 | render() { 89 | const { anchorPoint } = this.props; 90 | const top = anchorPoint.y * CARD_HEIGHT; 91 | const left = anchorPoint.x * CARD_WIDTH; 92 | 93 | return ( 94 | 95 | 96 | 97 | 98 | x: {anchorPoint.x}, y: {anchorPoint.y} 99 | 100 | 101 | ); 102 | } 103 | } 104 | 105 | const styles = StyleSheet.create({ 106 | background: { 107 | margin: 10, 108 | width: CARD_WIDTH, 109 | height: CARD_HEIGHT, 110 | backgroundColor: '#a6abaa', 111 | }, 112 | anchorPoint: { 113 | width: 8, 114 | height: 8, 115 | backgroundColor: '#cc3b92', 116 | position: 'absolute', 117 | }, 118 | blockBlue: { 119 | flex: 1, 120 | backgroundColor: '#03fcd3', 121 | }, 122 | text: { 123 | position: 'absolute', 124 | top: CARD_HEIGHT/2, 125 | color: '#bd488b', 126 | fontSize: 11 127 | }, 128 | }); 129 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/TestCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { StyleSheet, View, Animated, ViewProps, Text, RotateXTransform, RotateYTransform, 3 | PerpectiveTransform, RotateTransform, RotateZTransform, ScaleTransform, 4 | ScaleXTransform, ScaleYTransform, TranslateXTransform, TranslateYTransform, 5 | SkewXTransform, SkewYTransform } from 'react-native'; 6 | import { withAnchorPoint, Point, Size } from 'react-native-anchor-point'; 7 | 8 | interface IProps extends ViewProps { 9 | rotateX?: string | undefined; 10 | rotateY?: string | undefined; 11 | rotateZ?: string | undefined; 12 | anchorPoint: Point; 13 | } 14 | 15 | const CARD_WIDTH = 200; 16 | const CARD_HEIGHT = 200; 17 | 18 | export default class TestCard extends React.Component { 19 | getTransform = () => { 20 | // why perspective: 400 useless when put it at last 21 | let transformValues: 22 | (PerpectiveTransform 23 | | RotateTransform 24 | | RotateXTransform 25 | | RotateYTransform 26 | | RotateZTransform 27 | | ScaleTransform 28 | | ScaleXTransform 29 | | ScaleYTransform 30 | | TranslateXTransform 31 | | TranslateYTransform 32 | | SkewXTransform 33 | | SkewYTransform)[] = [{ perspective: 400 }]; 34 | 35 | if (this.props.rotateX) { 36 | transformValues.push({rotateX: this.props.rotateX}) 37 | } 38 | 39 | if (this.props.rotateY) { 40 | transformValues.push({rotateY: this.props.rotateY}) 41 | } 42 | 43 | if (this.props.rotateZ) { 44 | transformValues.push({rotateZ: this.props.rotateZ}) 45 | } 46 | 47 | let transform = { 48 | transform: transformValues, 49 | }; 50 | return withAnchorPoint(transform, this.props.anchorPoint, { width: CARD_WIDTH, height: CARD_HEIGHT }); 51 | }; 52 | 53 | render() { 54 | const {anchorPoint} = this.props; 55 | const top = anchorPoint.y * CARD_HEIGHT; 56 | const left = anchorPoint.x * CARD_WIDTH; 57 | 58 | return ( 59 | 60 | 61 | 62 | (x: {anchorPoint.x}, y: {anchorPoint.y} 63 | 64 | ); 65 | } 66 | } 67 | 68 | const styles = StyleSheet.create({ 69 | background: { 70 | margin: 50, 71 | width: CARD_WIDTH, 72 | height: CARD_HEIGHT, 73 | backgroundColor: '#a6abaa', 74 | }, 75 | anchorPoint: { 76 | width: 8, 77 | height: 8, 78 | backgroundColor: '#cc3b92', 79 | position: 'absolute', 80 | }, 81 | blockBlue: { 82 | flex: 1, 83 | backgroundColor: '#03fcd3', 84 | }, 85 | text: { 86 | position: 'absolute', 87 | left: 80, 88 | top: 80, 89 | color: '#bd488b', 90 | }, 91 | }); 92 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "anchor-point-test", 4 | "slug": "anchor-point-test", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/icon.png", 8 | "splash": { 9 | "image": "./assets/splash.png", 10 | "resizeMode": "contain", 11 | "backgroundColor": "#ffffff" 12 | }, 13 | "updates": { 14 | "fallbackToCacheTimeout": 0 15 | }, 16 | "assetBundlePatterns": [ 17 | "**/*" 18 | ], 19 | "ios": { 20 | "supportsTablet": true 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./assets/adaptive-icon.png", 25 | "backgroundColor": "#FFFFFF" 26 | } 27 | }, 28 | "web": { 29 | "favicon": "./assets/favicon.png" 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/assets/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/exmaple/anchor-point-test/assets/adaptive-icon.png -------------------------------------------------------------------------------- /exmaple/anchor-point-test/assets/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/exmaple/anchor-point-test/assets/favicon.png -------------------------------------------------------------------------------- /exmaple/anchor-point-test/assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/exmaple/anchor-point-test/assets/icon.png -------------------------------------------------------------------------------- /exmaple/anchor-point-test/assets/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/exmaple/anchor-point-test/assets/splash.png -------------------------------------------------------------------------------- /exmaple/anchor-point-test/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function(api) { 2 | api.cache(true); 3 | return { 4 | presets: ['babel-preset-expo'], 5 | }; 6 | }; 7 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "main": "node_modules/expo/AppEntry.js", 3 | "scripts": { 4 | "start": "expo start", 5 | "android": "expo start --android", 6 | "ios": "expo start --ios", 7 | "web": "expo start --web", 8 | "eject": "expo eject" 9 | }, 10 | "dependencies": { 11 | "expo": "~43.0.2", 12 | "expo-status-bar": "~1.1.0", 13 | "react": "17.0.1", 14 | "react-dom": "17.0.1", 15 | "react-native": "0.64.3", 16 | "react-native-web": "0.17.1", 17 | "react-native-anchor-point": "~1.0.6" 18 | }, 19 | "devDependencies": { 20 | "@babel/core": "^7.12.9", 21 | "@types/react": "~17.0.21", 22 | "@types/react-native": "~0.64.12", 23 | "typescript": "~4.3.5" 24 | }, 25 | "private": true 26 | } 27 | -------------------------------------------------------------------------------- /exmaple/anchor-point-test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "expo/tsconfig.base", 3 | "compilerOptions": { 4 | "strict": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /index.ts: -------------------------------------------------------------------------------- 1 | import type { TransformsStyle } from "react-native"; 2 | 3 | export interface Point { 4 | x: number; 5 | y: number; 6 | } 7 | 8 | export interface Size { 9 | width: number; 10 | height: number; 11 | } 12 | 13 | const isValidSize = (size: Size): boolean => { 14 | return size && size.width > 0 && size.height > 0; 15 | }; 16 | 17 | const defaultAnchorPoint = { x: 0.5, y: 0.5 }; 18 | 19 | export const withAnchorPoint = (transform: TransformsStyle, anchorPoint: Point, size: Size) => { 20 | if(!isValidSize(size)) { 21 | return transform; 22 | } 23 | 24 | let injectedTransform = transform.transform; 25 | if (!injectedTransform) { 26 | return transform; 27 | } 28 | 29 | if (anchorPoint.x !== defaultAnchorPoint.x && size.width) { 30 | const shiftTranslateX = []; 31 | 32 | // shift before rotation 33 | shiftTranslateX.push({ 34 | translateX: size.width * (anchorPoint.x - defaultAnchorPoint.x), 35 | }); 36 | injectedTransform = [...shiftTranslateX, ...injectedTransform]; 37 | // shift after rotation 38 | injectedTransform.push({ 39 | translateX: size.width * (defaultAnchorPoint.x - anchorPoint.x), 40 | }); 41 | } 42 | 43 | if (!Array.isArray(injectedTransform)) { 44 | return { transform: injectedTransform }; 45 | } 46 | 47 | if (anchorPoint.y !== defaultAnchorPoint.y && size.height) { 48 | let shiftTranslateY = []; 49 | // shift before rotation 50 | shiftTranslateY.push({ 51 | translateY: size.height * (anchorPoint.y - defaultAnchorPoint.y), 52 | }); 53 | injectedTransform = [...shiftTranslateY, ...injectedTransform]; 54 | // shift after rotation 55 | injectedTransform.push({ 56 | translateY: size.height * (defaultAnchorPoint.y - anchorPoint.y), 57 | }); 58 | } 59 | 60 | return { transform: injectedTransform }; 61 | }; 62 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-anchor-point", 3 | "version": "1.0.6", 4 | "description": "I provide a util function to inject anchor point, or called pivot point into your transform object, to achieve 3D transform animation in React-Native. This is similar to web transform-origin in React-Native. ", 5 | "main": "index.ts", 6 | "repository": "https://github.com/sueLan/react-native-anchor-point.git", 7 | "keywords": [ 8 | "react-native", 9 | "transform origin", 10 | "3d transform", 11 | "anchor point", 12 | "pivot point" 13 | ], 14 | "author": "RongyanZheng ", 15 | "license": "MIT", 16 | "private": false, 17 | "dependencies": {}, 18 | "devDependencies": { 19 | "@typescript-eslint/eslint-plugin": "^2.33.0", 20 | "@typescript-eslint/parser": "^2.33.0", 21 | "eslint": "^7.0.0", 22 | "eslint-plugin-react": "^7.20.0", 23 | "react-native": "^0.62.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /res/anchor_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/res/anchor_point.png -------------------------------------------------------------------------------- /res/external_point.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/res/external_point.png -------------------------------------------------------------------------------- /res/rotate.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/res/rotate.gif -------------------------------------------------------------------------------- /res/rotateXY.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/res/rotateXY.gif -------------------------------------------------------------------------------- /res/rotateZ.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sueLan/react-native-anchor-point/bf6b6a4e9037c41364993fed5631a8cc946f7324/res/rotateZ.gif --------------------------------------------------------------------------------