├── .gitignore ├── LICENSE ├── README.md ├── assets ├── button.png ├── card.png ├── project-liquid.gif └── video.mov ├── bun.lock ├── esbuild.config.js ├── liquid-glass-example ├── .gitignore ├── README.md ├── bun.lock ├── bun.lockb ├── next.config.ts ├── package.json ├── postcss.config.mjs ├── public │ └── favicon.ico ├── src │ ├── pages │ │ ├── _app.tsx │ │ ├── _document.tsx │ │ ├── api │ │ │ └── hello.ts │ │ └── index.tsx │ └── styles │ │ └── globals.css └── tsconfig.json ├── package-lock.json ├── package.json ├── src ├── index.tsx ├── shader-utils.ts └── utils.ts └── tsconfig.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Based on https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | 5 | logs 6 | _.log 7 | npm-debug.log_ 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | .pnpm-debug.log* 12 | 13 | # Caches 14 | 15 | .cache 16 | 17 | # Diagnostic reports (https://nodejs.org/api/report.html) 18 | 19 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 20 | 21 | # Runtime data 22 | 23 | pids 24 | _.pid 25 | _.seed 26 | *.pid.lock 27 | 28 | # Directory for instrumented libs generated by jscoverage/JSCover 29 | 30 | lib-cov 31 | 32 | # Coverage directory used by tools like istanbul 33 | 34 | coverage 35 | *.lcov 36 | 37 | # nyc test coverage 38 | 39 | .nyc_output 40 | 41 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 42 | 43 | .grunt 44 | 45 | # Bower dependency directory (https://bower.io/) 46 | 47 | bower_components 48 | 49 | # node-waf configuration 50 | 51 | .lock-wscript 52 | 53 | # Compiled binary addons (https://nodejs.org/api/addons.html) 54 | 55 | build/Release 56 | 57 | # Dependency directories 58 | 59 | node_modules/ 60 | jspm_packages/ 61 | 62 | # Snowpack dependency directory (https://snowpack.dev/) 63 | 64 | web_modules/ 65 | 66 | # TypeScript cache 67 | 68 | *.tsbuildinfo 69 | 70 | # Optional npm cache directory 71 | 72 | .npm 73 | 74 | # Optional eslint cache 75 | 76 | .eslintcache 77 | 78 | # Optional stylelint cache 79 | 80 | .stylelintcache 81 | 82 | # Microbundle cache 83 | 84 | .rpt2_cache/ 85 | .rts2_cache_cjs/ 86 | .rts2_cache_es/ 87 | .rts2_cache_umd/ 88 | 89 | # Optional REPL history 90 | 91 | .node_repl_history 92 | 93 | # Output of 'npm pack' 94 | 95 | *.tgz 96 | 97 | # Yarn Integrity file 98 | 99 | .yarn-integrity 100 | 101 | # dotenv environment variable files 102 | 103 | .env 104 | .env.development.local 105 | .env.test.local 106 | .env.production.local 107 | .env.local 108 | 109 | # parcel-bundler cache (https://parceljs.org/) 110 | 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Build output 124 | dist/ 125 | 126 | # Gatsby files 127 | 128 | # Comment in the public line in if your project uses Gatsby and not Next.js 129 | 130 | # https://nextjs.org/blog/next-9-1#public-directory-support 131 | 132 | # public 133 | 134 | # vuepress build output 135 | 136 | .vuepress/dist 137 | 138 | # vuepress v2.x temp and cache directory 139 | 140 | .temp 141 | 142 | # Docusaurus cache and generated files 143 | 144 | .docusaurus 145 | 146 | # Serverless directories 147 | 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | 152 | .fusebox/ 153 | 154 | # DynamoDB Local files 155 | 156 | .dynamodb/ 157 | 158 | # TernJS port file 159 | 160 | .tern-port 161 | 162 | # Stores VSCode versions used for testing VSCode extensions 163 | 164 | .vscode-test 165 | 166 | # yarn v2 167 | 168 | .yarn/cache 169 | .yarn/unplugged 170 | .yarn/build-state.yml 171 | .yarn/install-state.gz 172 | .pnp.* 173 | 174 | # IntelliJ based IDEs 175 | .idea 176 | 177 | # Finder (MacOS) folder config 178 | .DS_Store 179 | 180 | liquid-glass-example/src/components 181 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 MAX ROVENSKY 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Liquid Glass React 2 | 3 | Apple's Liquid Glass effect for React. 4 | 5 | Card Example | Button Example 6 | :-------------------------:|:-------------------------: 7 |  |  8 | 9 | ## 🎬 Demo 10 | 11 | [Click here](https://liquid-glass.maxrovensky.com) to see it in action! 12 | 13 |  14 | 15 | ## ✨ Features 16 | 17 | - Proper edgy bending and refraction 18 | - Multiple refraction modes 19 | - Configurable frosty level 20 | - Supports arbitrary child elements 21 | - Configurable paddings 22 | - Correct hover and click effects 23 | - Edges and highlights take on the underlying light like Apple's does 24 | - Configurable chromatic aberration 25 | - Configurable elasticity, to mimic Apple's "liquid" feel 26 | 27 | > **⚠️ NOTE:** Safari and Firefox only partially support the effect (displacement will not be visible) 28 | 29 | ## 🚀 Usage 30 | 31 | ### Installation 32 | 33 | ```bash 34 | npm install liquid-glass-react 35 | ``` 36 | 37 | ### Basic Usage 38 | 39 | ```tsx 40 | import LiquidGlass from 'liquid-glass-react' 41 | 42 | function App() { 43 | return ( 44 | <LiquidGlass> 45 | <div className="p-6"> 46 | <h2>Your content here</h2> 47 | <p>This will have the liquid glass effect</p> 48 | </div> 49 | </LiquidGlass> 50 | ) 51 | } 52 | ``` 53 | 54 | ### Button Example 55 | 56 | ```tsx 57 | <LiquidGlass 58 | displacementScale={64} 59 | blurAmount={0.1} 60 | saturation={130} 61 | aberrationIntensity={2} 62 | elasticity={0.35} 63 | cornerRadius={100} 64 | padding="8px 16px" 65 | onClick={() => console.log('Button clicked!')} 66 | > 67 | <span className="text-white font-medium">Click Me</span> 68 | </LiquidGlass> 69 | ``` 70 | 71 | ### Mouse Container Example 72 | 73 | When you want the glass effect to respond to mouse movement over a larger area (like a parent container), use the `mouseContainer` prop: 74 | 75 | ```tsx 76 | function App() { 77 | const containerRef = useRef<HTMLDivElement>(null) 78 | 79 | return ( 80 | <div ref={containerRef} className="w-full h-screen bg-image"> 81 | <LiquidGlass 82 | mouseContainer={containerRef} 83 | elasticity={0.3} 84 | style={{ position: 'fixed', top: '50%', left: '50%' }} 85 | > 86 | <div className="p-6"> 87 | <h2>Glass responds to mouse anywhere in the container</h2> 88 | </div> 89 | </LiquidGlass> 90 | </div> 91 | ) 92 | } 93 | ``` 94 | 95 | ## Props 96 | 97 | | Prop | Type | Default | Description | 98 | |------|------|---------|-------------| 99 | | `children` | `React.ReactNode` | - | The content to render inside the glass container | 100 | | `displacementScale` | `number` | `70` | Controls the intensity of the displacement effect | 101 | | `blurAmount` | `number` | `0.0625` | Controls the blur/frosting level | 102 | | `saturation` | `number` | `140` | Controls color saturation of the glass effect | 103 | | `aberrationIntensity` | `number` | `2` | Controls chromatic aberration intensity | 104 | | `elasticity` | `number` | `0.15` | Controls the "liquid" elastic feel (0 = rigid, higher = more elastic) | 105 | | `cornerRadius` | `number` | `999` | Border radius in pixels | 106 | | `className` | `string` | `""` | Additional CSS classes | 107 | | `padding` | `string` | - | CSS padding value | 108 | | `style` | `React.CSSProperties` | - | Additional inline styles | 109 | | `overLight` | `boolean` | `false` | Whether the glass is over a light background | 110 | | `onClick` | `() => void` | - | Click handler | 111 | | `mouseContainer` | `React.RefObject<HTMLElement \| null> \| null` | `null` | Container element to track mouse movement on (defaults to the glass component itself) | 112 | | `mode` | `"standard" \| "polar" \| "prominent" \| "shader"` | `"standard"` | Refraction mode for different visual effects. `shader` is the most accurate but not the most stable. | 113 | | `globalMousePos` | `{ x: number; y: number }` | - | Global mouse position coordinates for manual control | 114 | | `mouseOffset` | `{ x: number; y: number }` | - | Mouse position offset for fine-tuning positioning | 115 | -------------------------------------------------------------------------------- /assets/button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdev/liquid-glass-react/ac48eab18d1f7f444ae30002d240cae29c863a21/assets/button.png -------------------------------------------------------------------------------- /assets/card.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdev/liquid-glass-react/ac48eab18d1f7f444ae30002d240cae29c863a21/assets/card.png -------------------------------------------------------------------------------- /assets/project-liquid.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdev/liquid-glass-react/ac48eab18d1f7f444ae30002d240cae29c863a21/assets/project-liquid.gif -------------------------------------------------------------------------------- /assets/video.mov: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdev/liquid-glass-react/ac48eab18d1f7f444ae30002d240cae29c863a21/assets/video.mov -------------------------------------------------------------------------------- /bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "liquid-glass-react", 6 | "devDependencies": { 7 | "@biomejs/biome": "^1.9.4", 8 | "@types/react": "^18.2.0", 9 | "@types/react-dom": "^18.2.0", 10 | "esbuild": "^0.19.0", 11 | "react": "^18.2.0", 12 | "react-dom": "^18.2.0", 13 | "typescript": "^5.0.0", 14 | }, 15 | "peerDependencies": { 16 | "react": ">=16.8.0", 17 | "react-dom": ">=16.8.0", 18 | }, 19 | }, 20 | }, 21 | "packages": { 22 | "@biomejs/biome": ["@biomejs/biome@1.9.4", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "1.9.4", "@biomejs/cli-darwin-x64": "1.9.4", "@biomejs/cli-linux-arm64": "1.9.4", "@biomejs/cli-linux-arm64-musl": "1.9.4", "@biomejs/cli-linux-x64": "1.9.4", "@biomejs/cli-linux-x64-musl": "1.9.4", "@biomejs/cli-win32-arm64": "1.9.4", "@biomejs/cli-win32-x64": "1.9.4" }, "bin": { "biome": "bin/biome" } }, "sha512-1rkd7G70+o9KkTn5KLmDYXihGoTaIGO9PIIN2ZB7UJxFrWw04CZHPYiMRjYsaDvVV7hP1dYNRLxSANLaBFGpog=="], 23 | 24 | "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@1.9.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bFBsPWrNvkdKrNCYeAp+xo2HecOGPAy9WyNyB/jKnnedgzl4W4Hb9ZMzYNbf8dMCGmUdSavlYHiR01QaYR58cw=="], 25 | 26 | "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@1.9.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-ngYBh/+bEedqkSevPVhLP4QfVPCpb+4BBe2p7Xs32dBgs7rh9nY2AIYUL6BgLw1JVXV8GlpKmb/hNiuIxfPfZg=="], 27 | 28 | "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-fJIW0+LYujdjUgJJuwesP4EjIBl/N/TcOX3IvIHJQNsAqvV2CHIogsmA94BPG6jZATS4Hi+xv4SkBBQSt1N4/g=="], 29 | 30 | "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@1.9.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-v665Ct9WCRjGa8+kTr0CzApU0+XXtRgwmzIf1SeKSGAv+2scAlW6JR5PMFo6FzqqZ64Po79cKODKf3/AAmECqA=="], 31 | 32 | "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-lRCJv/Vi3Vlwmbd6K+oQ0KhLHMAysN8lXoCI7XeHlxaajk06u7G+UsFSO01NAs5iYuWKmVZjmiOzJ0OJmGsMwg=="], 33 | 34 | "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@1.9.4", "", { "os": "linux", "cpu": "x64" }, "sha512-gEhi/jSBhZ2m6wjV530Yy8+fNqG8PAinM3oV7CyO+6c3CEh16Eizm21uHVsyVBEB6RIM8JHIl6AGYCv6Q6Q9Tg=="], 35 | 36 | "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@1.9.4", "", { "os": "win32", "cpu": "arm64" }, "sha512-tlbhLk+WXZmgwoIKwHIHEBZUwxml7bRJgk0X2sPyNR3S93cdRq6XulAZRQJ17FYGGzWne0fgrXBKpl7l4M87Hg=="], 37 | 38 | "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@1.9.4", "", { "os": "win32", "cpu": "x64" }, "sha512-8Y5wMhVIPaWe6jw2H+KlEm4wP/f7EW3810ZLmDlrEEy5KvBsb9ECEfu/kMWD484ijfQ8+nIi0giMgu9g1UAuuA=="], 39 | 40 | "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.19.12", "", { "os": "aix", "cpu": "ppc64" }, "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA=="], 41 | 42 | "@esbuild/android-arm": ["@esbuild/android-arm@0.19.12", "", { "os": "android", "cpu": "arm" }, "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w=="], 43 | 44 | "@esbuild/android-arm64": ["@esbuild/android-arm64@0.19.12", "", { "os": "android", "cpu": "arm64" }, "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA=="], 45 | 46 | "@esbuild/android-x64": ["@esbuild/android-x64@0.19.12", "", { "os": "android", "cpu": "x64" }, "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew=="], 47 | 48 | "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.19.12", "", { "os": "darwin", "cpu": "arm64" }, "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g=="], 49 | 50 | "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.19.12", "", { "os": "darwin", "cpu": "x64" }, "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A=="], 51 | 52 | "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.19.12", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA=="], 53 | 54 | "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.19.12", "", { "os": "freebsd", "cpu": "x64" }, "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg=="], 55 | 56 | "@esbuild/linux-arm": ["@esbuild/linux-arm@0.19.12", "", { "os": "linux", "cpu": "arm" }, "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w=="], 57 | 58 | "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.19.12", "", { "os": "linux", "cpu": "arm64" }, "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA=="], 59 | 60 | "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.19.12", "", { "os": "linux", "cpu": "ia32" }, "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA=="], 61 | 62 | "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA=="], 63 | 64 | "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w=="], 65 | 66 | "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.19.12", "", { "os": "linux", "cpu": "ppc64" }, "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg=="], 67 | 68 | "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.19.12", "", { "os": "linux", "cpu": "none" }, "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg=="], 69 | 70 | "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.19.12", "", { "os": "linux", "cpu": "s390x" }, "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg=="], 71 | 72 | "@esbuild/linux-x64": ["@esbuild/linux-x64@0.19.12", "", { "os": "linux", "cpu": "x64" }, "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg=="], 73 | 74 | "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.19.12", "", { "os": "none", "cpu": "x64" }, "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA=="], 75 | 76 | "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.19.12", "", { "os": "openbsd", "cpu": "x64" }, "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw=="], 77 | 78 | "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.19.12", "", { "os": "sunos", "cpu": "x64" }, "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA=="], 79 | 80 | "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.19.12", "", { "os": "win32", "cpu": "arm64" }, "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A=="], 81 | 82 | "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.19.12", "", { "os": "win32", "cpu": "ia32" }, "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ=="], 83 | 84 | "@esbuild/win32-x64": ["@esbuild/win32-x64@0.19.12", "", { "os": "win32", "cpu": "x64" }, "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA=="], 85 | 86 | "@types/prop-types": ["@types/prop-types@15.7.15", "", {}, "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw=="], 87 | 88 | "@types/react": ["@types/react@18.3.23", "", { "dependencies": { "@types/prop-types": "*", "csstype": "^3.0.2" } }, "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w=="], 89 | 90 | "@types/react-dom": ["@types/react-dom@18.3.7", "", { "peerDependencies": { "@types/react": "^18.0.0" } }, "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ=="], 91 | 92 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 93 | 94 | "esbuild": ["esbuild@0.19.12", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.19.12", "@esbuild/android-arm": "0.19.12", "@esbuild/android-arm64": "0.19.12", "@esbuild/android-x64": "0.19.12", "@esbuild/darwin-arm64": "0.19.12", "@esbuild/darwin-x64": "0.19.12", "@esbuild/freebsd-arm64": "0.19.12", "@esbuild/freebsd-x64": "0.19.12", "@esbuild/linux-arm": "0.19.12", "@esbuild/linux-arm64": "0.19.12", "@esbuild/linux-ia32": "0.19.12", "@esbuild/linux-loong64": "0.19.12", "@esbuild/linux-mips64el": "0.19.12", "@esbuild/linux-ppc64": "0.19.12", "@esbuild/linux-riscv64": "0.19.12", "@esbuild/linux-s390x": "0.19.12", "@esbuild/linux-x64": "0.19.12", "@esbuild/netbsd-x64": "0.19.12", "@esbuild/openbsd-x64": "0.19.12", "@esbuild/sunos-x64": "0.19.12", "@esbuild/win32-arm64": "0.19.12", "@esbuild/win32-ia32": "0.19.12", "@esbuild/win32-x64": "0.19.12" }, "bin": "bin/esbuild" }, "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg=="], 95 | 96 | "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], 97 | 98 | "loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q=="], 99 | 100 | "react": ["react@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ=="], 101 | 102 | "react-dom": ["react-dom@18.3.1", "", { "dependencies": { "loose-envify": "^1.1.0", "scheduler": "^0.23.2" }, "peerDependencies": { "react": "^18.3.1" } }, "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw=="], 103 | 104 | "scheduler": ["scheduler@0.23.2", "", { "dependencies": { "loose-envify": "^1.1.0" } }, "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ=="], 105 | 106 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /esbuild.config.js: -------------------------------------------------------------------------------- 1 | const esbuild = require('esbuild'); 2 | const path = require('path'); 3 | 4 | const createBuildConfig = (format) => ({ 5 | entryPoints: ['src/index.ts'], 6 | bundle: true, 7 | format, 8 | outfile: format === 'esm' ? 'dist/index.esm.js' : 'dist/index.js', 9 | external: ['react', 'react-dom'], 10 | target: ['es2020'], 11 | jsx: 'automatic', 12 | jsxImportSource: 'react', 13 | minify: process.env.NODE_ENV === 'production', 14 | sourcemap: true, 15 | platform: 'browser', 16 | splitting: false, 17 | loader: { 18 | '.tsx': 'tsx', 19 | '.ts': 'ts', 20 | '.js': 'js', 21 | '.jsx': 'jsx' 22 | }, 23 | define: { 24 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development') 25 | } 26 | }); 27 | 28 | const buildAll = async () => { 29 | try { 30 | console.log('🚀 Building ESM bundle...'); 31 | await esbuild.build(createBuildConfig('esm')); 32 | console.log('✅ ESM bundle built successfully'); 33 | 34 | console.log('🚀 Building CJS bundle...'); 35 | await esbuild.build(createBuildConfig('cjs')); 36 | console.log('✅ CJS bundle built successfully'); 37 | 38 | console.log('🎉 All bundles built successfully!'); 39 | } catch (error) { 40 | console.error('❌ Build failed:', error); 41 | process.exit(1); 42 | } 43 | }; 44 | 45 | const watch = async () => { 46 | try { 47 | console.log('👀 Starting watch mode for ESM bundle...'); 48 | const ctx = await esbuild.context(createBuildConfig('esm')); 49 | await ctx.watch(); 50 | console.log('✅ Watch mode active - listening for changes...'); 51 | } catch (error) { 52 | console.error('❌ Watch mode failed:', error); 53 | process.exit(1); 54 | } 55 | }; 56 | 57 | // Check command line arguments 58 | const args = process.argv.slice(2); 59 | if (args.includes('--watch')) { 60 | watch(); 61 | } else { 62 | buildAll(); 63 | } 64 | 65 | module.exports = { createBuildConfig, buildAll, watch }; -------------------------------------------------------------------------------- /liquid-glass-example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /liquid-glass-example/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/pages/api-reference/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `pages/index.tsx`. The page auto-updates as you edit the file. 20 | 21 | [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) can be accessed on [http://localhost:3000/api/hello](http://localhost:3000/api/hello). This endpoint can be edited in `pages/api/hello.ts`. 22 | 23 | The `pages/api` directory is mapped to `/api/*`. Files in this directory are treated as [API routes](https://nextjs.org/docs/pages/building-your-application/routing/api-routes) instead of React pages. 24 | 25 | This project uses [`next/font`](https://nextjs.org/docs/pages/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 26 | 27 | ## Learn More 28 | 29 | To learn more about Next.js, take a look at the following resources: 30 | 31 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 32 | - [Learn Next.js](https://nextjs.org/learn-pages-router) - an interactive Next.js tutorial. 33 | 34 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 35 | 36 | ## Deploy on Vercel 37 | 38 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 39 | 40 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/pages/building-your-application/deploying) for more details. 41 | -------------------------------------------------------------------------------- /liquid-glass-example/bun.lock: -------------------------------------------------------------------------------- 1 | { 2 | "lockfileVersion": 1, 3 | "workspaces": { 4 | "": { 5 | "name": "liquid-glass-example", 6 | "dependencies": { 7 | "liquid-glass-react": "latest", 8 | "lucide-react": "^0.514.0", 9 | "next": "15.3.3", 10 | "react": "^19.0.0", 11 | "react-dom": "^19.0.0", 12 | }, 13 | "devDependencies": { 14 | "@tailwindcss/postcss": "^4", 15 | "@types/node": "^20", 16 | "@types/react": "^19", 17 | "@types/react-dom": "^19", 18 | "tailwindcss": "^4", 19 | "typescript": "^5", 20 | }, 21 | }, 22 | }, 23 | "packages": { 24 | "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], 25 | 26 | "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], 27 | 28 | "@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], 29 | 30 | "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.1.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-OfXHZPppddivUJnqyKoi5YVeHRkkNE2zUFT2gbpKxp/JZCFYEYubnMg+gOp6lWfasPrTS+KPosKqdI+ELYVDtg=="], 31 | 32 | "@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.1.0" }, "os": "darwin", "cpu": "x64" }, "sha512-dYvWqmjU9VxqXmjEtjmvHnGqF8GrVjM2Epj9rJ6BUIXvk8slvNDJbhGFvIoXzkDhrJC2jUxNLz/GUjjvSzfw+g=="], 33 | 34 | "@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.1.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA=="], 35 | 36 | "@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.1.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ=="], 37 | 38 | "@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.1.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA=="], 39 | 40 | "@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew=="], 41 | 42 | "@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.1.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ=="], 43 | 44 | "@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.1.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA=="], 45 | 46 | "@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q=="], 47 | 48 | "@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.1.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w=="], 49 | 50 | "@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.1.0", "", { "os": "linux", "cpu": "x64" }, "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A=="], 51 | 52 | "@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.1.0" }, "os": "linux", "cpu": "arm" }, "sha512-0DZzkvuEOqQUP9mo2kjjKNok5AmnOr1jB2XYjkaoNRwpAYMDzRmAqUIa1nRi58S2WswqSfPOWLNOr0FDT3H5RQ=="], 53 | 54 | "@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-D8n8wgWmPDakc83LORcfJepdOSN6MvWNzzz2ux0MnIbOqdieRZwVYY32zxVx+IFUT8er5KPcyU3XXsn+GzG/0Q=="], 55 | 56 | "@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.1.0" }, "os": "linux", "cpu": "s390x" }, "sha512-EGZ1xwhBI7dNISwxjChqBGELCWMGDvmxZXKjQRuqMrakhO8QoMgqCrdjnAqJq/CScxfRn+Bb7suXBElKQpPDiw=="], 57 | 58 | "@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-sD7J+h5nFLMMmOXYH4DD9UtSNBD05tWSSdWAcEyzqW8Cn5UxXvsHAxmxSesYUsTOBmUnjtxghKDl15EvfqLFbQ=="], 59 | 60 | "@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" }, "os": "linux", "cpu": "arm64" }, "sha512-NEE2vQ6wcxYav1/A22OOxoSOGiKnNmDzCYFOZ949xFmrWZOVII1Bp3NqVVpvj+3UeHMFyN5eP/V5hzViQ5CZNA=="], 61 | 62 | "@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.2", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.1.0" }, "os": "linux", "cpu": "x64" }, "sha512-DOYMrDm5E6/8bm/yQLCWyuDJwUnlevR8xtF8bs+gjZ7cyUNYXiSf/E8Kp0Ss5xasIaXSHzb888V1BE4i1hFhAA=="], 63 | 64 | "@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.2", "", { "dependencies": { "@emnapi/runtime": "^1.4.3" }, "cpu": "none" }, "sha512-/VI4mdlJ9zkaq53MbIG6rZY+QRN3MLbR6usYlgITEzi4Rpx5S6LFKsycOQjkOGmqTNmkIdLjEvooFKwww6OpdQ=="], 65 | 66 | "@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-cfP/r9FdS63VA5k0xiqaNaEoGxBg9k7uE+RQGzuK9fHt7jib4zAVVseR9LsE4gJcNWgT6APKMNnCcnyOtmSEUQ=="], 67 | 68 | "@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-QLjGGvAbj0X/FXl8n1WbtQ6iVBpWU7JO94u/P2M4a8CFYsvQi4GW2mRy/JqkRx0qpBzaOdKJKw8uc930EX2AHw=="], 69 | 70 | "@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.2", "", { "os": "win32", "cpu": "x64" }, "sha512-aUdT6zEYtDKCaxkofmmJDJYGCf0+pJg3eU9/oBuqvEeoB9dKI6ZLc/1iLJCTuJQDO4ptntAlkUmHgGjyuobZbw=="], 71 | 72 | "@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="], 73 | 74 | "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.8", "", { "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA=="], 75 | 76 | "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], 77 | 78 | "@jridgewell/set-array": ["@jridgewell/set-array@1.2.1", "", {}, "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A=="], 79 | 80 | "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.0", "", {}, "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ=="], 81 | 82 | "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.25", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ=="], 83 | 84 | "@next/env": ["@next/env@15.3.3", "", {}, "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw=="], 85 | 86 | "@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.3.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg=="], 87 | 88 | "@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.3.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw=="], 89 | 90 | "@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw=="], 91 | 92 | "@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.3.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA=="], 93 | 94 | "@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw=="], 95 | 96 | "@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.3.3", "", { "os": "linux", "cpu": "x64" }, "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw=="], 97 | 98 | "@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.3.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ=="], 99 | 100 | "@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.3.3", "", { "os": "win32", "cpu": "x64" }, "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw=="], 101 | 102 | "@swc/counter": ["@swc/counter@0.1.3", "", {}, "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ=="], 103 | 104 | "@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="], 105 | 106 | "@tailwindcss/node": ["@tailwindcss/node@4.1.8", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "enhanced-resolve": "^5.18.1", "jiti": "^2.4.2", "lightningcss": "1.30.1", "magic-string": "^0.30.17", "source-map-js": "^1.2.1", "tailwindcss": "4.1.8" } }, "sha512-OWwBsbC9BFAJelmnNcrKuf+bka2ZxCE2A4Ft53Tkg4uoiE67r/PMEYwCsourC26E+kmxfwE0hVzMdxqeW+xu7Q=="], 107 | 108 | "@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.8", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.8", "@tailwindcss/oxide-darwin-arm64": "4.1.8", "@tailwindcss/oxide-darwin-x64": "4.1.8", "@tailwindcss/oxide-freebsd-x64": "4.1.8", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.8", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.8", "@tailwindcss/oxide-linux-arm64-musl": "4.1.8", "@tailwindcss/oxide-linux-x64-gnu": "4.1.8", "@tailwindcss/oxide-linux-x64-musl": "4.1.8", "@tailwindcss/oxide-wasm32-wasi": "4.1.8", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.8", "@tailwindcss/oxide-win32-x64-msvc": "4.1.8" } }, "sha512-d7qvv9PsM5N3VNKhwVUhpK6r4h9wtLkJ6lz9ZY9aeZgrUWk1Z8VPyqyDT9MZlem7GTGseRQHkeB1j3tC7W1P+A=="], 109 | 110 | "@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.8", "", { "os": "android", "cpu": "arm64" }, "sha512-Fbz7qni62uKYceWYvUjRqhGfZKwhZDQhlrJKGtnZfuNtHFqa8wmr+Wn74CTWERiW2hn3mN5gTpOoxWKk0jRxjg=="], 111 | 112 | "@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.8", "", { "os": "darwin", "cpu": "arm64" }, "sha512-RdRvedGsT0vwVVDztvyXhKpsU2ark/BjgG0huo4+2BluxdXo8NDgzl77qh0T1nUxmM11eXwR8jA39ibvSTbi7A=="], 113 | 114 | "@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.8", "", { "os": "darwin", "cpu": "x64" }, "sha512-t6PgxjEMLp5Ovf7uMb2OFmb3kqzVTPPakWpBIFzppk4JE4ix0yEtbtSjPbU8+PZETpaYMtXvss2Sdkx8Vs4XRw=="], 115 | 116 | "@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.8", "", { "os": "freebsd", "cpu": "x64" }, "sha512-g8C8eGEyhHTqwPStSwZNSrOlyx0bhK/V/+zX0Y+n7DoRUzyS8eMbVshVOLJTDDC+Qn9IJnilYbIKzpB9n4aBsg=="], 117 | 118 | "@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.8", "", { "os": "linux", "cpu": "arm" }, "sha512-Jmzr3FA4S2tHhaC6yCjac3rGf7hG9R6Gf2z9i9JFcuyy0u79HfQsh/thifbYTF2ic82KJovKKkIB6Z9TdNhCXQ=="], 119 | 120 | "@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-qq7jXtO1+UEtCmCeBBIRDrPFIVI4ilEQ97qgBGdwXAARrUqSn/L9fUrkb1XP/mvVtoVeR2bt/0L77xx53bPZ/Q=="], 121 | 122 | "@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.8", "", { "os": "linux", "cpu": "arm64" }, "sha512-O6b8QesPbJCRshsNApsOIpzKt3ztG35gfX9tEf4arD7mwNinsoCKxkj8TgEE0YRjmjtO3r9FlJnT/ENd9EVefQ=="], 123 | 124 | "@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-32iEXX/pXwikshNOGnERAFwFSfiltmijMIAbUhnNyjFr3tmWmMJWQKU2vNcFX0DACSXJ3ZWcSkzNbaKTdngH6g=="], 125 | 126 | "@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.8", "", { "os": "linux", "cpu": "x64" }, "sha512-s+VSSD+TfZeMEsCaFaHTaY5YNj3Dri8rST09gMvYQKwPphacRG7wbuQ5ZJMIJXN/puxPcg/nU+ucvWguPpvBDg=="], 127 | 128 | "@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.8", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@emnapi/wasi-threads": "^1.0.2", "@napi-rs/wasm-runtime": "^0.2.10", "@tybys/wasm-util": "^0.9.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-CXBPVFkpDjM67sS1psWohZ6g/2/cd+cq56vPxK4JeawelxwK4YECgl9Y9TjkE2qfF+9/s1tHHJqrC4SS6cVvSg=="], 129 | 130 | "@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.8", "", { "os": "win32", "cpu": "arm64" }, "sha512-7GmYk1n28teDHUjPlIx4Z6Z4hHEgvP5ZW2QS9ygnDAdI/myh3HTHjDqtSqgu1BpRoI4OiLx+fThAyA1JePoENA=="], 131 | 132 | "@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.8", "", { "os": "win32", "cpu": "x64" }, "sha512-fou+U20j+Jl0EHwK92spoWISON2OBnCazIc038Xj2TdweYV33ZRkS9nwqiUi2d/Wba5xg5UoHfvynnb/UB49cQ=="], 133 | 134 | "@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.8", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.8", "@tailwindcss/oxide": "4.1.8", "postcss": "^8.4.41", "tailwindcss": "4.1.8" } }, "sha512-vB/vlf7rIky+w94aWMw34bWW1ka6g6C3xIOdICKX2GC0VcLtL6fhlLiafF0DVIwa9V6EHz8kbWMkS2s2QvvNlw=="], 135 | 136 | "@types/node": ["@types/node@20.19.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-hfrc+1tud1xcdVTABC2JiomZJEklMcXYNTVtZLAeqTVWD+qL5jkHKT+1lOtqDdGxt+mB53DTtiz673vfjU8D1Q=="], 137 | 138 | "@types/react": ["@types/react@19.1.7", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-BnsPLV43ddr05N71gaGzyZ5hzkCmGwhMvYc8zmvI8Ci1bRkkDSzDDVfAXfN2tk748OwI7ediiPX6PfT9p0QGVg=="], 139 | 140 | "@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="], 141 | 142 | "busboy": ["busboy@1.6.0", "", { "dependencies": { "streamsearch": "^1.1.0" } }, "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA=="], 143 | 144 | "caniuse-lite": ["caniuse-lite@1.0.30001721", "", {}, "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ=="], 145 | 146 | "chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="], 147 | 148 | "client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="], 149 | 150 | "color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="], 151 | 152 | "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], 153 | 154 | "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], 155 | 156 | "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], 157 | 158 | "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], 159 | 160 | "detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="], 161 | 162 | "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], 163 | 164 | "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], 165 | 166 | "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], 167 | 168 | "jiti": ["jiti@2.4.2", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A=="], 169 | 170 | "lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="], 171 | 172 | "lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="], 173 | 174 | "lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="], 175 | 176 | "lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="], 177 | 178 | "lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="], 179 | 180 | "lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="], 181 | 182 | "lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="], 183 | 184 | "lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="], 185 | 186 | "lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="], 187 | 188 | "lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="], 189 | 190 | "lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="], 191 | 192 | "liquid-glass-react": ["liquid-glass-react@1.0.0", "", { "peerDependencies": { "react": ">=19", "react-dom": ">=19" } }, "sha512-2IKXBoYJAkKZIINSIdRxHbita9BCPooSKlNzEZrs9uSw0retWovYeTl3fqKtXYvOQo0Myza4NL4DoRC28uJGtQ=="], 193 | 194 | "lucide-react": ["lucide-react@0.514.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-HXD0OAMd+JM2xCjlwG1EGW9Nuab64dhjO3+MvdyD+pSUeOTBaVAPhQblKIYmmX4RyBYbdzW0VWnJpjJmxWGr6w=="], 195 | 196 | "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], 197 | 198 | "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], 199 | 200 | "minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="], 201 | 202 | "mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="], 203 | 204 | "nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], 205 | 206 | "next": ["next@15.3.3", "", { "dependencies": { "@next/env": "15.3.3", "@swc/counter": "0.1.3", "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.3.3", "@next/swc-darwin-x64": "15.3.3", "@next/swc-linux-arm64-gnu": "15.3.3", "@next/swc-linux-arm64-musl": "15.3.3", "@next/swc-linux-x64-gnu": "15.3.3", "@next/swc-linux-x64-musl": "15.3.3", "@next/swc-win32-arm64-msvc": "15.3.3", "@next/swc-win32-x64-msvc": "15.3.3", "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw=="], 207 | 208 | "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], 209 | 210 | "postcss": ["postcss@8.5.4", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-QSa9EBe+uwlGTFmHsPKokv3B/oEMQZxfqW0QqNCyhpa6mB1afzulwn8hihglqAb2pOw+BJgNlmXQ8la2VeHB7w=="], 211 | 212 | "react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="], 213 | 214 | "react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="], 215 | 216 | "scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="], 217 | 218 | "semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="], 219 | 220 | "sharp": ["sharp@0.34.2", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.2", "@img/sharp-darwin-x64": "0.34.2", "@img/sharp-libvips-darwin-arm64": "1.1.0", "@img/sharp-libvips-darwin-x64": "1.1.0", "@img/sharp-libvips-linux-arm": "1.1.0", "@img/sharp-libvips-linux-arm64": "1.1.0", "@img/sharp-libvips-linux-ppc64": "1.1.0", "@img/sharp-libvips-linux-s390x": "1.1.0", "@img/sharp-libvips-linux-x64": "1.1.0", "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", "@img/sharp-libvips-linuxmusl-x64": "1.1.0", "@img/sharp-linux-arm": "0.34.2", "@img/sharp-linux-arm64": "0.34.2", "@img/sharp-linux-s390x": "0.34.2", "@img/sharp-linux-x64": "0.34.2", "@img/sharp-linuxmusl-arm64": "0.34.2", "@img/sharp-linuxmusl-x64": "0.34.2", "@img/sharp-wasm32": "0.34.2", "@img/sharp-win32-arm64": "0.34.2", "@img/sharp-win32-ia32": "0.34.2", "@img/sharp-win32-x64": "0.34.2" } }, "sha512-lszvBmB9QURERtyKT2bNmsgxXK0ShJrL/fvqlonCo7e6xBF8nT8xU6pW+PMIbLsz0RxQk3rgH9kd8UmvOzlMJg=="], 221 | 222 | "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], 223 | 224 | "source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="], 225 | 226 | "streamsearch": ["streamsearch@1.1.0", "", {}, "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg=="], 227 | 228 | "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], 229 | 230 | "tailwindcss": ["tailwindcss@4.1.8", "", {}, "sha512-kjeW8gjdxasbmFKpVGrGd5T4i40mV5J2Rasw48QARfYeQ8YS9x02ON9SFWax3Qf616rt4Cp3nVNIj6Hd1mP3og=="], 231 | 232 | "tapable": ["tapable@2.2.2", "", {}, "sha512-Re10+NauLTMCudc7T5WLFLAwDhQ0JWdrMK+9B2M8zR5hRExKmsRDCBA7/aV/pNJFltmBFO5BAMlQFi/vq3nKOg=="], 233 | 234 | "tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="], 235 | 236 | "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 237 | 238 | "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], 239 | 240 | "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], 241 | 242 | "yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="], 243 | 244 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.4.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.0.2", "tslib": "^2.4.0" }, "bundled": true }, "sha512-4m62DuCE07lw01soJwPiBGC0nAww0Q+RY70VZ+n49yDIO13yyinhbWCeNnaob0lakDtWQzSdtNWzJeOJt2ma+g=="], 245 | 246 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.4.3", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ=="], 247 | 248 | "@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.0.2", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-5n3nTJblwRi8LlXkJ9eBzu+kZR8Yxcc7ubakyQTFzPMtIhFpUBRbsnc2Dv88IZDIbCDlBiWrknhB4Lsz7mg6BA=="], 249 | 250 | "@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.11", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.9.0" }, "bundled": true }, "sha512-9DPkXtvHydrcOsopiYpUgPHpmj0HWZKMUnL2dZqpvC42lsratuBG06V5ipyno0fUek5VlFsNQ+AcFATSrJXgMA=="], 251 | 252 | "@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.9.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-6+7nlbMVX/PVDCwaIQ8nTOPveOcFLSt8GcXdx8hD0bt39uWxYT88uXzqTd4fTvqta7oeUJqudepapKNt2DYJFw=="], 253 | 254 | "@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], 255 | 256 | "next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="], 257 | } 258 | } 259 | -------------------------------------------------------------------------------- /liquid-glass-example/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdev/liquid-glass-react/ac48eab18d1f7f444ae30002d240cae29c863a21/liquid-glass-example/bun.lockb -------------------------------------------------------------------------------- /liquid-glass-example/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | reactStrictMode: true, 6 | experimental: { 7 | externalDir: true, 8 | }, 9 | }; 10 | 11 | export default nextConfig; 12 | -------------------------------------------------------------------------------- /liquid-glass-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liquid-glass-example", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbopack", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "liquid-glass-react": "^1.0.2", 13 | "lucide-react": "^0.514.0", 14 | "next": "15.3.3", 15 | "react": "^19.0.0", 16 | "react-dom": "^19.0.0" 17 | }, 18 | "devDependencies": { 19 | "typescript": "^5", 20 | "@types/node": "^20", 21 | "@types/react": "^19", 22 | "@types/react-dom": "^19", 23 | "@tailwindcss/postcss": "^4", 24 | "tailwindcss": "^4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /liquid-glass-example/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | }; 4 | 5 | export default config; 6 | -------------------------------------------------------------------------------- /liquid-glass-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rdev/liquid-glass-react/ac48eab18d1f7f444ae30002d240cae29c863a21/liquid-glass-example/public/favicon.ico -------------------------------------------------------------------------------- /liquid-glass-example/src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.css" 2 | import type { AppProps } from "next/app" 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return <Component {...pageProps} /> 6 | } 7 | -------------------------------------------------------------------------------- /liquid-glass-example/src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document" 2 | 3 | export default function Document() { 4 | return ( 5 | <Html lang="en"> 6 | <Head /> 7 | <body className="antialiased"> 8 | <Main /> 9 | <NextScript /> 10 | </body> 11 | </Html> 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /liquid-glass-example/src/pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from "next" 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler(_req: NextApiRequest, res: NextApiResponse<Data>) { 9 | res.status(200).json({ name: "John Doe" }) 10 | } 11 | -------------------------------------------------------------------------------- /liquid-glass-example/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import { Geist } from "next/font/google" 2 | import { useState, useRef } from "react" 3 | import LiquidGlass from "liquid-glass-react" 4 | import { LogOutIcon, Github } from "lucide-react" 5 | 6 | const geistSans = Geist({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | }) 10 | 11 | export default function Home() { 12 | // User Info Card Controls 13 | const [displacementScale, setDisplacementScale] = useState(100) 14 | const [blurAmount, setBlurAmount] = useState(0.5) 15 | const [saturation, setSaturation] = useState(140) 16 | const [aberrationIntensity, setAberrationIntensity] = useState(2) 17 | const [elasticity, setElasticity] = useState(0) 18 | const [cornerRadius, setCornerRadius] = useState(32) 19 | const [userInfoOverLight, setUserInfoOverLight] = useState(false) 20 | const [userInfoMode, setUserInfoMode] = useState<"standard" | "polar" | "prominent" | "shader">("standard") 21 | 22 | // Log Out Button Controls 23 | const [logoutDisplacementScale, setLogoutDisplacementScale] = useState(64) 24 | const [logoutBlurAmount, setLogoutBlurAmount] = useState(0.1) 25 | const [logoutSaturation, setLogoutSaturation] = useState(130) 26 | const [logoutAberrationIntensity, setLogoutAberrationIntensity] = useState(2) 27 | const [logoutElasticity, setLogoutElasticity] = useState(0.35) 28 | const [logoutCornerRadius, setLogoutCornerRadius] = useState(100) 29 | const [logoutOverLight, setLogoutOverLight] = useState(false) 30 | const [logoutMode, setLogoutMode] = useState<"standard" | "polar" | "prominent" | "shader">("standard") 31 | 32 | // Shared state 33 | const [activeTab, setActiveTab] = useState<"userInfo" | "logOut">("userInfo") 34 | const containerRef = useRef<HTMLDivElement>(null) 35 | 36 | const [scroll, setScroll] = useState(0) 37 | 38 | const handleScroll = (event: React.UIEvent<HTMLDivElement>) => { 39 | requestAnimationFrame(() => { 40 | setScroll((event?.target as any)?.scrollTop) 41 | }) 42 | } 43 | 44 | const scrollingOverBrightSection = scroll > 230 && scroll < 500 45 | 46 | return ( 47 | <div 48 | className={`${geistSans.className} grid grid-cols-1 grid-rows-2 md:grid-rows-1 md:grid-cols-3 shadow-2xl w-full max-w-5xl mx-auto md:my-10 h-screen md:max-h-[calc(100vh-5rem)] md:rounded-3xl overflow-hidden font-[family-name:var(--font-geist-sans)]`} 49 | > 50 | {/* Left Panel - Glass Effect Demo */} 51 | <div className="flex-1 relative overflow-auto min-h-screen md:col-span-2" ref={containerRef} onScroll={handleScroll}> 52 | <div className="w-full min-h-[200vh] absolute top-0 left-0 pb-96 mb-96"> 53 | <img src="https://picsum.photos/2000/2000" className="w-full h-96 object-cover" /> 54 | <div className="flex flex-col gap-2" id="bright-section"> 55 | <h2 className="text-2xl font-semibold my-5 text-center">Some Heading</h2> 56 | <p className="px-10"> 57 | Bacon ipsum dolor amet hamburger Bacon ipsum dolor amet hamburger <br /> 58 | Bacon ipsum dolor amet hamburger Bacon ipsum dolor amet hamburger 59 | <br /> 60 | Bacon ipsum dolor amet hamburger Bacon ipsum dolor amet hamburger 61 | <br /> 62 | Bacon ipsum dolor amet hamburger Bacon ipsum dolor amet hamburger 63 | <br /> 64 | Bacon ipsum dolor amet hamburger Bacon ipsum dolor amet hamburger 65 | <br /> 66 | Bacon ipsum dolor amet hamburger Bacon ipsum dolor amet hamburger 67 | </p> 68 | </div> 69 | <img src="https://picsum.photos/1200/1200" className="w-full h-80 object-cover my-10" /> 70 | <img src="https://picsum.photos/1400/1300" className="w-full h-72 object-cover my-10" /> 71 | <img src="https://picsum.photos/1100/1200" className="w-full h-96 object-cover my-10 mb-96" /> 72 | </div> 73 | 74 | {activeTab === "userInfo" && ( 75 | <LiquidGlass 76 | displacementScale={displacementScale} 77 | blurAmount={blurAmount} 78 | saturation={saturation} 79 | aberrationIntensity={aberrationIntensity} 80 | elasticity={elasticity} 81 | cornerRadius={cornerRadius} 82 | mouseContainer={containerRef} 83 | overLight={scrollingOverBrightSection || userInfoOverLight} 84 | mode={userInfoMode} 85 | style={{ 86 | position: "fixed", 87 | top: "25%", 88 | left: "40%", 89 | }} 90 | > 91 | <div className="w-72 text-shadow-lg"> 92 | <h3 className="text-xl font-semibold mb-4">User Info</h3> 93 | <div className="space-y-3"> 94 | <div className="flex items-center space-x-3"> 95 | <div className="w-12 h-12 bg-black/10 backdrop-blur rounded-full flex items-center justify-center text-white font-semibold">JD</div> 96 | <div> 97 | <p className="font-medium">John Doe</p> 98 | <p className="text-sm text-white">Software Engineer</p> 99 | </div> 100 | </div> 101 | <div className="pt-2 space-y-2"> 102 | <div className="flex justify-between"> 103 | <span className="text-sm text-white">Email:</span> 104 | <span className="text-sm">john.doe@example.com</span> 105 | </div> 106 | <div className="flex justify-between"> 107 | <span className="text-sm text-white">Location:</span> 108 | <span className="text-sm">San Francisco, CA</span> 109 | </div> 110 | <div className="flex justify-between"> 111 | <span className="text-sm text-white">Joined:</span> 112 | <span className="text-sm">March 2023</span> 113 | </div> 114 | </div> 115 | </div> 116 | </div> 117 | </LiquidGlass> 118 | )} 119 | 120 | {activeTab === "logOut" && ( 121 | <LiquidGlass 122 | displacementScale={logoutDisplacementScale} 123 | blurAmount={logoutBlurAmount} 124 | saturation={logoutSaturation} 125 | aberrationIntensity={logoutAberrationIntensity} 126 | elasticity={logoutElasticity} 127 | cornerRadius={logoutCornerRadius} 128 | mouseContainer={containerRef} 129 | overLight={scrollingOverBrightSection || logoutOverLight} 130 | mode={logoutMode} 131 | padding="8px 16px" 132 | onClick={() => { 133 | console.log("Logged out") 134 | }} 135 | style={{ 136 | position: "fixed", 137 | top: "20%", 138 | left: "40%", 139 | }} 140 | > 141 | <h3 className="text-lg font-medium flex items-center gap-2"> 142 | Log Out 143 | <LogOutIcon className="w-5 h-5" /> 144 | </h3> 145 | </LiquidGlass> 146 | )} 147 | </div> 148 | 149 | {/* Right Panel - Control Panel */} 150 | <div className="row-start-2 rounded-t-3xl md:rounded-none md:col-start-3 bg-gray-900/80 h-full overflow-y-auto backdrop-blur-md border-l border-white/10 p-8 flex flex-col"> 151 | <div className="mb-8"> 152 | <div className="flex items-center justify-between mb-4"> 153 | <h2 className="text-2xl font-bold text-white">Glassy Boi but Web</h2> 154 | <a href="https://github.com/rdev/liquid-glass-react" target="_blank" rel="noopener noreferrer" className="text-white/70 hover:text-white transition-colors p-2 hover:bg-white/10 rounded-lg" title="View on GitHub"> 155 | <Github className="w-6 h-6" /> 156 | </a> 157 | </div> 158 | <p className="text-white/60 text-sm">Liquid Glass container effect for React. With settings and effects and stuff.</p> 159 | 160 | <p className="font-semibold text-yellow-300 text-xs mt-2 leading-snug">⚠️ This doesn't fully work in Safari and Firefox. You will not see edge refraction on non-chromium browsers.</p> 161 | </div> 162 | 163 | {/* Tab Switcher */} 164 | <div className="flex mb-6 bg-white/5 rounded-lg p-1"> 165 | <button 166 | onClick={() => setActiveTab("userInfo")} 167 | className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all ${activeTab === "userInfo" ? "bg-blue-500 text-white shadow-lg" : "text-white/70 hover:text-white hover:bg-white/10"}`} 168 | > 169 | User Info Card 170 | </button> 171 | <button 172 | onClick={() => setActiveTab("logOut")} 173 | className={`flex-1 px-4 py-2 text-sm font-medium rounded-md transition-all ${activeTab === "logOut" ? "bg-blue-500 text-white shadow-lg" : "text-white/70 hover:text-white hover:bg-white/10"}`} 174 | > 175 | Log Out Button 176 | </button> 177 | </div> 178 | 179 | <div className="space-y-8 flex-1"> 180 | {activeTab === "userInfo" && ( 181 | <> 182 | <div> 183 | <span className="block text-sm font-semibold text-white/90 mb-3">Refraction Mode</span> 184 | <div className="space-y-2"> 185 | <div className="flex items-center space-x-3"> 186 | <input 187 | type="radio" 188 | id="userInfoModeStandard" 189 | name="userInfoMode" 190 | value="standard" 191 | checked={userInfoMode === "standard"} 192 | onChange={(e) => setUserInfoMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 193 | className="w-4 h-4 accent-blue-500" 194 | /> 195 | <label htmlFor="userInfoModeStandard" className="text-sm text-white/90"> 196 | Standard 197 | </label> 198 | </div> 199 | <div className="flex items-center space-x-3"> 200 | <input 201 | type="radio" 202 | id="userInfoModePolar" 203 | name="userInfoMode" 204 | value="polar" 205 | checked={userInfoMode === "polar"} 206 | onChange={(e) => setUserInfoMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 207 | className="w-4 h-4 accent-blue-500" 208 | /> 209 | <label htmlFor="userInfoModePolar" className="text-sm text-white/90"> 210 | Polar 211 | </label> 212 | </div> 213 | <div className="flex items-center space-x-3"> 214 | <input 215 | type="radio" 216 | id="userInfoModeProminent" 217 | name="userInfoMode" 218 | value="prominent" 219 | checked={userInfoMode === "prominent"} 220 | onChange={(e) => setUserInfoMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 221 | className="w-4 h-4 accent-blue-500" 222 | /> 223 | <label htmlFor="userInfoModeProminent" className="text-sm text-white/90"> 224 | Prominent 225 | </label> 226 | </div> 227 | <div className="flex items-center space-x-3"> 228 | <input 229 | type="radio" 230 | id="userInfoModeShader" 231 | name="userInfoMode" 232 | value="shader" 233 | checked={userInfoMode === "shader"} 234 | onChange={(e) => setUserInfoMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 235 | className="w-4 h-4 accent-blue-500" 236 | /> 237 | <label htmlFor="userInfoModeShader" className="text-sm text-white/90"> 238 | Shader (Experimental) 239 | </label> 240 | </div> 241 | </div> 242 | <p className="text-xs text-white/50 mt-2">Controls the refraction calculation method</p> 243 | </div> 244 | 245 | <div> 246 | <span className="block text-sm font-semibold text-white/90 mb-3">Displacement Scale</span> 247 | <div className="mb-2"> 248 | <span className="text-xl font-mono text-blue-300">{displacementScale}</span> 249 | </div> 250 | <input type="range" min="0" max="200" step="1" value={displacementScale} onChange={(e) => setDisplacementScale(Number(e.target.value))} className="w-full" /> 251 | <p className="text-xs text-white/50 mt-2">Controls the intensity of edge distortion</p> 252 | </div> 253 | 254 | <div> 255 | <span className="block text-sm font-semibold text-white/90 mb-3">Blur Amount</span> 256 | <div className="mb-2"> 257 | <span className="text-xl font-mono text-green-300">{blurAmount.toFixed(1)}</span> 258 | </div> 259 | <input type="range" min="0" max="1" step="0.01" value={blurAmount} onChange={(e) => setBlurAmount(Number(e.target.value))} className="w-full" /> 260 | <p className="text-xs text-white/50 mt-2">Controls backdrop blur intensity</p> 261 | </div> 262 | 263 | <div> 264 | <span className="block text-sm font-semibold text-white/90 mb-3">Saturation</span> 265 | <div className="mb-2"> 266 | <span className="text-xl font-mono text-purple-300">{saturation}%</span> 267 | </div> 268 | <input type="range" min="100" max="300" step="10" value={saturation} onChange={(e) => setSaturation(Number(e.target.value))} className="w-full" /> 269 | <p className="text-xs text-white/50 mt-2">Controls color saturation of the backdrop</p> 270 | </div> 271 | 272 | <div> 273 | <span className="block text-sm font-semibold text-white/90 mb-3">Chromatic Aberration</span> 274 | <div className="mb-2"> 275 | <span className="text-xl font-mono text-cyan-300">{aberrationIntensity}</span> 276 | </div> 277 | <input type="range" min="0" max="20" step="1" value={aberrationIntensity} onChange={(e) => setAberrationIntensity(Number(e.target.value))} className="w-full" /> 278 | <p className="text-xs text-white/50 mt-2">Controls RGB channel separation intensity</p> 279 | </div> 280 | 281 | <div> 282 | <span className="block text-sm font-semibold text-white/90 mb-3">Elasticity</span> 283 | <div className="mb-2"> 284 | <span className="text-xl font-mono text-orange-300">{elasticity.toFixed(2)}</span> 285 | </div> 286 | <input type="range" min="0" max="1" step="0.05" value={elasticity} onChange={(e) => setElasticity(Number(e.target.value))} className="w-full" /> 287 | <p className="text-xs text-white/50 mt-2">Controls how much the glass reaches toward the cursor</p> 288 | </div> 289 | 290 | <div> 291 | <span className="block text-sm font-semibold text-white/90 mb-3">Corner Radius</span> 292 | <div className="mb-2"> 293 | <span className="text-xl font-mono text-pink-300">{cornerRadius === 999 ? "Full" : `${cornerRadius}px`}</span> 294 | </div> 295 | <input type="range" min="0" max="100" step="1" value={cornerRadius} onChange={(e) => setCornerRadius(Number(e.target.value))} className="w-full" /> 296 | <p className="text-xs text-white/50 mt-2">Controls the roundness of the glass corners</p> 297 | </div> 298 | 299 | <div> 300 | <span className="block text-sm font-semibold text-white/90 mb-3">Over Light</span> 301 | <div className="flex items-center space-x-3"> 302 | <input type="checkbox" id="userInfoOverLight" checked={userInfoOverLight} onChange={(e) => setUserInfoOverLight(e.target.checked)} className="w-5 h-5 accent-blue-500" /> 303 | <label htmlFor="userInfoOverLight" className="text-sm text-white/90"> 304 | Tint liquid glass dark (use for bright backgrounds) 305 | </label> 306 | </div> 307 | <p className="text-xs text-white/50 mt-2">Makes the glass darker for better visibility on light backgrounds</p> 308 | </div> 309 | </> 310 | )} 311 | 312 | {activeTab === "logOut" && ( 313 | <> 314 | <div> 315 | <span className="block text-sm font-semibold text-white/90 mb-3">Refraction Mode</span> 316 | <div className="space-y-2"> 317 | <div className="flex items-center space-x-3"> 318 | <input 319 | type="radio" 320 | id="logoutModeStandard" 321 | name="logoutMode" 322 | value="standard" 323 | checked={logoutMode === "standard"} 324 | onChange={(e) => setLogoutMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 325 | className="w-4 h-4 accent-blue-500" 326 | /> 327 | <label htmlFor="logoutModeStandard" className="text-sm text-white/90"> 328 | Standard 329 | </label> 330 | </div> 331 | <div className="flex items-center space-x-3"> 332 | <input 333 | type="radio" 334 | id="logoutModePolar" 335 | name="logoutMode" 336 | value="polar" 337 | checked={logoutMode === "polar"} 338 | onChange={(e) => setLogoutMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 339 | className="w-4 h-4 accent-blue-500" 340 | /> 341 | <label htmlFor="logoutModePolar" className="text-sm text-white/90"> 342 | Polar 343 | </label> 344 | </div> 345 | <div className="flex items-center space-x-3"> 346 | <input 347 | type="radio" 348 | id="logoutModeProminent" 349 | name="logoutMode" 350 | value="prominent" 351 | checked={logoutMode === "prominent"} 352 | onChange={(e) => setLogoutMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 353 | className="w-4 h-4 accent-blue-500" 354 | /> 355 | <label htmlFor="logoutModeProminent" className="text-sm text-white/90"> 356 | Prominent 357 | </label> 358 | </div> 359 | <div className="flex items-center space-x-3"> 360 | <input 361 | type="radio" 362 | id="logoutModeShader" 363 | name="logoutMode" 364 | value="shader" 365 | checked={logoutMode === "shader"} 366 | onChange={(e) => setLogoutMode(e.target.value as "standard" | "polar" | "prominent" | "shader")} 367 | className="w-4 h-4 accent-blue-500" 368 | /> 369 | <label htmlFor="logoutModeShader" className="text-sm text-white/90"> 370 | Shader 371 | </label> 372 | </div> 373 | </div> 374 | <p className="text-xs text-white/50 mt-2">Controls the refraction calculation method</p> 375 | </div> 376 | 377 | <div> 378 | <span className="block text-sm font-semibold text-white/90 mb-3">Displacement Scale</span> 379 | <div className="mb-2"> 380 | <span className="text-xl font-mono text-blue-300">{logoutDisplacementScale}</span> 381 | </div> 382 | <input type="range" min="0" max="200" step="1" value={logoutDisplacementScale} onChange={(e) => setLogoutDisplacementScale(Number(e.target.value))} className="w-full" /> 383 | <p className="text-xs text-white/50 mt-2">Controls the intensity of edge distortion</p> 384 | </div> 385 | 386 | <div> 387 | <span className="block text-sm font-semibold text-white/90 mb-3">Blur Amount</span> 388 | <div className="mb-2"> 389 | <span className="text-xl font-mono text-green-300">{logoutBlurAmount.toFixed(1)}</span> 390 | </div> 391 | <input type="range" min="0" max="1" step="0.01" value={logoutBlurAmount} onChange={(e) => setLogoutBlurAmount(Number(e.target.value))} className="w-full" /> 392 | <p className="text-xs text-white/50 mt-2">Controls backdrop blur intensity</p> 393 | </div> 394 | 395 | <div> 396 | <span className="block text-sm font-semibold text-white/90 mb-3">Saturation</span> 397 | <div className="mb-2"> 398 | <span className="text-xl font-mono text-purple-300">{logoutSaturation}%</span> 399 | </div> 400 | <input type="range" min="100" max="300" step="10" value={logoutSaturation} onChange={(e) => setLogoutSaturation(Number(e.target.value))} className="w-full" /> 401 | <p className="text-xs text-white/50 mt-2">Controls color saturation of the backdrop</p> 402 | </div> 403 | 404 | <div> 405 | <span className="block text-sm font-semibold text-white/90 mb-3">Chromatic Aberration</span> 406 | <div className="mb-2"> 407 | <span className="text-xl font-mono text-cyan-300">{logoutAberrationIntensity}</span> 408 | </div> 409 | <input type="range" min="0" max="20" step="1" value={logoutAberrationIntensity} onChange={(e) => setLogoutAberrationIntensity(Number(e.target.value))} className="w-full" /> 410 | <p className="text-xs text-white/50 mt-2">Controls RGB channel separation intensity</p> 411 | </div> 412 | 413 | <div> 414 | <span className="block text-sm font-semibold text-white/90 mb-3">Elasticity</span> 415 | <div className="mb-2"> 416 | <span className="text-xl font-mono text-orange-300">{logoutElasticity.toFixed(2)}</span> 417 | </div> 418 | <input type="range" min="0" max="1" step="0.05" value={logoutElasticity} onChange={(e) => setLogoutElasticity(Number(e.target.value))} className="w-full" /> 419 | <p className="text-xs text-white/50 mt-2">Controls how much the glass reaches toward the cursor</p> 420 | </div> 421 | 422 | <div> 423 | <span className="block text-sm font-semibold text-white/90 mb-3">Corner Radius</span> 424 | <div className="mb-2"> 425 | <span className="text-xl font-mono text-pink-300">{logoutCornerRadius === 999 ? "Full" : `${logoutCornerRadius}px`}</span> 426 | </div> 427 | <input type="range" min="0" max="100" step="1" value={logoutCornerRadius} onChange={(e) => setLogoutCornerRadius(Number(e.target.value))} className="w-full" /> 428 | <p className="text-xs text-white/50 mt-2">Controls the roundness of the glass corners</p> 429 | </div> 430 | 431 | <div> 432 | <span className="block text-sm font-semibold text-white/90 mb-3">Over Light</span> 433 | <div className="flex items-center space-x-3"> 434 | <input type="checkbox" id="logoutOverLight" checked={logoutOverLight} onChange={(e) => setLogoutOverLight(e.target.checked)} className="w-5 h-5 accent-blue-500" /> 435 | <label htmlFor="logoutOverLight" className="text-sm text-white/90"> 436 | Tint liquid glass dark (use for bright backgrounds) 437 | </label> 438 | </div> 439 | <p className="text-xs text-white/50 mt-2">Makes the glass darker for better visibility on light backgrounds</p> 440 | </div> 441 | </> 442 | )} 443 | </div> 444 | </div> 445 | </div> 446 | ) 447 | } 448 | -------------------------------------------------------------------------------- /liquid-glass-example/src/styles/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | 28 | /* Custom range slider styling */ 29 | input[type="range"] { 30 | -webkit-appearance: none; 31 | appearance: none; 32 | background: #00000033; 33 | border-radius: 999px; 34 | cursor: pointer; 35 | height: 24px; 36 | } 37 | 38 | input[type="range"]::-webkit-slider-track { 39 | background: linear-gradient(90deg, rgba(59, 130, 246, 0.3), rgba(168, 85, 247, 0.3)); 40 | height: 6px; 41 | border-radius: 3px; 42 | border: 1px solid rgba(255, 255, 255, 0.1); 43 | } 44 | 45 | input[type="range"]::-webkit-slider-thumb { 46 | -webkit-appearance: none; 47 | appearance: none; 48 | background: linear-gradient(135deg, #60a5fa, #a78bfa); 49 | height: 24px; 50 | width: 24px; 51 | border-radius: 50%; 52 | border: 2px solid rgba(255, 255, 255, 0.8); 53 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 0 rgba(96, 165, 250, 0.4); 54 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 55 | cursor: pointer; 56 | } 57 | 58 | input[type="range"]::-webkit-slider-thumb:hover { 59 | background: linear-gradient(135deg, #3b82f6, #8b5cf6); 60 | transform: scale(1.15); 61 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5), 0 0 0 8px rgba(96, 165, 250, 0.2); 62 | border-color: white; 63 | } 64 | 65 | input[type="range"]::-webkit-slider-thumb:active { 66 | transform: scale(1.05); 67 | box-shadow: 0 2px 8px rgba(0, 0, 0, 0.6), 0 0 0 12px rgba(96, 165, 250, 0.3); 68 | } 69 | 70 | input[type="range"]::-moz-range-track { 71 | background: linear-gradient(90deg, rgba(59, 130, 246, 0.3), rgba(168, 85, 247, 0.3)); 72 | height: 6px; 73 | border-radius: 3px; 74 | border: 1px solid rgba(255, 255, 255, 0.1); 75 | } 76 | 77 | input[type="range"]::-moz-range-thumb { 78 | background: linear-gradient(135deg, #60a5fa, #a78bfa); 79 | height: 24px; 80 | width: 24px; 81 | border-radius: 50%; 82 | border: 2px solid rgba(255, 255, 255, 0.8); 83 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); 84 | cursor: pointer; 85 | transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1); 86 | } 87 | 88 | input[type="range"]::-moz-range-thumb:hover { 89 | background: linear-gradient(135deg, #3b82f6, #8b5cf6); 90 | transform: scale(1.15); 91 | box-shadow: 0 6px 16px rgba(0, 0, 0, 0.5); 92 | border-color: white; 93 | } 94 | 95 | input[type="range"]:focus { 96 | outline: none; 97 | } 98 | 99 | input[type="range"]:focus::-webkit-slider-thumb { 100 | box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4), 0 0 0 4px rgba(96, 165, 250, 0.3); 101 | } 102 | -------------------------------------------------------------------------------- /liquid-glass-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "preserve", 15 | "incremental": true, 16 | "paths": { 17 | "@/*": ["./src/*"] 18 | } 19 | }, 20 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liquid-glass-react", 3 | "version": "1.0.0", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "liquid-glass-react", 9 | "version": "1.0.0", 10 | "license": "MIT", 11 | "devDependencies": { 12 | "@types/react": "^18.2.0", 13 | "@types/react-dom": "^18.2.0", 14 | "esbuild": "^0.19.0", 15 | "react": "^18.2.0", 16 | "react-dom": "^18.2.0", 17 | "typescript": "^5.0.0" 18 | }, 19 | "peerDependencies": { 20 | "react": ">=16.8.0", 21 | "react-dom": ">=16.8.0" 22 | } 23 | }, 24 | "node_modules/@esbuild/aix-ppc64": { 25 | "version": "0.19.12", 26 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", 27 | "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", 28 | "cpu": [ 29 | "ppc64" 30 | ], 31 | "dev": true, 32 | "optional": true, 33 | "os": [ 34 | "aix" 35 | ], 36 | "engines": { 37 | "node": ">=12" 38 | } 39 | }, 40 | "node_modules/@esbuild/android-arm": { 41 | "version": "0.19.12", 42 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", 43 | "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", 44 | "cpu": [ 45 | "arm" 46 | ], 47 | "dev": true, 48 | "optional": true, 49 | "os": [ 50 | "android" 51 | ], 52 | "engines": { 53 | "node": ">=12" 54 | } 55 | }, 56 | "node_modules/@esbuild/android-arm64": { 57 | "version": "0.19.12", 58 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", 59 | "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", 60 | "cpu": [ 61 | "arm64" 62 | ], 63 | "dev": true, 64 | "optional": true, 65 | "os": [ 66 | "android" 67 | ], 68 | "engines": { 69 | "node": ">=12" 70 | } 71 | }, 72 | "node_modules/@esbuild/android-x64": { 73 | "version": "0.19.12", 74 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", 75 | "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", 76 | "cpu": [ 77 | "x64" 78 | ], 79 | "dev": true, 80 | "optional": true, 81 | "os": [ 82 | "android" 83 | ], 84 | "engines": { 85 | "node": ">=12" 86 | } 87 | }, 88 | "node_modules/@esbuild/darwin-arm64": { 89 | "version": "0.19.12", 90 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", 91 | "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", 92 | "cpu": [ 93 | "arm64" 94 | ], 95 | "dev": true, 96 | "optional": true, 97 | "os": [ 98 | "darwin" 99 | ], 100 | "engines": { 101 | "node": ">=12" 102 | } 103 | }, 104 | "node_modules/@esbuild/darwin-x64": { 105 | "version": "0.19.12", 106 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", 107 | "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", 108 | "cpu": [ 109 | "x64" 110 | ], 111 | "dev": true, 112 | "optional": true, 113 | "os": [ 114 | "darwin" 115 | ], 116 | "engines": { 117 | "node": ">=12" 118 | } 119 | }, 120 | "node_modules/@esbuild/freebsd-arm64": { 121 | "version": "0.19.12", 122 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", 123 | "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", 124 | "cpu": [ 125 | "arm64" 126 | ], 127 | "dev": true, 128 | "optional": true, 129 | "os": [ 130 | "freebsd" 131 | ], 132 | "engines": { 133 | "node": ">=12" 134 | } 135 | }, 136 | "node_modules/@esbuild/freebsd-x64": { 137 | "version": "0.19.12", 138 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", 139 | "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", 140 | "cpu": [ 141 | "x64" 142 | ], 143 | "dev": true, 144 | "optional": true, 145 | "os": [ 146 | "freebsd" 147 | ], 148 | "engines": { 149 | "node": ">=12" 150 | } 151 | }, 152 | "node_modules/@esbuild/linux-arm": { 153 | "version": "0.19.12", 154 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", 155 | "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", 156 | "cpu": [ 157 | "arm" 158 | ], 159 | "dev": true, 160 | "optional": true, 161 | "os": [ 162 | "linux" 163 | ], 164 | "engines": { 165 | "node": ">=12" 166 | } 167 | }, 168 | "node_modules/@esbuild/linux-arm64": { 169 | "version": "0.19.12", 170 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", 171 | "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", 172 | "cpu": [ 173 | "arm64" 174 | ], 175 | "dev": true, 176 | "optional": true, 177 | "os": [ 178 | "linux" 179 | ], 180 | "engines": { 181 | "node": ">=12" 182 | } 183 | }, 184 | "node_modules/@esbuild/linux-ia32": { 185 | "version": "0.19.12", 186 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", 187 | "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", 188 | "cpu": [ 189 | "ia32" 190 | ], 191 | "dev": true, 192 | "optional": true, 193 | "os": [ 194 | "linux" 195 | ], 196 | "engines": { 197 | "node": ">=12" 198 | } 199 | }, 200 | "node_modules/@esbuild/linux-loong64": { 201 | "version": "0.19.12", 202 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", 203 | "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", 204 | "cpu": [ 205 | "loong64" 206 | ], 207 | "dev": true, 208 | "optional": true, 209 | "os": [ 210 | "linux" 211 | ], 212 | "engines": { 213 | "node": ">=12" 214 | } 215 | }, 216 | "node_modules/@esbuild/linux-mips64el": { 217 | "version": "0.19.12", 218 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", 219 | "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", 220 | "cpu": [ 221 | "mips64el" 222 | ], 223 | "dev": true, 224 | "optional": true, 225 | "os": [ 226 | "linux" 227 | ], 228 | "engines": { 229 | "node": ">=12" 230 | } 231 | }, 232 | "node_modules/@esbuild/linux-ppc64": { 233 | "version": "0.19.12", 234 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", 235 | "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", 236 | "cpu": [ 237 | "ppc64" 238 | ], 239 | "dev": true, 240 | "optional": true, 241 | "os": [ 242 | "linux" 243 | ], 244 | "engines": { 245 | "node": ">=12" 246 | } 247 | }, 248 | "node_modules/@esbuild/linux-riscv64": { 249 | "version": "0.19.12", 250 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", 251 | "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", 252 | "cpu": [ 253 | "riscv64" 254 | ], 255 | "dev": true, 256 | "optional": true, 257 | "os": [ 258 | "linux" 259 | ], 260 | "engines": { 261 | "node": ">=12" 262 | } 263 | }, 264 | "node_modules/@esbuild/linux-s390x": { 265 | "version": "0.19.12", 266 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", 267 | "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", 268 | "cpu": [ 269 | "s390x" 270 | ], 271 | "dev": true, 272 | "optional": true, 273 | "os": [ 274 | "linux" 275 | ], 276 | "engines": { 277 | "node": ">=12" 278 | } 279 | }, 280 | "node_modules/@esbuild/linux-x64": { 281 | "version": "0.19.12", 282 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", 283 | "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", 284 | "cpu": [ 285 | "x64" 286 | ], 287 | "dev": true, 288 | "optional": true, 289 | "os": [ 290 | "linux" 291 | ], 292 | "engines": { 293 | "node": ">=12" 294 | } 295 | }, 296 | "node_modules/@esbuild/netbsd-x64": { 297 | "version": "0.19.12", 298 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", 299 | "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", 300 | "cpu": [ 301 | "x64" 302 | ], 303 | "dev": true, 304 | "optional": true, 305 | "os": [ 306 | "netbsd" 307 | ], 308 | "engines": { 309 | "node": ">=12" 310 | } 311 | }, 312 | "node_modules/@esbuild/openbsd-x64": { 313 | "version": "0.19.12", 314 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", 315 | "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", 316 | "cpu": [ 317 | "x64" 318 | ], 319 | "dev": true, 320 | "optional": true, 321 | "os": [ 322 | "openbsd" 323 | ], 324 | "engines": { 325 | "node": ">=12" 326 | } 327 | }, 328 | "node_modules/@esbuild/sunos-x64": { 329 | "version": "0.19.12", 330 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", 331 | "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", 332 | "cpu": [ 333 | "x64" 334 | ], 335 | "dev": true, 336 | "optional": true, 337 | "os": [ 338 | "sunos" 339 | ], 340 | "engines": { 341 | "node": ">=12" 342 | } 343 | }, 344 | "node_modules/@esbuild/win32-arm64": { 345 | "version": "0.19.12", 346 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", 347 | "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", 348 | "cpu": [ 349 | "arm64" 350 | ], 351 | "dev": true, 352 | "optional": true, 353 | "os": [ 354 | "win32" 355 | ], 356 | "engines": { 357 | "node": ">=12" 358 | } 359 | }, 360 | "node_modules/@esbuild/win32-ia32": { 361 | "version": "0.19.12", 362 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", 363 | "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", 364 | "cpu": [ 365 | "ia32" 366 | ], 367 | "dev": true, 368 | "optional": true, 369 | "os": [ 370 | "win32" 371 | ], 372 | "engines": { 373 | "node": ">=12" 374 | } 375 | }, 376 | "node_modules/@esbuild/win32-x64": { 377 | "version": "0.19.12", 378 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", 379 | "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", 380 | "cpu": [ 381 | "x64" 382 | ], 383 | "dev": true, 384 | "optional": true, 385 | "os": [ 386 | "win32" 387 | ], 388 | "engines": { 389 | "node": ">=12" 390 | } 391 | }, 392 | "node_modules/@types/prop-types": { 393 | "version": "15.7.15", 394 | "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", 395 | "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", 396 | "dev": true 397 | }, 398 | "node_modules/@types/react": { 399 | "version": "18.3.23", 400 | "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.23.tgz", 401 | "integrity": "sha512-/LDXMQh55EzZQ0uVAZmKKhfENivEvWz6E+EYzh+/MCjMhNsotd+ZHhBGIjFDTi6+fz0OhQQQLbTgdQIxxCsC0w==", 402 | "dev": true, 403 | "dependencies": { 404 | "@types/prop-types": "*", 405 | "csstype": "^3.0.2" 406 | } 407 | }, 408 | "node_modules/@types/react-dom": { 409 | "version": "18.3.7", 410 | "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", 411 | "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", 412 | "dev": true, 413 | "peerDependencies": { 414 | "@types/react": "^18.0.0" 415 | } 416 | }, 417 | "node_modules/csstype": { 418 | "version": "3.1.3", 419 | "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", 420 | "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", 421 | "dev": true 422 | }, 423 | "node_modules/esbuild": { 424 | "version": "0.19.12", 425 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", 426 | "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", 427 | "dev": true, 428 | "hasInstallScript": true, 429 | "bin": { 430 | "esbuild": "bin/esbuild" 431 | }, 432 | "engines": { 433 | "node": ">=12" 434 | }, 435 | "optionalDependencies": { 436 | "@esbuild/aix-ppc64": "0.19.12", 437 | "@esbuild/android-arm": "0.19.12", 438 | "@esbuild/android-arm64": "0.19.12", 439 | "@esbuild/android-x64": "0.19.12", 440 | "@esbuild/darwin-arm64": "0.19.12", 441 | "@esbuild/darwin-x64": "0.19.12", 442 | "@esbuild/freebsd-arm64": "0.19.12", 443 | "@esbuild/freebsd-x64": "0.19.12", 444 | "@esbuild/linux-arm": "0.19.12", 445 | "@esbuild/linux-arm64": "0.19.12", 446 | "@esbuild/linux-ia32": "0.19.12", 447 | "@esbuild/linux-loong64": "0.19.12", 448 | "@esbuild/linux-mips64el": "0.19.12", 449 | "@esbuild/linux-ppc64": "0.19.12", 450 | "@esbuild/linux-riscv64": "0.19.12", 451 | "@esbuild/linux-s390x": "0.19.12", 452 | "@esbuild/linux-x64": "0.19.12", 453 | "@esbuild/netbsd-x64": "0.19.12", 454 | "@esbuild/openbsd-x64": "0.19.12", 455 | "@esbuild/sunos-x64": "0.19.12", 456 | "@esbuild/win32-arm64": "0.19.12", 457 | "@esbuild/win32-ia32": "0.19.12", 458 | "@esbuild/win32-x64": "0.19.12" 459 | } 460 | }, 461 | "node_modules/js-tokens": { 462 | "version": "4.0.0", 463 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", 464 | "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", 465 | "dev": true 466 | }, 467 | "node_modules/loose-envify": { 468 | "version": "1.4.0", 469 | "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", 470 | "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", 471 | "dev": true, 472 | "dependencies": { 473 | "js-tokens": "^3.0.0 || ^4.0.0" 474 | }, 475 | "bin": { 476 | "loose-envify": "cli.js" 477 | } 478 | }, 479 | "node_modules/react": { 480 | "version": "18.3.1", 481 | "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", 482 | "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", 483 | "dev": true, 484 | "dependencies": { 485 | "loose-envify": "^1.1.0" 486 | }, 487 | "engines": { 488 | "node": ">=0.10.0" 489 | } 490 | }, 491 | "node_modules/react-dom": { 492 | "version": "18.3.1", 493 | "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", 494 | "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", 495 | "dev": true, 496 | "dependencies": { 497 | "loose-envify": "^1.1.0", 498 | "scheduler": "^0.23.2" 499 | }, 500 | "peerDependencies": { 501 | "react": "^18.3.1" 502 | } 503 | }, 504 | "node_modules/scheduler": { 505 | "version": "0.23.2", 506 | "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", 507 | "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", 508 | "dev": true, 509 | "dependencies": { 510 | "loose-envify": "^1.1.0" 511 | } 512 | }, 513 | "node_modules/typescript": { 514 | "version": "5.8.3", 515 | "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", 516 | "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", 517 | "dev": true, 518 | "bin": { 519 | "tsc": "bin/tsc", 520 | "tsserver": "bin/tsserver" 521 | }, 522 | "engines": { 523 | "node": ">=14.17" 524 | } 525 | } 526 | } 527 | } 528 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "liquid-glass-react", 3 | "version": "1.1.1", 4 | "description": "Apple's Liquid Glass effect for React", 5 | "main": "dist/index.js", 6 | "module": "dist/index.esm.js", 7 | "types": "dist/index.d.ts", 8 | "files": ["dist"], 9 | "workspaces": ["liquid-glass"], 10 | "scripts": { 11 | "build": "npm run clean && npm run build:esm && npm run build:cjs && npm run build:types", 12 | "build:esm": "esbuild src/index.tsx --bundle --format=esm --outfile=dist/index.esm.js --external:react --external:react-dom", 13 | "build:cjs": "esbuild src/index.tsx --bundle --format=cjs --outfile=dist/index.js --external:react --external:react-dom", 14 | "build:types": "tsc --emitDeclarationOnly --outDir dist", 15 | "clean": "rm -rf dist", 16 | "dev": "npm run build:esm -- --watch", 17 | "prepublishOnly": "npm run build" 18 | }, 19 | "peerDependencies": { 20 | "react": ">=18", 21 | "react-dom": ">=18" 22 | }, 23 | "devDependencies": { 24 | "@biomejs/biome": "^1.9.4", 25 | "@types/react": "^18.2.0", 26 | "@types/react-dom": "^18.2.0", 27 | "esbuild": "^0.19.0", 28 | "react": "^18.2.0", 29 | "react-dom": "^18.2.0", 30 | "typescript": "^5.0.0" 31 | }, 32 | "keywords": ["react", "component", "library", "typescript"], 33 | "author": "", 34 | "license": "MIT", 35 | "repository": { 36 | "type": "git", 37 | "url": "" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/index.tsx: -------------------------------------------------------------------------------- 1 | import { type CSSProperties, forwardRef, useCallback, useEffect, useId, useRef, useState } from "react" 2 | import { ShaderDisplacementGenerator, fragmentShaders } from "./shader-utils" 3 | import { displacementMap, polarDisplacementMap, prominentDisplacementMap } from "./utils" 4 | 5 | // Generate shader-based displacement map using shaderUtils 6 | const generateShaderDisplacementMap = (width: number, height: number): string => { 7 | const generator = new ShaderDisplacementGenerator({ 8 | width, 9 | height, 10 | fragment: fragmentShaders.liquidGlass, 11 | }) 12 | 13 | const dataUrl = generator.updateShader() 14 | generator.destroy() 15 | 16 | return dataUrl 17 | } 18 | 19 | const getMap = (mode: "standard" | "polar" | "prominent" | "shader", shaderMapUrl?: string) => { 20 | switch (mode) { 21 | case "standard": 22 | return displacementMap 23 | case "polar": 24 | return polarDisplacementMap 25 | case "prominent": 26 | return prominentDisplacementMap 27 | case "shader": 28 | return shaderMapUrl || displacementMap 29 | default: 30 | throw new Error(`Invalid mode: ${mode}`) 31 | } 32 | } 33 | 34 | /* ---------- SVG filter (edge-only displacement) ---------- */ 35 | const GlassFilter: React.FC<{ id: string; displacementScale: number; aberrationIntensity: number; width: number; height: number; mode: "standard" | "polar" | "prominent" | "shader"; shaderMapUrl?: string }> = ({ 36 | id, 37 | displacementScale, 38 | aberrationIntensity, 39 | width, 40 | height, 41 | mode, 42 | shaderMapUrl, 43 | }) => ( 44 | <svg style={{ position: "absolute", width, height }} aria-hidden="true"> 45 | <defs> 46 | <radialGradient id={`${id}-edge-mask`} cx="50%" cy="50%" r="50%"> 47 | <stop offset="0%" stopColor="black" stopOpacity="0" /> 48 | <stop offset={`${Math.max(30, 80 - aberrationIntensity * 2)}%`} stopColor="black" stopOpacity="0" /> 49 | <stop offset="100%" stopColor="white" stopOpacity="1" /> 50 | </radialGradient> 51 | <filter id={id} x="-35%" y="-35%" width="170%" height="170%" colorInterpolationFilters="sRGB"> 52 | <feImage id="feimage" x="0" y="0" width="100%" height="100%" result="DISPLACEMENT_MAP" href={getMap(mode, shaderMapUrl)} preserveAspectRatio="xMidYMid slice" /> 53 | 54 | {/* Create edge mask using the displacement map itself */} 55 | <feColorMatrix 56 | in="DISPLACEMENT_MAP" 57 | type="matrix" 58 | values="0.3 0.3 0.3 0 0 59 | 0.3 0.3 0.3 0 0 60 | 0.3 0.3 0.3 0 0 61 | 0 0 0 1 0" 62 | result="EDGE_INTENSITY" 63 | /> 64 | <feComponentTransfer in="EDGE_INTENSITY" result="EDGE_MASK"> 65 | <feFuncA type="discrete" tableValues={`0 ${aberrationIntensity * 0.05} 1`} /> 66 | </feComponentTransfer> 67 | 68 | {/* Original undisplaced image for center */} 69 | <feOffset in="SourceGraphic" dx="0" dy="0" result="CENTER_ORIGINAL" /> 70 | 71 | {/* Red channel displacement with slight offset */} 72 | <feDisplacementMap in="SourceGraphic" in2="DISPLACEMENT_MAP" scale={displacementScale * (mode === "shader" ? 1 : -1)} xChannelSelector="R" yChannelSelector="B" result="RED_DISPLACED" /> 73 | <feColorMatrix 74 | in="RED_DISPLACED" 75 | type="matrix" 76 | values="1 0 0 0 0 77 | 0 0 0 0 0 78 | 0 0 0 0 0 79 | 0 0 0 1 0" 80 | result="RED_CHANNEL" 81 | /> 82 | 83 | {/* Green channel displacement */} 84 | <feDisplacementMap in="SourceGraphic" in2="DISPLACEMENT_MAP" scale={displacementScale * ((mode === "shader" ? 1 : -1) - aberrationIntensity * 0.05)} xChannelSelector="R" yChannelSelector="B" result="GREEN_DISPLACED" /> 85 | <feColorMatrix 86 | in="GREEN_DISPLACED" 87 | type="matrix" 88 | values="0 0 0 0 0 89 | 0 1 0 0 0 90 | 0 0 0 0 0 91 | 0 0 0 1 0" 92 | result="GREEN_CHANNEL" 93 | /> 94 | 95 | {/* Blue channel displacement with slight offset */} 96 | <feDisplacementMap in="SourceGraphic" in2="DISPLACEMENT_MAP" scale={displacementScale * ((mode === "shader" ? 1 : -1) - aberrationIntensity * 0.1)} xChannelSelector="R" yChannelSelector="B" result="BLUE_DISPLACED" /> 97 | <feColorMatrix 98 | in="BLUE_DISPLACED" 99 | type="matrix" 100 | values="0 0 0 0 0 101 | 0 0 0 0 0 102 | 0 0 1 0 0 103 | 0 0 0 1 0" 104 | result="BLUE_CHANNEL" 105 | /> 106 | 107 | {/* Combine all channels with screen blend mode for chromatic aberration */} 108 | <feBlend in="GREEN_CHANNEL" in2="BLUE_CHANNEL" mode="screen" result="GB_COMBINED" /> 109 | <feBlend in="RED_CHANNEL" in2="GB_COMBINED" mode="screen" result="RGB_COMBINED" /> 110 | 111 | {/* Add slight blur to soften the aberration effect */} 112 | <feGaussianBlur in="RGB_COMBINED" stdDeviation={Math.max(0.1, 0.5 - aberrationIntensity * 0.1)} result="ABERRATED_BLURRED" /> 113 | 114 | {/* Apply edge mask to aberration effect */} 115 | <feComposite in="ABERRATED_BLURRED" in2="EDGE_MASK" operator="in" result="EDGE_ABERRATION" /> 116 | 117 | {/* Create inverted mask for center */} 118 | <feComponentTransfer in="EDGE_MASK" result="INVERTED_MASK"> 119 | <feFuncA type="table" tableValues="1 0" /> 120 | </feComponentTransfer> 121 | <feComposite in="CENTER_ORIGINAL" in2="INVERTED_MASK" operator="in" result="CENTER_CLEAN" /> 122 | 123 | {/* Combine edge aberration with clean center */} 124 | <feComposite in="EDGE_ABERRATION" in2="CENTER_CLEAN" operator="over" /> 125 | </filter> 126 | </defs> 127 | </svg> 128 | ) 129 | 130 | /* ---------- container ---------- */ 131 | const GlassContainer = forwardRef< 132 | HTMLDivElement, 133 | React.PropsWithChildren<{ 134 | className?: string 135 | style?: React.CSSProperties 136 | displacementScale?: number 137 | blurAmount?: number 138 | saturation?: number 139 | aberrationIntensity?: number 140 | mouseOffset?: { x: number; y: number } 141 | onMouseLeave?: () => void 142 | onMouseEnter?: () => void 143 | onMouseDown?: () => void 144 | onMouseUp?: () => void 145 | active?: boolean 146 | overLight?: boolean 147 | cornerRadius?: number 148 | padding?: string 149 | glassSize?: { width: number; height: number } 150 | onClick?: () => void 151 | mode?: "standard" | "polar" | "prominent" | "shader" 152 | }> 153 | >( 154 | ( 155 | { 156 | children, 157 | className = "", 158 | style, 159 | displacementScale = 25, 160 | blurAmount = 12, 161 | saturation = 180, 162 | aberrationIntensity = 2, 163 | onMouseEnter, 164 | onMouseLeave, 165 | onMouseDown, 166 | onMouseUp, 167 | active = false, 168 | overLight = false, 169 | cornerRadius = 999, 170 | padding = "24px 32px", 171 | glassSize = { width: 270, height: 69 }, 172 | onClick, 173 | mode = "standard", 174 | }, 175 | ref, 176 | ) => { 177 | const filterId = useId() 178 | const [shaderMapUrl, setShaderMapUrl] = useState<string>("") 179 | 180 | const isFirefox = navigator.userAgent.toLowerCase().includes("firefox") 181 | 182 | // Generate shader displacement map when in shader mode 183 | useEffect(() => { 184 | if (mode === "shader") { 185 | const url = generateShaderDisplacementMap(glassSize.width, glassSize.height) 186 | setShaderMapUrl(url) 187 | } 188 | }, [mode, glassSize.width, glassSize.height]) 189 | 190 | const backdropStyle = { 191 | filter: isFirefox ? null : `url(#${filterId})`, 192 | backdropFilter: `blur(${(overLight ? 12 : 4) + blurAmount * 32}px) saturate(${saturation}%)`, 193 | } 194 | 195 | return ( 196 | <div ref={ref} className={`relative ${className} ${active ? "active" : ""} ${Boolean(onClick) ? "cursor-pointer" : ""}`} style={style} onClick={onClick}> 197 | <GlassFilter mode={mode} id={filterId} displacementScale={displacementScale} aberrationIntensity={aberrationIntensity} width={glassSize.width} height={glassSize.height} shaderMapUrl={shaderMapUrl} /> 198 | 199 | <div 200 | className="glass" 201 | style={{ 202 | borderRadius: `${cornerRadius}px`, 203 | position: "relative", 204 | display: "inline-flex", 205 | alignItems: "center", 206 | gap: "24px", 207 | padding, 208 | overflow: "hidden", 209 | transition: "all 0.2s ease-in-out", 210 | boxShadow: overLight ? "0px 16px 70px rgba(0, 0, 0, 0.75)" : "0px 12px 40px rgba(0, 0, 0, 0.25)", 211 | }} 212 | onMouseEnter={onMouseEnter} 213 | onMouseLeave={onMouseLeave} 214 | onMouseDown={onMouseDown} 215 | onMouseUp={onMouseUp} 216 | > 217 | {/* backdrop layer that gets wiggly */} 218 | <span 219 | className="glass__warp" 220 | style={ 221 | { 222 | ...backdropStyle, 223 | position: "absolute", 224 | inset: "0", 225 | } as CSSProperties 226 | } 227 | /> 228 | 229 | {/* user content stays sharp */} 230 | <div 231 | className="transition-all duration-150 ease-in-out text-white" 232 | style={{ 233 | position: "relative", 234 | zIndex: 1, 235 | font: "500 20px/1 system-ui", 236 | textShadow: overLight ? "0px 2px 12px rgba(0, 0, 0, 0)" : "0px 2px 12px rgba(0, 0, 0, 0.4)", 237 | }} 238 | > 239 | {children} 240 | </div> 241 | </div> 242 | </div> 243 | ) 244 | }, 245 | ) 246 | 247 | GlassContainer.displayName = "GlassContainer" 248 | 249 | interface LiquidGlassProps { 250 | children: React.ReactNode 251 | displacementScale?: number 252 | blurAmount?: number 253 | saturation?: number 254 | aberrationIntensity?: number 255 | elasticity?: number 256 | cornerRadius?: number 257 | globalMousePos?: { x: number; y: number } 258 | mouseOffset?: { x: number; y: number } 259 | mouseContainer?: React.RefObject<HTMLElement | null> | null 260 | className?: string 261 | padding?: string 262 | style?: React.CSSProperties 263 | overLight?: boolean 264 | mode?: "standard" | "polar" | "prominent" | "shader" 265 | onClick?: () => void 266 | } 267 | 268 | export default function LiquidGlass({ 269 | children, 270 | displacementScale = 70, 271 | blurAmount = 0.0625, 272 | saturation = 140, 273 | aberrationIntensity = 2, 274 | elasticity = 0.15, 275 | cornerRadius = 999, 276 | globalMousePos: externalGlobalMousePos, 277 | mouseOffset: externalMouseOffset, 278 | mouseContainer = null, 279 | className = "", 280 | padding = "24px 32px", 281 | overLight = false, 282 | style = {}, 283 | mode = "standard", 284 | onClick, 285 | }: LiquidGlassProps) { 286 | const glassRef = useRef<HTMLDivElement>(null) 287 | const [isHovered, setIsHovered] = useState(false) 288 | const [isActive, setIsActive] = useState(false) 289 | const [glassSize, setGlassSize] = useState({ width: 270, height: 69 }) 290 | const [internalGlobalMousePos, setInternalGlobalMousePos] = useState({ x: 0, y: 0 }) 291 | const [internalMouseOffset, setInternalMouseOffset] = useState({ x: 0, y: 0 }) 292 | 293 | // Use external mouse position if provided, otherwise use internal 294 | const globalMousePos = externalGlobalMousePos || internalGlobalMousePos 295 | const mouseOffset = externalMouseOffset || internalMouseOffset 296 | 297 | // Internal mouse tracking 298 | const handleMouseMove = useCallback( 299 | (e: MouseEvent) => { 300 | const container = mouseContainer?.current || glassRef.current 301 | if (!container) { 302 | return 303 | } 304 | 305 | const rect = container.getBoundingClientRect() 306 | const centerX = rect.left + rect.width / 2 307 | const centerY = rect.top + rect.height / 2 308 | 309 | setInternalMouseOffset({ 310 | x: ((e.clientX - centerX) / rect.width) * 100, 311 | y: ((e.clientY - centerY) / rect.height) * 100, 312 | }) 313 | 314 | setInternalGlobalMousePos({ 315 | x: e.clientX, 316 | y: e.clientY, 317 | }) 318 | }, 319 | [mouseContainer], 320 | ) 321 | 322 | // Set up mouse tracking if no external mouse position is provided 323 | useEffect(() => { 324 | if (externalGlobalMousePos && externalMouseOffset) { 325 | // External mouse tracking is provided, don't set up internal tracking 326 | return 327 | } 328 | 329 | const container = mouseContainer?.current || glassRef.current 330 | if (!container) { 331 | return 332 | } 333 | 334 | container.addEventListener("mousemove", handleMouseMove) 335 | 336 | return () => { 337 | container.removeEventListener("mousemove", handleMouseMove) 338 | } 339 | }, [handleMouseMove, mouseContainer, externalGlobalMousePos, externalMouseOffset]) 340 | 341 | // Calculate directional scaling based on mouse position 342 | const calculateDirectionalScale = useCallback(() => { 343 | if (!globalMousePos.x || !globalMousePos.y || !glassRef.current) { 344 | return "scale(1)" 345 | } 346 | 347 | const rect = glassRef.current.getBoundingClientRect() 348 | const pillCenterX = rect.left + rect.width / 2 349 | const pillCenterY = rect.top + rect.height / 2 350 | const pillWidth = glassSize.width 351 | const pillHeight = glassSize.height 352 | 353 | const deltaX = globalMousePos.x - pillCenterX 354 | const deltaY = globalMousePos.y - pillCenterY 355 | 356 | // Calculate distance from mouse to pill edges (not center) 357 | const edgeDistanceX = Math.max(0, Math.abs(deltaX) - pillWidth / 2) 358 | const edgeDistanceY = Math.max(0, Math.abs(deltaY) - pillHeight / 2) 359 | const edgeDistance = Math.sqrt(edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY) 360 | 361 | // Activation zone: 200px from edges 362 | const activationZone = 200 363 | 364 | // If outside activation zone, no effect 365 | if (edgeDistance > activationZone) { 366 | return "scale(1)" 367 | } 368 | 369 | // Calculate fade-in factor (1 at edge, 0 at activation zone boundary) 370 | const fadeInFactor = 1 - edgeDistance / activationZone 371 | 372 | // Normalize the deltas for direction 373 | const centerDistance = Math.sqrt(deltaX * deltaX + deltaY * deltaY) 374 | if (centerDistance === 0) { 375 | return "scale(1)" 376 | } 377 | 378 | const normalizedX = deltaX / centerDistance 379 | const normalizedY = deltaY / centerDistance 380 | 381 | // Calculate stretch factors with fade-in 382 | const stretchIntensity = Math.min(centerDistance / 300, 1) * elasticity * fadeInFactor 383 | 384 | // X-axis scaling: stretch horizontally when moving left/right, compress when moving up/down 385 | const scaleX = 1 + Math.abs(normalizedX) * stretchIntensity * 0.3 - Math.abs(normalizedY) * stretchIntensity * 0.15 386 | 387 | // Y-axis scaling: stretch vertically when moving up/down, compress when moving left/right 388 | const scaleY = 1 + Math.abs(normalizedY) * stretchIntensity * 0.3 - Math.abs(normalizedX) * stretchIntensity * 0.15 389 | 390 | return `scaleX(${Math.max(0.8, scaleX)}) scaleY(${Math.max(0.8, scaleY)})` 391 | }, [globalMousePos, elasticity, glassSize]) 392 | 393 | // Helper function to calculate fade-in factor based on distance from element edges 394 | const calculateFadeInFactor = useCallback(() => { 395 | if (!globalMousePos.x || !globalMousePos.y || !glassRef.current) { 396 | return 0 397 | } 398 | 399 | const rect = glassRef.current.getBoundingClientRect() 400 | const pillCenterX = rect.left + rect.width / 2 401 | const pillCenterY = rect.top + rect.height / 2 402 | const pillWidth = glassSize.width 403 | const pillHeight = glassSize.height 404 | 405 | const edgeDistanceX = Math.max(0, Math.abs(globalMousePos.x - pillCenterX) - pillWidth / 2) 406 | const edgeDistanceY = Math.max(0, Math.abs(globalMousePos.y - pillCenterY) - pillHeight / 2) 407 | const edgeDistance = Math.sqrt(edgeDistanceX * edgeDistanceX + edgeDistanceY * edgeDistanceY) 408 | 409 | const activationZone = 200 410 | return edgeDistance > activationZone ? 0 : 1 - edgeDistance / activationZone 411 | }, [globalMousePos, glassSize]) 412 | 413 | // Helper function to calculate elastic translation 414 | const calculateElasticTranslation = useCallback(() => { 415 | if (!glassRef.current) { 416 | return { x: 0, y: 0 } 417 | } 418 | 419 | const fadeInFactor = calculateFadeInFactor() 420 | const rect = glassRef.current.getBoundingClientRect() 421 | const pillCenterX = rect.left + rect.width / 2 422 | const pillCenterY = rect.top + rect.height / 2 423 | 424 | return { 425 | x: (globalMousePos.x - pillCenterX) * elasticity * 0.1 * fadeInFactor, 426 | y: (globalMousePos.y - pillCenterY) * elasticity * 0.1 * fadeInFactor, 427 | } 428 | }, [globalMousePos, elasticity, calculateFadeInFactor]) 429 | 430 | // Update glass size whenever component mounts or window resizes 431 | useEffect(() => { 432 | const updateGlassSize = () => { 433 | if (glassRef.current) { 434 | const rect = glassRef.current.getBoundingClientRect() 435 | setGlassSize({ width: rect.width, height: rect.height }) 436 | } 437 | } 438 | 439 | updateGlassSize() 440 | window.addEventListener("resize", updateGlassSize) 441 | return () => window.removeEventListener("resize", updateGlassSize) 442 | }, []) 443 | 444 | const transformStyle = `translate(calc(-50% + ${calculateElasticTranslation().x}px), calc(-50% + ${calculateElasticTranslation().y}px)) ${isActive && Boolean(onClick) ? "scale(0.96)" : calculateDirectionalScale()}` 445 | 446 | const baseStyle = { 447 | ...style, 448 | transform: transformStyle, 449 | transition: "all ease-out 0.2s", 450 | } 451 | 452 | const positionStyles = { 453 | position: baseStyle.position || "relative", 454 | top: baseStyle.top || "50%", 455 | left: baseStyle.left || "50%", 456 | } 457 | 458 | return ( 459 | <> 460 | {/* Over light effect */} 461 | <div 462 | className={`bg-black transition-all duration-150 ease-in-out pointer-events-none ${overLight ? "opacity-20" : "opacity-0"}`} 463 | style={{ 464 | ...positionStyles, 465 | height: glassSize.height, 466 | width: glassSize.width, 467 | borderRadius: `${cornerRadius}px`, 468 | transform: baseStyle.transform, 469 | transition: baseStyle.transition, 470 | }} 471 | /> 472 | <div 473 | className={`bg-black transition-all duration-150 ease-in-out pointer-events-none mix-blend-overlay ${overLight ? "opacity-100" : "opacity-0"}`} 474 | style={{ 475 | ...positionStyles, 476 | height: glassSize.height, 477 | width: glassSize.width, 478 | borderRadius: `${cornerRadius}px`, 479 | transform: baseStyle.transform, 480 | transition: baseStyle.transition, 481 | }} 482 | /> 483 | 484 | <GlassContainer 485 | ref={glassRef} 486 | className={className} 487 | style={baseStyle} 488 | cornerRadius={cornerRadius} 489 | displacementScale={overLight ? displacementScale * 0.5 : displacementScale} 490 | blurAmount={blurAmount} 491 | saturation={saturation} 492 | aberrationIntensity={aberrationIntensity} 493 | glassSize={glassSize} 494 | padding={padding} 495 | mouseOffset={mouseOffset} 496 | onMouseEnter={() => setIsHovered(true)} 497 | onMouseLeave={() => setIsHovered(false)} 498 | onMouseDown={() => setIsActive(true)} 499 | onMouseUp={() => setIsActive(false)} 500 | active={isActive} 501 | overLight={overLight} 502 | onClick={onClick} 503 | mode={mode} 504 | > 505 | {children} 506 | </GlassContainer> 507 | 508 | {/* Border layer 1 - extracted from glass container */} 509 | <span 510 | style={{ 511 | ...positionStyles, 512 | height: glassSize.height, 513 | width: glassSize.width, 514 | borderRadius: `${cornerRadius}px`, 515 | transform: baseStyle.transform, 516 | transition: baseStyle.transition, 517 | pointerEvents: "none", 518 | mixBlendMode: "screen", 519 | opacity: 0.2, 520 | padding: "1.5px", 521 | WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)", 522 | WebkitMaskComposite: "xor", 523 | maskComposite: "exclude", 524 | boxShadow: "0 0 0 0.5px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(255, 255, 255, 0.25) inset, 0 1px 4px rgba(0, 0, 0, 0.35)", 525 | background: `linear-gradient( 526 | ${135 + mouseOffset.x * 1.2}deg, 527 | rgba(255, 255, 255, 0.0) 0%, 528 | rgba(255, 255, 255, ${0.12 + Math.abs(mouseOffset.x) * 0.008}) ${Math.max(10, 33 + mouseOffset.y * 0.3)}%, 529 | rgba(255, 255, 255, ${0.4 + Math.abs(mouseOffset.x) * 0.012}) ${Math.min(90, 66 + mouseOffset.y * 0.4)}%, 530 | rgba(255, 255, 255, 0.0) 100% 531 | )`, 532 | }} 533 | /> 534 | 535 | {/* Border layer 2 - duplicate with mix-blend-overlay */} 536 | <span 537 | style={{ 538 | ...positionStyles, 539 | height: glassSize.height, 540 | width: glassSize.width, 541 | borderRadius: `${cornerRadius}px`, 542 | transform: baseStyle.transform, 543 | transition: baseStyle.transition, 544 | pointerEvents: "none", 545 | mixBlendMode: "overlay", 546 | padding: "1.5px", 547 | WebkitMask: "linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0)", 548 | WebkitMaskComposite: "xor", 549 | maskComposite: "exclude", 550 | boxShadow: "0 0 0 0.5px rgba(255, 255, 255, 0.5) inset, 0 1px 3px rgba(255, 255, 255, 0.25) inset, 0 1px 4px rgba(0, 0, 0, 0.35)", 551 | background: `linear-gradient( 552 | ${135 + mouseOffset.x * 1.2}deg, 553 | rgba(255, 255, 255, 0.0) 0%, 554 | rgba(255, 255, 255, ${0.32 + Math.abs(mouseOffset.x) * 0.008}) ${Math.max(10, 33 + mouseOffset.y * 0.3)}%, 555 | rgba(255, 255, 255, ${0.6 + Math.abs(mouseOffset.x) * 0.012}) ${Math.min(90, 66 + mouseOffset.y * 0.4)}%, 556 | rgba(255, 255, 255, 0.0) 100% 557 | )`, 558 | }} 559 | /> 560 | 561 | {/* Hover effects */} 562 | {Boolean(onClick) && ( 563 | <> 564 | <div 565 | style={{ 566 | ...positionStyles, 567 | height: glassSize.height, 568 | width: glassSize.width + 1, 569 | borderRadius: `${cornerRadius}px`, 570 | transform: baseStyle.transform, 571 | pointerEvents: "none", 572 | transition: "all 0.2s ease-out", 573 | opacity: isHovered || isActive ? 0.5 : 0, 574 | backgroundImage: "radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.5) 0%, rgba(255, 255, 255, 0) 50%)", 575 | mixBlendMode: "overlay", 576 | }} 577 | /> 578 | <div 579 | style={{ 580 | ...positionStyles, 581 | height: glassSize.height, 582 | width: glassSize.width + 1, 583 | borderRadius: `${cornerRadius}px`, 584 | transform: baseStyle.transform, 585 | pointerEvents: "none", 586 | transition: "all 0.2s ease-out", 587 | opacity: isActive ? 0.5 : 0, 588 | backgroundImage: "radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 80%)", 589 | mixBlendMode: "overlay", 590 | }} 591 | /> 592 | <div 593 | style={{ 594 | ...baseStyle, 595 | height: glassSize.height, 596 | width: glassSize.width + 1, 597 | borderRadius: `${cornerRadius}px`, 598 | position: baseStyle.position, 599 | top: baseStyle.top, 600 | left: baseStyle.left, 601 | pointerEvents: "none", 602 | transition: "all 0.2s ease-out", 603 | opacity: isHovered ? 0.4 : isActive ? 0.8 : 0, 604 | backgroundImage: "radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 1) 0%, rgba(255, 255, 255, 0) 100%)", 605 | mixBlendMode: "overlay", 606 | }} 607 | /> 608 | </> 609 | )} 610 | </> 611 | ) 612 | } 613 | -------------------------------------------------------------------------------- /src/shader-utils.ts: -------------------------------------------------------------------------------- 1 | // Adapted from https://github.com/shuding/liquid-glass 2 | 3 | export interface Vec2 { 4 | x: number 5 | y: number 6 | } 7 | 8 | export interface ShaderOptions { 9 | width: number 10 | height: number 11 | fragment: (uv: Vec2, mouse?: Vec2) => Vec2 12 | mousePosition?: Vec2 13 | } 14 | 15 | function smoothStep(a: number, b: number, t: number): number { 16 | t = Math.max(0, Math.min(1, (t - a) / (b - a))) 17 | return t * t * (3 - 2 * t) 18 | } 19 | 20 | function length(x: number, y: number): number { 21 | return Math.sqrt(x * x + y * y) 22 | } 23 | 24 | function roundedRectSDF(x: number, y: number, width: number, height: number, radius: number): number { 25 | const qx = Math.abs(x) - width + radius 26 | const qy = Math.abs(y) - height + radius 27 | return Math.min(Math.max(qx, qy), 0) + length(Math.max(qx, 0), Math.max(qy, 0)) - radius 28 | } 29 | 30 | function texture(x: number, y: number): Vec2 { 31 | return { x, y } 32 | } 33 | 34 | // Shader fragment functions for different effects 35 | export const fragmentShaders = { 36 | liquidGlass: (uv: Vec2): Vec2 => { 37 | const ix = uv.x - 0.5 38 | const iy = uv.y - 0.5 39 | const distanceToEdge = roundedRectSDF(ix, iy, 0.3, 0.2, 0.6) 40 | const displacement = smoothStep(0.8, 0, distanceToEdge - 0.15) 41 | const scaled = smoothStep(0, 1, displacement) 42 | return texture(ix * scaled + 0.5, iy * scaled + 0.5) 43 | }, 44 | } 45 | 46 | export type FragmentShaderType = keyof typeof fragmentShaders 47 | 48 | export class ShaderDisplacementGenerator { 49 | private canvas: HTMLCanvasElement 50 | private context: CanvasRenderingContext2D 51 | private canvasDPI = 1 52 | 53 | constructor(private options: ShaderOptions) { 54 | this.canvas = document.createElement("canvas") 55 | this.canvas.width = options.width * this.canvasDPI 56 | this.canvas.height = options.height * this.canvasDPI 57 | this.canvas.style.display = "none" 58 | 59 | const context = this.canvas.getContext("2d") 60 | if (!context) { 61 | throw new Error("Could not get 2D context") 62 | } 63 | this.context = context 64 | } 65 | 66 | updateShader(mousePosition?: Vec2): string { 67 | const w = this.options.width * this.canvasDPI 68 | const h = this.options.height * this.canvasDPI 69 | 70 | let maxScale = 0 71 | const rawValues: number[] = [] 72 | 73 | // Calculate displacement values 74 | for (let y = 0; y < h; y++) { 75 | for (let x = 0; x < w; x++) { 76 | const uv: Vec2 = { x: x / w, y: y / h } 77 | 78 | const pos = this.options.fragment(uv, mousePosition) 79 | const dx = pos.x * w - x 80 | const dy = pos.y * h - y 81 | 82 | maxScale = Math.max(maxScale, Math.abs(dx), Math.abs(dy)) 83 | rawValues.push(dx, dy) 84 | } 85 | } 86 | 87 | // Improved normalization to prevent artifacts while maintaining intensity 88 | if (maxScale > 0) { 89 | maxScale = Math.max(maxScale, 1) // Ensure minimum scale to prevent over-normalization 90 | } else { 91 | maxScale = 1 92 | } 93 | 94 | // Create ImageData and fill it 95 | const imageData = this.context.createImageData(w, h) 96 | const data = imageData.data 97 | 98 | // Convert to image data with smoother normalization 99 | let rawIndex = 0 100 | for (let y = 0; y < h; y++) { 101 | for (let x = 0; x < w; x++) { 102 | const dx = rawValues[rawIndex++] 103 | const dy = rawValues[rawIndex++] 104 | 105 | // Smooth the displacement values at edges to prevent hard transitions 106 | const edgeDistance = Math.min(x, y, w - x - 1, h - y - 1) 107 | const edgeFactor = Math.min(1, edgeDistance / 2) // Smooth within 2 pixels of edge 108 | 109 | const smoothedDx = dx * edgeFactor 110 | const smoothedDy = dy * edgeFactor 111 | 112 | const r = smoothedDx / maxScale + 0.5 113 | const g = smoothedDy / maxScale + 0.5 114 | 115 | const pixelIndex = (y * w + x) * 4 116 | data[pixelIndex] = Math.max(0, Math.min(255, r * 255)) // Red channel (X displacement) 117 | data[pixelIndex + 1] = Math.max(0, Math.min(255, g * 255)) // Green channel (Y displacement) 118 | data[pixelIndex + 2] = Math.max(0, Math.min(255, g * 255)) // Blue channel (Y displacement for SVG filter compatibility) 119 | data[pixelIndex + 3] = 255 // Alpha channel 120 | } 121 | } 122 | 123 | this.context.putImageData(imageData, 0, 0) 124 | return this.canvas.toDataURL() 125 | } 126 | 127 | destroy(): void { 128 | this.canvas.remove() 129 | } 130 | 131 | getScale(): number { 132 | return this.canvasDPI 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const displacementMap = 2 | "data:image/jpeg;base64,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cUAUAkEkziomgGgkoxZGgxZFQFQlYnQHRdPfj/10KCSCKESCNiVkViPSLpD0h6I0Q0I0A2IoBWBIJIBKBIJoJIJ2R2J0JWBUJ0JUB0XTv479dFZDYiglYigkhEgjZFQjRFQjRFQjQigFYigHYigmgEgmglYlYnQlQlYlQHQlQnQ9P/kf1yVkNiNCNkNiVENiNiViNEViNkVCVgKCViViViSCViSCVgdCViVCViVCdgVCVCdD1D+U/XBWQ2I0I2Q2JUQ2I0JWQ0I2JUQ2JUI2JUI2J0JWJWJWA2R0BWJ0I2JUJ2BUJUJ0P//EABkQAQEBAQEBAAAAAAAAAAAAAAECABEDEP/aAAgBAQABAgB1atWrVq1atWrVq1atWrVq1atWrVq1atWrVq+OrVq1atWrVq1atWrVq1atWrVq1atWrVq1atXxVppppppdWrVq1atWrVq1NNNNNNNNNNNPVWmmmmms6tWrVq1atWpppppppppppppp6q0000uc51atWrVq1ammmmmmmmmmmmmt1Vpppc5znVq1atWrVqaaaaaaaaaaaaaeqtNLnOc51atWrVq1ammmmmmmmmmmmmnqrS5znOc6tWrVq16222mmmmmmlVppp6tKuc5znOrVq1a9TbbbbTTTTTSq000qtLnOc5zq1atWrW0222200000qqqtKqrnOc5zq1atTbbbbbbbbTTTSqqqqqq5znOc6tTTTbbbbbbbbTTTSqqqqrlVznOctNNNtttttttttNNNNKqqqrqznKqrTTTTbbbbbbbbbTTTSqqqqrqznOc5aaaabbbbbbbbbaaaaVVVVVdWc5znVq1NNttttttttttNNKqqqqudWc5znVq16tbbbbbbbbbbTTSqqqq5XVnOc6tWrVrb1tttttttttNNKqqqqrWrK5VWmmm2230bbbbbbaaaXOc5zlVa1KuVVppptttt9G22222mmlzlVznK6tWVVWmmmm2222222222mlznOc5znLWppVVWmmm22222229bTWrOc5znOcq1qaaVpWmm222222229erVqznOc5znKtatStK0rTbTTbbbberXr1as5znOc5aVpppppWlabaabbbb1ta9WrVnOc5znU0rTTTTTTTTTbTTbbbTWvVq1as5znOdTTStNNNNNNNNNtNNtttN6tWvVq1ZznOrU00rTTTTTTTTTTTTTbTWvVq1atWrOc6tTTTStNNNNNNNNNNtNNtNa9WrVq1Z1Z1NNNNNK1q1NNNNNNNNNNNtNatWrVq1atWrU00000rWrVq1atWrVq1alaaa1atWrVq1NNNammmmla1atWrVq1aterVq16tWrVnVqa1NK1qaaaVX/xAAWEAADAAAAAAAAAAAAAAAAAAAhgJD/2gAIAQEAAz8AaExf/8QAGhEBAQEBAQEBAAAAAAAAAAAAAQISEQADEP/aAAgBAgEBAgDx48ePHjx48ePHjx48ePHjx48ePHjx48ePHj86IiIiIiInjx48ePHjx48IiIiIj0oooooooooRERER73ve60UUUUUUVrWiiiiiihERERER73ve97ooooorRWiiiiihKERERER73ve973RRRRWtFFFFFFCIiIiIiPe973ve60UUVrRRRRRRQiIlCIiI973ve973pRRWiiiiiiiiiiiiiiihEe973ve973RRWtFFFFFFFFFFFFFFFFFFa13ve973WitaKKKKKKKKKKKKKKKKKK1rWtd1rutFa1oooooooooooosssooorWta1rWta1rRRRRRRRRRRZZZZZZZZZWta1rWta1rRRRRRRRRZZZZZZZZZZZZe9a1rWta1rWitaKLLLLLLLLLLLLLLLLL3rWta1rWtFbLLLLLLLLLLLLLLLLLLLL3vWta1rWita1ssssssss+hZZZZZZZZe961rWta0Vre97LLLLLLLLLLLPoWWWWWXrWta1oorWta3ssss+hZZZZ9Cyyyyyyyyiita1orWta1ve9llllllllllllllllFFa0VorWta1ve9llllllllllllllllllFFFaK1rWta1rWiyyyyyyyyyyyyiiiiiiitFFa1rWta1oosoosssssoooosoooorRRRWta1rWta0UUUUUWUUUUUUUUUUUVoooorWta1rWtaKKKKKKmiiiiiiiiiiiiiiitd73ve61oSiiipoqaKKKKKKKKKK0UUUVrve973vREREZoSihEooooorRRRRWtd73ve9EREREREoSiiiiitFllllla73ve9ERERERESiiiiiitH0PoWWWWVrXe96IiIiMoiJRRRRRRWjwlFFllllFFd6IiIiIlCUUUUUUUUUePHjx48ePCIiIiIiIiUUUUUUUUUUUePHjx48ePHjx48ePHjx48IiUUUUUUJRRRX//xAAWEQADAAAAAAAAAAAAAAAAAAABYJD/2gAIAQIBAz8AtEV7/8QAFxEBAQEBAAAAAAAAAAAAAAAAAAECEP/aAAgBAwEBAgCtNNNNNNNNNNNNNNNNNNNNNNNNNNNNNcrTTTTTTTTTTTTTTTTTTTTTTTTTTTTTXKrTTTTTTTU000000000000000000001FVpppppqampqaaaaaaaaaaaaaaaaaaaa5Vaaaaampqampqammmmmmmmmmmlaaaaaaiq0001NTU1NTU1NTTTTTTTTTTSqqtNNNcqtNNSyzU1LNTU1NTTTTTTTTTSqqq001ytNLLLLNTU1NTU1NTbbbTTTTTSqqq001ytNLLLLLNTU1NTU3NttttNNNNNKqq001KrSyyyyyzU1NTU3Nzc02220000qqqqrSqqyyyyyzU1NTU3Nzc3NttttNNNKqqqqqqssssss1NTU3Nzc3NzbbbbTTTSqqqqqqrLLLLLNTU1Nzc3Nzc22220000qqqqqqqqssss1NTU3Nzc3NzbbbbbTTSqqqqqqqqqqzU1NTc3Nzc3Nzbc22000qqqqqqqqqqqtTU3Nzc3Nzc3NtzbTTSqqqqrKqqqqqtNNzc23Nzc3Nzc3NTU1KqqqrKqqqqqtNNNNttzc3Nzc3NzU1NLLLLLKqqqqqqqq0022223Nzc3NzU1NSyyyyyyqqqqqqqrTTbbbbc3Nzc3NTU1LLLLLLKsqqqqqqrTTTTbbbc3Nzc1NTUsssssssqqqqqqrTTTTTbbbTc3NTU1NTUsssssqqqqqqqq0000222023NTU1NTUsssssqqqqqqqq000000003NTU1NTU1LLLLLNKrTSqqqqtNNNNNNtNNTU1NSzUssss00qq0qqqqrTTTTTTTTTU1NTUs1LLLNNNKrTTTSqqq00000000001NTU1LNTU0000qtNNNKqqqtNNNNNNNNTU1NTUs1NNNNNKss1NNNK00qtK0000001NNTU0s000000qq000001NKrStNNNNK1NNNNStNNNNNKqtNNNNNNNK0000000rU0000rTTTTTSq00000rTTTTTTTTTTTTTTTTStNNNNKr/xAAUEQEAAAAAAAAAAAAAAAAAAACg/9oACAEDAQM/AAAf/9k=" 3 | 4 | export const polarDisplacementMap = 5 | "data:image/jpeg;base64,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" 6 | 7 | export const prominentDisplacementMap = 8 | "data:image/png;base64,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" 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "lib": ["DOM", "DOM.Iterable", "ES6"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "esModuleInterop": true, 8 | "allowSyntheticDefaultImports": true, 9 | "strict": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "module": "ESNext", 13 | "moduleResolution": "node", 14 | "resolveJsonModule": true, 15 | "isolatedModules": true, 16 | "noEmit": false, 17 | "declaration": true, 18 | "declarationMap": true, 19 | "outDir": "dist", 20 | "jsx": "react-jsx" 21 | }, 22 | "include": [ 23 | "src" 24 | ], 25 | "exclude": [ 26 | "node_modules", 27 | "dist" 28 | ] 29 | } --------------------------------------------------------------------------------