@leftium/logo 0.3.0 → 0.4.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/dist/LeftiumLogo.svelte +17 -14
- package/dist/LeftiumLogo.svelte.d.ts +1 -0
- package/dist/app-logo/generate-favicon-set.d.ts +2 -2
- package/dist/app-logo/generate-favicon-set.js +5 -6
- package/dist/app-logo/squircle.d.ts +49 -29
- package/dist/app-logo/squircle.js +153 -157
- package/dist/assets/logo-parts/glow-squircle.svg +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/leftium-logo/generate-favicon-svg.d.ts +27 -0
- package/dist/leftium-logo/generate-favicon-svg.js +90 -0
- package/dist/leftium-logo/generate-svg.js +11 -275
- package/dist/leftium-logo/generate-zip-kit.d.ts +21 -0
- package/dist/leftium-logo/generate-zip-kit.js +55 -0
- package/dist/leftium-logo/l-ligature.d.ts +40 -0
- package/dist/leftium-logo/l-ligature.js +77 -0
- package/package.json +1 -1
package/dist/LeftiumLogo.svelte
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
import { onDestroy } from 'svelte';
|
|
34
34
|
import type { Attachment } from 'svelte/attachments';
|
|
35
35
|
import { Ripples, type RipplesOptions } from './webgl-ripples/webgl-ripples.js';
|
|
36
|
+
import { generateCornerPolygon } from './app-logo/squircle.js';
|
|
36
37
|
|
|
37
38
|
import logoGlow from './assets/logo-parts/glow.svg';
|
|
38
39
|
import logoGlowSquircle from './assets/logo-parts/glow-squircle.svg';
|
|
@@ -46,14 +47,14 @@
|
|
|
46
47
|
ripplesOptions?: RipplesOptions;
|
|
47
48
|
boundingBox?: 'square' | 'default' | 'cropped' | 'encircled';
|
|
48
49
|
squircle?: boolean;
|
|
50
|
+
encircledSquircleScale?: number;
|
|
49
51
|
class?: string;
|
|
50
52
|
onClick?: (event: MouseEvent | KeyboardEvent) => void;
|
|
51
53
|
[key: string]: unknown; // Allow any additional props
|
|
52
54
|
}
|
|
53
55
|
|
|
54
|
-
// Squircle clip-path (50% radius, K=2 superellipse
|
|
55
|
-
const SQUIRCLE_CLIP =
|
|
56
|
-
'polygon(50% 0%, 53.05% 0%, 55.96% 0%, 58.74% 0%, 61.38% 0%, 63.89% 0%, 66.27% 0%, 68.54% 0.01%, 70.69% 0.01%, 72.73% 0.02%, 74.66% 0.03%, 76.48% 0.04%, 78.21% 0.06%, 79.84% 0.09%, 81.37% 0.11%, 82.82% 0.15%, 84.18% 0.2%, 85.46% 0.25%, 86.66% 0.31%, 87.78% 0.39%, 88.83% 0.48%, 89.81% 0.58%, 90.73% 0.7%, 91.58% 0.83%, 92.37% 0.99%, 93.11% 1.16%, 93.79% 1.36%, 94.41% 1.58%, 94.99% 1.83%, 95.53% 2.11%, 96.02% 2.41%, 96.47% 2.75%, 96.88% 3.13%, 97.25% 3.53%, 97.59% 3.98%, 97.89% 4.47%, 98.17% 5.01%, 98.42% 5.59%, 98.64% 6.21%, 98.84% 6.89%, 99.01% 7.63%, 99.17% 8.42%, 99.3% 9.27%, 99.42% 10.19%, 99.52% 11.17%, 99.61% 12.22%, 99.69% 13.34%, 99.75% 14.54%, 99.8% 15.82%, 99.85% 17.18%, 99.89% 18.63%, 99.91% 20.16%, 99.94% 21.79%, 99.96% 23.52%, 99.97% 25.34%, 99.98% 27.27%, 99.99% 29.31%, 99.99% 31.46%, 100% 33.73%, 100% 36.11%, 100% 38.62%, 100% 41.26%, 100% 44.04%, 100% 46.95%, 100% 50%, 100% 50%, 100% 53.05%, 100% 55.96%, 100% 58.74%, 100% 61.38%, 100% 63.89%, 100% 66.27%, 99.99% 68.54%, 99.99% 70.69%, 99.98% 72.73%, 99.97% 74.66%, 99.96% 76.48%, 99.94% 78.21%, 99.91% 79.84%, 99.89% 81.37%, 99.85% 82.82%, 99.8% 84.18%, 99.75% 85.46%, 99.69% 86.66%, 99.61% 87.78%, 99.52% 88.83%, 99.42% 89.81%, 99.3% 90.73%, 99.17% 91.58%, 99.01% 92.37%, 98.84% 93.11%, 98.64% 93.79%, 98.42% 94.41%, 98.17% 94.99%, 97.89% 95.53%, 97.59% 96.02%, 97.25% 96.47%, 96.88% 96.88%, 96.47% 97.25%, 96.02% 97.59%, 95.53% 97.89%, 94.99% 98.17%, 94.41% 98.42%, 93.79% 98.64%, 93.11% 98.84%, 92.37% 99.01%, 91.58% 99.17%, 90.73% 99.3%, 89.81% 99.42%, 88.83% 99.52%, 87.78% 99.61%, 86.66% 99.69%, 85.46% 99.75%, 84.18% 99.8%, 82.82% 99.85%, 81.37% 99.89%, 79.84% 99.91%, 78.21% 99.94%, 76.48% 99.96%, 74.66% 99.97%, 72.73% 99.98%, 70.69% 99.99%, 68.54% 99.99%, 66.27% 100%, 63.89% 100%, 61.38% 100%, 58.74% 100%, 55.96% 100%, 53.05% 100%, 50% 100%, 50% 100%, 46.95% 100%, 44.04% 100%, 41.26% 100%, 38.62% 100%, 36.11% 100%, 33.73% 100%, 31.46% 99.99%, 29.31% 99.99%, 27.27% 99.98%, 25.34% 99.97%, 23.52% 99.96%, 21.79% 99.94%, 20.16% 99.91%, 18.63% 99.89%, 17.18% 99.85%, 15.82% 99.8%, 14.54% 99.75%, 13.34% 99.69%, 12.22% 99.61%, 11.17% 99.52%, 10.19% 99.42%, 9.27% 99.3%, 8.42% 99.17%, 7.63% 99.01%, 6.89% 98.84%, 6.21% 98.64%, 5.59% 98.42%, 5.01% 98.17%, 4.47% 97.89%, 3.98% 97.59%, 3.53% 97.25%, 3.13% 96.88%, 2.75% 96.47%, 2.41% 96.02%, 2.11% 95.53%, 1.83% 94.99%, 1.58% 94.41%, 1.36% 93.79%, 1.16% 93.11%, 0.99% 92.37%, 0.83% 91.58%, 0.7% 90.73%, 0.58% 89.81%, 0.48% 88.83%, 0.39% 87.78%, 0.31% 86.66%, 0.25% 85.46%, 0.2% 84.18%, 0.15% 82.82%, 0.11% 81.37%, 0.09% 79.84%, 0.06% 78.21%, 0.04% 76.48%, 0.03% 74.66%, 0.02% 72.73%, 0.01% 70.69%, 0.01% 68.54%, 0% 66.27%, 0% 63.89%, 0% 61.38%, 0% 58.74%, 0% 55.96%, 0% 53.05%, 0% 50%, 0% 50%, 0% 46.95%, 0% 44.04%, 0% 41.26%, 0% 38.62%, 0% 36.11%, 0% 33.73%, 0.01% 31.46%, 0.01% 29.31%, 0.02% 27.27%, 0.03% 25.34%, 0.04% 23.52%, 0.06% 21.79%, 0.09% 20.16%, 0.11% 18.63%, 0.15% 17.18%, 0.2% 15.82%, 0.25% 14.54%, 0.31% 13.34%, 0.39% 12.22%, 0.48% 11.17%, 0.58% 10.19%, 0.7% 9.27%, 0.83% 8.42%, 0.99% 7.63%, 1.16% 6.89%, 1.36% 6.21%, 1.58% 5.59%, 1.83% 5.01%, 2.11% 4.47%, 2.41% 3.98%, 2.75% 3.53%, 3.13% 3.13%, 3.53% 2.75%, 3.98% 2.41%, 4.47% 2.11%, 5.01% 1.83%, 5.59% 1.58%, 6.21% 1.36%, 6.89% 1.16%, 7.63% 0.99%, 8.42% 0.83%, 9.27% 0.7%, 10.19% 0.58%, 11.17% 0.48%, 12.22% 0.39%, 13.34% 0.31%, 14.54% 0.25%, 15.82% 0.2%, 17.18% 0.15%, 18.63% 0.11%, 20.16% 0.09%, 21.79% 0.06%, 23.52% 0.04%, 25.34% 0.03%, 27.27% 0.02%, 29.31% 0.01%, 31.46% 0.01%, 33.73% 0%, 36.11% 0%, 38.62% 0%, 41.26% 0%, 44.04% 0%, 46.95% 0%, 50% 0%)';
|
|
56
|
+
// Squircle clip-path (50% radius, K=2 superellipse) — generated from true Lamé curve
|
|
57
|
+
const SQUIRCLE_CLIP = generateCornerPolygon(50, 'squircle');
|
|
57
58
|
|
|
58
59
|
let {
|
|
59
60
|
size = '100%',
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
ripplesOptions: ripplesOptionsProp = {},
|
|
62
63
|
boundingBox = 'default',
|
|
63
64
|
squircle = false,
|
|
65
|
+
encircledSquircleScale = 1.08,
|
|
64
66
|
class: className = '',
|
|
65
67
|
onClick = undefined,
|
|
66
68
|
...restProps
|
|
@@ -74,13 +76,13 @@
|
|
|
74
76
|
const LIG_ORIG_T = -65.75;
|
|
75
77
|
const BLUR_PAD_ORIG = 50; // shadow extends 50px beyond ligature on each side
|
|
76
78
|
|
|
77
|
-
// Squircle: scaled to align inner corners with squircle boundary
|
|
78
|
-
// (
|
|
79
|
-
const LIG_SQRC_W =
|
|
80
|
-
const LIG_SQRC_H =
|
|
81
|
-
const LIG_SQRC_L =
|
|
82
|
-
const LIG_SQRC_T = -
|
|
83
|
-
const BLUR_PAD_SQRC =
|
|
79
|
+
// Squircle: scaled to align inner corners with true Lamé squircle boundary
|
|
80
|
+
// (scale 0.953 from previous squircle base, offset x=-18.1, y=+12.8 in 532-grid)
|
|
81
|
+
const LIG_SQRC_W = 405.2;
|
|
82
|
+
const LIG_SQRC_H = 613.6;
|
|
83
|
+
const LIG_SQRC_L = 121.4;
|
|
84
|
+
const LIG_SQRC_T = -19.8;
|
|
85
|
+
const BLUR_PAD_SQRC = 46.0;
|
|
84
86
|
|
|
85
87
|
// Select positioning values depending on squircle mode
|
|
86
88
|
let ligW = $derived(squircle ? LIG_SQRC_W : LIG_ORIG_W);
|
|
@@ -408,6 +410,7 @@
|
|
|
408
410
|
|
|
409
411
|
<logo-container
|
|
410
412
|
style:--size={size}
|
|
413
|
+
style:--encircled-squircle-scale={encircledSquircleScale}
|
|
411
414
|
class="{boundingBox} {className}"
|
|
412
415
|
class:squircle
|
|
413
416
|
role="none"
|
|
@@ -484,11 +487,11 @@
|
|
|
484
487
|
top: calc((100% - 100% / 1.5037) / 2);
|
|
485
488
|
}
|
|
486
489
|
|
|
487
|
-
/* Squircle + encircled: scale up
|
|
490
|
+
/* Squircle + encircled: scale up to compensate for smaller ligature */
|
|
488
491
|
&.encircled.squircle grid-logo {
|
|
489
|
-
width: calc(100% / 1.5037 * 1.04);
|
|
490
|
-
left: calc((100% - 100% / 1.5037 * 1.04) / 2);
|
|
491
|
-
top: calc((100% - 100% / 1.5037 * 1.04) / 2);
|
|
492
|
+
width: calc(100% / 1.5037 * var(--encircled-squircle-scale, 1.04));
|
|
493
|
+
left: calc((100% - 100% / 1.5037 * var(--encircled-squircle-scale, 1.04)) / 2);
|
|
494
|
+
top: calc((100% - 100% / 1.5037 * var(--encircled-squircle-scale, 1.04)) / 2);
|
|
492
495
|
}
|
|
493
496
|
|
|
494
497
|
/* Cropped bounding box mode - grid scaled and positioned to match reference SVG */
|
|
@@ -8,6 +8,7 @@ interface Props {
|
|
|
8
8
|
ripplesOptions?: RipplesOptions;
|
|
9
9
|
boundingBox?: 'square' | 'default' | 'cropped' | 'encircled';
|
|
10
10
|
squircle?: boolean;
|
|
11
|
+
encircledSquircleScale?: number;
|
|
11
12
|
class?: string;
|
|
12
13
|
onClick?: (event: MouseEvent | KeyboardEvent) => void;
|
|
13
14
|
[key: string]: unknown;
|
|
@@ -38,7 +38,7 @@ export declare function generateFaviconHtml(appInfo: AppInfo): string;
|
|
|
38
38
|
* static/logo.webp
|
|
39
39
|
* static/logo.svg
|
|
40
40
|
* static/manifest.webmanifest
|
|
41
|
-
*
|
|
42
|
-
*
|
|
41
|
+
* .logo/config.json
|
|
42
|
+
* .logo/favicon.htm
|
|
43
43
|
*/
|
|
44
44
|
export declare function generateZipKit(config: AppLogoConfig, appInfo: AppInfo): Promise<Blob>;
|
|
@@ -63,8 +63,8 @@ export function generateFaviconHtml(appInfo) {
|
|
|
63
63
|
* static/logo.webp
|
|
64
64
|
* static/logo.svg
|
|
65
65
|
* static/manifest.webmanifest
|
|
66
|
-
*
|
|
67
|
-
*
|
|
66
|
+
* .logo/config.json
|
|
67
|
+
* .logo/favicon.htm
|
|
68
68
|
*/
|
|
69
69
|
export async function generateZipKit(config, appInfo) {
|
|
70
70
|
const [faviconSet, logoPng, logoWebp, logoSvg] = await Promise.all([
|
|
@@ -75,8 +75,7 @@ export async function generateZipKit(config, appInfo) {
|
|
|
75
75
|
]);
|
|
76
76
|
const zip = new JSZip();
|
|
77
77
|
const staticDir = zip.folder('static');
|
|
78
|
-
const
|
|
79
|
-
const snippetsDir = zip.folder('_snippets');
|
|
78
|
+
const logoDir = zip.folder('.logo');
|
|
80
79
|
// Favicon files
|
|
81
80
|
staticDir.file('favicon.ico', faviconSet.ico);
|
|
82
81
|
staticDir.file('icon.svg', faviconSet.svg);
|
|
@@ -90,8 +89,8 @@ export async function generateZipKit(config, appInfo) {
|
|
|
90
89
|
// Manifest
|
|
91
90
|
staticDir.file('manifest.webmanifest', generateManifest(appInfo));
|
|
92
91
|
// Config for regeneration
|
|
93
|
-
|
|
92
|
+
logoDir.file('config.json', JSON.stringify(config, null, '\t'));
|
|
94
93
|
// HTML snippet
|
|
95
|
-
|
|
94
|
+
logoDir.file('favicon.htm', generateFaviconHtml(appInfo));
|
|
96
95
|
return zip.generateAsync({ type: 'blob' });
|
|
97
96
|
}
|
|
@@ -1,43 +1,63 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Superellipse / corner-shape path generation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the trigonometric Lamé parametrisation for SVG outline paths.
|
|
5
|
+
* This is the only approach that is both edge-tangent (no cusps at shared
|
|
6
|
+
* tangent points) and exactly traces the correct Lamé curve shape.
|
|
6
7
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The Lamé curve for a superellipse of parameter `p`:
|
|
9
|
+
* |a|^(2·|p|) + |b|^(2·|p|) = 1
|
|
10
|
+
*
|
|
11
|
+
* Sampled as: θ ∈ [π/2 → 0], e = 1/|p|
|
|
12
|
+
* a(θ) = cos(θ)^e (0 → 1, toward end tangent)
|
|
13
|
+
* b(θ) = sin(θ)^e (1 → 0, toward start tangent)
|
|
14
|
+
* point = centre + a·(end−centre) + b·(start−centre)
|
|
15
|
+
*
|
|
16
|
+
* The arc departs start tangent to the start edge and arrives at end tangent
|
|
17
|
+
* to the end edge — so no cusps even when adjacent arcs meet at 50% radius.
|
|
18
|
+
*
|
|
19
|
+
* Exponent mapping — two-sided, continuous at param=0:
|
|
20
|
+
*
|
|
21
|
+
* param ≥ 0: e = 2^(1−param) (exponential shrink, matches CSS K=2^param)
|
|
22
|
+
* param < 0: e = 2 − 2·param = 2·(1+|param|) (linear grow)
|
|
23
|
+
*
|
|
24
|
+
* Both branches give e=2 at param=0 (bevel). The negative side grows more slowly
|
|
25
|
+
* than the mirrored positive side, matching Chrome's usable range of -16 to +16.
|
|
26
|
+
*
|
|
27
|
+
* param = −10 → e=22 → near-notch (a+b≈0.001)
|
|
28
|
+
* param = −3 → e=8 → deep scoop (a+b=0.125)
|
|
29
|
+
* param = −1 → e=4 → scoop (a+b=0.5)
|
|
30
|
+
* param = 0 → e=2 → bevel (a+b=1.0)
|
|
31
|
+
* param = +1 → e=1 → round (a+b=1.414, exact arc)
|
|
32
|
+
* param = +2 → e=0.5 → squircle (a+b=1.682)
|
|
33
|
+
* param = +10 → e≈0.002 → near-square (a+b=1.999)
|
|
34
|
+
*
|
|
35
|
+
* Positive param: arc bows toward outer corner → convex rounding.
|
|
36
|
+
* Negative param: arc bows toward inner centre → concave scoop shapes.
|
|
10
37
|
*
|
|
11
38
|
* @see https://drafts.csswg.org/css-borders-4/#corner-shape-rendering
|
|
12
39
|
*/
|
|
13
40
|
import type { CornerShape } from './types.js';
|
|
14
|
-
/**
|
|
15
|
-
* Map a CornerShape keyword or superellipse(n) to its numeric curvature parameter.
|
|
16
|
-
*
|
|
17
|
-
* CSS spec keyword -> superellipse parameter:
|
|
18
|
-
* round -> 1 (standard elliptical arc)
|
|
19
|
-
* squircle -> 2 (iOS-style continuous curvature)
|
|
20
|
-
* square -> Infinity (sharp 90 deg corner)
|
|
21
|
-
* bevel -> 0 (straight diagonal cut)
|
|
22
|
-
* scoop -> -1 (inward concave curve)
|
|
23
|
-
* notch -> -Infinity (inward right-angle cut)
|
|
24
|
-
*/
|
|
25
41
|
export declare function cornerShapeToK(shape: CornerShape): number;
|
|
42
|
+
export declare function generateCornerPath(size: number, cornerRadius: number, cornerShape: CornerShape): string;
|
|
26
43
|
/**
|
|
27
|
-
* Generate
|
|
28
|
-
* superellipse corner shape.
|
|
44
|
+
* Generate a CSS `polygon()` string for a superellipse corner shape.
|
|
29
45
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
46
|
+
* Runs `generateCornerPath` on a 100×100 grid and converts the resulting
|
|
47
|
+
* SVG path points to percentage coordinates, producing a CSS polygon that
|
|
48
|
+
* scales with the element.
|
|
33
49
|
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
|
|
50
|
+
* @param cornerRadius Corner radius as percentage (0-50). Default: 50
|
|
51
|
+
* @param cornerShape CornerShape keyword or superellipse(n). Default: 'squircle'
|
|
52
|
+
*/
|
|
53
|
+
export declare function generateCornerPolygon(cornerRadius?: number, cornerShape?: CornerShape): string;
|
|
54
|
+
/**
|
|
55
|
+
* Generate percentage-based polygon points for a superellipse corner shape.
|
|
56
|
+
*
|
|
57
|
+
* Returns an array of [x%, y%] pairs (0-100 range), suitable for conversion
|
|
58
|
+
* to SVG `<polygon>` points at any absolute size.
|
|
37
59
|
*
|
|
38
|
-
* @param
|
|
39
|
-
* @param
|
|
40
|
-
* @param cornerShape - CornerShape keyword or superellipse(n)
|
|
41
|
-
* @returns SVG path `d` string
|
|
60
|
+
* @param cornerRadius Corner radius as percentage (0-50). Default: 50
|
|
61
|
+
* @param cornerShape CornerShape keyword or superellipse(n). Default: 'squircle'
|
|
42
62
|
*/
|
|
43
|
-
export declare function
|
|
63
|
+
export declare function generateCornerPolygonPoints(cornerRadius?: number, cornerShape?: CornerShape): [number, number][];
|
|
@@ -1,28 +1,44 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Superellipse / corner-shape path generation.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Uses the trigonometric Lamé parametrisation for SVG outline paths.
|
|
5
|
+
* This is the only approach that is both edge-tangent (no cusps at shared
|
|
6
|
+
* tangent points) and exactly traces the correct Lamé curve shape.
|
|
6
7
|
*
|
|
7
|
-
* The
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* The Lamé curve for a superellipse of parameter `p`:
|
|
9
|
+
* |a|^(2·|p|) + |b|^(2·|p|) = 1
|
|
10
|
+
*
|
|
11
|
+
* Sampled as: θ ∈ [π/2 → 0], e = 1/|p|
|
|
12
|
+
* a(θ) = cos(θ)^e (0 → 1, toward end tangent)
|
|
13
|
+
* b(θ) = sin(θ)^e (1 → 0, toward start tangent)
|
|
14
|
+
* point = centre + a·(end−centre) + b·(start−centre)
|
|
15
|
+
*
|
|
16
|
+
* The arc departs start tangent to the start edge and arrives at end tangent
|
|
17
|
+
* to the end edge — so no cusps even when adjacent arcs meet at 50% radius.
|
|
18
|
+
*
|
|
19
|
+
* Exponent mapping — two-sided, continuous at param=0:
|
|
20
|
+
*
|
|
21
|
+
* param ≥ 0: e = 2^(1−param) (exponential shrink, matches CSS K=2^param)
|
|
22
|
+
* param < 0: e = 2 − 2·param = 2·(1+|param|) (linear grow)
|
|
23
|
+
*
|
|
24
|
+
* Both branches give e=2 at param=0 (bevel). The negative side grows more slowly
|
|
25
|
+
* than the mirrored positive side, matching Chrome's usable range of -16 to +16.
|
|
26
|
+
*
|
|
27
|
+
* param = −10 → e=22 → near-notch (a+b≈0.001)
|
|
28
|
+
* param = −3 → e=8 → deep scoop (a+b=0.125)
|
|
29
|
+
* param = −1 → e=4 → scoop (a+b=0.5)
|
|
30
|
+
* param = 0 → e=2 → bevel (a+b=1.0)
|
|
31
|
+
* param = +1 → e=1 → round (a+b=1.414, exact arc)
|
|
32
|
+
* param = +2 → e=0.5 → squircle (a+b=1.682)
|
|
33
|
+
* param = +10 → e≈0.002 → near-square (a+b=1.999)
|
|
34
|
+
*
|
|
35
|
+
* Positive param: arc bows toward outer corner → convex rounding.
|
|
36
|
+
* Negative param: arc bows toward inner centre → concave scoop shapes.
|
|
10
37
|
*
|
|
11
38
|
* @see https://drafts.csswg.org/css-borders-4/#corner-shape-rendering
|
|
12
39
|
*/
|
|
13
|
-
/**
|
|
40
|
+
/** Segments per corner arc. */
|
|
14
41
|
const SEGMENTS = 64;
|
|
15
|
-
/**
|
|
16
|
-
* Map a CornerShape keyword or superellipse(n) to its numeric curvature parameter.
|
|
17
|
-
*
|
|
18
|
-
* CSS spec keyword -> superellipse parameter:
|
|
19
|
-
* round -> 1 (standard elliptical arc)
|
|
20
|
-
* squircle -> 2 (iOS-style continuous curvature)
|
|
21
|
-
* square -> Infinity (sharp 90 deg corner)
|
|
22
|
-
* bevel -> 0 (straight diagonal cut)
|
|
23
|
-
* scoop -> -1 (inward concave curve)
|
|
24
|
-
* notch -> -Infinity (inward right-angle cut)
|
|
25
|
-
*/
|
|
26
42
|
export function cornerShapeToK(shape) {
|
|
27
43
|
switch (shape) {
|
|
28
44
|
case 'round':
|
|
@@ -38,7 +54,6 @@ export function cornerShapeToK(shape) {
|
|
|
38
54
|
case 'notch':
|
|
39
55
|
return -Infinity;
|
|
40
56
|
default: {
|
|
41
|
-
// superellipse(n) -- parse the numeric value
|
|
42
57
|
const match = shape.match(/^superellipse\((.+)\)$/);
|
|
43
58
|
if (match) {
|
|
44
59
|
const val = match[1].trim();
|
|
@@ -50,164 +65,145 @@ export function cornerShapeToK(shape) {
|
|
|
50
65
|
if (!isNaN(n))
|
|
51
66
|
return n;
|
|
52
67
|
}
|
|
53
|
-
return 1;
|
|
68
|
+
return 1;
|
|
54
69
|
}
|
|
55
70
|
}
|
|
56
71
|
}
|
|
57
|
-
/**
|
|
58
|
-
* Generate an SVG path `d` attribute for a rounded rectangle with a
|
|
59
|
-
* superellipse corner shape.
|
|
60
|
-
*
|
|
61
|
-
* The curve for each corner is sampled parametrically per the CSS Borders L4 spec:
|
|
62
|
-
* K = 2^abs(curvature)
|
|
63
|
-
* point(T) = mapPointToCorner(T^K, (1-T)^K) for T in [0, 1]
|
|
64
|
-
*
|
|
65
|
-
* For convex shapes (curvature > 0), the curve bulges outward.
|
|
66
|
-
* For concave shapes (curvature < 0), the center reference flips to the
|
|
67
|
-
* outer corner, creating inward "scoop" curves.
|
|
68
|
-
*
|
|
69
|
-
* @param size - Square side length in pixels
|
|
70
|
-
* @param cornerRadius - Corner radius as percentage (0-50)
|
|
71
|
-
* @param cornerShape - CornerShape keyword or superellipse(n)
|
|
72
|
-
* @returns SVG path `d` string
|
|
73
|
-
*/
|
|
74
72
|
export function generateCornerPath(size, cornerRadius, cornerShape) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
}
|
|
79
|
-
const curvature = cornerShapeToK(cornerShape);
|
|
80
|
-
// square (Infinity): sharp corners, radius is ignored
|
|
81
|
-
if (curvature === Infinity) {
|
|
82
|
-
return `M0,0 H${size} V${size} H0 Z`;
|
|
83
|
-
}
|
|
84
|
-
// Convert percentage to pixels, clamped to half the side
|
|
73
|
+
if (cornerRadius <= 0)
|
|
74
|
+
return squarePath(size);
|
|
75
|
+
const param = cornerShapeToK(cornerShape);
|
|
85
76
|
const r = Math.min((cornerRadius / 100) * size, size / 2);
|
|
86
|
-
//
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
`H${rd(r)}`,
|
|
101
|
-
// bottom-left: inward to center, then out
|
|
102
|
-
`L${rd(r)},${rd(size - r)}`,
|
|
103
|
-
`L0,${rd(size - r)}`,
|
|
104
|
-
`V${rd(r)}`,
|
|
105
|
-
// top-left: inward to center, then out
|
|
106
|
-
`L${rd(r)},${rd(r)}`,
|
|
107
|
-
`L${rd(r)},0`,
|
|
108
|
-
`Z`
|
|
109
|
-
].join(' ');
|
|
110
|
-
}
|
|
111
|
-
// bevel (0): straight diagonal cut from tangent to tangent
|
|
112
|
-
if (curvature === 0) {
|
|
113
|
-
return [
|
|
114
|
-
`M${rd(r)},0`,
|
|
115
|
-
`H${rd(size - r)}`,
|
|
116
|
-
`L${rd(size)},${rd(r)}`,
|
|
117
|
-
`V${rd(size - r)}`,
|
|
118
|
-
`L${rd(size - r)},${rd(size)}`,
|
|
119
|
-
`H${rd(r)}`,
|
|
120
|
-
`L0,${rd(size - r)}`,
|
|
121
|
-
`V${rd(r)}`,
|
|
122
|
-
`Z`
|
|
123
|
-
].join(' ');
|
|
124
|
-
}
|
|
125
|
-
// round (1): use SVG arc commands for a perfect quarter-ellipse.
|
|
126
|
-
// This matches what browsers render for border-radius and is exact,
|
|
127
|
-
// unlike the parametric approximation.
|
|
128
|
-
if (curvature === 1) {
|
|
129
|
-
return [
|
|
130
|
-
`M${rd(r)},0`,
|
|
131
|
-
`H${rd(size - r)}`,
|
|
132
|
-
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size)},${rd(r)}`,
|
|
133
|
-
`V${rd(size - r)}`,
|
|
134
|
-
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size - r)},${rd(size)}`,
|
|
135
|
-
`H${rd(r)}`,
|
|
136
|
-
`A${rd(r)},${rd(r)} 0 0 1 0,${rd(size - r)}`,
|
|
137
|
-
`V${rd(r)}`,
|
|
138
|
-
`A${rd(r)},${rd(r)} 0 0 1 ${rd(r)},0`,
|
|
139
|
-
`Z`
|
|
140
|
-
].join(' ');
|
|
141
|
-
}
|
|
142
|
-
// Parametric superellipse curve.
|
|
143
|
-
// K = 2^abs(curvature) per the CSS spec: "Let K be 0.5^(-abs(curvature))"
|
|
144
|
-
const K = Math.pow(2, Math.abs(curvature));
|
|
145
|
-
// Invert: for the outline path (not clip-out), convex shapes use outer corner as ref
|
|
146
|
-
const useOuterAsRef = curvature > 0;
|
|
147
|
-
// Sample the corner arc and return SVG line-to commands.
|
|
148
|
-
//
|
|
149
|
-
// The CSS spec generates a "clip-out" path (what to REMOVE from the rect).
|
|
150
|
-
// Since we generate the OUTLINE path (what to KEEP), we invert the reference:
|
|
151
|
-
// - Convex (squircle, round): curveCenter = outerCorner (curve bows outward)
|
|
152
|
-
// - Concave (scoop): curveCenter = cornerCenter (curve bows inward)
|
|
153
|
-
//
|
|
154
|
-
// The parametric curve maps (T^K, (1-T)^K) through mapPointToCorner,
|
|
155
|
-
// which scales by vectors from curveCenter to the two tangent points.
|
|
77
|
+
// ── Degenerate / exact cases ───────────────────────────────────────────────
|
|
78
|
+
if (!isFinite(param))
|
|
79
|
+
return param > 0 ? squarePath(size) : notchPath(size, r);
|
|
80
|
+
// param=0: e=2 → bevel (|a|+|b|=1, straight diagonal)
|
|
81
|
+
if (param === 0)
|
|
82
|
+
return bevelPath(size, r);
|
|
83
|
+
// param=1: exact SVG quarter-ellipse arc (round)
|
|
84
|
+
if (param === 1)
|
|
85
|
+
return roundPath(size, r);
|
|
86
|
+
// ── Trig Lamé ──────────────────────────────────────────────────────────────
|
|
87
|
+
// e = 2^(1-p) for p ≥ 0 → e shrinks exponentially: bevel→circle→squircle→square
|
|
88
|
+
// e = 2-2p for p < 0 → e grows linearly: bevel→scoop→notch
|
|
89
|
+
// Both give e=2 at p=0 (bevel). Continuous, no special case needed at p=-1.
|
|
90
|
+
const e = param >= 0 ? Math.pow(2, 1 - param) : 2 - 2 * param;
|
|
156
91
|
const parts = [`M${rd(size - r)},0`];
|
|
157
|
-
// Top-right
|
|
158
|
-
parts.push(
|
|
159
|
-
// Right edge
|
|
92
|
+
// Top-right: centre=(size−r, r)
|
|
93
|
+
parts.push(sampleArc(size - r, 0, size, r, size - r, r, r, e));
|
|
160
94
|
parts.push(`L${rd(size)},${rd(size - r)}`);
|
|
161
|
-
// Bottom-right
|
|
162
|
-
parts.push(
|
|
163
|
-
// Bottom edge
|
|
95
|
+
// Bottom-right: centre=(size−r, size−r)
|
|
96
|
+
parts.push(sampleArc(size, size - r, size - r, size, size - r, size - r, r, e));
|
|
164
97
|
parts.push(`L${rd(r)},${rd(size)}`);
|
|
165
|
-
// Bottom-left
|
|
166
|
-
parts.push(
|
|
167
|
-
// Left edge
|
|
98
|
+
// Bottom-left: centre=(r, size−r)
|
|
99
|
+
parts.push(sampleArc(r, size, 0, size - r, r, size - r, r, e));
|
|
168
100
|
parts.push(`L0,${rd(r)}`);
|
|
169
|
-
// Top-left
|
|
170
|
-
parts.push(
|
|
101
|
+
// Top-left: centre=(r, r)
|
|
102
|
+
parts.push(sampleArc(0, r, r, 0, r, r, r, e));
|
|
171
103
|
parts.push('Z');
|
|
172
104
|
return parts.join(' ');
|
|
173
105
|
}
|
|
174
106
|
/**
|
|
175
|
-
* Sample
|
|
176
|
-
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
* @param useOuterAsRef - Whether to use outer corner as the curve reference point
|
|
107
|
+
* Sample one corner arc via the trigonometric Lamé parametrisation.
|
|
108
|
+
*
|
|
109
|
+
* θ: π/2 → 0, a = cos(θ)^e, b = sin(θ)^e
|
|
110
|
+
* point = centre + a·(end−centre) + b·(start−centre)
|
|
111
|
+
*
|
|
112
|
+
* Traces |a|^(2/e) + |b|^(2/e) = 1 exactly.
|
|
113
|
+
* Departs start tangent to the start edge; arrives at end tangent to end edge.
|
|
183
114
|
*/
|
|
184
|
-
function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const refX = useOuterAsRef ? ox : cx;
|
|
188
|
-
const refY = useOuterAsRef ? oy : cy;
|
|
189
|
-
// Vectors from curveCenter to the tangent points
|
|
190
|
-
const toEndX = ex - refX;
|
|
191
|
-
const toEndY = ey - refY;
|
|
192
|
-
const toStartX = sx - refX;
|
|
193
|
-
const toStartY = sy - refY;
|
|
115
|
+
function sampleArc(sx, sy, ex, ey, cx, cy, r, e) {
|
|
116
|
+
const ux = (ex - cx) / r, uy = (ey - cy) / r;
|
|
117
|
+
const vx = (sx - cx) / r, vy = (sy - cy) / r;
|
|
194
118
|
const cmds = [];
|
|
195
|
-
// Sample T from 0 to 1 (exclusive of 0 since we're already at start)
|
|
196
119
|
for (let i = 1; i <= SEGMENTS; i++) {
|
|
197
|
-
const
|
|
198
|
-
const
|
|
199
|
-
const
|
|
200
|
-
|
|
201
|
-
// refPoint + toEnd * xParam + toStart * yParam
|
|
202
|
-
const px = refX + toEndX * xParam + toStartX * yParam;
|
|
203
|
-
const py = refY + toEndY * xParam + toStartY * yParam;
|
|
204
|
-
cmds.push(`L${rd(px)},${rd(py)}`);
|
|
120
|
+
const theta = (1 - i / SEGMENTS) * (Math.PI / 2);
|
|
121
|
+
const a = Math.pow(Math.cos(theta), e);
|
|
122
|
+
const b = Math.pow(Math.sin(theta), e);
|
|
123
|
+
cmds.push(`L${rd(cx + r * (ux * a + vx * b))},${rd(cy + r * (uy * a + vy * b))}`);
|
|
205
124
|
}
|
|
206
125
|
return cmds.join(' ');
|
|
207
126
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
127
|
+
// ── Exact path helpers ────────────────────────────────────────────────────────
|
|
128
|
+
function squarePath(size) {
|
|
129
|
+
return `M0,0 H${size} V${size} H0 Z`;
|
|
130
|
+
}
|
|
131
|
+
function roundPath(size, r) {
|
|
132
|
+
return [
|
|
133
|
+
`M${rd(r)},0 H${rd(size - r)}`,
|
|
134
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size)},${rd(r)}`,
|
|
135
|
+
`V${rd(size - r)}`,
|
|
136
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(size - r)},${rd(size)}`,
|
|
137
|
+
`H${rd(r)}`,
|
|
138
|
+
`A${rd(r)},${rd(r)} 0 0 1 0,${rd(size - r)}`,
|
|
139
|
+
`V${rd(r)}`,
|
|
140
|
+
`A${rd(r)},${rd(r)} 0 0 1 ${rd(r)},0 Z`
|
|
141
|
+
].join(' ');
|
|
142
|
+
}
|
|
143
|
+
function bevelPath(size, r) {
|
|
144
|
+
return [
|
|
145
|
+
`M${rd(r)},0 H${rd(size - r)}`,
|
|
146
|
+
`L${rd(size)},${rd(r)} V${rd(size - r)}`,
|
|
147
|
+
`L${rd(size - r)},${rd(size)} H${rd(r)}`,
|
|
148
|
+
`L0,${rd(size - r)} V${rd(r)} Z`
|
|
149
|
+
].join(' ');
|
|
150
|
+
}
|
|
151
|
+
function notchPath(size, r) {
|
|
152
|
+
return [
|
|
153
|
+
`M${rd(r)},0 H${rd(size - r)}`,
|
|
154
|
+
`L${rd(size - r)},${rd(r)} L${rd(size)},${rd(r)}`,
|
|
155
|
+
`V${rd(size - r)} L${rd(size - r)},${rd(size - r)}`,
|
|
156
|
+
`L${rd(size - r)},${rd(size)} H${rd(r)}`,
|
|
157
|
+
`L${rd(r)},${rd(size - r)} L0,${rd(size - r)}`,
|
|
158
|
+
`V${rd(r)} L${rd(r)},${rd(r)} L${rd(r)},0 Z`
|
|
159
|
+
].join(' ');
|
|
160
|
+
}
|
|
211
161
|
function rd(n) {
|
|
212
162
|
return Math.round(n * 100) / 100;
|
|
213
163
|
}
|
|
164
|
+
// ── Polygon helpers ───────────────────────────────────────────────────────────
|
|
165
|
+
/**
|
|
166
|
+
* Extract (x, y) coordinate pairs from an SVG path `d` string.
|
|
167
|
+
*
|
|
168
|
+
* Parses M and L commands (the only ones `generateCornerPath` emits for
|
|
169
|
+
* non-degenerate Lamé curves). Ignores H/V/A/Z.
|
|
170
|
+
*/
|
|
171
|
+
function pathToPoints(d) {
|
|
172
|
+
const pts = [];
|
|
173
|
+
// Match M or L followed by x,y
|
|
174
|
+
for (const m of d.matchAll(/[ML]\s*([\d.e+-]+)\s*,\s*([\d.e+-]+)/gi)) {
|
|
175
|
+
pts.push([parseFloat(m[1]), parseFloat(m[2])]);
|
|
176
|
+
}
|
|
177
|
+
// Also pick up H (horizontal-line-to) and V (vertical-line-to) for degenerate paths
|
|
178
|
+
// Not needed for squircle, but included for completeness with round paths
|
|
179
|
+
return pts;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Generate a CSS `polygon()` string for a superellipse corner shape.
|
|
183
|
+
*
|
|
184
|
+
* Runs `generateCornerPath` on a 100×100 grid and converts the resulting
|
|
185
|
+
* SVG path points to percentage coordinates, producing a CSS polygon that
|
|
186
|
+
* scales with the element.
|
|
187
|
+
*
|
|
188
|
+
* @param cornerRadius Corner radius as percentage (0-50). Default: 50
|
|
189
|
+
* @param cornerShape CornerShape keyword or superellipse(n). Default: 'squircle'
|
|
190
|
+
*/
|
|
191
|
+
export function generateCornerPolygon(cornerRadius = 50, cornerShape = 'squircle') {
|
|
192
|
+
const path = generateCornerPath(100, cornerRadius, cornerShape);
|
|
193
|
+
const pts = pathToPoints(path);
|
|
194
|
+
const coords = pts.map(([x, y]) => `${rd(x)}% ${rd(y)}%`).join(', ');
|
|
195
|
+
return `polygon(${coords})`;
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Generate percentage-based polygon points for a superellipse corner shape.
|
|
199
|
+
*
|
|
200
|
+
* Returns an array of [x%, y%] pairs (0-100 range), suitable for conversion
|
|
201
|
+
* to SVG `<polygon>` points at any absolute size.
|
|
202
|
+
*
|
|
203
|
+
* @param cornerRadius Corner radius as percentage (0-50). Default: 50
|
|
204
|
+
* @param cornerShape CornerShape keyword or superellipse(n). Default: 'squircle'
|
|
205
|
+
*/
|
|
206
|
+
export function generateCornerPolygonPoints(cornerRadius = 50, cornerShape = 'squircle') {
|
|
207
|
+
const path = generateCornerPath(100, cornerRadius, cornerShape);
|
|
208
|
+
return pathToPoints(path);
|
|
209
|
+
}
|