@usespaceui/squircle 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +291 -0
- package/dist/index.d.mts +6 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +114 -0
- package/dist/index.mjs +114 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Space UI
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://squircle.spaceui.one" target="_blank">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://squircle.spaceui.one/logo.svg">
|
|
5
|
+
<source media="(prefers-color-scheme: light)" srcset="https://squircle.spaceui.one/logo.svg">
|
|
6
|
+
<img alt="Space UI logo" src="https://squircle.spaceui.one/logo.svg" width="100" />
|
|
7
|
+
</picture>
|
|
8
|
+
</a>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<h1 align="center">
|
|
12
|
+
@usespaceui/squircle
|
|
13
|
+
</h1>
|
|
14
|
+
|
|
15
|
+
<p align="center">
|
|
16
|
+
Figma • iOS style corner smoothing using CSS Houdini Paint Worklet.
|
|
17
|
+
</p>
|
|
18
|
+
|
|
19
|
+
<p align="center">
|
|
20
|
+
<a href="https://squircle.spaceui.one">Preview</a> •
|
|
21
|
+
<a href="https://github.com/usespaceui/squircle">Source Code</a> •
|
|
22
|
+
<a href="https://www.spaceui.one">SpaceUI.one</a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="https://github.com/usespaceui/squircle/blob/main/LICENSE">
|
|
27
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="MIT License" />
|
|
28
|
+
</a>
|
|
29
|
+
<a href="https://twitter.com/intent/follow?screen_name=usespaceui">
|
|
30
|
+
<img src="https://img.shields.io/twitter/follow/usespaceui.svg?label=Follow%20@usespaceui" alt="Follow @usespaceui" />
|
|
31
|
+
</a>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
<div align="center">
|
|
35
|
+
<a href="https://www.npmjs.com/package/@usespaceui/squircle">
|
|
36
|
+
<img src="https://img.shields.io/npm/v/@usespaceui/squircle?color=%23fa6400&label=version" />
|
|
37
|
+
</a>
|
|
38
|
+
<a href="https://www.npmjs.com/package/@usespaceui/squircle">
|
|
39
|
+
<img src="https://img.shields.io/npm/unpacked-size/%40usespaceui%2Fsquircle?label=install%20size">
|
|
40
|
+
</a>
|
|
41
|
+
<a href="https://www.npmjs.com/package/@usespaceui/squircle">
|
|
42
|
+
<img src="https://img.shields.io/bundlejs/size/%40usespaceui%2Fsquircle?format=min">
|
|
43
|
+
</a>
|
|
44
|
+
<a href="https://www.npmjs.com/package/@usespaceui/squircle">
|
|
45
|
+
<img src="https://img.shields.io/bundlejs/size/%40usespaceui%2Fsquircle">
|
|
46
|
+
</a>
|
|
47
|
+
<a href="https://github.com/usespaceui/squircle">
|
|
48
|
+
<img src="https://img.shields.io/github/repo-size/usespaceui/squircle">
|
|
49
|
+
</a>
|
|
50
|
+
<a href="https://www.npmjs.com/package/@usespaceui/squircle">
|
|
51
|
+
<img src="https://img.shields.io/npm/dm/@usespaceui/squircle" />
|
|
52
|
+
</a>
|
|
53
|
+
<a href="https://github.com/usespaceui/squircle/blob/main/LICENSE">
|
|
54
|
+
<img src="https://img.shields.io/npm/l/@usespaceui/squircle" />
|
|
55
|
+
</a>
|
|
56
|
+
<br><br>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## ✨ Overview
|
|
62
|
+
|
|
63
|
+
`@usespaceui/squircle` brings iOS-style squircles (mathematically perfect superellipses) to your Tailwind CSS projects. It mimics the continuous curvature corners found in Figma and Apple devices.
|
|
64
|
+
By utilizing the CSS Houdini Paint Worklet API, it provides a native-like performance and seamlessly integrates with Tailwind CSS.
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## 📦 Installation
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
pnpm add @usespaceui/squircle
|
|
72
|
+
# or
|
|
73
|
+
npm install @usespaceui/squircle
|
|
74
|
+
# or
|
|
75
|
+
yarn add @usespaceui/squircle
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Zero dependencies. Add the plugin to your Tailwind configuration or import it directly (Tailwind).
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 🚀 Setup
|
|
83
|
+
|
|
84
|
+
Because the plugin relies on a CSS Houdini worklet to draw the shape, you must initialize the script once in your app.
|
|
85
|
+
|
|
86
|
+
### With React / Next.js
|
|
87
|
+
|
|
88
|
+
Since the worklet interacts with the browser DOM, initialize it inside a Client Component provider.
|
|
89
|
+
|
|
90
|
+
```tsx
|
|
91
|
+
// components/SquircleProvider.tsx
|
|
92
|
+
'use client'
|
|
93
|
+
import * as React from 'react'
|
|
94
|
+
import { initSquircle } from '@usespaceui/squircle'
|
|
95
|
+
|
|
96
|
+
export function SquircleProvider({ children }: { children: React.ReactNode }) {
|
|
97
|
+
React.useEffect(() => {
|
|
98
|
+
initSquircle()
|
|
99
|
+
}, [])
|
|
100
|
+
|
|
101
|
+
return <>{children}</>
|
|
102
|
+
}
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Wrap your root layout:
|
|
106
|
+
|
|
107
|
+
```tsx
|
|
108
|
+
import { SquircleProvider } from './SquircleProvider'
|
|
109
|
+
|
|
110
|
+
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
|
111
|
+
return (
|
|
112
|
+
<html>
|
|
113
|
+
<body>
|
|
114
|
+
<SquircleProvider>{children}</SquircleProvider>
|
|
115
|
+
</body>
|
|
116
|
+
</html>
|
|
117
|
+
)
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### With Vanilla JS
|
|
122
|
+
|
|
123
|
+
Initialize the worklet directly in your client-side entry file:
|
|
124
|
+
|
|
125
|
+
```js
|
|
126
|
+
import { initSquircle } from '@usespaceui/squircle'
|
|
127
|
+
|
|
128
|
+
if (typeof window !== 'undefined') {
|
|
129
|
+
initSquircle()
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
## Configure Tailwind CSS 4
|
|
134
|
+
|
|
135
|
+
Add the squircle plugin to your `tailwind.config.ts`.
|
|
136
|
+
|
|
137
|
+
```css
|
|
138
|
+
@import '@usespaceui/squircle';
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Configure Tailwind CSS 3
|
|
142
|
+
|
|
143
|
+
Add the squircle plugin to your `tailwind.config.js` or `tailwind.config.ts`.
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
import squirclePlugin from '@usespaceui/squircle'
|
|
147
|
+
|
|
148
|
+
/** @type {import('tailwindcss').Config} */
|
|
149
|
+
export default {
|
|
150
|
+
content: ['./src/**/*.{js,ts,jsx,tsx}'],
|
|
151
|
+
theme: {
|
|
152
|
+
extend: {},
|
|
153
|
+
},
|
|
154
|
+
plugins: [squirclePlugin],
|
|
155
|
+
}
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## 🎨 Usage
|
|
161
|
+
|
|
162
|
+
### 1. Sizing
|
|
163
|
+
|
|
164
|
+
The plugin generates squircle utilities based on your existing Tailwind `borderRadius` theme. If your Tailwind config has `rounded-2xl`, you automatically have `squircle-2xl`.
|
|
165
|
+
|
|
166
|
+
```html
|
|
167
|
+
<div class="squircle-md p-4 bg-black text-white">Space UI Squircle!</div>
|
|
168
|
+
<div class="squircle-2xl">...</div>
|
|
169
|
+
<div class="squircle-full">...</div>
|
|
170
|
+
<!-- Automatically clamped to pill/circle -->
|
|
171
|
+
|
|
172
|
+
<!-- Arbitrary values are fully supported -->
|
|
173
|
+
<div class="squircle-[40px]">...</div>
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### 2. Smoothing Intensity
|
|
177
|
+
|
|
178
|
+
By default, the plugin applies a **60% smooth transition** between the straight edge and the corner arc, perfectly matching Apple and Figma's native smoothing.
|
|
179
|
+
|
|
180
|
+
You can adjust this intensity using modifiers (`/0`, `/20`, `/40`, `/60`, `/80`, `/100`).
|
|
181
|
+
|
|
182
|
+
```html
|
|
183
|
+
<!-- Maximum smoothing -->
|
|
184
|
+
<div class="squircle-2xl/100">...</div>
|
|
185
|
+
|
|
186
|
+
<!-- Minimal smoothing (closer to a plain rounded rectangle) -->
|
|
187
|
+
<div class="squircle-2xl/20">...</div>
|
|
188
|
+
|
|
189
|
+
<!-- Arbitrary smoothing -->
|
|
190
|
+
<div class="squircle-2xl/[25]">...</div>
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 3. Native Borders (The Magic)
|
|
194
|
+
|
|
195
|
+
Use standard Tailwind border utilities. The Houdini worklet reads your native CSS border classes and draws the squircle border perfectly. No workarounds needed.
|
|
196
|
+
|
|
197
|
+
```html
|
|
198
|
+
<!-- 8px red border -->
|
|
199
|
+
<div class="squircle-2xl border-8 border-red-500 bg-black">...</div>
|
|
200
|
+
|
|
201
|
+
<!-- 1px semi-transparent blue border -->
|
|
202
|
+
<div class="squircle-[30px] border border-blue-500/50 bg-white">...</div>
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
> **Note on border placement:**
|
|
206
|
+
> By default, the plugin draws the border **outside** the element (like an outline or a ring) to preserve the interior space for your content. If you want the border drawn inward (standard CSS border-box behavior), you can override the custom CSS variable:
|
|
207
|
+
> `<div class="squircle-2xl border-4 border-red-500 [--tw-squircle-outset:0]">...</div>`
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## 🛠 Advanced: Using with `tailwind-merge`
|
|
212
|
+
|
|
213
|
+
If you use `tailwind-merge` (standard in UI libraries like `shadcn/ui` for their `cn()` utility), you should configure it so that `squircle-*` classes properly override standard `rounded-*` classes when both are applied.
|
|
214
|
+
|
|
215
|
+
Extend your merge configuration to group `squircle` into the `rounded` class group:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { clsx, type ClassValue } from 'clsx'
|
|
219
|
+
import { extendTailwindMerge } from 'tailwind-merge'
|
|
220
|
+
|
|
221
|
+
const customTwMerge = extendTailwindMerge({
|
|
222
|
+
extend: {
|
|
223
|
+
classGroups: {
|
|
224
|
+
rounded: [
|
|
225
|
+
{ squircle: [() => true] }, // Merges any squircle-* class into the rounded group
|
|
226
|
+
],
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
export function cn(...inputs: ClassValue[]) {
|
|
232
|
+
return customTwMerge(clsx(inputs))
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## 🧰 Utilities Included
|
|
239
|
+
|
|
240
|
+
- `initSquircle()`
|
|
241
|
+
Registers the CSS Houdini Paint Worklet and sets up the custom CSS properties (`--tw-squircle-w` and `--tw-squircle-smooth`). It is safe to call multiple times and fails gracefully in environments where Houdini is not supported.
|
|
242
|
+
|
|
243
|
+
- `plugin(tailwindcss)`
|
|
244
|
+
The default export is a Tailwind CSS plugin that provides the `squircle-*` utility classes. It automatically provides fallback `border-radius` styles for browsers that don't support the Paint API.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
### Features
|
|
249
|
+
|
|
250
|
+
- 💃 **Native Tailwind Integration:** Use utility classes like `squircle-2xl` just like you would `rounded-2xl`. No framework-specific wrappers required and if the navigator don"'t support squricle the rounded value is applies.
|
|
251
|
+
- ⚡ **Zero-JS Layout Thrashing:** Driven by the native CSS Paint API (Houdini). No `ResizeObserver`, no DOM manipulation, no SVG paths. Repaints happen instantly on the compositor thread.
|
|
252
|
+
- 👌 **Framework Agnostic:** Works in React, Vue, Svelte, Solid, or plain HTML.
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 📦 Related Packages
|
|
257
|
+
|
|
258
|
+
| Package | Description |
|
|
259
|
+
| -------------------------------------------------------------- | --------------------------------------- |
|
|
260
|
+
| [`@usespaceui/avatars`](https://github.com/usespaceui/avatars) | Generative deterministic avatars |
|
|
261
|
+
| [`@usespaceui/sounds`](https://github.com/usespaceui/sounds) | UI sound effects and audio interactions |
|
|
262
|
+
|
|
263
|
+
---
|
|
264
|
+
|
|
265
|
+
## 🪪 License
|
|
266
|
+
|
|
267
|
+
MIT — Free for commercial and personal use.
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
## 📚 Resources
|
|
272
|
+
|
|
273
|
+
- 🔍 [Explore the squircles & Playground](https://squircle.spaceui.one)
|
|
274
|
+
- 🌍 [Space UI Official Site](https://www.spaceui.one)
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## 🛠 Maintenance
|
|
279
|
+
|
|
280
|
+
If you find a bug or have a feature request, please open an [issue on GitHub](https://github.com/usespaceui/squircle/issues).
|
|
281
|
+
Engine internals are intentionally not part of the public API.
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
<p align="center">
|
|
286
|
+
<a href="https://www.spaceui.one" target="_blank">
|
|
287
|
+
<img src="https://www.spaceui.one/favicon.ico" width="60" style="border-radius: 50%" alt="Space UI Logo" />
|
|
288
|
+
</a>
|
|
289
|
+
<br />
|
|
290
|
+
<b>Maintained by the Space UI Team</b>
|
|
291
|
+
</p>
|
package/dist/index.d.mts
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";var o=Object.defineProperty;var l=Object.getOwnPropertyDescriptor;var d=Object.getOwnPropertyNames;var p=Object.prototype.hasOwnProperty;var h=(r,t)=>{for(var i in t)o(r,i,{get:t[i],enumerable:!0})},m=(r,t,i,e)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of d(t))!p.call(r,s)&&s!==i&&o(r,s,{get:()=>t[s],enumerable:!(e=l(t,s))||e.enumerable});return r};var w=r=>m(o({},"__esModule",{value:!0}),r);var f={};h(f,{default:()=>S,initSquircle:()=>g});module.exports=w(f);function n(r,t){return{handler:r,config:t}}n.withOptions=function(r,t=()=>({})){function i(e){return{handler:r(e),config:t(e)}}return i.__isOptionsFunction=!0,i};var c=n;var u=`
|
|
2
|
+
class SquirclePainter {
|
|
3
|
+
static get inputProperties() {
|
|
4
|
+
return ['--tw-squircle-w', '--tw-squircle-smooth'];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
paint(ctx, size, properties) {
|
|
8
|
+
const getProp = (name) => {
|
|
9
|
+
const p = properties.get(name);
|
|
10
|
+
return p ? p.toString().trim() : '';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const radiusStr = getProp('--tw-squircle-w');
|
|
14
|
+
let radius = 0;
|
|
15
|
+
if (radiusStr.endsWith('%')) {
|
|
16
|
+
radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
|
|
17
|
+
} else {
|
|
18
|
+
radius = parseFloat(radiusStr) || 0;
|
|
19
|
+
}
|
|
20
|
+
const smoothStr = getProp('--tw-squircle-smooth');
|
|
21
|
+
let smooth = parseFloat(smoothStr);
|
|
22
|
+
if (isNaN(smooth)) smooth = 0.6;
|
|
23
|
+
|
|
24
|
+
radius = Math.min(radius, size.width / 2, size.height / 2);
|
|
25
|
+
|
|
26
|
+
this.drawSquircle(ctx, size.width, size.height, radius, smooth);
|
|
27
|
+
ctx.fillStyle = 'black';
|
|
28
|
+
ctx.fill();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
drawSquircle(ctx, width, height, radius, smooth) {
|
|
32
|
+
const n = 2 + smooth * 2;
|
|
33
|
+
const steps = 30;
|
|
34
|
+
const step = Math.PI / 2 / steps;
|
|
35
|
+
ctx.beginPath();
|
|
36
|
+
for (let i = 0; i <= steps; i++) {
|
|
37
|
+
const t = Math.PI / 2 - i * step;
|
|
38
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
39
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
40
|
+
const x = width - radius + X;
|
|
41
|
+
const y = radius - Y;
|
|
42
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
43
|
+
else ctx.lineTo(x, y);
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i <= steps; i++) {
|
|
46
|
+
const t = i * step;
|
|
47
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
48
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
49
|
+
ctx.lineTo(width - radius + X, height - radius + Y);
|
|
50
|
+
}
|
|
51
|
+
for (let i = 0; i <= steps; i++) {
|
|
52
|
+
const t = Math.PI / 2 - i * step;
|
|
53
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
54
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
55
|
+
ctx.lineTo(radius - X, height - radius + Y);
|
|
56
|
+
}
|
|
57
|
+
for (let i = 0; i <= steps; i++) {
|
|
58
|
+
const t = i * step;
|
|
59
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
60
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
61
|
+
ctx.lineTo(radius - X, radius - Y);
|
|
62
|
+
}
|
|
63
|
+
ctx.closePath();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class SquircleBorderPainter extends SquirclePainter {
|
|
68
|
+
static get inputProperties() {
|
|
69
|
+
return [
|
|
70
|
+
'--tw-squircle-w',
|
|
71
|
+
'--tw-squircle-smooth',
|
|
72
|
+
'border-top-width',
|
|
73
|
+
'border-top-color'
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
paint(ctx, size, properties) {
|
|
78
|
+
const getProp = (name) => {
|
|
79
|
+
const p = properties.get(name);
|
|
80
|
+
return p ? p.toString().trim() : '';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const radiusStr = getProp('--tw-squircle-w');
|
|
84
|
+
let radius = 0;
|
|
85
|
+
if (radiusStr.endsWith('%')) {
|
|
86
|
+
radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
|
|
87
|
+
} else {
|
|
88
|
+
radius = parseFloat(radiusStr) || 0;
|
|
89
|
+
}
|
|
90
|
+
const smoothStr = getProp('--tw-squircle-smooth');
|
|
91
|
+
let smooth = parseFloat(smoothStr);
|
|
92
|
+
if (isNaN(smooth)) smooth = 0.6;
|
|
93
|
+
|
|
94
|
+
radius = Math.min(radius, size.width / 2, size.height / 2);
|
|
95
|
+
|
|
96
|
+
const bwStr = getProp('border-top-width');
|
|
97
|
+
const bw = parseFloat(bwStr) || 0;
|
|
98
|
+
|
|
99
|
+
if (bw <= 0) return;
|
|
100
|
+
|
|
101
|
+
let color = getProp('border-top-color');
|
|
102
|
+
if (!color) color = 'black';
|
|
103
|
+
|
|
104
|
+
this.drawSquircle(ctx, size.width, size.height, radius, smooth);
|
|
105
|
+
|
|
106
|
+
ctx.lineWidth = bw * 2;
|
|
107
|
+
ctx.strokeStyle = color;
|
|
108
|
+
ctx.stroke();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
registerPaint('squircle', SquirclePainter);
|
|
113
|
+
registerPaint('squircle-border', SquircleBorderPainter);
|
|
114
|
+
`;function g(){if(typeof window<"u"){if("registerProperty"in CSS)try{CSS.registerProperty({name:"--tw-squircle-w",syntax:"<length> | <percentage>",inherits:!0,initialValue:"0px"}),CSS.registerProperty({name:"--tw-squircle-smooth",syntax:"<number>",inherits:!0,initialValue:"0.6"})}catch{}if("paintWorklet"in CSS){let r=`data:application/javascript;charset=utf8,${encodeURIComponent(u)}`;CSS.paintWorklet.addModule(r).catch(t=>console.error("Squircle worklet failed",t))}}}var S=c(function({matchUtilities:r,theme:t}){r({squircle:(i,{modifier:e})=>{let s=.6;if(e){let a=parseFloat(e);isNaN(a)||(s=a/100)}return{"border-radius":i,"--tw-squircle-w":i,"--tw-squircle-smooth":s.toString(),"@supports (mask-image: paint(squircle))":{"border-radius":"0","mask-image":"paint(squircle)"},'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"@supports (mask-image: paint(squircle))":{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 1)"}},"a&, input&, a &, input &":{"@supports (mask-image: paint(squircle))":{"border-radius":i,"mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}},{values:t("borderRadius"),modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}})});0&&(module.exports={initSquircle});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
function a(t,r){return{handler:t,config:r}}a.withOptions=function(t,r=()=>({})){function i(e){return{handler:t(e),config:r(e)}}return i.__isOptionsFunction=!0,i};var n=a;var c=`
|
|
2
|
+
class SquirclePainter {
|
|
3
|
+
static get inputProperties() {
|
|
4
|
+
return ['--tw-squircle-w', '--tw-squircle-smooth'];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
paint(ctx, size, properties) {
|
|
8
|
+
const getProp = (name) => {
|
|
9
|
+
const p = properties.get(name);
|
|
10
|
+
return p ? p.toString().trim() : '';
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const radiusStr = getProp('--tw-squircle-w');
|
|
14
|
+
let radius = 0;
|
|
15
|
+
if (radiusStr.endsWith('%')) {
|
|
16
|
+
radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
|
|
17
|
+
} else {
|
|
18
|
+
radius = parseFloat(radiusStr) || 0;
|
|
19
|
+
}
|
|
20
|
+
const smoothStr = getProp('--tw-squircle-smooth');
|
|
21
|
+
let smooth = parseFloat(smoothStr);
|
|
22
|
+
if (isNaN(smooth)) smooth = 0.6;
|
|
23
|
+
|
|
24
|
+
radius = Math.min(radius, size.width / 2, size.height / 2);
|
|
25
|
+
|
|
26
|
+
this.drawSquircle(ctx, size.width, size.height, radius, smooth);
|
|
27
|
+
ctx.fillStyle = 'black';
|
|
28
|
+
ctx.fill();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
drawSquircle(ctx, width, height, radius, smooth) {
|
|
32
|
+
const n = 2 + smooth * 2;
|
|
33
|
+
const steps = 30;
|
|
34
|
+
const step = Math.PI / 2 / steps;
|
|
35
|
+
ctx.beginPath();
|
|
36
|
+
for (let i = 0; i <= steps; i++) {
|
|
37
|
+
const t = Math.PI / 2 - i * step;
|
|
38
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
39
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
40
|
+
const x = width - radius + X;
|
|
41
|
+
const y = radius - Y;
|
|
42
|
+
if (i === 0) ctx.moveTo(x, y);
|
|
43
|
+
else ctx.lineTo(x, y);
|
|
44
|
+
}
|
|
45
|
+
for (let i = 0; i <= steps; i++) {
|
|
46
|
+
const t = i * step;
|
|
47
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
48
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
49
|
+
ctx.lineTo(width - radius + X, height - radius + Y);
|
|
50
|
+
}
|
|
51
|
+
for (let i = 0; i <= steps; i++) {
|
|
52
|
+
const t = Math.PI / 2 - i * step;
|
|
53
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
54
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
55
|
+
ctx.lineTo(radius - X, height - radius + Y);
|
|
56
|
+
}
|
|
57
|
+
for (let i = 0; i <= steps; i++) {
|
|
58
|
+
const t = i * step;
|
|
59
|
+
const X = radius * Math.pow(Math.cos(t), 2 / n);
|
|
60
|
+
const Y = radius * Math.pow(Math.sin(t), 2 / n);
|
|
61
|
+
ctx.lineTo(radius - X, radius - Y);
|
|
62
|
+
}
|
|
63
|
+
ctx.closePath();
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
class SquircleBorderPainter extends SquirclePainter {
|
|
68
|
+
static get inputProperties() {
|
|
69
|
+
return [
|
|
70
|
+
'--tw-squircle-w',
|
|
71
|
+
'--tw-squircle-smooth',
|
|
72
|
+
'border-top-width',
|
|
73
|
+
'border-top-color'
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
paint(ctx, size, properties) {
|
|
78
|
+
const getProp = (name) => {
|
|
79
|
+
const p = properties.get(name);
|
|
80
|
+
return p ? p.toString().trim() : '';
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const radiusStr = getProp('--tw-squircle-w');
|
|
84
|
+
let radius = 0;
|
|
85
|
+
if (radiusStr.endsWith('%')) {
|
|
86
|
+
radius = (Math.min(size.width, size.height) * parseFloat(radiusStr)) / 100;
|
|
87
|
+
} else {
|
|
88
|
+
radius = parseFloat(radiusStr) || 0;
|
|
89
|
+
}
|
|
90
|
+
const smoothStr = getProp('--tw-squircle-smooth');
|
|
91
|
+
let smooth = parseFloat(smoothStr);
|
|
92
|
+
if (isNaN(smooth)) smooth = 0.6;
|
|
93
|
+
|
|
94
|
+
radius = Math.min(radius, size.width / 2, size.height / 2);
|
|
95
|
+
|
|
96
|
+
const bwStr = getProp('border-top-width');
|
|
97
|
+
const bw = parseFloat(bwStr) || 0;
|
|
98
|
+
|
|
99
|
+
if (bw <= 0) return;
|
|
100
|
+
|
|
101
|
+
let color = getProp('border-top-color');
|
|
102
|
+
if (!color) color = 'black';
|
|
103
|
+
|
|
104
|
+
this.drawSquircle(ctx, size.width, size.height, radius, smooth);
|
|
105
|
+
|
|
106
|
+
ctx.lineWidth = bw * 2;
|
|
107
|
+
ctx.strokeStyle = color;
|
|
108
|
+
ctx.stroke();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
registerPaint('squircle', SquirclePainter);
|
|
113
|
+
registerPaint('squircle-border', SquircleBorderPainter);
|
|
114
|
+
`;function h(){if(typeof window<"u"){if("registerProperty"in CSS)try{CSS.registerProperty({name:"--tw-squircle-w",syntax:"<length> | <percentage>",inherits:!0,initialValue:"0px"}),CSS.registerProperty({name:"--tw-squircle-smooth",syntax:"<number>",inherits:!0,initialValue:"0.6"})}catch{}if("paintWorklet"in CSS){let t=`data:application/javascript;charset=utf8,${encodeURIComponent(c)}`;CSS.paintWorklet.addModule(t).catch(r=>console.error("Squircle worklet failed",r))}}}var m=n(function({matchUtilities:t,theme:r}){t({squircle:(i,{modifier:e})=>{let s=.6;if(e){let o=parseFloat(e);isNaN(o)||(s=o/100)}return{"border-radius":i,"--tw-squircle-w":i,"--tw-squircle-smooth":s.toString(),"@supports (mask-image: paint(squircle))":{"border-radius":"0","mask-image":"paint(squircle)"},'&[class~="border"], &[class*=" border-"], &[class^="border-"]':{"@supports (mask-image: paint(squircle))":{"border-image-source":"paint(squircle-border)","border-image-slice":"0 fill","border-image-outset":"var(--tw-squircle-outset, 1)"}},"a&, input&, a &, input &":{"@supports (mask-image: paint(squircle))":{"border-radius":i,"mask-image":"none","background-image":"paint(squircle)","border-image-source":"none"}}}}},{values:r("borderRadius"),modifiers:{0:"0",20:"20",40:"40",60:"60",80:"80",100:"100"}})});export{m as default,h as initSquircle};
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@usespaceui/squircle",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"files": [
|
|
5
|
+
"dist"
|
|
6
|
+
],
|
|
7
|
+
"description": "Squircle geometry generator",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"react",
|
|
10
|
+
"squircle",
|
|
11
|
+
"css",
|
|
12
|
+
"houdini",
|
|
13
|
+
"geometry",
|
|
14
|
+
"spaceui",
|
|
15
|
+
"corners",
|
|
16
|
+
"smooth"
|
|
17
|
+
],
|
|
18
|
+
"main": "./dist/index.js",
|
|
19
|
+
"module": "./dist/index.mjs",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"default": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
"./package.json": "./package.json"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsup",
|
|
36
|
+
"dev": "tsup --watch",
|
|
37
|
+
"typecheck": "tsc --noEmit"
|
|
38
|
+
},
|
|
39
|
+
"publishConfig": {
|
|
40
|
+
"access": "public"
|
|
41
|
+
},
|
|
42
|
+
"repository": {
|
|
43
|
+
"type": "git",
|
|
44
|
+
"url": "git+https://github.com/usespaceui/squircle.git"
|
|
45
|
+
},
|
|
46
|
+
"homepage": "https://squircle.spaceui.one",
|
|
47
|
+
"bugs": {
|
|
48
|
+
"url": "https://github.com/usespaceui/squircle/issues"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/react": "^19.2.18",
|
|
52
|
+
"react": "^19.2.8",
|
|
53
|
+
"tailwindcss": "^4.0.0",
|
|
54
|
+
"tsup": "^8.0.2",
|
|
55
|
+
"typescript": "^5.3.3"
|
|
56
|
+
}
|
|
57
|
+
}
|