├── .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 | [![Github issues](https://img.shields.io/github/issues/enumura1/react-component-liquidUI)](https://github.com/enumura1/react-component-liquidUI/issues) 11 | [![Github forks](https://img.shields.io/github/forks/enumura1/react-component-liquidUI)](https://github.com/enumura1/react-component-liquidUI/network/members) 12 | [![Github stars](https://img.shields.io/github/stars/enumura1/react-component-liquidUI)](https://github.com/enumura1/react-component-liquidUI/stargazers) 13 | [![Github top language](https://img.shields.io/github/languages/top/enumura1/react-component-liquidUI)](https://github.com/enumura1/react-component-liquidUI/) 14 | [![Github license](https://img.shields.io/github/license/enumura1/react-component-liquidUI)](https://github.com/enumura1/react-component-liquidUI/) 15 | [![workflow](https://github.com/enumura1/react-component-liquidUI/actions/workflows/main.yml/badge.svg)](https://github.com/enumura1/react-component-liquidUI/actions/workflows/main.yml) 16 | [![Open in Visual Studio Code](https://img.shields.io/static/v1?logo=visualstudiocode&label=&message=Open%20in%20Visual%20Studio%20Code&labelColor=2c2c32&color=007acc&logoColor=007acc)](https://open.vscode.dev/{enumura1}/{react-component-liquidUI}) 17 | 18 | # Demo 19 | 20 |
21 | sample-image-gif 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 | sample-image-gif 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 | sample-image-gif 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 | sample-image-gif 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 | sample-image-gif 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 | --------------------------------------------------------------------------------