@leftium/logo 0.2.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.
@@ -142,5 +142,10 @@
142
142
  top: 50%;
143
143
  left: 50%;
144
144
  translate: -50% -50%;
145
+ overflow: hidden;
146
+ }
147
+
148
+ .app-logo-icon :global(svg) {
149
+ display: block;
145
150
  }
146
151
  </style>
@@ -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, CSS polygon with percentages)
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
- // (base 94.46% * 1.023 scale, offset x=-6.5, y=7)
79
- const LIG_SQRC_W = 425.2;
80
- const LIG_SQRC_H = 643.6;
81
- const LIG_SQRC_L = 129.5;
82
- const LIG_SQRC_T = -47.6;
83
- const BLUR_PAD_SQRC = 48.3;
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 1.04x to compensate for smaller ligature */
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
- * static/_app-logo/config.json
42
- * _snippets/favicon-html.html
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
- * static/_app-logo/config.json
67
- * _snippets/favicon-html.html
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 appLogoDir = staticDir.folder('_app-logo');
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
- appLogoDir.file('config.json', JSON.stringify(config, null, '\t'));
92
+ logoDir.file('config.json', JSON.stringify(config, null, '\t'));
94
93
  // HTML snippet
95
- snippetsDir.file('favicon-html.html', generateFaviconHtml(appInfo));
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
- * Follows the CSS Borders Level 4 specification for corner-shape rendering.
5
- * Maps CornerShape keywords to superellipse parameters and generates SVG paths.
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 CSS spec defines the curve parametrically:
8
- * K = 2^abs(curvature)
9
- * For T in [0, 1]: point = mapPointToCorner(T^K, (1-T)^K)
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 an SVG path `d` attribute for a rounded rectangle with a
28
- * superellipse corner shape.
44
+ * Generate a CSS `polygon()` string for a superellipse corner shape.
29
45
  *
30
- * The curve for each corner is sampled parametrically per the CSS Borders L4 spec:
31
- * K = 2^abs(curvature)
32
- * point(T) = mapPointToCorner(T^K, (1-T)^K) for T in [0, 1]
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
- * For convex shapes (curvature > 0), the curve bulges outward.
35
- * For concave shapes (curvature < 0), the center reference flips to the
36
- * outer corner, creating inward "scoop" curves.
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 size - Square side length in pixels
39
- * @param cornerRadius - Corner radius as percentage (0-50)
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 generateCornerPath(size: number, cornerRadius: number, cornerShape: CornerShape): string;
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
- * Follows the CSS Borders Level 4 specification for corner-shape rendering.
5
- * Maps CornerShape keywords to superellipse parameters and generates SVG paths.
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 CSS spec defines the curve parametrically:
8
- * K = 2^abs(curvature)
9
- * For T in [0, 1]: point = mapPointToCorner(T^K, (1-T)^K)
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
- /** Number of line segments to sample per corner arc. */
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; // fallback to round
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
- // No rounding needed
76
- if (cornerRadius <= 0) {
77
- return `M0,0 H${size} V${size} H0 Z`;
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
- // notch (-Infinity): inward right-angle cut at each corner.
87
- // Goes from tangent point on one edge INWARD to the corner-region center,
88
- // then back out to the tangent point on the other edge (concave square corner).
89
- if (curvature === -Infinity) {
90
- return [
91
- `M${rd(r)},0`,
92
- `H${rd(size - r)}`,
93
- // top-right: inward to center of corner region, then out
94
- `L${rd(size - r)},${rd(r)}`,
95
- `L${rd(size)},${rd(r)}`,
96
- `V${rd(size - r)}`,
97
- // bottom-right: inward to center, then out
98
- `L${rd(size - r)},${rd(size - r)}`,
99
- `L${rd(size - r)},${rd(size)}`,
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 corner: start=(size-r, 0), end=(size, r), outer=(size, 0), center=(size-r, r)
158
- parts.push(sampleCorner(size - r, 0, size, r, size, 0, size - r, r, K, useOuterAsRef));
159
- // Right edge
92
+ // Top-right: centre=(sizer, 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 corner: start=(size, size-r), end=(size-r, size), outer=(size, size), center=(size-r, size-r)
162
- parts.push(sampleCorner(size, size - r, size - r, size, size, size, size - r, size - r, K, useOuterAsRef));
163
- // Bottom edge
95
+ // Bottom-right: centre=(sizer, sizer)
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 corner: start=(r, size), end=(0, size-r), outer=(0, size), center=(r, size-r)
166
- parts.push(sampleCorner(r, size, 0, size - r, 0, size, r, size - r, K, useOuterAsRef));
167
- // Left edge
98
+ // Bottom-left: centre=(r, sizer)
99
+ parts.push(sampleArc(r, size, 0, size - r, r, size - r, r, e));
168
100
  parts.push(`L0,${rd(r)}`);
169
- // Top-left corner: start=(0, r), end=(r, 0), outer=(0, 0), center=(r, r)
170
- parts.push(sampleCorner(0, r, r, 0, 0, 0, r, r, K, useOuterAsRef));
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 a superellipse corner arc as SVG line-to commands.
176
- *
177
- * @param sx, sy - Start point (tangent point on the "start" edge)
178
- * @param ex, ey - End point (tangent point on the "end" edge)
179
- * @param ox, oy - Outer corner point (the actual rectangle corner)
180
- * @param cx, cy - Center point (diagonally opposite the outer corner within the corner region)
181
- * @param K - Exponent: 2^abs(curvature)
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) + (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 sampleCorner(sx, sy, ex, ey, ox, oy, cx, cy, K, useOuterAsRef) {
185
- // For convex shapes (outline path), use outer corner so curve bows outward.
186
- // For concave shapes (outline path), use inner center so curve bows inward.
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 t = i / SEGMENTS;
198
- const xParam = Math.pow(t, K);
199
- const yParam = Math.pow(1 - t, K);
200
- // mapPointToCorner(xParam, yParam):
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
- * Round a number to 2 decimal places for clean SVG output.
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
+ }