├── .circleci └── config.yml ├── .eslintrc ├── .github └── FUNDING.yml ├── .gitignore ├── LICENSE ├── README.md ├── babel.config.js ├── jest-setup.js ├── jest.config.js ├── package.json ├── src ├── Animations.ts ├── Array.ts ├── Colors.ts ├── Coordinates.ts ├── Math.ts ├── Matrix3.ts ├── Matrix4.ts ├── Paths.ts ├── Physics.ts ├── ReText.tsx ├── Transforms.ts ├── Transitions.ts ├── Vectors.ts ├── __tests__ │ ├── Array.test.ts │ ├── Coordinates.test.ts │ ├── Math.test.ts │ ├── Matrix3.test.ts │ ├── Matrix4.test.ts │ ├── Paths.test.ts │ ├── Physics.test.ts │ ├── index.ts │ ├── matrix.ts │ └── paths.ts ├── index.d.ts └── index.ts ├── tsconfig.json └── yarn.lock /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Javascript Node CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-javascript/ for more details 4 | # 5 | version: 2 6 | jobs: 7 | build: 8 | docker: 9 | # specify the version you desire here 10 | - image: circleci/node:10.17.0 11 | 12 | # Specify service dependencies here if necessary 13 | # CircleCI maintains a library of pre-built images 14 | # documented at https://circleci.com/docs/2.0/circleci-images/ 15 | # - image: circleci/mongo:3.4.4 16 | 17 | working_directory: ~/repo 18 | 19 | steps: 20 | - checkout 21 | 22 | # Download and cache dependencies 23 | - restore_cache: 24 | keys: 25 | - v1-dependencies-{{ checksum "package.json" }} 26 | # fallback to using the latest cache if no exact match is found 27 | - v1-dependencies- 28 | 29 | - run: yarn install 30 | 31 | - save_cache: 32 | paths: 33 | - node_modules 34 | key: v1-dependencies-{{ checksum "package.json" }} 35 | 36 | # run tests! 37 | - run: yarn ci 38 | 39 | - deploy: 40 | name: NPM Deployment 41 | command: | 42 | if [ "${CIRCLE_BRANCH}" == "master" ]; then 43 | yarn semantic-release 44 | fi 45 | 46 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "react-native-wcandillon", 3 | "ignorePatterns": ["lib/**/*"], 4 | "rules": { 5 | "no-bitwise": 0, 6 | "import/extensions": [ 7 | 2, 8 | { 9 | "extensions": [ 10 | ".ts", 11 | ".tsx" 12 | ] 13 | } 14 | ], 15 | "import/no-named-as-default-member": 0 16 | }, 17 | "env": { 18 | "jest": true 19 | } 20 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | custom: https://react-native.shop/buy-me-a-coffee 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # docz 9 | .docz 10 | 11 | # Runtime data 12 | pids 13 | *.pid 14 | *.seed 15 | *.pid.lock 16 | 17 | # Directory for instrumented libs generated by jscoverage/JSCover 18 | lib-cov 19 | 20 | # Coverage directory used by tools like istanbul 21 | coverage 22 | 23 | # nyc test coverage 24 | .nyc_output 25 | 26 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 27 | .grunt 28 | 29 | # Bower dependency directory (https://bower.io/) 30 | bower_components 31 | 32 | # node-waf configuration 33 | .lock-wscript 34 | 35 | # Compiled binary addons (https://nodejs.org/api/addons.html) 36 | build/Release 37 | 38 | # Dependency directories 39 | node_modules/ 40 | jspm_packages/ 41 | 42 | # TypeScript v1 declaration files 43 | typings/ 44 | 45 | # Optional npm cache directory 46 | .npm 47 | 48 | # Optional eslint cache 49 | .eslintcache 50 | 51 | # Optional REPL history 52 | .node_repl_history 53 | 54 | # Output of 'npm pack' 55 | *.tgz 56 | 57 | # Yarn Integrity file 58 | .yarn-integrity 59 | 60 | # dotenv environment variables file 61 | .env 62 | 63 | # next.js build output 64 | .next 65 | 66 | # generated by bob 67 | lib/ 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 William Candillon 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 | # redash 2 | 3 | [![CircleCI](https://circleci.com/gh/wcandillon/react-native-redash.svg?style=svg)](https://circleci.com/gh/wcandillon/react-native-redash) 4 | [![npm version](https://badge.fury.io/js/react-native-redash.svg)](https://badge.fury.io/js/react-native-redash) 5 | 6 | The React Native Reanimated and Gesture Handler Toolbelt. As seen on the [“Can it be done in React Native?”](http://youtube.com/user/wcandill) YouTube series. 7 | 8 | ## Installation 9 | 10 | ```sh 11 | yarn add react-native-redash 12 | ``` 13 | 14 | ## Documentation 15 | 16 | [https://wcandillon.gitbook.io/redash/](https://wcandillon.gitbook.io/redash/) 17 | 18 | ## ⚠️ Reanimated v1 ⚠️ 19 | 20 | Please use `v14.2.2` 21 | v1 documentation: https://wcandillon.github.io/react-native-redash-v1-docs/ 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | // babel.config.js 2 | module.exports = { 3 | presets: ["babel-preset-expo"], 4 | plugins: [], 5 | }; 6 | -------------------------------------------------------------------------------- /jest-setup.js: -------------------------------------------------------------------------------- 1 | require("react-native-reanimated/lib/reanimated2/jestUtils").setUpTests(); 2 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | // For a detailed explanation regarding each configuration property, visit: 2 | // https://jestjs.io/docs/en/configuration.html 3 | 4 | module.exports = { 5 | // All imported modules in your tests should be mocked automatically 6 | // automock: false, 7 | 8 | // Stop running tests after `n` failures 9 | // bail: 0, 10 | 11 | // Respect "browser" field in package.json when resolving modules 12 | // browser: false, 13 | 14 | // The directory where Jest should store its cached dependency information 15 | // cacheDirectory: "/private/var/folders/xf/hkqnmwhs2894qycxl5cx3hf40000gn/T/jest_dx", 16 | 17 | // Automatically clear mock calls and instances between every test 18 | // clearMocks: false, 19 | 20 | // Indicates whether the coverage information should be collected while executing the test 21 | // collectCoverage: false, 22 | 23 | // An array of glob patterns indicating a set of files for which coverage information should be collected 24 | // collectCoverageFrom: null, 25 | 26 | // The directory where Jest should output its coverage files 27 | // coverageDirectory: null, 28 | 29 | // An array of regexp pattern strings used to skip coverage collection 30 | // coveragePathIgnorePatterns: [ 31 | // "/node_modules/" 32 | // ], 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | // coverageReporters: [ 36 | // "json", 37 | // "text", 38 | // "lcov", 39 | // "clover" 40 | // ], 41 | 42 | // An object that configures minimum threshold enforcement for coverage results 43 | // coverageThreshold: null, 44 | 45 | // A path to a custom dependency extractor 46 | // dependencyExtractor: null, 47 | 48 | // Make calling deprecated APIs throw helpful error messages 49 | // errorOnDeprecated: false, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: null, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: null, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "json", 75 | // "jsx", 76 | // "ts", 77 | // "tsx", 78 | // "node" 79 | // ], 80 | 81 | // A map from regular expressions to module names that allow to stub out resources with a single module 82 | // moduleNameMapper: {}, 83 | 84 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 85 | // modulePathIgnorePatterns: [], 86 | 87 | // Activates notifications for test results 88 | // notify: false, 89 | 90 | // An enum that specifies notification mode. Requires { notify: true } 91 | // notifyMode: "failure-change", 92 | 93 | // A preset that is used as a base for Jest's configuration 94 | preset: "react-native", 95 | 96 | // Run tests from one or more projects 97 | // projects: null, 98 | 99 | // Use this configuration option to add custom reporters to Jest 100 | // reporters: undefined, 101 | 102 | // Automatically reset mock state between every test 103 | // resetMocks: false, 104 | 105 | // Reset the module registry before running each individual test 106 | // resetModules: false, 107 | 108 | // A path to a custom resolver 109 | // resolver: null, 110 | 111 | // Automatically restore mock state between every test 112 | // restoreMocks: false, 113 | 114 | // The root directory that Jest should scan for tests and modules within 115 | // rootDir: null, 116 | 117 | // A list of paths to directories that Jest should use to search for files in 118 | roots: ["src"], 119 | 120 | // Allows you to use a custom runner instead of Jest's default test runner 121 | // runner: "jest-runner", 122 | 123 | // The paths to modules that run some code to configure or set up the testing environment before each test 124 | setupFiles: ["./jest-setup.js"], 125 | 126 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 127 | // setupFilesAfterEnv: [], 128 | 129 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 130 | // snapshotSerializers: [], 131 | 132 | // The test environment that will be used for testing 133 | testEnvironment: "node", 134 | 135 | // Options that will be passed to the testEnvironment 136 | // testEnvironmentOptions: {}, 137 | 138 | // Adds a location field to test results 139 | // testLocationInResults: false, 140 | 141 | // The glob patterns Jest uses to detect test files 142 | testMatch: ["/**/*.test.ts"], 143 | 144 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 145 | // testPathIgnorePatterns: [ 146 | // "/node_modules/" 147 | // ], 148 | 149 | // The regexp pattern or array of patterns that Jest uses to detect test files 150 | // testRegex: [], 151 | 152 | // This option allows the use of a custom results processor 153 | // testResultsProcessor: null, 154 | 155 | // This option allows use of a custom test runner 156 | // testRunner: "jasmine2", 157 | 158 | // This option sets the URL for the jsdom environment. It is reflected in properties such as location.href 159 | // testURL: "http://localhost", 160 | 161 | // Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout" 162 | // timers: "real", 163 | 164 | // A map from regular expressions to paths to transformers 165 | // transform: null, 166 | 167 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 168 | // transformIgnorePatterns: [ 169 | // "/node_modules/" 170 | // ], 171 | 172 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 173 | // unmockedModulePathPatterns: undefined, 174 | 175 | // Indicates whether each individual test should be reported during the run 176 | // verbose: null, 177 | 178 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 179 | // watchPathIgnorePatterns: [], 180 | 181 | // Whether to use watchman for file crawling 182 | // watchman: true, 183 | }; 184 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-redash", 3 | "version": "0.0.0-development", 4 | "description": "Utility library for React Native Reanimated", 5 | "sideEffects": false, 6 | "main": "lib/module/index.js", 7 | "scripts": { 8 | "lint": "eslint --ext .ts,.tsx . --max-warnings 0", 9 | "tsc": "tsc --noEmit", 10 | "test": "jest", 11 | "ci": "yarn lint && yarn tsc && yarn test", 12 | "prepare": "bob build", 13 | "semantic-release": "semantic-release" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/wcandillon/redash.git" 18 | }, 19 | "author": "William Candillon", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/wcandillon/redash/issues" 23 | }, 24 | "homepage": "https://github.com/wcandillon/redash#readme", 25 | "peerDependencies": { 26 | "react": "*", 27 | "react-native": "*", 28 | "react-native-gesture-handler": "*", 29 | "react-native-reanimated": ">=2.0.0" 30 | }, 31 | "devDependencies": { 32 | "@react-native-community/bob": "^0.4.1", 33 | "@types/color": "^3.0.0", 34 | "@types/jest": "^25.1.4", 35 | "@types/react": "*", 36 | "@types/react-native": "0.63.20", 37 | "babel-jest": "^24.9.0", 38 | "babel-preset-expo": "^8.0.0", 39 | "eslint": "^7.8.1", 40 | "eslint-config-react-native-wcandillon": "3.6.3", 41 | "jest": "^24.9.0", 42 | "react": "^16.8.6", 43 | "react-native": "^0.62.3", 44 | "react-native-gesture-handler": "~1.5.0", 45 | "react-native-reanimated": "~2.8.0", 46 | "semantic-release": "^15.13.3", 47 | "semantic-release-cli": "^4.1.2", 48 | "typescript": "4.3.5" 49 | }, 50 | "react-native": "lib/module/index.js", 51 | "module": "lib/module/index.js", 52 | "types": "lib/typescript/index.d.ts", 53 | "files": [ 54 | "src", 55 | "lib" 56 | ], 57 | "@react-native-community/bob": { 58 | "source": "src", 59 | "output": "lib", 60 | "targets": [ 61 | "module", 62 | "typescript" 63 | ] 64 | }, 65 | "dependencies": { 66 | "abs-svg-path": "^0.1.1", 67 | "normalize-svg-path": "^1.0.1", 68 | "parse-svg-path": "^0.1.2" 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Animations.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/consistent-type-imports */ 2 | import type { AnimatableValue, Animation } from "react-native-reanimated"; 3 | import Animated, { defineAnimation } from "react-native-reanimated"; 4 | 5 | interface PausableAnimation extends Animation { 6 | lastTimestamp: number; 7 | elapsed: number; 8 | } 9 | 10 | export const withPause = ( 11 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 12 | _nextAnimation: any, 13 | paused: Animated.SharedValue 14 | ) => { 15 | "worklet"; 16 | return defineAnimation(_nextAnimation, () => { 17 | "worklet"; 18 | const nextAnimation: PausableAnimation = 19 | typeof _nextAnimation === "function" ? _nextAnimation() : _nextAnimation; 20 | const onFrame = (state: PausableAnimation, now: number) => { 21 | const { lastTimestamp, elapsed } = state; 22 | if (paused.value) { 23 | state.elapsed = now - lastTimestamp; 24 | return false; 25 | } 26 | const dt = now - elapsed; 27 | const finished = nextAnimation.onFrame(nextAnimation, dt); 28 | state.current = nextAnimation.current; 29 | state.lastTimestamp = dt; 30 | return finished; 31 | }; 32 | const onStart = ( 33 | state: PausableAnimation, 34 | value: AnimatableValue, 35 | now: number, 36 | previousState: PausableAnimation 37 | ) => { 38 | state.lastTimestamp = now; 39 | state.elapsed = 0; 40 | state.current = 0; 41 | nextAnimation.onStart(nextAnimation, value, now, previousState); 42 | }; 43 | const callback = (finished?: boolean): void => { 44 | if (nextAnimation.callback) { 45 | nextAnimation.callback(finished); 46 | } 47 | }; 48 | return { 49 | onFrame, 50 | onStart, 51 | isHigherOrder: true, 52 | current: nextAnimation.current, 53 | callback, 54 | previousAnimation: null, 55 | startTime: 0, 56 | started: false, 57 | lastTimestamp: 0, 58 | elapsed: 0, 59 | }; 60 | }); 61 | }; 62 | 63 | export interface PhysicsAnimation extends Animation { 64 | velocity: number; 65 | current: number; 66 | } 67 | 68 | /** 69 | * @summary Add a bouncing behavior to a physics-based animation. 70 | * An animation is defined as being physics-based if it contains a velocity in its state. 71 | * @example 72 | // will bounce if the animations hits the position 0 or 100 73 | withBouncing(withDecay({ velocity }), 0, 100) 74 | * @worklet 75 | */ 76 | export const withBouncing = ( 77 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 78 | _nextAnimation: any, 79 | lowerBound: number, 80 | upperBound: number 81 | ): number => { 82 | "worklet"; 83 | return defineAnimation(_nextAnimation, () => { 84 | "worklet"; 85 | 86 | const nextAnimation: PhysicsAnimation = 87 | typeof _nextAnimation === "function" ? _nextAnimation() : _nextAnimation; 88 | 89 | const onFrame = (state: PhysicsAnimation, now: number) => { 90 | const finished = nextAnimation.onFrame(nextAnimation, now); 91 | const { velocity, current } = nextAnimation; 92 | state.current = current; 93 | if ( 94 | (velocity < 0 && state.current <= lowerBound) || 95 | (velocity > 0 && state.current >= upperBound) 96 | ) { 97 | state.current = velocity < 0 ? lowerBound : upperBound; 98 | nextAnimation.velocity *= -0.5; 99 | } 100 | return finished; 101 | }; 102 | const onStart = ( 103 | _state: PhysicsAnimation, 104 | value: number, 105 | now: number, 106 | previousState: PhysicsAnimation 107 | ) => { 108 | nextAnimation.onStart(nextAnimation, value, now, previousState); 109 | }; 110 | return { 111 | onFrame, 112 | onStart, 113 | current: nextAnimation.current, 114 | callback: nextAnimation.callback, 115 | velocity: 0, 116 | }; 117 | }); 118 | }; 119 | -------------------------------------------------------------------------------- /src/Array.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @worklet 3 | */ 4 | export const move = (input: T[], from: number, to: number) => { 5 | "worklet"; 6 | const offsets = input.slice(); 7 | while (from < 0) { 8 | from += offsets.length; 9 | } 10 | while (to < 0) { 11 | to += offsets.length; 12 | } 13 | if (to >= offsets.length) { 14 | let k = to - offsets.length; 15 | while (k-- + 1) { 16 | offsets.push(); 17 | } 18 | } 19 | offsets.splice(to, 0, offsets.splice(from, 1)[0]); 20 | return offsets; 21 | }; 22 | -------------------------------------------------------------------------------- /src/Colors.ts: -------------------------------------------------------------------------------- 1 | import { interpolateColor } from "react-native-reanimated"; 2 | 3 | import { clamp, fract, mix } from "./Math"; 4 | 5 | export type AnimatedColor = string | number; 6 | 7 | /** 8 | * @summary Returns black or white depending on the value of the background color. 9 | * @worklet 10 | */ 11 | export const isLight = (r: number, g: number, b: number) => { 12 | "worklet"; 13 | const L = 0.299 * r + 0.587 * g + 0.114 * b; 14 | return L > 186; 15 | }; 16 | 17 | /** 18 | * Interpolate color from 0 to 1 19 | * @param value 20 | * @param color1 21 | * @param color2 22 | * @param colorSpace 23 | * @worklet 24 | */ 25 | export const mixColor = ( 26 | value: number, 27 | color1: AnimatedColor, 28 | color2: AnimatedColor, 29 | colorSpace: "RGB" | "HSV" = "RGB" 30 | ) => { 31 | "worklet"; 32 | return interpolateColor(value, [0, 1], [color1, color2], colorSpace); 33 | }; 34 | 35 | export const hsv2rgb = (h: number, s: number, v: number) => { 36 | "worklet"; 37 | const K = { 38 | x: 1, 39 | y: 2 / 3, 40 | z: 1 / 3, 41 | w: 3, 42 | }; 43 | const p = { 44 | x: Math.abs(fract(h + K.x) * 6 - K.w), 45 | y: Math.abs(fract(h + K.y) * 6 - K.w), 46 | z: Math.abs(fract(h + K.z) * 6 - K.w), 47 | }; 48 | // return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y); 49 | const rgb = { 50 | x: v * mix(s, K.x, clamp(p.x - K.x, 0, 1)), 51 | y: v * mix(s, K.x, clamp(p.y - K.x, 0, 1)), 52 | z: v * mix(s, K.x, clamp(p.z - K.x, 0, 1)), 53 | }; 54 | 55 | const r = Math.round(rgb.x * 255); 56 | const g = Math.round(rgb.y * 255); 57 | const b = Math.round(rgb.z * 255); 58 | 59 | return { r, g, b }; 60 | }; 61 | 62 | export const opacity = (c: number): number => { 63 | "worklet"; 64 | return ((c >> 24) & 255) / 255; 65 | }; 66 | 67 | export const red = (c: number): number => { 68 | "worklet"; 69 | return (c >> 16) & 255; 70 | }; 71 | 72 | export const green = (c: number): number => { 73 | "worklet"; 74 | return (c >> 8) & 255; 75 | }; 76 | 77 | export const blue = (c: number): number => { 78 | "worklet"; 79 | return c & 255; 80 | }; 81 | -------------------------------------------------------------------------------- /src/Coordinates.ts: -------------------------------------------------------------------------------- 1 | import type { Vector } from "./Vectors"; 2 | 3 | export interface PolarPoint { 4 | theta: number; 5 | radius: number; 6 | } 7 | 8 | /** 9 | * @worklet 10 | */ 11 | export const canvas2Cartesian = (v: Vector, center: Vector) => { 12 | "worklet"; 13 | return { 14 | x: v.x - center.x, 15 | y: -1 * (v.y - center.y), 16 | }; 17 | }; 18 | 19 | /** 20 | * @worklet 21 | */ 22 | export const cartesian2Canvas = (v: Vector, center: Vector) => { 23 | "worklet"; 24 | return { 25 | x: v.x + center.x, 26 | y: -1 * v.y + center.y, 27 | }; 28 | }; 29 | 30 | /** 31 | * @worklet 32 | */ 33 | export const cartesian2Polar = (v: Vector) => { 34 | "worklet"; 35 | return { 36 | theta: Math.atan2(v.y, v.x), 37 | radius: Math.sqrt(v.x ** 2 + v.y ** 2), 38 | }; 39 | }; 40 | 41 | /** 42 | * @worklet 43 | */ 44 | export const polar2Cartesian = (p: PolarPoint) => { 45 | "worklet"; 46 | return { 47 | x: p.radius * Math.cos(p.theta), 48 | y: p.radius * Math.sin(p.theta), 49 | }; 50 | }; 51 | 52 | /** 53 | * @worklet 54 | */ 55 | export const polar2Canvas = (p: PolarPoint, center: Vector) => { 56 | "worklet"; 57 | return cartesian2Canvas(polar2Cartesian(p), center); 58 | }; 59 | 60 | /** 61 | * @worklet 62 | */ 63 | export const canvas2Polar = (v: Vector, center: Vector) => { 64 | "worklet"; 65 | return cartesian2Polar(canvas2Cartesian(v, center)); 66 | }; 67 | -------------------------------------------------------------------------------- /src/Math.ts: -------------------------------------------------------------------------------- 1 | import type { Vector } from "./Vectors"; 2 | 3 | export const { PI } = Math; 4 | export const TAU = PI * 2; 5 | 6 | /** 7 | * @summary Convert a boolean value into a number. 8 | * This can be useful in reanimated since 0 and 1 are used for conditional statements. 9 | * @worklet 10 | */ 11 | export const bin = (value: boolean): 0 | 1 => { 12 | "worklet"; 13 | return value ? 1 : 0; 14 | }; 15 | 16 | /** 17 | * Linear interpolation 18 | * @param value 19 | * @param x 20 | * @param y 21 | * @worklet 22 | */ 23 | export const mix = (value: number, x: number, y: number) => { 24 | "worklet"; 25 | return x * (1 - value) + y * value; 26 | }; 27 | 28 | /** 29 | * @summary Check is value is almost equal to the target. 30 | * @worklet 31 | */ 32 | export const approximates = ( 33 | value: number, 34 | target: number, 35 | epsilon = 0.001 36 | ) => { 37 | "worklet"; 38 | return Math.abs(value - target) < epsilon; 39 | }; 40 | 41 | /** 42 | * @summary Normalize any radian value between 0 and 2PI. 43 | * For example, if the value is -PI/2, it will be comverted to 1.5PI. 44 | * Or 4PI will be converted to 0. 45 | * @worklet 46 | */ 47 | export const normalizeRad = (value: number) => { 48 | "worklet"; 49 | const rest = value % TAU; 50 | return rest > 0 ? rest : TAU + rest; 51 | }; 52 | 53 | /** 54 | * @summary Transforms an angle from radians to degrees. 55 | * @worklet 56 | */ 57 | export const toDeg = (rad: number) => { 58 | "worklet"; 59 | return (rad * 180) / Math.PI; 60 | }; 61 | 62 | /** 63 | * @summary Transforms an angle from degrees to radians. 64 | * @worklet 65 | */ 66 | export const toRad = (deg: number) => { 67 | "worklet"; 68 | return (deg * Math.PI) / 180; 69 | }; 70 | 71 | /** 72 | * 73 | * @summary Returns the average value 74 | * @worklet 75 | */ 76 | export const avg = (values: number[]) => { 77 | "worklet"; 78 | return values.reduce((a, v) => a + v, 0) / values.length; 79 | }; 80 | 81 | /** 82 | * @summary Returns true if node is within lowerBound and upperBound. 83 | * @worklet 84 | */ 85 | export const between = ( 86 | value: number, 87 | lowerBound: number, 88 | upperBound: number, 89 | inclusive = true 90 | ) => { 91 | "worklet"; 92 | if (inclusive) { 93 | return value >= lowerBound && value <= upperBound; 94 | } 95 | return value > lowerBound && value < upperBound; 96 | }; 97 | 98 | /** 99 | * @summary Clamps a node with a lower and upper bound. 100 | * @example 101 | clamp(-1, 0, 100); // 0 102 | clamp(1, 0, 100); // 1 103 | clamp(101, 0, 100); // 100 104 | * @worklet 105 | */ 106 | export const clamp = ( 107 | value: number, 108 | lowerBound: number, 109 | upperBound: number 110 | ) => { 111 | "worklet"; 112 | return Math.min(Math.max(lowerBound, value), upperBound); 113 | }; 114 | 115 | /** 116 | * @description Returns the coordinate of a cubic bezier curve. t is the length of the curve from 0 to 1. 117 | * cubicBezier(0, p0, p1, p2, p3) equals p0 and cubicBezier(1, p0, p1, p2, p3) equals p3. 118 | * p0 and p3 are respectively the starting and ending point of the curve. p1 and p2 are the control points. 119 | * @worklet 120 | */ 121 | export const cubicBezier = ( 122 | t: number, 123 | from: number, 124 | c1: number, 125 | c2: number, 126 | to: number 127 | ) => { 128 | "worklet"; 129 | const term = 1 - t; 130 | const a = 1 * term ** 3 * t ** 0 * from; 131 | const b = 3 * term ** 2 * t ** 1 * c1; 132 | const c = 3 * term ** 1 * t ** 2 * c2; 133 | const d = 1 * term ** 0 * t ** 3 * to; 134 | return a + b + c + d; 135 | }; 136 | 137 | /** 138 | * @summary Computes animation node rounded to precision. 139 | * @worklet 140 | */ 141 | export const round = (value: number, precision = 0) => { 142 | "worklet"; 143 | const p = Math.pow(10, precision); 144 | return Math.round(value * p) / p; 145 | }; 146 | 147 | // https://stackoverflow.com/questions/27176423/function-to-solve-cubic-equation-analytically 148 | const cuberoot = (x: number) => { 149 | "worklet"; 150 | const y = Math.pow(Math.abs(x), 1 / 3); 151 | return x < 0 ? -y : y; 152 | }; 153 | 154 | const solveCubic = (a: number, b: number, c: number, d: number) => { 155 | "worklet"; 156 | if (Math.abs(a) < 1e-8) { 157 | // Quadratic case, ax^2+bx+c=0 158 | a = b; 159 | b = c; 160 | c = d; 161 | if (Math.abs(a) < 1e-8) { 162 | // Linear case, ax+b=0 163 | a = b; 164 | b = c; 165 | if (Math.abs(a) < 1e-8) { 166 | // Degenerate case 167 | return []; 168 | } 169 | return [-b / a]; 170 | } 171 | 172 | const D = b * b - 4 * a * c; 173 | if (Math.abs(D) < 1e-8) { 174 | return [-b / (2 * a)]; 175 | } else if (D > 0) { 176 | return [(-b + Math.sqrt(D)) / (2 * a), (-b - Math.sqrt(D)) / (2 * a)]; 177 | } 178 | return []; 179 | } 180 | 181 | // Convert to depressed cubic t^3+pt+q = 0 (subst x = t - b/3a) 182 | const p = (3 * a * c - b * b) / (3 * a * a); 183 | const q = (2 * b * b * b - 9 * a * b * c + 27 * a * a * d) / (27 * a * a * a); 184 | let roots; 185 | 186 | if (Math.abs(p) < 1e-8) { 187 | // p = 0 -> t^3 = -q -> t = -q^1/3 188 | roots = [cuberoot(-q)]; 189 | } else if (Math.abs(q) < 1e-8) { 190 | // q = 0 -> t^3 + pt = 0 -> t(t^2+p)=0 191 | roots = [0].concat(p < 0 ? [Math.sqrt(-p), -Math.sqrt(-p)] : []); 192 | } else { 193 | const D = (q * q) / 4 + (p * p * p) / 27; 194 | if (Math.abs(D) < 1e-8) { 195 | // D = 0 -> two roots 196 | roots = [(-1.5 * q) / p, (3 * q) / p]; 197 | } else if (D > 0) { 198 | // Only one real root 199 | const u = cuberoot(-q / 2 - Math.sqrt(D)); 200 | roots = [u - p / (3 * u)]; 201 | } else { 202 | // D < 0, three roots, but needs to use complex numbers/trigonometric solution 203 | const u = 2 * Math.sqrt(-p / 3); 204 | const t = Math.acos((3 * q) / p / u) / 3; // D < 0 implies p < 0 and acos argument in [-1..1] 205 | const k = (2 * Math.PI) / 3; 206 | roots = [u * Math.cos(t), u * Math.cos(t - k), u * Math.cos(t - 2 * k)]; 207 | } 208 | } 209 | 210 | // Convert back from depressed cubic 211 | for (let i = 0; i < roots.length; i++) { 212 | roots[i] -= b / (3 * a); 213 | } 214 | 215 | return roots; 216 | }; 217 | 218 | /** 219 | * @summary Given a cubic Bèzier curve, return the y value for x. 220 | * @example 221 | const x = 116; 222 | const from = vec.create(59, 218); 223 | const c1 = vec.create(131, 39); 224 | const c2 = vec.create(204, 223); 225 | const to = vec.create(227, 89); 226 | // y= 139 227 | const y = cubicBezierYForX(x, from, c1, c2, to))); 228 | * @worklet 229 | */ 230 | export const cubicBezierYForX = ( 231 | x: number, 232 | a: Vector, 233 | b: Vector, 234 | c: Vector, 235 | d: Vector, 236 | precision = 2 237 | ) => { 238 | "worklet"; 239 | const pa = -a.x + 3 * b.x - 3 * c.x + d.x; 240 | const pb = 3 * a.x - 6 * b.x + 3 * c.x; 241 | const pc = -3 * a.x + 3 * b.x; 242 | const pd = a.x - x; 243 | // eslint-disable-next-line prefer-destructuring 244 | const t = solveCubic(pa, pb, pc, pd) 245 | .map((root) => round(root, precision)) 246 | .filter((root) => root >= 0 && root <= 1)[0]; 247 | return cubicBezier(t, a.y, b.y, c.y, d.y); 248 | }; 249 | 250 | export const fract = (x: number) => { 251 | "worklet"; 252 | return x - Math.floor(x); 253 | }; 254 | -------------------------------------------------------------------------------- /src/Matrix3.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable prefer-destructuring */ 2 | export type Vec2 = readonly [number, number]; 3 | type Vec3 = readonly [number, number, number]; 4 | 5 | export type Matrix3 = readonly [ 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number, 12 | number, 13 | number, 14 | number 15 | ]; 16 | 17 | export interface TransformProp { 18 | transform: Transforms2d; 19 | } 20 | 21 | type Transformations = { 22 | translateX: number; 23 | translateY: number; 24 | scale: number; 25 | skewX: string; 26 | skewY: string; 27 | scaleX: number; 28 | scaleY: number; 29 | rotateZ: string; 30 | rotate: string; 31 | }; 32 | 33 | export type Transforms2d = ( 34 | | Pick 35 | | Pick 36 | | Pick 37 | | Pick 38 | | Pick 39 | | Pick 40 | | Pick 41 | | Pick 42 | | Pick 43 | )[]; 44 | 45 | /** 46 | * @worklet 47 | */ 48 | export const parseAngle = (angle: string) => { 49 | "worklet"; 50 | if (angle.endsWith("deg")) { 51 | return parseFloat(angle) * (Math.PI / 180); 52 | } 53 | return parseFloat(angle); 54 | }; 55 | 56 | /** 57 | * @worklet 58 | */ 59 | export const isTranslateX = ( 60 | transform: Transforms2d[0] 61 | ): transform is Pick => { 62 | "worklet"; 63 | return Object.keys(transform).indexOf("translateX") !== -1; 64 | }; 65 | 66 | /** 67 | * @worklet 68 | */ 69 | export const isTranslateY = ( 70 | transform: Transforms2d[0] 71 | ): transform is Pick => { 72 | "worklet"; 73 | return Object.keys(transform).indexOf("translateY") !== -1; 74 | }; 75 | 76 | /** 77 | * @worklet 78 | */ 79 | export const isScale = ( 80 | transform: Transforms2d[0] 81 | ): transform is Pick => { 82 | "worklet"; 83 | return Object.keys(transform).indexOf("scale") !== -1; 84 | }; 85 | 86 | /** 87 | * @worklet 88 | */ 89 | export const isScaleX = ( 90 | transform: Transforms2d[0] 91 | ): transform is Pick => { 92 | "worklet"; 93 | return Object.keys(transform).indexOf("scaleX") !== -1; 94 | }; 95 | 96 | /** 97 | * @worklet 98 | */ 99 | export const isScaleY = ( 100 | transform: Transforms2d[0] 101 | ): transform is Pick => { 102 | "worklet"; 103 | return Object.keys(transform).indexOf("scaleY") !== -1; 104 | }; 105 | 106 | /** 107 | * @worklet 108 | */ 109 | export const isSkewX = ( 110 | transform: Transforms2d[0] 111 | ): transform is Pick => { 112 | "worklet"; 113 | return Object.keys(transform).indexOf("skewX") !== -1; 114 | }; 115 | 116 | /** 117 | * @worklet 118 | */ 119 | export const isSkewY = ( 120 | transform: Transforms2d[0] 121 | ): transform is Pick => { 122 | "worklet"; 123 | return Object.keys(transform).indexOf("skewY") !== -1; 124 | }; 125 | 126 | /** 127 | * @worklet 128 | */ 129 | export const isRotate = ( 130 | transform: Transforms2d[0] 131 | ): transform is Pick => { 132 | "worklet"; 133 | return Object.keys(transform).indexOf("rotate") !== -1; 134 | }; 135 | 136 | /** 137 | * @worklet 138 | */ 139 | export const isRotateZ = ( 140 | transform: Transforms2d[0] 141 | ): transform is Pick => { 142 | "worklet"; 143 | return Object.keys(transform).indexOf("rotateZ") !== -1; 144 | }; 145 | 146 | /** 147 | * @worklet 148 | */ 149 | const exhaustiveCheck = (a: never): never => { 150 | "worklet"; 151 | throw new Error(`Unexhaustive handling for ${a}`); 152 | }; 153 | 154 | export const identity3: Matrix3 = [1, 0, 0, 0, 1, 0, 0, 0, 1]; 155 | 156 | /** 157 | * @worklet 158 | */ 159 | const translateXMatrix = (x: number): Matrix3 => { 160 | "worklet"; 161 | return [1, 0, x, 0, 1, 0, 0, 0, 1]; 162 | }; 163 | 164 | /** 165 | * @worklet 166 | */ 167 | const translateYMatrix = (y: number): Matrix3 => { 168 | "worklet"; 169 | return [1, 0, 0, 0, 1, y, 0, 0, 1]; 170 | }; 171 | 172 | /** 173 | * @worklet 174 | */ 175 | const scaleMatrix = (s: number): Matrix3 => { 176 | "worklet"; 177 | return [s, 0, 0, 0, s, 0, 0, 0, 1]; 178 | }; 179 | 180 | /** 181 | * @worklet 182 | */ 183 | const scaleXMatrix = (s: number): Matrix3 => { 184 | "worklet"; 185 | return [s, 0, 0, 0, 1, 0, 0, 0, 1]; 186 | }; 187 | 188 | /** 189 | * @worklet 190 | */ 191 | const scaleYMatrix = (s: number): Matrix3 => { 192 | "worklet"; 193 | return [1, 0, 0, 0, s, 0, 0, 0, 1]; 194 | }; 195 | 196 | /** 197 | * @worklet 198 | */ 199 | const skewXMatrix = (s: number): Matrix3 => { 200 | "worklet"; 201 | return [1, Math.tan(s), 0, 0, 1, 0, 0, 0, 1]; 202 | }; 203 | 204 | /** 205 | * @worklet 206 | */ 207 | const skewYMatrix = (s: number): Matrix3 => { 208 | "worklet"; 209 | return [1, 0, 0, Math.tan(s), 1, 0, 0, 0, 1]; 210 | }; 211 | 212 | /** 213 | * @worklet 214 | */ 215 | const rotateZMatrix = (r: number): Matrix3 => { 216 | "worklet"; 217 | return [ 218 | Math.cos(r), 219 | -1 * Math.sin(r), 220 | 0, 221 | Math.sin(r), 222 | Math.cos(r), 223 | 0, 224 | 0, 225 | 0, 226 | 1, 227 | ]; 228 | }; 229 | 230 | /** 231 | * @worklet 232 | */ 233 | export const dot3 = (row: Vec3, col: Vec3) => { 234 | "worklet"; 235 | return row[0] * col[0] + row[1] * col[1] + row[2] * col[2]; 236 | }; 237 | 238 | /** 239 | * @worklet 240 | */ 241 | export const matrixVecMul3 = (m: Matrix3, v: Vec3) => { 242 | "worklet"; 243 | return [ 244 | dot3([m[0], m[1], m[2]], v), 245 | dot3([m[3], m[4], m[5]], v), 246 | dot3([m[6], m[7], m[8]], v), 247 | ] as const; 248 | }; 249 | 250 | /** 251 | * @worklet 252 | */ 253 | export const mapPoint = (m: Matrix3, v: Vec2) => { 254 | "worklet"; 255 | const r = matrixVecMul3(m, [v[0], v[1], 1]); 256 | return [r[0] / r[2], r[1] / r[2]] as const; 257 | }; 258 | 259 | /** 260 | * @worklet 261 | */ 262 | export const multiply3 = (m1: Matrix3, m2: Matrix3) => { 263 | "worklet"; 264 | const row0 = [m1[0], m1[1], m1[2]] as const; 265 | const row1 = [m1[3], m1[4], m1[5]] as const; 266 | const row2 = [m1[6], m1[7], m1[8]] as const; 267 | const col0 = [m2[0], m2[3 + 0], m2[6 + 0]] as const; 268 | const col1 = [m2[1], m2[3 + 1], m2[6 + 1]] as const; 269 | const col2 = [m2[2], m2[3 + 2], m2[6 + 2]] as const; 270 | return [ 271 | dot3(row0, col0), 272 | dot3(row0, col1), 273 | dot3(row0, col2), 274 | dot3(row1, col0), 275 | dot3(row1, col1), 276 | dot3(row1, col2), 277 | dot3(row2, col0), 278 | dot3(row2, col1), 279 | dot3(row2, col2), 280 | ] as const; 281 | }; 282 | 283 | /** 284 | * @worklet 285 | */ 286 | const serializeToSVGMatrix = (m: Matrix3) => { 287 | "worklet"; 288 | return `matrix(${m[0]}, ${m[3 + 0]}, ${m[1]}, ${m[3 + 1]}, ${m[2]}, ${ 289 | m[3 + 2] 290 | })`; 291 | }; 292 | 293 | /** 294 | * @worklet 295 | */ 296 | export const svgMatrix = (transforms: Transforms2d) => { 297 | "worklet"; 298 | return serializeToSVGMatrix(processTransform2d(transforms)); 299 | }; 300 | 301 | /** 302 | * @worklet 303 | */ 304 | export const processTransform2d = (transforms: Transforms2d) => { 305 | "worklet"; 306 | return transforms.reduce((acc, transform) => { 307 | if (isTranslateX(transform)) { 308 | return multiply3(acc, translateXMatrix(transform.translateX)); 309 | } 310 | if (isTranslateY(transform)) { 311 | return multiply3(acc, translateYMatrix(transform.translateY)); 312 | } 313 | if (isScale(transform)) { 314 | return multiply3(acc, scaleMatrix(transform.scale)); 315 | } 316 | if (isScaleX(transform)) { 317 | return multiply3(acc, scaleXMatrix(transform.scaleX)); 318 | } 319 | if (isScaleY(transform)) { 320 | return multiply3(acc, scaleYMatrix(transform.scaleY)); 321 | } 322 | if (isSkewX(transform)) { 323 | return multiply3(acc, skewXMatrix(parseAngle(transform.skewX))); 324 | } 325 | if (isSkewY(transform)) { 326 | return multiply3(acc, skewYMatrix(parseAngle(transform.skewY))); 327 | } 328 | if (isRotate(transform)) { 329 | return multiply3(acc, rotateZMatrix(parseAngle(transform.rotate))); 330 | } 331 | if (isRotateZ(transform)) { 332 | return multiply3(acc, rotateZMatrix(parseAngle(transform.rotateZ))); 333 | } 334 | return exhaustiveCheck(transform); 335 | }, identity3); 336 | }; 337 | 338 | /** 339 | * @worklet 340 | */ 341 | const isMatrix3 = (arg: Matrix3 | Transforms2d): arg is Matrix3 => { 342 | "worklet"; 343 | return arg.length === 9 && arg[0] instanceof Array; 344 | }; 345 | 346 | // https://math.stackexchange.com/questions/13150/extracting-rotation-scale-values-from-2d-transformation-matrix 347 | 348 | // https://math.stackexchange.com/questions/296794/finding-the-transform-matrix-from-4-projected-points-with-javascript 349 | // https://franklinta.com/2014/09/08/computing-css-matrix3d-transforms/ 350 | // http://jsfiddle.net/dFrHS/1/ 351 | /** 352 | * @worklet 353 | */ 354 | export const decompose2d = (arg: Matrix3 | Transforms2d) => { 355 | "worklet"; 356 | const m = isMatrix3(arg) ? arg : processTransform2d(arg); 357 | const a = m[0]; 358 | const b = m[3 + 0]; 359 | const c = m[1]; 360 | const d = m[3 + 1]; 361 | const translateX = m[2]; 362 | const translateY = m[3 + 2]; 363 | const E = (a + d) / 2; 364 | const F = (a - d) / 2; 365 | const G = (c + b) / 2; 366 | const H = (c - b) / 2; 367 | const Q = Math.sqrt(Math.pow(E, 2) + Math.pow(H, 2)); 368 | const R = Math.sqrt(Math.pow(F, 2) + Math.pow(G, 2)); 369 | const scaleX = Q + R; 370 | const scaleY = Q - R; 371 | const a1 = Math.atan2(G, F); 372 | const a2 = Math.atan2(H, E); 373 | const theta = (a2 - a1) / 2; 374 | const phi = (a2 + a1) / 2; 375 | return [ 376 | { translateX }, 377 | { translateY }, 378 | { rotateZ: -1 * theta }, 379 | { scaleX }, 380 | { scaleY }, 381 | { rotateZ: -1 * phi }, 382 | ] as const; 383 | }; 384 | -------------------------------------------------------------------------------- /src/Matrix4.ts: -------------------------------------------------------------------------------- 1 | type Vec2 = readonly [number, number]; 2 | type Vec3 = readonly [number, number, number]; 3 | type Vec4 = readonly [number, number, number, number]; 4 | 5 | export type Matrix4 = readonly [ 6 | number, 7 | number, 8 | number, 9 | number, 10 | number, 11 | number, 12 | number, 13 | number, 14 | number, 15 | number, 16 | number, 17 | number, 18 | number, 19 | number, 20 | number, 21 | number 22 | ]; 23 | 24 | type Transform3dName = 25 | | "translateX" 26 | | "translateY" 27 | | "translateZ" 28 | | "translate" 29 | | "scale" 30 | | "scaleX" 31 | | "scaleY" 32 | | "skewX" 33 | | "skewY" 34 | | "rotateZ" 35 | | "rotate" 36 | | "perspective" 37 | | "rotateX" 38 | | "rotateY" 39 | | "rotateZ" 40 | | "matrix"; 41 | 42 | type Transformations = { 43 | [Name in Transform3dName]: Name extends "matrix" 44 | ? Matrix4 45 | : Name extends "translate" 46 | ? Vec3 | Vec2 47 | : number; 48 | }; 49 | 50 | type Transform3d = 51 | | Pick 52 | | Pick 53 | | Pick 54 | | Pick 55 | | Pick 56 | | Pick 57 | | Pick 58 | | Pick 59 | | Pick 60 | | Pick 61 | | Pick 62 | | Pick 63 | | Pick 64 | | Pick 65 | | Pick; 66 | 67 | export type Transforms3d = Transform3d[]; 68 | 69 | const exhaustiveCheck = (a: never): never => { 70 | "worklet"; 71 | throw new Error(`Unexhaustive handling for ${a}`); 72 | }; 73 | 74 | /** 75 | * @worklet 76 | */ 77 | export const identity4: Matrix4 = [ 78 | 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 79 | ]; 80 | 81 | /** 82 | * @worklet 83 | */ 84 | export const translate = (x: number, y: number, z: number): Matrix4 => { 85 | "worklet"; 86 | return [1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1]; 87 | }; 88 | 89 | /** 90 | * @worklet 91 | */ 92 | const scale = (sx: number, sy: number, sz: number): Matrix4 => { 93 | "worklet"; 94 | return [sx, 0, 0, 0, 0, sy, 0, 0, 0, 0, sz, 0, 0, 0, 0, 1]; 95 | }; 96 | 97 | /** 98 | * @worklet 99 | */ 100 | export const skewX = (s: number): Matrix4 => { 101 | "worklet"; 102 | return [1, 0, 0, 0, Math.tan(s), 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; 103 | }; 104 | 105 | /** 106 | * @worklet 107 | */ 108 | export const skewY = (s: number): Matrix4 => { 109 | "worklet"; 110 | return [1, Math.tan(s), 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1]; 111 | }; 112 | 113 | /** 114 | * @worklet 115 | */ 116 | export const perspective = (p: number): Matrix4 => { 117 | "worklet"; 118 | return [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, -1 / p, 1]; 119 | }; 120 | 121 | const normalizeVec = (vec: Vec3): Vec3 => { 122 | "worklet"; 123 | const [x, y, z] = vec; 124 | const length = Math.sqrt(x * x + y * y + z * z); 125 | // Check for zero length to avoid division by zero 126 | if (length === 0) { 127 | return [0, 0, 0]; 128 | } 129 | return [x / length, y / length, z / length]; 130 | }; 131 | 132 | const rotatedUnitSinCos = ( 133 | axisVec: Vec3, 134 | sinAngle: number, 135 | cosAngle: number 136 | ): Matrix4 => { 137 | "worklet"; 138 | const [x, y, z] = axisVec; 139 | const c = cosAngle; 140 | const s = sinAngle; 141 | const t = 1 - c; 142 | return [ 143 | t * x * x + c, 144 | t * x * y - s * z, 145 | t * x * z + s * y, 146 | 0, 147 | t * x * y + s * z, 148 | t * y * y + c, 149 | t * y * z - s * x, 150 | 0, 151 | t * x * z - s * y, 152 | t * y * z + s * x, 153 | t * z * z + c, 154 | 0, 155 | 0, 156 | 0, 157 | 0, 158 | 1, 159 | ]; 160 | }; 161 | 162 | /** 163 | * @worklet 164 | */ 165 | export const rotate = (axis: Vec3, value: number) => { 166 | "worklet"; 167 | return rotatedUnitSinCos( 168 | normalizeVec(axis), 169 | Math.sin(value), 170 | Math.cos(value) 171 | ); 172 | }; 173 | 174 | /** 175 | * @worklet 176 | */ 177 | export const matrixVecMul4 = (m: Matrix4, v: Vec4): Vec4 => { 178 | "worklet"; 179 | return [ 180 | m[0] * v[0] + m[1] * v[1] + m[2] * v[2] + m[3] * v[3], 181 | m[4] * v[0] + m[5] * v[1] + m[6] * v[2] + m[7] * v[3], 182 | m[8] * v[0] + m[9] * v[1] + m[10] * v[2] + m[11] * v[3], 183 | m[12] * v[0] + m[13] * v[1] + m[14] * v[2] + m[15] * v[3], 184 | ]; 185 | }; 186 | 187 | /** 188 | * @worklet 189 | */ 190 | export const mapPoint3d = (m: Matrix4, v: Vec3) => { 191 | "worklet"; 192 | const r = matrixVecMul4(m, [...v, 1]); 193 | return [r[0] / r[3], r[1] / r[3], r[2] / r[3]] as const; 194 | }; 195 | 196 | /** 197 | * @worklet 198 | */ 199 | export const multiply4 = (a: Matrix4, b: Matrix4): Matrix4 => { 200 | "worklet"; 201 | const result = new Array(16).fill(0); 202 | for (let i = 0; i < 4; i++) { 203 | for (let j = 0; j < 4; j++) { 204 | result[i * 4 + j] = 205 | a[i * 4] * b[j] + 206 | a[i * 4 + 1] * b[j + 4] + 207 | a[i * 4 + 2] * b[j + 8] + 208 | a[i * 4 + 3] * b[j + 12]; 209 | } 210 | } 211 | return result as unknown as Matrix4; 212 | }; 213 | 214 | /** 215 | * @worklet 216 | */ 217 | export const toMatrix3 = (m: Matrix4) => { 218 | "worklet"; 219 | return [m[0], m[1], m[3], m[4], m[5], m[7], m[12], m[13], m[15]]; 220 | }; 221 | 222 | /** 223 | * @worklet 224 | */ 225 | export const processTransform3d = (transforms: Transforms3d) => { 226 | "worklet"; 227 | return transforms.reduce((acc, val) => { 228 | const key = Object.keys(val)[0] as Transform3dName; 229 | const transform = val as Pick; 230 | if (key === "translateX") { 231 | const value = transform[key]; 232 | return multiply4(acc, translate(value, 0, 0)); 233 | } 234 | if (key === "translate") { 235 | const [x, y, z = 0] = transform[key]; 236 | return multiply4(acc, translate(x, y, z)); 237 | } 238 | if (key === "translateY") { 239 | const value = transform[key]; 240 | return multiply4(acc, translate(0, value, 0)); 241 | } 242 | if (key === "translateZ") { 243 | const value = transform[key]; 244 | return multiply4(acc, translate(0, 0, value)); 245 | } 246 | if (key === "scale") { 247 | const value = transform[key]; 248 | return multiply4(acc, scale(value, value, 1)); 249 | } 250 | if (key === "scaleX") { 251 | const value = transform[key]; 252 | return multiply4(acc, scale(value, 1, 1)); 253 | } 254 | if (key === "scaleY") { 255 | const value = transform[key]; 256 | return multiply4(acc, scale(1, value, 1)); 257 | } 258 | if (key === "skewX") { 259 | const value = transform[key]; 260 | return multiply4(acc, skewX(value)); 261 | } 262 | if (key === "skewY") { 263 | const value = transform[key]; 264 | return multiply4(acc, skewY(value)); 265 | } 266 | if (key === "rotateX") { 267 | const value = transform[key]; 268 | return multiply4(acc, rotate([1, 0, 0], value)); 269 | } 270 | if (key === "rotateY") { 271 | const value = transform[key]; 272 | return multiply4(acc, rotate([0, 1, 0], value)); 273 | } 274 | if (key === "perspective") { 275 | const value = transform[key]; 276 | return multiply4(acc, perspective(value)); 277 | } 278 | if (key === "rotate" || key === "rotateZ") { 279 | const value = transform[key]; 280 | return multiply4(acc, rotate([0, 0, 1], value)); 281 | } 282 | if (key === "matrix") { 283 | const value = transform[key]; 284 | return multiply4(acc, value); 285 | } 286 | return exhaustiveCheck(key); 287 | }, identity4); 288 | }; 289 | 290 | /** 291 | * @worklet 292 | */ 293 | export const concat4 = (m: Matrix4, transform: Transforms3d) => { 294 | "worklet"; 295 | return multiply4(m, processTransform3d(transform)); 296 | }; 297 | 298 | /** 299 | * @worklet 300 | */ 301 | export const rotateX = (value: number) => { 302 | "worklet"; 303 | return rotate([1, 0, 0], value); 304 | }; 305 | 306 | /** 307 | * @worklet 308 | */ 309 | export const rotateY = (value: number) => { 310 | "worklet"; 311 | return rotate([0, 1, 0], value); 312 | }; 313 | 314 | /** 315 | * @worklet 316 | */ 317 | export const rotateZ = (value: number) => { 318 | "worklet"; 319 | return rotate([0, 0, 1], value); 320 | }; 321 | 322 | export const Matrix4 = { 323 | translate, 324 | scale, 325 | rotateX, 326 | rotateY, 327 | rotateZ, 328 | }; 329 | -------------------------------------------------------------------------------- /src/Paths.ts: -------------------------------------------------------------------------------- 1 | import { interpolate, Extrapolation } from "react-native-reanimated"; 2 | import parseSVG from "parse-svg-path"; 3 | import absSVG from "abs-svg-path"; 4 | import normalizeSVG from "normalize-svg-path"; 5 | 6 | import type { Vector } from "./Vectors"; 7 | import { cartesian2Polar } from "./Coordinates"; 8 | import { cubicBezierYForX } from "./Math"; 9 | 10 | type SVGCloseCommand = ["Z"]; 11 | type SVGMoveCommand = ["M", number, number]; 12 | type SVGCurveCommand = ["C", number, number, number, number, number, number]; 13 | type SVGNormalizedCommands = [ 14 | SVGMoveCommand, 15 | ...(SVGCurveCommand | SVGCloseCommand)[] 16 | ]; 17 | 18 | interface Curve { 19 | to: Vector; 20 | c1: Vector; 21 | c2: Vector; 22 | } 23 | 24 | export type Path = { 25 | move: Vector; 26 | curves: Curve[]; 27 | close: boolean; 28 | }; 29 | 30 | /** 31 | * @summary Serialize a path into an SVG path string 32 | * @worklet 33 | */ 34 | export const serialize = (path: Path) => { 35 | "worklet"; 36 | return `M${path.move.x},${path.move.y} ${path.curves 37 | .map((c) => `C${c.c1.x},${c.c1.y} ${c.c2.x},${c.c2.y} ${c.to.x},${c.to.y}`) 38 | .join(" ")}${path.close ? "Z" : ""}`; 39 | }; 40 | 41 | /** 42 | * @description ⚠️ this function cannot run on the UI thread. It must be executed on the JS thread 43 | * @summary Parse an SVG path into a sequence of Bèzier curves. 44 | * The SVG is normalized to have absolute values and to be approximated to a sequence of Bèzier curves. 45 | */ 46 | export const parse = (d: string): Path => { 47 | const segments: SVGNormalizedCommands = normalizeSVG(absSVG(parseSVG(d))); 48 | const path = createPath({ x: segments[0][1], y: segments[0][2] }); 49 | segments.forEach((segment) => { 50 | if (segment[0] === "Z") { 51 | close(path); 52 | } else if (segment[0] === "C") { 53 | addCurve(path, { 54 | c1: { 55 | x: segment[1], 56 | y: segment[2], 57 | }, 58 | c2: { 59 | x: segment[3], 60 | y: segment[4], 61 | }, 62 | to: { 63 | x: segment[5], 64 | y: segment[6], 65 | }, 66 | }); 67 | } 68 | }); 69 | return path; 70 | }; 71 | 72 | /** 73 | * @summary Interpolate between paths. 74 | * @worklet 75 | */ 76 | export const interpolatePath = ( 77 | value: number, 78 | inputRange: number[], 79 | outputRange: Path[], 80 | extrapolate = Extrapolation.CLAMP 81 | ) => { 82 | "worklet"; 83 | const path = { 84 | move: { 85 | x: interpolate( 86 | value, 87 | inputRange, 88 | outputRange.map((p) => p.move.x), 89 | extrapolate 90 | ), 91 | y: interpolate( 92 | value, 93 | inputRange, 94 | outputRange.map((p) => p.move.y), 95 | extrapolate 96 | ), 97 | }, 98 | curves: outputRange[0].curves.map((_, index) => ({ 99 | c1: { 100 | x: interpolate( 101 | value, 102 | inputRange, 103 | outputRange.map((p) => p.curves[index].c1.x), 104 | extrapolate 105 | ), 106 | y: interpolate( 107 | value, 108 | inputRange, 109 | outputRange.map((p) => p.curves[index].c1.y), 110 | extrapolate 111 | ), 112 | }, 113 | c2: { 114 | x: interpolate( 115 | value, 116 | inputRange, 117 | outputRange.map((p) => p.curves[index].c2.x), 118 | extrapolate 119 | ), 120 | y: interpolate( 121 | value, 122 | inputRange, 123 | outputRange.map((p) => p.curves[index].c2.y), 124 | extrapolate 125 | ), 126 | }, 127 | to: { 128 | x: interpolate( 129 | value, 130 | inputRange, 131 | outputRange.map((p) => p.curves[index].to.x), 132 | extrapolate 133 | ), 134 | y: interpolate( 135 | value, 136 | inputRange, 137 | outputRange.map((p) => p.curves[index].to.y), 138 | extrapolate 139 | ), 140 | }, 141 | })), 142 | close: outputRange[0].close, 143 | }; 144 | return serialize(path); 145 | }; 146 | 147 | /** 148 | * @summary Interpolate two paths with an animation value that goes from 0 to 1 149 | * @worklet 150 | */ 151 | export const mixPath = ( 152 | value: number, 153 | p1: Path, 154 | p2: Path, 155 | extrapolate = Extrapolation.CLAMP 156 | ) => { 157 | "worklet"; 158 | return interpolatePath(value, [0, 1], [p1, p2], extrapolate); 159 | }; 160 | 161 | /** 162 | * @summary Create a new path 163 | * @worklet 164 | */ 165 | export const createPath = (move: Vector): Path => { 166 | "worklet"; 167 | return { 168 | move, 169 | curves: [], 170 | close: false, 171 | }; 172 | }; 173 | 174 | /** 175 | * @summary Add an arc command to a path 176 | * @worklet 177 | */ 178 | export const addArc = (path: Path, corner: Vector, to: Vector) => { 179 | "worklet"; 180 | const last = path.curves[path.curves.length - 1]; 181 | const from = last ? last.to : path.move; 182 | const arc = 9 / 16; 183 | path.curves.push({ 184 | c1: { 185 | x: (corner.x - from.x) * arc + from.x, 186 | y: (corner.y - from.y) * arc + from.y, 187 | }, 188 | c2: { 189 | x: (corner.x - to.x) * arc + to.x, 190 | y: (corner.y - to.y) * arc + to.y, 191 | }, 192 | to, 193 | }); 194 | }; 195 | 196 | /** 197 | * @summary Add a cubic Bèzier curve command to a path. 198 | * @worklet 199 | */ 200 | export const addCurve = (path: Path, c: Curve) => { 201 | "worklet"; 202 | path.curves.push({ 203 | c1: c.c1, 204 | c2: c.c2, 205 | to: c.to, 206 | }); 207 | }; 208 | 209 | /** 210 | * @summary Add a line command to a path. 211 | * @worklet 212 | */ 213 | export const addLine = (path: Path, to: Vector) => { 214 | "worklet"; 215 | const last = path.curves[path.curves.length - 1]; 216 | const from = last ? last.to : path.move; 217 | path.curves.push({ 218 | c1: from, 219 | c2: to, 220 | to, 221 | }); 222 | }; 223 | 224 | /** 225 | * @summary Add a quadratic Bèzier curve command to a path. 226 | * @worklet 227 | */ 228 | export const addQuadraticCurve = (path: Path, cp: Vector, to: Vector) => { 229 | "worklet"; 230 | const last = path.curves[path.curves.length - 1]; 231 | const from = last ? last.to : path.move; 232 | path.curves.push({ 233 | c1: { 234 | x: from.x / 3 + (2 / 3) * cp.x, 235 | y: from.y / 3 + (2 / 3) * cp.y, 236 | }, 237 | c2: { 238 | x: to.x / 3 + (2 / 3) * cp.x, 239 | y: to.y / 3 + (2 / 3) * cp.y, 240 | }, 241 | to, 242 | }); 243 | }; 244 | 245 | /** 246 | * @summary Add a close command to a path. 247 | * @worklet 248 | */ 249 | export const close = (path: Path) => { 250 | "worklet"; 251 | path.close = true; 252 | }; 253 | 254 | interface SelectedCurve { 255 | from: Vector; 256 | curve: Curve; 257 | } 258 | 259 | interface NullableSelectedCurve { 260 | from: Vector; 261 | curve: Curve | null; 262 | } 263 | 264 | /** 265 | * @worklet 266 | */ 267 | const curveIsFound = (c: NullableSelectedCurve): c is SelectedCurve => { 268 | "worklet"; 269 | return c.curve !== null; 270 | }; 271 | 272 | /** 273 | * @summary Return the curves at x. This function assumes that only one curve is available at x 274 | * @worklet 275 | */ 276 | export const selectCurve = (path: Path, x: number): SelectedCurve | null => { 277 | "worklet"; 278 | const result: NullableSelectedCurve = { 279 | from: path.move, 280 | curve: null, 281 | }; 282 | for (let i = 0; i < path.curves.length; i++) { 283 | const c = path.curves[i]; 284 | const contains = 285 | result.from.x > c.to.x 286 | ? x >= c.to.x && x <= result.from.x 287 | : x >= result.from.x && x <= c.to.x; 288 | if (contains) { 289 | result.curve = c; 290 | break; 291 | } 292 | result.from = c.to; 293 | } 294 | if (!curveIsFound(result)) { 295 | return null; 296 | } 297 | return result; 298 | }; 299 | 300 | /** 301 | * @summary Return the y value of a path given its x coordinate 302 | * @example 303 | const p1 = parse( 304 | "M150,0 C150,0 0,75 200,75 C75,200 200,225 200,225 C225,200 200,150 0,150" 305 | ); 306 | // 75 307 | getYForX(p1, 200)) 308 | // ~151 309 | getYForX(p1, 50) 310 | * @worklet 311 | */ 312 | export const getYForX = (path: Path, x: number, precision = 2) => { 313 | "worklet"; 314 | const c = selectCurve(path, x); 315 | if (c === null) { 316 | return null; 317 | } 318 | return cubicBezierYForX( 319 | x, 320 | c.from, 321 | c.curve.c1, 322 | c.curve.c2, 323 | c.curve.to, 324 | precision 325 | ); 326 | }; 327 | 328 | const controlPoint = ( 329 | current: Vector, 330 | previous: Vector, 331 | next: Vector, 332 | reverse: boolean, 333 | smoothing: number 334 | ) => { 335 | "worklet"; 336 | const p = previous || current; 337 | const n = next || current; 338 | // Properties of the opposed-line 339 | const lengthX = n.x - p.x; 340 | const lengthY = n.y - p.y; 341 | 342 | const o = cartesian2Polar({ x: lengthX, y: lengthY }); 343 | // If is end-control-point, add PI to the angle to go backward 344 | const angle = o.theta + (reverse ? Math.PI : 0); 345 | const length = o.radius * smoothing; 346 | // The control point position is relative to the current point 347 | const x = current.x + Math.cos(angle) * length; 348 | const y = current.y + Math.sin(angle) * length; 349 | return { x, y }; 350 | }; 351 | 352 | const exhaustiveCheck = (a: never): never => { 353 | throw new Error(`Unexhaustive handling for ${a}`); 354 | }; 355 | 356 | /** 357 | * @summary Link points via a smooth cubic Bézier curves 358 | * from https://github.com/rainbow-me/rainbow 359 | * @worklet 360 | */ 361 | export const curveLines = ( 362 | points: Vector[], 363 | smoothing: number, 364 | strategy: "complex" | "bezier" | "simple" 365 | ) => { 366 | "worklet"; 367 | const path = createPath(points[0]); 368 | // build the d attributes by looping over the points 369 | for (let i = 0; i < points.length; i++) { 370 | if (i === 0) { 371 | continue; 372 | } 373 | const point = points[i]; 374 | const next = points[i + 1]; 375 | const prev = points[i - 1]; 376 | const cps = controlPoint(prev, points[i - 2], point, false, smoothing); 377 | const cpe = controlPoint(point, prev, next, true, smoothing); 378 | switch (strategy) { 379 | case "simple": 380 | const cp = { 381 | x: (cps.x + cpe.x) / 2, 382 | y: (cps.y + cpe.y) / 2, 383 | }; 384 | addQuadraticCurve(path, cp, point); 385 | break; 386 | case "bezier": 387 | const p0 = points[i - 2] || prev; 388 | const p1 = points[i - 1]; 389 | const cp1x = (2 * p0.x + p1.x) / 3; 390 | const cp1y = (2 * p0.y + p1.y) / 3; 391 | const cp2x = (p0.x + 2 * p1.x) / 3; 392 | const cp2y = (p0.y + 2 * p1.y) / 3; 393 | const cp3x = (p0.x + 4 * p1.x + point.x) / 6; 394 | const cp3y = (p0.y + 4 * p1.y + point.y) / 6; 395 | path.curves.push({ 396 | c1: { x: cp1x, y: cp1y }, 397 | c2: { x: cp2x, y: cp2y }, 398 | to: { x: cp3x, y: cp3y }, 399 | }); 400 | if (i === points.length - 1) { 401 | path.curves.push({ 402 | to: points[points.length - 1], 403 | c1: points[points.length - 1], 404 | c2: points[points.length - 1], 405 | }); 406 | } 407 | break; 408 | case "complex": 409 | path.curves.push({ 410 | to: point, 411 | c1: cps, 412 | c2: cpe, 413 | }); 414 | break; 415 | default: 416 | exhaustiveCheck(strategy); 417 | } 418 | } 419 | return path; 420 | }; 421 | -------------------------------------------------------------------------------- /src/Physics.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @summary Select a point where the animation should snap to given the value of the gesture and it's velocity. 3 | * @worklet 4 | */ 5 | export const snapPoint = ( 6 | value: number, 7 | velocity: number, 8 | points: ReadonlyArray 9 | ): number => { 10 | "worklet"; 11 | const point = value + 0.2 * velocity; 12 | const deltas = points.map((p) => Math.abs(point - p)); 13 | const minDelta = Math.min.apply(null, deltas); 14 | return points.filter((p) => Math.abs(point - p) === minDelta)[0]; 15 | }; 16 | -------------------------------------------------------------------------------- /src/ReText.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import type { TextInputProps, TextProps as RNTextProps } from "react-native"; 3 | import { StyleSheet, TextInput } from "react-native"; 4 | import Animated, { useAnimatedProps } from "react-native-reanimated"; 5 | 6 | const styles = StyleSheet.create({ 7 | baseStyle: { 8 | color: "black", 9 | }, 10 | }); 11 | Animated.addWhitelistedNativeProps({ text: true }); 12 | 13 | interface TextProps extends Omit { 14 | text: Animated.SharedValue; 15 | style?: Animated.AnimateProps["style"]; 16 | } 17 | 18 | const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); 19 | 20 | const ReText = (props: TextProps) => { 21 | const { style, text, ...rest } = props; 22 | const animatedProps = useAnimatedProps(() => { 23 | return { 24 | text: text.value, 25 | // Here we use any because the text prop is not available in the type 26 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 27 | } as any; 28 | }); 29 | return ( 30 | 38 | ); 39 | }; 40 | 41 | export default ReText; 42 | -------------------------------------------------------------------------------- /src/Transforms.ts: -------------------------------------------------------------------------------- 1 | import type { TransformsStyle } from "react-native"; 2 | import type Animated from "react-native-reanimated"; 3 | import { useAnimatedStyle } from "react-native-reanimated"; 4 | 5 | import type { Vector } from "./Vectors"; 6 | import type { Transforms2d } from "./Matrix3"; 7 | 8 | type RNTransform = Exclude; 9 | 10 | export const transformOrigin = ( 11 | { x, y }: Vector, 12 | transformations: RNTransform 13 | ): RNTransform => { 14 | "worklet"; 15 | return ([{ translateX: x }, { translateY: y }] as RNTransform) 16 | .concat(transformations) 17 | .concat([{ translateX: -x }, { translateY: -y }]); 18 | }; 19 | 20 | export const transformOrigin2d = ( 21 | { x, y }: Vector, 22 | transformations: Transforms2d 23 | ): Transforms2d => { 24 | "worklet"; 25 | return ([{ translateX: x }, { translateY: y }] as Transforms2d) 26 | .concat(transformations) 27 | .concat([{ translateX: -x }, { translateY: -y }]); 28 | }; 29 | 30 | export const useTranslation = ({ 31 | x, 32 | y, 33 | }: Vector>) => 34 | useAnimatedStyle(() => ({ 35 | transform: [{ translateX: x.value }, { translateY: y.value }], 36 | })); 37 | -------------------------------------------------------------------------------- /src/Transitions.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from "react"; 2 | import type { 3 | WithSpringConfig, 4 | WithTimingConfig, 5 | } from "react-native-reanimated"; 6 | import { 7 | useSharedValue, 8 | useDerivedValue, 9 | withTiming, 10 | withSpring, 11 | } from "react-native-reanimated"; 12 | 13 | import { bin } from "./Math"; 14 | 15 | export const useSpring = ( 16 | state: boolean | number, 17 | config?: WithSpringConfig 18 | ) => { 19 | const value = useSharedValue(0); 20 | useEffect(() => { 21 | value.value = typeof state === "boolean" ? bin(state) : state; 22 | }, [state, value]); 23 | const transition = useDerivedValue(() => { 24 | return withSpring(value.value, config); 25 | }); 26 | return transition; 27 | }; 28 | 29 | export const useTiming = ( 30 | state: boolean | number, 31 | config?: WithTimingConfig 32 | ) => { 33 | const value = useSharedValue(0); 34 | useEffect(() => { 35 | value.value = typeof state === "boolean" ? bin(state) : state; 36 | }, [state, value]); 37 | const transition = useDerivedValue(() => { 38 | return withTiming(value.value, config); 39 | }); 40 | return transition; 41 | }; 42 | -------------------------------------------------------------------------------- /src/Vectors.ts: -------------------------------------------------------------------------------- 1 | import type Animated from "react-native-reanimated"; 2 | import { useSharedValue } from "react-native-reanimated"; 3 | 4 | /** 5 | * @summary Type representing a vector 6 | * @example 7 | export interface Vector { 8 | x: T; 9 | y: T; 10 | } 11 | */ 12 | export interface Vector { 13 | x: T; 14 | y: T; 15 | } 16 | 17 | /** 18 | * @summary Returns a vector of shared values 19 | */ 20 | export const useVector = ( 21 | x1 = 0, 22 | y1?: number 23 | ): Vector> => { 24 | const x = useSharedValue(x1); 25 | const y = useSharedValue(y1 ?? x1); 26 | return { x, y }; 27 | }; 28 | 29 | type Create = { 30 | (): Vector<0>; 31 | >(x: T, y?: T): Vector; 32 | }; 33 | 34 | /** 35 | * 36 | * @param x 37 | * @param y 38 | * @worklet 39 | */ 40 | export const vec2: Create = >( 41 | x?: T, 42 | y?: T 43 | ) => { 44 | "worklet"; 45 | return { 46 | x: x ?? 0, 47 | y: y ?? x ?? 0, 48 | }; 49 | }; 50 | 51 | export const vec = { 52 | create: vec2, 53 | }; 54 | -------------------------------------------------------------------------------- /src/__tests__/Array.test.ts: -------------------------------------------------------------------------------- 1 | import { move } from "../Array"; 2 | 3 | test("reorder()", () => { 4 | expect(move([0, 1, 2, 3, 4], 2, 0)).toStrictEqual([2, 0, 1, 3, 4]); 5 | expect(move([0, 1, 2, 3, 4], 3, 2)).toStrictEqual([0, 1, 3, 2, 4]); 6 | }); 7 | -------------------------------------------------------------------------------- /src/__tests__/Coordinates.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | canvas2Cartesian, 3 | cartesian2Canvas, 4 | cartesian2Polar, 5 | polar2Cartesian, 6 | } from "../Coordinates"; 7 | 8 | test("canvas2Cartesian 1", () => { 9 | const point = canvas2Cartesian({ x: 500, y: 200 }, { x: 500, y: 200 }); 10 | expect(point.x).toBe(0); 11 | expect(point.y).toBe(-0); 12 | }); 13 | 14 | test("canvas2Cartesian 2", () => { 15 | const point = canvas2Cartesian({ x: 0, y: 0 }, { x: 500, y: 200 }); 16 | expect(point.x).toBe(-500); 17 | expect(point.y).toBe(200); 18 | }); 19 | 20 | test("canvas2Cartesian 3", () => { 21 | const point = canvas2Cartesian({ x: 600, y: 300 }, { x: 500, y: 200 }); 22 | expect(point.x).toBe(100); 23 | expect(point.y).toBe(-100); 24 | }); 25 | 26 | test("cartesian2Canvas 1", () => { 27 | const point = cartesian2Canvas({ x: 0, y: 0 }, { x: 500, y: 200 }); 28 | expect(point.x).toBe(500); 29 | expect(point.y).toBe(200); 30 | }); 31 | 32 | test("cartesian2Canvas 2", () => { 33 | const point = cartesian2Canvas({ x: -500, y: 200 }, { x: 500, y: 200 }); 34 | expect(point.x).toBe(0); 35 | expect(point.y).toBe(0); 36 | }); 37 | 38 | test("cartesian2Canvas 3", () => { 39 | const point = cartesian2Canvas({ x: 100, y: -100 }, { x: 500, y: 200 }); 40 | expect(point.x).toBe(600); 41 | expect(point.y).toBe(300); 42 | }); 43 | 44 | test("cartesian2Polar 1", () => { 45 | const x = 0; 46 | const y = 100; 47 | const center = { x: 100, y: 100 }; 48 | const { theta, radius } = cartesian2Polar(canvas2Cartesian({ x, y }, center)); 49 | expect(theta).toBe(-Math.PI); 50 | expect(radius).toBe(100); 51 | const { x: x1, y: y1 } = cartesian2Canvas( 52 | polar2Cartesian({ theta, radius }), 53 | center 54 | ); 55 | expect(x1).toBe(0); 56 | expect(Math.round(y1)).toBe(100); 57 | }); 58 | -------------------------------------------------------------------------------- /src/__tests__/Math.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | mix, 3 | round, 4 | bin, 5 | cubicBezier, 6 | clamp, 7 | between, 8 | toRad, 9 | toDeg, 10 | cubicBezierYForX, 11 | } from "../Math"; 12 | import { vec } from "../Vectors"; 13 | 14 | test("bin()", () => { 15 | expect(bin(true)).toBe(1); 16 | expect(bin(false)).toBe(0); 17 | }); 18 | 19 | test("round()", () => { 20 | expect(round(5.123, 0)).toBe(5); 21 | expect(round(5.123, 1)).toBe(5.1); 22 | expect(round(5.123, 2)).toBe(5.12); 23 | expect(round(5.123, 3)).toBe(5.123); 24 | }); 25 | 26 | test("mix()", () => { 27 | expect(mix(0, 10, 20)).toBe(10); 28 | expect(mix(1, 10, 20)).toBe(20); 29 | expect(mix(0.5, 10, 20)).toBe(15); 30 | expect(mix(0.25, 10, 20)).toBe(12.5); 31 | expect(mix(0.8, 10, 20)).toBe(18); 32 | expect(mix(1.5, 10, 20)).toBe(25); 33 | }); 34 | 35 | test("cubicBezier()", () => { 36 | expect(cubicBezier(1, 0, 0.1, 0.1, 1)).toBe(1); 37 | expect(cubicBezier(0, 0, 0.1, 0.1, 1)).toBe(0); 38 | }); 39 | 40 | test("clamp()", () => { 41 | expect(clamp(-1, 0, 100)).toBe(0); 42 | expect(clamp(1, 0, 100)).toBe(1); 43 | expect(clamp(101, 0, 100)).toBe(100); 44 | }); 45 | 46 | test("between()", () => { 47 | expect(between(-1, 0, 100)).toBe(false); 48 | expect(between(0, 0, 100)).toBe(true); 49 | expect(between(0, 0, 100, false)).toBe(false); 50 | }); 51 | 52 | test("toRad()", () => { 53 | expect(toRad(180)).toBe(Math.PI); 54 | expect(toDeg(Math.PI)).toBe(180); 55 | expect(toDeg(Math.PI / 4)).toBe(45); 56 | }); 57 | 58 | test("cubicBezierYForX()", () => { 59 | const x = 198; 60 | const y = 94; 61 | const a = vec.create(20, 250); 62 | const b = vec.create(30, 20); 63 | const c = vec.create(203, 221); 64 | const d = vec.create(220, 20); 65 | expect(Math.round(cubicBezierYForX(x, a, b, c, d))).toBe(y); 66 | }); 67 | 68 | test("cubicBezierYForX2()", () => { 69 | const x = 116; 70 | const y = 139; 71 | const a = vec.create(59, 218); 72 | const b = vec.create(131, 39); 73 | const c = vec.create(204, 223); 74 | const d = vec.create(227, 89); 75 | expect(Math.round(cubicBezierYForX(x, a, b, c, d))).toBe(y); 76 | }); 77 | -------------------------------------------------------------------------------- /src/__tests__/Matrix3.test.ts: -------------------------------------------------------------------------------- 1 | import { mapPoint, multiply3, processTransform2d, svgMatrix } from "../Matrix3"; 2 | 3 | const expectArrayCloseTo = ( 4 | a: readonly number[], 5 | b: readonly number[], 6 | precision = 14 7 | ) => { 8 | expect(a.length).toEqual(b.length); 9 | for (let i = 0; i < a.length; i++) { 10 | expect(a[i]).toBeCloseTo(b[i], precision); 11 | } 12 | }; 13 | 14 | test("processTransform3d()", () => { 15 | const width = 100; 16 | const height = 100; 17 | expect( 18 | svgMatrix([ 19 | { translateX: width / 2 }, 20 | { translateY: height / 2 }, 21 | { rotate: `${Math.PI / 2}rad` }, 22 | { translateX: -width / 2 }, 23 | { translateY: -height / 2 }, 24 | ]) 25 | ).toStrictEqual( 26 | "matrix(6.123233995736766e-17, 1, -1, 6.123233995736766e-17, 100, -3.061616997868383e-15)" 27 | ); 28 | }); 29 | 30 | describe("3x3 matrices", () => { 31 | it("can make a translated 3x3 matrix", () => { 32 | expectArrayCloseTo( 33 | processTransform2d([{ translateX: 5 }, { translateY: -1 }]), 34 | [1, 0, 5, 0, 1, -1, 0, 0, 1] 35 | ); 36 | }); 37 | 38 | it("can make a scaled 3x3 matrix", () => { 39 | expectArrayCloseTo( 40 | processTransform2d([{ scaleX: 2 }, { scaleY: 3 }]), 41 | [2, 0, 0, 0, 3, 0, 0, 0, 1] 42 | ); 43 | }); 44 | 45 | it("can make a rotated 3x3 matrix", () => { 46 | expectArrayCloseTo( 47 | processTransform2d([ 48 | { translateX: 9 }, 49 | { translateY: 9 }, 50 | { rotate: `${Math.PI}rad` }, 51 | { translateX: -9 }, 52 | { translateY: -9 }, 53 | ]), 54 | [-1, 0, 18, 0, -1, 18, 0, 0, 1] 55 | ); 56 | }); 57 | 58 | it("can multiply 3x3 matrices", () => { 59 | const a = [0.1, 0.2, 0.3, 0.0, 0.6, 0.7, 0.9, -0.9, -0.8] as const; 60 | const b = [2.0, 3.0, 4.0, -3.0, -4.0, -5.0, 7.0, 8.0, 9.0] as const; 61 | const expected = [1.7, 1.9, 2.1, 3.1, 3.2, 3.3, -1.1, -0.1, 0.9]; 62 | expectArrayCloseTo(multiply3(a, b), expected); 63 | }); 64 | 65 | it("maps 2D points correctly with a 3x3 matrix", () => { 66 | const a = [3, 0, -4, 0, 2, 4, 0, 0, 1] as const; 67 | expectArrayCloseTo(mapPoint(a, [0, 0]), [-4, 4]); 68 | expectArrayCloseTo(mapPoint(a, [1, 1]), [-1, 6]); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /src/__tests__/Matrix4.test.ts: -------------------------------------------------------------------------------- 1 | import { 2 | concat4, 3 | identity4, 4 | translate, 5 | mapPoint3d, 6 | matrixVecMul4, 7 | multiply4, 8 | processTransform3d, 9 | } from "../Matrix4"; 10 | 11 | import { 12 | identity, 13 | rotated, 14 | multiply, 15 | transformPoint, 16 | translated, 17 | scaled, 18 | transformPoint3d, 19 | } from "./matrix"; 20 | 21 | const expectArrayCloseTo = ( 22 | a: readonly number[], 23 | b: readonly number[], 24 | precision = 14 25 | ) => { 26 | expect(a.length).toEqual(b.length); 27 | for (let i = 0; i < a.length; i++) { 28 | expect(a[i]).toBeCloseTo(b[i], precision); 29 | } 30 | }; 31 | 32 | test("processTransform3d()", () => { 33 | let ref = identity(); 34 | ref = multiply(ref, rotated([1, 0, 0], Math.PI)); 35 | ref = multiply(ref, rotated([0, 1, 0], Math.PI)); 36 | expect( 37 | processTransform3d([{ rotateX: Math.PI }, { rotateY: Math.PI }]) 38 | ).toStrictEqual(ref); 39 | }); 40 | 41 | test("multiply4()", () => { 42 | let ref = identity(); 43 | ref = multiply(ref, rotated([1, 0, 0], Math.PI)); 44 | ref = multiply(ref, rotated([0, 1, 0], Math.PI)); 45 | expect( 46 | multiply4( 47 | identity4, 48 | processTransform3d([{ rotateX: Math.PI }, { rotateY: Math.PI }]) 49 | ) 50 | ).toStrictEqual(ref); 51 | }); 52 | 53 | test("matrixVecMul4()", () => { 54 | let ref = identity(); 55 | ref = multiply(ref, rotated([1, 0, 0], Math.PI)); 56 | ref = multiply(ref, rotated([0, 1, 0], Math.PI)); 57 | const refVec = transformPoint(ref, [0.5, 0.5, 0, 1]); 58 | expect( 59 | matrixVecMul4( 60 | processTransform3d([{ rotateX: Math.PI }, { rotateY: Math.PI }]), 61 | [0.5, 0.5, 0, 1] 62 | ) 63 | ).toStrictEqual(refVec); 64 | }); 65 | 66 | describe("4x4 matrices", () => { 67 | it("can make a translated 4x4 matrix", () => { 68 | let ref = identity(); 69 | ref = multiply(ref, translated([5, 6, 7])); 70 | expectArrayCloseTo( 71 | processTransform3d([ 72 | { translateX: 5 }, 73 | { translateY: 6 }, 74 | { translateZ: 7 }, 75 | ]), 76 | ref 77 | ); 78 | }); 79 | 80 | it("can make a scaled 4x4 matrix", () => { 81 | let ref = identity(); 82 | ref = multiply(ref, scaled([5, 6, 1])); 83 | expectArrayCloseTo(processTransform3d([{ scaleX: 5 }, { scaleY: 6 }]), ref); 84 | }); 85 | 86 | it("can multiply 4x4 matrices", () => { 87 | const a = [ 88 | 0.1, 0.2, 0.3, 0.4, 0.0, 0.6, 0.7, 0.8, 0.9, -0.9, -0.8, -0.7, -0.6, -0.5, 89 | -0.4, -0.3, 90 | ] as const; 91 | const b = [ 92 | 2.0, 3.0, 4.0, 5.0, -3.0, -4.0, -5.0, -6.0, 7.0, 8.0, 9.0, 10.0, -4.0, 93 | -3.0, -2.0, -1.0, 94 | ] as const; 95 | let ref = identity(); 96 | ref = multiply(a, b); 97 | expect(multiply4(a, b)).toEqual(ref); 98 | }); 99 | 100 | it("maps 3D points correctly with a 3x3 matrix", () => { 101 | // 1. 102 | let ref = identity(); 103 | ref = multiply(ref, translated([10, 0, 0])); 104 | ref = multiply(ref, scaled([2, 2, 1])); 105 | ref = multiply(ref, translated([20, 0, 10])); 106 | let refVec = transformPoint3d(ref, [0, 0, 10]); 107 | const a = processTransform3d([ 108 | { translateX: 10 }, 109 | { scale: 2 }, 110 | { translateX: 20 }, 111 | { translateZ: 10 }, 112 | ]); 113 | expect(a).toEqual(ref); 114 | expect(mapPoint3d(a, [0, 0, 10])).toEqual(refVec); 115 | // 2. 116 | ref = identity(); 117 | ref = multiply(ref, translated([10, 20, 30])); 118 | refVec = transformPoint3d(ref, [0, 0, 0]); 119 | const b = processTransform3d([ 120 | { translateX: 10 }, 121 | { translateY: 20 }, 122 | { translateZ: 30 }, 123 | ]); 124 | expect(mapPoint3d(b, [0, 0, 0])).toEqual(refVec); 125 | // 3. 126 | expect( 127 | concat4( 128 | concat4(b, [ 129 | { translateX: 10 }, 130 | { translateY: 20 }, 131 | { translateZ: 30 }, 132 | ]), 133 | [{ translateX: 10 }, { translateY: 20 }, { translateZ: 30 }] 134 | ) 135 | ).toEqual( 136 | processTransform3d([ 137 | { translateX: 30 }, 138 | { translateY: 60 }, 139 | { translateZ: 90 }, 140 | ]) 141 | ); 142 | }); 143 | it("should correctly transform a point with an identity matrix", () => { 144 | const point = [100, -100, 200] as const; // Define some test point 145 | const result = mapPoint3d(identity4, point); 146 | expect(result).toEqual(point); 147 | }); 148 | it("should correctly transform a point with a translation matrix", () => { 149 | const translationMatrix = translate(100, 100, 100); 150 | const point = [100, -100, 200] as const; // Define some test point 151 | const expectedResult = [200, 0, 300] as const; 152 | const result = mapPoint3d(translationMatrix, point); 153 | expect(result).toEqual(expectedResult); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /src/__tests__/Paths.test.ts: -------------------------------------------------------------------------------- 1 | import { serialize, parse, getYForX, curveLines } from "../Paths"; 2 | import type { Vector } from "../Vectors"; 3 | 4 | import { d1, d2 } from "./paths"; 5 | 6 | // Graph line with random points 7 | const points: Vector[] = [ 8 | { x: 0, y: 192 }, 9 | { x: 16.189944134078214, y: 192 }, 10 | { x: 32.37988826815643, y: 192 }, 11 | { x: 48.56983240223464, y: 192 }, 12 | { x: 64.75977653631286, y: 192 }, 13 | { x: 80.94972067039106, y: 192 }, 14 | { x: 97.13966480446928, y: 192 }, 15 | { x: 113.32960893854748, y: 192 }, 16 | { x: 129.5195530726257, y: 192 }, 17 | { x: 145.70949720670393, y: 192 }, 18 | { x: 161.89944134078212, y: 192 }, 19 | { x: 178.08938547486034, y: 192 }, 20 | { x: 194.27932960893855, y: 192 }, 21 | { x: 210.46927374301674, y: 192 }, 22 | { x: 226.65921787709496, y: 192 }, 23 | { x: 242.84916201117318, y: 192 }, 24 | { x: 259.0391061452514, y: 192 }, 25 | { x: 275.2290502793296, y: 192 }, 26 | { x: 291.41899441340786, y: 192 }, 27 | { x: 307.60893854748605, y: 192 }, 28 | { x: 323.79888268156424, y: 192 }, 29 | { x: 339.9888268156424, y: 192 }, 30 | { x: 356.1787709497207, y: 192 }, 31 | { x: 372.36871508379886, y: 192 }, 32 | { x: 388.5586592178771, y: 192 }, 33 | { x: 404.7486033519553, y: 192 }, 34 | { x: 414, y: 192 }, 35 | ]; 36 | 37 | test("parse()", () => { 38 | const path = 39 | "M150,0 C150,0 0,75 200,75 C75,200 200,225 200,225 C225,200 200,150 0,150"; 40 | expect(serialize(parse(path))).toBe(path); 41 | }); 42 | 43 | test("getYForX()", () => { 44 | const p1 = parse( 45 | "M150,0 C150,0 0,75 200,75 C75,200 200,225 200,225 C225,200 200,150 0,150" 46 | ); 47 | expect(getYForX(p1, 200)).toBe(75); 48 | expect(getYForX(p1, 50)).toBe(151.160325); 49 | expect(getYForX(p1, 750)).toBe(null); 50 | }); 51 | 52 | test("getYForX2()", () => { 53 | const p1 = parse(d1); 54 | expect(getYForX(p1, 358.7)).toBeCloseTo(42.92573877023815, 4); 55 | expect(getYForX(p1, 358.8)).toBeCloseTo(42.90651208682783, 4); 56 | expect(getYForX(p1, 359)).toBeCloseTo(42.878457025833086, 4); 57 | }); 58 | 59 | test("getYForX3()", () => { 60 | const p2 = parse(d2); 61 | expect(getYForX(p2, 414)).toBe(15.75); 62 | }); 63 | 64 | /* 65 | Path generated by Rainbow react-native-animated-charts using `svgBezierPath(points, 0.1, 'complex')` 66 | https://github.com/rainbow-me/rainbow/blob/master/src/react-native-animated-charts/src/smoothing/smoothSVG.js 67 | */ 68 | test("curveLines(simple)", () => { 69 | expect(curveLines(points, 0.1, "simple")).toEqual( 70 | parse( 71 | // eslint-disable-next-line max-len 72 | "M 0,192 Q 7.285474860335196,192 16.189944134078214,192 Q 24.28491620111732,192 32.37988826815643,192 Q 40.47486033519553,192 48.56983240223464,192 Q 56.66480446927375,192 64.75977653631286,192 Q 72.85474860335196,192 80.94972067039106,192 Q 89.04469273743017,192 97.13966480446928,192 Q 105.23463687150837,192 113.32960893854748,192 Q 121.42458100558659,192 129.5195530726257,192 Q 137.6145251396648,192 145.70949720670393,192 Q 153.80446927374302,192 161.89944134078212,192 Q 169.99441340782124,192 178.08938547486034,192 Q 186.18435754189943,192 194.27932960893855,192 Q 202.37430167597765,192 210.46927374301674,192 Q 218.56424581005587,192 226.65921787709496,192 Q 234.75418994413405,192 242.84916201117318,192 Q 250.9441340782123,192 259.0391061452514,192 Q 267.13407821229055,192 275.2290502793296,192 Q 283.32402234636874,192 291.41899441340786,192 Q 299.5139664804469,192 307.60893854748605,192 Q 315.70391061452517,192 323.79888268156424,192 Q 331.8938547486033,192 339.9888268156424,192 Q 348.08379888268155,192 356.1787709497207,192 Q 364.2737430167598,192 372.36871508379886,192 Q 380.463687150838,192 388.5586592178771,192 Q 397.00055865921786,192 404.7486033519553,192 Q 410.1837988826816,192 414,192" 73 | ) 74 | ); 75 | }); 76 | 77 | test("curveLines(complex)", () => { 78 | const referencePath = 79 | // eslint-disable-next-line max-len 80 | "M 0,192 C 1.6189944134078216,192 12.95195530726257,192 16.189944134078214,192 C 19.427932960893855,192 29.141899441340787,192 32.37988826815643,192 C 35.61787709497207,192 45.33184357541899,192 48.56983240223464,192 C 51.80782122905028,192 61.52178770949721,192 64.75977653631286,192 C 67.9977653631285,192 77.71173184357542,192 80.94972067039106,192 C 84.1877094972067,192 93.90167597765364,192 97.13966480446928,192 C 100.37765363128491,192 110.09162011173184,192 113.32960893854748,192 C 116.56759776536312,192 126.28156424581007,192 129.5195530726257,192 C 132.75754189944135,192 142.4715083798883,192 145.70949720670393,192 C 148.94748603351957,192 158.66145251396648,192 161.89944134078212,192 C 165.13743016759776,192 174.8513966480447,192 178.08938547486034,192 C 181.32737430167597,192 191.04134078212292,192 194.27932960893855,192 C 197.5173184357542,192 207.2312849162011,192 210.46927374301674,192 C 213.70726256983238,192 223.42122905027932,192 226.65921787709496,192 C 229.8972067039106,192 239.61117318435754,192 242.84916201117318,192 C 246.08715083798882,192 255.8011173184358,192 259.0391061452514,192 C 262.27709497206706,192 271.991061452514,192 275.2290502793296,192 C 278.46703910614525,192 288.1810055865922,192 291.41899441340786,192 C 294.6569832402235,192 304.3709497206704,192 307.60893854748605,192 C 310.8469273743017,192 320.5608938547486,192 323.79888268156424,192 C 327.0368715083799,192 336.7508379888268,192 339.9888268156424,192 C 343.22681564245806,192 352.94078212290503,192 356.1787709497207,192 C 359.4167597765363,192 369.1307262569832,192 372.36871508379886,192 C 375.6067039106145,192 385.32067039106147,192 388.5586592178771,192 C 391.79664804469274,192 402.20446927374303,192 404.7486033519553,192 C 407.29273743016756,192 413.0748603351955,192 414,192"; 81 | expect(() => curveLines(points, 0.1, "complex")).not.toThrow(); 82 | expect(curveLines(points, 0.1, "complex")).toEqual(parse(referencePath)); 83 | }); 84 | 85 | test("curveLines(bezier)", () => { 86 | expect(curveLines(points, 0.1, "bezier")).toEqual( 87 | parse( 88 | // eslint-disable-next-line max-len 89 | "M 0,192 C 0,192 0,192 2.698324022346369,192 C 5.396648044692738,192 10.793296089385477,192 16.189944134078214,192 C 21.586592178770953,192 26.983240223463692,192 32.37988826815643,192 C 37.77653631284917,192 43.173184357541906,192 48.569832402234645,192 C 53.96648044692737,192 59.363128491620124,192 64.75977653631286,192 C 70.1564245810056,192 75.55307262569833,192 80.94972067039106,192 C 86.34636871508378,192 91.74301675977654,192 97.13966480446926,192 C 102.53631284916202,192 107.93296089385474,192 113.3296089385475,192 C 118.72625698324022,192 124.12290502793297,192 129.5195530726257,192 C 134.91620111731845,192 140.3128491620112,192 145.70949720670393,192 C 151.10614525139667,192 156.50279329608938,192 161.89944134078212,192 C 167.29608938547486,192 172.69273743016757,192 178.08938547486034,192 C 183.48603351955308,192 188.88268156424581,192 194.27932960893858,192 C 199.6759776536313,192 205.072625698324,192 210.46927374301674,192 C 215.86592178770948,192 221.2625698324022,192 226.65921787709496,192 C 232.0558659217877,192 237.45251396648044,192 242.8491620111732,192 C 248.24581005586592,192 253.64245810055868,192 259.0391061452514,192 C 264.43575418994413,192 269.8324022346369,192 275.2290502793296,192 C 280.6256983240224,192 286.0223463687151,192 291.41899441340786,192 C 296.8156424581006,192 302.21229050279334,192 307.60893854748605,192 C 313.00558659217876,192 318.4022346368715,192 323.79888268156424,192 C 329.19553072625695,192 334.5921787709497,192 339.9888268156424,192 C 345.38547486033514,192 350.78212290502796,192 356.1787709497207,192 C 361.5754189944134,192 366.97206703910615,192 372.36871508379886,192 C 377.7653631284916,192 383.16201117318434,192 388.55865921787705,192 C 393.9553072625699,192 399.3519553072626,192 403.5921787709497,192 C414,192 414,192 414,192" 90 | ) 91 | ); 92 | }); 93 | -------------------------------------------------------------------------------- /src/__tests__/Physics.test.ts: -------------------------------------------------------------------------------- 1 | import { snapPoint } from "../Physics"; 2 | 3 | test("snapPoint()", () => { 4 | expect(snapPoint(5, -2, [0, 10])).toBe(0); 5 | expect(snapPoint(5, 2, [0, 10])).toBe(10); 6 | expect(snapPoint(-432, -272, [0, -125, -393])).toBe(-393); 7 | expect(snapPoint(96, 600, [0, 125, 393])).toBe(125); 8 | expect(snapPoint(125, 1200, [0, 125, 393])).toBe(393); 9 | expect(snapPoint(-120, 600, [0, 125, 393])).toBe(0); 10 | }); 11 | -------------------------------------------------------------------------------- /src/__tests__/index.ts: -------------------------------------------------------------------------------- 1 | import "./Coordinates.test"; 2 | import "./Math.test"; 3 | import "./Physics.test"; 4 | import "./Paths.test"; 5 | import "./Array.test"; 6 | import "./Matrix4.test"; 7 | -------------------------------------------------------------------------------- /src/__tests__/matrix.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | /* eslint-disable @typescript-eslint/no-explicit-any */ 3 | export const sdot = (...args: number[]): number => { 4 | let acc = 0; 5 | for (let i = 0; i < args.length - 1; i += 2) { 6 | acc += args[i] * args[i + 1]; 7 | } 8 | return acc; 9 | }; 10 | 11 | const identityN = (n: number): number[] => { 12 | let size = n * n; 13 | const m = new Array(size); 14 | while (size--) { 15 | m[size] = size % (n + 1) === 0 ? 1.0 : 0.0; 16 | } 17 | return m; 18 | }; 19 | 20 | export const identity = () => identityN(4); 21 | 22 | function isnumber(val: number) { 23 | return !isNaN(val); 24 | } 25 | 26 | const m = (m1: number[], m2: number[]) => { 27 | const size = 4; 28 | if (!m1.every(isnumber) || !m2.every(isnumber)) { 29 | throw "Some members of matrices are NaN m1=" + m1 + ", m2=" + m2 + ""; 30 | } 31 | if (m1.length !== m2.length) { 32 | throw ( 33 | "Undefined for matrices of different sizes. m1.length=" + 34 | m1.length + 35 | ", m2.length=" + 36 | m2.length 37 | ); 38 | } 39 | if (size * size !== m1.length) { 40 | throw "Undefined for non-square matrices. array size was " + size; 41 | } 42 | 43 | const result = Array(m1.length); 44 | for (let r = 0; r < size; r++) { 45 | for (let c = 0; c < size; c++) { 46 | // accumulate a sum of m1[r,k]*m2[k, c] 47 | let acc = 0; 48 | for (let k = 0; k < size; k++) { 49 | acc += m1[size * r + k] * m2[size * k + c]; 50 | } 51 | result[r * size + c] = acc; 52 | } 53 | } 54 | return result; 55 | }; 56 | 57 | export const multiply = (...listOfMatrices: any[]) => { 58 | if (listOfMatrices.length < 2) { 59 | throw "multiplication expected two or more matrices"; 60 | } 61 | let result = m(listOfMatrices[0], listOfMatrices[1]); 62 | let next = 2; 63 | while (next < listOfMatrices.length) { 64 | result = m(result, listOfMatrices[next]); 65 | next++; 66 | } 67 | return result; 68 | }; 69 | 70 | export const stride = ( 71 | v: number[], 72 | m: number[], 73 | width: number, 74 | offset: number, 75 | colStride: number 76 | ): number[] => { 77 | for (let i = 0; i < v.length; i++) { 78 | m[i * width + ((i * colStride + offset + width) % width)] = v[i]; 79 | } 80 | return m; 81 | }; 82 | 83 | const dot = (a: number[], b: number[]): number => { 84 | if (a.length !== b.length) { 85 | throw new Error("Arrays must have the same length for dot product."); 86 | } 87 | return a.reduce((acc, val, i) => acc + val * b[i], 0); 88 | }; 89 | 90 | const vectorLengthSquared = (v: number[]): number => dot(v, v); 91 | const vectorLength = (v: number[]): number => Math.sqrt(vectorLengthSquared(v)); 92 | const mulScalar = (v: number[], s: number): number[] => v.map((val) => val * s); 93 | // const addVectors = (a: number[], b: number[]): number[] => 94 | // a.map((val, i) => val + b[i]); 95 | // const subVectors = (a: number[], b: number[]): number[] => 96 | // a.map((val, i) => val - b[i]); 97 | const normalize = (v: number[]): number[] => mulScalar(v, 1 / vectorLength(v)); 98 | 99 | // const cross = (a: number[], b: number[]): number[] => { 100 | // if (a.length !== 3 || b.length !== 3) { 101 | // throw new Error("Cross product is only defined for 3-dimensional vectors."); 102 | // } 103 | // return [ 104 | // a[1] * b[2] - a[2] * b[1], 105 | // a[2] * b[0] - a[0] * b[2], 106 | // a[0] * b[1] - a[1] * b[0], 107 | // ]; 108 | // }; 109 | 110 | // Matrix operations 111 | export const translated = (vec: number[]): number[] => 112 | stride(vec, identityN(4), 4, 3, 0); 113 | 114 | export const scaled = (vec: number[]): number[] => 115 | stride(vec, identityN(4), 4, 0, 1); 116 | 117 | export const rotated = (axisVec: number[], radians: number): number[] => { 118 | const normalizedAxisVec = normalize(axisVec); 119 | const sinRadians = Math.sin(radians); 120 | const cosRadians = Math.cos(radians); 121 | return rotatedUnitSinCos(normalizedAxisVec, sinRadians, cosRadians); 122 | }; 123 | 124 | const rotatedUnitSinCos = ( 125 | axisVec: number[], 126 | sinAngle: number, 127 | cosAngle: number 128 | ): number[] => { 129 | const [x, y, z] = axisVec; 130 | const c = cosAngle; 131 | const s = sinAngle; 132 | const t = 1 - c; 133 | return [ 134 | t * x * x + c, 135 | t * x * y - s * z, 136 | t * x * z + s * y, 137 | 0, 138 | t * x * y + s * z, 139 | t * y * y + c, 140 | t * y * z - s * x, 141 | 0, 142 | t * x * z - s * y, 143 | t * y * z + s * x, 144 | t * z * z + c, 145 | 0, 146 | 0, 147 | 0, 148 | 0, 149 | 1, 150 | ]; 151 | }; 152 | 153 | export const transformPoint = (m: number[], t: number[]): number[] => { 154 | const x = m[0] * t[0] + m[1] * t[1] + m[2] * t[2] + m[3] * t[3]; 155 | const y = m[4] * t[0] + m[5] * t[1] + m[6] * t[2] + m[7] * t[3]; 156 | const z = m[8] * t[0] + m[9] * t[1] + m[10] * t[2] + m[11] * t[3]; 157 | const w = m[12] * t[0] + m[13] * t[1] + m[14] * t[2] + m[15] * t[3]; 158 | return [x, y, z, w]; 159 | }; 160 | 161 | export const transformPoint3d = (m4: number[], t: number[]): number[] => { 162 | const [x, y, z, w] = transformPoint(m4, [...t, 1]); 163 | return [x / w, y / w, z / w]; 164 | }; 165 | -------------------------------------------------------------------------------- /src/__tests__/paths.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | export const d1 = 3 | "M 0,173.25 C 0,173.25 0,173.25 0.38333333333333336,172.7471016280217 C 0.7666666666666667,172.24420325604342 1.5333333333333334,171.23840651208684 2.3000000000000003,170.2730300040364 C 3.066666666666667,169.307653495986 3.833333333333334,168.38269722384177 4.6000000000000005,167.96083553841325 C 5.366666666666667,167.5389738529847 6.133333333333333,167.6202067542719 6.8999999999999995,167.2148271067857 C 7.666666666666667,166.80944745929946 8.433333333333334,165.91745526303987 9.200000000000001,165.26680719379289 C 9.966666666666667,164.6161591245459 10.733333333333334,164.20685518231153 11.5,163.87564470556578 C 12.266666666666666,163.54443422882002 13.033333333333331,163.29131721756292 13.799999999999997,163.17594295196662 C 14.566666666666668,163.06056868637037 15.333333333333334,163.08293716643493 16.1,163.12806655603893 C 16.86666666666667,163.1731959456429 17.633333333333336,163.2410862447863 18.400000000000002,162.6956092747903 C 19.166666666666668,162.15013230479437 19.933333333333337,160.99128806565906 20.700000000000003,160.5392093106696 C 21.46666666666667,160.08713055568015 22.233333333333334,160.34181728483654 23,160.0957640041261 C 23.766666666666666,159.84971072341568 24.53333333333333,159.1029174328385 25.3,158.85450957527917 C 26.066666666666666,158.60610171771987 26.833333333333332,158.85607929317845 27.599999999999998,158.5248688164327 C 28.366666666666664,158.19365833968695 29.13333333333333,157.28125981073688 29.899999999999995,156.64748620890705 C 30.666666666666668,156.0137126070772 31.433333333333337,155.65856393236757 32.2,155.15586177512668 C 32.96666666666667,154.6531596178858 33.733333333333334,154.00290397811364 34.5,153.5830044400592 C 35.26666666666667,153.16310490200476 36.03333333333334,152.973561465668 36.800000000000004,152.88016325066152 C 37.56666666666667,152.786765035655 38.333333333333336,152.78951204197872 39.1,152.83267928420864 C 39.86666666666667,152.87584652643855 40.63333333333333,152.9594340045746 41.400000000000006,152.64195855944746 C 42.16666666666667,152.32448311432032 42.93333333333334,151.60594474592995 43.70000000000001,150.9395994976903 C 44.46666666666667,150.2732542494506 45.23333333333333,149.6591021213616 46,149.052798582769 C 46.76666666666667,148.44649504417634 47.53333333333334,147.84804009508005 48.300000000000004,147.57961833430508 C 49.06666666666667,147.31119657353005 49.833333333333336,147.37280800107638 50.6,146.95840247566935 C 51.36666666666667,146.54399695026237 52.13333333333333,145.65357447190203 52.9,145.73519980266403 C 53.666666666666664,145.81682513342602 54.43333333333333,146.8704982733103 55.199999999999996,146.8999304839216 C 55.96666666666666,146.9293626945329 56.73333333333333,145.93455397587118 57.5,145.6367000044849 C 58.26666666666667,145.33884603309863 59.03333333333333,145.73794680898774 59.79999999999999,145.8344844597928 C 60.56666666666666,145.93102211059784 61.333333333333336,145.7249966363188 62.1,145.43970040812667 C 62.86666666666667,145.15440417993452 63.63333333333333,144.7898371978293 64.4,144.7462775261246 C 65.16666666666667,144.7027178544199 65.93333333333334,144.98016549311566 66.7,145.20698972956004 C 67.46666666666667,145.4338139660044 68.23333333333333,145.61001480019735 69,145.68457640041262 C 69.76666666666667,145.7591380006279 70.53333333333333,145.7320603668655 71.3,146.1802148271068 C 72.06666666666666,146.62836928734808 72.83333333333333,147.55175584159306 73.60000000000001,147.6274947302328 C 74.36666666666667,147.7032336188725 75.13333333333333,146.93132484190699 75.89999999999999,146.4921962595865 C 76.66666666666667,146.053067677266 77.43333333333334,145.94671928959053 78.2,145.67868995829033 C 78.96666666666667,145.4106606269902 79.73333333333333,144.9809503520653 80.5,144.18510337713596 C 81.26666666666667,143.38925640220657 82.03333333333335,142.22727272727272 82.80000000000001,141.60056285598958 C 83.56666666666668,140.97385298470647 84.33333333333333,140.88241691707404 85.10000000000001,140.80471588106025 C 85.86666666666667,140.72701484504643 86.63333333333333,140.66304884065121 87.40000000000002,140.7509530430103 C 88.16666666666667,140.83885724536933 88.93333333333334,141.07863165448268 89.7,141.11434273669104 C 90.46666666666665,141.1500538188994 91.23333333333333,140.9817015742028 92,140.3216351975602 C 92.76666666666667,139.6615688209176 93.53333333333335,138.509788312329 94.3,138.01846660985782 C 95.06666666666666,137.52714490738663 95.83333333333333,137.69628201103288 96.60000000000001,137.794781809212 C 97.36666666666667,137.89328160739112 98.13333333333334,137.92114410010313 98.90000000000002,137.71708077319818 C 99.66666666666667,137.51301744629322 100.43333333333334,137.07702829977129 101.2,136.8227340000897 C 101.96666666666665,136.56843970040813 102.73333333333333,136.49584024756695 103.5,135.90719603534106 C 104.26666666666667,135.3185518231152 105.03333333333335,134.21386285150467 105.80000000000001,133.51965511055297 C 106.56666666666666,132.82544736960128 107.33333333333333,132.5417208593084 108.10000000000001,132.72851728932145 C 108.86666666666667,132.91531371933445 109.63333333333333,133.57263308965332 110.39999999999998,134.0588532089519 C 111.16666666666667,134.54507332825042 111.93333333333332,134.86019419652868 112.69999999999999,134.72559088666637 C 113.46666666666665,134.59098757680405 114.23333333333333,134.00666008880117 115,133.29793245728123 C 115.76666666666667,132.5892048257613 116.53333333333335,131.75607705072431 117.30000000000001,131.1666479795488 C 118.06666666666666,130.5772189083733 118.83333333333333,130.23148854105935 119.59999999999998,130.48185854599274 C 120.36666666666667,130.73222855092615 121.13333333333333,131.57869892810695 121.90000000000002,131.94562048706103 C 122.66666666666667,132.31254204601518 123.43333333333332,132.19991478674262 124.19999999999999,132.10769386016057 C 124.96666666666665,132.0154729335785 125.73333333333335,131.94365833968695 126.5,132.09631340539084 C 127.2666666666667,132.24896847109474 128.03333333333333,132.62609319639412 128.8,132.49698389917927 C 129.5666666666667,132.3678746019644 130.33333333333334,131.73253128223527 131.1,130.9908395748307 C 131.86666666666667,130.2491478674261 132.63333333333333,129.40110777234605 133.4,128.52952190877696 C 134.16666666666666,127.65793604520788 134.9333333333333,126.76280441314975 135.7,125.84216486522853 C 136.46666666666667,124.92152531730727 137.23333333333332,123.9753778535229 138,123.40360810871418 C 138.76666666666668,122.83183836390545 139.53333333333333,122.6344463380724 140.3,122.62031887697896 C 141.0666666666667,122.60619141588553 141.83333333333334,122.77532851953178 142.6,122.71136251513657 C 143.36666666666665,122.64739651074136 144.13333333333333,122.35032739830471 144.9,121.83624478629412 C 145.66666666666666,121.32216217428352 146.43333333333334,120.591066062699 147.20000000000002,120.2924272323631 C 147.96666666666667,119.99378840202718 148.73333333333335,120.12760685293985 149.5,120.29870610396017 C 150.26666666666665,120.4698053549805 151.03333333333333,120.67818540610845 151.79999999999998,120.39995290846302 C 152.56666666666663,120.1217204108176 153.33333333333334,119.35687536439882 154.1,118.62538682333947 C 154.86666666666667,117.89389828228013 155.63333333333333,117.19576624658026 156.4,116.50312822352784 C 157.16666666666666,115.8104902004754 157.93333333333337,115.12334619007042 158.70000000000002,114.2191886800915 C 159.46666666666667,113.31503117011256 160.23333333333335,112.19386016055971 161,111.64367403686595 C 161.76666666666665,111.09348791317217 162.53333333333333,111.1142866753375 163.29999999999998,110.73951652688703 C 164.06666666666666,110.36474637843656 164.83333333333334,109.59440731937032 165.6,109.09445216845315 C 166.36666666666667,108.594497017536 167.13333333333335,108.36492577476791 167.9,108.38690182535767 C 168.66666666666666,108.40887787594744 169.4333333333333,108.68240121989506 170.2,109.29380634166033 C 170.96666666666667,109.90521146342557 171.73333333333335,110.85449836300847 172.5,111.50318428488139 C 173.26666666666665,112.15187020675428 174.03333333333333,112.49995515091717 174.79999999999998,112.67419383773603 C 175.5666666666667,112.84843252455488 176.33333333333334,112.8488249540297 177.10000000000002,112.6353433197291 C 177.86666666666667,112.42186168542854 178.63333333333333,111.99450598735257 179.4,112.01844418531641 C 180.16666666666666,112.04238238328026 180.9333333333333,112.51761447728393 181.69999999999996,112.56627573216129 C 182.46666666666667,112.61493698703862 183.23333333333335,112.23702740278962 184,111.48591738798943 C 184.76666666666665,110.73480737318921 185.53333333333333,109.61049692783781 186.29999999999998,109.20276270350273 C 187.0666666666667,108.79502847916758 187.83333333333334,109.10387047584875 188.60000000000002,109.15881060232319 C 189.36666666666667,109.21375072879759 190.13333333333333,109.01478898506525 190.9,108.75774767905996 C 191.66666666666666,108.50070637305468 192.43333333333337,108.18558550477644 193.20000000000002,107.57614253038525 C 193.9666666666667,106.96669955599407 194.73333333333335,106.06293447548997 195.5,105.70660851235591 C 196.26666666666665,105.35028254922186 197.03333333333333,105.54139570345785 197.79999999999998,105.48802529488272 C 198.5666666666667,105.43465488630757 199.33333333333334,105.1368009149213 200.10000000000002,104.93587702381485 C 200.86666666666667,104.73495313270844 201.63333333333333,104.63095932188186 202.4,104.47948154460242 C 203.16666666666666,104.32800376732295 203.93333333333337,104.12904202359061 204.70000000000002,103.50076243440822 C 205.4666666666667,102.87248284522582 206.23333333333335,101.81488541059336 207,101.50290397811364 C 207.76666666666665,101.19092254563395 208.5333333333333,101.62455711530698 209.29999999999995,101.54921065614208 C 210.06666666666663,101.47386419697717 210.83333333333334,100.88953670897429 211.60000000000002,100.31384266941741 C 212.36666666666667,99.73814862986052 213.13333333333335,99.1710880387496 213.9,98.71979414270977 C 214.6666666666667,98.26850024666994 215.43333333333337,97.93297304570122 216.20000000000002,97.67593173969594 C 216.9666666666667,97.41889043369063 217.73333333333335,97.24033502264878 218.5,97.45303179799971 C 219.26666666666665,97.66572857335068 220.0333333333333,98.2696775350944 220.79999999999995,98.79906489662285 C 221.56666666666663,99.32845225815133 222.33333333333334,99.7832780194645 223.1,99.42538233843118 C 223.86666666666665,99.06748665739785 224.63333333333333,97.89686953401802 225.39999999999998,96.98447100506793 C 226.16666666666666,96.07207247611785 226.9333333333333,95.41789254159751 227.70000000000002,94.59693008027985 C 228.4666666666667,93.77596761896218 229.23333333333335,92.7882226308472 230,92.12894111315426 C 230.76666666666665,91.46965959546128 231.53333333333333,91.13884154819034 232.29999999999998,90.71070099116474 C 233.0666666666667,90.28256043413914 233.83333333333334,89.75709736735884 234.6,89.33876754720366 C 235.36666666666665,88.92043772704847 236.13333333333333,88.6092411535184 236.89999999999998,88.45030721621742 C 237.66666666666666,88.29137327891642 238.4333333333333,88.28470197784456 239.19999999999996,87.94093375790465 C 239.96666666666667,87.59716553796476 240.73333333333335,86.91630039915684 241.5,86.59215365295779 C 242.26666666666668,86.26800690675877 243.03333333333333,86.30057855316859 243.79999999999998,86.59960981297934 C 244.5666666666667,86.89864107279006 245.33333333333334,87.4641319460017 246.1,87.40605238372875 C 246.86666666666665,87.3479728214558 247.63333333333333,86.66632282369825 248.39999999999998,86.29076781629816 C 249.16666666666666,85.91521280889805 249.9333333333333,85.84575279185539 250.69999999999996,85.29792124501053 C 251.46666666666667,84.75008969816567 252.23333333333335,83.72388662151859 253,83.78863748486343 C 253.76666666666668,83.85338834820828 254.53333333333333,85.00909315154506 255.30000000000004,85.63972731757634 C 256.06666666666666,86.27036148360764 256.8333333333333,86.37592501233348 257.59999999999997,86.12633986634972 C 258.3666666666667,85.87675472036597 259.1333333333333,85.2720208996726 259.9,84.96906534511369 C 260.6666666666667,84.66610979055478 261.43333333333334,84.66493250213033 262.2,84.67238866215185 C 262.96666666666664,84.67984482217338 263.73333333333335,84.69593443064089 264.5,84.53307619859174 C 265.26666666666665,84.37021796654258 266.03333333333336,84.02841189397677 266.8,83.91029062205678 C 267.56666666666666,83.79216935013679 268.3333333333333,83.89773287886263 269.09999999999997,83.94168498004215 C 269.8666666666667,83.9856370812217 270.6333333333333,83.96797775485491 271.4,84.18224424810512 C 272.1666666666667,84.39651074135534 272.93333333333334,84.84270305422254 273.7,85.15664663407632 C 274.46666666666664,85.47059021393012 275.23333333333335,85.65228506077051 276,85.27359061757188 C 276.76666666666665,84.89489617437323 277.53333333333336,83.95581244113556 278.3,83.55200251154864 C 279.06666666666666,83.1481925819617 279.8333333333333,83.27965645602548 280.6,83.26003498228461 C 281.36666666666673,83.24041350854375 282.1333333333334,83.06970668699825 282.90000000000003,82.5748531192537 C 283.6666666666667,82.07999955150916 284.43333333333334,81.26099923756558 285.2,80.82108579629546 C 285.96666666666664,80.38117235502534 286.73333333333335,80.32034578642866 287.5,80.49929362694533 C 288.26666666666665,80.67824146746199 289.0333333333333,81.09696371709198 289.79999999999995,81.44583352020452 C 290.56666666666666,81.79470332331705 291.3333333333333,82.07372067991211 292.1,81.74721935686416 C 292.86666666666673,81.42071803381621 293.6333333333334,80.48869803112527 294.40000000000003,79.59003453379378 C 295.1666666666667,78.6913710364623 295.93333333333334,77.82606404449028 296.7,77.32846347042202 C 297.46666666666664,76.83086289635376 298.23333333333335,76.70096874018925 299,76.66486522850607 C 299.76666666666665,76.6287617168229 300.53333333333336,76.68644884962102 301.3,76.72098264340494 C 302.06666666666666,76.75551643718886 302.8333333333333,76.76689689195857 303.59999999999997,76.92543839978474 C 304.3666666666666,77.0839799076109 305.1333333333334,77.38968246849352 305.90000000000003,77.08005561286272 C 306.6666666666667,76.77042875723191 307.43333333333334,75.84547248508768 308.2,75.32000941830741 C 308.96666666666664,74.79454635152712 309.73333333333335,74.66857649011078 310.5,74.69133739965018 C 311.26666666666665,74.71409830918958 312.03333333333336,74.8855899896847 312.8,74.53789747499663 C 313.56666666666666,74.19020496030856 314.3333333333333,73.32332825043727 315.09999999999997,72.69622594967933 C 315.8666666666666,72.06912364892138 316.6333333333334,71.68179575727676 317.40000000000003,71.10060770507242 C 318.1666666666667,70.51941965286811 318.93333333333334,69.74437144010405 319.7,68.97560209893709 C 320.46666666666664,68.2068327577701 321.23333333333335,67.44434228820022 322,66.6065053594654 C 322.76666666666665,65.76866843073059 323.53333333333336,64.85548504283088 324.3,63.91522402116877 C 325.06666666666666,62.97496299950666 325.8333333333333,62.00762434408216 326.59999999999997,61.35972328115889 C 327.3666666666666,60.71182221823563 328.13333333333327,60.38335874781361 328.9,59.906164506435836 C 329.6666666666667,59.42897026505808 330.43333333333334,58.80304525272458 331.20000000000005,58.33762389559132 C 331.9666666666667,57.87220253845808 332.73333333333335,57.56728483652509 333.5,57.68422882002064 C 334.26666666666665,57.801172803516174 335.03333333333336,58.339978472440244 335.8,58.67982239763197 C 336.56666666666666,59.0196663228237 337.3333333333333,59.16054850428309 338.09999999999997,58.87996142978877 C 338.8666666666666,58.59937435529444 339.63333333333327,57.897318024846406 340.4,57.01552899493206 C 341.1666666666667,56.133739965017725 341.93333333333334,55.07221823563708 342.7,54.3670224693905 C 343.4666666666667,53.66182670314391 344.23333333333335,53.31295690003139 345,52.91542584204152 C 345.76666666666665,52.51789478405166 346.5333333333333,52.071702471184466 347.3,51.75030273130915 C 348.06666666666666,51.42890299143383 348.8333333333333,51.23229582455039 349.59999999999997,50.771976050589764 C 350.3666666666666,50.311656276629144 351.1333333333334,49.58762389559133 351.8999999999999,49.00211911916401 C 352.6666666666667,48.416614342736686 353.43333333333334,47.969637170919846 354.2,47.19380409920617 C 354.9666666666667,46.417971027492484 355.73333333333335,45.313282055881956 356.5,44.47112840292416 C 357.26666666666665,43.62897474996637 358.0333333333333,43.0493564156613 358.8,42.90651208682783 C 359.56666666666666,42.76366775799435 360.3333333333333,43.05759743463246 361.09999999999997,43.650165941606495 C 361.8666666666666,44.24273444858053 362.6333333333334,45.13394178589048 363.3999999999999,45.495369332197164 C 364.1666666666667,45.85679687850384 364.93333333333334,45.68844463380725 365.7,45.12413104902006 C 366.4666666666667,44.55981746423286 367.23333333333335,43.59954253935506 368,42.5556801363412 C 368.76666666666665,41.51181773332734 369.5333333333333,40.38436785217741 370.3,39.7215544692111 C 371.06666666666666,39.05874108624479 371.8333333333333,38.860564201462076 372.59999999999997,38.59175001121227 C 373.3666666666666,38.32293582096246 374.1333333333334,37.98348432524555 374.8999999999999,37.45370453424228 C 375.6666666666667,36.923924743239006 376.43333333333334,36.203816656949364 377.2,35.352637126070775 C 377.9666666666667,34.501457595192186 378.73333333333335,33.519206619724635 379.5,32.973729649728675 C 380.26666666666665,32.428252679732715 381.0333333333333,32.319549715208346 381.8,31.726588778759496 C 382.56666666666666,31.13362784231065 383.3333333333333,30.05640893393733 384.09999999999997,29.596089159976703 C 384.86666666666673,29.13576938601608 385.6333333333334,29.29234874646815 386.40000000000003,29.545858187200082 C 387.1666666666667,29.799367627932014 387.93333333333334,30.149807148943808 388.7,30.476308471991754 C 389.4666666666667,30.802809795039696 390.23333333333335,31.105372920123795 391,31.23095035206531 C 391.76666666666665,31.35652778400683 392.5333333333333,31.305119522805768 393.29999999999995,30.884827555276505 C 394.0666666666666,30.464535587747246 394.8333333333333,29.675359913889782 395.59999999999997,28.83870027357943 C 396.36666666666673,28.002040633269075 397.1333333333334,27.11789702650583 397.90000000000003,26.50570704579093 C 398.6666666666667,25.893517065076036 399.43333333333334,25.553280710409485 400.2,24.91832982015519 C 400.9666666666667,24.28337892990089 401.73333333333335,23.353713504058845 402.5,22.422870789792352 C 403.26666666666665,21.492028075525855 404.0333333333333,20.560008072834915 404.8,19.97214871955869 C 405.56666666666666,19.384289366282463 406.3333333333333,19.14059066242096 407.09999999999997,18.55704803336772 C 407.8666666666666,17.973505404314484 408.6333333333334,17.05011885006952 409.40000000000003,16.616091850921652 C 410.1666666666667,16.182064851773788 410.93333333333334,16.237397407723023 411.7,16.174608691752265 C414,15.75 414,15.75 414,15.75"; 4 | export const d2 = 5 | "M 0,173.25 C 0,173.25 0,173.25 1.3269230769230769,172.52904871017813 C 2.6538461538461537,171.80809742035626 5.3076923076923075,170.36619484071255 7.961538461538463,169.38746396629463 C 10.615384615384615,168.4087330918767 13.269230769230768,167.89317392268458 15.923076923076925,166.51114273043092 C 18.576923076923077,165.12911153817723 21.23076923076923,162.880608322862 23.884615384615387,162.06264321088034 C 26.538461538461537,161.24467809889867 29.192307692307693,161.85725109025057 31.84615384615385,162.27967699016926 C 34.5,162.70210289008796 37.15384615384615,162.93438169857345 39.80769230769231,162.57432182718605 C 42.46153846153846,162.21426195579866 45.11538461538462,161.2618634045384 47.769230769230774,159.76840490797545 C 50.42307692307693,158.27494641141251 53.07692307692307,156.2404279695469 55.730769230769226,155.08790376228842 C 58.38461538461538,153.93537955502993 61.03846153846154,153.66484958237857 63.692307692307686,152.0483221228472 C 66.34615384615385,150.43179466331583 69,147.46926971690442 71.65384615384615,145.5633638849878 C 74.3076923076923,143.65745805307117 76.96153846153845,142.80817133564935 79.61538461538461,141.8233424495528 C 82.26923076923077,140.83851356345627 84.92307692307692,139.71814250868505 87.57692307692308,138.94369502550077 C 90.23076923076924,138.1692475423165 92.8846153846154,137.7407236307192 95.53846153846155,138.17257373050484 C 98.1923076923077,138.60442383029047 100.84615384615385,139.89664794145907 103.5,139.74198019070147 C 106.15384615384615,139.58731243994382 108.8076923076923,137.98575282725997 111.46153846153845,136.02247024909454 C 114.1153846153846,134.0591876709291 116.76923076923076,131.73418212728214 119.4230769230769,130.33856899992608 C 122.07692307692305,128.94295587257002 124.73076923076923,128.4767351615049 127.38461538461537,128.90470470840415 C 130.03846153846155,129.33267425530343 132.6923076923077,130.65483406016702 135.34615384615384,131.2272156109099 C 138,131.79959716165274 140.65384615384616,131.62220045827482 143.3076923076923,131.02293221967625 C 145.96153846153845,130.42366398107768 148.6153846153846,129.4025242072585 151.26923076923075,126.90400251311996 C 153.92307692307693,124.40548081898145 156.57692307692307,120.4295772045236 159.23076923076923,119.25875896222927 C 161.8846153846154,118.08794071993493 164.53846153846155,119.72220784980412 167.1923076923077,120.05870722152412 C 169.84615384615384,120.39520659324414 172.5,119.43393820681497 175.15384615384616,118.94387981373343 C 177.8076923076923,118.45382142065193 180.46153846153845,118.43497302091801 183.1153846153846,117.99314435656736 C 185.7692307692308,117.5513156922167 188.42307692307693,116.68650676324931 191.0769230769231,115.75739152930738 C 193.73076923076925,114.82827629536551 196.3846153846154,113.83485475644909 199.03846153846155,111.99436395890308 C 201.6923076923077,110.15387316135707 204.34615384615384,107.46631310518144 207,105.26160470101263 C 209.65384615384616,103.05689629684382 212.3076923076923,101.33503954468178 214.96153846153845,98.55711804272302 C 217.6153846153846,95.77919654076426 220.26923076923075,91.94521028900878 222.92307692307693,89.12460270529971 C 225.57692307692307,86.30399512159063 228.2307692307692,84.496766205928 230.8846153846154,82.99332914480004 C 233.53846153846152,81.4898920836721 236.19230769230765,80.29024687707884 238.84615384615384,80.09344740926896 C 241.49999999999997,79.89664794145908 244.1538461538461,80.70269421243255 246.80769230769226,79.92990982334244 C 249.46153846153845,79.15712543425234 252.1153846153846,76.80551038509866 254.7692307692308,75.00105329292629 C 257.4230769230769,73.19659620075392 260.0769230769231,71.93929706556285 262.7307692307692,71.33448517998373 C 265.3846153846154,70.7296732944046 268.0384615384615,70.77734865843742 270.6923076923077,70.81061054032078 C 273.34615384615387,70.84387242220414 276,70.86272082193805 278.65384615384613,71.01406238450734 C 281.3076923076923,71.16540394707664 283.96153846153845,71.44923867248133 286.6153846153846,70.79259368763395 C 289.2692307692308,70.13594870278659 291.9230769230769,68.53882400768718 294.5769230769231,66.99214650011086 C 297.2307692307692,65.44546899253454 299.8846153846154,63.94923867248133 302.53846153846155,61.54967107694582 C 305.1923076923077,59.15010348141029 307.84615384615387,55.84719861039247 310.5,52.90518515780912 C 313.15384615384613,49.96317170522578 315.8076923076923,47.38204967107692 318.46153846153845,45.95068002069627 C 321.1153846153846,44.5193103703156 323.7692307692308,44.237693103703144 326.42307692307696,43.07186414369132 C 329.0769230769231,41.906035183679485 331.7307692307692,39.8559945302683 334.3846153846154,38.702915958311756 C 337.03846153846155,37.54983738635522 339.6923076923077,37.29372089585334 342.3461538461538,35.311312735604986 C 345,33.32890457535663 347.6538461538462,29.620204745361804 350.3076923076923,26.94096015965702 C 352.9615384615385,24.261715573952234 355.6153846153846,22.611926232537495 358.2692307692308,21.52869761253601 C 360.9230769230769,20.44546899253453 363.5769230769231,19.928801093946305 366.2307692307692,19.7170337792889 C 368.8846153846154,19.5052664646315 371.5384615384615,19.598399733904916 374.1923076923077,19.858951141991255 C 376.8461538461538,20.119502550077595 379.5,20.54747209697685 382.1538461538462,21.39731317909674 C 384.8076923076923,22.247154261216632 387.4615384615384,23.51886687855715 390.1153846153846,23.082027496489 C 392.7692307692308,22.645188114420847 395.4230769230769,20.49979673294402 398.0769230769231,19.98091137556358 C 400.7307692307692,19.46202601818314 403.3846153846154,20.569646684899084 406.0384615384616,20.13557912632122 C414,15.75 414,15.75 414,15.75"; 6 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "parse-svg-path"; 2 | declare module "abs-svg-path"; 3 | declare module "normalize-svg-path"; 4 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Animations"; 2 | export * from "./Coordinates"; 3 | export * from "./Transitions"; 4 | export * from "./Math"; 5 | export * from "./Vectors"; 6 | export * from "./Paths"; 7 | export * from "./Physics"; 8 | export * from "./Array"; 9 | export * from "./Matrix3"; 10 | export * from "./Matrix4"; 11 | export * from "./Colors"; 12 | export * from "./Transforms"; 13 | export { default as ReText } from "./ReText"; 14 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "lib": ["es6"], 5 | "jsx": "react-native", 6 | "strict": true, 7 | "noImplicitAny": true, 8 | "strictNullChecks": true, 9 | "strictFunctionTypes": true, 10 | "strictPropertyInitialization": true, 11 | "noImplicitThis": true, 12 | "alwaysStrict": true, 13 | "noUnusedLocals": true, 14 | "noUnusedParameters": true, 15 | "noImplicitReturns": true, 16 | "noFallthroughCasesInSwitch": true, 17 | "forceConsistentCasingInFileNames": true, 18 | "moduleResolution": "node", 19 | "allowSyntheticDefaultImports": true, 20 | "types": ["react", "react-native", "jest"], 21 | "skipLibCheck": true 22 | }, 23 | "files": ["src/index.d.ts", "src/__tests__/index.ts"], 24 | "include": ["src/index.ts", "node_modules/@types/jest"] 25 | } 26 | --------------------------------------------------------------------------------