├── .eslintrc.json ├── .gitattributes ├── .github └── workflows │ └── nextjs.yml ├── .gitignore ├── .npmignore ├── LICENSE ├── README-images ├── --squircle-fill.png ├── --squircle-outline.png ├── --squircle-radius.png ├── --squircle-smooth.png ├── cover.png └── squircle-radius-separate.png ├── README.md ├── global.d.ts ├── lib └── squircle.js ├── next-env.d.ts ├── next.config.js ├── package.json ├── public ├── favicon-16x16.png ├── favicon-192x192.png ├── favicon-32x32.png ├── favicon-96x96.png └── squircle.min.js ├── squircle.min.js ├── src ├── assets │ └── zebra.png ├── components │ ├── Button │ │ ├── index.tsx │ │ └── style.module.scss │ ├── Range │ │ ├── index.tsx │ │ └── style.module.scss │ └── ThemeSwitcher │ │ ├── index.tsx │ │ └── style.module.scss ├── pages │ ├── _app.tsx │ ├── _document.tsx │ ├── app.module.scss │ └── index.tsx ├── sections │ ├── BenefitsSection │ │ ├── index.tsx │ │ └── style.module.scss │ ├── ButtonsSection │ │ ├── index.tsx │ │ └── style.module.scss │ ├── CSSSection │ │ ├── index.tsx │ │ └── style.module.scss │ ├── CanIUse │ │ ├── index.tsx │ │ └── style.module.scss │ ├── CompareSection │ │ ├── index.tsx │ │ └── style.module.scss │ ├── Footer │ │ ├── index.tsx │ │ └── style.module.scss │ ├── HowItWorksSection │ │ ├── index.tsx │ │ └── style.module.scss │ ├── Intro │ │ ├── index.tsx │ │ └── style.module.scss │ ├── Scalable │ │ ├── index.tsx │ │ └── style.module.scss │ └── TestSection │ │ ├── index.tsx │ │ └── style.module.scss └── styles │ ├── _mixins.scss │ ├── fonts │ ├── druk-wide-bold.woff2 │ └── fonts.scss │ ├── globals.scss │ └── normilize.css ├── tsconfig.json └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals", 3 | "node": true 4 | } 5 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/workflows/nextjs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Next.js site to GitHub Pages 2 | # 3 | # To get started with Next.js see: https://nextjs.org/docs/getting-started 4 | # 5 | name: Deploy Next.js site to Pages 6 | 7 | on: 8 | # Runs on pushes targeting the default branch 9 | push: 10 | branches: ["main"] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: "pages" 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v3 34 | - name: Detect package manager 35 | id: detect-package-manager 36 | run: | 37 | if [ -f "${{ github.workspace }}/yarn.lock" ]; then 38 | echo "manager=yarn" >> $GITHUB_OUTPUT 39 | echo "command=install" >> $GITHUB_OUTPUT 40 | echo "runner=yarn" >> $GITHUB_OUTPUT 41 | exit 0 42 | elif [ -f "${{ github.workspace }}/package.json" ]; then 43 | echo "manager=npm" >> $GITHUB_OUTPUT 44 | echo "command=ci" >> $GITHUB_OUTPUT 45 | echo "runner=npx --no-install" >> $GITHUB_OUTPUT 46 | exit 0 47 | else 48 | echo "Unable to determine package manager" 49 | exit 1 50 | fi 51 | - name: Setup Node 52 | uses: actions/setup-node@v3 53 | with: 54 | node-version: "16" 55 | cache: ${{ steps.detect-package-manager.outputs.manager }} 56 | - name: Setup Pages 57 | uses: actions/configure-pages@v3 58 | with: 59 | # Automatically inject basePath in your Next.js configuration file and disable 60 | # server side image optimization (https://nextjs.org/docs/api-reference/next/image#unoptimized). 61 | # 62 | # You may remove this line if you want to manage the configuration yourself. 63 | static_site_generator: next 64 | - name: Restore cache 65 | uses: actions/cache@v3 66 | with: 67 | path: | 68 | .next/cache 69 | # Generate a new cache whenever packages or source files change. 70 | key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} 71 | # If source files changed but packages didn't, rebuild from a prior cache. 72 | restore-keys: | 73 | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json', '**/yarn.lock') }}- 74 | - name: Install dependencies 75 | run: ${{ steps.detect-package-manager.outputs.manager }} ${{ steps.detect-package-manager.outputs.command }} 76 | - name: Build with Next.js 77 | run: ${{ steps.detect-package-manager.outputs.runner }} next build 78 | - name: Static HTML export with Next.js 79 | run: ${{ steps.detect-package-manager.outputs.runner }} next export 80 | - name: Upload artifact 81 | uses: actions/upload-pages-artifact@v1 82 | with: 83 | path: ./out 84 | 85 | # Deployment job 86 | deploy: 87 | environment: 88 | name: github-pages 89 | url: ${{ steps.deployment.outputs.page_url }} 90 | runs-on: ubuntu-latest 91 | needs: build 92 | steps: 93 | - name: Deploy to GitHub Pages 94 | id: deployment 95 | uses: actions/deploy-pages@v2 96 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Node.js 4 | node_modules 5 | 6 | # Build output 7 | /.next 8 | /out 9 | 10 | # Environment variables 11 | .env.local 12 | .env.development.local 13 | .env.test.local 14 | .env.production.local 15 | 16 | # Cache and logs 17 | /.pnp 18 | .pnp.js 19 | .next/cache 20 | /.next/server 21 | /.next/static 22 | /.next/ssr-bundle 23 | /.next/build-manifest.json 24 | /.next/react-loadable-manifest.json 25 | 26 | # Package manager files 27 | yarn.lock 28 | .pnpm-lock.yaml 29 | package-lock.json 30 | 31 | # IDE files 32 | .idea 33 | .vscode 34 | 35 | # misc 36 | .DS_Store 37 | .env.local 38 | .env.development.local 39 | .env.test.local 40 | .env.production.local 41 | 42 | npm-debug.log* 43 | yarn-debug.log* 44 | yarn-error.log* 45 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # Ignore everything 2 | * 3 | 4 | # But not these files... 5 | !lib/**/* 6 | !.npmignore 7 | !README.md 8 | !README-images 9 | # etc... 10 | 11 | # ...even if they are in subdirectories 12 | 13 | # if the files to be tracked are in subdirectories 14 | !squircle.min.js 15 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 LaptevPavel 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README-images/--squircle-fill.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/README-images/--squircle-fill.png -------------------------------------------------------------------------------- /README-images/--squircle-outline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/README-images/--squircle-outline.png -------------------------------------------------------------------------------- /README-images/--squircle-radius.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/README-images/--squircle-radius.png -------------------------------------------------------------------------------- /README-images/--squircle-smooth.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/README-images/--squircle-smooth.png -------------------------------------------------------------------------------- /README-images/cover.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/README-images/cover.png -------------------------------------------------------------------------------- /README-images/squircle-radius-separate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/README-images/squircle-radius-separate.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Squircle CSS Houdini 2 | 3 | ![Cover](https://raw.githubusercontent.com/PavelLaptev/squircle-houdini-css/main/README-images/cover.png) 4 | 5 | A tiny CSS Houdini module that allows adding a squircle shape to HTML elements. 6 | 7 | --- 8 | 9 | ### 🎻  [Demo on GitHub Pages](https://pavellaptev.github.io/css-houdini-squircle/) 10 | 11 | ### 👾  [Codepen examples](https://codepen.io/collection/XjgQqp) 12 | 13 | ### 📦  [NPM package](https://www.npmjs.com/package/css-houdini-squircle) 14 | 15 | ### 🗞  [Medium article](https://pavellaptev.medium.com/squircles-on-the-web-houdini-to-the-rescue-5ef11f646b72) 16 | 17 | --- 18 | 19 | # 🚀 Usage 20 | 21 | ## Add the script 22 | 23 | In order to use the module, you need to add the script to your HTML file. But it's not a usual JS module that you can import. 24 | 25 | 26 | ```js 27 | // Vanilla JS and Create React App 28 | // Add the script to the index.html file 29 | 30 | 37 | 38 | ``` 39 | 40 | ```tsx 41 | // NextJS for TSX files 42 | // Add the script to the _app.js file (or any other file that is loaded on every page) 43 | 44 | React.useEffect(() => { 45 | (CSS as any).paintWorklet.addModule("squircle.min.js"); 46 | }, []); 47 | ``` 48 | 49 | ## Add the styles 50 | 51 | ```css 52 | /* use mask */ 53 | .squircle { 54 | width: 200px; 55 | height: 200px; 56 | background: linear-gradient(45deg, yellow, blue); 57 | --squircle-smooth: 1; 58 | --squircle-radius: 10px; 59 | mask-image: paint(squircle); 60 | } 61 | ``` 62 | ```css 63 | /* use background */ 64 | .squircle { 65 | width: 200px; 66 | height: 200px; 67 | background: paint(squircle); 68 | --squircle-smooth: 1; 69 | --squircle-radius: 10px; 70 | --squircle-fill: #f45; 71 | } 72 | ``` 73 | 74 | ## 👉 [codepen example](https://codepen.io/PavelLaptev/pen/PoWjzyB) 75 | 76 | --- 77 | 78 | # 🎛  Custom CSS Properties 79 | 80 | ![--squircle-radius](./README-images/--squircle-radius.png) 81 | 82 | ## --squircle-radius property 83 | 84 | The property controls the roundness of the corners. 85 | You can provide 1, 2, 3 or 4 values, similar to padding/margin in CSS. 86 | The order is clockwise: top left, top right, bottom right, bottom left 87 | 88 | - Syntax: **``** OR **``** OR **``** OR **``** 89 | - Defaul value: **`8px`** (if no radius at all is defined) OR: **`0`** (if only some radii are defined) 90 | - Min/Max values: **`—`** (the radii are capped at half of the shorter side of width/ height) 91 | 92 | #### --squircle-radius-top-left, --squircle-radius-top-right, --squircle-radius-bottom-right, --squircle-radius-bottom-left 93 | 94 | Set radii for the corners individually 95 | 96 | ```css 97 | /* Usage */ 98 | 99 | .squircle { 100 | /* other properties */ 101 | width: 200px; 102 | height: 200px; 103 | background: paint(squircle); 104 | /* the property */ 105 | --squircle-radius: 20px; 106 | } 107 | ``` 108 | 109 | --- 110 | 111 | ![individual border radius](./README-images/squircle-radius-separate.png) 112 | 113 | ## individual border radius 114 | 115 | The property controls the roundness of the corners individually. 116 | 117 | ```css 118 | /* Usage */ 119 | 120 | .squircle { 121 | /* other properties */ 122 | width: 200px; 123 | height: 200px; 124 | background: paint(squircle); 125 | /* the property */ 126 | --squircle-radius-top-left: 0px; 127 | --squircle-radius-top-right: 15px; 128 | --squircle-radius-bottom-right: 30px; 129 | --squircle-radius-bottom-left: 40px; 130 | } 131 | ``` 132 | 133 | --- 134 | 135 | ![--squircle-smooth](./README-images/--squircle-smooth.png) 136 | 137 | ## --squircle-smooth property 138 | 139 | The property controls the length of bezier guide lines. Could be defined by `--squircle-ratio`. 140 | 141 | - Syntax: **``** 142 | - Defaul value: **`1`** 143 | - Min/Max values: **`0.1 / 1`** 144 | 145 | ```css 146 | /* Usage */ 147 | 148 | .squircle { 149 | /* other properties */ 150 | width: 200px; 151 | height: 200px; 152 | background: paint(squircle); 153 | --squircle-radius: 20px; 154 | /* the property */ 155 | --squircle-smooth: 0.5; 156 | } 157 | ``` 158 | 159 | --- 160 | 161 | ![--squircle-outline](./README-images/--squircle-outline.png) 162 | 163 | ## --squircle-outline property 164 | 165 | The property controls squircle outline. There are two methods how too use it with `background-mask` and `mask`+`:pseudo-element`. to find out more check [codepen examples](https://codepen.io/collection/XjgQqp). 166 | 167 | - Syntax: **``** 168 | - Defaul value: **`—`** 169 | - Min/Max values: **`—`** 170 | 171 | ```css 172 | /* Usage */ 173 | 174 | .squircle { 175 | /* other properties */ 176 | width: 200px; 177 | height: 200px; 178 | background: paint(squircle); 179 | --squircle-radius: 20px; 180 | /* the property */ 181 | --squircle-outline: 5px; 182 | } 183 | ``` 184 | 185 | --- 186 | 187 | ![--squircle-fill](./README-images/--squircle-fill.png) 188 | 189 | ## --squircle-fill property 190 | 191 | The property accepts any color, including variables. 192 | 193 | ⚠️ Work only with `background: paint(squircle);`. For `mask-image: paint(squircle);` use `background` property. 194 | 195 | - Syntax: **``** 196 | - Defaul value: **`#f45`** 197 | - Min/Max values: **`—`** 198 | 199 | ```css 200 | /* Usage */ 201 | 202 | .squircle { 203 | /* other properties */ 204 | width: 200px; 205 | height: 200px; 206 | background: paint(squircle); 207 | --squircle-radius: 20px; 208 | /* the property */ 209 | --squircle-fill: #f45; 210 | } 211 | ``` 212 | 213 | 214 | --- 215 | 216 | ## 🕹  How to install particular version 217 | 218 | ```js 219 | // latest version 220 | 227 | 228 | // or particular version 229 | 236 | ``` 237 | 238 | #### Install via NPM 239 | 240 | ``` 241 | npm i css-houdini-squircle 242 | ``` 243 | 244 | #### Download 245 | 246 | You can download the min version of the module [from UNPKG](https://www.unpkg.com/browse/css-houdini-squircle/squircle.min.js) 247 | 248 | ``` 249 | // latest version 250 | https://www.unpkg.com/browse/css-houdini-squircle/squircle.min.js 251 | ``` 252 | 253 | --- 254 | 255 | ## ✨  Use `css-paint-polyfill` 256 | 257 | In order to get the module work on other browsers, you can use [Paint Worklets polyfill](https://github.com/GoogleChromeLabs/css-paint-polyfill). 258 | 259 | ⚠️ Check for artefacts before deploying. 260 | 261 | ```js 262 | // use with polifill example 263 | 274 | ``` 275 | 276 | --- 277 | 278 | ## Contributing and testing 279 | 280 | If you have any ideas, just [open an issue](https://github.com/PavelLaptev/squircle-houdini-css/issues) and tell what you think. 281 | 282 | If you'd like to contribute, please fork the repository. Pull requests are warmly welcome. 283 | 284 | The project structure is separated into `nextJS` app and `lib` folder. The `lib` folder contains the script itself. The `nextJS` app is used for the demo. The `lib` folder is a separate NPM package. 285 | 286 | 287 | ``` 288 | 📁 root 289 | 📁 lib 290 | - package.json 291 | - squircle.js 292 | 📁 … other nextJS folders 293 | ``` 294 | 295 | In order to test the script locally: 296 | 297 | 1. you need to run `npm run dev` in the root folder. It will start the NextJS app. 298 | 2. Then you need to run `npm run watch:build` in the `lib` folder. It will start the watcher for the script. It will build the script every time you change it and create `squircle.min.js` file in the `lib` folder and in the `public` folder of the NextJS app. 299 | 3. In the `index.tsx` file of the NextJS app, you can uncomment the line with [test section](https://github.com/PavelLaptev/squircle-houdini-css/blob/70f81510d45185e3946ec2cbec3cd4ab6495224b/src/pages/index.tsx#L57) un comment other in order to ease the development process. 300 | 301 | --- 302 | 303 | ## Change log (v0.3.0) 304 | 305 | - Removed `--squircle-ratio` property. It's now fixed to `1.8`. It's still possible to change the ratio by changing `--squircle-smooth` property. 306 | - Moved the demo to NextJS 307 | - Added `--squircle-radius-top-left`, `--squircle-radius-top-right`, `--squircle-radius-bottom-right`, `--squircle-radius-bottom-left` properties 308 | - Added separate `lib` folder only for the script 309 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | declare module "css-paint-polyfill" { 2 | const value: any; 3 | export default value; 4 | } 5 | 6 | declare interface CSS { 7 | paintWorklet: { 8 | addModule( 9 | moduleURL: string | URL, 10 | options?: { credentials: Request["credentials"] } 11 | ): Promise; 12 | }; 13 | registerProperty(property: { 14 | name: string; 15 | syntax?: string; 16 | inherits: boolean; 17 | initialValue?: string | number; 18 | }): void; 19 | } 20 | 21 | declare function registerPaint(name: string, classReference: unknown): void; 22 | 23 | // export {}; 24 | -------------------------------------------------------------------------------- /lib/squircle.js: -------------------------------------------------------------------------------- 1 | ///////////////////////// 2 | ///////// UTILS ///////// 3 | ///////////////////////// 4 | 5 | const drawSquircle = (ctx, geom, radii, smooth, lineWidth, color) => { 6 | const defaultFill = color; 7 | const lineWidthOffset = lineWidth / 2; 8 | // OPEN LEFT-TOP CORNER 9 | ctx.beginPath(); 10 | ctx.lineTo(radii[0], lineWidthOffset); 11 | // TOP-RIGHT CORNER 12 | ctx.lineTo(geom.width - radii[1], lineWidthOffset); 13 | ctx.bezierCurveTo( 14 | geom.width - radii[1] / smooth, 15 | lineWidthOffset, // first bezier point 16 | geom.width - lineWidthOffset, 17 | radii[1] / smooth, // second bezier point 18 | geom.width - lineWidthOffset, 19 | radii[1] // last connect point 20 | ); 21 | // BOTTOM-RIGHT CORNER 22 | ctx.lineTo(geom.width - lineWidthOffset, geom.height - radii[2]); 23 | ctx.bezierCurveTo( 24 | geom.width - lineWidthOffset, 25 | geom.height - radii[2] / smooth, // first bezier point 26 | geom.width - radii[2] / smooth, 27 | geom.height - lineWidthOffset, // second bezier point 28 | geom.width - radii[2], 29 | geom.height - lineWidthOffset // last connect point 30 | ); 31 | // BOTTOM-LEFT CORNER 32 | ctx.lineTo(radii[3], geom.height - lineWidthOffset); 33 | ctx.bezierCurveTo( 34 | radii[3] / smooth, 35 | geom.height - lineWidthOffset, // first bezier point 36 | lineWidthOffset, 37 | geom.height - radii[3] / smooth, // second bezier point 38 | lineWidthOffset, 39 | geom.height - radii[3] // last connect point 40 | ); 41 | // CLOSE LEFT-TOP CORNER 42 | ctx.lineTo(lineWidthOffset, radii[0]); 43 | ctx.bezierCurveTo( 44 | lineWidthOffset, 45 | radii[0] / smooth, // first bezier point 46 | radii[0] / smooth, 47 | lineWidthOffset, // second bezier point 48 | radii[0], 49 | lineWidthOffset // last connect point 50 | ); 51 | ctx.closePath(); 52 | 53 | if (lineWidth) { 54 | ctx.strokeStyle = defaultFill; 55 | ctx.lineWidth = lineWidth; 56 | ctx.stroke(); 57 | } else { 58 | ctx.fillStyle = defaultFill; 59 | ctx.fill(); 60 | } 61 | }; 62 | 63 | ///////////////////////// 64 | ///////// CLASS ///////// 65 | ///////////////////////// 66 | 67 | class SquircleClass { 68 | static get contextOptions() { 69 | return { alpha: true }; 70 | } 71 | static get inputProperties() { 72 | return [ 73 | "--squircle-radius", 74 | "--squircle-radius-top-left", // <-- if the order changes ... 75 | "--squircle-radius-top-right", 76 | "--squircle-radius-bottom-right", 77 | "--squircle-radius-bottom-left", // --> the slice values of individualRadiiProps need to be updated 78 | "--squircle-smooth", 79 | "--squircle-outline", 80 | "--squircle-fill" 81 | ]; 82 | } 83 | 84 | paint(ctx, geom, properties) { 85 | const smoothRatio = 10; 86 | const distanceRatio = 1.8; 87 | const squircleSmooth = parseFloat( 88 | properties.get("--squircle-smooth") * smoothRatio 89 | ); 90 | 91 | // CALCULATE RADII 92 | const individualRadiiProps = SquircleClass.inputProperties.slice(1, 5); 93 | 94 | let squircleRadii = individualRadiiProps.map((prop) => { 95 | const value = properties.get(prop); 96 | return value ? parseInt(value, 10) * distanceRatio : NaN; 97 | }); 98 | 99 | let shorthand_R; 100 | 101 | // Check if any of the individual radii are NaN, if so, process the shorthand 102 | if (squircleRadii.some(isNaN)) { 103 | const radiusRegex = /([0-9]+[a-z%]*)/g; // Units are ignored. 104 | 105 | const radius_shorthand = properties.get("--squircle-radius").toString(); 106 | const matches = radius_shorthand.match(radiusRegex); 107 | 108 | if (matches) { 109 | shorthand_R = matches.map((val) => parseInt(val, 10) * distanceRatio); 110 | 111 | while (shorthand_R.length < 4) { 112 | if (shorthand_R.length === 1) { 113 | // If there's only one value, duplicate it for all corners 114 | shorthand_R.push(shorthand_R[0]); 115 | } else if (shorthand_R.length === 2) { 116 | // If there are two values, first one applies to top and bottom, second to left and right 117 | shorthand_R = [ 118 | shorthand_R[0], 119 | shorthand_R[1], 120 | shorthand_R[0], 121 | shorthand_R[1] 122 | ]; 123 | } else if (shorthand_R.length === 3) { 124 | // If there are three values, first applies to top, second to right and left, third to bottom 125 | shorthand_R = [ 126 | shorthand_R[0], 127 | shorthand_R[1], 128 | shorthand_R[2], 129 | shorthand_R[1] 130 | ]; 131 | } 132 | } 133 | } else { 134 | // if no radii at all are provided, set default radius = 8, otherwise set undefined ones to 0 135 | const defaultRadius = squircleRadii.every(isNaN) 136 | ? 8 * distanceRatio 137 | : 0; 138 | shorthand_R = [ 139 | defaultRadius, 140 | defaultRadius, 141 | defaultRadius, 142 | defaultRadius 143 | ]; 144 | } 145 | } 146 | 147 | // Replace NaN values in radii with corresponding values from shorthand or default 148 | squircleRadii = squircleRadii.map((val, i) => 149 | isNaN(val) ? shorthand_R[i] : val 150 | ); 151 | 152 | const squrcleOutline = parseFloat(properties.get("--squircle-outline"), 10); 153 | const squrcleColor = properties.get("--squircle-fill").toString(); 154 | 155 | const isSmooth = () => { 156 | if (typeof properties.get("--squircle-smooth")[0] !== "undefined") { 157 | if (squircleSmooth === 0) { 158 | return 1; 159 | } 160 | return squircleSmooth; 161 | } else { 162 | return 10; 163 | } 164 | }; 165 | 166 | const isOutline = () => { 167 | if (squrcleOutline) { 168 | return squrcleOutline; 169 | } else { 170 | return 0; 171 | } 172 | }; 173 | 174 | const isColor = () => { 175 | if (squrcleColor) { 176 | return squrcleColor; 177 | } else { 178 | return "#f45"; 179 | } 180 | }; 181 | 182 | const maxRadius = Math.max(...squircleRadii); 183 | if (maxRadius < geom.width / 2 && maxRadius < geom.height / 2) { 184 | drawSquircle( 185 | ctx, 186 | geom, 187 | squircleRadii, 188 | isSmooth(), 189 | isOutline(), 190 | isColor() 191 | ); 192 | } else { 193 | const minRadius = Math.min(geom.width / 2, geom.height / 2); 194 | drawSquircle( 195 | ctx, 196 | geom, 197 | squircleRadii.map(() => minRadius), 198 | isSmooth(), 199 | isOutline(), 200 | isColor() 201 | ); 202 | } 203 | } 204 | } 205 | 206 | ///////////////////////// 207 | /////// REGISTER //////// 208 | ///////////////////////// 209 | 210 | if (typeof registerPaint !== "undefined") { 211 | registerPaint("squircle", SquircleClass); 212 | } 213 | -------------------------------------------------------------------------------- /next-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /// 3 | 4 | // NOTE: This file should not be edited 5 | // see https://nextjs.org/docs/basic-features/typescript for more information. 6 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: true 4 | }; 5 | 6 | module.exports = nextConfig; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-houdini-squircle", 3 | "version": "0.3.0", 4 | "description": "A tiny CSS Houdini module that allows to add a squircle shape to HTML elements", 5 | "main": "lib/squircle.js", 6 | "private": false, 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/PavelLaptev/squircle-houdini-css" 11 | }, 12 | "homepage": "https://github.com/PavelLaptev/squircle-houdini-css", 13 | "scripts": { 14 | "dev": "next dev", 15 | "build": "next build", 16 | "watch:lib-build": "nodemon --watch lib/squircle.js --exec 'npm run lib-build'", 17 | "lib-build": "uglifyjs lib/squircle.js -o squircle.min.js && cp squircle.min.js public/squircle.min.js", 18 | "start": "next start", 19 | "lint": "next lint" 20 | }, 21 | "dependencies": {}, 22 | "devDependencies": { 23 | "@types/node": "20.3.1", 24 | "@types/react": "18.2.12", 25 | "@types/react-dom": "18.2.4", 26 | "css-paint-polyfill": "^3.4.0", 27 | "eslint": "8.42.0", 28 | "eslint-config-next": "13.4.4", 29 | "next": "13.4.4", 30 | "nodemon": "^2.0.22", 31 | "react": "18.2.0", 32 | "react-dom": "18.2.0", 33 | "sass": "^1.63.4", 34 | "typescript": "5.1.3", 35 | "uglify-js": "^3.17.4" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /public/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/public/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/public/favicon-192x192.png -------------------------------------------------------------------------------- /public/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/public/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/public/favicon-96x96.png -------------------------------------------------------------------------------- /public/squircle.min.js: -------------------------------------------------------------------------------- 1 | const drawSquircle=(ctx,geom,radii,smooth,lineWidth,color)=>{const defaultFill=color;const lineWidthOffset=lineWidth/2;ctx.beginPath();ctx.lineTo(radii[0],lineWidthOffset);ctx.lineTo(geom.width-radii[1],lineWidthOffset);ctx.bezierCurveTo(geom.width-radii[1]/smooth,lineWidthOffset,geom.width-lineWidthOffset,radii[1]/smooth,geom.width-lineWidthOffset,radii[1]);ctx.lineTo(geom.width-lineWidthOffset,geom.height-radii[2]);ctx.bezierCurveTo(geom.width-lineWidthOffset,geom.height-radii[2]/smooth,geom.width-radii[2]/smooth,geom.height-lineWidthOffset,geom.width-radii[2],geom.height-lineWidthOffset);ctx.lineTo(radii[3],geom.height-lineWidthOffset);ctx.bezierCurveTo(radii[3]/smooth,geom.height-lineWidthOffset,lineWidthOffset,geom.height-radii[3]/smooth,lineWidthOffset,geom.height-radii[3]);ctx.lineTo(lineWidthOffset,radii[0]);ctx.bezierCurveTo(lineWidthOffset,radii[0]/smooth,radii[0]/smooth,lineWidthOffset,radii[0],lineWidthOffset);ctx.closePath();if(lineWidth){ctx.strokeStyle=defaultFill;ctx.lineWidth=lineWidth;ctx.stroke()}else{ctx.fillStyle=defaultFill;ctx.fill()}};class SquircleClass{static get contextOptions(){return{alpha:true}}static get inputProperties(){return["--squircle-radius","--squircle-radius-top-left","--squircle-radius-top-right","--squircle-radius-bottom-right","--squircle-radius-bottom-left","--squircle-smooth","--squircle-outline","--squircle-fill"]}paint(ctx,geom,properties){const smoothRatio=10;const distanceRatio=1.8;const squircleSmooth=parseFloat(properties.get("--squircle-smooth")*smoothRatio);const individualRadiiProps=SquircleClass.inputProperties.slice(1,5);let squircleRadii=individualRadiiProps.map(prop=>{const value=properties.get(prop);return value?parseInt(value,10)*distanceRatio:NaN});let shorthand_R;if(squircleRadii.some(isNaN)){const radiusRegex=/([0-9]+[a-z%]*)/g;const radius_shorthand=properties.get("--squircle-radius").toString();const matches=radius_shorthand.match(radiusRegex);if(matches){shorthand_R=matches.map(val=>parseInt(val,10)*distanceRatio);while(shorthand_R.length<4){if(shorthand_R.length===1){shorthand_R.push(shorthand_R[0])}else if(shorthand_R.length===2){shorthand_R=[shorthand_R[0],shorthand_R[1],shorthand_R[0],shorthand_R[1]]}else if(shorthand_R.length===3){shorthand_R=[shorthand_R[0],shorthand_R[1],shorthand_R[2],shorthand_R[1]]}}}else{const defaultRadius=squircleRadii.every(isNaN)?8*distanceRatio:0;shorthand_R=[defaultRadius,defaultRadius,defaultRadius,defaultRadius]}}squircleRadii=squircleRadii.map((val,i)=>isNaN(val)?shorthand_R[i]:val);const squrcleOutline=parseFloat(properties.get("--squircle-outline"),10);const squrcleColor=properties.get("--squircle-fill").toString();const isSmooth=()=>{if(typeof properties.get("--squircle-smooth")[0]!=="undefined"){if(squircleSmooth===0){return 1}return squircleSmooth}else{return 10}};const isOutline=()=>{if(squrcleOutline){return squrcleOutline}else{return 0}};const isColor=()=>{if(squrcleColor){return squrcleColor}else{return"#f45"}};const maxRadius=Math.max(...squircleRadii);if(maxRadiusminRadius),isSmooth(),isOutline(),isColor())}}}if(typeof registerPaint!=="undefined"){registerPaint("squircle",SquircleClass)} -------------------------------------------------------------------------------- /squircle.min.js: -------------------------------------------------------------------------------- 1 | const drawSquircle=(ctx,geom,radii,smooth,lineWidth,color)=>{const defaultFill=color;const lineWidthOffset=lineWidth/2;ctx.beginPath();ctx.lineTo(radii[0],lineWidthOffset);ctx.lineTo(geom.width-radii[1],lineWidthOffset);ctx.bezierCurveTo(geom.width-radii[1]/smooth,lineWidthOffset,geom.width-lineWidthOffset,radii[1]/smooth,geom.width-lineWidthOffset,radii[1]);ctx.lineTo(geom.width-lineWidthOffset,geom.height-radii[2]);ctx.bezierCurveTo(geom.width-lineWidthOffset,geom.height-radii[2]/smooth,geom.width-radii[2]/smooth,geom.height-lineWidthOffset,geom.width-radii[2],geom.height-lineWidthOffset);ctx.lineTo(radii[3],geom.height-lineWidthOffset);ctx.bezierCurveTo(radii[3]/smooth,geom.height-lineWidthOffset,lineWidthOffset,geom.height-radii[3]/smooth,lineWidthOffset,geom.height-radii[3]);ctx.lineTo(lineWidthOffset,radii[0]);ctx.bezierCurveTo(lineWidthOffset,radii[0]/smooth,radii[0]/smooth,lineWidthOffset,radii[0],lineWidthOffset);ctx.closePath();if(lineWidth){ctx.strokeStyle=defaultFill;ctx.lineWidth=lineWidth;ctx.stroke()}else{ctx.fillStyle=defaultFill;ctx.fill()}};class SquircleClass{static get contextOptions(){return{alpha:true}}static get inputProperties(){return["--squircle-radius","--squircle-radius-top-left","--squircle-radius-top-right","--squircle-radius-bottom-right","--squircle-radius-bottom-left","--squircle-smooth","--squircle-outline","--squircle-fill"]}paint(ctx,geom,properties){const smoothRatio=10;const distanceRatio=1.8;const squircleSmooth=parseFloat(properties.get("--squircle-smooth")*smoothRatio);const individualRadiiProps=SquircleClass.inputProperties.slice(1,5);let squircleRadii=individualRadiiProps.map(prop=>{const value=properties.get(prop);return value?parseInt(value,10)*distanceRatio:NaN});let shorthand_R;if(squircleRadii.some(isNaN)){const radiusRegex=/([0-9]+[a-z%]*)/g;const radius_shorthand=properties.get("--squircle-radius").toString();const matches=radius_shorthand.match(radiusRegex);if(matches){shorthand_R=matches.map(val=>parseInt(val,10)*distanceRatio);while(shorthand_R.length<4){if(shorthand_R.length===1){shorthand_R.push(shorthand_R[0])}else if(shorthand_R.length===2){shorthand_R=[shorthand_R[0],shorthand_R[1],shorthand_R[0],shorthand_R[1]]}else if(shorthand_R.length===3){shorthand_R=[shorthand_R[0],shorthand_R[1],shorthand_R[2],shorthand_R[1]]}}}else{const defaultRadius=squircleRadii.every(isNaN)?8*distanceRatio:0;shorthand_R=[defaultRadius,defaultRadius,defaultRadius,defaultRadius]}}squircleRadii=squircleRadii.map((val,i)=>isNaN(val)?shorthand_R[i]:val);const squrcleOutline=parseFloat(properties.get("--squircle-outline"),10);const squrcleColor=properties.get("--squircle-fill").toString();const isSmooth=()=>{if(typeof properties.get("--squircle-smooth")[0]!=="undefined"){if(squircleSmooth===0){return 1}return squircleSmooth}else{return 10}};const isOutline=()=>{if(squrcleOutline){return squrcleOutline}else{return 0}};const isColor=()=>{if(squrcleColor){return squrcleColor}else{return"#f45"}};const maxRadius=Math.max(...squircleRadii);if(maxRadiusminRadius),isSmooth(),isOutline(),isColor())}}}if(typeof registerPaint!=="undefined"){registerPaint("squircle",SquircleClass)} -------------------------------------------------------------------------------- /src/assets/zebra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/src/assets/zebra.png -------------------------------------------------------------------------------- /src/components/Button/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | interface ButtonProps { 5 | text?: string; 6 | link?: string; 7 | className?: string; 8 | } 9 | 10 | const Button = (props: ButtonProps) => { 11 | return ( 12 | 22 | ); 23 | }; 24 | 25 | Button.defaultProps = { 26 | onClick: () => {}, 27 | text: "Button", 28 | link: "#" 29 | }; 30 | 31 | export default Button; 32 | -------------------------------------------------------------------------------- /src/components/Button/style.module.scss: -------------------------------------------------------------------------------- 1 | .buttonWrap { 2 | display: flex; 3 | background: var(--dark-clr); 4 | mask-image: paint(squircle); 5 | --squircle-radius: 16px; 6 | --squircle-smooth: 0.5; 7 | } 8 | 9 | .button { 10 | padding: 14px 20px; 11 | font-family: "Druck"; 12 | font-size: 16px; 13 | color: var(--light-clr); 14 | transition: all 0.1s ease; 15 | 16 | &:hover { 17 | background: var(--accent-clr); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/components/Range/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | interface RangeProps { 5 | onChange?: (e: React.ChangeEvent) => void; 6 | min?: number; 7 | max?: number; 8 | value?: string; 9 | step?: number; 10 | className?: string; 11 | tooltip?: string; 12 | } 13 | 14 | const Range: React.FC = (props) => { 15 | const [value, setValue] = React.useState(props.value || "0"); 16 | 17 | const handleChange = (e: React.ChangeEvent) => { 18 | setValue(e.target.value); 19 | 20 | if (props.onChange) { 21 | props.onChange(e); 22 | } 23 | }; 24 | 25 | return ( 26 |
27 | {props.tooltip !== "" ? ( 28 |
36 | {props.tooltip} 37 |
38 | ) : null} 39 | 48 |
49 | ); 50 | }; 51 | 52 | Range.defaultProps = { 53 | onChange: () => {}, 54 | min: 0, 55 | max: 10, 56 | value: "1", 57 | step: 1, 58 | className: "", 59 | tooltip: "" 60 | }; 61 | 62 | export default Range; 63 | -------------------------------------------------------------------------------- /src/components/Range/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | position: relative; 3 | display: flex; 4 | } 5 | 6 | .tooltip { 7 | pointer-events: none; 8 | position: absolute; 9 | left: 0; 10 | bottom: 28px; 11 | transform: translateX(-50%); 12 | // height: 30px; 13 | max-width: 100px; 14 | line-height: 1.4; 15 | padding: 8px 12px; 16 | background: var(--dark-clr); 17 | font-family: "Druck"; 18 | font-size: 12px; 19 | color: var(--light-clr); 20 | --squircle-smooth: 1; 21 | --squircle-radius: 10px; 22 | mask-image: paint(squircle); 23 | } 24 | 25 | .range { 26 | -webkit-appearance: none; 27 | width: 100%; 28 | height: 24px; 29 | outline: none; 30 | appearance: none; 31 | border: none; 32 | background: none; 33 | &::-webkit-slider-runnable-track { 34 | width: 100%; 35 | height: 2px; 36 | background: var(--dark-clr); 37 | } 38 | &::-webkit-slider-thumb { 39 | -webkit-appearance: none; 40 | appearance: none; 41 | width: 24px; 42 | height: 24px; 43 | transform: translateY(-11px); 44 | background: var(--dark-clr); 45 | cursor: pointer; 46 | border-radius: 24px; 47 | outline: none; 48 | transition: all 0.1s ease; 49 | &:hover { 50 | background: var(--accent-clr); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | interface ThemeSwitcherProps { 5 | className?: string; 6 | } 7 | 8 | const ThemeSwitcher = (props: ThemeSwitcherProps) => { 9 | const [toggle, setToggle] = React.useState(false); 10 | 11 | const handleClick = () => { 12 | setToggle(!toggle); 13 | }; 14 | 15 | return ( 16 |
20 | ); 21 | }; 22 | 23 | ThemeSwitcher.defaultProps = { 24 | className: null 25 | }; 26 | 27 | export default ThemeSwitcher; 28 | -------------------------------------------------------------------------------- /src/components/ThemeSwitcher/style.module.scss: -------------------------------------------------------------------------------- 1 | .toggle { 2 | cursor: pointer; 3 | width: 60px; 4 | height: 32px; 5 | background: var(--dark-clr); 6 | mask-image: paint(squircle); 7 | --squircle-smooth: 1; 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import "@/styles/globals.scss"; 2 | import type { AppProps } from "next/app"; 3 | 4 | export default function App({ Component, pageProps }: AppProps) { 5 | return ; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from "next/document"; 2 | 3 | export default function Document() { 4 | return ( 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/app.module.scss: -------------------------------------------------------------------------------- 1 | .app { 2 | display: flex; 3 | flex-direction: column; 4 | align-items: center; 5 | } 6 | 7 | .legacyBorder { 8 | width: 400px; 9 | height: 300px; 10 | background: #3b99ff; 11 | border-radius: var(--base-radius); 12 | margin-bottom: 20px; 13 | } 14 | -------------------------------------------------------------------------------- /src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import Head from "next/head"; 3 | 4 | import TestSection from "@/sections/TestSection"; 5 | import Intro from "@/sections/Intro"; 6 | import CSSSection from "@/sections/CSSSection"; 7 | import Scalable from "@/sections/Scalable"; 8 | import ButtonsSection from "@/sections/ButtonsSection"; 9 | import BenefitsSection from "@/sections/BenefitsSection"; 10 | import CanIUse from "@/sections/CanIUse"; 11 | import HowItWorksSection from "@/sections/HowItWorksSection"; 12 | import CompareSection from "@/sections/CompareSection"; 13 | import Footer from "@/sections/Footer"; 14 | 15 | import styles from "./app.module.scss"; 16 | 17 | export default function Home() { 18 | React.useEffect(() => { 19 | if (!(("paintWorklet" in CSS) as any)) { 20 | import("css-paint-polyfill"); 21 | } 22 | 23 | (CSS as any).paintWorklet.addModule("squircle.min.js"); 24 | }, []); 25 | 26 | return ( 27 | <> 28 | 29 | Squircle CSS Houdini 30 | 34 | 35 | 36 | 37 | 43 | 49 | 55 | 56 |
57 | {/* */} 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 |
67 |
68 | 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /src/sections/BenefitsSection/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | const svgIcons = { 5 | css: ( 6 | <> 7 | 11 | 12 | ), 13 | flexible: ( 14 | <> 15 | 20 | 25 | 31 | 32 | ), 33 | outlineBackground: ( 34 | <> 35 | 39 | 44 | 45 | ), 46 | smooth: ( 47 | <> 48 | 53 | 58 | 59 | ), 60 | fast: ( 61 | <> 62 | 67 | 72 | 73 | ), 74 | small: ( 75 | <> 76 | 80 | 84 | 88 | 93 | 94 | ) 95 | }; 96 | 97 | interface CardProps { 98 | title: string; 99 | svg: React.ReactNode; 100 | radius: number; 101 | smooth: number; 102 | } 103 | 104 | const Card = (props: CardProps) => { 105 | return ( 106 |
115 | 121 | {props.svg} 122 | 123 | 124 |

{props.title}

125 |
126 | ); 127 | }; 128 | 129 | const BenefitsSection = () => { 130 | return ( 131 |
132 |
133 | 139 | 145 | 151 | 157 | 163 | 169 |
170 |
171 | ); 172 | }; 173 | 174 | export default BenefitsSection; 175 | -------------------------------------------------------------------------------- /src/sections/BenefitsSection/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrapWidth { 2 | border-top: 2px solid var(--dark-clr); 3 | border-bottom: 2px solid var(--dark-clr); 4 | width: 100%; 5 | } 6 | 7 | .wrap { 8 | max-width: var(--max-width); 9 | display: flex; 10 | flex-wrap: wrap; 11 | width: 100%; 12 | margin: 0 auto; 13 | padding: var(--vertical-paddings) var(--horizontal-paddings); 14 | @media only screen and (max-width: 620px) { 15 | margin-top: var(--padding-xs); 16 | margin-bottom: var(--padding-xs); 17 | } 18 | } 19 | 20 | .card { 21 | padding: var(--padding-m); 22 | background: var(--light-clr); 23 | mask-image: paint(squircle); 24 | --squircle-smooth: 0.8; 25 | --squircle-radius: 16px; 26 | flex: 1; 27 | display: flex; 28 | justify-content: space-between; 29 | flex-direction: column; 30 | margin: var(--padding-xs); 31 | @media only screen and (max-width: 620px) { 32 | padding: 24px; 33 | margin: var(--padding-xxs); 34 | } 35 | } 36 | 37 | .svg { 38 | width: 100px; 39 | height: 100px; 40 | } 41 | 42 | .h2 { 43 | font-family: "Druck", sans-serif; 44 | font-size: 24px; 45 | color: var(--dark-clr); 46 | margin-bottom: 0; 47 | margin-top: 32px; 48 | @media only screen and (max-width: 620px) { 49 | font-size: 18px; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/sections/ButtonsSection/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-comment-textnodes */ 2 | import React from "react"; 3 | import styles from "./style.module.scss"; 4 | 5 | import Range from "../../components/Range"; 6 | 7 | const ButtonsSection = () => { 8 | const [radius, setRadius] = React.useState(20); 9 | const [smooth, setSmooth] = React.useState(0.8); 10 | 11 | return ( 12 |
13 |
22 | Fill 23 |
24 |
33 | Outline 34 |
35 |
44 | Gradient 45 |
46 |
55 | Shadow 56 |
57 |
66 | Image 67 |
68 |
69 | { 74 | setRadius(Number(e.target.value)); 75 | }} 76 | /> 77 | { 84 | setSmooth(Number(e.target.value)); 85 | }} 86 | /> 87 |
88 |
89 | ); 90 | }; 91 | 92 | export default ButtonsSection; 93 | -------------------------------------------------------------------------------- /src/sections/ButtonsSection/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | max-width: var(--max-width); 3 | display: flex; 4 | flex-wrap: wrap; 5 | align-items: center; 6 | width: 100%; 7 | margin: 0 auto; 8 | overflow: hidden; 9 | padding: var(--vertical-paddings) var(--horizontal-paddings); 10 | } 11 | 12 | .button { 13 | font-family: "Druck"; 14 | color: var(--dark-clr); 15 | text-transform: uppercase; 16 | font-size: 46px; 17 | padding: 24px 32px; 18 | margin: 16px; 19 | 20 | --squircle-radius: 24px; 21 | --squircle-smooth: 0.9; 22 | 23 | @media only screen and (max-width: 1100px) { 24 | font-size: 32px; 25 | } 26 | @media only screen and (max-width: 620px) { 27 | font-size: 24px; 28 | margin: 8px; 29 | } 30 | } 31 | 32 | .fill { 33 | color: var(--light-clr); 34 | background: var(--dark-clr); 35 | mask-image: paint(squircle); 36 | } 37 | 38 | .outline { 39 | color: var(--dark-clr); 40 | background: paint(squircle); 41 | --squircle-outline: 2px; 42 | --squircle-fill: var(--dark-clr); 43 | } 44 | 45 | .gradient { 46 | color: var(--light-clr); 47 | background: linear-gradient(122.37deg, #ff0000 6.31%, #00ffa3 85.76%); 48 | mask-image: paint(squircle); 49 | } 50 | 51 | .shadow { 52 | background: paint(squircle); 53 | --squircle-fill: var(--main-clr); 54 | filter: drop-shadow(-1px 10px 10px #a198b1); 55 | } 56 | 57 | .image { 58 | color: white; 59 | background-color: #8321ff; 60 | background-image: url(../../assets/zebra.png); 61 | background-size: cover; 62 | mask-image: paint(squircle); 63 | } 64 | 65 | .controls { 66 | display: flex; 67 | flex-direction: column; 68 | width: 280px; 69 | margin: 16px 32px; 70 | @media only screen and (max-width: 620px) { 71 | width: 100%; 72 | margin: 32px 12px 24px; 73 | } 74 | } 75 | 76 | .range { 77 | width: 100%; 78 | 79 | &:first-child { 80 | margin-bottom: 24px; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/sections/CSSSection/index.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-comment-textnodes */ 2 | import React from "react"; 3 | import styles from "./style.module.scss"; 4 | 5 | const CSSSection = () => { 6 | const [toggleBtn, setToggleBtn] = React.useState(false); 7 | const handleClick = () => { 8 | setToggleBtn(!toggleBtn); 9 | }; 10 | 11 | return ( 12 |
13 |
14 |
15 |

How to use in CSS:

16 |
17 |
18 |
19 |
25 | Fill 26 |
27 |
33 | Outline 34 |
35 |
40 |
41 | 42 |
46 |             background: color
47 |             mask-image: paint(squircle)
48 |             background: color mask-image: paint(squircle)
49 |             --squircle-radius: px
50 |             --squircle-smooth: number
51 |             
52 |               
53 |                 // if you want to use property as background instead of mask
54 |               
55 |             
56 |             background: paint(squircle)
57 |             
58 |               --squircle-fill: color // accept CSS variables
59 |             
60 |           
61 | 62 |
66 |             
67 |               // Outline via background
68 |             
69 |             background: paint(squircle)
70 |             --squircle-radius: px
71 |             --squircle-smooth: number
72 |             --squircle-outline: px
73 |             
74 |               
75 |                 // Outline via{" "}
76 |                 
81 |                   pseudo-element
82 |                 
83 |               
84 |             
85 |           
86 |
87 |
88 |
89 | ); 90 | }; 91 | 92 | export default CSSSection; 93 | -------------------------------------------------------------------------------- /src/sections/CSSSection/style.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/mixins"; 2 | 3 | .wrapWidth { 4 | border-top: 2px solid var(--dark-clr); 5 | border-bottom: 2px solid var(--dark-clr); 6 | width: 100%; 7 | } 8 | 9 | .wrap { 10 | max-width: var(--max-width); 11 | display: flex; 12 | width: 100%; 13 | margin: 0 auto; 14 | @media only screen and (max-width: 1100px) { 15 | flex-direction: column; 16 | } 17 | } 18 | 19 | .title { 20 | width: 50%; 21 | max-width: 600px; 22 | border-right: 2px solid var(--dark-clr); 23 | padding: var(--vertical-paddings) var(--horizontal-paddings); 24 | @media only screen and (max-width: 1100px) { 25 | width: 100%; 26 | border-right: none; 27 | padding-bottom: 0; 28 | } 29 | h2 { 30 | @include h2; 31 | } 32 | } 33 | 34 | .content { 35 | width: 100%; 36 | padding: var(--vertical-paddings) var(--horizontal-paddings); 37 | } 38 | 39 | .switcher { 40 | position: relative; 41 | display: flex; 42 | padding: 8px; 43 | margin-bottom: var(--padding-m); 44 | @media only screen and (max-width: 620px) { 45 | margin-bottom: var(--padding-s); 46 | } 47 | &:after { 48 | content: ""; 49 | position: absolute; 50 | top: 0; 51 | left: 0; 52 | pointer-events: none; 53 | display: inline-block; 54 | width: 100%; 55 | height: 100%; 56 | background: var(--dark-clr); 57 | mask-image: paint(squircle); 58 | --squircle-smooth: 0.8; 59 | --squircle-outline: 2px; 60 | } 61 | &Button { 62 | z-index: 1; 63 | cursor: pointer; 64 | flex: 0 0 50%; 65 | text-align: center; 66 | color: var(--dark-clr); 67 | padding: 16px; 68 | font-family: "Druck"; 69 | text-transform: uppercase; 70 | font-size: 32px; 71 | transition: all 0.2s ease; 72 | &:hover { 73 | text-decoration: underline; 74 | } 75 | @media only screen and (max-width: 1100px) { 76 | font-size: 24px; 77 | } 78 | @media only screen and (max-width: 620px) { 79 | padding: 8px; 80 | font-size: 16px; 81 | } 82 | } 83 | &Back { 84 | position: absolute; 85 | top: 8px; 86 | left: 8px; 87 | height: calc(100% - 16px); 88 | width: 50%; 89 | background: var(--dark-clr); 90 | mask-image: paint(squircle); 91 | --squircle-smooth: 1; 92 | transition: all 300ms cubic-bezier(0.61, -0.15, 0.445, 0.995); 93 | &Active { 94 | transform: translateX(calc(100% - 16px)); 95 | } 96 | } 97 | &Active { 98 | pointer-events: none; 99 | color: var(--light-clr); 100 | &:hover { 101 | text-decoration: none; 102 | } 103 | } 104 | } 105 | 106 | .CSSExample { 107 | display: flex; 108 | flex-direction: column; 109 | color: var(--dark-clr); 110 | overflow: scroll; 111 | // border: 2px solid red; 112 | padding: 40px; 113 | background: paint(squircle); 114 | --squircle-smooth: 0.8; 115 | --squircle-radius: 16px; 116 | --squircle-outline: 2px; 117 | --squircle-fill: var(--dark-clr); 118 | @media only screen and (max-width: 620px) { 119 | padding: 32px; 120 | } 121 | &::-webkit-scrollbar { 122 | display: none; 123 | } 124 | code { 125 | font-family: "Roboto Mono", monospace; 126 | font-size: 16px; 127 | margin: 0; 128 | margin-bottom: var(--padding-xs); 129 | &:last-child { 130 | margin-bottom: 0; 131 | } 132 | } 133 | span { 134 | color: rgba(0, 0, 0, 0.3); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/sections/CanIUse/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | const CanIUse = () => { 5 | return ( 6 |
7 |

8 | Can I{" "} 9 | 14 | use it 15 | {" "} 16 | today? 17 |

18 | 19 |
20 |
21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 |

Chrome, Edge, Opera

36 |
37 | 38 |
39 | 45 | 50 | 51 |

Safari. Enable by flag

52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default CanIUse; 59 | -------------------------------------------------------------------------------- /src/sections/CanIUse/style.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/mixins"; 2 | 3 | .wrap { 4 | flex-wrap: wrap; 5 | max-width: var(--max-width); 6 | display: flex; 7 | width: 100%; 8 | margin: 0 auto; 9 | padding: var(--vertical-paddings) var(--horizontal-paddings); 10 | @media only screen and (max-width: 1100px) { 11 | flex-direction: column; 12 | } 13 | } 14 | 15 | .h2 { 16 | @include h2; 17 | flex: 1; 18 | max-width: 500px; 19 | margin-bottom: var(--padding-m); 20 | margin-right: var(--padding-m); 21 | @media only screen and (max-width: 1100px) { 22 | margin-top: var(--padding-xs); 23 | } 24 | @media only screen and (max-width: 620px) { 25 | margin-bottom: var(--padding-s); 26 | } 27 | } 28 | 29 | .cards { 30 | display: flex; 31 | flex: 2 1; 32 | @media only screen and (max-width: 620px) { 33 | flex-direction: column; 34 | } 35 | } 36 | 37 | .card { 38 | display: flex; 39 | align-items: center; 40 | padding: var(--padding-m); 41 | mask-image: paint(squircle); 42 | margin: var(--padding-xxs) 0; 43 | margin-right: var(--padding-xxs); 44 | --squircle-smooth: 0.4; 45 | --squircle-radius: 22px; 46 | @media only screen and (max-width: 1100px) { 47 | flex-direction: column; 48 | align-items: flex-start; 49 | padding: var(--padding-s); 50 | &:last-child { 51 | margin-right: 0; 52 | } 53 | } 54 | @media only screen and (max-width: 620px) { 55 | justify-content: flex-start; 56 | margin-right: 0; 57 | padding: var(--padding-s) 24px; 58 | } 59 | &Header { 60 | font-family: "Druck"; 61 | color: black; 62 | font-size: 20px; 63 | margin: 0; 64 | @media only screen and (max-width: 1100px) { 65 | margin-top: var(--padding-s); 66 | } 67 | } 68 | &Svg { 69 | flex: none; 70 | height: 46px; 71 | margin-right: 32px; 72 | @media only screen and (max-width: 620px) { 73 | margin-right: 16px; 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/sections/CompareSection/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | import Range from "../../components/Range"; 5 | 6 | const CompareSection = () => { 7 | const [radius, setRadius] = React.useState(80); 8 | const [smooth, setSmooth] = React.useState(1); 9 | 10 | return ( 11 |
12 |
13 |
14 | { 21 | setRadius(Number(e.target.value)); 22 | }} 23 | /> 24 | { 32 | setSmooth(Number(e.target.value)); 33 | }} 34 | /> 35 |
36 |
37 |
43 |
52 |
53 |
54 |
55 | ); 56 | }; 57 | 58 | export default CompareSection; 59 | -------------------------------------------------------------------------------- /src/sections/CompareSection/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrapWidth { 2 | border-top: 2px solid var(--dark-clr); 3 | width: 100%; 4 | @media only screen and (max-width: 1100px) { 5 | display: none; 6 | } 7 | } 8 | 9 | .wrap { 10 | max-width: var(--max-width); 11 | display: flex; 12 | flex-wrap: wrap; 13 | align-items: center; 14 | width: 100%; 15 | margin: 0 auto; 16 | overflow: hidden; 17 | padding: var(--vertical-paddings) var(--horizontal-paddings) 0; 18 | } 19 | 20 | .compare { 21 | position: relative; 22 | flex: 2; 23 | height: 300px; 24 | &Value { 25 | margin-bottom: 32px; 26 | font-family: "Roboto Mono", monospace; 27 | color: var(--dark-clr); 28 | font-size: 16px; 29 | span { 30 | opacity: 0.4; 31 | } 32 | } 33 | } 34 | 35 | .standart { 36 | position: absolute; 37 | top: 0; 38 | left: 50%; 39 | transform: translateX(-50%); 40 | width: 80%; 41 | height: 600px; 42 | color: var(--light-clr); 43 | // background: rgba(255, 66, 66, 1); 44 | border: 2px solid #f45; 45 | } 46 | 47 | .squircle { 48 | position: absolute; 49 | top: 0; 50 | left: 50%; 51 | transform: translateX(-50%); 52 | --squircle-radius: 24px; 53 | --squircle-smooth: 0.9; 54 | --squircle-outline: 2px; 55 | width: 80%; 56 | height: 600px; 57 | color: var(--light-clr); 58 | background: var(--dark-clr); 59 | mask-image: paint(squircle); 60 | } 61 | 62 | .controls { 63 | display: flex; 64 | flex: 1; 65 | align-items: center; 66 | flex-direction: column; 67 | margin-right: 40px; 68 | margin-bottom: 40px; 69 | @media only screen and (max-width: 620px) { 70 | width: 100%; 71 | margin: 32px 12px 24px; 72 | } 73 | } 74 | 75 | .range { 76 | width: 100%; 77 | margin-bottom: 64px; 78 | 79 | &:last-child { 80 | margin-bottom: 24px; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/sections/Footer/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | const Footer = () => { 5 | return ( 6 |
7 |
8 |

9 | Notice a bug, want to improve the module or have an idea? 10 |

11 |

12 | Let me know{" "} 13 | 18 | {" "} 19 | in the GitHub issues 20 | {" "} 21 | or send a pull request 22 |

23 |
24 |
25 | ); 26 | }; 27 | 28 | export default Footer; 29 | -------------------------------------------------------------------------------- /src/sections/Footer/style.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/mixins"; 2 | 3 | .wrapWidth { 4 | width: 100%; 5 | background-color: var(--dark-clr); 6 | } 7 | 8 | .wrap { 9 | max-width: var(--max-width); 10 | display: flex; 11 | flex-direction: column; 12 | width: 100%; 13 | margin: 0 auto; 14 | padding: var(--vertical-paddings) var(--horizontal-paddings); 15 | // align-items: center; 16 | } 17 | 18 | .title { 19 | // font-family: "Druck"; 20 | font-family: "Roboto Mono", monospace; 21 | font-size: 16px; 22 | margin: 8px 0; 23 | color: var(--light-clr); 24 | // text-align: center; 25 | } 26 | 27 | .subtitle { 28 | // font-family: "Druck"; 29 | font-family: "Roboto Mono", monospace; 30 | font-size: 16px; 31 | margin: 8px 0; 32 | color: var(--light-clr); 33 | // text-align: center; 34 | } 35 | -------------------------------------------------------------------------------- /src/sections/HowItWorksSection/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | import Range from "../../components/Range"; 5 | 6 | const HowItWorksSection = () => { 7 | const canvasRef = React.useRef(null); 8 | const [radius, setRadius] = React.useState(80); 9 | const [smooth, setSmooth] = React.useState(4); 10 | 11 | React.useEffect(() => { 12 | const canvas = canvasRef.current as HTMLCanvasElement | null; 13 | 14 | if (!canvas) { 15 | return; 16 | } 17 | 18 | const ctx = canvas.getContext("2d"); 19 | 20 | if (!ctx) { 21 | return; 22 | } 23 | 24 | const drawBezLine = ( 25 | color: string, 26 | pointOne: { 27 | x: number; 28 | y: number; 29 | }, 30 | PointTwo: { 31 | x: number; 32 | y: number; 33 | } 34 | ) => { 35 | ctx.strokeStyle = color; 36 | ctx.lineWidth = 2; 37 | ctx.beginPath(); 38 | ctx.moveTo(pointOne.x, pointOne.y); 39 | ctx.lineTo(PointTwo.x, PointTwo.y); 40 | ctx.stroke(); 41 | }; 42 | 43 | const drawDot = (color: string, x: number, y: number) => { 44 | ctx.fillStyle = color; 45 | ctx.beginPath(); 46 | ctx.arc(x, y, 6, 0, 2 * Math.PI); 47 | ctx.closePath(); 48 | ctx.fill(); 49 | }; 50 | 51 | const draw = (containerWidth: number) => { 52 | let geom = { width: containerWidth / 1.4, height: 600 }; 53 | let shiftFigure = { 54 | x: (containerWidth - containerWidth / 1.4) / 2, 55 | y: 20 56 | }; 57 | 58 | /////////////////////// 59 | // BEZIER DOTS 60 | let oneBezPoint = { 61 | x: shiftFigure.x, 62 | y: radius / smooth + shiftFigure.y 63 | }; 64 | let twoBezPoint = { 65 | x: radius / smooth + shiftFigure.x, 66 | y: shiftFigure.y 67 | }; 68 | let threeBezPoint = { 69 | x: geom.width - radius / smooth + shiftFigure.x, 70 | y: shiftFigure.y 71 | }; 72 | let fourBezPoint = { 73 | x: geom.width + shiftFigure.x, 74 | y: radius / smooth + shiftFigure.y 75 | }; 76 | 77 | // RECT DOTS 78 | let oneRectDot = { x: shiftFigure.x, y: radius + shiftFigure.y }; 79 | let twoRectDot = { x: radius + shiftFigure.x, y: shiftFigure.y }; 80 | let threeRectDot = { 81 | x: geom.width - radius + shiftFigure.x, 82 | y: shiftFigure.y 83 | }; 84 | let fourRectDot = { 85 | x: geom.width + shiftFigure.x, 86 | y: radius + shiftFigure.y 87 | }; 88 | 89 | /////////////////////// 90 | // DRAW BEZIER LINE 91 | let bezLineColor = "#f45"; 92 | drawBezLine(bezLineColor, oneBezPoint, oneRectDot); 93 | drawBezLine(bezLineColor, twoBezPoint, twoRectDot); 94 | drawBezLine(bezLineColor, threeBezPoint, threeRectDot); 95 | drawBezLine(bezLineColor, fourBezPoint, fourRectDot); 96 | 97 | /////////////////////// 98 | // DRAW BEZIER CONTROL DOTS 99 | let bezDotColor = "#7F7CFF"; 100 | drawDot(bezDotColor, oneBezPoint.x, oneBezPoint.y); 101 | drawDot(bezDotColor, twoBezPoint.x, twoBezPoint.y); 102 | drawDot(bezDotColor, threeBezPoint.x, threeBezPoint.y); 103 | drawDot(bezDotColor, fourBezPoint.x, fourBezPoint.y); 104 | 105 | /////////////////////// 106 | // DRAW RECTANGLE 107 | // OPEN LEFT-TOP CORNER 108 | ctx.beginPath(); 109 | ctx.lineTo(radius + shiftFigure.x, shiftFigure.y); 110 | // TOP-RIGHT CORNER 111 | ctx.lineTo(geom.width - radius + shiftFigure.x, shiftFigure.y); 112 | ctx.bezierCurveTo( 113 | geom.width - radius / smooth + shiftFigure.x, 114 | shiftFigure.y, // first bezier point 115 | geom.width + shiftFigure.x, 116 | radius / smooth + shiftFigure.y, // second bezier point 117 | geom.width + shiftFigure.x, 118 | radius + shiftFigure.y // last connect point 119 | ); 120 | // BOTTOM-RIGHT CORNER 121 | ctx.lineTo( 122 | geom.width + shiftFigure.x, 123 | geom.height - radius + shiftFigure.y 124 | ); 125 | ctx.bezierCurveTo( 126 | geom.width + shiftFigure.x, 127 | geom.height - radius / smooth + shiftFigure.y, // first bezier point 128 | geom.width - radius / smooth + shiftFigure.x, 129 | geom.height + shiftFigure.y, // second bezier point 130 | geom.width - radius + shiftFigure.x, 131 | geom.height + shiftFigure.y // last connect point 132 | ); 133 | // BOTTOM-LEFT CORNER 134 | ctx.lineTo(radius + shiftFigure.x, geom.height + shiftFigure.y); 135 | ctx.bezierCurveTo( 136 | radius / smooth + shiftFigure.x, 137 | geom.height + shiftFigure.y, // first bezier point 138 | shiftFigure.x, 139 | geom.height - radius / smooth + shiftFigure.y, // second bezier point 140 | shiftFigure.x, 141 | geom.height - radius + shiftFigure.y // last connect point 142 | ); 143 | // CLOSE LEFT-TOP CORNER 144 | ctx.lineTo(shiftFigure.x, radius + shiftFigure.y); 145 | ctx.bezierCurveTo( 146 | shiftFigure.x, 147 | radius / smooth + shiftFigure.y, // first bezier point 148 | radius / smooth + shiftFigure.x, 149 | shiftFigure.y, // second bezier point 150 | radius + shiftFigure.x, 151 | shiftFigure.y // last connect point 152 | ); 153 | ctx.closePath(); 154 | ctx.lineWidth = 2; 155 | ctx.strokeStyle = "rgba(0,0,0,1"; 156 | ctx.stroke(); 157 | 158 | /////////////////////// 159 | // DRAW RECTANGLE CONTROL DOTS 160 | let rectDotColor = "#f45"; 161 | drawDot(rectDotColor, oneRectDot.x, oneRectDot.y); 162 | drawDot(rectDotColor, twoRectDot.x, twoRectDot.y); 163 | drawDot(rectDotColor, threeRectDot.x, threeRectDot.y); 164 | drawDot(rectDotColor, fourRectDot.x, fourRectDot.y); 165 | }; 166 | 167 | const resizeCanvas = () => { 168 | let parent = canvas.parentNode?.parentNode; 169 | 170 | if (parent) { 171 | let styles = getComputedStyle(parent as HTMLElement); 172 | let w = parseInt(styles.getPropertyValue("width"), 10); 173 | 174 | canvas.width = w / 2; 175 | canvas.height = 360; 176 | if (window.innerWidth < 1100) { 177 | canvas.width = w; 178 | canvas.height = 240; 179 | } 180 | draw(canvas.width); 181 | } 182 | }; 183 | 184 | resizeCanvas(); 185 | 186 | window.addEventListener("resize", resizeCanvas, false); 187 | }, [radius, smooth]); 188 | 189 | return ( 190 |
191 |
192 |
193 | 194 |
195 |
196 |

