├── .browserslistrc ├── .gitattributes ├── .github ├── FUNDING.yml ├── images │ ├── cta.png │ ├── cta6.png │ └── example.png └── workflows │ ├── auto-assign.yml │ ├── cd.yml │ ├── codeql-analysis.yml │ ├── lockfile.yml │ └── markdownlint.yml ├── .gitignore ├── .mdlrc ├── .npmignore ├── .releaserc ├── .vscode ├── extensions.json └── settings.json ├── .yarnrc.yml ├── LICENSE ├── README.md ├── index.html ├── package.json ├── paint.js ├── sandbox.config.json └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 2 years 2 | not < 0.05% 3 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | index.html linguist-vendored 2 | .yarn/ export-ignore 3 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: wopian 2 | ko_fi: wopian 3 | custom: 'https://paypal.me/wopian' 4 | -------------------------------------------------------------------------------- /.github/images/cta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wopian/smooth-corners/f237921e1d62c83d99c8290431ec85d2b1ffeb04/.github/images/cta.png -------------------------------------------------------------------------------- /.github/images/cta6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wopian/smooth-corners/f237921e1d62c83d99c8290431ec85d2b1ffeb04/.github/images/cta6.png -------------------------------------------------------------------------------- /.github/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wopian/smooth-corners/f237921e1d62c83d99c8290431ec85d2b1ffeb04/.github/images/example.png -------------------------------------------------------------------------------- /.github/workflows/auto-assign.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign 2 | on: 3 | issues: 4 | types: [opened] 5 | jobs: 6 | auto-assign: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: 'Auto Assign Issues' 10 | uses: pozil/auto-assign-issue@v1 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | assignees: wopian 14 | -------------------------------------------------------------------------------- /.github/workflows/cd.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [develop] 6 | workflow_dispatch: 7 | branches: [develop] 8 | 9 | env: 10 | FORCE_COLOR: true 11 | NODE_VERSION: 18 12 | 13 | jobs: 14 | setup: 15 | name: Deploy Package 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - uses: actions/checkout@v4 20 | 21 | - uses: actions/setup-node@v4 22 | with: 23 | node-version: ${{ env.NODE_VERSION }} 24 | 25 | - name: Get yarn cache directory path 26 | id: yarn-cache-dir-path 27 | run: echo "dir=$(yarn config get cacheFolder)" >> $GITHUB_OUTPUT 28 | 29 | - uses: actions/cache@v3 30 | id: yarn-cache # use this to check for `cache-hit` (`steps.yarn-cache.outputs.cache-hit != 'true'`) 31 | with: 32 | path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 33 | key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 34 | restore-keys: | 35 | ${{ runner.os }}-yarn- 36 | 37 | - name: Install Dependencies 38 | run: yarn install --immutable 39 | 40 | - name: Build Package 41 | run: yarn build 42 | 43 | - name: Create Release 44 | if: success() && github.ref == 'refs/heads/develop' 45 | run: yarn semantic-release 46 | env: 47 | GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} 48 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 49 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ master ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ master ] 20 | schedule: 21 | - cron: '0 6 * * 1' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'javascript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://git.io/codeql-language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v4 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v3 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below) 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@v3 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 62 | # and modify them (or add more) to build your code if your project 63 | # uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@v3 71 | -------------------------------------------------------------------------------- /.github/workflows/lockfile.yml: -------------------------------------------------------------------------------- 1 | name: Yarn 2 | on: 3 | pull_request: 4 | branches: 5 | - renovate/** 6 | jobs: 7 | yarn_lock_changes: 8 | name: Lock File Changes 9 | runs-on: ubuntu-latest 10 | # Permission overwrite is required for Dependabot PRs 11 | permissions: 12 | pull-requests: write 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | - name: Digest Lock File 17 | uses: Simek/yarn-lock-changes@23b5437388098454b9d9c1574b50066b3338dbf1 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | collapsibleThreshold: 25 21 | failOnDowngrade: false 22 | path: yarn.lock 23 | updateComment: true 24 | -------------------------------------------------------------------------------- /.github/workflows/markdownlint.yml: -------------------------------------------------------------------------------- 1 | name: Lint Markdown 2 | on: [push, pull_request] 3 | jobs: 4 | markdownlint: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: Check out code 8 | uses: actions/checkout@v4 9 | - name: Run mdl 10 | uses: actionshub/markdownlint@v3.1.4 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | 4 | .yarn/cache 5 | .yarn/unplugged 6 | .yarn/sdks 7 | .yarn/*.gz 8 | .pnp.cjs 9 | .pnp.loader.mjs 10 | -------------------------------------------------------------------------------- /.mdlrc: -------------------------------------------------------------------------------- 1 | rules '~MD002', '~MD013', '~MD033' 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .github 2 | .mdlrc 3 | .browserslistrc 4 | example.png 5 | index.html 6 | sandbox.config.json 7 | yarn.lock 8 | .yarn 9 | .vscode 10 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | branches: 2 | - develop 3 | debug: true 4 | ci: true 5 | dryRun: false 6 | plugins: 7 | - "@semantic-release/commit-analyzer" 8 | - "@semantic-release/release-notes-generator" 9 | - "@semantic-release/changelog" 10 | - "@semantic-release/github" 11 | - "@semantic-release/npm" 12 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "arcanis.vscode-zipfs", 4 | "dbaeumer.vscode-eslint", 5 | "esbenp.prettier-vscode" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "search.exclude": { 4 | "**/.yarn": true, 5 | "**/.pnp.*": true 6 | }, 7 | "markiscodecoverage.searchCriteria": "coverage/lcov.info" 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | cloneConcurrency: 8 2 | defaultSemverRangePrefix: '~' 3 | enableScripts: false 4 | enableTelemetry: false 5 | httpRetry: 3 6 | httpTimeout: 60000 7 | nmMode: classic 8 | nodeLinker: node-modules 9 | plugins: 10 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 11 | spec: '@yarnpkg/plugin-workspace-tools' 12 | yarnPath: .yarn/releases/yarn-4.9.1.cjs 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 James Harris 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Smooth Corners

