├── .eslintrc.cjs
├── .github
├── CODEOWNERS
└── workflows
│ ├── develop.yml
│ ├── frontTest.yml
│ └── main.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── jest.config.js
├── jest.setup.ts
├── package-lock.json
├── package.json
├── src
├── LiquidUI.tsx
├── __test__
│ └── LiquidUI.test.tsx
├── css
│ └── LiquidUIStyles.css
├── utils
│ ├── fetchMoveLevel.tsx
│ ├── fetchStyles.tsx
│ ├── selectSize.ts
│ └── sizeHandler.tsx
└── vite-env.d.ts
├── tsconfig.json
├── tsconfig.node.json
├── types
├── App.d.ts
├── LiquidUI.d.ts
├── __test__
│ └── LiquidUI.test.d.ts
└── utils
│ ├── fetchMoveLevel.d.ts
│ ├── fetchStyles.d.ts
│ ├── selectSize.d.ts
│ └── sizeHandler.d.ts
└── vite.config.ts
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true , "node": true},
4 | extends: [
5 | "eslint:recommended",
6 | "plugin:@typescript-eslint/recommended",
7 | "plugin:react-hooks/recommended",
8 | ],
9 | ignorePatterns: ["dist", ".eslintrc.cjs", "vite.config.ts"],
10 | parser: "@typescript-eslint/parser",
11 | plugins: ["react-refresh"],
12 | rules: {
13 | "react-refresh/only-export-components": [
14 | "warn",
15 | { allowConstantExport: true },
16 | ],
17 | "no-console": ["warn", { allow: ["warn", "error"] }],
18 | "quotes": ["error", "double", { "avoidEscape": true, "allowTemplateLiterals": true }],
19 | },
20 | }
21 |
--------------------------------------------------------------------------------
/.github/CODEOWNERS:
--------------------------------------------------------------------------------
1 | * @enumura1
2 |
--------------------------------------------------------------------------------
/.github/workflows/develop.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | types: [opened, reopened]
6 | branches:
7 | - develop
8 |
9 | jobs:
10 | cicd_test:
11 | name: feature_to_devlop
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Check:if PR is from feature to develop branch
15 | run: |
16 | if [ "${{ github.event.pull_request.base.ref }}" == "develop" ] && [[ "${{ github.event.pull_request.head.ref }}" =~ ^feature/.* ]]; then
17 | echo "Pull request from feature to develop branch"
18 | else
19 | echo "Pull request not from feature to develop branch, exiting with code 1"
20 | exit 1
21 | fi
22 |
--------------------------------------------------------------------------------
/.github/workflows/frontTest.yml:
--------------------------------------------------------------------------------
1 | name: Run Tests
2 |
3 | on:
4 | push:
5 | branches:
6 | - main
7 | - develop
8 | pull_request:
9 | branches:
10 | - main
11 | - develop
12 |
13 | jobs:
14 | test:
15 | name: front_test
16 | runs-on: ubuntu-latest
17 |
18 | steps:
19 | - name: Checkout Repository
20 | uses: actions/checkout@v2
21 |
22 | - name: Set up Node.js
23 | uses: actions/setup-node@v3
24 | with:
25 | node-version: 20
26 |
27 | - name: Install Dependencies
28 | run: |
29 | npm install
30 |
31 | - name: Run Tests
32 | run: |
33 | npm run test
34 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | types: [opened, reopened]
6 | branches:
7 | - main
8 |
9 | jobs:
10 | cicd_test:
11 | name: cicd_to_main
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Check:if PR is from develop to main branch
15 | run: |
16 | if [ "${{ github.event.pull_request.base.ref }}" == "main" ] && [ "${{ github.event.pull_request.head.ref }}" != "develop" ]; then
17 | echo "Pull request not from develop to main branch"
18 | exit 1
19 | else
20 | echo "Pull request from develop to main branch"
21 | fi
22 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /src
3 | tsconfig.json
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 enumura1
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 | # liquidui-animation
2 |
3 |
4 |
5 | This liquidui-animation Component makes it easy to create a shaking UI React application.
6 | With the liquidui-animation Component, you can implement UI elements with Liquid-like shaking animation in a short time.
7 |
8 |
9 |
10 | [](https://github.com/enumura1/react-component-liquidUI/issues)
11 | [](https://github.com/enumura1/react-component-liquidUI/network/members)
12 | [](https://github.com/enumura1/react-component-liquidUI/stargazers)
13 | [](https://github.com/enumura1/react-component-liquidUI/)
14 | [](https://github.com/enumura1/react-component-liquidUI/)
15 | [](https://github.com/enumura1/react-component-liquidUI/actions/workflows/main.yml)
16 | [](https://open.vscode.dev/{enumura1}/{react-component-liquidUI})
17 |
18 | # Demo
19 |
20 |
21 |

22 |
23 |
24 |
25 |
26 | sample source
27 |
28 | ```tsx
29 | import LiquidUI from "@enumura/liquidui-animation";
30 |
31 | const App = () => {
32 | return (
33 |
34 |
43 |
44 |
52 |
53 |
61 |
62 |
70 |
71 | );
72 | };
73 |
74 | export default App;
75 | ```
76 |
77 |
78 |
79 |
80 |
81 | # Advantages
82 |
83 | - **Customizability**:The liquidui-animation Component allows customization of various parameters such as shape, size, and animation intensity.
84 |
85 | # Installation
86 |
87 | ```
88 |
89 | npm i @enumura/liquidui-animation
90 |
91 | ```
92 |
93 | # Example
94 |
95 | ## 1. Example of rotation
96 |
97 | This is a sample of a rotating liquid UI.
98 | Decreasing the value of the `rotateDuration` property speeds up the rotation and increasing the value slows it down.
99 |
100 |
101 |

102 |
103 |
104 |
105 |
106 | ```tsx
107 | import LiquidUI from "@enumura/liquidui-animation";
108 |
109 | const App = () => {
110 | return (
111 |
119 | );
120 | };
121 | ```
122 |
123 | ## 2. Example of animation speed adjustment
124 |
125 | This is an example of adjusting the `liquidDuration` property to change how fast the UI moves.
126 | Lowering the value makes the UI move faster, and raising the value makes the UI move slower.
127 |
128 |
129 |

130 |
131 |
132 | ```tsx
133 | import LiquidUI from "@enumura/liquidui-animation";
134 |
135 | const App = () => {
136 | return (
137 | <>
138 |
146 | >
147 | );
148 | };
149 | ```
150 |
151 | ## 3. Example of displaying image
152 |
153 | This is a sample of liquid UI including images.
154 | Specify the path of the image for the `bgImg` property.
155 |
156 |
157 |

158 |
159 |
160 |
161 |
162 | ```tsx
163 | import LiquidUI from "@enumura/liquidui-animation";
164 |
165 | const App = () => {
166 | return (
167 |
175 | );
176 | };
177 | ```
178 |
179 | ## 4. Example with blur
180 |
181 | This is a sample of liquid UI with blurring.
182 | Increasing the value of the `blurIntensity` property increases the blur intensity.
183 |
184 |
185 |

186 |
187 |
188 |
189 |
190 | ```tsx
191 | import LiquidUI from "@enumura/liquidui-animation";
192 |
193 | const App = () => {
194 | return (
195 |
203 | );
204 | };
205 | ```
206 |
207 | ## 5. Example of liquidui-animation component with custom size
208 |
209 | This is an example of a case where you want to specify a custom UI size.
210 | Specify an arbitrary value for the `size` property in object format such as `{width: 'XXXpx', height: '○○○px'}`.
211 |
212 | ```tsx
213 | import LiquidUI from "@enumura/liquidui-animation";
214 |
215 | const App = () => {
216 | return (
217 |
225 | );
226 | };
227 | ```
228 |
229 | # Properties
230 |
231 | | Property | Type | Description |
232 | | ---------------------- | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
233 | | **figureShape** | string | Specifies the shape of the UI. Supported values are 'circle' and 'square'. |
234 | | **size** | string, object | Specifies the size of the UI. Specify 'small', 'middle', or 'large' as a string, or specify an object in the form {width: 'XXXpx', height: '○○○px'}. |
235 | | **bgColor** | string | Specifies the background color of the UI. Specify CSS color names, color codes, gradients, etc. |
236 | | **bgImg** | string | Specifies the background image of the UI. should be a valid path to an image file located in the 'public/assets' directory. |
237 | | **liquidDuration** | number | Specifies the background color of the UI; can be a CSS color code or a gradient. |
238 | | **liquidDuration** | number | Specifies the time in milliseconds that a set of animations assigned to the UI will run. |
239 | | **animationIntensity** | string | Specifies the animation intensity. Supported values are 'small', 'middle', and 'strong'. |
240 | | **rotateDuration** | number | Specifies the time in milliseconds per rotation when the UI rotates. |
241 | | **blurIntensity** | number | Specifies the intensity of the blur applied to the UI; a value greater than 0 will blur the UI. |
242 |
243 | # Contributors
244 |
245 | - [enumura1](https://github.com/enumura1)
246 |
247 |
248 |
249 | # Tags
250 |
251 | `react` `UI` `Animation`
252 |
253 | # License
254 |
255 | MIT license. See the [LICENSE file](/LICENSE) for details.
256 |
--------------------------------------------------------------------------------
/jest.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('ts-jest').JestConfigWithTsJest} */
2 |
3 | module.exports = {
4 | preset: "ts-jest",
5 | testEnvironment: "jest-environment-jsdom",
6 | setupFilesAfterEnv: ["./jest.setup.ts"],
7 | moduleNameMapper: {
8 | '\\.css$': '/src/LiquidUI.tsx',
9 | },
10 | };
11 |
--------------------------------------------------------------------------------
/jest.setup.ts:
--------------------------------------------------------------------------------
1 | import "@testing-library/jest-dom";
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@enumura/liquidui-animation",
3 | "version": "0.1",
4 | "description": "liquidui-animation UI is available as a component in this package.",
5 | "author": "enumura",
6 | "main": "dist/liquidui-animation.umd.js",
7 | "module": "dist/liquidui-animation.mjs",
8 | "types": "dist/LiquidUI.d.ts",
9 | "license": "MIT",
10 | "repository": {
11 | "type": "git",
12 | "url": "https://github.com/enumura1/liquidui-animation.git"
13 | },
14 | "files": [
15 | "package.json",
16 | "README.md",
17 | "LICENSE",
18 | "types",
19 | "dist"
20 | ],
21 | "scripts": {
22 | "dev": "vite",
23 | "build": "tsc && vite build",
24 | "lint": "eslint src --ext .ts,.tsx",
25 | "lint-fix": "eslint src --ext .ts,.tsx --fix",
26 | "test": "jest",
27 | "preview": "vite preview"
28 | },
29 | "dependencies": {
30 | "react": "^18.2.0",
31 | "react-dom": "^18.2.0",
32 | "vite-plugin-css-injected-by-js": "^3.5.1",
33 | "vite-plugin-dts": "^3.9.0"
34 | },
35 | "devDependencies": {
36 | "@testing-library/jest-dom": "^6.4.5",
37 | "@testing-library/react": "^15.0.6",
38 | "@testing-library/user-event": "^14.5.2",
39 | "@types/jest": "^29.5.12",
40 | "@types/react": "^18.2.66",
41 | "@types/react-dom": "^18.2.22",
42 | "@typescript-eslint/eslint-plugin": "^7.2.0",
43 | "@typescript-eslint/parser": "^7.2.0",
44 | "@vitejs/plugin-react": "^4.2.1",
45 | "eslint": "^8.57.0",
46 | "eslint-plugin-react-hooks": "^4.6.0",
47 | "eslint-plugin-react-refresh": "^0.4.6",
48 | "jest": "^29.7.0",
49 | "jest-environment-jsdom": "^29.7.0",
50 | "ts-jest": "^29.1.2",
51 | "typescript": "^5.2.2",
52 | "vite": "^5.2.0"
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/LiquidUI.tsx:
--------------------------------------------------------------------------------
1 | import fetchStyles from "./utils/fetchStyles";
2 | import SizeHandler from "./utils/sizeHandler";
3 | import "./css/LiquidUIStyles.css";
4 |
5 | export type LiquidUIProps = {
6 | figureShape: "circle" | "square";
7 | size?: { width: string, height: string,} | ("small" | "middle" | "large"),
8 | bgColor?: string,
9 | animationIntensity: "small" | "middle" | "strong",
10 | liquidDuration: number,
11 | rotateDuration?: number,
12 | blurIntensity?: number,
13 | bgImg?: string,
14 | children?: React.ReactNode,
15 | }
16 |
17 | const LiquidUI: React.FC = ({
18 | figureShape,
19 | size,
20 | bgColor = "",
21 | liquidDuration,
22 | blurIntensity = 0,
23 | animationIntensity,
24 | rotateDuration = 0,
25 | bgImg = "",
26 | children,
27 | }) => {
28 |
29 | const { width, height } = SizeHandler({ size });
30 |
31 | const validLiquidDuration = Math.max(0, liquidDuration);
32 | const validRotateDuration = Math.max(0, rotateDuration);
33 | const validBlurIntensity = Math.max(0, blurIntensity);
34 |
35 | const stylesArray = fetchStyles(
36 | figureShape,
37 | bgColor,
38 | validLiquidDuration.toString(),
39 | animationIntensity,
40 | validBlurIntensity,
41 | validRotateDuration.toString(),
42 | bgImg,
43 | );
44 |
45 | const [backgroundStyle= "", animationStyle= "", appliedBlur= ""] = stylesArray;
46 |
47 | const generatedStyles = {
48 | width: `${width}`,
49 | height: `${height}`,
50 | background: backgroundStyle,
51 | animation: animationStyle,
52 | ...(appliedBlur && { filter: appliedBlur }),
53 | ...(bgImg && { backgroundSize: "cover"}),
54 | ...(bgImg && { backgroundPosition: "center center"}),
55 | };
56 |
57 | return (
58 |
59 | {children}
60 |
61 | );
62 | };
63 |
64 | export default LiquidUI;
65 |
--------------------------------------------------------------------------------
/src/__test__/LiquidUI.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen } from "@testing-library/react";
2 | import LiquidUI, { LiquidUIProps } from "../LiquidUI";
3 |
4 | describe("test LiquidUI Component", () => {
5 |
6 | // test figureShape: test circle, square
7 | test.each<"circle" | "square">(["circle", "square"])(
8 | "should render correctly with figureShape: %s", (figureShapeArgs) => {
9 | const figureShapeProps: LiquidUIProps = {
10 | figureShape: figureShapeArgs,
11 | size: { width: "400px", height: "400px" },
12 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
13 | liquidDuration: 10,
14 | animationIntensity: "small",
15 | rotateDuration: 100,
16 | };
17 | render(
18 |
19 | Dummy text to access LiquidUI components
20 |
21 | );
22 |
23 | const liquidUIWrapper9 = screen.getByText(/Dummy text to access LiquidUI components/i);
24 | const figureShapeStyles = liquidUIWrapper9.style.animation;
25 |
26 | let expectedStyleAnimationPattern = "";
27 |
28 | if (figureShapeArgs === "circle") {
29 | expectedStyleAnimationPattern = `circleSmallMovement 10s linear infinite, rotate 100s linear infinite`;
30 | } else if (figureShapeArgs === "square") {
31 | expectedStyleAnimationPattern = `squareSmallMovement 10s linear infinite, rotate 100s linear infinite`;
32 | } else{
33 | throw new Error(`Invalid figure shape: ${figureShapeArgs}`);
34 | }
35 | expect(figureShapeStyles).toBe(expectedStyleAnimationPattern);
36 | }
37 | );
38 |
39 |
40 | // bgColor: test red
41 | test("should render correctly with bgColor", () => {
42 | const bgColorProps: LiquidUIProps = {
43 | figureShape: "circle",
44 | size: { width: "400px", height: "400px" },
45 | bgColor: "red",
46 | liquidDuration: 10,
47 | animationIntensity: "small",
48 | rotateDuration: 100,
49 | };
50 | render(
51 |
52 | Dummy text to access LiquidUI components
53 |
54 | );
55 |
56 | const liquidUIWrapper8 = screen.getByText(/Dummy text to access LiquidUI components/i);
57 | const backgroundStyle = liquidUIWrapper8.style.background;
58 |
59 | expect(backgroundStyle).toBe("red");
60 | });
61 |
62 |
63 | // size: test
64 | describe("with size property", () => {
65 |
66 | // size: test small, middle, or strong
67 | test.each<"small" | "middle" | "large">(["small", "middle", "large"])(
68 | "should render correctly with size: %s", (sizeArgs) => {
69 | const stringSizeProps: LiquidUIProps = {
70 | figureShape: "circle",
71 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
72 | liquidDuration: 10,
73 | animationIntensity: "small",
74 | rotateDuration: 100,
75 | size: sizeArgs,
76 | };
77 | render(
78 |
79 | Dummy text to access LiquidUI components
80 |
81 | );
82 |
83 | const liquidUIWrapper7 = screen.getByText(/Dummy text to access LiquidUI components/i);
84 | const uiWidth = liquidUIWrapper7.style.width;
85 | const uiHeight = liquidUIWrapper7.style.height;
86 | let expectedWidth;
87 | let expectedHeight;
88 |
89 | switch (sizeArgs) {
90 | case "small":
91 | expectedWidth = "200px";
92 | expectedHeight = "200px";
93 | break;
94 | case "middle":
95 | expectedWidth = "400px";
96 | expectedHeight = "400px";
97 | break;
98 | case "large":
99 | expectedWidth = "600px";
100 | expectedHeight = "600px";
101 | break;
102 | default:
103 | throw new Error("Invalid size");
104 | }
105 |
106 | expect(uiWidth).toBe(expectedWidth);
107 | expect(uiHeight).toBe(expectedHeight);
108 | }
109 | );
110 |
111 |
112 | // size test object
113 | test("should render correctly with size as an object", () => {
114 | const objectSizeProps: LiquidUIProps = {
115 | figureShape: "circle",
116 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
117 | liquidDuration: 10,
118 | animationIntensity: "small",
119 | rotateDuration: 100,
120 | size: { width: "400px", height: "400px" },
121 | };
122 | render(
123 |
124 | Dummy text to access LiquidUI components
125 |
126 | );
127 |
128 | const liquidUIWrapper6 = screen.getByText(/Dummy text to access LiquidUI components/i);
129 | const uiWidth = liquidUIWrapper6.style.width;
130 | const uiHeight = liquidUIWrapper6.style.height;
131 |
132 | expect(uiWidth).toBe("400px");
133 | expect(uiHeight).toBe("400px");
134 | });
135 | });
136 |
137 |
138 | // liquidDuration: test [-10, 0, 5]
139 | test.each([-10, 0, 5])("should render correctly with liquidDuration: %i", (liquidDurationArgs) => {
140 | const liquidDurationProps: LiquidUIProps = {
141 | figureShape: "circle",
142 | size: { width: "300px", height: "300px" },
143 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
144 | liquidDuration: liquidDurationArgs,
145 | animationIntensity: "small",
146 | rotateDuration: 100,
147 | };
148 | render(
149 |
150 | Dummy text to access LiquidUI components
151 |
152 | );
153 |
154 | const liquidUIWrapper5 = screen.getByText(/Dummy text to access LiquidUI components/i);
155 | const styleLiquid = liquidUIWrapper5.style.animation;
156 |
157 | let expectedLiquidStylepp = "";
158 | if (liquidDurationArgs >= 0) {
159 | expectedLiquidStylepp = `circleSmallMovement ${liquidDurationArgs}s linear infinite, rotate 100s linear infinite`;
160 | }else {
161 | expectedLiquidStylepp = "circleSmallMovement 0s linear infinite, rotate 100s linear infinite";
162 | }
163 |
164 | expect(styleLiquid).toBe(expectedLiquidStylepp);
165 | });
166 |
167 |
168 | // test: animationIntensity 'small' | 'middle' | 'strong'
169 | test.each<"small" | "middle" | "strong">(["small", "middle", "strong"])(
170 | "should render correctly with animationIntensity: %s", (animationIntensityArgs) => {
171 | const animationIntensityProps: LiquidUIProps = {
172 | figureShape: "circle",
173 | size: { width: "300px", height: "300px" },
174 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
175 | liquidDuration: 10,
176 | animationIntensity: animationIntensityArgs,
177 | rotateDuration: 100,
178 | };
179 | render(
180 |
181 | Dummy text to access LiquidUI components
182 |
183 | );
184 |
185 | const liquidUIWrapper4 = screen.getByText(/Dummy text to access LiquidUI components/i);
186 | const styleAnimation = liquidUIWrapper4.style.animation;
187 |
188 | let expectedStyleAnimationPattern = "";
189 |
190 | switch (animationIntensityArgs) {
191 | case "small":
192 | expectedStyleAnimationPattern = `circleSmallMovement 10s linear infinite, rotate 100s linear infinite`;
193 | break;
194 | case "middle":
195 | expectedStyleAnimationPattern = `circleMiddleMovement 10s linear infinite, rotate 100s linear infinite`;
196 | break;
197 | case "strong":
198 | expectedStyleAnimationPattern = `circleLargeMovement 10s linear infinite, rotate 100s linear infinite`;
199 | break;
200 | default:
201 | throw new Error(`Invalid animationIntensity: ${animationIntensityArgs}`);
202 | break;
203 | }
204 | expect(styleAnimation).toBe(expectedStyleAnimationPattern);
205 | }
206 | );
207 |
208 |
209 | // test: rotateDuration [-10, 0, 10]
210 | test.each([-10, 0, 10])("should render correctly with rotateDuration: %i", (rotateDurationArgs) => {
211 | const rotateDurationProps: LiquidUIProps = {
212 | figureShape: "circle",
213 | size: { width: "300px", height: "300px" },
214 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
215 | animationIntensity: "small",
216 | liquidDuration: 10,
217 | blurIntensity: 10,
218 | rotateDuration: rotateDurationArgs,
219 | };
220 | render(
221 |
222 | Dummy text to access LiquidUI components
223 |
224 | );
225 |
226 | const liquidUIWrapper3 = screen.getByText(/Dummy text to access LiquidUI components/i);
227 | const styleFilter = liquidUIWrapper3.style.animation;
228 |
229 | let expectedStyleAnimation = "";
230 | if (rotateDurationArgs > 0) {
231 | expectedStyleAnimation = `circleSmallMovement 10s linear infinite, rotate ${rotateDurationArgs}s linear infinite`;
232 | }else {
233 | expectedStyleAnimation = "circleSmallMovement 10s linear infinite";
234 | }
235 |
236 | expect(styleFilter).toBe(expectedStyleAnimation);
237 | });
238 |
239 |
240 | // test: blurIntensity [-10, 0, 10]
241 | test.each([-10, 0, 10])("should render correctly with blurIntensity: %i", (blurIntensityArgs) => {
242 | const blurIntensityProps: LiquidUIProps = {
243 | figureShape: "circle",
244 | size: { width: "300px", height: "300px" },
245 | bgColor: "linear-gradient(90deg, #00c6ff, #0072ff)",
246 | animationIntensity: "small",
247 | liquidDuration: 10,
248 | blurIntensity: blurIntensityArgs,
249 | rotateDuration: 10,
250 | };
251 | render(
252 |
253 | Dummy text to access LiquidUI components
254 |
255 | );
256 | const liquidUIWrapper2 = screen.getByText(/Dummy text to access LiquidUI components/i);
257 | const styleFilter = liquidUIWrapper2.style.filter;
258 |
259 | let expectedStyleFilter = "";
260 | if (blurIntensityArgs > 0) {
261 | expectedStyleFilter = `blur(${blurIntensityArgs}px)`;
262 | }
263 |
264 | expect(styleFilter).toBe(expectedStyleFilter);
265 | });
266 |
267 | test("renders with correct background style when bgImg prop is provided", () => {
268 | render(
269 |
278 | Dummy text to access LiquidUI components
279 |
280 | );
281 |
282 | const liquidUIWrapper = screen.getByText(/Dummy text to access LiquidUI components/i);
283 | const backgroundImageUrl = liquidUIWrapper.style.background;
284 |
285 | expect(backgroundImageUrl).toBe("url(../assets/sampleImg.png)");
286 | });
287 | });
288 |
--------------------------------------------------------------------------------
/src/css/LiquidUIStyles.css:
--------------------------------------------------------------------------------
1 | body {
2 | padding: 0;
3 | margin: 0;
4 | }
5 |
6 | main {
7 | display: flex;
8 | flex-direction: row;
9 | justify-content: space-around;
10 | align-items: center;
11 | width: 100vw;
12 | height: 100vh;
13 | background-color: aliceblue;
14 | }
15 |
16 | .liquid-ui-left {
17 | width: 400px;
18 | height: 400px;
19 | background: linear-gradient(90deg, #00c6ff, #0072ff);
20 | border-radius: 27% 69% 40% 46%;
21 | animation: smallMovement 10s linear infinite;
22 | }
23 |
24 | .liquid-ui-center {
25 | width: 400px;
26 | height: 400px;
27 | background: linear-gradient(90deg, #00c6ff, #0072ff);
28 | border-radius: 20% 60% 30% 50%;
29 | animation: middleMovement 12s linear infinite;
30 | }
31 |
32 | @keyframes squareSmallMovement {
33 | 0%,
34 | 100% {
35 | border-radius: 10% 15% 10% 15%;
36 | }
37 | 15% {
38 | border-radius: 22% 20% 10% 20%;
39 | }
40 | 30% {
41 | border-radius: 28% 30% 15% 25%;
42 | }
43 | 50% {
44 | border-radius: 30% 30% 30% 27%;
45 | }
46 | 65% {
47 | border-radius: 20% 25% 20% 25%;
48 | }
49 | 80% {
50 | border-radius: 15% 20% 15% 20%;
51 | }
52 | }
53 |
54 | @keyframes squareMiddleMovement {
55 | 0%,
56 | 100% {
57 | border-radius: 10% 15% 10% 15%;
58 | }
59 | 15% {
60 | border-radius: 18% 29% 22% 40%;
61 | }
62 | 30% {
63 | border-radius: 25% 39% 32% 40%;
64 | }
65 | 50% {
66 | border-radius: 33% 43% 37% 43%;
67 | }
68 | 65% {
69 | border-radius: 29% 38% 34% 37%;
70 | }
71 | 80% {
72 | border-radius: 23% 28% 25% 24%;
73 | }
74 | }
75 |
76 | @keyframes squareStrongMovement {
77 | 0%,
78 | 100% {
79 | border-radius: 10% 15% 10% 15%;
80 | }
81 | 15% {
82 | border-radius: 20% 40% 23% 40%;
83 | }
84 | 30% {
85 | border-radius: 33% 52% 31% 55%;
86 | }
87 | 50% {
88 | border-radius: 46% 52% 44% 54%;
89 | }
90 | 65% {
91 | border-radius: 37% 60% 37% 50%;
92 | }
93 | 80% {
94 | border-radius: 25% 40% 29% 35%;
95 | }
96 | }
97 |
98 | @keyframes circleSmallMovement {
99 | 0%,
100 | 100% {
101 | border-radius: 39% 47% 39% 47%;
102 | }
103 | 15% {
104 | border-radius: 43% 50% 45% 50%;
105 | }
106 | 30% {
107 | border-radius: 45% 45% 39% 50%;
108 | }
109 | 50% {
110 | border-radius: 47% 39% 47% 39%;
111 | }
112 | 65% {
113 | border-radius: 48% 45% 55% 45%;
114 | }
115 | 80% {
116 | border-radius: 45% 43% 45% 49%;
117 | }
118 | }
119 |
120 | @keyframes circleMiddleMovement {
121 | 0%,
122 | 100% {
123 | border-radius: 39% 57% 39% 57%;
124 | }
125 | 15% {
126 | border-radius: 45% 51% 45% 51%;
127 | }
128 | 30% {
129 | border-radius: 51% 45% 39% 45%;
130 | }
131 | 50% {
132 | border-radius: 57% 39% 57% 39%;
133 | }
134 | 65% {
135 | border-radius: 51% 45% 51% 43%;
136 | }
137 | 80% {
138 | border-radius: 45% 43% 45% 51%;
139 | }
140 | }
141 |
142 | @keyframes circleLargeMovement {
143 | 0%,
144 | 100% {
145 | border-radius: 75% 38% 75% 38%;
146 | }
147 | 15% {
148 | border-radius: 60% 48% 60% 48%;
149 | }
150 | 30% {
151 | border-radius: 50% 60% 48% 44%;
152 | }
153 | 50% {
154 | border-radius: 38% 75% 38% 75%;
155 | }
156 | 65% {
157 | border-radius: 48% 54% 48% 60%;
158 | }
159 | 80% {
160 | border-radius: 54% 50% 42% 48%;
161 | }
162 | }
163 |
164 | @keyframes rotate {
165 | from {
166 | transform: rotate(0deg);
167 | }
168 | to {
169 | transform: rotate(360deg);
170 | }
171 | }
172 |
--------------------------------------------------------------------------------
/src/utils/fetchMoveLevel.tsx:
--------------------------------------------------------------------------------
1 | const selectCssPatern = (
2 | figureShape: string = "circle",
3 | animationIntensity: string,
4 | ) => {
5 |
6 | let selectedCssPatern:string = "";
7 |
8 | switch (figureShape) {
9 | case "circle":
10 | if (animationIntensity === "small"){
11 | selectedCssPatern = "circleSmallMovement";
12 | } else if (animationIntensity === "middle") {
13 | selectedCssPatern = "circleMiddleMovement";
14 | } else if (animationIntensity === "strong") {
15 | selectedCssPatern = "circleLargeMovement";
16 | }
17 | return selectedCssPatern;
18 |
19 | case "square":
20 | if (animationIntensity === "small"){
21 | selectedCssPatern = "squareSmallMovement";
22 | } else if (animationIntensity === "middle") {
23 | selectedCssPatern = "squareMiddleMovement";
24 | } else if (animationIntensity === "strong") {
25 | selectedCssPatern = "squareStrongMovement";
26 | }
27 | return selectedCssPatern;
28 |
29 | default:
30 | console.error("error");
31 | }
32 | }
33 |
34 | export default selectCssPatern;
35 |
--------------------------------------------------------------------------------
/src/utils/fetchStyles.tsx:
--------------------------------------------------------------------------------
1 | import selectCssPatern from "./fetchMoveLevel";
2 |
3 | type FetchStylesProps = (
4 | figureShape: string,
5 | bgColor: string,
6 | liquidDuration?: string,
7 | animationIntensity?: "small" | "middle" | "strong",
8 | blurIntensity?: number,
9 | rotateDuration?: string,
10 | bgImg?: string
11 | ) => string[];
12 |
13 | const fetchStyles: FetchStylesProps = (
14 | figureShape,
15 | bgColor,
16 | liquidDuration = "10",
17 | animationIntensity = "small",
18 | blurIntensity = 0,
19 | rotateDuration = "0",
20 | bgImg = "",
21 | ) => {
22 |
23 | const backgroundStyle = bgImg === ""
24 | ? bgColor
25 | : `url(${bgImg})`;
26 |
27 | const animationStyleBuilder = () => {
28 | const selectedCSS = selectCssPatern(figureShape, animationIntensity);
29 | if (rotateDuration !== "0") {
30 | return `${selectedCSS} ${liquidDuration}s linear infinite, rotate ${rotateDuration}s linear infinite`;
31 | } else {
32 | return `${selectedCSS} ${liquidDuration}s linear infinite`;
33 | }
34 | };
35 |
36 | const animationStyle = animationStyleBuilder();
37 |
38 | const appliedBluer = `blur(${blurIntensity}px)`;
39 | const generatedStyles = blurIntensity !== 0
40 | ? [backgroundStyle, animationStyle, appliedBluer]
41 | : [backgroundStyle, animationStyle];
42 |
43 | return generatedStyles;
44 | };
45 |
46 | export default fetchStyles;
47 |
--------------------------------------------------------------------------------
/src/utils/selectSize.ts:
--------------------------------------------------------------------------------
1 | type Size = {
2 | width: string;
3 | height: string;
4 | };
5 |
6 | type SizeOption = "small" | "middle" | "large";
7 |
8 |
9 | export const getSizeFromOption = (option: SizeOption): Size => {
10 | switch (option) {
11 | case "small":
12 | return { width: "200px", height: "200px" };
13 | case "middle":
14 | return { width: "400px", height: "400px" };
15 | case "large":
16 | return { width: "600px", height: "600px" };
17 | default:
18 | console.error("Invalid size option:", option);
19 | return { width: "200px", height: "200px" };
20 | }
21 | };
--------------------------------------------------------------------------------
/src/utils/sizeHandler.tsx:
--------------------------------------------------------------------------------
1 | import { getSizeFromOption } from "./selectSize";
2 |
3 | type Size = {
4 | width: string;
5 | height: string;
6 | };
7 |
8 | type SizeOption = "small" | "middle" | "large";
9 |
10 | type SetSizesProps = {
11 | size?: Size | SizeOption;
12 | };
13 |
14 | const DEFAULT_SIZE: Size = { width: "200px", height: "200px" };
15 |
16 | const calculateUiSize = (size?: Size | SizeOption): Size => {
17 |
18 | if (typeof size === "object") {
19 | return size;
20 | }
21 | else if (typeof size === "string") {
22 | return getSizeFromOption(size);
23 | }
24 | else {
25 | console.error("Size not specified. Using default size.");
26 | return DEFAULT_SIZE;
27 | }
28 | };
29 |
30 |
31 | const SizeHandler = ({ size }: SetSizesProps) => {
32 | const uiSize = calculateUiSize(size);
33 |
34 | return uiSize;
35 | };
36 |
37 | export default SizeHandler;
38 |
--------------------------------------------------------------------------------
/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "useDefineForClassFields": true,
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "module": "ESNext",
7 | "skipLibCheck": true,
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "allowImportingTsExtensions": true,
12 | "resolveJsonModule": true,
13 | "isolatedModules": true,
14 | "emitDeclarationOnly": true,
15 | "declaration": true,
16 | "declarationDir": "./types",
17 | "jsx": "react-jsx",
18 |
19 | /* Linting */
20 | "strict": true,
21 | "noUnusedLocals": true,
22 | "noUnusedParameters": true,
23 | "noFallthroughCasesInSwitch": true,
24 | "esModuleInterop": true,
25 | "types": ["@testing-library/jest-dom"],
26 | },
27 | "include": ["src"],
28 | "references": [{ "path": "./tsconfig.node.json" }]
29 | }
30 |
--------------------------------------------------------------------------------
/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "skipLibCheck": true,
5 | "module": "ESNext",
6 | "moduleResolution": "bundler",
7 | "allowSyntheticDefaultImports": true,
8 | "strict": true
9 | },
10 | "include": ["vite.config.ts"]
11 | }
12 |
--------------------------------------------------------------------------------
/types/App.d.ts:
--------------------------------------------------------------------------------
1 | import './css/LiquidUIStyles.css';
2 | declare const App: () => import("react/jsx-runtime").JSX.Element;
3 | export default App;
4 |
--------------------------------------------------------------------------------
/types/LiquidUI.d.ts:
--------------------------------------------------------------------------------
1 | import "./css/LiquidUIStyles.css";
2 | export type LiquidUIProps = {
3 | figureShape: "circle" | "square";
4 | size?: {
5 | width: string;
6 | height: string;
7 | } | ("small" | "middle" | "large");
8 | bgColor?: string;
9 | animationIntensity: "small" | "middle" | "strong";
10 | liquidDuration: number;
11 | rotateDuration?: number;
12 | blurIntensity?: number;
13 | bgImg?: string;
14 | children?: React.ReactNode;
15 | };
16 | declare const LiquidUI: React.FC;
17 | export default LiquidUI;
18 |
--------------------------------------------------------------------------------
/types/__test__/LiquidUI.test.d.ts:
--------------------------------------------------------------------------------
1 | export {};
2 |
--------------------------------------------------------------------------------
/types/utils/fetchMoveLevel.d.ts:
--------------------------------------------------------------------------------
1 | declare const selectCssPatern: (figureShape: string | undefined, animationIntensity: string) => string | undefined;
2 | export default selectCssPatern;
3 |
--------------------------------------------------------------------------------
/types/utils/fetchStyles.d.ts:
--------------------------------------------------------------------------------
1 | type FetchStylesProps = (figureShape: string, bgColor: string, liquidDuration?: string, animationIntensity?: "small" | "middle" | "strong", blurIntensity?: number, rotateDuration?: string, bgImg?: string) => string[];
2 | declare const fetchStyles: FetchStylesProps;
3 | export default fetchStyles;
4 |
--------------------------------------------------------------------------------
/types/utils/selectSize.d.ts:
--------------------------------------------------------------------------------
1 | type Size = {
2 | width: string;
3 | height: string;
4 | };
5 | type SizeOption = "small" | "middle" | "large";
6 | export declare const getSizeFromOption: (option: SizeOption) => Size;
7 | export {};
8 |
--------------------------------------------------------------------------------
/types/utils/sizeHandler.d.ts:
--------------------------------------------------------------------------------
1 | type Size = {
2 | width: string;
3 | height: string;
4 | };
5 | type SizeOption = "small" | "middle" | "large";
6 | type SetSizesProps = {
7 | size?: Size | SizeOption;
8 | };
9 | declare const SizeHandler: ({ size }: SetSizesProps) => Size;
10 | export default SizeHandler;
11 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { resolve } from 'path'
2 | import { defineConfig } from 'vite'
3 | import react from '@vitejs/plugin-react'
4 | import dts from 'vite-plugin-dts'
5 | import cssInjectedByJsPlugin from 'vite-plugin-css-injected-by-js'
6 |
7 | // https://vitejs.dev/config/
8 | export default defineConfig({
9 | plugins: [react(), dts(), cssInjectedByJsPlugin(),],
10 | build: {
11 | lib: {
12 | entry: resolve(__dirname, 'src/LiquidUI.tsx'),
13 | name: 'liquidui-animation',
14 | fileName: 'liquidui-animation',
15 | },
16 | },
17 | })
18 |
--------------------------------------------------------------------------------