[
  {
    "path": ".gitignore",
    "content": "# dependencies\nnode_modules\n.pnp\n.pnp.js\n\n# testing\ncoverage\n\n# next.js\n.next/\nout/\nbuild\n\n# misc\n.DS_Store\n*.pem\n\n# debug\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\n.pnpm-debug.log*\n\n# local env files\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\n# turbo\n.turbo\n\n#parcel\n.parcel-cache\n\n# build\ndist/\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 Tien Pham\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Figma Squircle\n\n[![Stable Release](https://img.shields.io/npm/v/figma-squircle)](https://npm.im/figma-squircle) [![license](https://badgen.now.sh/badge/license/MIT)](./LICENSE)\n\n> Figma-flavored squircles for everyone\n\n## Disclaimer\n\n> This library is not an official product from the Figma team and does not guarantee to produce the same results as you would get in Figma.\n\n## What is this?\n\nFigma has a great feature called [corner smoothing](https://help.figma.com/hc/en-us/articles/360050986854-Adjust-corner-radius-and-smoothing), allowing you to create rounded shapes with a seamless continuous curve (squircles).\n\n![](squircle.jpg)\n\nThis library helps you bring those squircles to your apps.\n\n## Installation\n\n```sh\nnpm install figma-squircle\n```\n\n## Usage\n\n```jsx\nimport { getSvgPath } from 'figma-squircle'\n\nconst svgPath = getSvgPath({\n  width: 200,\n  height: 200,\n  cornerRadius: 24, // defaults to 0\n  cornerSmoothing: 0.8, // cornerSmoothing goes from 0 to 1\n})\n\nconst svgPath = getSvgPath({\n  width: 200,\n  height: 200,\n  cornerRadius: 24,\n  cornerSmoothing: 0.8,\n  // You can also adjust the radius of each corner individually\n  topLeftCornerRadius: 48,\n})\n\n// svgPath can now be used to create SVG elements\nfunction PinkSquircle() {\n  return (\n    <svg width=\"200\" height=\"200\" xmlns=\"http://www.w3.org/2000/svg\">\n      <path d={svgPath} fill=\"pink\" />\n    </svg>\n  )\n}\n\n// Or with the clip-path CSS property\nfunction ProfilePicture() {\n  return (\n    <div\n      style={{\n        width: 200,\n        height: 200,\n        clipPath: `path('${svgPath}')`,\n      }}\n    >\n      ...\n    </div>\n  )\n}\n```\n\n## Preserve Smoothing\n\nThe larger the corner radius, the less space we have left to make a smooth transition from the straight line to the rounded corner. As a result, you might have noticed that the smoothing effect appears to be less pronounced as the radius gets bigger.\n\nTry enabling `preserveSmoothing` if you're not happy with the generated shape. \n\n```jsx\nconst svgPath = getSvgPath({\n  width: 200,\n  height: 200,\n  cornerRadius: 80,\n  cornerSmoothing: 0.8,\n  preserveSmoothing: true, // defaults to false\n})\n```\n\nThere's also a [Figma plugin](https://www.figma.com/community/plugin/1122437229616103296) that utilizes this option.\n\n## Thanks\n\n- Figma team for publishing [this article](https://www.figma.com/blog/desperately-seeking-squircles/) and [MartinRGB](https://github.com/MartinRGB) for [figuring out all the math](https://github.com/MartinRGB/Figma_Squircles_Approximation) behind it.\n- [George Francis](https://github.com/georgedoescode) for creating [Squircley](https://squircley.app/), which was my introduction to squircles.\n\n## Related\n\n- https://github.com/phamfoo/react-native-figma-squircle\n"
  },
  {
    "path": "eslint.config.js",
    "content": "import pluginJs from '@eslint/js'\nimport tseslint from 'typescript-eslint'\n\nexport default [\n  { ignores: ['dist/'] },\n  pluginJs.configs.recommended,\n  ...tseslint.configs.recommended,\n]\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"figma-squircle\",\n  \"description\": \"Figma-flavored squircles for everyone\",\n  \"type\": \"module\",\n  \"author\": \"Tien Pham\",\n  \"version\": \"1.1.0\",\n  \"license\": \"MIT\",\n  \"source\": \"src/index.ts\",\n  \"main\": \"dist/index.js\",\n  \"types\": \"dist/index.d.ts\",\n  \"scripts\": {\n    \"lint\": \"eslint\",\n    \"build\": \"tsup src/index.ts --format esm --dts\",\n    \"prepare\": \"npm run build\"\n  },\n  \"devDependencies\": {\n    \"@eslint/js\": \"^9.9.1\",\n    \"eslint\": \"^9.9.1\",\n    \"prettier\": \"^3.3.3\",\n    \"tsup\": \"^8.3.0\",\n    \"typescript\": \"^5.5.4\",\n    \"typescript-eslint\": \"^8.3.0\"\n  },\n  \"files\": [\n    \"dist\",\n    \"src\"\n  ],\n  \"keywords\": [\n    \"squircle\",\n    \"react\",\n    \"figma\"\n  ],\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"https://github.com/phamfoo/figma-squircle.git\"\n  }\n}\n"
  },
  {
    "path": "prettier.config.js",
    "content": "export default {\n  singleQuote: true,\n  tabWidth: 2,\n  trailingComma: 'es5',\n  useTabs: false,\n  semi: false,\n}\n"
  },
  {
    "path": "site/.gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\nlerna-debug.log*\n\nnode_modules\ndist\ndist-ssr\n*.local\n\n# Editor directories and files\n.vscode/*\n!.vscode/extensions.json\n.idea\n.DS_Store\n*.suo\n*.ntvs*\n*.njsproj\n*.sln\n*.sw?\n"
  },
  {
    "path": "site/index.html",
    "content": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/figma.svg\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <title>Figma Squircle</title>\n    <meta name=\"description\" content=\"Figma-flavored squircles for everyone\">\n  </head>\n  <body>\n    <div id=\"app\"></div>\n    <script type=\"module\" src=\"/src/main.tsx\"></script>\n  </body>\n</html>\n"
  },
  {
    "path": "site/package.json",
    "content": "{\n  \"name\": \"site2\",\n  \"private\": true,\n  \"version\": \"0.0.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite\",\n    \"build\": \"tsc -b && vite build\",\n    \"preview\": \"vite preview\"\n  },\n  \"dependencies\": {\n    \"figma-squircle\": \"file:..\",\n    \"preact\": \"^10.23.1\"\n  },\n  \"devDependencies\": {\n    \"@preact/preset-vite\": \"^2.9.0\",\n    \"autoprefixer\": \"^10.4.20\",\n    \"postcss\": \"^8.4.45\",\n    \"tailwindcss\": \"^3.4.10\",\n    \"typescript\": \"^5.5.3\",\n    \"vite\": \"^5.4.1\"\n  }\n}\n"
  },
  {
    "path": "site/postcss.config.js",
    "content": "export default {\n  plugins: {\n    tailwindcss: {},\n    autoprefixer: {},\n  },\n}\n"
  },
  {
    "path": "site/src/App.tsx",
    "content": "import { useEffect, useState } from 'preact/hooks'\nimport './app.css'\nimport { getSvgPath } from 'figma-squircle'\nimport sizeIcon from './assets/size.svg'\nimport radiusIcon from './assets/radius.svg'\n\nexport function App() {\n  const [size, setSize] = useState(400)\n  const [cornerRadius, setCornerRadius] = useState(120)\n  const [cornerSmoothingPercent, setCornerSmoothingPercent] = useState(60)\n  const [preserveSmoothing, setPreserveSmoothing] = useState(false)\n\n  const svgPath = getSvgPath({\n    width: size,\n    height: size,\n    cornerRadius,\n    cornerSmoothing: cornerSmoothingPercent / 100,\n    preserveSmoothing,\n  })\n\n  return (\n    <div className=\"flex flex-1\">\n      <div className=\"flex flex-1 bg-neutral-900 justify-center items-center\">\n        <svg width={size} height={size} xmlns=\"http://www.w3.org/2000/svg\">\n          <path d={svgPath} fill=\"pink\" />\n        </svg>\n      </div>\n\n      <div className=\"flex flex-col bg-neutral-800 w-80 p-6 gap-8\">\n        <div className=\"flex gap-4\">\n          <div className=\"flex-1\">\n            <NumberInput\n              label=\"Size\"\n              icon={sizeIcon}\n              value={size}\n              onChange={setSize}\n            />\n          </div>\n          <div className=\"flex-1\">\n            <NumberInput\n              label=\"Radius\"\n              icon={radiusIcon}\n              value={cornerRadius}\n              onChange={setCornerRadius}\n            />\n          </div>\n        </div>\n\n        <CornerSmoothingSlider\n          value={cornerSmoothingPercent}\n          onChange={setCornerSmoothingPercent}\n        />\n\n        <PreserveSmoothingToggle\n          value={preserveSmoothing}\n          onChange={setPreserveSmoothing}\n        />\n      </div>\n    </div>\n  )\n}\n\ninterface NumberInputProps {\n  label: string\n  icon: string\n  value: number\n  onChange: (value: number) => void\n}\n\nfunction NumberInput({ label, icon, value, onChange }: NumberInputProps) {\n  return (\n    <label>\n      <span className=\"text-neutral-400 text-xs uppercase font-bold tracking-widest\">\n        {label}\n      </span>\n      <div className=\"flex rounded-sm py-1 -translate-x-2 has-[:focus]:bg-neutral-700 has-[:focus]:ring-2 has-[:focus]:ring-blue-600 transition duration-200 ease-out\">\n        <IconSlider value={value} onChange={onChange} icon={icon} />\n\n        <input\n          value={value}\n          onChange={(e) => onChange(Number(e.currentTarget.value))}\n          type=\"number\"\n          class=\"w-full font-normal bg-transparent text-white text-lg focus:outline-none [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none\"\n        />\n      </div>\n    </label>\n  )\n}\n\ninterface IconSliderProps {\n  value: number\n  onChange: (value: number) => void\n  icon: string\n}\n\n// https://dev.to/graftini/how-to-change-numeric-input-by-dragging-in-react-315\nfunction IconSlider({ value, onChange, icon }: IconSliderProps) {\n  const [initialX, setInitialX] = useState<number | null>(null)\n  const [snapshot, setSnapshot] = useState(value)\n\n  useEffect(() => {\n    function onUpdate(event: MouseEvent) {\n      if (initialX !== null) {\n        onChange(snapshot + event.clientX - initialX)\n      }\n    }\n\n    function onEnd() {\n      setInitialX(null)\n    }\n\n    document.addEventListener('mousemove', onUpdate)\n    document.addEventListener('mouseup', onEnd)\n\n    return () => {\n      document.removeEventListener('mousemove', onUpdate)\n      document.removeEventListener('mouseup', onEnd)\n    }\n  }, [initialX, onChange, snapshot])\n\n  return (\n    <div\n      className=\"flex justify-center items-center cursor-ew-resize select-none px-2\"\n      draggable={false}\n      onMouseDown={(e) => {\n        setInitialX(e.clientX)\n        setSnapshot(value)\n      }}\n    >\n      <img\n        src={icon}\n        alt=\"\"\n        draggable={false}\n        className=\"fill-neutral-300 w-4 h-4\"\n      />\n    </div>\n  )\n}\n\ninterface PreserveSmoothingToggleProps {\n  value: boolean\n  onChange: (value: boolean) => void\n}\n\nfunction PreserveSmoothingToggle({\n  value,\n  onChange,\n}: PreserveSmoothingToggleProps) {\n  return (\n    <label class=\"flex items-center gap-2\">\n      <div class=\"relative\">\n        <input\n          type=\"checkbox\"\n          class=\"sr-only peer\"\n          checked={value}\n          onChange={(e) => onChange(e.currentTarget.checked)}\n        />\n        <div class=\"w-11 h-6 peer-focus-visible:outline-none peer-focus-visible:ring-2 peer-focus-visible:ring-blue-800 rounded-full bg-neutral-700 peer-checked:bg-blue-600\" />\n        <div class=\"absolute top-[2px] left-[2px] bg-white border rounded-full h-5 w-5 transition-all peer-checked:translate-x-full border-neutral-600\" />\n      </div>\n      <span class=\"text-neutral-400 text-xs uppercase font-bold tracking-widest\">\n        Preserve Smoothing\n      </span>\n    </label>\n  )\n}\n\ninterface CornerSmoothingSliderProps {\n  value: number\n  onChange: (value: number) => void\n}\n\nfunction CornerSmoothingSlider({\n  value,\n  onChange,\n}: CornerSmoothingSliderProps) {\n  return (\n    <div>\n      <div className=\"flex justify-between items-baseline\">\n        <label\n          className=\"flex-1 text-neutral-400 text-xs uppercase font-bold tracking-widest\"\n          htmlFor=\"corner-smoothing-slider\"\n        >\n          Corner Smoothing\n        </label>\n        <div className=\"text-white\">{value}%</div>\n      </div>\n      <input\n        id=\"corner-smoothing-slider\"\n        type=\"range\"\n        min=\"0\"\n        max=\"100\"\n        value={value}\n        onInput={(e) => onChange(Number(e.currentTarget.value))}\n        className=\"w-full rounded-full appearance-none focus:outline-none focus:ring-blue-600 focus-visible:ring-2 bg-neutral-700 accent-white\"\n      />\n    </div>\n  )\n}\n"
  },
  {
    "path": "site/src/app.css",
    "content": "#app {\n  display: flex;\n  flex: 1;\n}\n"
  },
  {
    "path": "site/src/index.css",
    "content": "@tailwind base;\n@tailwind components;\n@tailwind utilities;\n\n:root {\n  font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;\n  line-height: 1.5;\n  font-weight: 400;\n\n  text-rendering: optimizeLegibility;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\nbody {\n  display: flex;\n  min-height: 100vh;\n}\n"
  },
  {
    "path": "site/src/main.tsx",
    "content": "import { render } from 'preact'\nimport { App } from './App'\nimport './index.css'\n\nrender(<App />, document.getElementById('app')!)\n"
  },
  {
    "path": "site/src/vite-env.d.ts",
    "content": "/// <reference types=\"vite/client\" />\n"
  },
  {
    "path": "site/tailwind.config.js",
    "content": "/** @type {import('tailwindcss').Config} */\nexport default {\n  mode: 'jit',\n  content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],\n  theme: {\n    extend: {},\n  },\n  plugins: [],\n}\n"
  },
  {
    "path": "site/tsconfig.app.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2020\",\n    \"useDefineForClassFields\": true,\n    \"module\": \"ESNext\",\n    \"lib\": [\"ES2020\", \"DOM\", \"DOM.Iterable\"],\n    \"skipLibCheck\": true,\n    \"paths\": {\n      \"react\": [\"./node_modules/preact/compat/\"],\n      \"react-dom\": [\"./node_modules/preact/compat/\"]\n    },\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n    \"jsx\": \"react-jsx\",\n    \"jsxImportSource\": \"preact\",\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"src\"]\n}\n"
  },
  {
    "path": "site/tsconfig.json",
    "content": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.app.json\" },\n    { \"path\": \"./tsconfig.node.json\" }\n  ]\n}\n"
  },
  {
    "path": "site/tsconfig.node.json",
    "content": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"lib\": [\"ES2023\"],\n    \"module\": \"ESNext\",\n    \"skipLibCheck\": true,\n\n    /* Bundler mode */\n    \"moduleResolution\": \"bundler\",\n    \"allowImportingTsExtensions\": true,\n    \"isolatedModules\": true,\n    \"moduleDetection\": \"force\",\n    \"noEmit\": true,\n\n    /* Linting */\n    \"strict\": true,\n    \"noUnusedLocals\": true,\n    \"noUnusedParameters\": true,\n    \"noFallthroughCasesInSwitch\": true\n  },\n  \"include\": [\"vite.config.ts\"]\n}\n"
  },
  {
    "path": "site/vite.config.ts",
    "content": "import { defineConfig } from 'vite'\nimport preact from '@preact/preset-vite'\n\n// https://vitejs.dev/config/\nexport default defineConfig({\n  plugins: [preact()],\n})\n"
  },
  {
    "path": "src/distribute.ts",
    "content": "interface RoundedRectangle {\n  topLeftCornerRadius: number\n  topRightCornerRadius: number\n  bottomRightCornerRadius: number\n  bottomLeftCornerRadius: number\n  width: number\n  height: number\n}\n\ninterface NormalizedCorner {\n  radius: number\n  roundingAndSmoothingBudget: number\n}\n\ninterface NormalizedCorners {\n  topLeft: NormalizedCorner\n  topRight: NormalizedCorner\n  bottomLeft: NormalizedCorner\n  bottomRight: NormalizedCorner\n}\n\ntype Corner = keyof NormalizedCorners\n\ntype Side = 'top' | 'left' | 'right' | 'bottom'\n\ninterface Adjacent {\n  side: Side\n  corner: Corner\n}\n\nexport function distributeAndNormalize({\n  topLeftCornerRadius,\n  topRightCornerRadius,\n  bottomRightCornerRadius,\n  bottomLeftCornerRadius,\n  width,\n  height,\n}: RoundedRectangle): NormalizedCorners {\n  const roundingAndSmoothingBudgetMap: Record<Corner, number> = {\n    topLeft: -1,\n    topRight: -1,\n    bottomLeft: -1,\n    bottomRight: -1,\n  }\n\n  const cornerRadiusMap: Record<Corner, number> = {\n    topLeft: topLeftCornerRadius,\n    topRight: topRightCornerRadius,\n    bottomLeft: bottomLeftCornerRadius,\n    bottomRight: bottomRightCornerRadius,\n  }\n\n  Object.entries(cornerRadiusMap)\n    // Let the bigger corners choose first\n    .sort(([, radius1], [, radius2]) => {\n      return radius2 - radius1\n    })\n    .forEach(([cornerName, radius]) => {\n      const corner = cornerName as Corner\n      const adjacents = adjacentsByCorner[corner]\n\n      // Look at the 2 adjacent sides, figure out how much space we can have on both sides,\n      // then take the smaller one\n      const budget = Math.min(\n        ...adjacents.map((adjacent) => {\n          const adjacentCornerRadius = cornerRadiusMap[adjacent.corner]\n          if (radius === 0 && adjacentCornerRadius === 0) {\n            return 0\n          }\n\n          const adjacentCornerBudget =\n            roundingAndSmoothingBudgetMap[adjacent.corner]\n\n          const sideLength =\n            adjacent.side === 'top' || adjacent.side === 'bottom'\n              ? width\n              : height\n\n          // If the adjacent corner's already been given the rounding and smoothing budget,\n          // we'll just take the rest\n          if (adjacentCornerBudget >= 0) {\n            return sideLength - roundingAndSmoothingBudgetMap[adjacent.corner]\n          } else {\n            return (radius / (radius + adjacentCornerRadius)) * sideLength\n          }\n        })\n      )\n\n      roundingAndSmoothingBudgetMap[corner] = budget\n      cornerRadiusMap[corner] = Math.min(radius, budget)\n    })\n\n  return {\n    topLeft: {\n      radius: cornerRadiusMap.topLeft,\n      roundingAndSmoothingBudget: roundingAndSmoothingBudgetMap.topLeft,\n    },\n    topRight: {\n      radius: cornerRadiusMap.topRight,\n      roundingAndSmoothingBudget: roundingAndSmoothingBudgetMap.topRight,\n    },\n    bottomLeft: {\n      radius: cornerRadiusMap.bottomLeft,\n      roundingAndSmoothingBudget: roundingAndSmoothingBudgetMap.bottomLeft,\n    },\n    bottomRight: {\n      radius: cornerRadiusMap.bottomRight,\n      roundingAndSmoothingBudget: roundingAndSmoothingBudgetMap.bottomRight,\n    },\n  }\n}\n\nconst adjacentsByCorner: Record<Corner, Array<Adjacent>> = {\n  topLeft: [\n    {\n      corner: 'topRight',\n      side: 'top',\n    },\n    {\n      corner: 'bottomLeft',\n      side: 'left',\n    },\n  ],\n  topRight: [\n    {\n      corner: 'topLeft',\n      side: 'top',\n    },\n    {\n      corner: 'bottomRight',\n      side: 'right',\n    },\n  ],\n  bottomLeft: [\n    {\n      corner: 'bottomRight',\n      side: 'bottom',\n    },\n    {\n      corner: 'topLeft',\n      side: 'left',\n    },\n  ],\n  bottomRight: [\n    {\n      corner: 'bottomLeft',\n      side: 'bottom',\n    },\n    {\n      corner: 'topRight',\n      side: 'right',\n    },\n  ],\n}\n"
  },
  {
    "path": "src/draw.ts",
    "content": "interface CornerPathParams {\n  a: number\n  b: number\n  c: number\n  d: number\n  p: number\n  cornerRadius: number\n  arcSectionLength: number\n}\n\ninterface CornerParams {\n  cornerRadius: number\n  cornerSmoothing: number\n  preserveSmoothing: boolean\n  roundingAndSmoothingBudget: number\n}\n\n// The article from figma's blog\n// https://www.figma.com/blog/desperately-seeking-squircles/\n//\n// The original code by MartinRGB\n// https://github.com/MartinRGB/Figma_Squircles_Approximation/blob/bf29714aab58c54329f3ca130ffa16d39a2ff08c/js/rounded-corners.js#L64\nexport function getPathParamsForCorner({\n  cornerRadius,\n  cornerSmoothing,\n  preserveSmoothing,\n  roundingAndSmoothingBudget,\n}: CornerParams): CornerPathParams {\n  // From figure 12.2 in the article\n  // p = (1 + cornerSmoothing) * q\n  // in this case q = R because theta = 90deg\n  let p = (1 + cornerSmoothing) * cornerRadius\n\n  // When there's not enough space left (p > roundingAndSmoothingBudget), there are 2 options:\n  //\n  // 1. What figma's currently doing: limit the smoothing value to make sure p <= roundingAndSmoothingBudget\n  // But what this means is that at some point when cornerRadius is large enough,\n  // increasing the smoothing value wouldn't do anything\n  //\n  // 2. Keep the original smoothing value and use it to calculate the bezier curve normally,\n  // then adjust the control points to achieve similar curvature profile\n  //\n  // preserveSmoothing is a new option I added\n  //\n  // If preserveSmoothing is on then we'll just keep using the original smoothing value\n  // and adjust the bezier curve later\n  if (!preserveSmoothing) {\n    const maxCornerSmoothing = roundingAndSmoothingBudget / cornerRadius - 1\n    cornerSmoothing = Math.min(cornerSmoothing, maxCornerSmoothing)\n    p = Math.min(p, roundingAndSmoothingBudget)\n  }\n\n  // In a normal rounded rectangle (cornerSmoothing = 0), this is 90\n  // The larger the smoothing, the smaller the arc\n  const arcMeasure = 90 * (1 - cornerSmoothing)\n  const arcSectionLength =\n    Math.sin(toRadians(arcMeasure / 2)) * cornerRadius * Math.sqrt(2)\n\n  // In the article this is the distance between 2 control points: P3 and P4\n  const angleAlpha = (90 - arcMeasure) / 2\n  const p3ToP4Distance = cornerRadius * Math.tan(toRadians(angleAlpha / 2))\n\n  // a, b, c and d are from figure 11.1 in the article\n  const angleBeta = 45 * cornerSmoothing\n  const c = p3ToP4Distance * Math.cos(toRadians(angleBeta))\n  const d = c * Math.tan(toRadians(angleBeta))\n\n  let b = (p - arcSectionLength - c - d) / 3\n  let a = 2 * b\n\n  // Adjust the P1 and P2 control points if there's not enough space left\n  if (preserveSmoothing && p > roundingAndSmoothingBudget) {\n    const p1ToP3MaxDistance =\n      roundingAndSmoothingBudget - d - arcSectionLength - c\n\n    // Try to maintain some distance between P1 and P2 so the curve wouldn't look weird\n    const minA = p1ToP3MaxDistance / 6\n    const maxB = p1ToP3MaxDistance - minA\n\n    b = Math.min(b, maxB)\n    a = p1ToP3MaxDistance - b\n    p = Math.min(p, roundingAndSmoothingBudget)\n  }\n\n  return {\n    a,\n    b,\n    c,\n    d,\n    p,\n    arcSectionLength,\n    cornerRadius,\n  }\n}\n\ninterface SVGPathInput {\n  width: number\n  height: number\n  topRightPathParams: CornerPathParams\n  bottomRightPathParams: CornerPathParams\n  bottomLeftPathParams: CornerPathParams\n  topLeftPathParams: CornerPathParams\n}\n\nexport function getSVGPathFromPathParams({\n  width,\n  height,\n  topLeftPathParams,\n  topRightPathParams,\n  bottomLeftPathParams,\n  bottomRightPathParams,\n}: SVGPathInput) {\n  return `\n    M ${width - topRightPathParams.p} 0\n    ${drawTopRightPath(topRightPathParams)}\n    L ${width} ${height - bottomRightPathParams.p}\n    ${drawBottomRightPath(bottomRightPathParams)}\n    L ${bottomLeftPathParams.p} ${height}\n    ${drawBottomLeftPath(bottomLeftPathParams)}\n    L 0 ${topLeftPathParams.p}\n    ${drawTopLeftPath(topLeftPathParams)}\n    Z\n  `\n    .replace(/[\\t\\s\\n]+/g, ' ')\n    .trim()\n}\n\nfunction drawTopRightPath({\n  cornerRadius,\n  a,\n  b,\n  c,\n  d,\n  p,\n  arcSectionLength,\n}: CornerPathParams) {\n  if (cornerRadius) {\n    return rounded`\n    c ${a} 0 ${a + b} 0 ${a + b + c} ${d}\n    a ${cornerRadius} ${cornerRadius} 0 0 1 ${arcSectionLength} ${arcSectionLength}\n    c ${d} ${c}\n        ${d} ${b + c}\n        ${d} ${a + b + c}`\n  } else {\n    return rounded`l ${p} 0`\n  }\n}\n\nfunction drawBottomRightPath({\n  cornerRadius,\n  a,\n  b,\n  c,\n  d,\n  p,\n  arcSectionLength,\n}: CornerPathParams) {\n  if (cornerRadius) {\n    return rounded`\n    c 0 ${a}\n      0 ${a + b}\n      ${-d} ${a + b + c}\n    a ${cornerRadius} ${cornerRadius} 0 0 1 -${arcSectionLength} ${arcSectionLength}\n    c ${-c} ${d}\n      ${-(b + c)} ${d}\n      ${-(a + b + c)} ${d}`\n  } else {\n    return rounded`l 0 ${p}`\n  }\n}\n\nfunction drawBottomLeftPath({\n  cornerRadius,\n  a,\n  b,\n  c,\n  d,\n  p,\n  arcSectionLength,\n}: CornerPathParams) {\n  if (cornerRadius) {\n    return rounded`\n    c ${-a} 0\n      ${-(a + b)} 0\n      ${-(a + b + c)} ${-d}\n    a ${cornerRadius} ${cornerRadius} 0 0 1 -${arcSectionLength} -${arcSectionLength}\n    c ${-d} ${-c}\n      ${-d} ${-(b + c)}\n      ${-d} ${-(a + b + c)}`\n  } else {\n    return rounded`l ${-p} 0`\n  }\n}\n\nfunction drawTopLeftPath({\n  cornerRadius,\n  a,\n  b,\n  c,\n  d,\n  p,\n  arcSectionLength,\n}: CornerPathParams) {\n  if (cornerRadius) {\n    return rounded`\n    c 0 ${-a}\n      0 ${-(a + b)}\n      ${d} ${-(a + b + c)}\n    a ${cornerRadius} ${cornerRadius} 0 0 1 ${arcSectionLength} -${arcSectionLength}\n    c ${c} ${-d}\n      ${b + c} ${-d}\n      ${a + b + c} ${-d}`\n  } else {\n    return rounded`l 0 ${-p}`\n  }\n}\n\nfunction toRadians(degrees: number) {\n  return (degrees * Math.PI) / 180\n}\n\nfunction rounded(strings: TemplateStringsArray, ...values: number[]): string {\n  return strings.reduce((acc, str, i) => {\n    const value = values[i]\n\n    if (typeof value === 'number') {\n      return acc + str + value.toFixed(4)\n    } else {\n      return acc + str + (value ?? '')\n    }\n  }, '')\n}\n"
  },
  {
    "path": "src/index.ts",
    "content": "import { distributeAndNormalize } from './distribute'\nimport { getPathParamsForCorner, getSVGPathFromPathParams } from './draw'\n\nexport interface FigmaSquircleParams {\n  cornerRadius?: number\n  topLeftCornerRadius?: number\n  topRightCornerRadius?: number\n  bottomRightCornerRadius?: number\n  bottomLeftCornerRadius?: number\n  cornerSmoothing: number\n  width: number\n  height: number\n  preserveSmoothing?: boolean\n}\n\nexport function getSvgPath({\n  cornerRadius = 0,\n  topLeftCornerRadius,\n  topRightCornerRadius,\n  bottomRightCornerRadius,\n  bottomLeftCornerRadius,\n  cornerSmoothing,\n  width,\n  height,\n  preserveSmoothing = false,\n}: FigmaSquircleParams) {\n  topLeftCornerRadius = topLeftCornerRadius ?? cornerRadius\n  topRightCornerRadius = topRightCornerRadius ?? cornerRadius\n  bottomLeftCornerRadius = bottomLeftCornerRadius ?? cornerRadius\n  bottomRightCornerRadius = bottomRightCornerRadius ?? cornerRadius\n\n  if (\n    topLeftCornerRadius === topRightCornerRadius &&\n    topRightCornerRadius === bottomRightCornerRadius &&\n    bottomRightCornerRadius === bottomLeftCornerRadius &&\n    bottomLeftCornerRadius === topLeftCornerRadius\n  ) {\n    const roundingAndSmoothingBudget = Math.min(width, height) / 2\n    const cornerRadius = Math.min(\n      topLeftCornerRadius,\n      roundingAndSmoothingBudget\n    )\n\n    const pathParams = getPathParamsForCorner({\n      cornerRadius,\n      cornerSmoothing,\n      preserveSmoothing,\n      roundingAndSmoothingBudget,\n    })\n\n    return getSVGPathFromPathParams({\n      width,\n      height,\n      topLeftPathParams: pathParams,\n      topRightPathParams: pathParams,\n      bottomLeftPathParams: pathParams,\n      bottomRightPathParams: pathParams,\n    })\n  }\n\n  const { topLeft, topRight, bottomLeft, bottomRight } = distributeAndNormalize(\n    {\n      topLeftCornerRadius,\n      topRightCornerRadius,\n      bottomRightCornerRadius,\n      bottomLeftCornerRadius,\n      width,\n      height,\n    }\n  )\n\n  return getSVGPathFromPathParams({\n    width,\n    height,\n    topLeftPathParams: getPathParamsForCorner({\n      cornerSmoothing,\n      preserveSmoothing,\n      cornerRadius: topLeft.radius,\n      roundingAndSmoothingBudget: topLeft.roundingAndSmoothingBudget,\n    }),\n    topRightPathParams: getPathParamsForCorner({\n      cornerSmoothing,\n      preserveSmoothing,\n      cornerRadius: topRight.radius,\n      roundingAndSmoothingBudget: topRight.roundingAndSmoothingBudget,\n    }),\n    bottomRightPathParams: getPathParamsForCorner({\n      cornerSmoothing,\n      preserveSmoothing,\n      cornerRadius: bottomRight.radius,\n      roundingAndSmoothingBudget: bottomRight.roundingAndSmoothingBudget,\n    }),\n    bottomLeftPathParams: getPathParamsForCorner({\n      cornerSmoothing,\n      preserveSmoothing,\n      cornerRadius: bottomLeft.radius,\n      roundingAndSmoothingBudget: bottomLeft.roundingAndSmoothingBudget,\n    }),\n  })\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": { \n    \"target\": \"es2022\",\n    \"module\": \"esnext\",\n    \"esModuleInterop\": true,\n    \"declaration\": true,\n    \"strict\": true,\n    \"skipLibCheck\": true,\n    \"isolatedModules\": true,\n    \"rootDir\": \"src\",\n    \"outDir\": \"dist\"\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  }
]