2 | 3 |

4 | npm 5 | npm 6 | bundlephobia 7 |

8 | 9 |

10 | checks 11 | 12 | devDeps 13 | sponsor 14 |

15 | 16 |

Superellipse masks using the CSS Houdini API

17 | 18 | ![Static demo of Smooth Corners][CTA] 19 | 20 | ## Demo 21 | 22 | [Live demo](https://wopian.github.io/smooth-corners/) featuring several different `--smooth-corners` values and an interactive editor 23 | 24 | ## Limitations 25 | 26 | To avoid leaking visited sites, the CSS Paint API is disabled on Chromium-based browsers for `` elements with an `href` attribute and all children of that element. For further details see the following: 27 | 28 | - The CSS Painting API [Privacy Considerations section](https://drafts.css-houdini.org/css-paint-api/#privacy-considerations) 29 | - The CSS Painting API spec issue [“CSS Paint API leaks browsing history”](https://github.com/w3c/css-houdini-drafts/issues/791) 30 | 31 | To work around this limitation, `mask-image: paint(smooth-corners)` can be applied to the parent element of the `` element, for example: 32 | 33 | ```html 34 |
35 | Smooth Corners 36 |
37 | ``` 38 | 39 | ## Usage 40 | 41 | ### CSS 42 | 43 | Add `mask-image: paint(smooth-corners)` to the elements you want to mask 44 | 45 | #### Default (Squircle) 46 | 47 | ```css 48 | .squircle { 49 | mask-image: paint(smooth-corners); 50 | -webkit-mask-image: paint(smooth-corners); 51 | background: #d01257; /* So you can see it */ 52 | } 53 | ``` 54 | 55 | #### Customise Curvature 56 | 57 | You can customise the mask curvature by using a CSS variable. This can be scoped locally to the selector or defined globally in `:root {}` 58 | 59 | `--smooth-corners: X[, Y]` 60 | 61 | - **X** - Float, Curvature of the X axis 62 | - **Y** - Float, Curvature of the Y axis (optional, defaults to X axis) 63 | 64 | ##### Shapes by **X** value 65 | 66 | - `0.6` - [Astroid] 67 | - `< 1` - Concave rhombus 68 | - `= 1` - Rhombus 69 | - `> 1 and < 2` - Convex rhombus 70 | - `= 2` - Circle 71 | - `> 2` - Rounded rectangles 72 | - `2.6` - KakaoTalk profile icon 73 | - `4.0` - Squircle 74 | - `5.0` - iOS app icon 75 | 76 | ###### Example 77 | 78 | ```css 79 | .mask { 80 | --smooth-corners: 3; 81 | mask-image: paint(smooth-corners); 82 | -webkit-mask-image: paint(smooth-corners); 83 | background: #d01257; /* So you can see it */ 84 | ``` 85 | 86 | ### Registering the Paint Worklet 87 | 88 | Register the [Paint Worklet] to the distributed path of [paint.js]. 89 | 90 | #### Register with a CDN (preferred) 91 | 92 | Use any CDN that serves packages from the NPM registry, for example: 93 | 94 | - [unpkg.com/smooth-corners](https://unpkg.com/smooth-corners) 95 | - [cdn.jsdelivr.net/npm/smooth-corners/paint.js](https://cdn.jsdelivr.net/npm/smooth-corners/lib/paint.js) 96 | 97 | ```html 98 | 101 | ``` 102 | 103 | #### Register with a file path 104 | 105 | Download [paint.js] or install with `npm install smooth-corners` 106 | 107 | ```js 108 | // src/assets/paint.js 109 | import 'smooth-corners' // ES Modules 110 | ``` 111 | 112 | ```js 113 | // src/assets/paint.js 114 | require('smooth-corners') // CommonJS 115 | ``` 116 | 117 | Like Web Workers, the [Paint Worklet] API requests the module path in the browser during runtime and must be a seperate entryfile. This is not the path to the source code location. 118 | 119 | ```html 120 | 123 | ``` 124 | 125 | ### Result 126 | 127 | ![2 examples: A rounded pink square and a pink squircle][Example] 128 | 129 | [paint.js]:https://wopian.github.io/smooth-corners/paint.js 130 | [Paint Worklet]:https://developer.mozilla.org/en-US/docs/Web/API/PaintWorklet 131 | [CTA]:https://raw.githubusercontent.com/wopian/smooth-corners/master/.github/images/cta.png 132 | [Example]:https://raw.githubusercontent.com/wopian/smooth-corners/master/.github/images/example.png 133 | [Astroid]:https://en.wikipedia.org/wiki/Astroid 134 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Static Template 8 | 247 | 248 | 249 |
250 | Your browser does not support the Houdini CSS Paint API yet! 251 | View supported browsers 252 |
253 | 279 | 280 |
281 |
282 |
283 |
284 | 292 | 293 |
294 |
295 | 304 | 305 |
306 |
307 | 308 | 309 |
310 |
311 | 316 | 317 |
318 |
319 |
--smooth-corners: 4
320 | mask-image: paint(smooth-corners);
321 | -webkit-mask-image: paint(smooth-corners);
322 |
323 |
324 |
325 |
326 | 327 |
328 |
16, 2
329 |
5
330 |
4
331 |
2.6
332 |
2
333 |
0.6, 2
334 |
1.2
335 |
1
336 |
0.8
337 |
0.6
338 |
339 | 340 |
341 | 345 | 349 | 353 | 357 | 361 | 365 | 369 | 373 | 377 | 381 |
382 | 383 |
384 | 388 | 392 | 396 | 400 | 404 | 408 | 412 | 416 | 420 | 424 |
425 | 426 |
427 | 431 | 435 | 439 | 443 | 447 | 451 | 455 | 459 | 463 | 467 |
468 |
469 |
470 |
6
471 |
8
472 |
10
473 |
12
474 |
14
475 |
16
476 |
32
477 |
64
478 |
80
479 |
96
480 |
481 | 482 |
483 | 487 | 491 | 495 | 499 | 503 | 507 | 511 | 515 | 519 | 523 |
524 | 525 |
526 | 530 | 534 | 538 | 542 | 546 | 550 | 554 | 558 | 562 | 566 |
567 | 575 | 639 | 640 | 641 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smooth-corners", 3 | "version": "1.0.8", 4 | "description": "CSS superellipse masks with the Houdini API", 5 | "source": "paint.js", 6 | "main": "lib/paint.js", 7 | "module": "lib/paint.js", 8 | "unpkg": "lib/paint.js", 9 | "browser": "lib/paint.js", 10 | "scripts": { 11 | "start": "serve", 12 | "build": "microbundle --no-sourcemap --compress --format=es", 13 | "dev": "microbundle watch --no-sourcemap" 14 | }, 15 | "repository": "https://github.com/wopian/smooth-corners", 16 | "funding": "https://github.com/sponsors/wopian", 17 | "keywords": [ 18 | "CSS", 19 | "Houdini", 20 | "Superellipse", 21 | "PaintWorklet", 22 | "Squircle", 23 | "Hypoellipse", 24 | "Hyperellipse", 25 | "Asteroid" 26 | ], 27 | "author": "James Harris ", 28 | "license": "MIT", 29 | "bugs": { 30 | "url": "https://github.com/wopian/smooth-corners/issues" 31 | }, 32 | "homepage": "https://github.com/wopian/smooth-corners#readme", 33 | "devDependencies": { 34 | "@semantic-release/changelog": "~6.0.2", 35 | "@semantic-release/commit-analyzer": "~11.1.0", 36 | "@semantic-release/github": "~9.2.0", 37 | "@semantic-release/npm": "~11.0.0", 38 | "@semantic-release/release-notes-generator": "~12.1.0", 39 | "microbundle": "^0.15.0", 40 | "semantic-release": "~22.0.0", 41 | "serve": "^14.0.0" 42 | }, 43 | "renovate": { 44 | "extends": [ 45 | "github>wopian/renovate-config" 46 | ] 47 | }, 48 | "packageManager": "yarn@4.9.1" 49 | } 50 | -------------------------------------------------------------------------------- /paint.js: -------------------------------------------------------------------------------- 1 | class SmoothCornersPainter { 2 | static get inputProperties() { 3 | return ['--smooth-corners'] 4 | } 5 | 6 | superellipse(a, b, nX = 4, nY) { 7 | if (Number.isNaN(nX)) nX = 4 8 | if (typeof nY === 'undefined' || Number.isNaN(nY)) nY = nX 9 | if (nX > 100) nX = 100 10 | if (nY > 100) nY = 100 11 | if (nX < 0.00000000001) nX = 0.00000000001 12 | if (nY < 0.00000000001) nY = 0.00000000001 13 | 14 | const nX2 = 2 / nX 15 | const nY2 = nY ? 2 / nY : nX2 16 | const steps = 360 17 | const step = (2 * Math.PI) / steps 18 | const points = t => { 19 | const cosT = Math.cos(t) 20 | const sinT = Math.sin(t) 21 | return { 22 | x: Math.abs(cosT) ** nX2 * a * Math.sign(cosT), 23 | y: Math.abs(sinT) ** nY2 * b * Math.sign(sinT) 24 | } 25 | } 26 | return Array.from({ length: steps }, (_, i) => points(i * step)) 27 | } 28 | 29 | paint(ctx, geom, properties) { 30 | const [nX, nY] = properties 31 | .get('--smooth-corners') 32 | .toString() 33 | .replace(/ /g, '') 34 | .split(',') 35 | 36 | const width = geom.width / 2 37 | const height = geom.height / 2 38 | const smooth = this.superellipse( 39 | width, 40 | height, 41 | parseFloat(nX), 42 | parseFloat(nY) 43 | ) 44 | 45 | ctx.fillStyle = '#000' 46 | ctx.setTransform(1, 0, 0, 1, width, height) 47 | ctx.beginPath() 48 | 49 | for (let i = 0; i < smooth.length; i++) { 50 | const { x, y } = smooth[i] 51 | if (i === 0) ctx.moveTo(x, y) 52 | else ctx.lineTo(x, y) 53 | } 54 | 55 | ctx.closePath() 56 | ctx.fill() 57 | } 58 | } 59 | 60 | // eslint-disable-next-line no-undef 61 | registerPaint('smooth-corners', SmoothCornersPainter) 62 | -------------------------------------------------------------------------------- /sandbox.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "template": "static" 3 | } 4 | --------------------------------------------------------------------------------