├── .eslintrc.js
├── .github
└── workflows
│ ├── check.yml
│ └── publish.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── doc
└── example.gif
├── package.json
├── release.config.js
├── src
├── BallIndicator.tsx
├── DotIndicator.tsx
├── index.ts
└── utils
│ ├── array.ts
│ ├── easing.ts
│ └── get-loop-interpolate-range.ts
└── tsconfig.json
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extends: ["@ken0x0a/eslint-config"],
3 | rules: {
4 | camelcase: 0,
5 | "react/require-default-props": 0,
6 | "react/react-in-jsx-scope": 0,
7 | "@typescript-eslint/naming-convention": 0,
8 | },
9 | };
10 |
--------------------------------------------------------------------------------
/.github/workflows/check.yml:
--------------------------------------------------------------------------------
1 | name: PR Check
2 |
3 | on:
4 | pull_request:
5 | types: [opened, synchronize, reopened]
6 | branches: [main]
7 |
8 | jobs:
9 | typecheck:
10 | name: Type check
11 | runs-on: ubuntu-latest
12 | steps:
13 | - uses: actions/checkout@v3
14 | - uses: actions/setup-node@v3
15 | with:
16 | node-version: 16
17 | - name: Get yarn cache directory path
18 | id: yarn-cache-dir-path
19 | run: echo "::set-output name=dir::$(yarn cache dir)"
20 |
21 | - uses: actions/cache@v3
22 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
23 | with:
24 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
25 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }}
26 | restore-keys: |
27 | ${{ runner.os }}-yarn-
28 |
29 | - name: Install dependencies
30 | run: yarn install
31 |
32 | - name: Type Check
33 | run: yarn run type-check
34 |
35 | lint:
36 | name: Lint
37 | runs-on: ubuntu-latest
38 | steps:
39 | - uses: actions/checkout@v3
40 | - uses: actions/setup-node@v3
41 | with:
42 | node-version: 16
43 | - name: Get yarn cache directory path
44 | id: yarn-cache-dir-path
45 | run: echo "::set-output name=dir::$(yarn cache dir)"
46 |
47 | - uses: actions/cache@v3
48 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
49 | with:
50 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
51 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/package.json') }}
52 | restore-keys: |
53 | ${{ runner.os }}-yarn-
54 |
55 | - name: Install dependencies
56 | run: yarn install
57 |
58 | - name: Lint
59 | run: yarn run lint
60 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 |
3 | on:
4 | push:
5 | branches: [main]
6 |
7 | jobs:
8 | typecheck:
9 | name: Type check
10 | runs-on: ubuntu-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - uses: actions/setup-node@v3
14 | with:
15 | node-version: 16
16 | - name: Get yarn cache directory path
17 | id: yarn-cache-dir-path
18 | run: echo "::set-output name=dir::$(yarn cache dir)"
19 |
20 | - uses: actions/cache@v3
21 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
22 | with:
23 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
24 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
25 | restore-keys: |
26 | ${{ runner.os }}-yarn-
27 |
28 | - name: Install dependencies
29 | run: yarn install --frozen-lockfile
30 |
31 | - name: Type Check
32 | run: yarn run type-check
33 |
34 | lint:
35 | name: Lint
36 | runs-on: ubuntu-latest
37 | steps:
38 | - uses: actions/checkout@v3
39 | - uses: actions/setup-node@v3
40 | with:
41 | node-version: 16
42 | - name: Get yarn cache directory path
43 | id: yarn-cache-dir-path
44 | run: echo "::set-output name=dir::$(yarn cache dir)"
45 |
46 | - uses: actions/cache@v3
47 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
48 | with:
49 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
50 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
51 | restore-keys: |
52 | ${{ runner.os }}-yarn-
53 |
54 | - name: Install dependencies
55 | run: yarn install --frozen-lockfile
56 |
57 | - name: Lint
58 | run: yarn run lint
59 |
60 | release:
61 | name: Release
62 | needs:
63 | - typecheck
64 | - lint
65 | runs-on: ubuntu-latest
66 | steps:
67 | - uses: actions/checkout@v3
68 | with:
69 | token: ${{ secrets.GH_TOKEN }}
70 | - uses: actions/setup-node@v3
71 | with:
72 | node-version: 16
73 | - name: Get yarn cache directory path
74 | id: yarn-cache-dir-path
75 | run: echo "::set-output name=dir::$(yarn cache dir)"
76 |
77 | - uses: actions/cache@v3
78 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`)
79 | with:
80 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
81 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
82 | restore-keys: |
83 | ${{ runner.os }}-yarn-
84 |
85 | - name: Install dependencies
86 | run: yarn install --frozen-lockfile
87 |
88 | - name: Release (semantic release)
89 | run: npx semantic-release
90 | env: # https://help.github.com/en/articles/workflow-syntax-for-github-actions#jobsjob_idstepsenv
91 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
92 | # GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
93 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
94 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 |
4 | #########################################
5 | ######### Generated by scripts ########
6 | #########################################
7 | # for production
8 | /lib
9 | /build
10 | /public/assets
11 |
12 | # others
13 | __generated__/**
14 | src/__generated__/**
15 | !src/__generated__/.gitkeep
16 |
17 | src/graphql/__synced__/**
18 | !.gitkeep
19 |
20 | ############################
21 | ######### Secrets ########
22 | ############################
23 | # env
24 | .env
25 | .env.*
26 | !.env.example
27 | .env.local
28 | .env.development.local
29 | .env.test.local
30 | .env.production.local
31 | .env.secret
32 |
33 | ############################
34 | ######### Secrets ########
35 | ############################
36 | # expo
37 | .expo/
38 |
39 | # dependencies
40 | node_modules
41 |
42 | npm-debug.log*
43 | yarn-debug.log*
44 | yarn-error.log*
45 |
46 | ########################
47 | ######## Test ########
48 | ########################
49 | # Jest
50 | .jest/
51 |
52 | # detox
53 | e2e/*.app
54 | e2e/detox_artifacts
55 | e2e/screenshot
56 | # THIS SHOULD BE RENAMED TO
57 | # `e2e/__generated__/detox_artifacts`
58 | # `e2e/__generated__/screenshot`
59 |
60 | #
61 | # Linter
62 | #
63 |
64 | # eslint
65 | .eslintcache
66 |
67 | # Mac
68 | .DS_Store
69 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # [4.0.0](https://github.com/ken0x0a/react-native-reanimated-indicators/compare/v3.0.0...v4.0.0) (2023-06-23)
2 |
3 |
4 | ### Features
5 |
6 | * enable v3 (replace v1 syntax) ([#36](https://github.com/ken0x0a/react-native-reanimated-indicators/issues/36)) ([c3c20d7](https://github.com/ken0x0a/react-native-reanimated-indicators/commit/c3c20d78f505961103f71f3e8529f4bf9f6bc495))
7 |
8 |
9 | ### BREAKING CHANGES
10 |
11 | * not work with v1 anymore
12 |
13 | * chore: disable eslint rule "react/react-in-jsx-scope"
14 |
15 | * chore: add `declarationMap` for dev experience
16 |
17 | This enables jump to src code
18 |
19 | * doc: add example
20 |
21 | * chore(ci): remove yarn.lock for monorepo dev
22 |
23 | * chore: rm eslint cache
24 |
25 | # [3.0.0](https://github.com/ken0x0a/react-native-reanimated-indicators/compare/v2.0.0...v3.0.0) (2022-11-11)
26 |
27 |
28 | ### Features
29 |
30 | * RNReanimated v2 ([#29](https://github.com/ken0x0a/react-native-reanimated-indicators/issues/29)) ([511db88](https://github.com/ken0x0a/react-native-reanimated-indicators/commit/511db88679526c2e90d3875f4bad6be4df51371f))
31 |
32 |
33 | ### BREAKING CHANGES
34 |
35 | * drop reanimated v1
36 |
37 | * chore(ci): setup GitHub Actions
38 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 Ken Owada
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 | [](https://github.com/ken0x0a/react-native-reanimated-indicators/actions)
2 | [](https://www.npmjs.com/package/react-native-reanimated-indicators)
3 |
4 | Non JS thread blocking indicator components for React Native.
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 | The code for this example is [here](#example).
13 |
14 |
15 |
16 | ---
17 |
18 | - [Usage](#usage)
19 | - [Components](#components)
20 | - [1. ``](#1-ballindicator-)
21 | - [2. ``](#2-dotindicator-)
22 | - [Example](#example)
23 | - [Status](#status)
24 | - [Why I created this library](#why-i-created-this-library)
25 |
26 | ---
27 |
28 | ## Usage
29 |
30 | ```sh
31 | yarn add react-native-reanimated-indicators
32 | ```
33 |
34 |
35 | ## Components
36 |
37 | ### 1. ``
38 |
39 | Looks almost the same as the original.
40 |
41 | ```tsx
42 |
43 | ```
44 |
45 | ### 2. ``
46 |
47 | Looks like the indicator at "Messages" at "mac os" or "iOS", but I couldn't create the same.
48 |
49 | ```tsx
50 |
51 | ```
52 |
53 | ## Example
54 |
55 | ```tsx
56 | import React from "react";
57 | import { StyleSheet, Text, View } from "react-native";
58 | import { BallIndicator, DotIndicator } from "react-native-reanimated-indicators";
59 |
60 | export const IndicatorScreen: React.FC = () => {
61 | return (
62 |
63 | IndicatorScreen
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 | );
76 | };
77 |
78 | const styles = StyleSheet.create({
79 | container: {
80 | flex: 1,
81 | backgroundColor: "black",
82 | },
83 | rect: {
84 | flex: 1,
85 | alignItems: "center",
86 | justifyContent: "center",
87 | flexDirection: "row",
88 | },
89 | ball: {
90 | flex: 1,
91 | flexDirection: "row",
92 | },
93 | });
94 | ```
95 |
96 | ## Status
97 |
98 | If anyone is interested in adding new indicators, I appreciate the PR 🙌
99 |
100 | ## Why I created this library
101 |
102 | There is [an awesome indicators library (react-native-indicators)][react-native-indicators] from a long time ago.
103 | But, as `Animated` from `react-native` contact with JS thread when animation is finished, it makes `InteractionManager.runAfterInteraction` never run...
104 |
105 | I was using [the awesome library (react-native-indicators)][react-native-indicators], until use it with `InteractionManager.runAfterInteraction()`.
106 |
107 | As `Animated` from `react-native` contact with JS thread when animation is finished, `InteractionManager.runAfterInteraction` never runs...
108 | I didn't understand for a while, and I switch to `ActivityIndicator` at that moment.
109 |
110 | After that,
111 | I got to know [Can it be done in React Native?](https://www.youtube.com/user/wcandill/videos) and I started to think "CAN IT BE DONE? by using `react-native-reanimated`"
112 |
113 |
114 |
115 |
116 |
117 | [react-native-indicators]: https://github.com/n4kz/react-native-indicators
118 |
--------------------------------------------------------------------------------
/doc/example.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ken0x0a/react-native-reanimated-indicators/b2ba4ac030c52f8c5d7c1768dfa11a7a3a3332a1/doc/example.gif
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-native-reanimated-indicators",
3 | "version": "4.0.0",
4 | "description": "non ui blocking indicators for react-native using reanimated",
5 | "main": "lib/commonjs/index.js",
6 | "module": "lib/module/index.js",
7 | "react-native": "lib/module/index.js",
8 | "types": "lib/typescript/index.d.ts",
9 | "files": [
10 | "lib/",
11 | "src/"
12 | ],
13 | "repository": {
14 | "type": "git",
15 | "url": "https://github.com/ken0x0a/react-native-reanimated-indicators.git"
16 | },
17 | "homepage": "https://github.com/ken0x0a/react-native-reanimated-indicators#readme",
18 | "author": "Ken Owada",
19 | "license": "MIT",
20 | "prettier": "@ken0x0a/prettier-config",
21 | "scripts": {
22 | "lint": "eslint --ext .ts,.tsx --report-unused-disable-directives src --cache",
23 | "type-check": "tsc --noEmit",
24 | "type-check-ci": "tsc --incremental --outDir './build'",
25 | "test": "yarn run type-check && yarn run lint",
26 | "prepare": "bob build",
27 | "tsw": "tsc --noEmit --watch",
28 | "semantic-release": "semantic-release"
29 | },
30 | "peerDependencies": {
31 | "react": "*",
32 | "react-native": "*",
33 | "react-native-reanimated": ">2.0.0"
34 | },
35 | "dependencies": {
36 | "react-native-reanimated-hooks": "^4.0.0"
37 | },
38 | "devDependencies": {
39 | "@ken0x0a/configs": "^2.8.5",
40 | "@ken0x0a/eslint-config-react-deps": "^6.3.2",
41 | "@react-native-community/bob": "0.17.1",
42 | "@semantic-release/changelog": "^6.0.3",
43 | "@semantic-release/git": "^10.0.1",
44 | "@types/react": "^18.2.13",
45 | "@types/react-native": "0.70",
46 | "react": "18.2.0",
47 | "react-native": "0.72.0",
48 | "react-native-reanimated": "^3.3.0",
49 | "semantic-release": "^19.0.5",
50 | "typescript": "5.1.3"
51 | },
52 | "@react-native-community/bob": {
53 | "source": "src",
54 | "output": "lib",
55 | "targets": [
56 | "commonjs",
57 | "module",
58 | "typescript"
59 | ]
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/release.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | branches: ["main"],
3 | // https://github.com/semantic-release/semantic-release/blob/master/docs/usage/plugins.md
4 | // https://github.com/semantic-release/semantic-release/blob/master/docs/extending/plugins-list.md
5 | plugins: [
6 | "@semantic-release/commit-analyzer",
7 | "@semantic-release/release-notes-generator",
8 | "@semantic-release/changelog",
9 | "@semantic-release/npm",
10 | "@semantic-release/github",
11 | "@semantic-release/git",
12 | ],
13 | };
14 |
--------------------------------------------------------------------------------
/src/BallIndicator.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import { useMemo } from "react";
3 | import type { StyleProp, ViewProps, ViewStyle } from "react-native";
4 | import { StyleSheet, View } from "react-native";
5 | import Animated, { Easing, interpolate, useAnimatedStyle } from "react-native-reanimated";
6 | import { useLoop } from "react-native-reanimated-hooks";
7 | import { range } from "./utils/array";
8 | import { getLoopInterpolateRanges } from "./utils/get-loop-interpolate-range";
9 |
10 | type BallIndicatorProps = ViewProps & {
11 | animating?: boolean;
12 | /**
13 | * @default 'black'
14 | */
15 | color?: string;
16 | containerStyle?: StyleProp;
17 | /**
18 | * @default 8
19 | */
20 | count?: number;
21 | /**
22 | * @default 10
23 | */
24 | dotSize?: number;
25 | /**
26 | * @default Easing.linear
27 | */
28 | easing?: Animated.EasingFunction;
29 | /**
30 | * @default 1000
31 | */
32 | interval?: number;
33 | /**
34 | * size of the indicator
35 | * @default 52
36 | */
37 | size?: number;
38 | };
39 |
40 | export function BallIndicator({
41 | animating,
42 | interval = 1000,
43 | easing = Easing.linear,
44 | // dot
45 | color: backgroundColor = "black",
46 | size = 52,
47 | dotSize = 10,
48 | count = 8,
49 | // container View
50 | containerStyle,
51 | // inner View
52 | style,
53 | ...viewProps
54 | }: BallIndicatorProps) {
55 | const animation = useLoop({ animating, interval, easing });
56 |
57 | const balls = useMemo(() => {
58 | const ballStyle = {
59 | backgroundColor,
60 | borderRadius: dotSize / 2,
61 | height: dotSize,
62 | width: dotSize,
63 | };
64 |
65 | return range(count).map((_, index) => {
66 | return ;
67 | });
68 | }, [backgroundColor, dotSize, count, animation.value]);
69 |
70 | return (
71 |
72 |
73 | {balls}
74 |
75 |
76 | );
77 | }
78 |
79 | type BallProps = {
80 | style: StyleProp>>;
81 | index: number;
82 | count: number;
83 | value: Animated.SharedValue;
84 | };
85 | function Ball({ style, index, count, value }: BallProps) {
86 | const angle = (index * 360) / count;
87 |
88 | const rotate = {
89 | transform: [{ rotateZ: `${angle}deg` }],
90 | alignItems: "center" as const,
91 | };
92 |
93 | const ballAnimStyle = useAnimatedStyle(() => {
94 | const count_m1 = count - 1;
95 | const interpolationRanges = getLoopInterpolateRanges({
96 | count,
97 | calcOutputRange: (idx) => 1.0 - (0.46 / count_m1) * ((idx + count_m1 - index) % count),
98 | });
99 | return {
100 | transform: [
101 | { scale: interpolate(value.value, interpolationRanges.inputRange, interpolationRanges.outputRange) },
102 | ],
103 | };
104 | }, [value]);
105 | return (
106 |
107 |
108 |
109 | );
110 | }
111 |
112 | const styles = StyleSheet.create({
113 | container: {
114 | flex: 1,
115 | justifyContent: "center",
116 | alignItems: "center",
117 | },
118 | });
119 |
--------------------------------------------------------------------------------
/src/DotIndicator.tsx:
--------------------------------------------------------------------------------
1 | import type { ReactNode } from "react";
2 | import React, { useMemo } from "react";
3 | import type { StyleProp, ViewProps, ViewStyle } from "react-native";
4 | import { StyleSheet } from "react-native";
5 | import Animated, { Easing, interpolate, useAnimatedStyle } from "react-native-reanimated";
6 | import { useLoop } from "react-native-reanimated-hooks";
7 | import { range } from "./utils/array";
8 | import {
9 | getLoopInterpolateInputRange,
10 | getLoopInterpolateOutputRange,
11 | } from "./utils/get-loop-interpolate-range";
12 |
13 | const defaultEasing = Easing.bezier(0.3, 0.01, 0.3, 0.15).factory();
14 |
15 | type IndicatorEnableProps = {
16 | opacityEnabled?: boolean;
17 | scaleEnabled?: boolean;
18 | };
19 | type DotIndicatorProps = ViewProps &
20 | IndicatorEnableProps & {
21 | animating?: boolean;
22 | /**
23 | * @default 'black'
24 | */
25 | color?: string;
26 | containerStyle?: StyleProp;
27 | /**
28 | * @default 3
29 | */
30 | count?: number;
31 | /**
32 | * @default 4
33 | */
34 | dotMargin?: number;
35 | /**
36 | * @default 9
37 | */
38 | dotSize?: number;
39 | /**
40 | * @default Easing.bezier(0.3, 0.01, 0.3, 0.15)
41 | */
42 | easing?: Animated.EasingFunction;
43 | /**
44 | * @default 1000
45 | */
46 | interval?: number;
47 | };
48 |
49 | export function DotIndicator({
50 | // animating
51 | animating,
52 | interval = 1000,
53 | easing = defaultEasing,
54 | // dot
55 | color: backgroundColor = "black",
56 | dotMargin = 4,
57 | dotSize = 9,
58 | count = 3,
59 | // switch
60 | opacityEnabled = true,
61 | scaleEnabled = false,
62 | // container View
63 | containerStyle,
64 | // inner View
65 | ...viewProps
66 | }: DotIndicatorProps) {
67 | const animation = useLoop({ animating, interval, easing });
68 |
69 | const dots = useMemo(() => {
70 | const dotStyle = {
71 | backgroundColor,
72 | marginHorizontal: dotMargin / 2,
73 | borderRadius: dotSize / 2,
74 | height: dotSize,
75 | width: dotSize,
76 | };
77 |
78 | return range(count).map((_, index) => {
79 | const inputRange = getLoopInterpolateInputRange({ count });
80 | return (
81 |
87 | );
88 | });
89 | }, [backgroundColor, dotMargin, dotSize, count, scaleEnabled, opacityEnabled, animation.value]);
90 |
91 | return (
92 |
93 | {dots}
94 |
95 | );
96 | }
97 |
98 | type DotProps = {
99 | style: StyleProp>>;
100 | index: number;
101 | count: number;
102 | value: Animated.SharedValue;
103 | scaleEnabled: boolean;
104 | opacityEnabled: boolean;
105 | inputRange: number[];
106 | };
107 | function Dot({ style, index, count, value, scaleEnabled, opacityEnabled, inputRange }: DotProps) {
108 | const animStyle = useAnimatedStyle(() => {
109 | const count_m1 = count - 1;
110 |
111 | const transform: Animated.AnimateStyle>["transform"] = [];
112 | if (scaleEnabled)
113 | transform.push({
114 | scale: interpolate(
115 | value.value,
116 | inputRange,
117 | getLoopInterpolateOutputRange({
118 | count,
119 | calcRange: (idx) => 1.0 - (0.46 / count_m1) * ((count_m1 + index - idx) % count),
120 | }),
121 | ),
122 | });
123 |
124 | return {
125 | opacity: opacityEnabled
126 | ? interpolate(
127 | value.value,
128 | inputRange,
129 | getLoopInterpolateOutputRange({
130 | count,
131 | calcRange: (idx) => 1 - 0.55 * (((count_m1 + index - idx) % count) / count) ** 0.14,
132 | }),
133 | )
134 | : undefined,
135 | transform,
136 | } satisfies Animated.AnimateStyle>;
137 | }, []);
138 |
139 | return ;
140 | }
141 |
142 | const styles = StyleSheet.create({
143 | container: {
144 | flex: 1,
145 | justifyContent: "center",
146 | alignItems: "center",
147 | flexDirection: "row",
148 | },
149 | });
150 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from "./BallIndicator";
2 | export * from "./DotIndicator";
3 |
--------------------------------------------------------------------------------
/src/utils/array.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Range
3 | */
4 | export function range(size: number): 0[] {
5 | "worklet";
6 |
7 | return Array(size).fill(0) as 0[];
8 | }
9 |
--------------------------------------------------------------------------------
/src/utils/easing.ts:
--------------------------------------------------------------------------------
1 | // import { Easing } from 'react-native-reanimated'
2 |
3 | // export const easeInOut = Easing.inOut(Easing.ease)
4 | // export const easeIn = Easing.in(Easing.ease)
5 | // export const easeOut = Easing.out(Easing.ease)
6 |
--------------------------------------------------------------------------------
/src/utils/get-loop-interpolate-range.ts:
--------------------------------------------------------------------------------
1 | import { range } from "./array";
2 |
3 | interface GetLoopInterpolateInputRangesOptions {
4 | count: number;
5 | }
6 | /**
7 | * @return [0..count]
8 | */
9 | export function getLoopInterpolateInputRange({ count }: GetLoopInterpolateInputRangesOptions): number[] {
10 | "worklet";
11 |
12 | return range(count + 1).map((_, idx) => idx / count);
13 | }
14 |
15 | interface GetLoopInterpolateOutputRangesOptions {
16 | /**
17 | * - number
18 | * - ~~string => color | degrees~~
19 | */
20 | calcRange: (positionIndex: number) => number; // string
21 | count: number;
22 | }
23 |
24 | /**
25 | * []
26 | */
27 | export function getLoopInterpolateOutputRange({
28 | count,
29 | calcRange,
30 | }: GetLoopInterpolateOutputRangesOptions): number[] {
31 | "worklet";
32 |
33 | const outputRange = range(count).map((_, idx) => calcRange(idx));
34 | outputRange.unshift(outputRange.slice(-1)[0]);
35 |
36 | return outputRange;
37 | }
38 |
39 | type GetLoopInterpolateRangesOptions = {
40 | /**
41 | * - number
42 | * - ~~string => color | degrees~~
43 | */
44 | calcOutputRange: (positionIndex: number) => number; // string
45 | count: number;
46 | };
47 |
48 | type GetLoopInterpolateRangesResult = {
49 | inputRange: number[];
50 | outputRange: number[];
51 | };
52 | export function getLoopInterpolateRanges({
53 | count,
54 | calcOutputRange,
55 | }: GetLoopInterpolateRangesOptions): GetLoopInterpolateRangesResult {
56 | "worklet";
57 |
58 | const inputRange = getLoopInterpolateInputRange({ count });
59 |
60 | const outputRange = getLoopInterpolateOutputRange({ count, calcRange: calcOutputRange });
61 |
62 | return { inputRange, outputRange };
63 | }
64 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "@ken0x0a/tsconfig/bob",
3 | "compilerOptions": {
4 | "declaration": true,
5 | "declarationMap": true,
6 | },
7 | }
--------------------------------------------------------------------------------