[
  {
    "path": ".eslintrc",
    "content": "{\n  \"root\": true,\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"@typescript-eslint\"],\n  \"extends\": [\n    \"eslint:recommended\",\n    \"plugin:@typescript-eslint/eslint-recommended\",\n    \"plugin:@typescript-eslint/recommended\"\n  ]\n}\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Build and Deploy\non: [push]\njobs:\n  build-and-deploy:\n    concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout 🛎️\n        uses: actions/checkout@v3\n\n      - name: Install and Build 🔧 # This example project is built using npm and outputs the result to the 'build' folder. Replace with the commands required to build your project, or remove this step entirely if your site is pre-built.\n        run: |\n          npm ci\n          npm run build\n      \n      - name: Test 🔍 \n        run: |\n          npm ci\n          npm run test\n\n      - name: Deploy 🚀\n        uses: JamesIves/github-pages-deploy-action@v4.3.3\n        with:\n          branch: gh-pages # The branch the action should deploy to.\n          folder: dist # The folder the action should deploy.\n"
  },
  {
    "path": ".gitignore",
    "content": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs.org/api/report.html)\nreport.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json\n\n# Runtime data\npids\n*.pid\n*.seed\n*.pid.lock\n\n# Directory for instrumented libs generated by jscoverage/JSCover\nlib-cov\n\n# Coverage directory used by tools like istanbul\ncoverage\n*.lcov\n\n# nyc test coverage\n.nyc_output\n\n# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)\n.grunt\n\n# Bower dependency directory (https://bower.io/)\nbower_components\n\n# node-waf configuration\n.lock-wscript\n\n# Compiled binary addons (https://nodejs.org/api/addons.html)\nbuild/Release\n\n# Dependency directories\nnode_modules/\njspm_packages/\n\n# TypeScript v1 declaration files\ntypings/\n\n# TypeScript cache\n*.tsbuildinfo\n\n# Optional npm cache directory\n.npm\n\n# Optional eslint cache\n.eslintcache\n\n# Microbundle cache\n.rpt2_cache/\n.rts2_cache_cjs/\n.rts2_cache_es/\n.rts2_cache_umd/\n\n# Optional REPL history\n.node_repl_history\n\n# Output of 'npm pack'\n*.tgz\n\n# Yarn Integrity file\n.yarn-integrity\n\n# dotenv environment variables file\n.env\n.env.test\n\n# parcel-bundler cache (https://parceljs.org/)\n.cache\n\n# Next.js build output\n.next\n\n# Nuxt.js build / generate output\n.nuxt\n\n# Gatsby files\n.cache/\n# Comment in the public line in if your project uses Gatsby and *not* Next.js\n# https://nextjs.org/blog/next-9-1#public-directory-support\n# public\n\n# vuepress build output\n.vuepress/dist\n\n# Serverless directories\n.serverless/\n\n# FuseBox cache\n.fusebox/\n\n# DynamoDB Local files\n.dynamodb/\n\n# TernJS port file\n.tern-port\n"
  },
  {
    "path": ".prettierrc.json",
    "content": "{}\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2021 David Aerne\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.\n"
  },
  {
    "path": "README.md",
    "content": "# RampenSau 🎢🐷🎨\n\n**RampenSau** is a color palette generation library that utilizes **hue cycling** and\n**easing functions** to generate color ramps. It can generate a sequence of hues, or use a list of hues to generate a color ramp.\n\nPerfect for generating color palettes for data visualizations, visual design, generative art, or just for fun.\n\n![generated RampenSau color palettes Animation](./rampensau.gif)\n\n## Demos\n\n- [Official Docs & Demo](https://meodai.github.io/rampensau/)\n  Interactive Documentation with example function calls\n- [1000 Generative Samples](https://codepen.io/meodai/pen/ExQWwar?editors=0010)\n  Generating a 1000 palettes using similar settings in with lch\n- [p5.js Example](https://editor.p5js.org/meodai/sketches/dzEX_4wTN)\n  3d example using p5.js's `color()` function\n- [Syntax Highlighting](https://meodai.github.io/rampensau/highlighter.html)\n  Generative syntax highlighting themes using RampenSau\n- [Mini HDR Posters](https://codepen.io/meodai/pen/zYeXEyw)\n  Generative posters using lCH (p3+ gamut)\n- [Color Ratios](https://codepen.io/meodai/full/vYbwbym)\n  Generative rectangles\n- [p5.js Stamps](https://openprocessing.org/sketch/2628160)\n  Fork of a sketch originally made by [Okazz](https://openprocessing.org/user/128718/?view=sketches).\n- [Farbvelo Color Generator](https://farbvelo.elastiq.ch/).\n  Project this code is based on\n\n## Installation\n\n**Rampensau** is bundled as both UMD and ES on npm. Install it using your package manager of choice:\n\n```bash\nnpm install rampensau\n```\n\nYou can then import RampenSau into your project:\n\n```js\n// ES style: import individual methods\nimport { generateColorRamp } from \"rampensau\";\n\n// Depending on your setup, you might need to import the MJS version directly\nimport { generateColorRamp } from \"rampensau/dist/index.mjs\";\n\n// CJS style\nlet generateColorRamp = require(\"rampensau\");\n```\n\nOr include it directly in your HTML:\n\n```html\n<script src=\"https://cdn.jsdelivr.net/npm/rampensau/dist/index.js\"></script>\n<!-- or -->\n<script type=\"module\">\n  import { generateColorRamp } from \"https://esm.sh/rampensau/\";\n</script>\n```\n\n## Basic Usage\n\n```js\nimport { generateColorRamp } from 'rampensau';\n\nconst hslColorValues = generateColorRamp({\n  // hue generation options\n  total: 9,                           // number of colors in the ramp\n  hStart: Math.random() * 360,         // hue at the start of the ramp\n  hCycles: 1,                           // number of full hue cycles \n                                       // (.5 = 180°, 1 = 360°, 2 = 720°, etc.)\n  hStartCenter: 0.5,                    // where in the ramp the hue should be centered\n  hEasing: (x, fr) => x,                // hue easing function x is a value between 0 and 1 \n                                       // fr is the size of each fraction of the ramp: (1 / total)\n\n  // if you want to use a specific list of hues, you can pass an array of hues to the hueList option\n  // all other hue options will be ignored\n\n  // hueList: [...],                      // list of hues to use\n\n  // saturation\n  sRange: [0.4, 0.35],                 // saturation range\n  sEasing: (x, fr) => Math.pow(x, 2),   // saturation easing function\n\n  // lightness\n  lRange: [Math.random() * 0.1, 0.9],  // lightness range\n  lEasing: (x, fr) => Math.pow(x, 1.5), // lightness easing function\n\n  transformFn: (color, i) => color; // function to adjust/convert the color after generation\n}); // => [[0…360,0…1,0…1], …]\n```\n\n### generateColorRamp(Options{})\n\n**generateColorRamp** is the core function of **RampenSau**. It returns an array of colors in **HSL** format (`[0…360, 0…1, 0…1]`). To get a better understanding of the options, it might be helpful to familiarize yourself with the [HSL color model](https://en.wikipedia.org/wiki/HSL_and_HSV) or to play with the interactive [Demo / Documentation](https://meodai.github.io/rampensau/).\n\nThe function returns an array of colors, each represented as an array of three values: `[Hue, Saturation, Lightness]`. Hue is in degrees `(0-360)`, while Saturation and Lightness are normalized values between `0 and 1`. We use the term \"HXX\" loosely because while the structure is similar to HSL, these base values can be mapped to various polar color models like HSL, HSV, LCH, or OKLCH.\n\n**Important**: When converting to a specific model, map these values appropriately. For HSL, Saturation and Lightness map directly (usually scaled to percentages). For models like LCH or OKLCH, the library's Saturation value needs to be mapped and potentially scaled to represent Chroma, and Lightness maps to the corresponding Lightness component. The examples using CSS (`hsl()`, `oklch()`) and the Culori library demonstrate this mapping and scaling. The provided colorToCSS helper function handles this conversion automatically for common CSS formats.\n\n#### Options\n\nEvery single option has a default value, so you can just call the function without any arguments.\nIt will generate a color ramp with 9 colors, starting at a random hue, with a single hue cycle.\n\nWhile the function always generates some sort of color ramp, there are two main ways to generate hues independently of saturation and lightness: **Let the function generate a sequence of hues**, or **pass a list of hues** to use.\n\n##### Hue sequence generation\n\nIf you want to generate a sequence of hues, you can use the following options:\n\n- `total` int 3…∞           → Amount of colors the function will generate.\n- `hStart` float 0…360      → Starting point of the hue ramp. 0 Red, 180 Teal etc..\n- `hStartCenter`: float 0…1 → Center the hue in the color ramp. 0 = start, 0.5 = middle, 1 = end.\n- `hCycles` float -∞…0…+∞   → Number of hue cycles. (.5 = 180°, 1 = 360°, 2 = 720°, etc.)\n- `hEasing` function(x)     → Hue easing function\n\nThe `hStart` sets the starting point of the hue ramp. The `hStartCenter` sets where in the ramp the hue should be centered. If your ramp starts with a high or low lightness, you might want to center the hue in the middle of the ramp. That is why the default value for `hStartCenter` is `0.5`. (In the center of a given ramp).\n\nThe `hStartCenter` option tells the function where the start hue should be in your ramp. A value of `0` will generate a ramp that starts with the hue at the beginning of the ramp. A value of `0.5` will generate a ramp that starts with the hue in the middle of the ramp. A value of `1` will generate a ramp that starts with the hue at the end of the ramp.\n\nThe `hCycles` option sets the number of hue cycles. A value of `1` will generate a ramp with a single hue cycle. Meaning they will go around the color wheel once. A value of `0.5` will generate a ramp with 180° hue cycle (starting from hStart to its complementary hue). A value of `2` will rotate around the color wheel twice. A value of `-1` will generate a ramp with a reversed hue cycle. A value of `-0.5` will generate a ramp with a reversed 180° hue cycle. A value of `-2` will generate a ramp with a reversed 720° hue cycle.\n\n**Note:** The further away `hCycles` is from `0`, the more hue variation you will get in the ramp. \n\n##### Hue List\n\nIf you want to use a specific list of hues, you can pass an array of hues to the `hueList` option. All other hue options will be ignored. For example, if you want to generate a ramp with 3 colors, but you want to use random unique hues, you can do this:\n\n- `hueList` array [0…360]   → List of hues to use. All other hue options will be ignored.\n\n**Example:**\n\n```js\nimport {\n  generateColorRamp,\n  colorUtils,\n} from \"rampensau\";\n\nconst { uniqueRandomHues } = colorUtils;\n\n\ngenerateColorRamp({\n  hueList: uniqueRandomHues({\n    startHue: Math.random() * 360, \n    total: 5, \n    minHueDiffAngle: 90,\n  })\n})\n```\n\nThe `uniqueRandomHues` function will generate a list of unique hues with a minimum distance of 90° between each hue. This list is then passed to the `hueList` option of `generateColorRamp`. `uniqueRandomHues` is also exported by RampenSau, so you can use it directly.\n\n##### Saturation & Lightness\n\n- `sRange` array [0…1,0…1]  → Saturation Range\n- `lRange` array [0…1,0…1]  → Lightness Range\n\n##### Easing Functions\n\nEach of the color dimensions can be eased using a custom function.\nThe function takes an input value `x` and returns a value between 0 and 1:\n\n- `hEasing` function(x)     → Hue easing function\n- `sEasing` function(x)     → Saturation easing function\n- `lEasing` function(x)     → Lightness easing function\n\n##### Transform Function\n\n- `transformFn` function(color, i) → Function to adjust/transform or convert the color after generation. The function takes the generated color and its index as arguments. You can use this function to apply any adjustments you want to the generated colors.\n\n**Example:**\n\n```js\nconst hslColorValues = generateColorRamp({\n  transformFn: ([h, s, l], i) => {\n    // Adjust the color to be more saturated\n    return [h, s, .2 + l * .8];\n  }\n});\n```\n\nIt could also be used to get a CSS String instead of the array. Just use the `colorToCSS` function from the color utility functions:\n\n```js\nconst hslColorValues = generateColorRamp({\n  transformFn: ([h, s, l]) => colorToCSS(color, 'oklch')\n});\n```\n\n**TypeScript Node** `transformFn` is typed as `(color: number[], i: number) => number[] | string`. If you need to return anything else, you can use a type assertion to cast the return value to whatever you need.\n\n### generateColorRampWithCurve(Options{})\n\n**generateColorRampWithCurve** is a convenience function that uses pre-defined curve methods for easing functions. It accepts all the same options as `generateColorRamp` plus two additional options:\n\n- `curveMethod` string      → The curve method to use for easing. One of `'lamé'`, `'sine'`, `'power'`, or `'linear'`.\n- `curveAccent` float 0…5   → The accent of the curve, affecting how pronounced the curve's effect is.\n\n**Note:** It is recommended to use `HSV` as the color space for the `curveMethod` option. It produces nicer looking ramps and is easier to work with, because the lightness and saturation are both 100% at the upper right corner of the the `HSV` slice.\n\n**Example:**\n\n```js\nimport { generateColorRampWithCurve } from 'rampensau';\n\nconst hslColorValues = generateColorRampWithCurve({\n  total: 9,\n  hStart: 180,\n  curveMethod: 'lamé',\n  curveAccent: 0.5,\n  sRange: [0.4, 0.8],\n  lRange: [0.2, 0.8],\n}); // => [[0…360,0…1,0…1], …]\n```\n\n## Hue Generation Functions\n\n### uniqueRandomHues(Options{})\n\nFunction returns an array of unique random hues. Mostly useful for generating a list of hues to use with `hueList`. Alternatively you can use `(x) => Math.random()` as the `hEasing` function in `generateColorRamp` but this will not guarantee unique hues.\n\n- `startHue` float 0…360        → Starting point of the hue ramp. 0 Red, 180 Teal etc..\n- `total` int 3…∞               → Amount of base colors.\n- `minHueDiffAngle` float 0…360 → Minimum angle between hues.\n- `rndFn` function()            → Random function. Defaults to `Math.random`.\n\n### colorHarmonies.colorHarmony(Options{})\n\nFunction returns an array of hues based on color harmony theory.\n\nAvailable harmonies:\n- `complementary` - Base hue and its complement (180° opposite)\n- `splitComplementary` - Base hue and two hues on either side of its complement\n- `triadic` - Three hues evenly spaced around the color wheel\n- `tetradic` - Four hues evenly spaced around the color wheel\n- `monochromatic` - Just the base hue\n- `doubleComplementary` - Two complementary pairs\n- `compound` - A mix of complementary and analogous\n- `analogous` - A series of adjacent hues\n\n**Example:**\n\n```js\nimport {\n  generateColorRamp,\n  colorUtils,\n} from \"rampensau\";\n\nconst { colorHarmonies } = colorUtils;\n\ngenerateColorRamp({\n  hueList: colorHarmonies.splitComplementary(Math.random() * 360),\n  sRange: [0.4, 0.35],\n  lRange: [Math.random() * 0.1, 0.9],\n});\n```\n\n## Color Utility Functions\n\nTo keep the library small, RampenSau does not include any color conversion functions. \nHowever, it does provide a few utility functions to help you work with **RampenSau** or\nits generated colors.\n\n### colorToCSS(color, mode)\n\nIn order to use the colors generated by **RampenSau** in CSS or Canvas, you need to convert them to a CSS color format. This helper function does just that. It returns a CSS string from a color in the format generated from **generateColorRamp** (`[0…360,0…1,0…1]`).\n\n- `color` array [0…360,0…1,0…1] → Color in format generated from **generateColorRamp** (`[0…360,0…1,0…1]`).\n- `mode` string → Color mode to use. One of `hsl`, `hsv`, `lch` or `oklch`. Defaults to `oklch`. (Note that `hsl` is clamped to the sRGB gamut, while `lch` and `oklch` will make use of the full gamut supported by the target monitor / device.)\n\n**Example**:\n  \n```js\nimport {\n  colorUtils\n} from \"rampensau\";\n\nconst { colorToCSS } = colorUtils;\n\nconsole.log( \n  generateColorRamp().map(color => colorToCSS(color, 'oklch')) \n); // ['oklch(5.57% 40% 348.39)', 'oklch(14.59% 38.72% 314.74)', …]\n```\n\n### harveyHue(h)\n\nTransforms a hue to create a more evenly distributed spectrum without the over-abundance of green and ultramarine in the standard HSL/HSV color wheel. Originally written by [@harvey](https://twitter.com/harvey_rayner/status/1748159440010809665) and adapted for use in RampenSau.\n\n- `h` float 0…360 → Hue value to transform, normalized to 0-360 range.\n\n**Example**:\n\n```js\nimport { colorUtils } from \"rampensau\";\nconst { harveyHue } = colorUtils;\n\nconst transformedHue = harveyHue(0.5); // Returns a transformed hue value\n```\n\n## Using RampenSau with a color library\n\nIf you are already using a color library like [culori](https://culorijs.org/api/) you can use its\n`formatCSS` function instead. Just don't forget to scale the chroma value to the [adequate range](https://culorijs.org/color-spaces/).\n\n```js\nculori.formatCss({ mode: 'oklch', \n  l: color[2],\n  c: color[1] * 0.4,\n  h: color[0],\n})\n\nculori.formatCss({ mode: 'lch', \n  l: color[2] * 100,\n  c: color[1] * 150,\n  h: color[0],\n});\n```\n\n## Other Utility Functions\n\nRampenSau also provides several utility functions for working with arrays and curves:\n\n### Usage\n\n```js\nimport { utils } from 'rampensau';\nconst { shuffleArray, scaleSpreadArray, lerp, pointOnCurve, makeCurveEasings } = utils;\n```\n\n### shuffleArray(array, rndFn)\n\nReturns a new shuffled array based on the input array.\n\n- `array` array      → The array to shuffle\n- `rndFn` function() → Random function. Defaults to `Math.random`\n\n### scaleSpreadArray(valuesToFill, targetSize, padding, fillFunction)\n\nScales and spreads an array to the target size using interpolation.\nIn the context of RampenSau, this is used to create a smooth transition between colors.\nLet's say you have an array of 3 colors and you want to create a ramp of 10 colors.\nYou can use this function to fill the gaps between the colors and create a smooth transition.\n\n- `valuesToFill` array     → Initial array of values\n- `targetSize` int         → Desired size of the resulting array\n- `padding` float 0…1      → Optional padding value (defaults to 0)\n- `fillFunction` function  → Interpolation function (defaults to lerp)\n\n### lerp(amt, from, to)\n\nLinearly interpolates between two values. \nMainly used for the `fillFunction` in `scaleSpreadArray`.\n\n- `amt` float 0…1   → The interpolation amount\n- `from` number     → The starting value\n- `to` number       → The ending value\n\n### pointOnCurve(curveMethod, curveAccent)\n\nReturns a function that calculates the point on a curve at a given t value.\n\n- `curveMethod` string or function → The curve method to use\n- `curveAccent` float              → The accent of the curve\n\n### makeCurveEasings(curveMethod, curveAccent)\n\nGenerates saturation and lightness easing functions based on a curve method.\n\n- `curveMethod` string or function → The curve method to use\n- `curveAccent` float              → The accent of the curve\n\n## About the Name\n\nFor non-German speakers, \"Rampe\" in German means both a ramp (or gradient) and a theatrical stage. A \"RampenSau\" (literally \"stage-sow/pig\") is a German expression for someone who thrives in the spotlight - a natural performer. The name playfully combines this concept with the library's purpose of creating color ramps and gradients.\n\n## License\n\nRampensau is distributed under the [MIT License](./LICENSE).\nFeel free to use it in your projects, but please give credit where it's due. If you find this library useful, consider starring the repository on GitHub or sharing it with your friends and colleagues.\n"
  },
  {
    "path": "build.js",
    "content": "import { build } from \"esbuild\";\n\n// Bundled CJS\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  format: \"cjs\",\n  outfile: \"dist/index.cjs\",\n});\n\n// Bundled CJS, minified\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  minify: true,\n  format: \"cjs\",\n  outfile: \"dist/index.min.cjs\",\n});\n\n// Bundled ESM\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  format: \"esm\",\n  target: \"es2019\",\n  outfile: \"dist/index.mjs\",\n});\n\n// Bundled ESM, minified\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  minify: true,\n  format: \"esm\",\n  target: \"es2019\",\n  outfile: \"dist/index.min.mjs\",\n});\n\n// Bundled IIFE\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  format: \"iife\",\n  target: \"node14\",\n  globalName: \"rampensau\",\n  outfile: \"dist/index.js\",\n});\n\n// Bundled IIFE, minified\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  minify: true,\n  format: \"iife\",\n  target: \"es6\",\n  globalName: \"rampensau\",\n  outfile: \"dist/index.min.js\",\n});\n\n// Bundled UMD\n// Adapted from: https://github.com/umdjs/umd/blob/master/templates/returnExports.js\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle: true,\n  format: \"iife\",\n  target: \"es6\",\n  globalName: \"rampensau\",\n  banner: {\n    js: `(function(root, factory) {\n      if (typeof define === 'function' && define.amd) {\n      \tdefine([], factory);\n      } else if (typeof module === 'object' && module.exports) {\n      \tmodule.exports = factory();\n      } else {\n      \troot.rampensau = factory();\n      }\n    }\n    (typeof self !== 'undefined' ? self : this, function() {`,\n  },\n  footer: {\n    js: `return rampensau; }));`,\n  },\n  outfile: \"dist/index.umd.js\",\n});\n"
  },
  {
    "path": "dist/colorUtils.d.ts",
    "content": "export declare type Vector2 = [number, number];\nexport declare type Vector3 = [...Vector2, number];\n/**\n * Converts a color from HSL to HSV.\n * @param {Array} hsl - The HSL color values.\n * @returns {Array} - The HSV color values.\n */\nexport declare function normalizeHue(h: number): number;\n/**\n * Get a more evenly distributed spectrum without the over abundance of green and ultramarine\n * https://twitter.com/harvey_rayner/status/1748159440010809665\n * @param h - The hue value to be converted 0-360\n * @returns h\n */\nexport declare function harveyHue(h: number): number;\nexport declare type colorHarmony = \"complementary\" | \"splitComplementary\" | \"triadic\" | \"tetradic\" | \"pentadic\" | \"hexadic\" | \"monochromatic\" | \"doubleComplementary\" | \"compound\" | \"analogous\";\nexport declare type colorHarmonyFn = (h: number) => number[];\n/**\n * Generates a list of hues based on a color harmony.\n * @param {number} h - The base hue.\n * @param {colorHarmony} harmony - The color harmony.\n * @returns {Array<number>} - The list of hues.\n */\nexport declare const colorHarmonies: {\n    [key in colorHarmony]: colorHarmonyFn;\n};\nexport declare type uniqueRandomHuesArguments = {\n    startHue?: number;\n    total?: number;\n    minHueDiffAngle?: number;\n    rndFn?: () => number;\n};\n/**\n * Generates a list of unique hues.\n * @param {uniqueRandomHuesArguments} args - The arguments to generate the hues.\n * @returns {Array<number>} - The list of hues.\n */\nexport declare function uniqueRandomHues({ startHue, total, minHueDiffAngle, rndFn, }?: {\n    startHue?: number | undefined;\n    total?: number | undefined;\n    minHueDiffAngle?: number | undefined;\n    rndFn?: (() => number) | undefined;\n}): number[];\n/**\n * Converts a color from HSV to HSL.\n * @param {Array} hsv - The HSV color values.\n * @returns {Array} - The HSL color values.\n */\nexport declare const hsv2hsl: ([h, s, v]: [number, number, number]) => [number, number, number];\nexport declare type colorToCSSMode = \"oklch\" | \"lch\" | \"hsl\" | \"hsv\";\n/**\n * Converts color values to a CSS color function string.\n *\n * @param {Vector3} color - Array of three color values based on the color mode.\n * @param {colorToCSSMode} mode - The color mode to use (oklch, lch, hsl, or hsv).\n * @returns {string} - The CSS color function string in the appropriate format.\n */\nexport declare const colorToCSS: (color: [number, number, number], mode?: colorToCSSMode) => string;\n"
  },
  {
    "path": "dist/core.d.ts",
    "content": "import type { Vector2, Vector3 } from \"./colorUtils\";\nimport type { CurveMethod } from \"./utils\";\nexport declare type ModifiedEasingFn = (x: number, fr?: number) => number;\nexport declare type hueArguments = {\n    hStart?: number;\n    hStartCenter?: number;\n    hCycles?: number;\n    hEasing?: ModifiedEasingFn;\n};\nexport declare type presetHues = {\n    hueList: number[];\n};\nexport declare type saturationArguments = {\n    sRange?: Vector2;\n    sEasing?: ModifiedEasingFn;\n};\nexport declare type lightnessArguments = {\n    lRange?: Vector2;\n    lEasing?: ModifiedEasingFn;\n};\ndeclare type BaseGenerateColorRampArgument = {\n    total?: number;\n    transformFn?: (hsl: Vector3, i?: number) => Vector3 | string;\n} & hueArguments & saturationArguments & lightnessArguments;\nexport declare type GenerateColorRampArgument = BaseGenerateColorRampArgument & {\n    hueList?: never;\n};\nexport declare type GenerateColorRampArgumentFixedHues = BaseGenerateColorRampArgument & presetHues;\n/**\n * Generates a color ramp based on the HSL color space.\n * @param {GenerateColorRampArgument} args - The arguments to generate the ramp.\n * @returns {Array<number>} - The color ramp.\n */\nexport declare function generateColorRamp({ total, hStart, hStartCenter, hEasing, hCycles, sRange, sEasing, lRange, lEasing, transformFn, hueList, }?: GenerateColorRampArgument | GenerateColorRampArgumentFixedHues): Vector3[];\nexport declare const generateColorRampWithCurve: ({ total, hStart, hStartCenter, hCycles, sRange, lRange, hueList, curveMethod, curveAccent, transformFn, }?: (GenerateColorRampArgument | GenerateColorRampArgumentFixedHues) & {\n    curveMethod?: CurveMethod | undefined;\n    curveAccent?: number | undefined;\n}) => Vector3[];\nexport {};\n"
  },
  {
    "path": "dist/highlighter.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>RampenSau Syntax Highlighter Demo</title>\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n    <style>\n      body {\n        margin: 0;\n        padding: 0;\n        font-family: system-ui, -apple-system, sans-serif;\n        background: #18181a;\n        color: #f8f8f2;\n        min-height: 100vh;\n      }\n      h1 {\n        text-align: center;\n      }\n      #syntax-container {\n        transition: background-color 0.3s ease;\n      }\n      button {\n        font-size: 1rem;\n        border-radius: 0.375rem;\n        transition: all 0.2s ease;\n        display: block;\n        width: 100%;\n        margin-bottom: 2rem;\n        padding: 0.5rem 1.5rem;\n        color: white;\n        border: none;\n        border-radius: 0.25rem;\n        cursor: pointer;\n        font-weight: 600;\n      }\n      button:hover {\n        opacity: 0.9;\n      }\n      .code-block {\n        transition: background-color 0.3s ease;\n        border-radius: 0.5rem;\n        padding: 1.5rem;\n        margin-bottom: 2rem;\n      }\n      pre {\n        margin: 0;\n        line-height: 1.5;\n        font-family: monospace;\n        overflow: auto;\n      }\n      h3 {\n        margin-bottom: 1rem;\n        margin-top: 0;\n        font-weight: 300;\n      }\n      .wrapper {\n        max-width: 64rem;\n        margin: 0 auto;\n        padding: 2rem;\n      }\n    </style>\n  </head>\n  <body>\n    <div id=\"syntax-container\"></div>\n    <script type=\"module\">\n      import {\n        generateColorRamp,\n        generateColorRampWithCurve,\n        generateColorRampParams,\n        colorUtils,\n        utils,\n      } from \"./index.mjs\";\n\n      const { uniqueRandomHues, colorHarmonies, colorToCSS, harveyHue } =\n        colorUtils;\n\n      const { scaleSpreadArray, shuffleArray } = utils;\n\n      const random = (min, max) => Math.random() * (max - min) + min;\n\n      function getRampensauColors(isLightMode = false, colorCount = 24) {\n        return generateColorRamp({\n          total: colorCount,\n          hCycles: random(0.1, 2),\n          hStartCenter: 0.5,\n          sRange: [random(0.9, 0.7), random(0.5, 0.3)],\n          lRange: isLightMode\n            ? [random(0.85, 1), random(0, 0.15)]\n            : [random(0, 0.15), random(0.8, 0.99)],\n          lEasing: isLightMode\n            ? (x) => -(Math.cos(Math.PI * x) - 1) / 2\n            : (x, fr) => Math.pow(x, 1.5),\n        }).map((hsl) => colorToCSS(hsl, \"hsl\"));\n      }\n\n      class SyntaxHighlighter {\n        constructor() {\n          this.colors = [];\n\n          // Semantic color indices - organize by purpose\n          this.colorMap = {\n            // Background and UI elements\n            background: 2,\n            codeBackground: 0,\n            buttonBackground: 3,\n\n            // Syntax elements\n            comment: 5,\n            string: random(-20, -15),\n            space: 15,\n            keyword: -16,\n            number: -14,\n            function: -12,\n            method: -10,\n            property: -9,\n            operator: -7,\n            builtin: -5,\n            variable: -3,\n            default: -1,\n\n            // Always use the last color for text\n            text: -1,\n          };\n\n          // Language keywords and syntax definitions\n          this.keywords = [\n            \"class\",\n            \"def\",\n            \"return\",\n            \"for\",\n            \"in\",\n            \"if\",\n            \"const\",\n            \"let\",\n            \"var\",\n            \"async\",\n            \"await\",\n            \"try\",\n            \"catch\",\n            \"throw\",\n            \"new\",\n            \"function\",\n            \"typeof\",\n            \"instanceof\",\n            \"this\",\n            \"super\",\n            \"extends\",\n            \"static\",\n          ];\n          this.builtins = [\n            \"console\",\n            \"Math\",\n            \"Object\",\n            \"Array\",\n            \"String\",\n            \"Number\",\n            \"Promise\",\n            \"Date\",\n            \"RegExp\",\n            \"Map\",\n            \"Set\",\n            \"JSON\",\n            \"Error\",\n          ];\n          this.operators = [\n            \"+\",\n            \"-\",\n            \"*\",\n            \"/\",\n            \"%\",\n            \"=\",\n            \"==\",\n            \"===\",\n            \"!=\",\n            \"!==\",\n            \">\",\n            \"<\",\n            \">=\",\n            \"<=\",\n            \"&&\",\n            \"||\",\n            \"!\",\n            \"??\",\n            \"?.\",\n          ];\n        }\n        tokenizeLine(line) {\n          const tokens = [];\n          let current = 0;\n          let buffer = \"\";\n          const pushBuffer = (type = \"default\") => {\n            if (buffer) {\n              if (this.keywords.includes(buffer)) {\n                tokens.push({ type: \"keyword\", content: buffer });\n              } else if (this.builtins.includes(buffer)) {\n                tokens.push({ type: \"builtin\", content: buffer });\n              } else {\n                const nextChar = line[current];\n                if (nextChar === \"(\") {\n                  tokens.push({ type: \"function\", content: buffer });\n                } else if (nextChar === \".\") {\n                  tokens.push({ type: \"property\", content: buffer });\n                } else {\n                  tokens.push({ type, content: buffer });\n                }\n              }\n              buffer = \"\";\n            }\n          };\n          while (current < line.length) {\n            if (line[current] === '\"' || line[current] === \"'\") {\n              pushBuffer();\n              const quote = line[current];\n              let string = quote;\n              current++;\n              while (current < line.length && line[current] !== quote) {\n                string += line[current];\n                current++;\n              }\n              if (current < line.length) {\n                string += quote;\n                current++;\n              }\n              tokens.push({ type: \"string\", content: string });\n              continue;\n            }\n            if (line[current] === \"/\" && line[current + 1] === \"/\") {\n              pushBuffer();\n              tokens.push({ type: \"comment\", content: line.slice(current) });\n              break;\n            }\n            const possibleOperator = this.operators.find((op) =>\n              line.slice(current).startsWith(op)\n            );\n            if (possibleOperator) {\n              pushBuffer();\n              tokens.push({ type: \"operator\", content: possibleOperator });\n              current += possibleOperator.length;\n              continue;\n            }\n            if (line[current] === \".\") {\n              pushBuffer();\n              tokens.push({ type: \"operator\", content: \".\" });\n              current++;\n              while (current < line.length && /[\\w$_]/.test(line[current])) {\n                buffer += line[current];\n                current++;\n              }\n              if (buffer) {\n                tokens.push({ type: \"method\", content: buffer });\n                buffer = \"\";\n              }\n              continue;\n            }\n            if (/[0-9]/.test(line[current])) {\n              pushBuffer();\n              let number = \"\";\n              while (current < line.length && /[0-9.]/.test(line[current])) {\n                number += line[current];\n                current++;\n              }\n              tokens.push({ type: \"number\", content: number });\n              continue;\n            }\n            if (/\\s/.test(line[current])) {\n              pushBuffer();\n              let spaces = \"\";\n              while (current < line.length && /\\s/.test(line[current])) {\n                spaces += line[current] === \" \" ? \"·\" : line[current];\n                current++;\n              }\n              tokens.push({ type: \"space\", content: spaces });\n              continue;\n            }\n            if (/[\\w$_]/.test(line[current])) {\n              buffer += line[current];\n              current++;\n              continue;\n            }\n            pushBuffer();\n            tokens.push({ type: \"operator\", content: line[current] });\n            current++;\n          }\n          pushBuffer();\n          return tokens;\n        }\n        createTokenSpan(token) {\n          const span = document.createElement(\"span\");\n          span.textContent = token.content;\n          let colorIndex = this.colorMap[token.type];\n          if (typeof colorIndex === \"undefined\")\n            colorIndex = this.colorMap.default;\n          // Support negative indices for end of array\n          const idx =\n            colorIndex < 0 ? this.colors.length + colorIndex : colorIndex;\n          span.style.color =\n            this.colors[idx] || this.colors[this.colors.length - 1];\n          return span;\n        }\n        renderLine(line) {\n          const div = document.createElement(\"div\");\n          const tokens = this.tokenizeLine(line);\n          tokens.forEach((token) => {\n            div.appendChild(this.createTokenSpan(token));\n          });\n          return div;\n        }\n\n        createCodeBlocks() {\n          const codeContainer = document.createElement(\"div\");\n          codeContainer.style.display = \"flex\";\n          codeContainer.style.flexDirection = \"column\";\n          codeContainer.style.gap = \"2rem\";\n          codeContainer.appendChild(\n            this.createCodeBlock(\"JavaScript Example\", javascriptCode)\n          );\n          codeContainer.appendChild(\n            this.createCodeBlock(\"Python Example\", pythonCode)\n          );\n          return codeContainer;\n        }\n\n        createShuffleButton() {\n          const shuffleButton = document.createElement(\"button\");\n          shuffleButton.textContent = \"Generate Theme\";\n          shuffleButton.style.background = this.colors[3];\n          shuffleButton.style.color =\n            this.colors[this.colors.length - 1] || \"#fff\";\n          shuffleButton.addEventListener(\"click\", () => {\n            this.shuffle();\n            this.updateColors();\n          });\n          return shuffleButton;\n        }\n\n        applyContainerStyles(container) {\n          container.style.background = this.colors[2];\n          container.style.color = this.colors[this.colors.length - 1];\n          return container;\n        }\n\n        render(container) {\n          container.innerHTML = \"\";\n          container.className = \"\";\n\n          // Apply base styles\n          this.applyContainerStyles(container);\n\n          // Create and style wrapper\n          const wrapper = document.createElement(\"div\");\n          wrapper.className = \"wrapper\";\n          wrapper.style.color = this.colors[this.colors.length - 1];\n\n          const title = document.createElement(\"h1\");\n          title.textContent = \"RampenSau Syntax Highlighter\";\n          title.style.color = \"currentColor\";\n          // Add button and code blocks\n          wrapper.appendChild(title);\n          wrapper.appendChild(this.createShuffleButton());\n          wrapper.appendChild(this.createCodeBlocks());\n\n          // Add everything to container\n          container.appendChild(wrapper);\n        }\n\n        createCodeBlock(title, code) {\n          const block = document.createElement(\"div\");\n          block.classList.add(\"code-block\");\n          block.style.background = this.colors[0];\n          block.style.color = this.colors[this.colors.length - 1];\n          const heading = document.createElement(\"h3\");\n          heading.textContent = title;\n          heading.style.color = this.colors[this.colors.length - 1];\n          block.appendChild(heading);\n          const pre = document.createElement(\"pre\");\n          pre.style.color = this.colors[this.colors.length - 1];\n          code.split(\"\\n\").forEach((line) => {\n            pre.appendChild(this.renderLine(line));\n          });\n          block.appendChild(pre);\n          return block;\n        }\n\n        shuffle() {\n          this.colors = getRampensauColors(random(0, 1) < 0.5, 40);\n        }\n\n        updateColors() {\n          const container = document.querySelector(\"#syntax-container\");\n          this.applyContainerStyles(container);\n\n          const wrapper = container.querySelector(\".wrapper\");\n          wrapper.style.color = this.colors[this.colors.length - 1];\n\n          // Update button styles\n          const button = wrapper.querySelector(\"button\");\n          button.style.background = this.colors[3];\n          button.style.color = this.colors[this.colors.length - 1];\n\n          // Replace code container with new one\n          const oldCodeContainer = wrapper.querySelector(\"div\");\n          const newCodeContainer = this.createCodeBlocks();\n          wrapper.replaceChild(newCodeContainer, oldCodeContainer);\n        }\n      }\n\n      const pythonCode = `class DataProcessor:\n    def __init__(self, data: List[Dict]):\n        self.data = data\n        self._processed = False\n\n    def process(self) -> None:\n        \"\"\"Process the data and update internal state.\"\"\"\n        for item in self.data:\n            if item['status'] == 'active':\n                self._transform(item)\n        self._processed = True\n\n    @property\n    def is_processed(self) -> bool:\n        return self._processed`;\n\n      const javascriptCode = `const fetchUserData = async (userId) => {\n  try {\n    const response = await api.get(${\"`/users/${userId}`\"});\n    const { data } = response;\n    \n    if (!data.isActive) {\n      throw new Error('User account inactive');\n    }\n\n    return {\n      ...data,\n      lastLogin: new Date(data.lastLogin)\n    };\n  } catch (error) {\n    console.error('Failed to fetch user:', error);\n    return null;\n  }\n};`;\n\n      // Mount highlighter\n      const container = document.getElementById(\"syntax-container\");\n      const highlighter = new SyntaxHighlighter();\n      highlighter.shuffle();\n      highlighter.render(container);\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "dist/index.cjs",
    "content": "\"use strict\";\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOwnPropNames = Object.getOwnPropertyNames;\nvar __hasOwnProp = Object.prototype.hasOwnProperty;\nvar __export = (target, all) => {\n  for (var name in all)\n    __defProp(target, name, { get: all[name], enumerable: true });\n};\nvar __copyProps = (to, from, except, desc) => {\n  if (from && typeof from === \"object\" || typeof from === \"function\") {\n    for (let key of __getOwnPropNames(from))\n      if (!__hasOwnProp.call(to, key) && key !== except)\n        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n  }\n  return to;\n};\nvar __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n// src/index.ts\nvar index_exports = {};\n__export(index_exports, {\n  colorUtils: () => colorUtils_exports,\n  generateColorRamp: () => generateColorRamp,\n  generateColorRampParams: () => generateColorRampParams,\n  generateColorRampWithCurve: () => generateColorRampWithCurve,\n  utils: () => utils_exports\n});\nmodule.exports = __toCommonJS(index_exports);\n\n// src/utils.ts\nvar utils_exports = {};\n__export(utils_exports, {\n  lerp: () => lerp,\n  makeCurveEasings: () => makeCurveEasings,\n  pointOnCurve: () => pointOnCurve,\n  scaleSpreadArray: () => scaleSpreadArray,\n  shuffleArray: () => shuffleArray\n});\nfunction shuffleArray(array, rndFn = Math.random) {\n  const copy = [...array];\n  let currentIndex = copy.length, randomIndex;\n  while (currentIndex != 0) {\n    randomIndex = Math.floor(rndFn() * currentIndex);\n    currentIndex--;\n    [copy[currentIndex], copy[randomIndex]] = [\n      copy[randomIndex],\n      copy[currentIndex]\n    ];\n  }\n  return copy;\n}\nvar lerp = (amt, from, to) => from + amt * (to - from);\nvar scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {\n  if (!valuesToFill || valuesToFill.length < 2) {\n    throw new Error(\"valuesToFill array must have at least two values.\");\n  }\n  if (targetSize < 1 && padding > 0) {\n    throw new Error(\"Target size must be at least 1\");\n  }\n  if (targetSize < valuesToFill.length && padding === 0) {\n    throw new Error(\n      \"Target size must be greater than or equal to the valuesToFill array length.\"\n    );\n  }\n  const result = new Array(targetSize);\n  if (padding <= 0) {\n    const len = valuesToFill.length;\n    const lastIdx = len - 1;\n    const totalAdded = targetSize - len;\n    const baseAdds = Math.floor(totalAdded / lastIdx);\n    const remainder = totalAdded % lastIdx;\n    let currentResultIdx = 0;\n    for (let i = 0; i < lastIdx; i++) {\n      const startVal = valuesToFill[i];\n      const endVal = valuesToFill[i + 1];\n      const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);\n      for (let j = 0; j < segmentLen; j++) {\n        const t = j / segmentLen;\n        result[currentResultIdx++] = fillFunction(t, startVal, endVal);\n      }\n    }\n    result[currentResultIdx] = valuesToFill[lastIdx];\n    return result;\n  }\n  const domainStart = padding;\n  const domainEnd = 1 - padding;\n  const lenMinus1 = valuesToFill.length - 1;\n  const normalizedPositions = new Float64Array(valuesToFill.length);\n  for (let i = 0; i < valuesToFill.length; i++) {\n    normalizedPositions[i] = i / lenMinus1;\n  }\n  let segmentIndex = 0;\n  for (let i = 0; i < targetSize; i++) {\n    const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);\n    const adjustedT = domainStart + t * (domainEnd - domainStart);\n    while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {\n      segmentIndex++;\n    }\n    const segmentStart = normalizedPositions[segmentIndex];\n    const segmentEnd = normalizedPositions[segmentIndex + 1];\n    let segmentT = 0;\n    if (segmentEnd > segmentStart) {\n      segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);\n    }\n    const fromValue = valuesToFill[segmentIndex];\n    const toValue = valuesToFill[segmentIndex + 1];\n    result[i] = fillFunction(segmentT, fromValue, toValue);\n  }\n  return result;\n};\nvar pointOnCurve = (curveMethod, curveAccent) => {\n  return (t) => {\n    const limit = Math.PI / 2;\n    const slice = limit / 1;\n    const percentile = t;\n    let x = 0, y = 0;\n    if (curveMethod === \"lam\\xE9\") {\n      const t2 = percentile * limit;\n      const exp = 2 / (2 + 20 * curveAccent);\n      const cosT = Math.cos(t2);\n      const sinT = Math.sin(t2);\n      x = Math.sign(cosT) * Math.abs(cosT) ** exp;\n      y = Math.sign(sinT) * Math.abs(sinT) ** exp;\n    } else if (curveMethod === \"arc\") {\n      y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);\n      x = Math.sin(Math.PI / 2 + t * slice - curveAccent);\n    } else if (curveMethod === \"pow\") {\n      x = Math.pow(1 - percentile, 1 - curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (curveMethod === \"powY\") {\n      x = Math.pow(1 - percentile, curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (curveMethod === \"powX\") {\n      x = Math.pow(percentile, curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (typeof curveMethod === \"function\") {\n      const [xFunc, yFunc] = curveMethod(t, curveAccent);\n      x = xFunc;\n      y = yFunc;\n    } else {\n      throw new Error(\n        `pointOnCurve() curveMethod parameter is expected to be \"lam\\xE9\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${curveMethod}\\` given.`\n      );\n    }\n    return { x, y };\n  };\n};\nvar makeCurveEasings = (curveMethod, curveAccent) => {\n  const point = pointOnCurve(curveMethod, curveAccent);\n  return {\n    sEasing: (t) => point(t).x,\n    lEasing: (t) => point(t).y\n  };\n};\n\n// src/colorUtils.ts\nvar colorUtils_exports = {};\n__export(colorUtils_exports, {\n  colorHarmonies: () => colorHarmonies,\n  colorToCSS: () => colorToCSS,\n  harveyHue: () => harveyHue,\n  hsv2hsl: () => hsv2hsl,\n  normalizeHue: () => normalizeHue,\n  uniqueRandomHues: () => uniqueRandomHues\n});\nfunction normalizeHue(h) {\n  return (h % 360 + 360) % 360;\n}\nfunction harveyHue(h) {\n  h = normalizeHue(h) / 360;\n  if (h === 1 || h === 0) return h;\n  h = 1 + h % 1;\n  const seg = 1 / 6;\n  const a = h % seg / seg * Math.PI / 2;\n  const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];\n  const i = Math.floor(h * 6);\n  const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];\n  return cases[i % 6] * 360;\n}\nvar colorHarmonies = {\n  complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],\n  splitComplementary: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 150),\n    normalizeHue(h - 150)\n  ],\n  triadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 120),\n    normalizeHue(h + 240)\n  ],\n  tetradic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 90),\n    normalizeHue(h + 180),\n    normalizeHue(h + 270)\n  ],\n  pentadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 72),\n    normalizeHue(h + 144),\n    normalizeHue(h + 216),\n    normalizeHue(h + 288)\n  ],\n  hexadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 60),\n    normalizeHue(h + 120),\n    normalizeHue(h + 180),\n    normalizeHue(h + 240),\n    normalizeHue(h + 300)\n  ],\n  monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],\n  // min 2 for RampenSau\n  doubleComplementary: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 180),\n    normalizeHue(h + 30),\n    normalizeHue(h + 210)\n  ],\n  compound: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 180),\n    normalizeHue(h + 60),\n    normalizeHue(h + 240)\n  ],\n  analogous: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 30),\n    normalizeHue(h + 60),\n    normalizeHue(h + 90),\n    normalizeHue(h + 120),\n    normalizeHue(h + 150)\n  ]\n};\nfunction uniqueRandomHues({\n  startHue = 0,\n  total = 9,\n  minHueDiffAngle = 60,\n  rndFn = Math.random\n} = {}) {\n  minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);\n  const baseHue = startHue ?? rndFn() * 360;\n  const huesToPickFrom = Array.from(\n    {\n      length: Math.round(360 / minHueDiffAngle)\n    },\n    (_, i) => (baseHue + i * minHueDiffAngle) % 360\n  );\n  let randomizedHues = shuffleArray(huesToPickFrom, rndFn);\n  if (randomizedHues.length > total) {\n    randomizedHues = randomizedHues.slice(0, total);\n  }\n  return randomizedHues;\n}\nvar hsv2hsl = ([h, s, v]) => {\n  const l = v - v * s / 2;\n  const m = Math.min(l, 1 - l);\n  const s_hsl = m === 0 ? 0 : (v - l) / m;\n  return [h, s_hsl, l];\n};\nvar colorModsCSS = {\n  oklch: (color) => [\n    color[2] * 100 + \"%\",\n    color[1] * 100 + \"%\",\n    color[0]\n  ],\n  lch: (color) => [\n    color[2] * 100 + \"%\",\n    color[1] * 100 + \"%\",\n    color[0]\n  ],\n  hsl: (color) => [\n    color[0],\n    color[1] * 100 + \"%\",\n    color[2] * 100 + \"%\"\n  ],\n  hsv: (color) => {\n    const [h, s, l] = hsv2hsl(color);\n    return [h, s * 100 + \"%\", l * 100 + \"%\"];\n  }\n};\nvar colorToCSS = (color, mode = \"oklch\") => {\n  const cssMode = mode === \"hsv\" ? \"hsl\" : mode;\n  return `${cssMode}(${colorModsCSS[mode](color).join(\" \")})`;\n};\n\n// src/core.ts\nfunction generateColorRamp({\n  total = 9,\n  hStart = Math.random() * 360,\n  hStartCenter = 0.5,\n  hEasing = (x) => x,\n  hCycles = 1,\n  sRange = [0.4, 0.35],\n  sEasing = (x) => Math.pow(x, 2),\n  lRange = [Math.random() * 0.1, 0.9],\n  lEasing = (x) => Math.pow(x, 1.5),\n  transformFn = ([h, s, l]) => [h, s, l],\n  hueList\n} = {}) {\n  const lDiff = lRange[1] - lRange[0];\n  const sDiff = sRange[1] - sRange[0];\n  const length = hueList && hueList.length > 0 ? hueList.length : total;\n  return Array.from({ length }, (_, i) => {\n    const relI = length > 1 ? i / (length - 1) : 0;\n    const fraction = 1 / length;\n    const hue = hueList ? hueList[i] : normalizeHue(\n      hStart + // Add the starting hue\n      (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)\n      // Calculate the hue based on the easing function\n    );\n    const saturation = sRange[0] + sDiff * sEasing(relI, fraction);\n    const lightness = lRange[0] + lDiff * lEasing(relI, fraction);\n    return transformFn([hue, saturation, lightness], i);\n  });\n}\nvar generateColorRampWithCurve = ({\n  total = 9,\n  hStart = Math.random() * 360,\n  hStartCenter = 0.5,\n  hCycles = 1,\n  sRange = [0.4, 0.35],\n  lRange = [Math.random() * 0.1, 0.9],\n  hueList,\n  curveMethod = \"lam\\xE9\",\n  curveAccent = 0.5,\n  transformFn = ([h, s, l]) => [h, s, l]\n} = {}) => {\n  const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);\n  return generateColorRamp({\n    total,\n    hStart,\n    hStartCenter,\n    hCycles,\n    sRange,\n    lRange,\n    sEasing,\n    lEasing,\n    transformFn,\n    hueList\n  });\n};\n\n// src/index.ts\nvar generateColorRampParams = {\n  total: {\n    default: 5,\n    props: { min: 4, max: 50, step: 1 }\n  },\n  hStart: {\n    default: 0,\n    props: { min: 0, max: 360, step: 0.1 }\n  },\n  hCycles: {\n    default: 1,\n    props: { min: -2, max: 2, step: 1e-3 }\n  },\n  hStartCenter: {\n    default: 0.5,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  minLight: {\n    default: Math.random() * 0.2,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  maxLight: {\n    default: 0.89 + Math.random() * 0.11,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  minSaturation: {\n    default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  maxSaturation: {\n    default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  curveMethod: {\n    default: \"lam\\xE9\",\n    props: { options: [\"lam\\xE9\", \"sine\", \"power\", \"linear\"] }\n  },\n  curveAccent: {\n    default: 0.5,\n    props: { min: 0, max: 5, step: 0.01 }\n  }\n};\n"
  },
  {
    "path": "dist/index.d.ts",
    "content": "export * as utils from \"./utils\";\nexport * as colorUtils from \"./colorUtils\";\nexport { generateColorRamp, generateColorRampWithCurve } from \"./core\";\n/**\n * A set of default parameters and sane ranges to use with `generateColorRamp`\n * when coming up with random color ramps.\n */\nexport declare const generateColorRampParams: {\n    total: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    hStart: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    hCycles: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    hStartCenter: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    minLight: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    maxLight: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    minSaturation: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    maxSaturation: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n    curveMethod: {\n        default: string;\n        props: {\n            options: string[];\n        };\n    };\n    curveAccent: {\n        default: number;\n        props: {\n            min: number;\n            max: number;\n            step: number;\n        };\n    };\n};\n"
  },
  {
    "path": "dist/index.html",
    "content": "<!DOCTYPE html>\n\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <!-- Primary Meta Tags -->\n    <title>\n      RampenSau — Color ramp generator using curves within the HSL color model\n    </title>\n    <meta\n      name=\"title\"\n      content=\"RampenSau — Color ramp generator using curves within the HSL color model\"\n    />\n    <meta\n      name=\"description\"\n      content=\"RampenSau is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSL color model. This page serves as preview for the variety of options the function takes.\"\n    />\n    <meta\n      name=\"keywords\"\n      content=\"color, colour, generative, generative-art, generative-design, palette, colorpalette\"\n    />\n\n    <!-- Open Graph / Facebook -->\n    <meta property=\"og:type\" content=\"website\" />\n    <meta property=\"og:url\" content=\"https://meodai.github.io/rampensau/\" />\n    <meta\n      property=\"og:title\"\n      content=\"RampenSau — Color ramp generator using curves within the HSL color model\"\n    />\n    <meta\n      property=\"og:description\"\n      content=\"RampenSau is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSL color model. This page serves as preview for the variety of options the function takes.\"\n    />\n    <meta\n      property=\"og:image\"\n      content=\"https://meodai.github.io/rampensau/socialfb.png\"\n    />\n\n    <!-- Twitter -->\n    <meta property=\"twitter:card\" content=\"summary_large_image\" />\n    <meta\n      property=\"twitter:url\"\n      content=\"https://meodai.github.io/rampensau/\"\n    />\n    <meta\n      property=\"twitter:title\"\n      content=\"RampenSau — Color ramp generator using curves within the HSL color model\"\n    />\n    <meta\n      property=\"twitter:description\"\n      content=\"RampenSau is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSL color model. This page serves as preview for the variety of options the function takes.rampensau is lightweight, dependency free and fast JavaScript function written in TypeScript. It generates color ramps based on a curve within the HSL color model. This page serves as preview for the variety of options the function takes.\"\n    />\n    <meta\n      property=\"twitter:image\"\n      content=\"https://meodai.github.io/rampensau/socialfb.png\"\n    />\n\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" />\n\n    <link rel=\"icon\" type=\"image/png\" />\n    <link rel=\"preconnect\" href=\"https://fonts.googleapis.com\" crossorigin />\n\n    <link\n      href=\"https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,800&display=swap\"\n      rel=\"stylesheet\"\n    />\n    <link\n      rel=\"stylesheet\"\n      href=\"https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css\"\n    />\n    <script type=\"module\">\n      // Pre-load token-beam for use later\n      window.tokenBeamPromise = import('https://esm.sh/token-beam@1.6.0/es2022/token-beam.mjs');\n    </script>\n    <style>\n      :root {\n        font-family: \"Bricolage Grotesque\", sans-serif;\n        --light: var(--clast);\n        --dark: var(--c0);\n        --highlight: var(--csecondlast);\n\n        background: var(--dark);\n        color: var(--dark);\n\n        --sidebarwidth: max(22rem, 20vw);\n\n        scrollbar-color: var(--dark) var(--light);\n        scrollbar-width: thin;\n      }\n\n      ::selection {\n        background: var(--dark);\n        color: var(--highlight);\n      }\n\n      a,\n      [data-code] i {\n        color: var(--highlight);\n      }\n\n      .ellogo__font {\n        fill: var(--light);\n      }\n\n      .ellogo__logo {\n        stroke: var(--highlight);\n      }\n\n      .sidebar {\n        position: fixed;\n        width: var(--sidebarwidth);\n        order: 1;\n        left: 0;\n        top: 0;\n        bottom: 0;\n\n        z-index: 2;\n\n        border-right: 1px solid var(--light);\n      }\n\n      .sidebar__button {\n        display: none;\n        position: absolute;\n        right: 0;\n        top: 0;\n        transform: translate(calc(100% + 1em), 1em);\n        width: 1.6rem;\n        height: 1.3rem;\n        --line-color: var(--dark);\n      }\n\n      .sidebar__button i {\n        position: absolute;\n        top: 0;\n        left: 0;\n        right: 0;\n        height: 2px;\n        background: var(--line-color);\n      }\n\n      .sidebar__button i:nth-child(2) {\n        top: 50%;\n        transform: translateY(-50%);\n      }\n\n      .sidebar__button i:nth-child(3) {\n        bottom: 0;\n        top: auto;\n      }\n\n      .code-title {\n        margin-top: 0;\n      }\n\n      .settings {\n        position: absolute;\n        padding: 2rem 2rem 0;\n        background: var(--dark);\n        color: var(--light);\n        inset: 0;\n        overflow-y: auto;\n        box-sizing: border-box;\n\n        /* style scrollbar */\n        scrollbar-color: var(--highlight) var(--dark);\n        scrollbar-width: thin;\n\n        overscroll-behavior: contain;\n        scroll-behavior: smooth;\n      }\n\n      .button {\n        display: block;\n        margin-bottom: 1rem;\n        padding: 0.8rem 1rem 0.65rem;\n        color: var(--highlight);\n        border: 2px solid currentColor;\n        display: block;\n        width: 100%;\n        font-weight: 900;\n      }\n\n      .button--main {\n        background: var(--dark);\n        color: var(--light);\n        border-color: var(--light);\n        display: none;\n      }\n\n      @media (max-width: 500px) {\n        .button--main {\n          display: block;\n        }\n      }\n\n      .button:hover {\n        background: var(--highlight);\n        color: var(--dark);\n        border-color: var(--highlight);\n      }\n\n      .projectlink {\n        display: inline-block;\n        color: var(--dark);\n        margin-top: 0.75em;\n        font-size: 0.8em;\n        position: relative;\n        z-index: 10;\n        text-decoration: none;\n      }\n\n      .projectlink::after {\n        content: \"↗\";\n        display: inline-block;\n        margin-left: 0.5em;\n        font-size: 0.8em;\n      }\n\n      figcaption {\n        display: none;\n      }\n\n      .tabs {\n        width: 100%;\n        background: var(--dark);\n        box-shadow: 0 0 0 2px var(--dark), 0 0 0.6rem var(--dark),\n          0 -2rem 0 2rem var(--dark);\n      }\n\n      .tabs__contents {\n        border: 2px solid var(--light);\n        overflow: hidden;\n        background: var(--dark);\n        z-index: 1;\n        position: relative;\n      }\n\n      .tabs__slider {\n        display: flex;\n        gap: 0;\n        width: calc(var(--tabs-length, 1) * 100%);\n        transform: translateX(\n          calc(var(--active-tab, 0) * calc(-100% / var(--tabs-length, 1)))\n        );\n        transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1);\n      }\n\n      .tabs__content {\n        flex: 0 0 calc(100% / var(--tabs-length, 1));\n        width: calc(100% / var(--tabs-length, 1));\n        box-sizing: border-box;\n        padding: 0.5rem;\n        pointer-events: none;\n        transform: scale(0.8);\n        opacity: 0;\n\n        transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1) 50ms,\n          opacity 300ms cubic-bezier(0.7, 0.3, 0, 1) 50ms;\n\n        will-change: transform, opacity;\n\n        position: relative;\n      }\n\n      .tabs__content--active {\n        pointer-events: auto;\n\n        transform: scale(1);\n        opacity: 1;\n\n        transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1) 100ms,\n          opacity 300ms cubic-bezier(0.7, 0.3, 0, 1) 100ms;\n      }\n\n      .tabs__controls {\n        display: flex;\n        margin-top: -2px;\n      }\n\n      .tabs__control {\n        border: 2px solid var(--light);\n        padding: 0.8rem 0.5rem;\n\n        font-size: 0.8rem;\n        font-weight: 800;\n        line-height: 1;\n\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        flex-grow: 1;\n\n        cursor: pointer;\n      }\n\n      .tabs__control:hover,\n      .tabs__control--active {\n        background: var(--light);\n        color: var(--dark);\n      }\n\n      .tabs__control--active {\n        z-index: 1;\n      }\n\n      .tabs__control + .tabs__control {\n        margin-left: -2px;\n      }\n\n      .tabs__icon {\n        display: inline-block;\n        height: 1.25em;\n        aspect-ratio: 1;\n        margin-right: 0.5em;\n      }\n\n      .tabs__label {\n        display: inline-block;\n        font-weight: 800;\n        line-height: 1;\n        margin-top: 0.1em;\n        white-space: nowrap;\n        overflow: hidden;\n        text-overflow: ellipsis;\n      }\n\n      .icon::before {\n        content: \"\";\n        position: absolute;\n        inset: 0;\n      }\n\n      .icon {\n        position: relative;\n      }\n\n      .icon--hue::before {\n        border-radius: 50%;\n        box-shadow: inset 0 0 0 1.5px currentColor;\n      }\n\n      .icon--sl::before {\n        box-shadow: inset 0 0 0 1.5px currentColor;\n      }\n\n      .icon--fncall::before {\n        content: \"{}\";\n        font-weight: 100;\n        font-size: 1.25em;\n        line-height: 1;\n      }\n\n      .disc--mini {\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        height: calc(100% + 3px);\n        width: 1px;\n\n        transform: translate(-50%, -50%) rotate(calc(var(--c-h) * 1deg));\n      }\n\n      .disc--mini::before {\n        position: absolute;\n        content: \"\";\n        top: 0;\n        left: 50%;\n        width: 100%;\n        height: 1px;\n        background: var(--light);\n      }\n\n      .disc--mini-sl {\n        left: calc(4px + var(--c-s) * calc(100% - 8px));\n        top: calc(4px + (1 - var(--c-l)) * calc(100% - 8px));\n\n        position: absolute;\n        width: 1.5px;\n        height: 1.5px;\n        transform: translate(-50%, -50%);\n      }\n\n      .disc--mini-sl::before {\n        position: absolute;\n        width: 1px;\n        height: 1px;\n      }\n\n      .settings__top {\n        margin-bottom: 2rem;\n        padding: 2rem;\n        padding-top: 0.5rem;\n        margin: -2rem -2rem 2rem;\n\n        position: sticky;\n        top: 0;\n        z-index: 1;\n      }\n\n      @media (max-width: 500px) {\n        .settings__top {\n          padding-top: 1.5rem;\n        }\n      }\n\n      .h-viz {\n        position: relative;\n        aspect-ratio: 1;\n        perspective: 400px;\n        will-change: perspective;\n        transition: perspective 300ms cubic-bezier(0.7, 0.3, 0, 1);\n      }\n\n      .h-viz:hover {\n        perspective: 1000px;\n      }\n\n      .h-viz .disc {\n        position: absolute;\n        bottom: 50%;\n        left: 50%;\n        width: 7rem;\n        height: 7rem;\n\n        transform: translate(-50%, 100%) translateY(calc(-100% * var(--c-i)))\n          rotateX(34deg) scale(calc(sin(0.1 + var(--c-i2) * 1.5))) scale(1.5);\n        transition: calc(300ms - 50ms + var(--c-i2) * 200ms) transform\n          cubic-bezier(0.175, 0.885, 0.32, 1.275);\n      }\n\n      .h-viz .disc::before {\n        content: \"\";\n        position: absolute;\n        top: 0;\n        left: 0;\n        width: 100%;\n        height: 100%;\n        border-radius: 50%;\n        background: conic-gradient(\n          hsl(360, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(315, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(270, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(225, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(180, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(135, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(90, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(45, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),\n          hsl(0, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%))\n        );\n        transform: scaleX(-1); /* lazy flip the gradient */\n        opacity: 0.55;\n      }\n\n      .h-viz .disc::after {\n        content: \"\";\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        width: 0.6rem;\n        border-radius: 50%;\n        height: 0.6rem;\n        transform: translate(-50%, -50%) rotate(calc(var(--c-h) * 1deg))\n          translateY(-3.3rem) scale(calc(3 - var(--c-i2) * 4.5));\n        transform-origin: 50% 50%;\n        box-shadow: 0 0 0 1px var(--dark),\n          0 0 0 2px\n            hsl(var(--c-h), calc(var(--c-s) * 100%), calc(var(--c-l) * 100%));\n        background: hsl(\n          var(--c-h),\n          calc(var(--c-s) * 100%),\n          calc(var(--c-l) * 100%)\n        );\n        transition: calc(300ms - 50ms + var(--c-i2) * 200ms) transform\n          cubic-bezier(0.175, 0.885, 0.32, 1.275);\n        transition-duration: calc(100ms + var(--c-i2) * 200ms);\n      }\n\n      .h-viz:hover .disc {\n        transform: translate(-50%, 100%) translateY(calc(-100% * var(--c-i)))\n          rotateX(34deg) scale(1);\n      }\n      .h-viz:hover .disc::after {\n        transform: translate(-50%, -50%) rotate(calc(var(--c-h) * 1deg))\n          translateY(-3.3rem) scale(1);\n      }\n      .h-viz:hover .disc::before {\n        mask-image: radial-gradient(\n          circle farthest-side at center,\n          transparent 90%,\n          white 70%\n        );\n        -webkit-mask-image: radial-gradient(\n          circle farthest-side at center,\n          transparent 90%,\n          white 70%\n        );\n      }\n\n      .sl-viz {\n        position: relative;\n        aspect-ratio: 1;\n      }\n\n      .sl-viz .disc--sl {\n        position: absolute;\n        width: 100%;\n        height: 100%;\n      }\n\n      .sl-viz .disc--sl::after {\n        content: \"\";\n        position: absolute;\n        inset: 0;\n      }\n\n      .sl-viz .disc--sl::before {\n        left: calc(5% + var(--c-s) * 90%);\n        top: calc(5% + (1 - var(--c-l)) * 90%);\n\n        content: \"\";\n        position: absolute;\n\n        width: 0.6rem;\n        aspect-ratio: 1;\n        border-radius: 50%;\n\n        box-shadow: 0 0 0 1px var(--light),\n          0 0 0 2px\n            hsl(var(--c-h), calc(var(--c-s) * 100%), calc(var(--c-l) * 100%));\n        background: hsl(\n          var(--c-h),\n          calc(var(--c-s) * 100%),\n          calc(var(--c-l) * 100%)\n        );\n        transform: translate(-50%, -50%);\n      }\n\n      .main {\n        margin-left: var(--sidebarwidth);\n        background: var(--light);\n        padding: 4rem;\n      }\n\n      .section {\n        display: flex;\n        margin-top: 4rem;\n        max-width: calc(50rem + 12.5vw);\n      }\n      .section--first {\n        margin-top: 0;\n      }\n      .section--vertical {\n        flex-direction: column;\n      }\n      .section__text {\n        flex: 1 0 calc(100% - 25rem - 4rem);\n        order: 1;\n        width: calc(100% - 25rem - 4rem);\n      }\n      .section--vertical .section__text {\n        flex: 1 0 calc(100% - 10rem - 4rem);\n        order: 1;\n        width: calc(100% - 10rem - 4rem);\n      }\n\n      .section__fig {\n        flex: 1 1 25rem;\n        order: 0;\n        margin-right: 4rem;\n      }\n      .section code {\n        font-family: monospace;\n        background: var(--dark);\n        color: var(--light);\n        padding: 0 0.75ex 0.2ex;\n      }\n\n      .intro {\n        margin-bottom: 4rem;\n      }\n\n      h1 {\n        font-size: calc(0.64rem + 5vw);\n        font-weight: 900;\n        letter-spacing: -0.025em;\n        margin-top: 0;\n        line-height: 0.85;\n        margin-left: -0.045em;\n        margin-bottom: 2rem;\n        text-shadow: var(--tsss);\n        transition: text-shadow 300ms cubic-bezier(0.7, 0.3, 0, 1);\n      }\n\n      h1:hover {\n        text-shadow: var(--ts);\n      }\n\n      h2,\n      h3,\n      [data-names] strong {\n        margin: 0 0 2rem;\n        font-size: calc(1.5rem + 2vw);\n        font-weight: 800;\n        letter-spacing: -0.02em;\n        line-height: 0.9;\n      }\n\n      h2 + p {\n        margin-bottom: 1rem;\n      }\n\n      h2 span {\n        font-size: 0.9rem;\n        line-height: 1.2;\n        font-weight: 400;\n      }\n\n      h3 {\n        margin-top: 1.5em;\n        font-size: calc(0.5rem + 1vw);\n      }\n      pre {\n        font-family: monospace;\n        font-size: 0.75rem;\n        max-width: 100%;\n        overflow: hidden;\n      }\n\n      .fncall {\n        position: absolute;\n        inset: 0;\n        padding: 2rem;\n        display: flex;\n        flex-direction: column;\n        justify-content: center;\n        align-items: center;\n      }\n\n      .code-sample {\n        display: block;\n        max-width: 100%;\n      }\n\n      a {\n        font-size: 1em;\n        margin-top: 0.4em;\n        font-weight: 700;\n      }\n\n      button {\n        display: block;\n        background: none;\n        padding: 0;\n        border: none;\n        border-radius: 0;\n        font: inherit;\n        font-size: inherit;\n        color: inherit;\n      }\n      .main button:not(.button) {\n        cursor: pointer;\n        font-weight: bold;\n        display: inline;\n        text-decoration: underline;\n        margin: 0 0.2em;\n      }\n\n      .main button:not(.button):hover {\n        text-decoration-color: var(--highlight);\n      }\n\n      [data-colors] {\n        position: relative;\n        display: flex;\n        flex-wrap: wrap;\n        width: 100%;\n        height: calc(100vmin - 8rem);\n        margin-bottom: 4rem;\n        flex-direction: column-reverse;\n        box-shadow: 0 0 0 2px var(--dark);\n        transition: box-shadow 300ms cubic-bezier(0.7, 0.3, 0, 1);\n        transition-delay: 300ms;\n        background: var(--dark);\n        overflow: hidden;\n      }\n      [data-colors]::after {\n        content: \"\";\n        position: absolute;\n        inset: -1vmin;\n        background: linear-gradient(0deg, var(--gradient));\n        opacity: 1;\n        z-index: 1;\n        filter: blur(10vmin);\n        transform: scale(0.9) translateX(-50%);\n        transition: opacity 200ms linear;\n        transition-delay: 500ms;\n        will-change: opacity;\n      }\n      [data-colors]:hover::after {\n        opacity: 0;\n        transition-delay: 0ms;\n      }\n\n      [data-colors] i {\n        flex: 1 0 auto;\n        width: 100%;\n        background: var(--color);\n        position: relative;\n\n        font-size: calc(20vmin * 1 / var(--total));\n        transition: transform 460ms cubic-bezier(0.7, 0.3, 0, 1);\n        transition-delay: calc(100ms + var(--i) * 200ms);\n      }\n      [data-colors] span {\n        position: absolute;\n        left: 1em;\n        top: 50%;\n        background: var(--color);\n        width: 0.8em;\n        height: 0.8em;\n        border-radius: 50%;\n        transform: translateY(-50%) rotate(calc(var(--h) * 1deg));\n        z-index: 2;\n        box-shadow: 0 0 0 2px var(--c);\n        overflow: hidden;\n        display: none;\n      }\n      [data-colors] span::before {\n        content: \"\";\n        position: absolute;\n        inset: 0;\n        transform-origin: 50% 50%;\n        transform: rotate(calc(var(--h) * -1deg))\n          translateY(calc(-1 * var(--l) * 100%));\n        background: var(--c);\n        display: none;\n      }\n      [data-colors] span::after {\n        content: \"\";\n        position: absolute;\n        top: 0;\n        left: 50%;\n        width: 30%;\n        aspect-ratio: 1;\n        background: var(--c);\n        transform: translate(-50%, -50%) rotate(45deg);\n      }\n\n      [data-colors] b {\n        position: absolute;\n        top: 50%;\n        right: 1em;\n        transform: translateY(-50%);\n        color: var(--c);\n        font-weight: 800;\n        filter: hue-rotate(180deg);\n        z-index: 2;\n      }\n\n      [data-palette] {\n        display: flex;\n        flex-wrap: wrap;\n        gap: 2px;\n        cursor: pointer;\n      }\n      [data-palette] .palette-sample {\n        --rotation: round(calc(var(--rnd) * 360deg), 90deg);\n      }\n      .palette-sample {\n        position: relative;\n        background: var(--col-0);\n        aspect-ratio: 1;\n        margin: 0;\n        user-select: none;\n        flex: 0 0 calc(100% / var(--x) - 2px);\n      }\n      .palette-sample b {\n        position: absolute;\n        top: 50%;\n        left: 50%;\n        width: 50%;\n        height: 50%;\n        transform: translate(-50%, -50%) rotate(var(--rotation));\n        background: var(--col-1);\n      }\n      .palette-sample i {\n        position: absolute;\n        width: 50%;\n        height: 50%;\n        right: 0;\n      }\n      .palette-sample i:first-child {\n        background: var(--col-2);\n      }\n      .palette-sample i:last-child {\n        bottom: 0;\n        background: var(--col-3);\n      }\n\n      figure {\n        margin: 0;\n        padding: 0;\n      }\n      [data-figure] {\n        background-image: linear-gradient(to top, black, rgba(0, 0, 0, 0)),\n          linear-gradient(\n            to left,\n            hsl(var(--deg, 0deg), 100%, 50%),\n            hsl(var(--deg, 0deg), 0%, 100%)\n          );\n      }\n      [data-ramp] {\n        height: 10rem;\n        box-shadow: 0 0 0 2px var(--dark);\n      }\n      [data-list] {\n        margin: 0 0 2rem;\n        display: flex;\n        gap: 4px;\n      }\n\n      .swatch {\n        position: relative;\n        font-size: 0.8rem;\n        line-height: 1.2;\n        padding-bottom: 0.5em;\n      }\n\n      [data-list] h3 {\n        display: none;\n        margin-top: 0;\n      }\n\n      .swatch::before {\n        user-select: none;\n        content: \"\";\n        background: var(--col);\n        display: block;\n        width: 100%;\n        height: 1.2em;\n\n        box-shadow: 0 0 0 2px var(--dark);\n      }\n\n      .swatch div {\n        transform: translateY(-100%);\n        color: var(--coltext);\n        margin-left: 0.2em;\n      }\n\n      .color-info {\n        padding: 0.5rem;\n      }\n\n      .color-info strong {\n        font-size: 1rem;\n        display: block;\n        margin-bottom: 1ex;\n      }\n      .color-info button {\n        font-size: 0.6rem;\n        margin-top: 0.5em;\n\n        text-align: left;\n        overflow: hidden;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        max-width: 100%;\n      }\n      .color-info__contrast {\n        position: absolute;\n        top: 8.7rem;\n        left: 0.5rem;\n        color: var(--colbg);\n        font-size: 0.8em;\n      }\n      .color-info__contrast h5 {\n        display: none;\n      }\n      [data-list] > div {\n        flex: 1;\n      }\n      [data-copy] {\n        cursor: pointer;\n      }\n      p {\n        margin-top: 1em;\n        max-width: 42ch;\n        font-size: 0.9em;\n        letter-spacing: 0.002em;\n        line-height: 1.45;\n        font-weight: 400;\n      }\n\n      .pane__section,\n      .pane__label {\n        display: block;\n      }\n\n      .pane__label {\n        font-size: 0.8rem;\n        font-weight: 800;\n        line-height: 1;\n        margin-bottom: 0.3em;\n      }\n\n      .pane {\n        box-sizing: border-box;\n        background: var(--color-bg);\n        display: block;\n        cursor: default;\n        background: var(--dark);\n        color: var(--light);\n        --size-gutter: 1rem;\n        --color-inverted: var(--light);\n\n        margin-top: calc(var(--size-gutter) * 2);\n        select,\n        option {\n          /* windows does not apply it correctly otherwise */\n          color: var(--light);\n        }\n      }\n      .pane__section {\n        display: block;\n      }\n      .pane__section + .pane__section {\n        margin-top: calc(var(--size-gutter) * 2);\n      }\n      .pane__section--hidden {\n        display: none;\n      }\n      .pane__inputs {\n        display: flex;\n        touch-action: manipulation;\n        gap: calc(var(--size-gutter) * 0.5);\n      }\n      .pane__input--number {\n        flex-grow: 1;\n      }\n      .pane .pane__input--number + input[type=\"text\"] {\n        display: block;\n        flex-basis: 3.3rem;\n        width: 3.3rem;\n      }\n      .pane__desc {\n        margin: 1em 0 3em;\n        font-size: 0.6em;\n      }\n      .pane select {\n        font-size: 0.8em;\n        border-radius: 2rem;\n        padding: 0.2rem;\n      }\n      .pane input,\n      .pane select {\n        display: block;\n        box-sizing: border-box;\n        touch-action: manipulation;\n        font-family: \"Space Mono\", monospace;\n        border: none;\n        width: auto;\n      }\n      .pane select option {\n        color: var(--light);\n      }\n      .pane input[type=\"text\"],\n      .pane select[type=\"number\"] {\n        color: var(--color-inverted);\n        background: none;\n        border: none;\n        text-align: right;\n        font-size: 0.8em;\n        flex: 0 0 3rem;\n        width: 3rem;\n      }\n      .pane input {\n        background-color: transparent;\n        accent-color: var(--highlight);\n      }\n      .pane input[type=\"range\"] {\n        -webkit-appearance: none;\n      }\n      .pane input[type=\"range\"] {\n        margin: 0;\n        padding-top: 0.7em;\n        margin-top: -0.7em;\n      }\n      .pane input[type=\"range\"]:focus {\n        outline: none;\n      }\n      .pane input[type=\"range\"]:hover::-webkit-slider-thumb {\n        background-color: var(--color-inverted);\n        clip-path: polygon(100% 0%, 0% 0%, 50% 100%, 50% 100%);\n      }\n      .pane input[type=\"range\"]::-webkit-slider-runnable-track {\n        width: 100%;\n        height: 1rem;\n        background: transparent;\n        color: var(--dark);\n        border-radius: 0;\n        border: solid var(--light);\n        border-width: 0 0 2px;\n      }\n      .pane input[type=\"range\"]::-webkit-slider-thumb {\n        border: 2px solid transparent;\n        height: 0.75rem;\n        width: 0.5rem;\n        border-radius: 0;\n        background: var(--light);\n        -webkit-appearance: none;\n        margin-top: 0.25rem;\n        transition: 150ms background-color, 200ms clip-path,\n          200ms -webkit-clip-path;\n        clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);\n      }\n      .pane input[type=\"range\"]::-moz-range-track {\n        width: 100%;\n        height: 1rem;\n        background: transparent;\n        color: var(--dark);\n        border-radius: 0;\n        border: solid var(--light);\n        border-width: 0 0 2px;\n      }\n      .pane input[type=\"range\"]::-moz-range-thumb {\n        border: 2px solid transparent;\n        height: 0.75rem;\n        width: 0.5rem;\n        border-radius: 0;\n        background: var(--light);\n        -webkit-appearance: none;\n        margin-top: 0.25rem;\n        transition: 150ms background-color, 200ms clip-path,\n          200ms -webkit-clip-path;\n        clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);\n      }\n      .pane input[type=\"range\"]::-ms-track {\n        width: 100%;\n        height: 1rem;\n        background: transparent;\n        color: var(--dark);\n        border-radius: 0;\n        border: solid var(--light);\n        border-width: 0 0 2px;\n      }\n      .pane input[type=\"range\"]::-ms-fill-lower {\n        background: var(--light);\n        border: none;\n        border-radius: 100%;\n      }\n      .pane input[type=\"range\"]::-ms-fill-upper {\n        background: var(--light);\n        border-radius: 100%;\n        box-shadow: none;\n      }\n      .pane input[type=\"range\"]::-ms-thumb {\n        border: 2px solid transparent;\n        height: 0.75rem;\n        width: 0.5rem;\n        border-radius: 0;\n        background: var(--light);\n        -webkit-appearance: none;\n        margin-top: 0.25rem;\n        transition: 150ms background-color, 200ms clip-path,\n          200ms -webkit-clip-path;\n        clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);\n      }\n      .pane select {\n        color: var(--light);\n        width: 100%;\n        box-sizing: border-box;\n        -webkit-appearance: none;\n        border: 0;\n        box-shadow: 0 2px 0 0 var(--light);\n        border-radius: 0;\n        padding: 0.25rem 1rem 0.25rem 0rem;\n        background-color: transparent;\n        background-size: 1.25em 1.25em;\n        background-image: conic-gradient(\n          var(--light) 5%,\n          transparent 0 95%,\n          var(--light) 0\n        );\n        background-repeat: no-repeat;\n        background-position: right 0% top 120%;\n      }\n      .pane select:focus {\n        outline: none;\n        background-color: transparent;\n      }\n\n      svg:not(:root) {\n        overflow: visible;\n      }\n      [data-names] ol {\n        display: flex;\n        margin-top: 4rem;\n        flex-wrap: wrap;\n        gap: 2px;\n      }\n\n      [data-names] li {\n        position: relative;\n        width: 10rem;\n        background: var(--light);\n        color: var(--dark);\n      }\n\n      [data-names] li::before {\n        content: \"\";\n        display: block;\n        padding-top: 100%;\n        background: var(--col);\n      }\n\n      .color-names {\n        /*position: relative;\n    z-index: 10;*/\n        padding: 4rem;\n        padding-left: calc(var(--sidebarwidth) + 4rem);\n        color: var(--light);\n      }\n\n      .color-names .section__text {\n        margin-top: 4rem;\n      }\n\n      .color-names__copy {\n        margin-top: 2rem;\n        width: max-content;\n      }\n\n      /* Token Beam Widget Styles */\n      .token-beam-widget {\n        margin-top: 2rem;\n        margin-bottom: 1rem;\n        padding: 0;\n        border: 2px solid var(--light);\n        display: flex;\n        align-items: center;\n        max-width: 400px;\n        background: var(--dark);\n        position: relative;\n      }\n\n      .token-beam-widget__label {\n        position: relative;\n        font-size: 0.65rem;\n        font-weight: 600;\n        text-transform: uppercase;\n        letter-spacing: 0.08em;\n        color: var(--light);\n        white-space: nowrap;\n        padding: 0.6rem 1.5rem 0.6rem 3em;\n        line-height: 1;\n        margin: 0;\n      }\n\n      .token-beam-widget__label::before {\n        content: '⊷';\n        font-size: 2em;\n        transform: translateY(-50%) rotate(45deg);\n        position: absolute;\n        top: 50%;\n        left: 0.5em;\n      }\n\n      .token-beam-widget__token-wrap {\n        position: relative;\n        flex: 1;\n        display: flex;\n        align-items: center;\n      }\n\n      .token-beam-widget__token {\n        flex: 1;\n        padding: 0.6rem;\n        background: var(--dark);\n        border: 2px solid var(--light);\n        border-top: 0;\n        border-bottom: 0;\n        font-size: 0.75rem;\n        letter-spacing: 0.18em;\n        text-align: left;\n        cursor: pointer;\n        font-family: 'Courier New', monospace;\n        color: var(--light);\n        transition: all 0.2s ease;\n      }\n\n      .token-beam-widget__token:hover {\n        background: var(--highlight);\n        color: var(--dark);\n      }\n\n      .token-beam-widget__help {\n        position: relative;\n        display: inline-flex;\n        align-items: center;\n      }\n\n      .token-beam-widget__help-btn {\n        width: 1.95rem;\n        height: 1.95rem;\n        border-radius: 0;\n        background: var(--light);\n        border: 2px solid var(--light);\n        color: var(--dark);\n        font-size: 0.8rem;\n        font-weight: 700;\n        cursor: pointer;\n        display: flex;\n        align-items: center;\n        justify-content: center;\n        transition: all 0.2s ease;\n      }\n\n      .token-beam-widget__help-btn:hover {\n        background: var(--highlight);\n        border-color: var(--highlight);\n      }\n\n      .token-beam-widget__help-dropdown {\n        position: absolute;\n        bottom: calc(100% + 0.5rem);\n        right: 0;\n        background: var(--light);\n        color: var(--dark);\n        padding: 0.8rem;\n        border: 2px solid var(--highlight);\n        min-width: 280px;\n        font-size: 0.7rem;\n        line-height: 1.4;\n        display: none;\n        z-index: 100;\n      }\n\n      .token-beam-widget__help-dropdown::after {\n        content: '';\n        position: absolute;\n        top: 100%;\n        right: 0.5rem;\n        width: 0;\n        height: 0;\n        border-left: 6px solid transparent;\n        border-right: 6px solid transparent;\n        border-top: 6px solid var(--highlight);\n      }\n\n      .token-beam-widget__help.is-open .token-beam-widget__help-dropdown {\n        display: block;\n      }\n\n      .token-beam-widget__help-dropdown a {\n        text-decoration: underline;\n      }\n\n      /* Hide help button when connected */\n      .token-beam-widget--connected .token-beam-widget__help {\n        display: none;\n      }\n\n      .token-beam-widget__unlink {\n        display: inline-flex;\n        align-items: center;\n        gap: 0.3rem;\n        background: var(--light);\n        border: 2px solid var(--light);\n        padding: 0.6rem;\n        font-size: 0.6rem;\n        text-transform: uppercase;\n        letter-spacing: 0.08em;\n        cursor: pointer;\n        color: var(--dark);\n        transition: all 0.2s ease;\n        border-left: 0;\n      }\n\n      .token-beam-widget__dot {\n        width: 8px;\n        height: 8px;\n        border-radius: 50%;\n        background: #1b8a2d;\n        flex-shrink: 0;\n      }\n\n      @keyframes pulse {\n        0%, 100% { opacity: 1; transform: scale(1); }\n        50% { opacity: 0.55; transform: scale(0.85); }\n      }\n\n      .token-beam-widget__dot--active {\n        animation: pulse 2s ease infinite;\n      }\n\n      .token-beam-widget__unlink:hover {\n        background: #c4342d;\n        border-color: #c4342d;\n        color: var(--light);\n      }\n\n      .token-beam-widget__unlink:hover .token-beam-widget__dot {\n        background: var(--light);\n      }\n\n      .token-beam-widget--waiting .token-beam-widget__token {\n        cursor: default;\n      }\n\n      .color-names h2 {\n        color: var(--light);\n      }\n\n      .color-names h3 {\n        margin-top: 4rem;\n      }\n\n      .ellogo {\n        display: block;\n        width: 40%;\n        margin: 4rem 0 2rem;\n      }\n\n      [data-rndsamples] {\n        display: flex;\n        flex-wrap: wrap;\n        gap: 2px;\n        min-height: 100vh;\n        margin-bottom: 4rem;\n      }\n      .palette {\n        position: relative;\n        aspect-ratio: 2;\n        flex: 1 1 20%;\n        background: var(--crnd);\n        display: flex;\n        align-items: center;\n        justify-content: center;\n      }\n      .palette::after {\n        content: \"\";\n        position: absolute;\n        inset: 0;\n        z-index: -1;\n        background: linear-gradient(90deg, var(--gc));\n        transform: translateY(min(1vw, 4px));\n        border-radius: 0.3rem;\n      }\n      .palette:hover {\n        background: linear-gradient(90deg, var(--gs));\n        /*box-shadow: inset 0 0 0 min(0.3vw, 2px) var(--c4), inset 0 0 0 min(0.5vw, 4px) var(--c0);*/\n      }\n      .palette:hover > * {\n        display: none;\n      }\n      .palette:hover::after {\n        background: var(--c4);\n      }\n      .palette__swatch {\n        width: 16%;\n        aspect-ratio: 1;\n        background: var(--color);\n        border-radius: 50%;\n        margin-left: -8%;\n        position: relative;\n        left: 0.5vw;\n        /*transform: rotate(-45deg);*/\n        box-shadow: 0 0 0 min(0.2vw, 2px) var(--c0),\n          0 0 0 min(0.4vw, 4px) var(--color);\n      }\n\n      @media (max-width: 500px) {\n        .sidebar {\n          width: 100%;\n          order: 0;\n          z-index: 2;\n          transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1);\n          transform: translateX(-100%);\n        }\n\n        .color-names {\n          padding-left: 4rem;\n        }\n\n        .sidebar-open .sidebar {\n          transform: translateX(0%);\n        }\n        .sidebar__button {\n          display: block;\n        }\n        .sidebar-open .sidebar__button {\n          transform: translate(calc(-100% - 0.8rem), 1em);\n          --line-color: var(--light);\n          z-index: 3;\n        }\n        .sidebar-open .sidebar__button i {\n          top: 50%;\n          transform: translateY(-50%);\n        }\n        .sidebar-open .sidebar__button i:nth-child(1) {\n          transform: rotate(45deg);\n        }\n\n        .sidebar-open .sidebar__button i:nth-child(3) {\n          transform: rotate(-45deg);\n        }\n        .sidebar-open .sidebar__button i:nth-child(2) {\n          display: none;\n        }\n        .main {\n          transform: translateX(0);\n          margin-left: 0;\n        }\n        .section__fig {\n          margin-right: 0;\n        }\n        .section--vertical .section__text,\n        .section__text {\n          flex: 0 0 100%;\n          order: 1;\n          width: 100%;\n        }\n\n        .palette {\n          aspect-ratio: 1.5;\n          flex: 1 1 40%;\n        }\n\n        .section {\n          flex-direction: column;\n        }\n\n        .section h2 {\n          margin-top: 1em;\n        }\n\n        .section__fig {\n          flex: 0 0 100%;\n        }\n\n        [data-names] li {\n          width: 45%;\n        }\n\n        .color-info__contrast {\n          top: 51%;\n        }\n      }\n    </style>\n  </head>\n\n  <body>\n    <article class=\"app\">\n      <div\n        class=\"sidebar\"\n        id=\"sidebar\"\n        role=\"complementary\"\n        aria-label=\"Settings Sidebar\"\n      >\n        <button\n          class=\"sidebar__button\"\n          aria-expanded=\"true\"\n          aria-controls=\"sidebar\"\n          aria-label=\"Toggle sidebar\"\n          tabindex=\"0\"\n        >\n          <i class=\"line\"></i>\n          <i class=\"line\"></i>\n          <i class=\"line\"></i>\n        </button>\n        <section class=\"settings\" aria-label=\"Color Ramp Settings\">\n          <div class=\"settings__top\">\n            <div class=\"tabs\" role=\"tablist\" aria-label=\"Visualization Tabs\">\n              <div class=\"tabs__contents\" data-tabswrap>\n                <div class=\"tabs__slider\">\n                  <div\n                    class=\"tabs__content\"\n                    data-tab=\"viz-h\"\n                    role=\"tabpanel\"\n                    aria-labelledby=\"tab-viz-h\"\n                  >\n                    <figure data-viz class=\"h-viz\"></figure>\n                  </div>\n                  <div\n                    class=\"tabs__content\"\n                    data-tab=\"viz-sl\"\n                    role=\"tabpanel\"\n                    aria-labelledby=\"tab-viz-sl\"\n                  >\n                    <figure data-slviz class=\"sl-viz\"></figure>\n                  </div>\n                  <div\n                    class=\"tabs__content\"\n                    data-tab=\"fncall\"\n                    role=\"tabpanel\"\n                    aria-labelledby=\"tab-fncall\"\n                  >\n                    <aside class=\"fncall\" aria-label=\"Function call\">\n                      <code class=\"code-sample\">\n                        <pre data-code data-copy></pre>\n                      </code>\n                    </aside>\n                  </div>\n                </div>\n              </div>\n              <div class=\"tabs__controls\">\n                <button\n                  class=\"tabs__control tabs__control--active\"\n                  data-tabtarget=\"viz-h\"\n                  id=\"tab-viz-h\"\n                  role=\"tab\"\n                  aria-selected=\"true\"\n                  aria-controls=\"viz-h\"\n                  tabindex=\"0\"\n                >\n                  <i data-icon=\"hue\" class=\"tabs__icon icon icon--hue\"></i>\n                  <span class=\"tabs__label\"><span>Hue</span></span>\n                </button>\n                <button\n                  class=\"tabs__control\"\n                  data-tabtarget=\"viz-sl\"\n                  id=\"tab-viz-sl\"\n                  role=\"tab\"\n                  aria-selected=\"false\"\n                  aria-controls=\"viz-sl\"\n                  tabindex=\"-1\"\n                >\n                  <i data-icon=\"sl\" class=\"tabs__icon icon icon--sl\"></i>\n                  <span class=\"tabs__label\"\n                    ><span title=\"Saturation &amp; Lightness\">S/L</span></span\n                  >\n                </button>\n                <button\n                  class=\"tabs__control\"\n                  data-tabtarget=\"fncall\"\n                  id=\"tab-fncall\"\n                  role=\"tab\"\n                  aria-selected=\"false\"\n                  aria-controls=\"fncall\"\n                  tabindex=\"-1\"\n                >\n                  <i\n                    data-icon=\"fncall\"\n                    class=\"tabs__icon icon icon--fncall\"\n                  ></i>\n                  <span class=\"tabs__label\"><span>Function</span></span>\n                </button>\n              </div>\n            </div>\n            <div class=\"vizes\"></div>\n          </div>\n          <aside aria-label=\"settings\" class=\"settings__inner\">\n            <!-- h3>Settings</h3 -->\n            <button class=\"button\" data-randomize>Randomize Settings</button>\n            <div class=\"settings__inner\">\n              <div data-pane></div>\n            </div>\n          </aside>\n          <footer>\n            <a href=\"https://www.elastiq.ch/\" hreflang=\"en\" class=\"ellogo\"\n              ><svg xmlns=\"http://www.w3.org/2000/svg\" viewBox=\"0 0 352 185\">\n                <g fill=\"none\" fill-rule=\"evenodd\" transform=\"translate(0 6)\">\n                  <path\n                    class=\"ellogo__font\"\n                    fill-rule=\"nonzero\"\n                    d=\"M179.54 71.84a9 9 0 00-1.91.21 7.74 7.74 0 00-1.83.64 4 4 0 00-1.4 1.15 2.81 2.81 0 001.49 4.38 29.19 29.19 0 007 1.45 17.65 17.65 0 018.93 3.36 9.22 9.22 0 013.4 7.7c0 4.993-1.743 8.907-5.23 11.74s-8.323 4.25-14.51 4.25a21.41 21.41 0 01-8-1.36 17.6 17.6 0 01-5.53-3.4 14.1 14.1 0 01-3.28-4.51 12.64 12.64 0 01-1.19-4.68l10.55-2.55a7.32 7.32 0 002.38 4.81c1.42 1.333 3.577 2 6.47 2a13.24 13.24 0 005.19-.94 3.34 3.34 0 002.21-3.32 3.24 3.24 0 00-1.7-2.85c-1.133-.707-3.233-1.203-6.3-1.49a17.19 17.19 0 01-9.74-3.49 9.73 9.73 0 01-3.62-7.91 13.4 13.4 0 011.49-6.38 14 14 0 014-4.68 17.87 17.87 0 015.74-2.85 23.84 23.84 0 016.85-1 20.07 20.07 0 017.4 1.19 15 15 0 014.85 3 11.8 11.8 0 012.76 3.87 15.47 15.47 0 011.15 3.87l-10.45 2.72a5.27 5.27 0 00-2.13-3.62 8.32 8.32 0 00-5.04-1.31zm39.25 1.68h-11.91V63.33H221l3.91-18.55h10.72l-3.92 18.55h14.63v10.19h-16.83l-4.8 21.89.85.6 11.4-7.83 5.36 8-12.3 8.34a12 12 0 01-6.89 2.21 10.08 10.08 0 01-3.57-.64 8.74 8.74 0 01-5-4.72 9.22 9.22 0 01-.77-3.83 8 8 0 01.08-1.23c.053-.367.137-.863.25-1.49l4.67-21.3zm63.58 31a11.67 11.67 0 01-3.49 1.7 12.69 12.69 0 01-3.49.51 10.08 10.08 0 01-3.57-.64 9.25 9.25 0 01-3-1.79 8 8 0 01-2-2.81 9.16 9.16 0 01-.72-3.7 12.25 12.25 0 01.34-3l5.1-21.36-.85-.6-11.4 7.83-5.36-8 12.34-8.34a11.68 11.68 0 013.49-1.7 12.68 12.68 0 013.49-.51 10.11 10.11 0 013.57.64 9.28 9.28 0 013 1.79 8 8 0 012 2.81 9.18 9.18 0 01.72 3.7 12.32 12.32 0 01-.34 3l-5.11 21.36.85.6 11.4-7.83 5.36 8-12.33 8.34zm7.4-53.69a8 8 0 01-.64 3.19 7.68 7.68 0 01-1.74 2.55 8.57 8.57 0 01-2.59 1.7 7.82 7.82 0 01-3.11.64 7.72 7.72 0 01-3.15-.64 8.69 8.69 0 01-2.55-1.7 7.67 7.67 0 01-1.74-2.55 8.29 8.29 0 010-6.38 7.71 7.71 0 011.74-2.55 8.73 8.73 0 012.55-1.7 7.7 7.7 0 013.15-.64 7.8 7.8 0 013.11.64 8.61 8.61 0 012.59 1.7 7.72 7.72 0 011.74 2.55 8 8 0 01.64 3.18v.01zm42.8 48.58h-1.53a21.9 21.9 0 01-2.13 2.77 13 13 0 01-2.85 2.34 14.44 14.44 0 01-4 1.62 21.53 21.53 0 01-5.36.6 14 14 0 01-10.17-4.25 14.71 14.71 0 01-3.15-4.94 17.39 17.39 0 01-1.15-6.47 37.71 37.71 0 011.57-10.93 28.53 28.53 0 014.64-9.23 23.16 23.16 0 017.49-6.38 21 21 0 0110.12-2.38c3.46 0 6.057.71 7.79 2.13a10.62 10.62 0 013.53 5.19h1.53l1.28-6.13h10.72l-10.47 48.92.85.6 4.76-3.23 5.36 8-5.7 3.74a11.59 11.59 0 01-3.53 1.7 13.14 13.14 0 01-3.46.44 9.35 9.35 0 01-6.51-2.42 8.55 8.55 0 01-2.68-6.68c.017-.946.13-1.887.34-2.81l2.71-12.2zm-10.38-2.89a12.38 12.38 0 005.62-1.28 14.13 14.13 0 004.42-3.45 15.84 15.84 0 002.89-5 17 17 0 001-5.87 8.39 8.39 0 00-2.34-6.34 9 9 0 00-6.51-2.25 12.31 12.31 0 00-5.66 1.32 14 14 0 00-4.42 3.49 16.25 16.25 0 00-2.85 5 17 17 0 00-1 5.87c0 2.78.78 4.893 2.34 6.34a9.21 9.21 0 006.51 2.17z\"\n                  ></path>\n                  <path\n                    class=\"ellogo__logo\"\n                    stroke-linecap=\"round\"\n                    stroke-linejoin=\"round\"\n                    stroke-width=\"11.38\"\n                    d=\"M96.45 70.41c25.89-7.67 59 2.55 80.49-31.66 25.23-40.1 60.94-44.68 85.27-31.62 34.2 18.36 31.67 68.27 7.58 90.7-27.42 25.53-29.58 52.91-13.68 67.24 22.95 20.68 47.1-2.67 35.85-19.93-8.94-13.73-31.93-25.89-98.1 6.9-65.32 32.36-129.62 19-133.91-32.42-1.03-12.53 2.88-39.25 36.5-49.21h0z\"\n                  ></path>\n                  <path\n                    class=\"ellogo__font\"\n                    fill-rule=\"nonzero\"\n                    d=\"M.3 104.52l12.41-58.55h35.31v10.72H21.65l-2.94 13.62h23.06v10.72H16.46l-2.89 13.78h25.14v10.71H.3v-1zm77.08 1.69a12.69 12.69 0 01-3.49.51 10.08 10.08 0 01-3.57-.64 9.25 9.25 0 01-3-1.79 8 8 0 01-2-2.81 9.16 9.16 0 01-.72-3.7 12.93 12.93 0 01.34-3l9.27-38.71-.85-.6-11.4 7.83-5.36-8 12.34-8.34a11.68 11.68 0 013.49-1.7 12.67 12.67 0 013.49-.51 10.09 10.09 0 013.57.64 9.26 9.26 0 013 1.79 8.05 8.05 0 012 2.81 9.17 9.17 0 01.72 3.7 14.62 14.62 0 01-.34 3L75.6 95.4l.85.6 11.4-7.83 5.36 8-12.34 8.35a11.68 11.68 0 01-3.49 1.69zm62.88-10.8l.85.6 4.08-2.89 5.36 8-5 3.4a12.39 12.39 0 01-7 2.21 9.85 9.85 0 01-5.79-1.79 7.85 7.85 0 01-3.23-5H128a15.69 15.69 0 01-1.79 2.68 9.7 9.7 0 01-2.5 2.1 14.05 14.05 0 01-3.49 1.45 17.83 17.83 0 01-4.72.55 14.23 14.23 0 01-6.13-1.32 15.05 15.05 0 01-4.89-3.66 17.4 17.4 0 01-3.28-5.49 19.3 19.3 0 01-1.19-6.89 35.69 35.69 0 011.53-10.63 26.73 26.73 0 014.42-8.64 20.46 20.46 0 0116.59-8 12.9 12.9 0 017.4 1.87 9.08 9.08 0 013.66 4.94h1.53l1.19-5.62h10.72l-6.79 32.13zm-20.55 1.11a11.54 11.54 0 009.4-4.51 15.06 15.06 0 002.42-4.81c.574-1.89.86-3.855.85-5.83a9.27 9.27 0 00-2.3-6.51 7.91 7.91 0 00-6.13-2.51 11.68 11.68 0 00-5.45 1.23 12.16 12.16 0 00-4 3.28 14.54 14.54 0 00-2.47 4.81 19.7 19.7 0 00-.85 5.83 10.06 10.06 0 002.08 6.3c1.38 1.813 3.53 2.72 6.45 2.72z\"\n                  ></path>\n                </g></svg\n            ></a>\n          </footer>\n        </section>\n      </div>\n      <section class=\"main\">\n        <aside class=\"section section--vertical section--first\">\n          <aside class=\"intro\">\n            <h1>RampenSau</h1>\n            <p>\n              RampenSau is a lightweight, dependency-free and blazingly fast\n              color generation library. It makes use of hue cycling and easing\n              functions to generate pleasing color ramps.\n            </p>\n            <a class=\"projectlink\" href=\"https://github.com/meodai/rampensau\"\n              >Github</a\n            >\n          </aside>\n          <div class=\"section__text\">\n            <h2>Generating a Color-Ramp</h2>\n            <p>\n              The illustration above shows the full color ramp generated by the\n              function.\n              <code>hStart</code> [<button\n                data-pantrigger=\"hStart\"\n                data-panvalue=\"0\"\n              >\n                0° (red)</button\n              >,\n              <button data-pantrigger=\"hStart\" data-panvalue=\"45\">\n                45° (yellow)</button\n              >,\n              <button data-pantrigger=\"hStart\" data-panvalue=\"90\">\n                90° (green)</button\n              >,\n              <button data-pantrigger=\"hStart\" data-panvalue=\"180\">\n                180° (teal)\n              </button>\n              etc.] sets the starting hue, at the darkest color in generated\n              ramp.\n            </p>\n            <p>\n              The color set using <code>hStart</code> can be positioned anywhere\n              in the ramp by using <code>hStartCenter</code>.\n              <button data-pantrigger=\"hStartCenter\" data-panvalue=\"0\">\n                0\n              </button>\n              is the start of the ramp,\n              <button data-pantrigger=\"hStartCenter\" data-panvalue=\"1\">\n                1\n              </button>\n              is the end of the ramp. By default <code>hStartCenter</code> is\n              set to\n              <button data-pantrigger=\"hStartCenter\" data-panvalue=\"0.5\">\n                0.5\n              </button>\n              which means the starting hue is in the middle of the ramp.\n            </p>\n            <p>\n              <code>hCycles</code> [<button\n                data-pantrigger=\"hCycles\"\n                data-panvalue=\"0\"\n              >\n                0</button\n              >,\n              <button data-pantrigger=\"hCycles\" data-panvalue=\"0.5\">0.5</button\n              >, <button data-pantrigger=\"hCycles\" data-panvalue=\"1\">1</button>,\n              <button data-pantrigger=\"hCycles\" data-panvalue=\"-0.5\">\n                -0.5</button\n              >] Defines the about of hue variation in the ramp.\n              <code>1</code> is a full cycle around the starting hue,\n              <code>0.5</code> is half a cycle (complementary color),\n              <code>-0.5</code> is half a cycle in the opposite direction.\n            </p>\n          </div>\n          <figure class=\"section__fig\">\n            <div data-colors></div>\n            <figcaption>Full Color Ramp</figcaption>\n          </figure>\n          <button class=\"button button--main\" data-randomize>\n            Randomize Settings\n          </button>\n        </aside>\n\n        <aside class=\"section section--vertical\">\n          <div class=\"section__text\">\n            <h2>Randomly Generated Samples</h2>\n            <p>\n              All samples use the same <code>maxLight</code>,\n              <code>maxLight</code>, <code>maxSaturation</code> and\n              <code>minSaturation</code> values but randomizes the other\n              settings.\n            </p>\n          </div>\n          <div class=\"section__fig\">\n            <div data-rndsamples></div>\n          </div>\n        </aside>\n\n        <aside class=\"section\">\n          <div class=\"section__text\">\n            <h2>Example Use</h2>\n            <p>click & hold to re-generate</p>\n            <p>\n              Each of those squares shows four random entires from the generated\n              colors. In a single square every color is unique.\n            </p>\n          </div>\n          <div class=\"section__fig\">\n            <div data-palette></div>\n          </div>\n        </aside>\n\n        <aside class=\"section\">\n          <div class=\"section__text\">\n            <h2>Color Ramp</h2>\n            <p>All generated colors shown as a continuous gradient.</p>\n          </div>\n          <div class=\"section__fig\">\n            <div data-ramp></div>\n          </div>\n        </aside>\n\n        <aside class=\"section\">\n          <div class=\"section__text\">\n            <h2>HSL Colors</h2>\n            <p>Full list of the generated colors.</p>\n            <p>\n              We used\n              <a title=\"culori\" href=\"https://culorijs.org/\">a library</a> to\n              convert the the HSL colors into different color models. We\n              deliberately choose to only deliver HSL\n              <code>[0…360, 0…1, 0…1]</code> to keep the function fast and\n              lightweight as possible. There are plenty of awesome color\n              libraries if you need to have the colors converted to an other\n              color model.\n            </p>\n          </div>\n          <div class=\"section__fig\">\n            <div data-list></div>\n          </div>\n        </aside>\n\n        <p>\n          fork on\n          <a href=\"https://github.com/meodai/rampensau\">github</a> made by\n          <a href=\"https://www.elastiq.ch/\">elastiq</a>.\n        </p>\n      </section>\n      <aside class=\"color-names\">\n        <h2>For Curious Minds <span>and Desperate Deadlines</span></h2>\n        <div data-names></div>\n        \n        <button class=\"button color-names__copy\" data-copy data-copycolors>\n          Copy Palette to Clipboard\n        </button>\n        \n        <!-- Token Beam Widget -->\n        <div class=\"token-beam-widget token-beam-widget--waiting\" data-token-beam-widget>\n          <h4 class=\"token-beam-widget__label\">Token Beam</h4>\n          <div class=\"token-beam-widget__token-wrap\" data-token-wrap>\n            <button class=\"token-beam-widget__token\" data-token-button>\n              Generating Token...\n            </button>\n          </div>\n          <div class=\"token-beam-widget__help\" data-help>\n            <button class=\"token-beam-widget__help-btn\" data-help-btn title=\"What is this?\">?</button>\n            <div class=\"token-beam-widget__help-dropdown\" data-help-dropdown>\n              Sync your color palette live with design tools like Figma, Sketch, Aseprite & more. Click to generate a token, then paste it in your design tool's plugin.\n              <br><br>\n              <a href=\"https://github.com/meodai/token-beam\" target=\"_blank\">Learn more →</a>\n            </div>\n          </div>\n        </div>\n        \n        <div class=\"section__text\">\n          <p>\n            I got requests to export palettes—so here you go. Still, I’d gently\n            suggest looking into integrating the library or\n            <a href=\"https://meodai.github.io/poline/\">something similar</a>\n            into your workflow.\n          </p>\n          <p>\n            A palette can be more than just colors—it can be a living system.\n            One that responds to different data, brands, themes, or even future\n            you, exploring new directions without starting from scratch.\n          </p>\n          <p>\n            Thinking in systems doesn’t limit creativity—it gives it room to\n            grow. There’s a quiet kind of magic in flexible tools.\n          </p>\n        </div>\n      </aside>\n    </article>\n    <script src=\"https://cdn.jsdelivr.net/npm/culori@4.0.1/bundled/culori.umd.js\"></script>\n\n    <script type=\"module\">\n      import {\n        generateColorRamp,\n        generateColorRampWithCurve,\n        generateColorRampParams,\n        colorUtils,\n        utils,\n      } from \"./index.mjs\";\n\n      const { uniqueRandomHues, colorHarmonies, colorToCSS, harveyHue } =\n        colorUtils;\n\n      const { scaleSpreadArray, shuffleArray } = utils;\n\n      import { rybHsl2rgb } from \"https://esm.sh/rybitten/\";\n\n      console.clear();\n\n      const $favicon = document.querySelector('link[rel=\"icon\"]');\n      const $favCanvas = document.createElement(\"canvas\");\n      const favCtx = $favCanvas.getContext(\"2d\");\n\n      function updateFavicon(colors) {\n        $favCanvas.width = 64;\n        $favCanvas.height = 64;\n        favCtx.clearRect(0, 0, 64, 64);\n        colors.forEach((c, i) => {\n          favCtx.fillStyle = c;\n          favCtx.fillRect(0, (i * 64) / colors.length, 64, 64 / colors.length);\n        });\n        // add a border\n        favCtx.strokeStyle = \"#000\";\n        favCtx.lineWidth = 2;\n        favCtx.strokeRect(2, 2, 60, 60);\n        $favicon.href = $favCanvas.toDataURL();\n      }\n\n      const easingFunctions = {\n        linear: (x) => x,\n        easeInSine: (x) => 1 - Math.cos((x * Math.PI) / 2),\n        easeOutSine: (x) => Math.sin((x * Math.PI) / 2),\n        easeInOutSine: (x) => -(Math.cos(Math.PI * x) - 1) / 2,\n        easeInQuad: (x) => x * x,\n        easeOutQuad: (x) => 1 - (1 - x) * (1 - x),\n        easeInOutQuad: (x) =>\n          x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2,\n        easeInCubic: (x) => x * x * x,\n        easeOutCubic: (x) => 1 - Math.pow(1 - x, 3),\n        easeInOutCubic: (x) =>\n          x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2,\n        easeInQuart: (x) => x * x * x * x,\n        easeOutQuart: (x) => 1 - Math.pow(1 - x, 4),\n        easeInOutQuart: (x) =>\n          x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2,\n        easeInQuint: (x) => x * x * x * x * x,\n        easeOutQuint: (x) => 1 - Math.pow(1 - x, 5),\n        easeInOutQuint: (x) =>\n          x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2,\n        easeInExpo: (x) => (x === 0 ? 0 : Math.pow(2, 10 * x - 10)),\n        easeOutExpo: (x) => (x === 1 ? 1 : 1 - Math.pow(2, -10 * x)),\n        easeInOutExpo: (x) => {\n          if (x === 0) {\n            return 0;\n          }\n          if (x === 1) {\n            return 1;\n          }\n          if (x < 0.5) {\n            return Math.pow(2, 20 * x - 10) / 2;\n          }\n          return (2 - Math.pow(2, -20 * x + 10)) / 2;\n        },\n        easeInCirc: (x) => 1 - Math.sqrt(1 - Math.pow(x, 2)),\n        easeOutCirc: (x) => Math.sqrt(1 - Math.pow(x - 1, 2)),\n        easeInOutCirc: (x) => {\n          if (x < 0.5) {\n            return (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2;\n          }\n          return (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2;\n        },\n        relativeRandom50: (x, fr) => x + (-fr + Math.random() * fr * 2) * 0.5,\n        random: (x) => Math.random(),\n      };\n\n      const adjustmentFunctions = {\n        none: (hsl) => hsl,\n        harveyHue: ([h, s, l]) => [harveyHue(h), s, l],\n        muted: ([h, s, l]) => [h, s * 0.5, l],\n        pastel: ([h, s, l]) => [h, s, 0.2 + l * 0.8],\n        pseudoPrint: ([h, s, l]) => {\n          // if light is above .9 make the hue yellowish\n          if (l > 0.9) {\n            h = 36;\n          }\n          // clamp white to 95\n          l = Math.min(l, 0.92);\n          // make sure black is never too dark\n          l = Math.max(l, 0.11);\n\n          // make sure none of the colors are too saturated\n          s = Math.min(s, 0.7);\n          // when greenish lower saturation more\n          if (h > 80 && h < 160) {\n            s = Math.min(s, 0.5);\n          }\n\n          // if light teal make darker\n          if (h > 172 && h < 195) {\n            l *= 0.8;\n            s += 0.15;\n          }\n\n          return [h, s, l];\n        },\n      };\n\n      const easingFunctionsKeys = Object.keys(easingFunctions);\n      const adjustmentFunctionsKeys = Object.keys(adjustmentFunctions);\n\n      const newOptions = (overwrites = {}) => {\n        const lPow = 0.1 + Math.random() * 0.9;\n        const cPow = 0.2 + Math.random() * 0.2;\n\n        const lRange = [\n          Math.pow(0.05 + Math.random() * 0.2, lPow),\n          Math.pow(1 - Math.random() * 0.15, lPow),\n        ];\n\n        return {\n          ...{\n            total: 5 + Math.floor(Math.random() * 5),\n            hStart: Math.random() * 360,\n            hStartCenter: 0.5,\n            hEasing: easingFunctions[\"linear\"],\n            hCycles: Math.random() * 1.25,\n            sRange: [\n              Math.pow(0.005 + Math.random() * 0.25, cPow),\n              Math.pow(1 - Math.random(), cPow),\n            ],\n            sEasing: easingFunctions[\"easeInOutCubic\"],\n            lRange,\n            lEasing: easingFunctions[\"easeInOutSine\"],\n            transformFn: adjustmentFunctions[\"none\"],\n          },\n          ...overwrites,\n        };\n      };\n\n      const palette = (colors, method) => {\n        let allColors = shuffleArray(colors);\n\n        let localcolors = [...allColors];\n        $pal.innerHTML = \"\";\n\n        $pal.appendChild(paletteDom(localcolors, 1));\n        localcolors = shuffleArray(localcolors);\n\n        for (let i = 0; i < 2; i++) {\n          $pal.appendChild(paletteDom(localcolors, 2));\n          localcolors = shuffleArray(localcolors);\n        }\n\n        for (let i = 0; i < 4 * 2; i++) {\n          $pal.appendChild(paletteDom(localcolors, 4));\n          localcolors = shuffleArray(localcolors);\n        }\n\n        $ramp.style.setProperty(\n          \"background\",\n          `linear-gradient(90deg, ${colors.map((c) => c.css).join(\",\")})`\n        );\n      };\n\n      const roundTo = (num, dec = 2) => {\n        return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);\n      };\n\n      class Pane {\n        constructor($dom, nameSpace = \"pane\", throttleTimer = 50) {\n          this.watches = new Map();\n          this.nameSpace = nameSpace;\n\n          this.$dom = $dom;\n          this.$el = document.createElement(\"div\");\n          this.$el.classList.add(nameSpace);\n          this.$el.addEventListener(\n            \"input\",\n            this._onChange.bind(this),\n            {\n              captures: true,\n              passive: true,\n            },\n            true\n          );\n\n          $dom.appendChild(this.$el);\n\n          this.events = {};\n          this.timer;\n        }\n\n        _callCallbacks(ref, key) {\n          // Added key parameter\n          if (this.events.hasOwnProperty(\"change\")) {\n            clearTimeout(this.timer);\n            this.events[\"change\"].forEach((fn) => {\n              this.timer = setTimeout(() => {\n                // Pass both ref and key to the callback function\n                fn.call(ref || this, { reference: ref, key: key });\n              }, this.throttleTimer);\n            });\n          }\n        }\n\n        _onChange(event) {\n          const $target = event.target;\n\n          if (\"key\" in $target.dataset) {\n            const key = $target.dataset.key;\n            const ref = this.watches.get(key).reference;\n\n            if ($target.dataset.type === \"number\") {\n              ref[key] = parseFloat($target.value);\n            } else {\n              ref[key] = $target.value;\n            }\n\n            if (\"siblingid\" in $target.dataset) {\n              if (Number($target.value) && $target.value !== \"\") {\n                // validate that the values is within the min and max range of the input if present\n                const $sibling = document.getElementById(\n                  $target.dataset.siblingid\n                );\n\n                if ($sibling) {\n                  const min = $sibling.getAttribute(\"min\");\n                  const max = $sibling.getAttribute(\"max\");\n\n                  if (min && Number($sibling.value) < Number(min)) {\n                    $sibling.value = min;\n                  } else if (max && Number($sibling.value) > Number(max)) {\n                    $sibling.value = max;\n                  } else {\n                    $sibling.value = roundTo($target.value, 2);\n                  }\n                }\n              }\n            }\n\n            // Pass the key to the callbacks\n            this._callCallbacks(ref, key);\n          }\n        }\n\n        on(event, fn) {\n          if (!this.events.hasOwnProperty(event)) {\n            this.events[event] = [];\n          }\n          this.events[event].push(fn);\n        }\n\n        addInput(reference, key, options) {\n          const $inputs = this._appendInput(reference, key, options);\n          this.watches.set(key, { reference, $inputs, key });\n        }\n\n        _inputToValue($input, ref, key, value) {\n          if (!$input) {\n            return;\n          }\n\n          if ($input.matches(\"select\")) {\n            Array.from($input.querySelectorAll(\"option\")).forEach(\n              ($input) =>\n                ($input.selected = $input.value === (value || ref[key]))\n            );\n          } else if ($input.matches('[inputmode=\"numeric\"]')) {\n            $input.value = value || roundTo(ref[key], 2);\n          } else {\n            $input.value = value || ref[key];\n          }\n        }\n\n        updateInputs(key, value) {\n          if (key) {\n            const watcher = this.watches.get(key);\n            watcher.$inputs.forEach(($input) =>\n              this._inputToValue($input, watcher.reference, watcher.key, value)\n            );\n          } else {\n            this.watches.forEach((watcher) => {\n              watcher.$inputs.forEach(($input) =>\n                this._inputToValue($input, watcher.reference, watcher.key)\n              );\n            });\n          }\n\n          this._callCallbacks();\n        }\n\n        _appendInput(reference, key, options) {\n          let type = typeof reference[key];\n\n          const $e = document.createElement(\"label\");\n          const $label = document.createElement(\"strong\");\n          const $section = document.createElement(\"div\");\n\n          let $i, $in;\n\n          if (type === \"number\") {\n            $i = document.createElement(\"input\");\n            $in = document.createElement(\"input\");\n\n            $i.setAttribute(\"type\", \"range\");\n            $in.setAttribute(\"type\", \"text\");\n            $in.setAttribute(\"inputmode\", \"numeric\");\n            $in.setAttribute(\"pattern\", \"[0-9]*\");\n\n            // type=\"text\" inputmode=\"numeric\" pattern=\"[0-9]*\"\n\n            if (options.hasOwnProperty(\"min\")) {\n              $i.setAttribute(\"min\", options.min);\n              $in.setAttribute(\"min\", options.min);\n            }\n            if (options.hasOwnProperty(\"max\")) {\n              $i.setAttribute(\"max\", options.max);\n              $in.setAttribute(\"max\", options.max);\n            }\n            if (options.hasOwnProperty(\"step\")) {\n              $i.setAttribute(\"step\", options.step);\n              $in.setAttribute(\"step\", options.step);\n            }\n          } else if (type === \"string\") {\n            if (options.hasOwnProperty(\"options\")) {\n              $i = document.createElement(\"select\");\n\n              options.options.forEach((option) => {\n                const $opt = document.createElement(\"option\");\n                $opt.setAttribute(\"value\", option);\n                if (option === reference[key]) {\n                  $opt.setAttribute(\"selected\", true);\n                }\n                $opt.innerHTML = option;\n                $i.appendChild($opt);\n              });\n            } else {\n              $i = document.createElement(\"input\");\n              $i.setAttribute(\"type\", \"text\");\n            }\n          }\n\n          $i.dataset.key = key;\n          $i.dataset.type = type;\n          $i.value = reference[key];\n          $i.id = `${this.nameSpace}--${key}`;\n          $i.classList.add(\n            `${this.nameSpace}__input`,\n            `${this.nameSpace}__input--${type}`\n          );\n\n          $section.appendChild($i);\n          $section.classList.add(\n            `${this.nameSpace}__inputs`,\n            `${this.nameSpace}__inputs--${type}`\n          );\n\n          if ($in) {\n            $in.dataset.key = key;\n            $in.dataset.type = type;\n            $in.value = roundTo(reference[key], 2);\n            $in.id = `${this.nameSpace}--${key}--value`;\n            $i.classList.add(\n              `${this.nameSpace}__input`,\n              `${this.nameSpace}__input--${type}`\n            );\n            $in.dataset.siblingid = `${this.nameSpace}--${key}`;\n            $i.dataset.siblingid = `${this.nameSpace}--${key}--value`;\n            $section.appendChild($in);\n          }\n\n          $label.innerHTML = key;\n          $label.classList.add(`${this.nameSpace}__label`);\n\n          // make a css compatible version of the key\n          const cssKey = key.replace(/[^a-z0-9]/gi, \"_\").toLowerCase();\n          $e.classList.add(\n            `${this.nameSpace}__section`,\n            `${this.nameSpace}__section--${type}`,\n            `${this.nameSpace}__section--${cssKey}`\n          );\n          $e.appendChild($label);\n          $e.append($section);\n\n          this.$el.appendChild($e);\n\n          return [$i, $in];\n        }\n      }\n\n      const pane = new Pane(document.querySelector(\"[data-pane]\"));\n\n      const PARAMS = {};\n\n      let darkMode = window.matchMedia(\"(prefers-color-scheme: dark)\").matches;\n\n      if (darkMode) {\n        generateColorRampParams.maxLight.default = Math.random() * 0.2;\n        generateColorRampParams.minLight.default = 0.89 + Math.random() * 0.11;\n      }\n\n      const defaultParams = {\n        hueGenerator: {\n          default: \"Default\",\n          props: {\n            options: [\"Default\", \"Unique Random\", \"Color Harmony\"],\n          },\n        },\n        colorHarmony: {\n          default: \"splitComplementary\",\n          props: {\n            options: Object.keys(colorHarmonies).reverse(),\n          },\n        },\n        hMinDiffAngle: {\n          default: 60,\n          props: {\n            min: 15,\n            max: 180,\n            step: 1,\n          },\n        },\n        hueSpread: {\n          default: 0,\n          props: {\n            min: 0,\n            max: 1,\n            step: 0.01,\n          },\n        },\n        ...generateColorRampParams,\n        easingMode: {\n          default: \"Individual Axis\",\n          props: {\n            options: [\"Individual Axis\", \"S/L Curve\"],\n          },\n        },\n        curveMethod: {\n          default: \"lamé\",\n          props: {\n            options: [\"lamé\", \"arc\", \"pow\", \"powX\", \"powY\"],\n          },\n        },\n        curveAccent: {\n          default: 0,\n          props: {\n            min: -0.05,\n            max: 1,\n            step: 0.01,\n          },\n        },\n        hEasing: {\n          default: \"linear\",\n          props: {\n            options: easingFunctionsKeys,\n          },\n        },\n        sEasing: {\n          default: \"easeInOutCubic\",\n          props: {\n            options: easingFunctionsKeys,\n          },\n        },\n        lEasing: {\n          default: \"easeInOutSine\",\n          props: {\n            options: easingFunctionsKeys,\n          },\n        },\n        transformFn: {\n          default: \"none\",\n          props: {\n            options: adjustmentFunctionsKeys,\n          },\n        },\n      };\n\n      Object.keys(defaultParams).forEach((key) => {\n        const param = defaultParams[key];\n        PARAMS[key] = param.default;\n        pane.addInput(PARAMS, key, param.props);\n      });\n\n      PARAMS[\"Color Mode\"] = Math.random() < 0.9 ? \"hsl\" : \"RYBItten\";\n\n      pane.addInput(PARAMS, \"Color Mode\", {\n        options: [\n          \"hsl\",\n          \"okhsl\",\n          \"hsv\",\n          \"RYBItten\",\n          \"okhsv\",\n          \"hsi\",\n          \"hwb\",\n          \"lch\",\n          \"oklch\",\n        ],\n      });\n\n      const createPalette = (hueList = null) => {\n        let colors = generateColorRamp(\n          newOptions({\n            hueList,\n            sRange: [PARAMS.minSaturation, PARAMS.maxSaturation],\n            lRange: [PARAMS.minLight, PARAMS.maxLight],\n            transformFn: adjustmentFunctions[PARAMS.transformFn],\n          })\n        );\n\n        colors = hslToColorObj(colors);\n\n        const cssHSL = colors.map((color) => color.css);\n\n        const $w = document.createElement(\"article\");\n        $w.classList.add(\"palette\");\n        $w.style.setProperty(\"--gc\", cssHSL.join());\n        $w.style.setProperty(\n          \"--gs\",\n          cssHSL\n            .map(\n              (c, i) =>\n                `${c} ${(i / colors.length) * 100}%, ${c} ${\n                  ((i + 1) / colors.length) * 100\n                }%`\n            )\n            .join()\n        );\n        $w.style.setProperty(\n          \"--crnd\",\n          cssHSL[~~(cssHSL.length * Math.random())]\n        );\n\n        cssHSL.forEach((color, i) => {\n          $w.style.setProperty(`--c${i}`, color);\n          const $swatch = document.createElement(\"div\");\n          $swatch.classList.add(\"palette__swatch\");\n          $swatch.style.setProperty(\"--color\", color);\n          $w.appendChild($swatch);\n        });\n\n        return $w;\n      };\n\n      const $pal = document.querySelector(\"[data-palette]\");\n      const $ramp = document.querySelector(\"[data-ramp]\");\n      const $rndsamples = document.querySelector(\"[data-rndsamples]\");\n      const $rnd = document.querySelectorAll(\"[data-randomize]\");\n\n      const np = (huelist = null) => {\n        $rndsamples.innerHTML = \"\";\n        new Array(28).fill(0).forEach((_) => {\n          $rndsamples.appendChild(createPalette(huelist));\n        });\n      };\n\n      np();\n\n      const newPalette = () => {\n        const newParams = newOptions();\n\n        Object.keys(newParams).forEach((key) => {\n          PARAMS[key] = newParams[key];\n        });\n\n        pane.updateInputs();\n      };\n      let timers = [];\n\n      $rnd.forEach(($b) => {\n        $b.addEventListener(\"click\", (e) => {\n          e.preventDefault();\n          if (timers.length) {\n            timers.forEach((t) => clearTimeout(t));\n            timers = [];\n          }\n          newPalette();\n        });\n      });\n\n      let colors = [];\n\n      function paletteDom(colorArr, x = 1) {\n        const $div = document.createElement(\"div\");\n        $div.classList.add(\"palette-sample\");\n        $div.style.setProperty(\"--x\", x);\n        $div.style.setProperty(\"--rnd\", Math.random());\n        $div.innerHTML = \"<b><i></i><i></i></b>\";\n\n        for (let i = 0; i < 4; i++) {\n          $div.style.setProperty(\n            `--col-${i}`,\n            colorArr[i % (colorArr.length - 1)].css\n          );\n        }\n\n        return $div;\n      }\n\n      function hslToColorObj(colors) {\n        const colorMode = PARAMS[\"Color Mode\"];\n        // Mutate colors object\n        return colors.map(([h, s, l]) => {\n          const channels = {};\n          let css;\n          if (colorMode === \"hsl\" || colorMode === \"okhsl\") {\n            channels.h = h;\n            channels.s = s;\n            channels.l = l;\n            if (colorMode === \"hsl\") {\n              css = colorToCSS([h, s, l], \"hsl\");\n            }\n          } else if (colorMode === \"hsv\" || colorMode === \"okhsv\") {\n            channels.h = h;\n            channels.s = s;\n            channels.v = l + (s * Math.min(l, 1 - l)) / 2;\n          } else if (colorMode === \"hsi\") {\n            channels.h = h;\n            channels.s = s;\n            channels.i = l * 2;\n          } else if (colorMode === \"hwb\") {\n            channels.h = h;\n            channels.w = l;\n            channels.b = s;\n          } else if (colorMode === \"oklch\") {\n            channels.l = l;\n            channels.c = s * 0.4;\n            channels.h = h;\n            css = colorToCSS([h, s, l], \"oklch\");\n          } else if (colorMode === \"lch\") {\n            channels.l = l * 100;\n            channels.c = s * 150;\n            channels.h = h;\n            css = colorToCSS([h, s, l], \"lch\");\n          } else if (colorMode === \"RYBItten\") {\n            const [ry, gy, by] = rybHsl2rgb([h, s, l]);\n            channels.r = ry;\n            channels.g = gy;\n            channels.b = by;\n          }\n\n          const computedColorMode =\n            colorMode === \"RYBItten\" ? \"rgb\" : colorMode;\n\n          const { r, g, b } = culori.converter(\"rgb\")({\n            mode: computedColorMode,\n            ...channels,\n          });\n\n          return {\n            hsl: [h, s, l],\n            rgb: [r * 255, g * 255, b * 255],\n            hex: culori.formatHex({ mode: computedColorMode, ...channels }),\n            css:\n              css || culori.formatHex({ mode: computedColorMode, ...channels }),\n            contrast: {\n              white: culori.wcagContrast(\"#ffffff\", {\n                mode: \"hsl\",\n                ...channels,\n              }),\n              black: culori.wcagContrast(\"#000000\", {\n                mode: \"hsl\",\n                ...channels,\n              }),\n            },\n          };\n        });\n      }\n\n      const $viz = document.querySelector(\"[data-viz]\");\n      const $slViz = document.querySelector(\"[data-slviz]\");\n      const $vizMini = document.querySelector(\"[data-icon='hue']\");\n      const $vizMiniSl = document.querySelector(\"[data-icon='sl']\");\n\n      function createDisc(color, i) {\n        const $disc = document.createElement(\"div\");\n        $disc.classList.add(\"disc\");\n        $disc.style.setProperty(\"--c-h\", color.hsl[0]);\n        $disc.style.setProperty(\"--c-s\", color.hsl[1]);\n        $disc.style.setProperty(\"--c-l\", color.hsl[2]);\n        $disc.style.setProperty(\"--c-i\", i);\n        // number going from 0 to 0.5 and back to 0\n        $disc.style.setProperty(\"--c-i2\", i > 0.5 ? 1 - i : i);\n\n        return $disc;\n      }\n\n      function viz(colors) {\n        $viz.innerHTML = \"\";\n        $slViz.innerHTML = \"\";\n        $vizMini.innerHTML = \"\";\n        $vizMiniSl.innerHTML = \"\";\n\n        colors.forEach((color, i) => {\n          $viz.appendChild(createDisc(color, i / colors.length));\n\n          const $slDisc = createDisc(color, i / colors.length);\n          $slDisc.classList.add(\"disc--sl\");\n          $slViz.appendChild($slDisc);\n\n          const $miniDisc = createDisc(color, i / colors.length);\n          $miniDisc.classList.add(\"disc--mini\");\n          $vizMini.appendChild($miniDisc);\n\n          const $miniDiscSl = createDisc(color, i / colors.length);\n          $miniDiscSl.classList.add(\"disc--mini\");\n          $miniDiscSl.classList.add(\"disc--mini-sl\");\n          $vizMiniSl.appendChild($miniDiscSl);\n        });\n      }\n\n      function bam(eventData) {\n        // Accept eventData object\n        const changedKey = eventData ? eventData.key : null; // Extract the key\n\n        // --- TAB SWITCHING LOGIC MOVED HERE ---\n        if (changedKey) {\n          const activeTabButton = document.querySelector(\n            \".tabs__control--active\"\n          );\n          // Check if the 'fncall' tab is currently active\n          if (\n            !activeTabButton ||\n            activeTabButton.dataset.tabtarget !== \"fncall\"\n          ) {\n            const slKeys = [\n              \"minLight\",\n              \"maxLight\",\n              \"minSaturation\",\n              \"maxSaturation\",\n              \"sEasing\",\n              \"lEasing\",\n              \"curveAccent\",\n              \"curveMethod\",\n            ];\n            const hKeys = [\n              \"hueGenerator\",\n              \"hStart\",\n              \"hCycles\",\n              \"hStartCenter\",\n              \"hMinDiffAngle\",\n              \"colorHarmony\",\n            ];\n\n            let targetTabId = null;\n            if (slKeys.includes(changedKey)) {\n              targetTabId = \"viz-sl\";\n            } else if (hKeys.includes(changedKey)) {\n              targetTabId = \"viz-h\";\n            }\n\n            if (targetTabId) {\n              const tabButton = document.querySelector(\n                `.tabs__control[data-tabtarget=\"${targetTabId}\"]`\n              );\n              // Check if the tab isn't already active before clicking to avoid unnecessary actions\n              if (\n                tabButton &&\n                !tabButton.classList.contains(\"tabs__control--active\")\n              ) {\n                tabButton.click(); // Consider calling activateTab directly if possible\n              }\n            }\n          }\n        }\n        // --- END TAB SWITCHING LOGIC ---\n\n        document\n          .querySelector(\".pane__section--hstart\")\n          .classList.remove(\"pane__section--hidden\");\n        document\n          .querySelector(\".pane__section--hstartcenter\")\n          .classList.remove(\"pane__section--hidden\");\n        document\n          .querySelector(\".pane__section--hcycles\")\n          .classList.remove(\"pane__section--hidden\");\n        document\n          .querySelector(\".pane__section--heasing\")\n          .classList.remove(\"pane__section--hidden\");\n        document\n          .querySelector(\".pane__section--colorharmony\")\n          .classList.add(\"pane__section--hidden\");\n        document\n          .querySelector(\".pane__section--hmindiffangle\")\n          .classList.add(\"pane__section--hidden\");\n\n        // Show hueSpread only for 'Unique Random' or 'Color Harmony'\n        if (\n          PARAMS.hueGenerator === \"Unique Random\" ||\n          PARAMS.hueGenerator === \"Color Harmony\"\n        ) {\n          document\n            .querySelector(\".pane__section--huespread\")\n            .classList.remove(\"pane__section--hidden\");\n        } else {\n          document\n            .querySelector(\".pane__section--huespread\")\n            .classList.add(\"pane__section--hidden\");\n        }\n\n        let hueList;\n\n        if (PARAMS.hueGenerator === \"Default\") {\n        } else if (PARAMS.hueGenerator === \"Color Harmony\") {\n          document\n            .querySelector(\".pane__section--hstartcenter\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--hcycles\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--heasing\")\n            .classList.add(\"pane__section--hidden\");\n\n          document\n            .querySelector(\".pane__section--colorharmony\")\n            .classList.remove(\"pane__section--hidden\");\n\n          hueList = colorHarmonies[PARAMS.colorHarmony](PARAMS.hStart);\n        } else if (PARAMS.hueGenerator === \"Unique Random\") {\n          document\n            .querySelector(\".pane__section--hmindiffangle\")\n            .classList.remove(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--hstart\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--hstartcenter\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--hcycles\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--heasing\")\n            .classList.add(\"pane__section--hidden\");\n\n          let totalRandomHues = 4;\n\n          if (PARAMS.total > 4 && PARAMS.total < 8) {\n            totalRandomHues = 3;\n          }\n\n          hueList = uniqueRandomHues({\n            startHue: PARAMS.hStart,\n            total: totalRandomHues,\n          });\n        }\n\n        if (PARAMS.easingMode === \"S/L Curve\") {\n          document\n            .querySelector(\".pane__section--seasing\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--leasing\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--curvemethod\")\n            .classList.remove(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--curveaccent\")\n            .classList.remove(\"pane__section--hidden\");\n        } else {\n          document\n            .querySelector(\".pane__section--seasing\")\n            .classList.remove(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--leasing\")\n            .classList.remove(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--curvemethod\")\n            .classList.add(\"pane__section--hidden\");\n          document\n            .querySelector(\".pane__section--curveaccent\")\n            .classList.add(\"pane__section--hidden\");\n        }\n\n        if (hueList && hueList.length > PARAMS.total) {\n          hueList = hueList.slice(0, PARAMS.total);\n        } else if (hueList && hueList.length < PARAMS.total) {\n          hueList = scaleSpreadArray(hueList, PARAMS.total, PARAMS.hueSpread);\n        }\n\n        const generatorFn =\n          PARAMS.easingMode === \"S/L Curve\"\n            ? generateColorRampWithCurve\n            : generateColorRamp;\n\n        colors = generatorFn({\n          total: PARAMS.total,\n          hStart: PARAMS.hStart,\n          hCycles: PARAMS.hCycles,\n          hStartCenter: PARAMS.hStartCenter,\n          hEasing:\n            typeof PARAMS.hEasing === \"string\"\n              ? easingFunctions[PARAMS.hEasing]\n              : PARAMS.hEasing,\n          sRange: [PARAMS.minSaturation, PARAMS.maxSaturation],\n          sEasing:\n            typeof PARAMS.sEasing === \"string\"\n              ? easingFunctions[PARAMS.sEasing]\n              : PARAMS.sEasing,\n          lRange: [PARAMS.minLight, PARAMS.maxLight],\n          lEasing:\n            typeof PARAMS.lEasing === \"string\"\n              ? easingFunctions[PARAMS.lEasing]\n              : PARAMS.lEasing,\n\n          curveMethod: PARAMS.curveMethod,\n          curveAccent: PARAMS.curveAccent,\n\n          hueList: hueList,\n          transformFn: adjustmentFunctions[PARAMS.transformFn],\n        });\n\n        document.documentElement.style.setProperty(\"--total\", colors.length);\n        document.documentElement.style.setProperty(\"--hStart\", PARAMS.hStart);\n        document.documentElement.style.setProperty(\n          \"--hStartCenter\",\n          PARAMS.hStartCenter\n        );\n        document.documentElement.style.setProperty(\"--hCycles\", PARAMS.hCycles);\n        document.documentElement.style.setProperty(\n          \"--sRangeMin\",\n          PARAMS.minSaturation\n        );\n        document.documentElement.style.setProperty(\n          \"--sRangeMax\",\n          PARAMS.maxSaturation\n        );\n        document.documentElement.style.setProperty(\n          \"--lRangeMin\",\n          PARAMS.minLight\n        );\n        document.documentElement.style.setProperty(\n          \"--lRangeMax\",\n          PARAMS.maxLight\n        );\n\n        const colorMode = PARAMS[\"Color Mode\"];\n        // Mutate colors object\n        colors = hslToColorObj(colors);\n\n        viz(colors);\n\n        let hueParams = `  total: <i>${PARAMS.total}</i>,\n  hStart: <i>${PARAMS.hStart.toFixed(3)}</i>,\n  hStartCenter: <i>${PARAMS.hStartCenter.toFixed(3)}</i>,\n  hEasing: \n    <i>${\n      typeof PARAMS.hEasing === \"string\"\n        ? easingFunctions[PARAMS.hEasing]\n        : PARAMS.hEasing\n    }</i>,\n  hCycles: <i>${PARAMS.hCycles.toFixed(3)}</i>,\n`;\n\n        if (PARAMS.hueGenerator != \"Default\") {\n          if (PARAMS.hueGenerator === \"Color Harmony\") {\n            hueParams = `  hueList: <i>colorHarmonies\n    .${PARAMS.colorHarmony}(${PARAMS.hStart.toFixed(1)})</i>,`;\n          } else if (PARAMS.hueGenerator === \"Unique Random\") {\n            hueParams = `  hueList: <i>uniqueRandomHues({\n    startHue: ${PARAMS.hStart.toFixed(2)},\n    total: ${PARAMS.total > 4 && PARAMS.total < 8 ? 3 : 4},\n    minDistance: ${PARAMS.hMinDiffAngle},\n  })</i>,`;\n          }\n        }\n        const codeString = `${\n          PARAMS.easingMode === \"S/L Curve\"\n            ? \"generateColorRampWithCurve\"\n            : \"generateColorRamp\"\n        }({\n${hueParams}\n  sRange: <i>[${PARAMS.minSaturation.toFixed(\n    3\n  )}, ${PARAMS.maxSaturation.toFixed(3)}]</i>,\n  lRange: <i>[${PARAMS.minLight.toFixed(3)}, ${PARAMS.maxLight.toFixed(3)}]</i>,\n${\n  PARAMS.easingMode === \"S/L Curve\"\n    ? `  curveMethod: <i>${PARAMS.curveMethod}</i>,\n  curveAccent: <i>${PARAMS.curveAccent.toFixed(3)}</i>,`\n    : `\n  sEasing:\n    <i>${\n      typeof PARAMS.sEasing === \"string\"\n        ? easingFunctions[PARAMS.sEasing]\n        : PARAMS.sEasing\n    }</i>,\n  lEasing: \n    <i>${\n      typeof PARAMS.lEasing === \"string\"\n        ? easingFunctions[PARAMS.lEasing]\n        : PARAMS.lEasing\n    }</i>,`\n}\n});\n`;\n        const $code = document.querySelector(\"[data-code]\");\n\n        $code.innerHTML = codeString;\n        $code.dataset.copy = codeString\n          .replace(/<i>/g, \"\")\n          .replace(/<\\/i>/g, \"\");\n\n        document.documentElement.style.setProperty(\n          \"--gradient\",\n          colors.map((c) => c.css).join(\",\")\n        );\n        document.querySelector(\"[data-colors]\").innerHTML = colors.reduce(\n          (r, c, i) => {\n            const relI = i / PARAMS.total;\n            const bestcontrast =\n              c.contrast.white < c.contrast.black ? \"black\" : \"white\";\n            return `${r}<i style=\"--w: ${1 / PARAMS.total}; --color: ${\n              c.css\n            }; --c: ${bestcontrast}; --h: ${c.hsl[0]}; --l: ${\n              c.hsl[2]\n            }; --rnd: ${-1 + Math.random() * 2};  --rnd2: ${\n              -1 + Math.random() * 2\n            }; --i: ${relI};\">\n            <b>\n              ${c.hsl\n                .map((c, i) => Math.round(i ? c * 100 : c) + (i ? \"%\" : \"°\"))\n                .join(\" \")}\n            </b>\n            <span></span>\n          </i>`;\n          },\n          \"\"\n        );\n\n        palette(colors, \"random\");\n        list(colors);\n\n        const $body = document.querySelector(\"body\");\n\n        colors.forEach((c, i) => {\n          document.documentElement.style.setProperty(\n            `--c${i}`,\n            `hsl(${c.hsl[0]}, ${c.hsl[1] * 100}%, ${c.hsl[2] * 100}%)`\n          );\n        });\n\n        document.documentElement.style.setProperty(\n          `--clast`,\n          colors[colors.length - 1].css\n        );\n\n        const remColors = [...colors];\n\n        const bgc = remColors.shift();\n        remColors.pop();\n        const bestContrastTobgc = remColors.sort(\n          (a, b) =>\n            culori.wcagContrast(a.hex, bgc.hex) -\n            culori.wcagContrast(b.hex, bgc.hex)\n        );\n\n        if (bestContrastTobgc.length) {\n          document.documentElement.style.setProperty(\n            `--csecondlast`,\n            bestContrastTobgc.at(-2).css\n          );\n        }\n\n        document.documentElement.style.setProperty(\n          `--ts`,\n          colors.map((c, i) => `${-i * 1}px ${-i * 1}px ${0}px ${c.css}`).join()\n        );\n\n        document.documentElement.style.setProperty(\n          `--tsss`,\n          colors.map((c, i) => `0 ${0}px ${0}px ${c.css}`).join()\n        );\n\n        document.documentElement.style.setProperty(\n          `--tss`,\n          -colors.length * 4 + \"px\"\n        );\n\n        names(colors);\n        updateFavicon(colors.map((c) => c.hex));\n        np(hueList);\n        \n        // Sync palette to Token Beam\n        if (window.tokenBeamSync && typeof window.tokenBeamSync === 'function') {\n          window.tokenBeamSync(colors);\n        }\n      }\n\n      const xmlns = \"http://www.w3.org/2000/svg\";\n\n      const printColors = (arr, simple, names) =>\n        arr\n          .map(({ hsl, rgb, hex, css, contrast }, i) => {\n            if (simple) {\n              return `\n        <li class=\"swatch swatch--simple\" style=\"--col:${hex}; --coltext: ${\n                contrast.black > contrast.white ? \"#202125\" : \"#fff\"\n              }\">\n          <div data-copy title=\"Click to copy\">${Math.floor(\n            hsl[0]\n          )}&deg; ${Math.floor(hsl[1] * 100)}% ${Math.floor(\n                hsl[2] * 100\n              )}%</div>\n        </li>`;\n            } else {\n              return `\n          <li class=\"full\" style=\"--col:${hex}; --coltext: ${\n                contrast.black < contrast.white ? \"#202125\" : \"#fff\"\n              }; --colbg: ${\n                contrast.black > contrast.white ? \"#202125\" : \"#fff\"\n              }\">\n            <div class=\"color-info\">\n              <strong>${names[i].name}</strong>\n              <button data-copy title=\"Click to copy\">hsl(${Math.floor(\n                hsl[0]\n              )},${Math.floor(hsl[1] * 100)}%,${Math.floor(\n                hsl[2] * 100\n              )}%)</button>\n              <button data-copy title=\"Click to copy\">rgb(${Math.floor(\n                rgb[0]\n              )},${Math.floor(rgb[1])},${Math.floor(rgb[2])})</button>\n              <button data-copy title=\"Click to copy\">${css}</button>\n              <div class=\"color-info__contrast\">\n                <h5>wcag contrast:</h5>\n                <em><span style=\"color: #ffffff;\"\">Aa</span> ${contrast.white.toFixed(\n                  2\n                )}</em>\n                <span style=\"color: #000000;\">Aa</span> ${contrast.black.toFixed(\n                  2\n                )}</em>\n              </div>\n            </div>\n          </li>\n        `;\n            }\n          })\n          .join(\"\");\n\n      function list(colors) {\n        document.querySelector(\"[data-list]\").innerHTML = `\n      <div>\n        <h3>Colors</h3>\n        <ol>${printColors(colors, true)}</ol>\n      </div>\n    `;\n      }\n\n      let timer;\n      const $names = document.querySelector(\"[data-names]\");\n      const $copyButton = document.querySelector(\"[data-copycolors]\");\n\n      function names(colors) {\n        $names.innerHTML = `<strong style=\"--col: #fff\">…</strong>`;\n        clearTimeout(timer);\n        timer = setTimeout(() => {\n          const hexes = colors.map((c) => c.hex.replace(\"#\", \"\"));\n          fetch(\n            `https://api.color.pizza/v1/?values=${hexes.join()}&goodnamesonly=true&noduplicates=true`,\n            {\n              method: \"GET\",\n              headers: {\n                \"Content-Type\": \"application/json\",\n                \"X-Referrer\": \"https://meodai.github.io/rampensau/\",\n              },\n            }\n          )\n            .then((r) => r.json())\n            .then((d) => {\n              $names.innerHTML = `<ol>${printColors(\n                colors,\n                false,\n                d.colors\n              )}</ol>`;\n            });\n        }, 1000);\n\n        $copyButton.setAttribute(\n          \"data-copy\",\n          colors.map((c) => c.hex).join(\", \")\n        );\n      }\n\n      $pal.addEventListener(\"pointerdown\", (e) => {\n        palette(colors, \"random\");\n        timer = setInterval(() => palette(colors, \"random\"), 100);\n      });\n      $pal.addEventListener(\"pointerup\", () => clearInterval(timer));\n\n      pane.on(\"change\", bam);\n\n      document.documentElement.addEventListener(\"pointerdown\", (e) => {\n        const $target = e.target;\n        if ($target.matches(`[data-pantrigger]`)) {\n          let value = !isNaN($target.dataset.panvalue)\n            ? new Number($target.dataset.panvalue)\n            : $target.dataset.panvalue;\n          PARAMS[$target.dataset.pantrigger] = value;\n          pane.updateInputs(\n            $target.dataset.pantrigger,\n            $target.dataset.panvalue\n          );\n        }\n      });\n\n      if (navigator.clipboard) {\n        document.querySelector(\"body\").addEventListener(\"click\", (e) => {\n          const $el = e.target;\n          if ($el.matches(\"[data-copy]\")) {\n            let oldText = $el.innerText;\n            let text;\n            // check if data copy has a value\n            if (!$el.dataset.copy) {\n              text = $el.innerText;\n            } else {\n              text = $el.dataset.copy;\n            }\n\n            navigator.clipboard.writeText(text).then(() => {\n              $el.innerText = \"copied!\";\n              setTimeout(() => {\n                $el.innerText = oldText;\n              }, 500);\n            });\n          }\n        });\n      }\n\n      document\n        .querySelector('[aria-controls=\"sidebar\"]')\n        .addEventListener(\"click\", () => {\n          document.documentElement.classList.toggle(\"sidebar-open\");\n        });\n\n      newPalette();\n      bam();\n\n      // tabs logic\n      const $tabs = document.querySelectorAll(\"[data-tab]\");\n      const $tabTriggers = document.querySelectorAll(\"[data-tabtarget]\");\n      const $tabsWrap = document.querySelector(\"[data-tabswrap]\");\n\n      let activeTabIndex = 0;\n\n      $tabsWrap.style.setProperty(\"--tabs-length\", $tabs.length);\n\n      function activateTab($tab) {\n        $tabs.forEach(($t) => {\n          $t.classList.remove(\"tabs__content--active\");\n          $t.setAttribute(\"aria-hidden\", true);\n        });\n        $tabTriggers.forEach(($trigger) => {\n          $trigger.setAttribute(\"aria-selected\", false);\n          $trigger.classList.remove(\"tabs__control--active\");\n        });\n\n        const $trigger = document.querySelector(\n          `[data-tabtarget=\"${$tab.dataset.tab}\"]`\n        );\n        $trigger.setAttribute(\"aria-selected\", true);\n        $trigger.classList.add(\"tabs__control--active\");\n\n        $tab.classList.add(\"tabs__content--active\");\n        $tab.setAttribute(\"aria-hidden\", false);\n        activeTabIndex = [...$tabs].indexOf($tab);\n        $tabsWrap.style.setProperty(\"--active-tab\", activeTabIndex);\n      }\n\n      activateTab($tabs[activeTabIndex]);\n\n      $tabTriggers.forEach(($trigger) => {\n        $trigger.addEventListener(\"click\", (e) => {\n          e.preventDefault();\n          const target = $trigger.dataset.tabtarget;\n          const $target = document.querySelector(`[data-tab=\"${target}\"]`);\n          activateTab($target);\n        });\n      });\n\n      // Token Beam Integration\n      (async function initTokenBeam() {\n        try {\n          const { SourceSession, createCollection } = await window.tokenBeamPromise;\n          \n          const widget = document.querySelector('[data-token-beam-widget]');\n          const tokenButton = document.querySelector('[data-token-button]');\n          const tokenWrap = document.querySelector('[data-token-wrap]');\n          const helpEl = document.querySelector('[data-help]');\n          const helpBtn = document.querySelector('[data-help-btn]');\n          const helpDropdown = document.querySelector('[data-help-dropdown]');\n          \n          let syncClient = null;\n          let sessionToken = null;\n          \n          // Help dropdown toggle\n          helpBtn.addEventListener('click', (e) => {\n            e.stopPropagation();\n            helpEl.classList.toggle('is-open');\n          });\n          \n          document.addEventListener('click', (e) => {\n            if (!helpEl.contains(e.target)) {\n              helpEl.classList.remove('is-open');\n            }\n          });\n          \n          const SYNC_SERVER_URL = 'wss://tokenbeam.dev';\n          \n          // Helper to show/hide unlink button and help\n          function showUnlinkButton() {\n            widget.classList.add('token-beam-widget--connected');\n            const existingBtn = tokenWrap.querySelector('.token-beam-widget__unlink');\n            if (!existingBtn) {\n              const unlinkBtn = document.createElement('button');\n              unlinkBtn.className = 'token-beam-widget__unlink';\n              unlinkBtn.innerHTML = '<span class=\"token-beam-widget__dot token-beam-widget__dot--active\"></span>';\n              unlinkBtn.title = 'Click to disconnect';\n              unlinkBtn.addEventListener('click', handleUnlink);\n              tokenWrap.appendChild(unlinkBtn);\n            }\n          }\n          \n          function hideUnlinkButton() {\n            widget.classList.remove('token-beam-widget--connected');\n            const unlinkBtn = tokenWrap.querySelector('.token-beam-widget__unlink');\n            if (unlinkBtn) unlinkBtn.remove();\n          }\n          \n          // Initialize sync client - it handles token creation automatically\n          function initSyncClient() {\n            if (syncClient) {\n              syncClient.disconnect();\n            }\n            \n            syncClient = new SourceSession({\n              serverUrl: SYNC_SERVER_URL,\n              clientType: 'web',\n              origin: 'RampenSau Color Generator',\n              icon: { type: 'unicode', value: '🎨' },\n            });\n\n            syncClient.on('paired', ({ sessionToken: token }) => {\n              sessionToken = token;\n              widget.classList.remove('token-beam-widget--waiting');\n              tokenButton.textContent = sessionToken;\n              tokenButton.title = 'Click to copy token - waiting for design tool...';\n            });\n\n            syncClient.on('peer-connected', ({ clientType }) => {\n              showUnlinkButton();\n              tokenButton.title = `Connected to ${clientType}`;\n              if (colors && colors.length > 0) {\n                syncPalette(colors);\n              }\n            });\n\n            syncClient.on('peer-disconnected', () => {\n              if (!syncClient.hasPeers()) {\n                hideUnlinkButton();\n                tokenButton.title = 'Design tool disconnected';\n              }\n            });\n\n            syncClient.on('connected', () => {\n              tokenButton.title = 'Connecting to sync server...';\n            });\n\n            syncClient.on('disconnected', () => {\n              hideUnlinkButton();\n              tokenButton.title = 'Disconnected - click unlink to reconnect';\n            });\n\n            syncClient.on('error', ({ message }) => {\n              console.error('Sync error:', message);\n              tokenButton.title = 'Sync error - check console';\n            });\n            \n            syncClient.connect().catch((error) => {\n              console.error('Failed to connect to sync server:', error);\n              tokenButton.textContent = 'Connection failed';\n              tokenButton.title = 'Failed to connect to sync server';\n            });\n          }\n          \n          // Handle token click\n          tokenButton.addEventListener('click', () => {\n            if (!sessionToken) {\n              // Token not yet ready\n              return;\n            }\n            // Copy token to clipboard\n            navigator.clipboard.writeText(sessionToken).then(() => {\n              const oldText = tokenButton.textContent;\n              tokenButton.textContent = 'Copied!';\n              setTimeout(() => {\n                tokenButton.textContent = oldText;\n              }, 500);\n            });\n          });\n          \n          // Handle unlink\n          function handleUnlink() {\n            if (syncClient) {\n              syncClient.disconnect();\n              syncClient = null;\n            }\n            sessionToken = null;\n            \n            // Update UI\n            widget.classList.add('token-beam-widget--waiting');\n            hideUnlinkButton();\n            tokenButton.textContent = 'Generating Token...';\n            tokenButton.title = 'Generating sync token...';\n            \n            // Reinitialize\n            initSyncClient();\n          }\n          \n          // Sync current palette to design tool\n          function syncPalette(colorArray) {\n            if (!syncClient || !syncClient.hasPeers()) return;\n            \n            try {\n              const collection = createCollection('rampensau', \n                colorArray.map((c, i) => ({\n                  name: String(i + 1).padStart(2, '0'),\n                  type: 'color',\n                  value: c.hex,\n                }))\n              );\n              \n              syncClient.sync(collection);\n            } catch (error) {\n              console.error('Failed to sync palette:', error);\n            }\n          }\n          \n          // Expose sync function globally so bam() can call it\n          window.tokenBeamSync = syncPalette;\n          \n          // Auto-initialize on page load\n          initSyncClient();\n          \n        } catch (error) {\n          console.error('Token Beam initialization error:', error);\n          const tokenButton = document.querySelector('[data-token-button]');\n          if (tokenButton) {\n            tokenButton.textContent = 'Token Beam Unavailable';\n            tokenButton.disabled = true;\n            tokenButton.title = 'Failed to load Token Beam library';\n          }\n        }\n      })();\n    </script>\n  </body>\n</html>\n"
  },
  {
    "path": "dist/index.js",
    "content": "\"use strict\";\nvar rampensau = (() => {\n  var __defProp = Object.defineProperty;\n  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;\n  var __getOwnPropNames = Object.getOwnPropertyNames;\n  var __hasOwnProp = Object.prototype.hasOwnProperty;\n  var __export = (target, all) => {\n    for (var name in all)\n      __defProp(target, name, { get: all[name], enumerable: true });\n  };\n  var __copyProps = (to, from, except, desc) => {\n    if (from && typeof from === \"object\" || typeof from === \"function\") {\n      for (let key of __getOwnPropNames(from))\n        if (!__hasOwnProp.call(to, key) && key !== except)\n          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n    }\n    return to;\n  };\n  var __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n  // src/index.ts\n  var index_exports = {};\n  __export(index_exports, {\n    colorUtils: () => colorUtils_exports,\n    generateColorRamp: () => generateColorRamp,\n    generateColorRampParams: () => generateColorRampParams,\n    generateColorRampWithCurve: () => generateColorRampWithCurve,\n    utils: () => utils_exports\n  });\n\n  // src/utils.ts\n  var utils_exports = {};\n  __export(utils_exports, {\n    lerp: () => lerp,\n    makeCurveEasings: () => makeCurveEasings,\n    pointOnCurve: () => pointOnCurve,\n    scaleSpreadArray: () => scaleSpreadArray,\n    shuffleArray: () => shuffleArray\n  });\n  function shuffleArray(array, rndFn = Math.random) {\n    const copy = [...array];\n    let currentIndex = copy.length, randomIndex;\n    while (currentIndex != 0) {\n      randomIndex = Math.floor(rndFn() * currentIndex);\n      currentIndex--;\n      [copy[currentIndex], copy[randomIndex]] = [\n        copy[randomIndex],\n        copy[currentIndex]\n      ];\n    }\n    return copy;\n  }\n  var lerp = (amt, from, to) => from + amt * (to - from);\n  var scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {\n    if (!valuesToFill || valuesToFill.length < 2) {\n      throw new Error(\"valuesToFill array must have at least two values.\");\n    }\n    if (targetSize < 1 && padding > 0) {\n      throw new Error(\"Target size must be at least 1\");\n    }\n    if (targetSize < valuesToFill.length && padding === 0) {\n      throw new Error(\n        \"Target size must be greater than or equal to the valuesToFill array length.\"\n      );\n    }\n    const result = new Array(targetSize);\n    if (padding <= 0) {\n      const len = valuesToFill.length;\n      const lastIdx = len - 1;\n      const totalAdded = targetSize - len;\n      const baseAdds = Math.floor(totalAdded / lastIdx);\n      const remainder = totalAdded % lastIdx;\n      let currentResultIdx = 0;\n      for (let i = 0; i < lastIdx; i++) {\n        const startVal = valuesToFill[i];\n        const endVal = valuesToFill[i + 1];\n        const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);\n        for (let j = 0; j < segmentLen; j++) {\n          const t = j / segmentLen;\n          result[currentResultIdx++] = fillFunction(t, startVal, endVal);\n        }\n      }\n      result[currentResultIdx] = valuesToFill[lastIdx];\n      return result;\n    }\n    const domainStart = padding;\n    const domainEnd = 1 - padding;\n    const lenMinus1 = valuesToFill.length - 1;\n    const normalizedPositions = new Float64Array(valuesToFill.length);\n    for (let i = 0; i < valuesToFill.length; i++) {\n      normalizedPositions[i] = i / lenMinus1;\n    }\n    let segmentIndex = 0;\n    for (let i = 0; i < targetSize; i++) {\n      const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);\n      const adjustedT = domainStart + t * (domainEnd - domainStart);\n      while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {\n        segmentIndex++;\n      }\n      const segmentStart = normalizedPositions[segmentIndex];\n      const segmentEnd = normalizedPositions[segmentIndex + 1];\n      let segmentT = 0;\n      if (segmentEnd > segmentStart) {\n        segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);\n      }\n      const fromValue = valuesToFill[segmentIndex];\n      const toValue = valuesToFill[segmentIndex + 1];\n      result[i] = fillFunction(segmentT, fromValue, toValue);\n    }\n    return result;\n  };\n  var pointOnCurve = (curveMethod, curveAccent) => {\n    return (t) => {\n      const limit = Math.PI / 2;\n      const slice = limit / 1;\n      const percentile = t;\n      let x = 0, y = 0;\n      if (curveMethod === \"lam\\xE9\") {\n        const t2 = percentile * limit;\n        const exp = 2 / (2 + 20 * curveAccent);\n        const cosT = Math.cos(t2);\n        const sinT = Math.sin(t2);\n        x = Math.sign(cosT) * Math.abs(cosT) ** exp;\n        y = Math.sign(sinT) * Math.abs(sinT) ** exp;\n      } else if (curveMethod === \"arc\") {\n        y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);\n        x = Math.sin(Math.PI / 2 + t * slice - curveAccent);\n      } else if (curveMethod === \"pow\") {\n        x = Math.pow(1 - percentile, 1 - curveAccent);\n        y = Math.pow(percentile, 1 - curveAccent);\n      } else if (curveMethod === \"powY\") {\n        x = Math.pow(1 - percentile, curveAccent);\n        y = Math.pow(percentile, 1 - curveAccent);\n      } else if (curveMethod === \"powX\") {\n        x = Math.pow(percentile, curveAccent);\n        y = Math.pow(percentile, 1 - curveAccent);\n      } else if (typeof curveMethod === \"function\") {\n        const [xFunc, yFunc] = curveMethod(t, curveAccent);\n        x = xFunc;\n        y = yFunc;\n      } else {\n        throw new Error(\n          `pointOnCurve() curveMethod parameter is expected to be \"lam\\xE9\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${curveMethod}\\` given.`\n        );\n      }\n      return { x, y };\n    };\n  };\n  var makeCurveEasings = (curveMethod, curveAccent) => {\n    const point = pointOnCurve(curveMethod, curveAccent);\n    return {\n      sEasing: (t) => point(t).x,\n      lEasing: (t) => point(t).y\n    };\n  };\n\n  // src/colorUtils.ts\n  var colorUtils_exports = {};\n  __export(colorUtils_exports, {\n    colorHarmonies: () => colorHarmonies,\n    colorToCSS: () => colorToCSS,\n    harveyHue: () => harveyHue,\n    hsv2hsl: () => hsv2hsl,\n    normalizeHue: () => normalizeHue,\n    uniqueRandomHues: () => uniqueRandomHues\n  });\n  function normalizeHue(h) {\n    return (h % 360 + 360) % 360;\n  }\n  function harveyHue(h) {\n    h = normalizeHue(h) / 360;\n    if (h === 1 || h === 0) return h;\n    h = 1 + h % 1;\n    const seg = 1 / 6;\n    const a = h % seg / seg * Math.PI / 2;\n    const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];\n    const i = Math.floor(h * 6);\n    const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];\n    return cases[i % 6] * 360;\n  }\n  var colorHarmonies = {\n    complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],\n    splitComplementary: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 150),\n      normalizeHue(h - 150)\n    ],\n    triadic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 120),\n      normalizeHue(h + 240)\n    ],\n    tetradic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 90),\n      normalizeHue(h + 180),\n      normalizeHue(h + 270)\n    ],\n    pentadic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 72),\n      normalizeHue(h + 144),\n      normalizeHue(h + 216),\n      normalizeHue(h + 288)\n    ],\n    hexadic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 60),\n      normalizeHue(h + 120),\n      normalizeHue(h + 180),\n      normalizeHue(h + 240),\n      normalizeHue(h + 300)\n    ],\n    monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],\n    // min 2 for RampenSau\n    doubleComplementary: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 180),\n      normalizeHue(h + 30),\n      normalizeHue(h + 210)\n    ],\n    compound: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 180),\n      normalizeHue(h + 60),\n      normalizeHue(h + 240)\n    ],\n    analogous: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 30),\n      normalizeHue(h + 60),\n      normalizeHue(h + 90),\n      normalizeHue(h + 120),\n      normalizeHue(h + 150)\n    ]\n  };\n  function uniqueRandomHues({\n    startHue = 0,\n    total = 9,\n    minHueDiffAngle = 60,\n    rndFn = Math.random\n  } = {}) {\n    minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);\n    const baseHue = startHue ?? rndFn() * 360;\n    const huesToPickFrom = Array.from(\n      {\n        length: Math.round(360 / minHueDiffAngle)\n      },\n      (_, i) => (baseHue + i * minHueDiffAngle) % 360\n    );\n    let randomizedHues = shuffleArray(huesToPickFrom, rndFn);\n    if (randomizedHues.length > total) {\n      randomizedHues = randomizedHues.slice(0, total);\n    }\n    return randomizedHues;\n  }\n  var hsv2hsl = ([h, s, v]) => {\n    const l = v - v * s / 2;\n    const m = Math.min(l, 1 - l);\n    const s_hsl = m === 0 ? 0 : (v - l) / m;\n    return [h, s_hsl, l];\n  };\n  var colorModsCSS = {\n    oklch: (color) => [\n      color[2] * 100 + \"%\",\n      color[1] * 100 + \"%\",\n      color[0]\n    ],\n    lch: (color) => [\n      color[2] * 100 + \"%\",\n      color[1] * 100 + \"%\",\n      color[0]\n    ],\n    hsl: (color) => [\n      color[0],\n      color[1] * 100 + \"%\",\n      color[2] * 100 + \"%\"\n    ],\n    hsv: (color) => {\n      const [h, s, l] = hsv2hsl(color);\n      return [h, s * 100 + \"%\", l * 100 + \"%\"];\n    }\n  };\n  var colorToCSS = (color, mode = \"oklch\") => {\n    const cssMode = mode === \"hsv\" ? \"hsl\" : mode;\n    return `${cssMode}(${colorModsCSS[mode](color).join(\" \")})`;\n  };\n\n  // src/core.ts\n  function generateColorRamp({\n    total = 9,\n    hStart = Math.random() * 360,\n    hStartCenter = 0.5,\n    hEasing = (x) => x,\n    hCycles = 1,\n    sRange = [0.4, 0.35],\n    sEasing = (x) => Math.pow(x, 2),\n    lRange = [Math.random() * 0.1, 0.9],\n    lEasing = (x) => Math.pow(x, 1.5),\n    transformFn = ([h, s, l]) => [h, s, l],\n    hueList\n  } = {}) {\n    const lDiff = lRange[1] - lRange[0];\n    const sDiff = sRange[1] - sRange[0];\n    const length = hueList && hueList.length > 0 ? hueList.length : total;\n    return Array.from({ length }, (_, i) => {\n      const relI = length > 1 ? i / (length - 1) : 0;\n      const fraction = 1 / length;\n      const hue = hueList ? hueList[i] : normalizeHue(\n        hStart + // Add the starting hue\n        (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)\n        // Calculate the hue based on the easing function\n      );\n      const saturation = sRange[0] + sDiff * sEasing(relI, fraction);\n      const lightness = lRange[0] + lDiff * lEasing(relI, fraction);\n      return transformFn([hue, saturation, lightness], i);\n    });\n  }\n  var generateColorRampWithCurve = ({\n    total = 9,\n    hStart = Math.random() * 360,\n    hStartCenter = 0.5,\n    hCycles = 1,\n    sRange = [0.4, 0.35],\n    lRange = [Math.random() * 0.1, 0.9],\n    hueList,\n    curveMethod = \"lam\\xE9\",\n    curveAccent = 0.5,\n    transformFn = ([h, s, l]) => [h, s, l]\n  } = {}) => {\n    const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);\n    return generateColorRamp({\n      total,\n      hStart,\n      hStartCenter,\n      hCycles,\n      sRange,\n      lRange,\n      sEasing,\n      lEasing,\n      transformFn,\n      hueList\n    });\n  };\n\n  // src/index.ts\n  var generateColorRampParams = {\n    total: {\n      default: 5,\n      props: { min: 4, max: 50, step: 1 }\n    },\n    hStart: {\n      default: 0,\n      props: { min: 0, max: 360, step: 0.1 }\n    },\n    hCycles: {\n      default: 1,\n      props: { min: -2, max: 2, step: 1e-3 }\n    },\n    hStartCenter: {\n      default: 0.5,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    minLight: {\n      default: Math.random() * 0.2,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    maxLight: {\n      default: 0.89 + Math.random() * 0.11,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    minSaturation: {\n      default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    maxSaturation: {\n      default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    curveMethod: {\n      default: \"lam\\xE9\",\n      props: { options: [\"lam\\xE9\", \"sine\", \"power\", \"linear\"] }\n    },\n    curveAccent: {\n      default: 0.5,\n      props: { min: 0, max: 5, step: 0.01 }\n    }\n  };\n  return __toCommonJS(index_exports);\n})();\n"
  },
  {
    "path": "dist/index.min.cjs",
    "content": "\"use strict\";var w=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var q=Object.prototype.hasOwnProperty;var T=(e,t)=>{for(var r in t)w(e,r,{get:t[r],enumerable:!0})},B=(e,t,r,o)=>{if(t&&typeof t==\"object\"||typeof t==\"function\")for(let a of j(t))!q.call(e,a)&&a!==r&&w(e,a,{get:()=>t[a],enumerable:!(o=P(t,a))||o.enumerable});return e};var X=e=>B(w({},\"__esModule\",{value:!0}),e);var z={};T(z,{colorUtils:()=>H,generateColorRamp:()=>R,generateColorRampParams:()=>L,generateColorRampWithCurve:()=>G,utils:()=>A});module.exports=X(z);var A={};T(A,{lerp:()=>F,makeCurveEasings:()=>E,pointOnCurve:()=>S,scaleSpreadArray:()=>Y,shuffleArray:()=>V});function V(e,t=Math.random){let r=[...e],o=r.length,a;for(;o!=0;)a=Math.floor(t()*o),o--,[r[o],r[a]]=[r[a],r[o]];return r}var F=(e,t,r)=>t+e*(r-t),Y=(e,t,r=0,o=F)=>{if(!e||e.length<2)throw new Error(\"valuesToFill array must have at least two values.\");if(t<1&&r>0)throw new Error(\"Target size must be at least 1\");if(t<e.length&&r===0)throw new Error(\"Target size must be greater than or equal to the valuesToFill array length.\");let a=new Array(t);if(r<=0){let s=e.length,m=s-1,h=t-s,d=Math.floor(h/m),g=h%m,f=0;for(let b=0;b<m;b++){let x=e[b],C=e[b+1],y=1+d+(b<g?1:0);for(let M=0;M<y;M++){let k=M/y;a[f++]=o(k,x,C)}}return a[f]=e[m],a}let u=r,p=1-r,i=e.length-1,l=new Float64Array(e.length);for(let s=0;s<e.length;s++)l[s]=s/i;let c=0;for(let s=0;s<t;s++){let m=t===1?.5:s/(t-1),h=u+m*(p-u);for(;c<i&&h>l[c+1];)c++;let d=l[c],g=l[c+1],f=0;g>d&&(f=(h-d)/(g-d));let b=e[c],x=e[c+1];a[s]=o(f,b,x)}return a},S=(e,t)=>r=>{let o=Math.PI/2,a=o/1,u=r,p=0,i=0;if(e===\"lam\\xE9\"){let l=u*o,c=2/(2+20*t),s=Math.cos(l),m=Math.sin(l);p=Math.sign(s)*Math.abs(s)**c,i=Math.sign(m)*Math.abs(m)**c}else if(e===\"arc\")i=Math.cos(-Math.PI/2+r*a+t),p=Math.sin(Math.PI/2+r*a-t);else if(e===\"pow\")p=Math.pow(1-u,1-t),i=Math.pow(u,1-t);else if(e===\"powY\")p=Math.pow(1-u,t),i=Math.pow(u,1-t);else if(e===\"powX\")p=Math.pow(u,t),i=Math.pow(u,1-t);else if(typeof e==\"function\"){let[l,c]=e(r,t);p=l,i=c}else throw new Error(`pointOnCurve() curveMethod parameter is expected to be \"lam\\xE9\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${e}\\` given.`);return{x:p,y:i}},E=(e,t)=>{let r=S(e,t);return{sEasing:o=>r(o).x,lEasing:o=>r(o).y}};var H={};T(H,{colorHarmonies:()=>$,colorToCSS:()=>W,harveyHue:()=>_,hsv2hsl:()=>I,normalizeHue:()=>n,uniqueRandomHues:()=>O});function n(e){return(e%360+360)%360}function _(e){if(e=n(e)/360,e===1||e===0)return e;e=1+e%1;let t=1/6,r=e%t/t*Math.PI/2,[o,a]=[t*Math.cos(r),t*Math.sin(r)],u=Math.floor(e*6);return[a,1/3-o,1/3+a,2/3-o,2/3+a,1-o][u%6]*360}var $={complementary:e=>[n(e),n(e+180)],splitComplementary:e=>[n(e),n(e+150),n(e-150)],triadic:e=>[n(e),n(e+120),n(e+240)],tetradic:e=>[n(e),n(e+90),n(e+180),n(e+270)],pentadic:e=>[n(e),n(e+72),n(e+144),n(e+216),n(e+288)],hexadic:e=>[n(e),n(e+60),n(e+120),n(e+180),n(e+240),n(e+300)],monochromatic:e=>[n(e),n(e)],doubleComplementary:e=>[n(e),n(e+180),n(e+30),n(e+210)],compound:e=>[n(e),n(e+180),n(e+60),n(e+240)],analogous:e=>[n(e),n(e+30),n(e+60),n(e+90),n(e+120),n(e+150)]};function O({startHue:e=0,total:t=9,minHueDiffAngle:r=60,rndFn:o=Math.random}={}){r=Math.min(r,360/t);let a=e??o()*360,u=Array.from({length:Math.round(360/r)},(i,l)=>(a+l*r)%360),p=V(u,o);return p.length>t&&(p=p.slice(0,t)),p}var I=([e,t,r])=>{let o=r-r*t/2,a=Math.min(o,1-o),u=a===0?0:(r-o)/a;return[e,u,o]},U={oklch:e=>[e[2]*100+\"%\",e[1]*100+\"%\",e[0]],lch:e=>[e[2]*100+\"%\",e[1]*100+\"%\",e[0]],hsl:e=>[e[0],e[1]*100+\"%\",e[2]*100+\"%\"],hsv:e=>{let[t,r,o]=I(e);return[t,r*100+\"%\",o*100+\"%\"]}},W=(e,t=\"oklch\")=>`${t===\"hsv\"?\"hsl\":t}(${U[t](e).join(\" \")})`;function R({total:e=9,hStart:t=Math.random()*360,hStartCenter:r=.5,hEasing:o=m=>m,hCycles:a=1,sRange:u=[.4,.35],sEasing:p=m=>Math.pow(m,2),lRange:i=[Math.random()*.1,.9],lEasing:l=m=>Math.pow(m,1.5),transformFn:c=([m,h,d])=>[m,h,d],hueList:s}={}){let m=i[1]-i[0],h=u[1]-u[0],d=s&&s.length>0?s.length:e;return Array.from({length:d},(g,f)=>{let b=d>1?f/(d-1):0,x=1/d,C=s?s[f]:n(t+(1-o(b,x)-r)*(360*a)),y=u[0]+h*p(b,x),M=i[0]+m*l(b,x);return c([C,y,M],f)})}var G=({total:e=9,hStart:t=Math.random()*360,hStartCenter:r=.5,hCycles:o=1,sRange:a=[.4,.35],lRange:u=[Math.random()*.1,.9],hueList:p,curveMethod:i=\"lam\\xE9\",curveAccent:l=.5,transformFn:c=([s,m,h])=>[s,m,h]}={})=>{let{sEasing:s,lEasing:m}=E(i,l);return R({total:e,hStart:t,hStartCenter:r,hCycles:o,sRange:a,lRange:u,sEasing:s,lEasing:m,transformFn:c,hueList:p})};var L={total:{default:5,props:{min:4,max:50,step:1}},hStart:{default:0,props:{min:0,max:360,step:.1}},hCycles:{default:1,props:{min:-2,max:2,step:.001}},hStartCenter:{default:.5,props:{min:0,max:1,step:.001}},minLight:{default:Math.random()*.2,props:{min:0,max:1,step:.001}},maxLight:{default:.89+Math.random()*.11,props:{min:0,max:1,step:.001}},minSaturation:{default:Math.random()<.5?.4:.8+Math.random()*.2,props:{min:0,max:1,step:.001}},maxSaturation:{default:Math.random()<.5?.35:.9+Math.random()*.1,props:{min:0,max:1,step:.001}},curveMethod:{default:\"lam\\xE9\",props:{options:[\"lam\\xE9\",\"sine\",\"power\",\"linear\"]}},curveAccent:{default:.5,props:{min:0,max:5,step:.01}}};\n"
  },
  {
    "path": "dist/index.min.mjs",
    "content": "var G=Object.defineProperty;var A=(e,t)=>{for(var r in t)G(e,r,{get:t[r],enumerable:!0})};var V={};A(V,{lerp:()=>R,makeCurveEasings:()=>T,pointOnCurve:()=>F,scaleSpreadArray:()=>k,shuffleArray:()=>w});function w(e,t=Math.random){let r=[...e],o=r.length,a;for(;o!=0;)a=Math.floor(t()*o),o--,[r[o],r[a]]=[r[a],r[o]];return r}var R=(e,t,r)=>t+e*(r-t),k=(e,t,r=0,o=R)=>{if(!e||e.length<2)throw new Error(\"valuesToFill array must have at least two values.\");if(t<1&&r>0)throw new Error(\"Target size must be at least 1\");if(t<e.length&&r===0)throw new Error(\"Target size must be greater than or equal to the valuesToFill array length.\");let a=new Array(t);if(r<=0){let s=e.length,m=s-1,h=t-s,d=Math.floor(h/m),g=h%m,f=0;for(let b=0;b<m;b++){let x=e[b],C=e[b+1],y=1+d+(b<g?1:0);for(let M=0;M<y;M++){let I=M/y;a[f++]=o(I,x,C)}}return a[f]=e[m],a}let u=r,p=1-r,i=e.length-1,l=new Float64Array(e.length);for(let s=0;s<e.length;s++)l[s]=s/i;let c=0;for(let s=0;s<t;s++){let m=t===1?.5:s/(t-1),h=u+m*(p-u);for(;c<i&&h>l[c+1];)c++;let d=l[c],g=l[c+1],f=0;g>d&&(f=(h-d)/(g-d));let b=e[c],x=e[c+1];a[s]=o(f,b,x)}return a},F=(e,t)=>r=>{let o=Math.PI/2,a=o/1,u=r,p=0,i=0;if(e===\"lam\\xE9\"){let l=u*o,c=2/(2+20*t),s=Math.cos(l),m=Math.sin(l);p=Math.sign(s)*Math.abs(s)**c,i=Math.sign(m)*Math.abs(m)**c}else if(e===\"arc\")i=Math.cos(-Math.PI/2+r*a+t),p=Math.sin(Math.PI/2+r*a-t);else if(e===\"pow\")p=Math.pow(1-u,1-t),i=Math.pow(u,1-t);else if(e===\"powY\")p=Math.pow(1-u,t),i=Math.pow(u,1-t);else if(e===\"powX\")p=Math.pow(u,t),i=Math.pow(u,1-t);else if(typeof e==\"function\"){let[l,c]=e(r,t);p=l,i=c}else throw new Error(`pointOnCurve() curveMethod parameter is expected to be \"lam\\xE9\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${e}\\` given.`);return{x:p,y:i}},T=(e,t)=>{let r=F(e,t);return{sEasing:o=>r(o).x,lEasing:o=>r(o).y}};var E={};A(E,{colorHarmonies:()=>j,colorToCSS:()=>X,harveyHue:()=>P,hsv2hsl:()=>H,normalizeHue:()=>n,uniqueRandomHues:()=>q});function n(e){return(e%360+360)%360}function P(e){if(e=n(e)/360,e===1||e===0)return e;e=1+e%1;let t=1/6,r=e%t/t*Math.PI/2,[o,a]=[t*Math.cos(r),t*Math.sin(r)],u=Math.floor(e*6);return[a,1/3-o,1/3+a,2/3-o,2/3+a,1-o][u%6]*360}var j={complementary:e=>[n(e),n(e+180)],splitComplementary:e=>[n(e),n(e+150),n(e-150)],triadic:e=>[n(e),n(e+120),n(e+240)],tetradic:e=>[n(e),n(e+90),n(e+180),n(e+270)],pentadic:e=>[n(e),n(e+72),n(e+144),n(e+216),n(e+288)],hexadic:e=>[n(e),n(e+60),n(e+120),n(e+180),n(e+240),n(e+300)],monochromatic:e=>[n(e),n(e)],doubleComplementary:e=>[n(e),n(e+180),n(e+30),n(e+210)],compound:e=>[n(e),n(e+180),n(e+60),n(e+240)],analogous:e=>[n(e),n(e+30),n(e+60),n(e+90),n(e+120),n(e+150)]};function q({startHue:e=0,total:t=9,minHueDiffAngle:r=60,rndFn:o=Math.random}={}){r=Math.min(r,360/t);let a=e!=null?e:o()*360,u=Array.from({length:Math.round(360/r)},(i,l)=>(a+l*r)%360),p=w(u,o);return p.length>t&&(p=p.slice(0,t)),p}var H=([e,t,r])=>{let o=r-r*t/2,a=Math.min(o,1-o),u=a===0?0:(r-o)/a;return[e,u,o]},B={oklch:e=>[e[2]*100+\"%\",e[1]*100+\"%\",e[0]],lch:e=>[e[2]*100+\"%\",e[1]*100+\"%\",e[0]],hsl:e=>[e[0],e[1]*100+\"%\",e[2]*100+\"%\"],hsv:e=>{let[t,r,o]=H(e);return[t,r*100+\"%\",o*100+\"%\"]}},X=(e,t=\"oklch\")=>`${t===\"hsv\"?\"hsl\":t}(${B[t](e).join(\" \")})`;function S({total:e=9,hStart:t=Math.random()*360,hStartCenter:r=.5,hEasing:o=m=>m,hCycles:a=1,sRange:u=[.4,.35],sEasing:p=m=>Math.pow(m,2),lRange:i=[Math.random()*.1,.9],lEasing:l=m=>Math.pow(m,1.5),transformFn:c=([m,h,d])=>[m,h,d],hueList:s}={}){let m=i[1]-i[0],h=u[1]-u[0],d=s&&s.length>0?s.length:e;return Array.from({length:d},(g,f)=>{let b=d>1?f/(d-1):0,x=1/d,C=s?s[f]:n(t+(1-o(b,x)-r)*(360*a)),y=u[0]+h*p(b,x),M=i[0]+m*l(b,x);return c([C,y,M],f)})}var Y=({total:e=9,hStart:t=Math.random()*360,hStartCenter:r=.5,hCycles:o=1,sRange:a=[.4,.35],lRange:u=[Math.random()*.1,.9],hueList:p,curveMethod:i=\"lam\\xE9\",curveAccent:l=.5,transformFn:c=([s,m,h])=>[s,m,h]}={})=>{let{sEasing:s,lEasing:m}=T(i,l);return S({total:e,hStart:t,hStartCenter:r,hCycles:o,sRange:a,lRange:u,sEasing:s,lEasing:m,transformFn:c,hueList:p})};var L={total:{default:5,props:{min:4,max:50,step:1}},hStart:{default:0,props:{min:0,max:360,step:.1}},hCycles:{default:1,props:{min:-2,max:2,step:.001}},hStartCenter:{default:.5,props:{min:0,max:1,step:.001}},minLight:{default:Math.random()*.2,props:{min:0,max:1,step:.001}},maxLight:{default:.89+Math.random()*.11,props:{min:0,max:1,step:.001}},minSaturation:{default:Math.random()<.5?.4:.8+Math.random()*.2,props:{min:0,max:1,step:.001}},maxSaturation:{default:Math.random()<.5?.35:.9+Math.random()*.1,props:{min:0,max:1,step:.001}},curveMethod:{default:\"lam\\xE9\",props:{options:[\"lam\\xE9\",\"sine\",\"power\",\"linear\"]}},curveAccent:{default:.5,props:{min:0,max:5,step:.01}}};export{E as colorUtils,S as generateColorRamp,L as generateColorRampParams,Y as generateColorRampWithCurve,V as utils};\n"
  },
  {
    "path": "dist/index.mjs",
    "content": "var __defProp = Object.defineProperty;\nvar __export = (target, all) => {\n  for (var name in all)\n    __defProp(target, name, { get: all[name], enumerable: true });\n};\n\n// src/utils.ts\nvar utils_exports = {};\n__export(utils_exports, {\n  lerp: () => lerp,\n  makeCurveEasings: () => makeCurveEasings,\n  pointOnCurve: () => pointOnCurve,\n  scaleSpreadArray: () => scaleSpreadArray,\n  shuffleArray: () => shuffleArray\n});\nfunction shuffleArray(array, rndFn = Math.random) {\n  const copy = [...array];\n  let currentIndex = copy.length, randomIndex;\n  while (currentIndex != 0) {\n    randomIndex = Math.floor(rndFn() * currentIndex);\n    currentIndex--;\n    [copy[currentIndex], copy[randomIndex]] = [\n      copy[randomIndex],\n      copy[currentIndex]\n    ];\n  }\n  return copy;\n}\nvar lerp = (amt, from, to) => from + amt * (to - from);\nvar scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {\n  if (!valuesToFill || valuesToFill.length < 2) {\n    throw new Error(\"valuesToFill array must have at least two values.\");\n  }\n  if (targetSize < 1 && padding > 0) {\n    throw new Error(\"Target size must be at least 1\");\n  }\n  if (targetSize < valuesToFill.length && padding === 0) {\n    throw new Error(\n      \"Target size must be greater than or equal to the valuesToFill array length.\"\n    );\n  }\n  const result = new Array(targetSize);\n  if (padding <= 0) {\n    const len = valuesToFill.length;\n    const lastIdx = len - 1;\n    const totalAdded = targetSize - len;\n    const baseAdds = Math.floor(totalAdded / lastIdx);\n    const remainder = totalAdded % lastIdx;\n    let currentResultIdx = 0;\n    for (let i = 0; i < lastIdx; i++) {\n      const startVal = valuesToFill[i];\n      const endVal = valuesToFill[i + 1];\n      const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);\n      for (let j = 0; j < segmentLen; j++) {\n        const t = j / segmentLen;\n        result[currentResultIdx++] = fillFunction(t, startVal, endVal);\n      }\n    }\n    result[currentResultIdx] = valuesToFill[lastIdx];\n    return result;\n  }\n  const domainStart = padding;\n  const domainEnd = 1 - padding;\n  const lenMinus1 = valuesToFill.length - 1;\n  const normalizedPositions = new Float64Array(valuesToFill.length);\n  for (let i = 0; i < valuesToFill.length; i++) {\n    normalizedPositions[i] = i / lenMinus1;\n  }\n  let segmentIndex = 0;\n  for (let i = 0; i < targetSize; i++) {\n    const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);\n    const adjustedT = domainStart + t * (domainEnd - domainStart);\n    while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {\n      segmentIndex++;\n    }\n    const segmentStart = normalizedPositions[segmentIndex];\n    const segmentEnd = normalizedPositions[segmentIndex + 1];\n    let segmentT = 0;\n    if (segmentEnd > segmentStart) {\n      segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);\n    }\n    const fromValue = valuesToFill[segmentIndex];\n    const toValue = valuesToFill[segmentIndex + 1];\n    result[i] = fillFunction(segmentT, fromValue, toValue);\n  }\n  return result;\n};\nvar pointOnCurve = (curveMethod, curveAccent) => {\n  return (t) => {\n    const limit = Math.PI / 2;\n    const slice = limit / 1;\n    const percentile = t;\n    let x = 0, y = 0;\n    if (curveMethod === \"lam\\xE9\") {\n      const t2 = percentile * limit;\n      const exp = 2 / (2 + 20 * curveAccent);\n      const cosT = Math.cos(t2);\n      const sinT = Math.sin(t2);\n      x = Math.sign(cosT) * Math.abs(cosT) ** exp;\n      y = Math.sign(sinT) * Math.abs(sinT) ** exp;\n    } else if (curveMethod === \"arc\") {\n      y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);\n      x = Math.sin(Math.PI / 2 + t * slice - curveAccent);\n    } else if (curveMethod === \"pow\") {\n      x = Math.pow(1 - percentile, 1 - curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (curveMethod === \"powY\") {\n      x = Math.pow(1 - percentile, curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (curveMethod === \"powX\") {\n      x = Math.pow(percentile, curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (typeof curveMethod === \"function\") {\n      const [xFunc, yFunc] = curveMethod(t, curveAccent);\n      x = xFunc;\n      y = yFunc;\n    } else {\n      throw new Error(\n        `pointOnCurve() curveMethod parameter is expected to be \"lam\\xE9\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${curveMethod}\\` given.`\n      );\n    }\n    return { x, y };\n  };\n};\nvar makeCurveEasings = (curveMethod, curveAccent) => {\n  const point = pointOnCurve(curveMethod, curveAccent);\n  return {\n    sEasing: (t) => point(t).x,\n    lEasing: (t) => point(t).y\n  };\n};\n\n// src/colorUtils.ts\nvar colorUtils_exports = {};\n__export(colorUtils_exports, {\n  colorHarmonies: () => colorHarmonies,\n  colorToCSS: () => colorToCSS,\n  harveyHue: () => harveyHue,\n  hsv2hsl: () => hsv2hsl,\n  normalizeHue: () => normalizeHue,\n  uniqueRandomHues: () => uniqueRandomHues\n});\nfunction normalizeHue(h) {\n  return (h % 360 + 360) % 360;\n}\nfunction harveyHue(h) {\n  h = normalizeHue(h) / 360;\n  if (h === 1 || h === 0) return h;\n  h = 1 + h % 1;\n  const seg = 1 / 6;\n  const a = h % seg / seg * Math.PI / 2;\n  const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];\n  const i = Math.floor(h * 6);\n  const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];\n  return cases[i % 6] * 360;\n}\nvar colorHarmonies = {\n  complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],\n  splitComplementary: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 150),\n    normalizeHue(h - 150)\n  ],\n  triadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 120),\n    normalizeHue(h + 240)\n  ],\n  tetradic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 90),\n    normalizeHue(h + 180),\n    normalizeHue(h + 270)\n  ],\n  pentadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 72),\n    normalizeHue(h + 144),\n    normalizeHue(h + 216),\n    normalizeHue(h + 288)\n  ],\n  hexadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 60),\n    normalizeHue(h + 120),\n    normalizeHue(h + 180),\n    normalizeHue(h + 240),\n    normalizeHue(h + 300)\n  ],\n  monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],\n  // min 2 for RampenSau\n  doubleComplementary: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 180),\n    normalizeHue(h + 30),\n    normalizeHue(h + 210)\n  ],\n  compound: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 180),\n    normalizeHue(h + 60),\n    normalizeHue(h + 240)\n  ],\n  analogous: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 30),\n    normalizeHue(h + 60),\n    normalizeHue(h + 90),\n    normalizeHue(h + 120),\n    normalizeHue(h + 150)\n  ]\n};\nfunction uniqueRandomHues({\n  startHue = 0,\n  total = 9,\n  minHueDiffAngle = 60,\n  rndFn = Math.random\n} = {}) {\n  minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);\n  const baseHue = startHue != null ? startHue : rndFn() * 360;\n  const huesToPickFrom = Array.from(\n    {\n      length: Math.round(360 / minHueDiffAngle)\n    },\n    (_, i) => (baseHue + i * minHueDiffAngle) % 360\n  );\n  let randomizedHues = shuffleArray(huesToPickFrom, rndFn);\n  if (randomizedHues.length > total) {\n    randomizedHues = randomizedHues.slice(0, total);\n  }\n  return randomizedHues;\n}\nvar hsv2hsl = ([h, s, v]) => {\n  const l = v - v * s / 2;\n  const m = Math.min(l, 1 - l);\n  const s_hsl = m === 0 ? 0 : (v - l) / m;\n  return [h, s_hsl, l];\n};\nvar colorModsCSS = {\n  oklch: (color) => [\n    color[2] * 100 + \"%\",\n    color[1] * 100 + \"%\",\n    color[0]\n  ],\n  lch: (color) => [\n    color[2] * 100 + \"%\",\n    color[1] * 100 + \"%\",\n    color[0]\n  ],\n  hsl: (color) => [\n    color[0],\n    color[1] * 100 + \"%\",\n    color[2] * 100 + \"%\"\n  ],\n  hsv: (color) => {\n    const [h, s, l] = hsv2hsl(color);\n    return [h, s * 100 + \"%\", l * 100 + \"%\"];\n  }\n};\nvar colorToCSS = (color, mode = \"oklch\") => {\n  const cssMode = mode === \"hsv\" ? \"hsl\" : mode;\n  return `${cssMode}(${colorModsCSS[mode](color).join(\" \")})`;\n};\n\n// src/core.ts\nfunction generateColorRamp({\n  total = 9,\n  hStart = Math.random() * 360,\n  hStartCenter = 0.5,\n  hEasing = (x) => x,\n  hCycles = 1,\n  sRange = [0.4, 0.35],\n  sEasing = (x) => Math.pow(x, 2),\n  lRange = [Math.random() * 0.1, 0.9],\n  lEasing = (x) => Math.pow(x, 1.5),\n  transformFn = ([h, s, l]) => [h, s, l],\n  hueList\n} = {}) {\n  const lDiff = lRange[1] - lRange[0];\n  const sDiff = sRange[1] - sRange[0];\n  const length = hueList && hueList.length > 0 ? hueList.length : total;\n  return Array.from({ length }, (_, i) => {\n    const relI = length > 1 ? i / (length - 1) : 0;\n    const fraction = 1 / length;\n    const hue = hueList ? hueList[i] : normalizeHue(\n      hStart + // Add the starting hue\n      (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)\n      // Calculate the hue based on the easing function\n    );\n    const saturation = sRange[0] + sDiff * sEasing(relI, fraction);\n    const lightness = lRange[0] + lDiff * lEasing(relI, fraction);\n    return transformFn([hue, saturation, lightness], i);\n  });\n}\nvar generateColorRampWithCurve = ({\n  total = 9,\n  hStart = Math.random() * 360,\n  hStartCenter = 0.5,\n  hCycles = 1,\n  sRange = [0.4, 0.35],\n  lRange = [Math.random() * 0.1, 0.9],\n  hueList,\n  curveMethod = \"lam\\xE9\",\n  curveAccent = 0.5,\n  transformFn = ([h, s, l]) => [h, s, l]\n} = {}) => {\n  const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);\n  return generateColorRamp({\n    total,\n    hStart,\n    hStartCenter,\n    hCycles,\n    sRange,\n    lRange,\n    sEasing,\n    lEasing,\n    transformFn,\n    hueList\n  });\n};\n\n// src/index.ts\nvar generateColorRampParams = {\n  total: {\n    default: 5,\n    props: { min: 4, max: 50, step: 1 }\n  },\n  hStart: {\n    default: 0,\n    props: { min: 0, max: 360, step: 0.1 }\n  },\n  hCycles: {\n    default: 1,\n    props: { min: -2, max: 2, step: 1e-3 }\n  },\n  hStartCenter: {\n    default: 0.5,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  minLight: {\n    default: Math.random() * 0.2,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  maxLight: {\n    default: 0.89 + Math.random() * 0.11,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  minSaturation: {\n    default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  maxSaturation: {\n    default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,\n    props: { min: 0, max: 1, step: 1e-3 }\n  },\n  curveMethod: {\n    default: \"lam\\xE9\",\n    props: { options: [\"lam\\xE9\", \"sine\", \"power\", \"linear\"] }\n  },\n  curveAccent: {\n    default: 0.5,\n    props: { min: 0, max: 5, step: 0.01 }\n  }\n};\nexport {\n  colorUtils_exports as colorUtils,\n  generateColorRamp,\n  generateColorRampParams,\n  generateColorRampWithCurve,\n  utils_exports as utils\n};\n"
  },
  {
    "path": "dist/index.umd.js",
    "content": "(function(root, factory) {\n      if (typeof define === 'function' && define.amd) {\n      \tdefine([], factory);\n      } else if (typeof module === 'object' && module.exports) {\n      \tmodule.exports = factory();\n      } else {\n      \troot.rampensau = factory();\n      }\n    }\n    (typeof self !== 'undefined' ? self : this, function() {\n\"use strict\";\nvar rampensau = (() => {\n  var __defProp = Object.defineProperty;\n  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;\n  var __getOwnPropNames = Object.getOwnPropertyNames;\n  var __hasOwnProp = Object.prototype.hasOwnProperty;\n  var __pow = Math.pow;\n  var __export = (target, all) => {\n    for (var name in all)\n      __defProp(target, name, { get: all[name], enumerable: true });\n  };\n  var __copyProps = (to, from, except, desc) => {\n    if (from && typeof from === \"object\" || typeof from === \"function\") {\n      for (let key of __getOwnPropNames(from))\n        if (!__hasOwnProp.call(to, key) && key !== except)\n          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });\n    }\n    return to;\n  };\n  var __toCommonJS = (mod) => __copyProps(__defProp({}, \"__esModule\", { value: true }), mod);\n\n  // src/index.ts\n  var index_exports = {};\n  __export(index_exports, {\n    colorUtils: () => colorUtils_exports,\n    generateColorRamp: () => generateColorRamp,\n    generateColorRampParams: () => generateColorRampParams,\n    generateColorRampWithCurve: () => generateColorRampWithCurve,\n    utils: () => utils_exports\n  });\n\n  // src/utils.ts\n  var utils_exports = {};\n  __export(utils_exports, {\n    lerp: () => lerp,\n    makeCurveEasings: () => makeCurveEasings,\n    pointOnCurve: () => pointOnCurve,\n    scaleSpreadArray: () => scaleSpreadArray,\n    shuffleArray: () => shuffleArray\n  });\n  function shuffleArray(array, rndFn = Math.random) {\n    const copy = [...array];\n    let currentIndex = copy.length, randomIndex;\n    while (currentIndex != 0) {\n      randomIndex = Math.floor(rndFn() * currentIndex);\n      currentIndex--;\n      [copy[currentIndex], copy[randomIndex]] = [\n        copy[randomIndex],\n        copy[currentIndex]\n      ];\n    }\n    return copy;\n  }\n  var lerp = (amt, from, to) => from + amt * (to - from);\n  var scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {\n    if (!valuesToFill || valuesToFill.length < 2) {\n      throw new Error(\"valuesToFill array must have at least two values.\");\n    }\n    if (targetSize < 1 && padding > 0) {\n      throw new Error(\"Target size must be at least 1\");\n    }\n    if (targetSize < valuesToFill.length && padding === 0) {\n      throw new Error(\n        \"Target size must be greater than or equal to the valuesToFill array length.\"\n      );\n    }\n    const result = new Array(targetSize);\n    if (padding <= 0) {\n      const len = valuesToFill.length;\n      const lastIdx = len - 1;\n      const totalAdded = targetSize - len;\n      const baseAdds = Math.floor(totalAdded / lastIdx);\n      const remainder = totalAdded % lastIdx;\n      let currentResultIdx = 0;\n      for (let i = 0; i < lastIdx; i++) {\n        const startVal = valuesToFill[i];\n        const endVal = valuesToFill[i + 1];\n        const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);\n        for (let j = 0; j < segmentLen; j++) {\n          const t = j / segmentLen;\n          result[currentResultIdx++] = fillFunction(t, startVal, endVal);\n        }\n      }\n      result[currentResultIdx] = valuesToFill[lastIdx];\n      return result;\n    }\n    const domainStart = padding;\n    const domainEnd = 1 - padding;\n    const lenMinus1 = valuesToFill.length - 1;\n    const normalizedPositions = new Float64Array(valuesToFill.length);\n    for (let i = 0; i < valuesToFill.length; i++) {\n      normalizedPositions[i] = i / lenMinus1;\n    }\n    let segmentIndex = 0;\n    for (let i = 0; i < targetSize; i++) {\n      const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);\n      const adjustedT = domainStart + t * (domainEnd - domainStart);\n      while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {\n        segmentIndex++;\n      }\n      const segmentStart = normalizedPositions[segmentIndex];\n      const segmentEnd = normalizedPositions[segmentIndex + 1];\n      let segmentT = 0;\n      if (segmentEnd > segmentStart) {\n        segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);\n      }\n      const fromValue = valuesToFill[segmentIndex];\n      const toValue = valuesToFill[segmentIndex + 1];\n      result[i] = fillFunction(segmentT, fromValue, toValue);\n    }\n    return result;\n  };\n  var pointOnCurve = (curveMethod, curveAccent) => {\n    return (t) => {\n      const limit = Math.PI / 2;\n      const slice = limit / 1;\n      const percentile = t;\n      let x = 0, y = 0;\n      if (curveMethod === \"lam\\xE9\") {\n        const t2 = percentile * limit;\n        const exp = 2 / (2 + 20 * curveAccent);\n        const cosT = Math.cos(t2);\n        const sinT = Math.sin(t2);\n        x = Math.sign(cosT) * __pow(Math.abs(cosT), exp);\n        y = Math.sign(sinT) * __pow(Math.abs(sinT), exp);\n      } else if (curveMethod === \"arc\") {\n        y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);\n        x = Math.sin(Math.PI / 2 + t * slice - curveAccent);\n      } else if (curveMethod === \"pow\") {\n        x = Math.pow(1 - percentile, 1 - curveAccent);\n        y = Math.pow(percentile, 1 - curveAccent);\n      } else if (curveMethod === \"powY\") {\n        x = Math.pow(1 - percentile, curveAccent);\n        y = Math.pow(percentile, 1 - curveAccent);\n      } else if (curveMethod === \"powX\") {\n        x = Math.pow(percentile, curveAccent);\n        y = Math.pow(percentile, 1 - curveAccent);\n      } else if (typeof curveMethod === \"function\") {\n        const [xFunc, yFunc] = curveMethod(t, curveAccent);\n        x = xFunc;\n        y = yFunc;\n      } else {\n        throw new Error(\n          `pointOnCurve() curveMethod parameter is expected to be \"lam\\xE9\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${curveMethod}\\` given.`\n        );\n      }\n      return { x, y };\n    };\n  };\n  var makeCurveEasings = (curveMethod, curveAccent) => {\n    const point = pointOnCurve(curveMethod, curveAccent);\n    return {\n      sEasing: (t) => point(t).x,\n      lEasing: (t) => point(t).y\n    };\n  };\n\n  // src/colorUtils.ts\n  var colorUtils_exports = {};\n  __export(colorUtils_exports, {\n    colorHarmonies: () => colorHarmonies,\n    colorToCSS: () => colorToCSS,\n    harveyHue: () => harveyHue,\n    hsv2hsl: () => hsv2hsl,\n    normalizeHue: () => normalizeHue,\n    uniqueRandomHues: () => uniqueRandomHues\n  });\n  function normalizeHue(h) {\n    return (h % 360 + 360) % 360;\n  }\n  function harveyHue(h) {\n    h = normalizeHue(h) / 360;\n    if (h === 1 || h === 0) return h;\n    h = 1 + h % 1;\n    const seg = 1 / 6;\n    const a = h % seg / seg * Math.PI / 2;\n    const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];\n    const i = Math.floor(h * 6);\n    const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];\n    return cases[i % 6] * 360;\n  }\n  var colorHarmonies = {\n    complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],\n    splitComplementary: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 150),\n      normalizeHue(h - 150)\n    ],\n    triadic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 120),\n      normalizeHue(h + 240)\n    ],\n    tetradic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 90),\n      normalizeHue(h + 180),\n      normalizeHue(h + 270)\n    ],\n    pentadic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 72),\n      normalizeHue(h + 144),\n      normalizeHue(h + 216),\n      normalizeHue(h + 288)\n    ],\n    hexadic: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 60),\n      normalizeHue(h + 120),\n      normalizeHue(h + 180),\n      normalizeHue(h + 240),\n      normalizeHue(h + 300)\n    ],\n    monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],\n    // min 2 for RampenSau\n    doubleComplementary: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 180),\n      normalizeHue(h + 30),\n      normalizeHue(h + 210)\n    ],\n    compound: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 180),\n      normalizeHue(h + 60),\n      normalizeHue(h + 240)\n    ],\n    analogous: (h) => [\n      normalizeHue(h),\n      normalizeHue(h + 30),\n      normalizeHue(h + 60),\n      normalizeHue(h + 90),\n      normalizeHue(h + 120),\n      normalizeHue(h + 150)\n    ]\n  };\n  function uniqueRandomHues({\n    startHue = 0,\n    total = 9,\n    minHueDiffAngle = 60,\n    rndFn = Math.random\n  } = {}) {\n    minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);\n    const baseHue = startHue != null ? startHue : rndFn() * 360;\n    const huesToPickFrom = Array.from(\n      {\n        length: Math.round(360 / minHueDiffAngle)\n      },\n      (_, i) => (baseHue + i * minHueDiffAngle) % 360\n    );\n    let randomizedHues = shuffleArray(huesToPickFrom, rndFn);\n    if (randomizedHues.length > total) {\n      randomizedHues = randomizedHues.slice(0, total);\n    }\n    return randomizedHues;\n  }\n  var hsv2hsl = ([h, s, v]) => {\n    const l = v - v * s / 2;\n    const m = Math.min(l, 1 - l);\n    const s_hsl = m === 0 ? 0 : (v - l) / m;\n    return [h, s_hsl, l];\n  };\n  var colorModsCSS = {\n    oklch: (color) => [\n      color[2] * 100 + \"%\",\n      color[1] * 100 + \"%\",\n      color[0]\n    ],\n    lch: (color) => [\n      color[2] * 100 + \"%\",\n      color[1] * 100 + \"%\",\n      color[0]\n    ],\n    hsl: (color) => [\n      color[0],\n      color[1] * 100 + \"%\",\n      color[2] * 100 + \"%\"\n    ],\n    hsv: (color) => {\n      const [h, s, l] = hsv2hsl(color);\n      return [h, s * 100 + \"%\", l * 100 + \"%\"];\n    }\n  };\n  var colorToCSS = (color, mode = \"oklch\") => {\n    const cssMode = mode === \"hsv\" ? \"hsl\" : mode;\n    return `${cssMode}(${colorModsCSS[mode](color).join(\" \")})`;\n  };\n\n  // src/core.ts\n  function generateColorRamp({\n    total = 9,\n    hStart = Math.random() * 360,\n    hStartCenter = 0.5,\n    hEasing = (x) => x,\n    hCycles = 1,\n    sRange = [0.4, 0.35],\n    sEasing = (x) => Math.pow(x, 2),\n    lRange = [Math.random() * 0.1, 0.9],\n    lEasing = (x) => Math.pow(x, 1.5),\n    transformFn = ([h, s, l]) => [h, s, l],\n    hueList\n  } = {}) {\n    const lDiff = lRange[1] - lRange[0];\n    const sDiff = sRange[1] - sRange[0];\n    const length = hueList && hueList.length > 0 ? hueList.length : total;\n    return Array.from({ length }, (_, i) => {\n      const relI = length > 1 ? i / (length - 1) : 0;\n      const fraction = 1 / length;\n      const hue = hueList ? hueList[i] : normalizeHue(\n        hStart + // Add the starting hue\n        (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)\n        // Calculate the hue based on the easing function\n      );\n      const saturation = sRange[0] + sDiff * sEasing(relI, fraction);\n      const lightness = lRange[0] + lDiff * lEasing(relI, fraction);\n      return transformFn([hue, saturation, lightness], i);\n    });\n  }\n  var generateColorRampWithCurve = ({\n    total = 9,\n    hStart = Math.random() * 360,\n    hStartCenter = 0.5,\n    hCycles = 1,\n    sRange = [0.4, 0.35],\n    lRange = [Math.random() * 0.1, 0.9],\n    hueList,\n    curveMethod = \"lam\\xE9\",\n    curveAccent = 0.5,\n    transformFn = ([h, s, l]) => [h, s, l]\n  } = {}) => {\n    const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);\n    return generateColorRamp({\n      total,\n      hStart,\n      hStartCenter,\n      hCycles,\n      sRange,\n      lRange,\n      sEasing,\n      lEasing,\n      transformFn,\n      hueList\n    });\n  };\n\n  // src/index.ts\n  var generateColorRampParams = {\n    total: {\n      default: 5,\n      props: { min: 4, max: 50, step: 1 }\n    },\n    hStart: {\n      default: 0,\n      props: { min: 0, max: 360, step: 0.1 }\n    },\n    hCycles: {\n      default: 1,\n      props: { min: -2, max: 2, step: 1e-3 }\n    },\n    hStartCenter: {\n      default: 0.5,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    minLight: {\n      default: Math.random() * 0.2,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    maxLight: {\n      default: 0.89 + Math.random() * 0.11,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    minSaturation: {\n      default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    maxSaturation: {\n      default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,\n      props: { min: 0, max: 1, step: 1e-3 }\n    },\n    curveMethod: {\n      default: \"lam\\xE9\",\n      props: { options: [\"lam\\xE9\", \"sine\", \"power\", \"linear\"] }\n    },\n    curveAccent: {\n      default: 0.5,\n      props: { min: 0, max: 5, step: 0.01 }\n    }\n  };\n  return __toCommonJS(index_exports);\n})();\nreturn rampensau; }));\n"
  },
  {
    "path": "dist/utils.d.ts",
    "content": "/**\n * returns a new shuffled array\n * @param {Array} array - The array to shuffle.\n * @param {function} rndFn - The random function to use.\n * @returns {Array} - The shuffled array.\n */\nexport declare function shuffleArray<T>(array: readonly T[], rndFn?: () => number): T[];\ndeclare type FillFunction<T> = T extends number ? (amt: number, from: T, to: T) => T : (amt: number, from: T | null, to: T | null) => T;\n/**\n * Linearly interpolates between two values.\n *\n * @param {number} amt - The interpolation amount (usually between 0 and 1).\n * @param {number} from - The starting value.\n * @param {number} to - The ending value.\n * @returns {number} - The interpolated value.\n */\nexport declare const lerp: FillFunction<number>;\n/**\n * Scales and spreads an array to the target size using interpolation, with optional padding.\n *\n * This function takes an initial array of values, a target size, an optional padding value,\n * and an interpolation function (defaults to `lerp`). It returns a scaled and spread\n * version of the initial array to the target size using the specified interpolation function.\n *\n * The padding parameter (between 0 and 1) compresses the normalized domain from both ends,\n * matching the behavior of chroma.js's scale() function. This is particularly useful for\n * color scales to prevent the endpoints from being too extreme.\n *\n * When padding is 0 (default), the original algorithm is used where values are distributed\n * and interpolated across segments.\n *\n * When padding > 0, the normalized domain (0-1) is compressed to [padding, 1-padding],\n * allowing for more graceful handling of extreme values.\n *\n * @param {Array<T>} valuesToFill - The initial array of values.\n * @param {number} targetSize - The desired size of the resulting array.\n * @param {number} padding - Optional padding value between 0 and 1 (default: 0).\n * @param {FillFunction<T>} fillFunction - The interpolation function (default is lerp).\n * @returns {Array<T>} The scaled and spread array.\n * @throws {Error} If the initial array is invalid or target size is invalid.\n */\nexport declare const scaleSpreadArray: <T>(valuesToFill: T[], targetSize: number, padding?: number, fillFunction?: FillFunction<T>) => T[];\nexport declare type CurveMethod = \"lamé\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" | ((i: number, curveAccent: number) => [number, number]);\n/**\n * function pointOnCurve\n * @param curveMethod {String|Function} Defines how the curve is drawn\n * @param curveAccent {Number} Defines the accent of the curve\n * @returns {Function} A function that takes a number between 0 and 1 and returns the x and y coordinates of the curve at that point\n * @throws {Error} If the curveMethod is not a valid type\n */\nexport declare const pointOnCurve: (curveMethod: CurveMethod, curveAccent: number) => (t: number) => {\n    x: number;\n    y: number;\n};\n/**\n * makeCurveEasings generates two easing functions based on a curve method and accent.\n * @param {CurveMethod} curveMethod - The method used to generate the curve.\n * @param {number} curveAccent - The accent of the curve.\n * @returns {Object} An object containing two easing functions: sEasing and lEasing.\n */\nexport declare const makeCurveEasings: (curveMethod: CurveMethod, curveAccent: number) => {\n    sEasing: (t: number) => number;\n    lEasing: (t: number) => number;\n};\nexport {};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"rampensau\",\n  \"version\": \"2.3.0\",\n  \"description\": \"Color ramp generator using curves within the HSL color model\",\n  \"type\": \"module\",\n  \"main\": \"./dist/index.cjs\",\n  \"module\": \"./dist/index.mjs\",\n  \"browser\": \"./dist/index.min.js\",\n  \"jsdelivr\": \"./dist/index.umd.js\",\n  \"exports\": {\n    \"require\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.cjs\"\n    },\n    \"import\": {\n      \"types\": \"./dist/index.d.ts\",\n      \"default\": \"./dist/index.mjs\"\n    }\n  },\n  \"scripts\": {\n    \"build\": \"npm run lint && tsc --build && node ./build.js\",\n    \"dev\": \"tsc --build --watch\",\n    \"test\": \"vitest\",\n    \"lint\": \"eslint . --ext .ts && npx prettier --check ./src/\",\n    \"prettier\": \"npx prettier --write ./src/\"\n  },\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/meodai/rampensau.git\"\n  },\n  \"keywords\": [\n    \"color\",\n    \"generative-art\",\n    \"colour\",\n    \"palette-generation\",\n    \"generative\"\n  ],\n  \"author\": \"David Aerne @meodai\",\n  \"license\": \"MIT\",\n  \"bugs\": {\n    \"url\": \"https://github.com/meodai/fettepalette/issues\"\n  },\n  \"homepage\": \"https://github.com/meodai/fettepalette#readme\",\n  \"devDependencies\": {\n    \"@typescript-eslint/eslint-plugin\": \"^4.31.2\",\n    \"@typescript-eslint/parser\": \"^4.31.2\",\n    \"esbuild\": \"^0.25.3\",\n    \"eslint\": \"^7.32.0\",\n    \"prettier\": \"2.4.1\",\n    \"typescript\": \"^4.4.3\",\n    \"vitest\": \"^3.1.2\"\n  }\n}\n"
  },
  {
    "path": "src/colorUtils.ts",
    "content": "import { shuffleArray } from \"./utils.js\";\n\nexport type Vector2 = [number, number];\nexport type Vector3 = [...Vector2, number];\n\n/**\n * Converts a color from HSL to HSV.\n * @param {Array} hsl - The HSL color values.\n * @returns {Array} - The HSV color values.\n */\nexport function normalizeHue(h: number): number {\n  return ((h % 360) + 360) % 360;\n}\n\n/**\n * Get a more evenly distributed spectrum without the over abundance of green and ultramarine\n * https://twitter.com/harvey_rayner/status/1748159440010809665\n * @param h - The hue value to be converted 0-360\n * @returns h\n */\nexport function harveyHue(h: number): number {\n  // modified this part to make it more usable with rampenSau\n  h = normalizeHue(h) / 360; // this ensures the value stays within the 0-360 range and normalizes it to 0-1\n\n  if (h === 1 || h === 0) return h;\n  h = 1 + (h % 1);\n\n  const seg = 1 / 6;\n  const a = (((h % seg) / seg) * Math.PI) / 2;\n  const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];\n  const i = Math.floor(h * 6);\n  const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];\n\n  return (cases[i % 6] as number) * 360;\n}\n\nexport type colorHarmony =\n  | \"complementary\"\n  | \"splitComplementary\"\n  | \"triadic\"\n  | \"tetradic\"\n  | \"pentadic\"\n  | \"hexadic\"\n  | \"monochromatic\"\n  | \"doubleComplementary\"\n  | \"compound\"\n  | \"analogous\";\nexport type colorHarmonyFn = (h: number) => number[];\n\n/**\n * Generates a list of hues based on a color harmony.\n * @param {number} h - The base hue.\n * @param {colorHarmony} harmony - The color harmony.\n * @returns {Array<number>} - The list of hues.\n */\nexport const colorHarmonies: {\n  [key in colorHarmony]: colorHarmonyFn;\n} = {\n  complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],\n  splitComplementary: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 150),\n    normalizeHue(h - 150),\n  ],\n  triadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 120),\n    normalizeHue(h + 240),\n  ],\n  tetradic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 90),\n    normalizeHue(h + 180),\n    normalizeHue(h + 270),\n  ],\n  pentadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 72),\n    normalizeHue(h + 144),\n    normalizeHue(h + 216),\n    normalizeHue(h + 288),\n  ],\n  hexadic: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 60),\n    normalizeHue(h + 120),\n    normalizeHue(h + 180),\n    normalizeHue(h + 240),\n    normalizeHue(h + 300),\n  ],\n  monochromatic: (h) => [normalizeHue(h), normalizeHue(h)], // min 2 for RampenSau\n  doubleComplementary: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 180),\n    normalizeHue(h + 30),\n    normalizeHue(h + 210),\n  ],\n  compound: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 180),\n    normalizeHue(h + 60),\n    normalizeHue(h + 240),\n  ],\n  analogous: (h) => [\n    normalizeHue(h),\n    normalizeHue(h + 30),\n    normalizeHue(h + 60),\n    normalizeHue(h + 90),\n    normalizeHue(h + 120),\n    normalizeHue(h + 150),\n  ],\n};\n\nexport type uniqueRandomHuesArguments = {\n  startHue?: number;\n  total?: number;\n  minHueDiffAngle?: number;\n  rndFn?: () => number;\n};\n/**\n * Generates a list of unique hues.\n * @param {uniqueRandomHuesArguments} args - The arguments to generate the hues.\n * @returns {Array<number>} - The list of hues.\n */\nexport function uniqueRandomHues({\n  startHue = 0,\n  total = 9,\n  minHueDiffAngle = 60,\n  rndFn = Math.random,\n} = {}): number[] {\n  minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);\n  const baseHue = startHue ?? rndFn() * 360;\n  const huesToPickFrom = Array.from(\n    {\n      length: Math.round(360 / minHueDiffAngle),\n    },\n    (_, i) => (baseHue + i * minHueDiffAngle) % 360\n  );\n\n  let randomizedHues = shuffleArray(huesToPickFrom, rndFn);\n  if (randomizedHues.length > total) {\n    randomizedHues = randomizedHues.slice(0, total);\n  }\n  return randomizedHues;\n}\n\n/**\n * Converts a color from HSV to HSL.\n * @param {Array} hsv - The HSV color values.\n * @returns {Array} - The HSL color values.\n */\nexport const hsv2hsl = ([h, s, v]: Vector3): Vector3 => {\n  const l = v - (v * s) / 2;\n  const m = Math.min(l, 1 - l);\n  const s_hsl = m === 0 ? 0 : (v - l) / m;\n  return [h, s_hsl, l];\n};\n\n/**\n * functions to convert from the ramp's colors values to CSS color functions.\n */\nconst colorModsCSS = {\n  oklch: (color: Vector3) => [\n    color[2] * 100 + \"%\",\n    color[1] * 100 + \"%\",\n    color[0],\n  ],\n  lch: (color: Vector3) => [\n    color[2] * 100 + \"%\",\n    color[1] * 100 + \"%\",\n    color[0],\n  ],\n  hsl: (color: Vector3) => [\n    color[0],\n    color[1] * 100 + \"%\",\n    color[2] * 100 + \"%\",\n  ],\n  hsv: (color: Vector3) => {\n    const [h, s, l] = hsv2hsl(color);\n    return [h, s * 100 + \"%\", l * 100 + \"%\"];\n  },\n};\n\nexport type colorToCSSMode = \"oklch\" | \"lch\" | \"hsl\" | \"hsv\";\n\n/**\n * Converts color values to a CSS color function string.\n *\n * @param {Vector3} color - Array of three color values based on the color mode.\n * @param {colorToCSSMode} mode - The color mode to use (oklch, lch, hsl, or hsv).\n * @returns {string} - The CSS color function string in the appropriate format.\n */\nexport const colorToCSS = (\n  color: Vector3,\n  mode: colorToCSSMode = \"oklch\"\n): string => {\n  const cssMode = mode === \"hsv\" ? \"hsl\" : mode; // Use HSL for HSV input\n  return `${cssMode}(${colorModsCSS[mode](color).join(\" \")})`;\n};\n"
  },
  {
    "path": "src/core.ts",
    "content": "import { makeCurveEasings } from \"./utils\";\nimport { normalizeHue } from \"./colorUtils\";\n\nimport type { Vector2, Vector3 } from \"./colorUtils\";\nimport type { CurveMethod } from \"./utils\";\n\nexport type ModifiedEasingFn = (x: number, fr?: number) => number;\n\nexport type hueArguments = {\n  hStart?: number;\n  hStartCenter?: number;\n  hCycles?: number;\n  hEasing?: ModifiedEasingFn;\n};\nexport type presetHues = {\n  hueList: number[];\n};\nexport type saturationArguments = {\n  sRange?: Vector2;\n  sEasing?: ModifiedEasingFn;\n};\nexport type lightnessArguments = {\n  lRange?: Vector2;\n  lEasing?: ModifiedEasingFn;\n};\n\ntype BaseGenerateColorRampArgument = {\n  total?: number;\n  transformFn?: (hsl: Vector3, i?: number) => Vector3 | string;\n} & hueArguments &\n  saturationArguments &\n  lightnessArguments;\n\nexport type GenerateColorRampArgument = BaseGenerateColorRampArgument & {\n  hueList?: never;\n};\n\nexport type GenerateColorRampArgumentFixedHues = BaseGenerateColorRampArgument &\n  presetHues;\n\n/**\n * Generates a color ramp based on the HSL color space.\n * @param {GenerateColorRampArgument} args - The arguments to generate the ramp.\n * @returns {Array<number>} - The color ramp.\n */\nexport function generateColorRamp({\n  total = 9,\n  hStart = Math.random() * 360,\n  hStartCenter = 0.5,\n  hEasing = (x) => x,\n  hCycles = 1,\n\n  sRange = [0.4, 0.35],\n  sEasing = (x) => Math.pow(x, 2),\n\n  lRange = [Math.random() * 0.1, 0.9],\n  lEasing = (x) => Math.pow(x, 1.5),\n\n  transformFn = ([h, s, l]) => [h, s, l],\n\n  hueList,\n}:\n  | GenerateColorRampArgument\n  | GenerateColorRampArgumentFixedHues = {}): Vector3[] {\n  // creates a range of lightness and saturation based on the corresponding min and max values\n  const lDiff: number = lRange[1] - lRange[0];\n  const sDiff: number = sRange[1] - sRange[0];\n\n  // if hueList is provided, use it's length as the length of the ramp\n  const length = hueList && hueList.length > 0 ? hueList.length : total;\n\n  return Array.from({ length }, (_, i) => {\n    const relI = length > 1 ? i / (length - 1) : 0;\n    const fraction = 1 / length;\n\n    const hue = hueList\n      ? (hueList[i] as number)\n      : normalizeHue(\n          hStart + // Add the starting hue\n            (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles) // Calculate the hue based on the easing function\n        );\n\n    const saturation = sRange[0] + sDiff * sEasing(relI, fraction);\n    const lightness = lRange[0] + lDiff * lEasing(relI, fraction);\n\n    return transformFn([hue, saturation, lightness], i) as Vector3; // Ensure the array is of type Vector3\n  });\n}\n\nexport const generateColorRampWithCurve = ({\n  total = 9,\n  hStart = Math.random() * 360,\n  hStartCenter = 0.5,\n  hCycles = 1,\n  sRange = [0.4, 0.35],\n  lRange = [Math.random() * 0.1, 0.9],\n  hueList,\n  curveMethod = \"lamé\",\n  curveAccent = 0.5,\n  transformFn = ([h, s, l]) => [h, s, l],\n}: (GenerateColorRampArgument | GenerateColorRampArgumentFixedHues) & {\n  curveMethod?: CurveMethod;\n  curveAccent?: number;\n} = {}): Vector3[] => {\n  const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);\n\n  return generateColorRamp({\n    total,\n    hStart,\n    hStartCenter,\n    hCycles,\n    sRange,\n    lRange,\n    sEasing,\n    lEasing,\n    transformFn,\n    hueList,\n  });\n};\n"
  },
  {
    "path": "src/index.ts",
    "content": "export * as utils from \"./utils\";\nexport * as colorUtils from \"./colorUtils\";\n\nexport { generateColorRamp, generateColorRampWithCurve } from \"./core\";\n\n/**\n * A set of default parameters and sane ranges to use with `generateColorRamp`\n * when coming up with random color ramps.\n */\nexport const generateColorRampParams = {\n  total: {\n    default: 5,\n    props: { min: 4, max: 50, step: 1 },\n  },\n  hStart: {\n    default: 0,\n    props: { min: 0, max: 360, step: 0.1 },\n  },\n  hCycles: {\n    default: 1,\n    props: { min: -2, max: 2, step: 0.001 },\n  },\n  hStartCenter: {\n    default: 0.5,\n    props: { min: 0, max: 1, step: 0.001 },\n  },\n  minLight: {\n    default: Math.random() * 0.2,\n    props: { min: 0, max: 1, step: 0.001 },\n  },\n  maxLight: {\n    default: 0.89 + Math.random() * 0.11,\n    props: { min: 0, max: 1, step: 0.001 },\n  },\n  minSaturation: {\n    default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,\n    props: { min: 0, max: 1, step: 0.001 },\n  },\n  maxSaturation: {\n    default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,\n    props: { min: 0, max: 1, step: 0.001 },\n  },\n  curveMethod: {\n    default: \"lamé\",\n    props: { options: [\"lamé\", \"sine\", \"power\", \"linear\"] },\n  },\n  curveAccent: {\n    default: 0.5,\n    props: { min: 0, max: 5, step: 0.01 },\n  },\n};\n"
  },
  {
    "path": "src/utils.ts",
    "content": "/**\n * returns a new shuffled array\n * @param {Array} array - The array to shuffle.\n * @param {function} rndFn - The random function to use.\n * @returns {Array} - The shuffled array.\n */\n\nexport function shuffleArray<T>(array: readonly T[], rndFn = Math.random): T[] {\n  // Create a copy of the input array\n  const copy = [...array];\n\n  let currentIndex = copy.length,\n    randomIndex;\n\n  while (currentIndex != 0) {\n    randomIndex = Math.floor(rndFn() * currentIndex);\n    currentIndex--;\n\n    [copy[currentIndex], copy[randomIndex]] = [\n      copy[randomIndex] as T,\n      copy[currentIndex] as T,\n    ];\n  }\n\n  return copy;\n}\n\ntype FillFunction<T> = T extends number\n  ? (amt: number, from: T, to: T) => T\n  : (amt: number, from: T | null, to: T | null) => T;\n\n/**\n * Linearly interpolates between two values.\n *\n * @param {number} amt - The interpolation amount (usually between 0 and 1).\n * @param {number} from - The starting value.\n * @param {number} to - The ending value.\n * @returns {number} - The interpolated value.\n */\nexport const lerp: FillFunction<number> = (amt, from, to) =>\n  from + amt * (to - from);\n\n/**\n * Scales and spreads an array to the target size using interpolation, with optional padding.\n *\n * This function takes an initial array of values, a target size, an optional padding value,\n * and an interpolation function (defaults to `lerp`). It returns a scaled and spread\n * version of the initial array to the target size using the specified interpolation function.\n *\n * The padding parameter (between 0 and 1) compresses the normalized domain from both ends,\n * matching the behavior of chroma.js's scale() function. This is particularly useful for\n * color scales to prevent the endpoints from being too extreme.\n *\n * When padding is 0 (default), the original algorithm is used where values are distributed\n * and interpolated across segments.\n *\n * When padding > 0, the normalized domain (0-1) is compressed to [padding, 1-padding],\n * allowing for more graceful handling of extreme values.\n *\n * @param {Array<T>} valuesToFill - The initial array of values.\n * @param {number} targetSize - The desired size of the resulting array.\n * @param {number} padding - Optional padding value between 0 and 1 (default: 0).\n * @param {FillFunction<T>} fillFunction - The interpolation function (default is lerp).\n * @returns {Array<T>} The scaled and spread array.\n * @throws {Error} If the initial array is invalid or target size is invalid.\n */\nexport const scaleSpreadArray = <T>(\n  valuesToFill: T[],\n  targetSize: number,\n  padding = 0,\n  fillFunction: FillFunction<T> = lerp as unknown as FillFunction<T>\n): T[] => {\n  // Validation checks\n  if (!valuesToFill || valuesToFill.length < 2) {\n    throw new Error(\"valuesToFill array must have at least two values.\");\n  }\n  if (targetSize < 1 && padding > 0) {\n    throw new Error(\"Target size must be at least 1\");\n  }\n  if (targetSize < valuesToFill.length && padding === 0) {\n    throw new Error(\n      \"Target size must be greater than or equal to the valuesToFill array length.\"\n    );\n  }\n\n  const result = new Array(targetSize);\n\n  // For case without padding, use the original algorithm optimized\n  if (padding <= 0) {\n    const len = valuesToFill.length;\n    const lastIdx = len - 1;\n    const totalAdded = targetSize - len;\n\n    // Calculate how many items are added per segment\n    // The original logic distributed 'valuesToAdd' in a round-robin fashion\n    // starting from the first segment.\n    const baseAdds = Math.floor(totalAdded / lastIdx);\n    const remainder = totalAdded % lastIdx;\n\n    let currentResultIdx = 0;\n\n    for (let i = 0; i < lastIdx; i++) {\n      const startVal = valuesToFill[i] as T;\n      const endVal = valuesToFill[i + 1] as T;\n\n      // A segment consists of the start value + any added intermediate values.\n      // If i < remainder, this segment gets an extra slot (matching the original round-robin).\n      const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);\n\n      for (let j = 0; j < segmentLen; j++) {\n        const t = j / segmentLen;\n        result[currentResultIdx++] = fillFunction(t, startVal, endVal);\n      }\n    }\n\n    // The loop above handles [start, ...intermediates] for every segment.\n    // We must manually add the very last value of the input array,\n    // as it is never a 'start' value for a segment.\n    result[currentResultIdx] = valuesToFill[lastIdx];\n\n    return result;\n  }\n\n  // Implement chroma.js style padding (Optimized)\n\n  // The padding essentially shifts the start and end of the normalized range\n  const domainStart = padding;\n  const domainEnd = 1 - padding;\n  const lenMinus1 = valuesToFill.length - 1;\n\n  // Optimization: Pre-calculate normalized positions once\n  // This avoids creating a new array and iterating it inside the main loop (O(N) -> O(1))\n  const normalizedPositions = new Float64Array(valuesToFill.length);\n  for (let i = 0; i < valuesToFill.length; i++) {\n    normalizedPositions[i] = i / lenMinus1;\n  }\n\n  let segmentIndex = 0;\n\n  // Generate evenly spaced positions in the target array\n  for (let i = 0; i < targetSize; i++) {\n    // Generate normalized position (0-1)\n    const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);\n\n    // Apply padding by adjusting t\n    const adjustedT = domainStart + t * (domainEnd - domainStart);\n\n    // Optimization: Monotonic search.\n    // Since 'i' increases, 'adjustedT' increases. We can continue searching\n    // from the previous segmentIndex instead of searching from 0 every time.\n    while (\n      segmentIndex < lenMinus1 &&\n      adjustedT > (normalizedPositions[segmentIndex + 1] as number)\n    ) {\n      segmentIndex++;\n    }\n\n    // Get the segment boundaries in normalized space\n    const segmentStart = normalizedPositions[segmentIndex] as number;\n    const segmentEnd = normalizedPositions[segmentIndex + 1] as number;\n\n    // Calculate relative position within segment (0-1)\n    let segmentT = 0;\n    if (segmentEnd > segmentStart) {\n      segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);\n    }\n\n    // Get the values from the segments\n    const fromValue = valuesToFill[segmentIndex] as T;\n    const toValue = valuesToFill[segmentIndex + 1] as T;\n\n    // Get the interpolated value from the correct segment\n    result[i] = fillFunction(segmentT, fromValue, toValue);\n  }\n\n  return result;\n};\n\nexport type CurveMethod =\n  | \"lamé\"\n  | \"arc\"\n  | \"pow\"\n  | \"powY\"\n  | \"powX\"\n  | ((i: number, curveAccent: number) => [number, number]);\n\n/**\n * function pointOnCurve\n * @param curveMethod {String|Function} Defines how the curve is drawn\n * @param curveAccent {Number} Defines the accent of the curve\n * @returns {Function} A function that takes a number between 0 and 1 and returns the x and y coordinates of the curve at that point\n * @throws {Error} If the curveMethod is not a valid type\n */\nexport const pointOnCurve = (curveMethod: CurveMethod, curveAccent: number) => {\n  return (t: number): { x: number; y: number } => {\n    const limit = Math.PI / 2;\n    const slice = limit / 1;\n    const percentile = t;\n\n    let x = 0,\n      y = 0;\n\n    if (curveMethod === \"lamé\") {\n      const t = percentile * limit;\n      const exp = 2 / (2 + 20 * curveAccent);\n      const cosT = Math.cos(t);\n      const sinT = Math.sin(t);\n      x = Math.sign(cosT) * Math.abs(cosT) ** exp;\n      y = Math.sign(sinT) * Math.abs(sinT) ** exp;\n    } else if (curveMethod === \"arc\") {\n      y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);\n      x = Math.sin(Math.PI / 2 + t * slice - curveAccent);\n    } else if (curveMethod === \"pow\") {\n      x = Math.pow(1 - percentile, 1 - curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (curveMethod === \"powY\") {\n      x = Math.pow(1 - percentile, curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (curveMethod === \"powX\") {\n      x = Math.pow(percentile, curveAccent);\n      y = Math.pow(percentile, 1 - curveAccent);\n    } else if (typeof curveMethod === \"function\") {\n      const [xFunc, yFunc] = curveMethod(t, curveAccent) as [number, number];\n      x = xFunc;\n      y = yFunc;\n    } else {\n      throw new Error(\n        `pointOnCurve() curveMethod parameter is expected to be \"lamé\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but \\`${curveMethod}\\` given.`\n      );\n    }\n\n    return { x, y };\n  };\n};\n\n/**\n * makeCurveEasings generates two easing functions based on a curve method and accent.\n * @param {CurveMethod} curveMethod - The method used to generate the curve.\n * @param {number} curveAccent - The accent of the curve.\n * @returns {Object} An object containing two easing functions: sEasing and lEasing.\n */\nexport const makeCurveEasings = (\n  curveMethod: CurveMethod,\n  curveAccent: number\n): {\n  sEasing: (t: number) => number;\n  lEasing: (t: number) => number;\n} => {\n  const point = pointOnCurve(curveMethod, curveAccent);\n\n  return {\n    sEasing: (t: number) => point(t).x,\n    lEasing: (t: number) => point(t).y,\n  };\n};\n"
  },
  {
    "path": "tea.yaml",
    "content": "# https://tea.xyz/what-is-this-file\n---\nversion: 1.0.0\ncodeOwners:\n  - '0xFA64435d1281921E36b90CeA9a1fbf0e5c408e65'\nquorum: 1\n"
  },
  {
    "path": "test/colorUtils.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { normalizeHue, harveyHue, colorHarmonies, uniqueRandomHues, hsv2hsl, colorToCSS } from '../src/colorUtils';\nimport type { Vector3 } from '../src/colorUtils';\n\ndescribe('normalizeHue', () => {\n  it('should normalize positive hues within the 0-360 range', () => {\n    expect(normalizeHue(0)).toBe(0);\n    expect(normalizeHue(360)).toBe(0); // 360 is normalized to 0\n    expect(normalizeHue(720)).toBe(0); // 720 is normalized to 0\n    expect(normalizeHue(450)).toBe(90); // 450 is normalized to 90\n  });\n\n  it('should normalize negative hues within the 0-360 range', () => {\n    expect(normalizeHue(-360)).toBe(0); // -360 is normalized to 0\n    expect(normalizeHue(-90)).toBe(270); // -90 is normalized to 270\n    expect(normalizeHue(-450)).toBe(270); // -450 is normalized to 270\n  });\n\n  it('should handle edge cases correctly', () => {\n    expect(normalizeHue(0)).toBe(0);\n    expect(normalizeHue(-0)).toBe(0); // Negative zero should also normalize to 0\n    expect(normalizeHue(360)).toBe(0); // 360 is normalized to 0\n    expect(normalizeHue(-360)).toBe(0); // -360 is normalized to 0\n  });\n});\n\n\ndescribe('harveyHue', () => {\n  it('should return a value between 0 and 360', () => {\n    expect(harveyHue(0)).toBeGreaterThanOrEqual(0);\n    expect(harveyHue(0)).toBeLessThanOrEqual(360);\n    expect(harveyHue(360)).toBeGreaterThanOrEqual(0);\n    expect(harveyHue(360)).toBeLessThanOrEqual(360);\n  });\n\n  it('should normalize input hue to the 0-360 range', () => {\n    expect(harveyHue(-360)).toBeGreaterThanOrEqual(0);\n    expect(harveyHue(-360)).toBeLessThanOrEqual(360);\n    expect(harveyHue(720)).toBeGreaterThanOrEqual(0);\n    expect(harveyHue(720)).toBeLessThanOrEqual(360);\n  });\n\n  it('should handle edge cases correctly', () => {\n    expect(harveyHue(0)).toBe(0);\n    expect(harveyHue(360)).toBe(0); // 360 is normalized to 0\n  });\n});\n\ndescribe('colorHarmonies', () => {\n  it('complementary should return two hues 180 degrees apart', () => {\n    const baseHue = 60;\n    const harmony = colorHarmonies.complementary(baseHue);\n    expect(harmony).toHaveLength(2);\n    expect(harmony[0]).toBe(baseHue);\n    expect(harmony[1]).toBe((baseHue + 180) % 360);\n  });\n\n  it('triadic should return three hues 120 degrees apart', () => {\n    const baseHue = 30;\n    const harmony = colorHarmonies.triadic(baseHue);\n    expect(harmony).toHaveLength(3);\n    expect(harmony[0]).toBe(baseHue);\n    expect(harmony[1]).toBe((baseHue + 120) % 360);\n    expect(harmony[2]).toBe((baseHue + 240) % 360);\n  });\n\n  // TODO: Add tests for other harmonies (splitComplementary, tetradic, etc.)\n});\n\ndescribe('uniqueRandomHues', () => {\n  it('should generate the correct number of hues', () => {\n    const total = 5;\n    const hues = uniqueRandomHues({ total });\n    expect(hues).toHaveLength(total);\n  });\n\n  it('should generate hues within the 0-360 range', () => {\n    const hues = uniqueRandomHues({ total: 10 });\n    hues.forEach(hue => {\n      expect(hue).toBeGreaterThanOrEqual(0);\n      expect(hue).toBeLessThan(360);\n    });\n  });\n\n  it('should generate unique hues', () => {\n    const hues = uniqueRandomHues({ total: 6, minHueDiffAngle: 1 }); // Ensure uniqueness is likely\n    const uniqueHues = new Set(hues);\n    expect(hues.length).toBe(uniqueHues.size);\n  });\n\n  it('should respect minHueDiffAngle (probabilistically)', () => {\n    const total = 4;\n    const minHueDiffAngle = 90;\n    const hues = uniqueRandomHues({ total, minHueDiffAngle });\n\n    // Check differences between sorted hues\n    const sortedHues = [...hues].sort((a, b) => a - b);\n    for (let i = 0; i < sortedHues.length; i++) {\n      const h1 = sortedHues[i] as number;\n      const h2 = sortedHues[(i + 1) % sortedHues.length] as number;\n      let diff = Math.round(Math.abs(h1 - h2));\n      if (diff > 180) diff = 360 - diff; // Handle wrap-around difference\n      // Because it picks from pre-calculated slots, the difference should be >= minHueDiffAngle\n      expect(diff).toBeGreaterThanOrEqual(minHueDiffAngle);\n    }\n  });\n});\n\ndescribe('hsv2hsl', () => {\n  it('should convert HSV black to HSL black', () => {\n    const hsv: Vector3 = [0, 0, 0];\n    const hsl = hsv2hsl(hsv);\n    expect(hsl[0]).toBe(0); // Hue is irrelevant for black\n    expect(hsl[1]).toBe(0); // Saturation\n    expect(hsl[2]).toBe(0); // Lightness\n  });\n\n  it('should convert HSV white to HSL white', () => {\n    const hsv: Vector3 = [0, 0, 1];\n    const hsl = hsv2hsl(hsv);\n    expect(hsl[0]).toBe(0); // Hue is irrelevant for white\n    expect(hsl[1]).toBe(0); // Saturation\n    expect(hsl[2]).toBe(1); // Lightness\n  });\n\n  it('should convert HSV red to HSL red', () => {\n    const hsv: Vector3 = [0, 1, 1]; // Max saturation, max value\n    const hsl = hsv2hsl(hsv);\n    expect(hsl[0]).toBe(0);   // Hue\n    expect(hsl[1]).toBe(1);   // Saturation\n    expect(hsl[2]).toBe(0.5); // Lightness\n  });\n\n  it('should convert HSV gray to HSL gray', () => {\n    const hsv: Vector3 = [0, 0, 0.5]; // No saturation, mid value\n    const hsl = hsv2hsl(hsv);\n    expect(hsl[0]).toBe(0);   // Hue\n    expect(hsl[1]).toBe(0);   // Saturation\n    expect(hsl[2]).toBe(0.5); // Lightness\n  });\n});\n\ndescribe('colorToCSS', () => {\n  it('should format HSL correctly', () => {\n    const color: Vector3 = [120, 0.5, 0.75]; // H, S, L\n    expect(colorToCSS(color, 'hsl')).toBe('hsl(120 50% 75%)');\n  });\n\n  it('should format LCH correctly', () => {\n    // Note: Direct conversion from HSL to LCH isn't done here,\n    // assuming input is already LCH-like for formatting purposes\n    const color: Vector3 = [240, .8, .6]; // H, C, L (example values)\n    expect(colorToCSS(color, 'lch')).toBe('lch(60% 80% 240)');\n  });\n\n  it('should format OKLCH correctly', () => {\n     // Assuming input is OKLCH-like\n    const color: Vector3 = [180, 0.2, 0.8]; // H, C, L (example values)\n    expect(colorToCSS(color, 'oklch')).toBe('oklch(80% 20% 180)');\n  });\n\n  it('should format HSV correctly (by converting to HSL)', () => {\n    const color: Vector3 = [0, 1, 1]; // HSV Red\n    // Expected HSL: [0, 1, 0.5] => hsl(0 100% 50%)\n    expect(colorToCSS(color, 'hsv')).toBe('hsl(0 100% 50%)');\n  });\n\n  it('should default to oklch if mode is omitted', () => {\n     // Assuming input is OKLCH-like\n    const color: Vector3 = [180, 0.2, 0.8];\n    expect(colorToCSS(color)).toBe('oklch(80% 20% 180)');\n  });\n});\n"
  },
  {
    "path": "test/core.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { generateColorRamp, generateColorRampWithCurve } from '../src/core';\nimport type { Vector3 } from '../src/colorUtils';\n\ndescribe('generateColorRamp', () => {\n  it('should generate the correct number of colors', () => {\n    const colors = generateColorRamp({ total: 5 });\n    expect(colors).toHaveLength(5);\n  });\n\n  it('should generate colors within HSL constraints', () => {\n    const colors = generateColorRamp({ total: 10 });\n    colors.forEach((color: Vector3) => {\n      expect(color[0]).toBeGreaterThanOrEqual(0); // Hue\n      expect(color[0]).toBeLessThanOrEqual(360);\n      expect(color[1]).toBeGreaterThanOrEqual(0); // Saturation\n      expect(color[1]).toBeLessThanOrEqual(1);\n      expect(color[2]).toBeGreaterThanOrEqual(0); // Lightness\n      expect(color[2]).toBeLessThanOrEqual(1);\n    });\n  });\n\n  it('should respect hStart parameter', () => {\n    const hStart = 180;\n    const colors = generateColorRamp({ total: 5, hStart, hStartCenter: 0 });\n    // The first color's hue should be close to hStart when hStartCenter is 0\n    expect(colors[0]?.[0]).toBeCloseTo(hStart);\n  });\n\n  it('should respect hCycles parameter', () => {\n    const hStart = 0;\n    const hCycles = 1;\n    const total = 5;\n    const colors = generateColorRamp({ total, hStart, hCycles, hStartCenter: 0 });\n    // With 1 cycle and hStartCenter 0, the last color should be close to the first\n    expect(colors[total - 1]?.[0]).toBeCloseTo(colors[0]?.[0] as number, 0); // Allow some tolerance\n  });\n\n  it('should use hueList when provided', () => {\n    const hueList = [0, 120, 240];\n    const colors = generateColorRamp({ hueList });\n    expect(colors).toHaveLength(hueList.length);\n    expect(colors[0]?.[0]).toBe(hueList[0]);\n    expect(colors[1]?.[0]).toBe(hueList[1]);\n    expect(colors[2]?.[0]).toBe(hueList[2]);\n  });\n\n  it('should apply transformFn', () => {\n    const transformFn = ([h, s, l]: Vector3): Vector3 => [h, s * 0.5, l];\n    const colors = generateColorRamp({ total: 3, transformFn, sRange: [1, 1] });\n    colors.forEach(color => {\n      expect(color[1]).toBeLessThanOrEqual(0.5); // Saturation should be halved\n    });\n  });\n});\n\ndescribe('generateColorRampWithCurve', () => {\n  it('should generate the correct number of colors', () => {\n    const colors = generateColorRampWithCurve({ total: 7 });\n    expect(colors).toHaveLength(7);\n  });\n\n  it('should generate colors within HSL constraints', () => {\n    const colors = generateColorRampWithCurve({ total: 10 });\n    colors.forEach((color: Vector3) => {\n      expect(color[0]).toBeGreaterThanOrEqual(0);\n      expect(color[0]).toBeLessThanOrEqual(360);\n      expect(color[1]).toBeGreaterThanOrEqual(0);\n      expect(color[1]).toBeLessThanOrEqual(1);\n      expect(color[2]).toBeGreaterThanOrEqual(0);\n      expect(color[2]).toBeLessThanOrEqual(1);\n    });\n  });\n\n  it('should use curveMethod for easing', () => {\n    // This test is a bit indirect, checking if output differs from default generateColorRamp\n    // A more robust test would mock makeCurveEasings or check specific curve outputs\n    const colorsCurve = generateColorRampWithCurve({ total: 5, curveMethod: 'lamé', curveAccent: 0.5 });\n    const colorsDefault = generateColorRamp({ total: 5 }); // Uses default easings\n\n    // Expecting the results to be different due to different easing functions\n    // Note: This might fail if default easings happen to produce the same result for a specific input\n    expect(colorsCurve).not.toEqual(colorsDefault);\n  });\n});\n"
  },
  {
    "path": "test/utils.test.ts",
    "content": "import { describe, it, expect } from 'vitest';\nimport { shuffleArray, scaleSpreadArray, makeCurveEasings, pointOnCurve } from '../src/utils';\n\n// Define lerp function locally for testing\nconst lerp = (amt: number, from: number, to: number): number => from + amt * (to - from);\n\ndescribe('shuffleArray', () => {\n  it('should return an array with the same length', () => {\n    const original = [1, 2, 3, 4, 5];\n    const shuffled = shuffleArray(original);\n    expect(shuffled).toHaveLength(original.length);\n  });\n\n  it('should contain the same elements', () => {\n    const original = [1, 2, 3, 4, 5];\n    const shuffled = shuffleArray(original);\n    expect(shuffled).toEqual(expect.arrayContaining(original));\n    expect(original).toEqual(expect.arrayContaining(shuffled));\n  });\n\n  it('should shuffle the array (usually)', () => {\n    const original = Array.from({ length: 100 }, (_, i) => i);\n    const shuffled = shuffleArray(original);\n    // It's statistically improbable but possible for the array to remain unchanged\n    expect(shuffled).not.toEqual(original);\n  });\n\n  it('should use the provided random function', () => {\n    const original = [1, 2, 3];\n    // A deterministic \"random\" function that cycles through 0.5, 0.2, 0.8\n    const mockRndFn = (() => {\n      const sequence = [0.5, 0.2, 0.8];\n      let index = 0;\n      return () => sequence[index++ % sequence.length];\n    })();\n    const shuffled = shuffleArray(original, mockRndFn);\n    // With this mock, the shuffle should produce a predictable result\n    expect(shuffled).toEqual([3, 1, 2]);\n  });\n});\n\ndescribe('lerp', () => {\n  it('should interpolate correctly', () => {\n    expect(lerp(0, 0, 10)).toBe(0);\n    expect(lerp(1, 0, 10)).toBe(10);\n    expect(lerp(0.5, 0, 10)).toBe(5);\n    expect(lerp(0.25, 10, 20)).toBe(12.5);\n    expect(lerp(0.75, -10, 10)).toBe(5);\n  });\n});\n\ndescribe('scaleSpreadArray', () => {\n  it('should scale and spread the array correctly with lerp', () => {\n    const initial = [0, 10];\n    const targetSize = 5;\n    const expected = [0, 2.5, 5, 7.5, 10];\n    const result = scaleSpreadArray(initial, targetSize, 0, lerp);\n    expect(result).toHaveLength(targetSize);\n    result.forEach((val, i) => expect(val).toBeCloseTo(expected[i] as number));\n  });\n\n  it('should handle arrays with more than two elements', () => {\n    const initial = [0, 10, 5];\n    const targetSize = 7;\n    // Expected: [0, 5, 10, 7.5, 5, ?, ?] - Calculation gets complex, focus on length and bounds\n    const result = scaleSpreadArray(initial, targetSize, 0, lerp);\n    expect(result).toHaveLength(targetSize);\n    expect(result[0]).toBe(0);\n    expect(result[2]).toBe(6.666666666666666); // Second original element position\n    expect(result[4]).toBe(8.333333333333334); // Third original element position\n  });\n\n  it('should throw error if initial array is too small', () => {\n    expect(() => scaleSpreadArray([1], 8.333333333333334)).toThrow();\n  });\n\n  it('should throw error if target size is smaller than initial size', () => {\n    expect(() => scaleSpreadArray([1, 2, 3], 2)).toThrow();\n  });\n});\n\ndescribe('pointOnCurve', () => {\n  it('should return points for \"lamé\" curve', () => {\n    const poc = pointOnCurve('lamé', 0.5);\n    const point = poc(0.5);\n    expect(point.x).toBeGreaterThanOrEqual(0);\n    expect(point.x).toBeLessThanOrEqual(1);\n    expect(point.y).toBeGreaterThanOrEqual(0);\n    expect(point.y).toBeLessThanOrEqual(1);\n  });\n\n  it('should return points for \"arc\" curve', () => {\n    const poc = pointOnCurve('arc', 0.1);\n    const point = poc(0.2);\n    expect(point.x).toBeGreaterThanOrEqual(0);\n    expect(point.x).toBeLessThanOrEqual(1);\n    expect(point.y).toBeGreaterThanOrEqual(0);\n    expect(point.y).toBeLessThanOrEqual(1);\n  });\n\n  /*\n  it('should throw error for invalid curve method', () => {\n    // @ts-expect-error Testing invalid input\n    expect(() => pointOnCurve('invalidMethod', 0.5)).toThrow(\n      'pointOnCurve() curveAccent parameter is expected to be \"lamé\" | \"arc\" | \"pow\" | \"powY\" | \"powX\" or a function but `invalidMethod` given.'\n    );\n  });*/\n});\n\n\ndescribe('makeCurveEasings', () => {\n  it('should return sEasing and lEasing functions', () => {\n    const easings = makeCurveEasings('lamé', 0.5);\n    expect(easings.sEasing).toBeInstanceOf(Function);\n    expect(easings.lEasing).toBeInstanceOf(Function);\n  });\n\n  it('easing functions should return values between 0 and 1 for t between 0 and 1', () => {\n    const easings = makeCurveEasings('lamé', 0.5);\n    const tValues = [0, 0.25, 0.5, 0.75, 1];\n    tValues.forEach(t => {\n      const sVal = easings.sEasing(t);\n      const lVal = easings.lEasing(t);\n      expect(sVal).toBeGreaterThanOrEqual(0);\n      expect(sVal).toBeLessThanOrEqual(1);\n      expect(lVal).toBeGreaterThanOrEqual(0);\n      expect(lVal).toBeLessThanOrEqual(1);\n    });\n  });\n});\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"noEmitOnError\": true,\n    \"removeComments\": false,\n    \"sourceMap\": true,\n    \"target\": \"es2019\",\n    \"outDir\": \"dist\",\n    \"declaration\": true,\n    \"emitDeclarationOnly\": true,\n    \"noUncheckedIndexedAccess\": true,\n    \"strict\": true\n  },\n  \"include\": [\"src/**/*\"]\n}\n"
  },
  {
    "path": "vitest.config.ts",
    "content": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    // environment: 'jsdom', // uncomment if you need DOM APIs\n    globals: true, // Use global APIs like describe, it, expect\n  },\n});\n"
  }
]