How it works

197 |
198 | { 204 | setRadius(Number(e.target.value)); 205 | }} 206 | /> 207 | { 214 | setSmooth(Number(e.target.value)); 215 | }} 216 | /> 217 |
218 |
219 |
220 |
221 | ); 222 | }; 223 | 224 | export default HowItWorksSection; 225 | -------------------------------------------------------------------------------- /src/sections/HowItWorksSection/style.module.scss: -------------------------------------------------------------------------------- 1 | @import "../../styles/mixins"; 2 | 3 | .wrapWidth { 4 | border-top: 2px solid var(--dark-clr); 5 | width: 100%; 6 | border-bottom: 2px solid var(--dark-clr); 7 | @media only screen and (max-width: 620px) { 8 | border-top: none; 9 | } 10 | } 11 | 12 | .wrap { 13 | max-width: var(--max-width); 14 | display: flex; 15 | width: 100%; 16 | margin: 0 auto; 17 | padding: 0 var(--horizontal-paddings); 18 | align-items: center; 19 | @media only screen and (max-width: 1100px) { 20 | flex-direction: column-reverse; 21 | } 22 | } 23 | 24 | .canvasWrap { 25 | display: flex; 26 | flex: 1; 27 | align-self: flex-end; 28 | @media only screen and (max-width: 1100px) { 29 | align-self: unset; 30 | } 31 | } 32 | 33 | .content { 34 | flex: 1; 35 | width: 100%; 36 | padding: var(--vertical-paddings) 0; 37 | @media only screen and (max-width: 1100px) { 38 | padding-bottom: 0; 39 | } 40 | } 41 | 42 | .h2 { 43 | @include h2; 44 | margin-bottom: var(--padding-m); 45 | @media only screen and (max-width: 1100px) { 46 | margin-top: 24px; 47 | } 48 | } 49 | 50 | .controls { 51 | display: flex; 52 | flex-direction: column; 53 | margin-bottom: 32px; 54 | max-width: 450px; 55 | @media only screen and (max-width: 1100px) { 56 | max-width: none; 57 | margin-bottom: 0; 58 | } 59 | } 60 | 61 | .range { 62 | margin-bottom: 32px; 63 | } 64 | -------------------------------------------------------------------------------- /src/sections/Intro/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | import Range from "../../components/Range"; 5 | import Button from "../../components/Button"; 6 | 7 | const Intro = () => { 8 | const [radius, setRadius] = React.useState(60); 9 | const [smooth, setSmooth] = React.useState(0.8); 10 | const [outline, setOutline] = React.useState(2); 11 | 12 | const Logo = () => { 13 | return ( 14 |
15 |
16 |
26 |
36 | 41 | 45 | 46 |
47 | ); 48 | }; 49 | 50 | return ( 51 |
52 |
53 | 54 |
55 |

A lightweight CSS module for squircles

56 | {/*

A lightweight CSS module squircles as a custom property

*/} 57 |
58 |
59 |
72 |
73 |
74 |
84 |
85 | setOutline(Number(e.target.value))} 88 | value={outline.toString()} 89 | tooltip={`Outline: ${outline}px`} 90 | step={1} 91 | min={1} 92 | max={10} 93 | /> 94 | setRadius(Number(e.target.value))} 97 | value={radius.toString()} 98 | tooltip={`Radius: ${radius}px`} 99 | max={200} 100 | /> 101 | setSmooth(Number(e.target.value))} 104 | value={smooth.toString()} 105 | tooltip={`Smooth: ${smooth}`} 106 | step={0.1} 107 | min={0.1} 108 | max={1} 109 | /> 110 |
111 |
112 |
113 |
114 | ); 115 | }; 116 | 117 | export default Intro; 118 | -------------------------------------------------------------------------------- /src/sections/Intro/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | position: relative; 3 | display: flex; 4 | max-width: var(--max-width); 5 | width: 100%; 6 | padding: var(--vertical-paddings) var(--horizontal-paddings); 7 | @media only screen and (max-width: 1100px) { 8 | flex-direction: column; 9 | } 10 | } 11 | 12 | .logo { 13 | position: relative; 14 | @media only screen and (max-width: 1100px) { 15 | max-width: 500px; 16 | } 17 | &QLine { 18 | position: absolute; 19 | width: 6%; 20 | height: 20%; 21 | background: var(--dark-clr); 22 | left: 49%; 23 | top: 32%; 24 | transform: rotate(-19deg); 25 | } 26 | &QInner { 27 | position: absolute; 28 | top: -7%; 29 | left: 35%; 30 | width: 19%; 31 | height: 34%; 32 | background: var(--dark-clr); 33 | mask-image: paint(squircle); 34 | --squircle-outline: 2px; 35 | } 36 | &QOuter { 37 | position: absolute; 38 | left: 28%; 39 | width: 33%; 40 | height: 42%; 41 | background: var(--dark-clr); 42 | mask-image: paint(squircle); 43 | --squircle-outline: 2px; 44 | } 45 | } 46 | 47 | .about { 48 | flex: 1; 49 | display: flex; 50 | flex-direction: column; 51 | justify-content: center; 52 | margin-right: var(--padding-m); 53 | @media only screen and (max-width: 1100px) { 54 | margin-right: 0; 55 | } 56 | } 57 | 58 | .description { 59 | width: 100%; 60 | max-width: 600px; 61 | margin-top: var(--padding-m); 62 | display: flex; 63 | h4 { 64 | max-width: 400px; 65 | flex: 1; 66 | margin: 0; 67 | font-family: "Druck", sans-serif; 68 | font-size: 24px; 69 | color: var(--dark-clr); 70 | @media only screen and (max-width: 620px) { 71 | font-size: 20px; 72 | } 73 | } 74 | } 75 | 76 | .buttons { 77 | display: flex; 78 | gap: 8px; 79 | margin-top: var(--padding-l); 80 | @media only screen and (max-width: 1100px) { 81 | margin-top: var(--padding-m); 82 | } 83 | @media only screen and (max-width: 620px) { 84 | flex-direction: column; 85 | } 86 | } 87 | 88 | // 89 | 90 | .squircleWrap { 91 | flex: 1; 92 | display: flex; 93 | align-items: center; 94 | justify-content: center; 95 | margin-left: var(--padding-m); 96 | @media only screen and (max-width: 1100px) { 97 | margin-left: 0; 98 | margin-top: var(--padding-m); 99 | margin-bottom: var(--padding-s); 100 | } 101 | } 102 | 103 | .squircle { 104 | position: relative; 105 | width: 100%; 106 | max-width: 550px; 107 | height: 550px; 108 | padding: 40px; 109 | display: flex; 110 | justify-content: center; 111 | align-items: center; 112 | background: var(--light-clr); 113 | mask-image: paint(squircle); 114 | @media only screen and (max-width: 1100px) { 115 | max-width: none; 116 | height: 300px; 117 | } 118 | &:after { 119 | pointer-events: none; 120 | content: ""; 121 | position: absolute; 122 | display: inline-block; 123 | width: 100%; 124 | height: 100%; 125 | background: var(--dark-clr); 126 | mask-image: paint(squircle); 127 | --squircle-outline: var(--intro-outline-width); 128 | } 129 | } 130 | 131 | .rangeWrap { 132 | width: 100%; 133 | max-width: 300px; 134 | @media only screen and (max-width: 1100px) { 135 | margin-top: 32px; 136 | max-width: none; 137 | } 138 | } 139 | 140 | .range { 141 | width: 100%; 142 | margin-bottom: 64px; 143 | @media only screen and (max-width: 1100px) { 144 | margin-bottom: 56px; 145 | } 146 | &:last-child { 147 | margin-bottom: 0; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/sections/Scalable/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | const Scalable = () => { 5 | const [mousePos, setMousePos] = React.useState({ x: 400, y: 400 }); 6 | 7 | const handleMouseMove = (e: MouseEvent) => { 8 | setMousePos({ x: e.screenX, y: e.screenY }); 9 | }; 10 | 11 | React.useEffect(() => { 12 | window.addEventListener("mousemove", handleMouseMove); 13 | 14 | return () => { 15 | window.removeEventListener("mousemove", handleMouseMove); 16 | }; 17 | }, []); 18 | 19 | return ( 20 |
21 |
32 | 37 | SC 38 | 39 |
40 |
51 | 52 | AL 53 | 54 |
55 |
66 | 67 | AB 68 | 69 |
70 |
81 | 82 | LE 83 | 84 |
85 |
86 | ); 87 | }; 88 | 89 | export default Scalable; 90 | -------------------------------------------------------------------------------- /src/sections/Scalable/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | max-width: var(--max-width); 3 | display: flex; 4 | align-items: center; 5 | width: 100%; 6 | margin: 0 auto; 7 | overflow: hidden; 8 | height: 480px; 9 | padding: var(--vertical-paddings) var(--horizontal-paddings); 10 | @media only screen and (max-width: 620px) { 11 | display: none; 12 | } 13 | } 14 | 15 | .block { 16 | font-family: "Druck", sans-serif; 17 | font-size: 20px; 18 | color: var(--light-clr); 19 | background: var(--dark-clr); 20 | padding: var(--padding-s); 21 | background: var(--dark-clr); 22 | mask-image: paint(squircle); 23 | margin: var(--padding-xxs); 24 | --squircle-smooth: 0.4; 25 | --squircle-radius: 22px; 26 | display: flex; 27 | justify-content: center; 28 | align-items: center; 29 | max-height: 300px; 30 | } 31 | -------------------------------------------------------------------------------- /src/sections/TestSection/index.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styles from "./style.module.scss"; 3 | 4 | import Range from "../../components/Range"; 5 | import Button from "../../components/Button"; 6 | 7 | const TestSection = () => { 8 | return ( 9 |
10 |
11 |
12 | ); 13 | }; 14 | 15 | export default TestSection; 16 | -------------------------------------------------------------------------------- /src/sections/TestSection/style.module.scss: -------------------------------------------------------------------------------- 1 | .wrap { 2 | position: relative; 3 | display: flex; 4 | max-width: var(--max-width); 5 | width: 100%; 6 | padding: var(--vertical-paddings) var(--horizontal-paddings); 7 | border-bottom: 2px solid var(--dark-clr); 8 | } 9 | 10 | .squircle { 11 | width: 300px; 12 | height: 300px; 13 | background: paint(squircle); 14 | // --squircle-fill: rgba(0, 0, 0, 0.1); 15 | // mask-image: paint(squircle); 16 | // --squircle-outline: 2px; 17 | // --squircle-radius: 40px; 18 | --squircle-radius-top-left: 40px; 19 | --squircle-radius-top-right: 10px; 20 | } 21 | -------------------------------------------------------------------------------- /src/styles/_mixins.scss: -------------------------------------------------------------------------------- 1 | @mixin h2 { 2 | font-family: "Druck"; 3 | color: var(--dark-clr); 4 | text-transform: uppercase; 5 | font-size: 46px; 6 | margin-bottom: 0; 7 | @media only screen and (max-width: 1100px) { 8 | font-size: 32px; 9 | } 10 | @media only screen and (max-width: 620px) { 11 | font-size: 24px; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/styles/fonts/druk-wide-bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PavelLaptev/css-houdini-squircle/3a8b3dc1c219fe2c5715dc8dccdd4f865f0a2988/src/styles/fonts/druk-wide-bold.woff2 -------------------------------------------------------------------------------- /src/styles/fonts/fonts.scss: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Druck"; 3 | font-weight: bolder; 4 | font-style: normal; 5 | src: url(./druk-wide-bold.woff2) format("woff2"); // Am Ende das Schlusslicht für IE11 6 | } 7 | -------------------------------------------------------------------------------- /src/styles/globals.scss: -------------------------------------------------------------------------------- 1 | @import "./normilize.css"; 2 | @import url("https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"); 3 | @import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap"); 4 | @import "./fonts/fonts.scss"; 5 | 6 | :root { 7 | --min-width: 500px; 8 | --max-width: 1600px; 9 | // 10 | --padding-xl: 100px; 11 | --padding-l: 80px; 12 | --padding-m: 40px; 13 | --padding-s: 20px; 14 | --padding-xs: 16px; 15 | --padding-xxs: 8px; 16 | // 17 | --vertical-paddings: var(--padding-xl); 18 | --horizontal-paddings: var(--padding-l); 19 | // 20 | --main-clr: #ebe1ff; 21 | --dark-clr: #000; 22 | --light-clr: #fff; 23 | --accent-clr: #5f0fff; 24 | // 25 | --base-radius: 60px; 26 | // 27 | @media only screen and (max-width: 1100px) { 28 | --vertical-paddings: var(--padding-m); 29 | --horizontal-paddings: var(--padding-m); 30 | } 31 | @media only screen and (max-width: 620px) { 32 | --vertical-paddings: var(--padding-s); 33 | --horizontal-paddings: var(--padding-s); 34 | } 35 | } 36 | 37 | *, 38 | *::after, 39 | *::before { 40 | box-sizing: border-box; 41 | } 42 | 43 | *::selection { 44 | color: var(--light-clr); 45 | background: var(--dark-clr); 46 | } 47 | 48 | body { 49 | -webkit-font-smoothing: antialiased; 50 | -moz-osx-font-smoothing: grayscale; 51 | font-family: "Roboto", sans-serif; 52 | background: var(--main-clr); 53 | overflow-x: hidden; 54 | } 55 | 56 | a { 57 | color: #f45; 58 | &:hover { 59 | text-decoration: none; 60 | } 61 | } 62 | 63 | pre { 64 | white-space: pre-wrap; 65 | } 66 | -------------------------------------------------------------------------------- /src/styles/normilize.css: -------------------------------------------------------------------------------- 1 | /* html5doctor.com Reset Stylesheet (Eric Meyer's Reset Reloaded + HTML5 baseline) v1.4 2009-07-27 | Authors: Eric Meyer & Richard Clark html5doctor.com/html-5-reset-stylesheet/*/ 2 | html, 3 | body, 4 | div, 5 | span, 6 | object, 7 | iframe, 8 | h1, 9 | h2, 10 | h3, 11 | h4, 12 | h5, 13 | h6, 14 | p, 15 | blockquote, 16 | pre, 17 | abbr, 18 | address, 19 | cite, 20 | code, 21 | del, 22 | dfn, 23 | em, 24 | img, 25 | ins, 26 | kbd, 27 | q, 28 | samp, 29 | small, 30 | strong, 31 | sub, 32 | sup, 33 | var, 34 | b, 35 | i, 36 | dl, 37 | dt, 38 | dd, 39 | ol, 40 | ul, 41 | li, 42 | fieldset, 43 | form, 44 | label, 45 | legend, 46 | table, 47 | caption, 48 | tbody, 49 | tfoot, 50 | thead, 51 | tr, 52 | th, 53 | td, 54 | article, 55 | aside, 56 | figure, 57 | footer, 58 | header, 59 | hgroup, 60 | menu, 61 | nav, 62 | section, 63 | menu, 64 | time, 65 | mark, 66 | audio, 67 | video { 68 | margin: 0; 69 | padding: 0; 70 | border: 0; 71 | outline: 0; 72 | font-size: 100%; 73 | vertical-align: baseline; 74 | } 75 | 76 | article, 77 | aside, 78 | figure, 79 | footer, 80 | header, 81 | hgroup, 82 | nav, 83 | section { 84 | display: block; 85 | } 86 | 87 | nav ul { 88 | list-style: none; 89 | } 90 | 91 | blockquote, 92 | q { 93 | quotes: none; 94 | } 95 | 96 | blockquote:before, 97 | blockquote:after, 98 | q:before, 99 | q:after { 100 | content: ""; 101 | content: none; 102 | } 103 | 104 | a { 105 | margin: 0; 106 | padding: 0; 107 | font-size: 100%; 108 | vertical-align: baseline; 109 | } 110 | 111 | ins { 112 | background-color: #ff9; 113 | color: #000; 114 | text-decoration: none; 115 | } 116 | 117 | mark { 118 | background-color: #ff9; 119 | color: #000; 120 | font-style: italic; 121 | font-weight: bold; 122 | } 123 | 124 | del { 125 | text-decoration: line-through; 126 | } 127 | 128 | abbr[title], 129 | dfn[title] { 130 | border-bottom: 1px dotted #000; 131 | cursor: help; 132 | } 133 | 134 | /* tables still need cellspacing="0" in the markup */ 135 | table { 136 | border-collapse: collapse; 137 | border-spacing: 0; 138 | } 139 | 140 | hr { 141 | display: block; 142 | height: 1px; 143 | border: 0; 144 | border-top: 1px solid #ccc; 145 | margin: 1em 0; 146 | padding: 0; 147 | } 148 | 149 | input, 150 | select { 151 | vertical-align: middle; 152 | } 153 | /* END RESET CSS */ 154 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "paths": { 18 | "@/*": ["./src/*"] 19 | } 20 | }, 21 | "include": [ 22 | "next-env.d.ts", 23 | "**/*.ts", 24 | "**/*.tsx", 25 | "lib/paintWorklet/squircle.txs" 26 | ], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------