@tetrax/squircle 1.0.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/README.md ADDED
@@ -0,0 +1,257 @@
1
+ # @tetrax/squircle
2
+
3
+ Figma-accurate squircles for React, React Native, & HTML Canvas. Install once, use with className.
4
+
5
+ > Uses the same algorithm as Figma's corner smoothing — 2 cubic Bézier curves + 1 circular arc per corner (Standard mode), or Pavel Klavík's optimal cubic Bézier coefficients for pure curvature-continuous corners (G2 mode).
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @tetrax/squircle
11
+ ```
12
+
13
+ No config changes needed. Just import and use.
14
+
15
+ ## React / Next.js
16
+
17
+ ```jsx
18
+ import { Squircle } from "@tetrax/squircle/react";
19
+
20
+ function App() {
21
+ return (
22
+ <Squircle className="rounded-squircle-24 squircle-border border-gray-200 bg-white p-6">
23
+ <h2>Card Title</h2>
24
+ <p>This has squircle corners and a squircle-shaped border.</p>
25
+ </Squircle>
26
+ );
27
+ }
28
+ ```
29
+
30
+ ### Available Classes
31
+
32
+ | Class | Effect |
33
+ |---|---|
34
+ | `rounded-squircle-{0-96}` | Corner radius in px |
35
+ | `corner-smoothing-{0-100}` | Smoothing (default: 100 = full squircle, G1 only) |
36
+ | `squircle-g2` | Enable G2 curvature-continuous mode (ignores manual smoothing percent) |
37
+ | `squircle-border` | Enable squircle border overlay (1px) |
38
+ | `squircle-border-{n}` | Border width (e.g. `squircle-border-2`, `squircle-border-1.5`) |
39
+ | `border-{color}` | Border color — standard Tailwind (e.g. `border-gray-200`) |
40
+ | `border-{color}/{opacity}` | Border color with opacity (e.g. `border-gray-600/50`) |
41
+ | `border-{width}` | Border width — standard Tailwind (e.g. `border-2`) |
42
+
43
+ All other Tailwind classes (`bg-*`, `p-*`, `text-*`, etc.) pass through normally.
44
+
45
+ ### Examples
46
+
47
+ ```jsx
48
+ // Basic squircle (G1)
49
+ <Squircle className="rounded-squircle-16 bg-blue-500 p-4 text-white">
50
+ Hello World
51
+ </Squircle>
52
+
53
+ // G2 Curvature-continuous squircle (ultra-smooth corners)
54
+ <Squircle className="rounded-squircle-24 squircle-g2 bg-purple-600 p-4 text-white">
55
+ G2 Smooth
56
+ </Squircle>
57
+
58
+ // With border (standard Tailwind)
59
+ <Squircle className="rounded-squircle-24 squircle-border border-gray-200 bg-white p-6">
60
+ Card with border
61
+ </Squircle>
62
+
63
+ // Thick colored border
64
+ <Squircle className="rounded-squircle-20 squircle-border border-2 border-emerald-300 bg-emerald-500 p-4">
65
+ Green card
66
+ </Squircle>
67
+
68
+ // Border with opacity
69
+ <Squircle className="rounded-squircle-24 squircle-border border-gray-600/50 bg-white p-6">
70
+ Semi-transparent border
71
+ </Squircle>
72
+
73
+ // Decimal border width
74
+ <Squircle className="rounded-squircle-16 squircle-border-1.5 border-gray-400 bg-white p-4">
75
+ 1.5px squircle border
76
+ </Squircle>
77
+
78
+ // Custom element tag
79
+ <Squircle as="button" className="rounded-squircle-12 bg-black text-white px-6 py-3">
80
+ Click me
81
+ </Squircle>
82
+ ```
83
+
84
+ ### Props
85
+
86
+ | Prop | Type | Default | Description |
87
+ |---|---|---|---|
88
+ | `className` | `string` | `""` | Tailwind classes including squircle classes |
89
+ | `cornerRadius` | `number` | from `rounded-squircle-*` | Override corner radius |
90
+ | `cornerSmoothing` | `number` | `1` | Override smoothing (0–1, Standard G1 only) |
91
+ | `g2Continuous` | `boolean` | `false` | Enable pure G2 curvature continuity |
92
+ | `preserveSmoothing` | `boolean` | `false` | Preserve smoothing at large radii |
93
+ | `as` | `ComponentType \| string` | `"div"` | HTML tag or component to render |
94
+
95
+ ### useSquircle Hook
96
+
97
+ For custom elements where you need more control:
98
+
99
+ ```jsx
100
+ import { useSquircle } from "@tetrax/squircle/react";
101
+
102
+ function MyCard({ className }) {
103
+ const { ref, clipPath, borderStyle, parsed } = useSquircle(className);
104
+
105
+ return (
106
+ <div
107
+ ref={ref}
108
+ className={parsed.restClasses}
109
+ style={{ clipPath, ...borderStyle }}
110
+ >
111
+ Custom element with squircle
112
+ </div>
113
+ );
114
+ }
115
+ ```
116
+
117
+ ### Zero-Config: Standard HTML Elements (Optional)
118
+
119
+ Alternatively, you can use standard HTML tags (like `<div>` or `<button>`) directly without wrapper components or hooks!
120
+
121
+ Once the React package is imported anywhere in your app, a global observer dynamically scans the DOM and automatically applies the Figma-accurate clip-path and border overlay styles to any element containing the `rounded-squircle-` classes.
122
+
123
+ This works **directly on void elements** (like `<input>`, `<img>`, etc.) where appending children is physically disallowed by HTML. The SDK automatically detects void tags, inserts the border overlay as a sibling, and aligns it perfectly.
124
+
125
+ ```html
126
+ <!-- Automatically squirckled standard div! -->
127
+ <div class="rounded-squircle-24 squircle-border border-gray-200 bg-white p-6">
128
+ Hello from standard div!
129
+ </div>
130
+
131
+ <!-- Squirckled input tag (void element) — border renders perfectly! -->
132
+ <input
133
+ type="password"
134
+ placeholder="Thick border: yellow-400/20"
135
+ class="rounded-squircle-9 squircle-border-2 border-yellow-400/20 bg-[#1B1B1B] text-white px-4 py-3 outline-none"
136
+ />
137
+ ```
138
+
139
+ ## React Native
140
+
141
+ ```jsx
142
+ import { SquircleNative } from "@tetrax/squircle/react-native";
143
+
144
+ function Card() {
145
+ return (
146
+ <SquircleNative
147
+ cornerRadius={24}
148
+ squircleBorder
149
+ borderColor="#e5e7eb"
150
+ borderWidth={1}
151
+ backgroundColor="white"
152
+ style={{ padding: 16 }}
153
+ >
154
+ <Text>Card content</Text>
155
+ </SquircleNative>
156
+ );
157
+ }
158
+ ```
159
+
160
+ ### Props
161
+
162
+ | Prop | Type | Default | Description |
163
+ |---|---|---|---|
164
+ | `cornerRadius` | `number` | *required* | Corner radius in dp |
165
+ | `cornerSmoothing` | `number` | `1` | Smoothing (0–1) |
166
+ | `squircleBorder` | `boolean` | `false` | Show squircle border |
167
+ | `borderColor` | `string` | `"transparent"` | Border color |
168
+ | `borderWidth` | `number` | `1` | Border width in dp |
169
+ | `backgroundColor` | `string` | — | Background fill |
170
+ | `style` | `ViewStyle` | — | Additional styles |
171
+
172
+ > React Native requires `react-native-svg` as a peer dependency.
173
+
174
+ ## Core (Headless & Canvas 2D)
175
+
176
+ Use the math engine or direct Canvas drawing utility without React:
177
+
178
+ ### SVG Path Calculations
179
+
180
+ ```js
181
+ import { getSvgPath, computeSquirclePath } from "@tetrax/squircle/core";
182
+
183
+ // Full API (matches figma-squircle's getSvgPath)
184
+ const path = getSvgPath({
185
+ width: 200,
186
+ height: 100,
187
+ cornerRadius: 24,
188
+ cornerSmoothing: 0.8, // Optional (default: 1)
189
+ preserveSmoothing: true,
190
+ g2Continuous: false, // Set true for G2 Béziers
191
+ });
192
+
193
+ // Convenience wrapper
194
+ const path = computeSquirclePath(200, 100, 24, 0.8);
195
+
196
+ // Use with clip-path CSS
197
+ element.style.clipPath = `path('${path}')`;
198
+ ```
199
+
200
+ ### Canvas 2D Drawing Utility
201
+
202
+ Draw squircles directly onto a canvas context with pixel-perfect precision matching the SVG output:
203
+
204
+ ```js
205
+ import { drawSquircleRect } from "@tetrax/squircle/core";
206
+
207
+ const canvas = document.getElementById("canvas");
208
+ const ctx = canvas.getContext("2d");
209
+
210
+ ctx.beginPath();
211
+ // Supports uniform radius:
212
+ drawSquircleRect(ctx, 10, 10, 200, 100, 24);
213
+
214
+ // Supports asymmetric per-corner radii:
215
+ drawSquircleRect(ctx, 10, 150, 200, 100, {
216
+ tl: 8, tr: 32, br: 8, bl: 32
217
+ });
218
+
219
+ ctx.fillStyle = "#3b82f6";
220
+ ctx.fill();
221
+ ```
222
+
223
+ ### getSvgPath Options
224
+
225
+ | Option | Type | Default | Description |
226
+ |---|---|---|---|
227
+ | `width` | `number` | *required* | Element width |
228
+ | `height` | `number` | *required* | Element height |
229
+ | `cornerRadius` | `number` | `0` | Uniform corner radius |
230
+ | `topLeftCornerRadius` | `number` | `cornerRadius` | Override top-left |
231
+ | `topRightCornerRadius` | `number` | `cornerRadius` | Override top-right |
232
+ | `bottomRightCornerRadius` | `number` | `cornerRadius` | Override bottom-right |
233
+ | `bottomLeftCornerRadius` | `number` | `cornerRadius` | Override bottom-left |
234
+ | `cornerSmoothing` | `number` | `1` | 0 = rounded rect, 1 = full squircle |
235
+ | `preserveSmoothing` | `boolean` | `false` | Preserve smoothing at large radii |
236
+ | `g2Continuous` | `boolean` | `false` | Enable G2 curvature-continuous mode |
237
+
238
+ ## How It Works
239
+
240
+ ### Via Component / Hook:
241
+ 1. The `<Squircle>` component (or `useSquircle` hook) wraps or hooks into your element.
242
+ 2. `ResizeObserver` measures the element's actual size.
243
+ 3. The Figma squircle algorithm generates an SVG path (2 Bézier curves + 1 arc per corner in standard mode, or 3 pure Béziers in G2 mode).
244
+ 4. The path is applied as `clip-path: path('...')` via inline style.
245
+ 5. If `squircle-border` is present, an absolutely-positioned overlay SVG renders the squircle-shaped border, inset by `borderWidth / 2` so it remains fully inside the parent's `clip-path` bounding box without clipping.
246
+
247
+ ### Via Standard HTML Elements:
248
+ 1. A global `MutationObserver` watches the document body for changes.
249
+ 2. When an element with class `rounded-squircle-` is added or has its classes modified, the observer registers a `ResizeObserver` on it.
250
+ 3. On size changes, it calculates the squircle path and applies it to the element's `clipPath` and `WebkitClipPath` style properties.
251
+ 4. If `squircle-border` is in the class name:
252
+ - **For standard container elements**: it dynamically appends and updates an `<svg data-squircle-border="true">` child overlay.
253
+ - **For void elements (like `<input>`, `<img>`)**: it inserts the SVG border overlay as a sibling right after the element and dynamically aligns it using the element's `offsetTop`/`offsetLeft` parameters to guarantee exact rendering alignment over the clipped control.
254
+
255
+ ## License
256
+
257
+ MIT
@@ -0,0 +1,68 @@
1
+ interface SquirclePathParams {
2
+ width: number;
3
+ height: number;
4
+ cornerRadius?: number;
5
+ topLeftCornerRadius?: number;
6
+ topRightCornerRadius?: number;
7
+ bottomRightCornerRadius?: number;
8
+ bottomLeftCornerRadius?: number;
9
+ cornerSmoothing?: number;
10
+ preserveSmoothing?: boolean;
11
+ g2Continuous?: boolean;
12
+ }
13
+ /**
14
+ * Generate an SVG path string for a squircle rectangle.
15
+ * Direct port of figma-squircle's getSvgPath(), with optional pure G2 Bézier mode.
16
+ */
17
+ declare function getSvgPath({ cornerRadius, topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius, cornerSmoothing, width, height, preserveSmoothing, g2Continuous, }: SquirclePathParams): string;
18
+ /**
19
+ * Compute an SVG path string from width/height/radius/smoothing.
20
+ * Convenience wrapper around getSvgPath().
21
+ */
22
+ declare function computeSquirclePath(width: number, height: number, cornerRadius: number, cornerSmoothing?: number, preserveSmoothing?: boolean, g2Continuous?: boolean): string;
23
+ /**
24
+ * Render a squircle rectangle directly onto a Canvas 2D context using
25
+ * G2 curvature-continuous Bézier curves (Pavel Klavík's optimal coefficients).
26
+ *
27
+ * FIX #5: Rewrote to use the same *relative delta* G2 coefficients as the
28
+ * SVG path drawer, so Canvas and SVG paths are numerically identical.
29
+ * Previous version used accumulated offsets (0.804, 0.912, 0.961) which
30
+ * did not match the SVG path coefficients (0.185, 0.293, 0.342).
31
+ *
32
+ * @example
33
+ * const ctx = canvas.getContext('2d');
34
+ * ctx.beginPath();
35
+ * drawSquircleRect(ctx, 0, 0, 200, 100, 24);
36
+ * ctx.fillStyle = '#3b82f6';
37
+ * ctx.fill();
38
+ */
39
+ declare function drawSquircleRect(ctx: CanvasRenderingContext2D | any, x: number, y: number, width: number, height: number, cornerRadius: number | {
40
+ tl?: number;
41
+ tr?: number;
42
+ br?: number;
43
+ bl?: number;
44
+ }, smoothing?: number): void;
45
+
46
+ interface SquircleClassConfig {
47
+ cornerRadius: number | null;
48
+ cornerSmoothing: number;
49
+ squircleBorder: boolean;
50
+ borderColor: string | null;
51
+ borderWidth: number;
52
+ g2Continuous: boolean;
53
+ restClasses: string;
54
+ }
55
+ /**
56
+ * Parse Tailwind-style class names and extract squircle config.
57
+ *
58
+ * When `squircle-border` is present, standard Tailwind `border-{color}` and
59
+ * `border-{width}` classes are intercepted for the squircle border overlay.
60
+ * Opacity modifiers like `border-gray-600/50` are fully supported.
61
+ *
62
+ * @example
63
+ * parseSquircleClasses("rounded-squircle-24 squircle-border border-gray-600/50")
64
+ * // → { cornerRadius: 24, squircleBorder: true, borderColor: "rgba(75, 85, 99, 0.5)", ... }
65
+ */
66
+ declare function parseSquircleClasses(className?: string): SquircleClassConfig;
67
+
68
+ export { type SquircleClassConfig, computeSquirclePath, drawSquircleRect, getSvgPath, parseSquircleClasses };
package/dist/core.d.ts ADDED
@@ -0,0 +1,68 @@
1
+ interface SquirclePathParams {
2
+ width: number;
3
+ height: number;
4
+ cornerRadius?: number;
5
+ topLeftCornerRadius?: number;
6
+ topRightCornerRadius?: number;
7
+ bottomRightCornerRadius?: number;
8
+ bottomLeftCornerRadius?: number;
9
+ cornerSmoothing?: number;
10
+ preserveSmoothing?: boolean;
11
+ g2Continuous?: boolean;
12
+ }
13
+ /**
14
+ * Generate an SVG path string for a squircle rectangle.
15
+ * Direct port of figma-squircle's getSvgPath(), with optional pure G2 Bézier mode.
16
+ */
17
+ declare function getSvgPath({ cornerRadius, topLeftCornerRadius, topRightCornerRadius, bottomRightCornerRadius, bottomLeftCornerRadius, cornerSmoothing, width, height, preserveSmoothing, g2Continuous, }: SquirclePathParams): string;
18
+ /**
19
+ * Compute an SVG path string from width/height/radius/smoothing.
20
+ * Convenience wrapper around getSvgPath().
21
+ */
22
+ declare function computeSquirclePath(width: number, height: number, cornerRadius: number, cornerSmoothing?: number, preserveSmoothing?: boolean, g2Continuous?: boolean): string;
23
+ /**
24
+ * Render a squircle rectangle directly onto a Canvas 2D context using
25
+ * G2 curvature-continuous Bézier curves (Pavel Klavík's optimal coefficients).
26
+ *
27
+ * FIX #5: Rewrote to use the same *relative delta* G2 coefficients as the
28
+ * SVG path drawer, so Canvas and SVG paths are numerically identical.
29
+ * Previous version used accumulated offsets (0.804, 0.912, 0.961) which
30
+ * did not match the SVG path coefficients (0.185, 0.293, 0.342).
31
+ *
32
+ * @example
33
+ * const ctx = canvas.getContext('2d');
34
+ * ctx.beginPath();
35
+ * drawSquircleRect(ctx, 0, 0, 200, 100, 24);
36
+ * ctx.fillStyle = '#3b82f6';
37
+ * ctx.fill();
38
+ */
39
+ declare function drawSquircleRect(ctx: CanvasRenderingContext2D | any, x: number, y: number, width: number, height: number, cornerRadius: number | {
40
+ tl?: number;
41
+ tr?: number;
42
+ br?: number;
43
+ bl?: number;
44
+ }, smoothing?: number): void;
45
+
46
+ interface SquircleClassConfig {
47
+ cornerRadius: number | null;
48
+ cornerSmoothing: number;
49
+ squircleBorder: boolean;
50
+ borderColor: string | null;
51
+ borderWidth: number;
52
+ g2Continuous: boolean;
53
+ restClasses: string;
54
+ }
55
+ /**
56
+ * Parse Tailwind-style class names and extract squircle config.
57
+ *
58
+ * When `squircle-border` is present, standard Tailwind `border-{color}` and
59
+ * `border-{width}` classes are intercepted for the squircle border overlay.
60
+ * Opacity modifiers like `border-gray-600/50` are fully supported.
61
+ *
62
+ * @example
63
+ * parseSquircleClasses("rounded-squircle-24 squircle-border border-gray-600/50")
64
+ * // → { cornerRadius: 24, squircleBorder: true, borderColor: "rgba(75, 85, 99, 0.5)", ... }
65
+ */
66
+ declare function parseSquircleClasses(className?: string): SquircleClassConfig;
67
+
68
+ export { type SquircleClassConfig, computeSquirclePath, drawSquircleRect, getSvgPath, parseSquircleClasses };