Full Code of meodai/rampensau for AI

main e328099f6de1 cached
30 files
233.8 KB
69.1k tokens
67 symbols
1 requests
Download .txt
Showing preview only (244K chars total). Download the full file or copy to clipboard to get everything.
Repository: meodai/rampensau
Branch: main
Commit: e328099f6de1
Files: 30
Total size: 233.8 KB

Directory structure:
gitextract_ws2pvfso/

├── .eslintrc
├── .github/
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── build.js
├── dist/
│   ├── colorUtils.d.ts
│   ├── core.d.ts
│   ├── highlighter.html
│   ├── index.cjs
│   ├── index.d.ts
│   ├── index.html
│   ├── index.js
│   ├── index.min.cjs
│   ├── index.min.mjs
│   ├── index.mjs
│   ├── index.umd.js
│   └── utils.d.ts
├── package.json
├── src/
│   ├── colorUtils.ts
│   ├── core.ts
│   ├── index.ts
│   └── utils.ts
├── tea.yaml
├── test/
│   ├── colorUtils.test.ts
│   ├── core.test.ts
│   └── utils.test.ts
├── tsconfig.json
└── vitest.config.ts

================================================
FILE CONTENTS
================================================

================================================
FILE: .eslintrc
================================================
{
  "root": true,
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/eslint-recommended",
    "plugin:@typescript-eslint/recommended"
  ]
}


================================================
FILE: .github/workflows/main.yml
================================================
name: Build and Deploy
on: [push]
jobs:
  build-and-deploy:
    concurrency: ci-${{ github.ref }} # Recommended if you intend to make multiple deployments in quick succession.
    runs-on: ubuntu-latest
    steps:
      - name: Checkout 🛎️
        uses: actions/checkout@v3

      - 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.
        run: |
          npm ci
          npm run build
      
      - name: Test 🔍 
        run: |
          npm ci
          npm run test

      - name: Deploy 🚀
        uses: JamesIves/github-pages-deploy-action@v4.3.3
        with:
          branch: gh-pages # The branch the action should deploy to.
          folder: dist # The folder the action should deploy.


================================================
FILE: .gitignore
================================================
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*

# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage
*.lcov

# nyc test coverage
.nyc_output

# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Dependency directories
node_modules/
jspm_packages/

# TypeScript v1 declaration files
typings/

# TypeScript cache
*.tsbuildinfo

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env
.env.test

# parcel-bundler cache (https://parceljs.org/)
.cache

# Next.js build output
.next

# Nuxt.js build / generate output
.nuxt

# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and *not* Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public

# vuepress build output
.vuepress/dist

# Serverless directories
.serverless/

# FuseBox cache
.fusebox/

# DynamoDB Local files
.dynamodb/

# TernJS port file
.tern-port


================================================
FILE: .prettierrc.json
================================================
{}


================================================
FILE: LICENSE
================================================
MIT License

Copyright (c) 2021 David Aerne

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


================================================
FILE: README.md
================================================
# RampenSau 🎢🐷🎨

**RampenSau** is a color palette generation library that utilizes **hue cycling** and
**easing functions** to generate color ramps. It can generate a sequence of hues, or use a list of hues to generate a color ramp.

Perfect for generating color palettes for data visualizations, visual design, generative art, or just for fun.

![generated RampenSau color palettes Animation](./rampensau.gif)

## Demos

- [Official Docs & Demo](https://meodai.github.io/rampensau/)
  Interactive Documentation with example function calls
- [1000 Generative Samples](https://codepen.io/meodai/pen/ExQWwar?editors=0010)
  Generating a 1000 palettes using similar settings in with lch
- [p5.js Example](https://editor.p5js.org/meodai/sketches/dzEX_4wTN)
  3d example using p5.js's `color()` function
- [Syntax Highlighting](https://meodai.github.io/rampensau/highlighter.html)
  Generative syntax highlighting themes using RampenSau
- [Mini HDR Posters](https://codepen.io/meodai/pen/zYeXEyw)
  Generative posters using lCH (p3+ gamut)
- [Color Ratios](https://codepen.io/meodai/full/vYbwbym)
  Generative rectangles
- [p5.js Stamps](https://openprocessing.org/sketch/2628160)
  Fork of a sketch originally made by [Okazz](https://openprocessing.org/user/128718/?view=sketches).
- [Farbvelo Color Generator](https://farbvelo.elastiq.ch/).
  Project this code is based on

## Installation

**Rampensau** is bundled as both UMD and ES on npm. Install it using your package manager of choice:

```bash
npm install rampensau
```

You can then import RampenSau into your project:

```js
// ES style: import individual methods
import { generateColorRamp } from "rampensau";

// Depending on your setup, you might need to import the MJS version directly
import { generateColorRamp } from "rampensau/dist/index.mjs";

// CJS style
let generateColorRamp = require("rampensau");
```

Or include it directly in your HTML:

```html
<script src="https://cdn.jsdelivr.net/npm/rampensau/dist/index.js"></script>
<!-- or -->
<script type="module">
  import { generateColorRamp } from "https://esm.sh/rampensau/";
</script>
```

## Basic Usage

```js
import { generateColorRamp } from 'rampensau';

const hslColorValues = generateColorRamp({
  // hue generation options
  total: 9,                           // number of colors in the ramp
  hStart: Math.random() * 360,         // hue at the start of the ramp
  hCycles: 1,                           // number of full hue cycles 
                                       // (.5 = 180°, 1 = 360°, 2 = 720°, etc.)
  hStartCenter: 0.5,                    // where in the ramp the hue should be centered
  hEasing: (x, fr) => x,                // hue easing function x is a value between 0 and 1 
                                       // fr is the size of each fraction of the ramp: (1 / total)

  // if 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

  // hueList: [...],                      // list of hues to use

  // saturation
  sRange: [0.4, 0.35],                 // saturation range
  sEasing: (x, fr) => Math.pow(x, 2),   // saturation easing function

  // lightness
  lRange: [Math.random() * 0.1, 0.9],  // lightness range
  lEasing: (x, fr) => Math.pow(x, 1.5), // lightness easing function

  transformFn: (color, i) => color; // function to adjust/convert the color after generation
}); // => [[0…360,0…1,0…1], …]
```

### generateColorRamp(Options{})

**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/).

The 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.

**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.

#### Options

Every single option has a default value, so you can just call the function without any arguments.
It will generate a color ramp with 9 colors, starting at a random hue, with a single hue cycle.

While 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.

##### Hue sequence generation

If you want to generate a sequence of hues, you can use the following options:

- `total` int 3…∞           → Amount of colors the function will generate.
- `hStart` float 0…360      → Starting point of the hue ramp. 0 Red, 180 Teal etc..
- `hStartCenter`: float 0…1 → Center the hue in the color ramp. 0 = start, 0.5 = middle, 1 = end.
- `hCycles` float -∞…0…+∞   → Number of hue cycles. (.5 = 180°, 1 = 360°, 2 = 720°, etc.)
- `hEasing` function(x)     → Hue easing function

The `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).

The `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.

The `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.

**Note:** The further away `hCycles` is from `0`, the more hue variation you will get in the ramp. 

##### Hue List

If 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:

- `hueList` array [0…360]   → List of hues to use. All other hue options will be ignored.

**Example:**

```js
import {
  generateColorRamp,
  colorUtils,
} from "rampensau";

const { uniqueRandomHues } = colorUtils;


generateColorRamp({
  hueList: uniqueRandomHues({
    startHue: Math.random() * 360, 
    total: 5, 
    minHueDiffAngle: 90,
  })
})
```

The `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.

##### Saturation & Lightness

- `sRange` array [0…1,0…1]  → Saturation Range
- `lRange` array [0…1,0…1]  → Lightness Range

##### Easing Functions

Each of the color dimensions can be eased using a custom function.
The function takes an input value `x` and returns a value between 0 and 1:

- `hEasing` function(x)     → Hue easing function
- `sEasing` function(x)     → Saturation easing function
- `lEasing` function(x)     → Lightness easing function

##### Transform Function

- `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.

**Example:**

```js
const hslColorValues = generateColorRamp({
  transformFn: ([h, s, l], i) => {
    // Adjust the color to be more saturated
    return [h, s, .2 + l * .8];
  }
});
```

It could also be used to get a CSS String instead of the array. Just use the `colorToCSS` function from the color utility functions:

```js
const hslColorValues = generateColorRamp({
  transformFn: ([h, s, l]) => colorToCSS(color, 'oklch')
});
```

**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.

### generateColorRampWithCurve(Options{})

**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:

- `curveMethod` string      → The curve method to use for easing. One of `'lamé'`, `'sine'`, `'power'`, or `'linear'`.
- `curveAccent` float 0…5   → The accent of the curve, affecting how pronounced the curve's effect is.

**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.

**Example:**

```js
import { generateColorRampWithCurve } from 'rampensau';

const hslColorValues = generateColorRampWithCurve({
  total: 9,
  hStart: 180,
  curveMethod: 'lamé',
  curveAccent: 0.5,
  sRange: [0.4, 0.8],
  lRange: [0.2, 0.8],
}); // => [[0…360,0…1,0…1], …]
```

## Hue Generation Functions

### uniqueRandomHues(Options{})

Function 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.

- `startHue` float 0…360        → Starting point of the hue ramp. 0 Red, 180 Teal etc..
- `total` int 3…∞               → Amount of base colors.
- `minHueDiffAngle` float 0…360 → Minimum angle between hues.
- `rndFn` function()            → Random function. Defaults to `Math.random`.

### colorHarmonies.colorHarmony(Options{})

Function returns an array of hues based on color harmony theory.

Available harmonies:
- `complementary` - Base hue and its complement (180° opposite)
- `splitComplementary` - Base hue and two hues on either side of its complement
- `triadic` - Three hues evenly spaced around the color wheel
- `tetradic` - Four hues evenly spaced around the color wheel
- `monochromatic` - Just the base hue
- `doubleComplementary` - Two complementary pairs
- `compound` - A mix of complementary and analogous
- `analogous` - A series of adjacent hues

**Example:**

```js
import {
  generateColorRamp,
  colorUtils,
} from "rampensau";

const { colorHarmonies } = colorUtils;

generateColorRamp({
  hueList: colorHarmonies.splitComplementary(Math.random() * 360),
  sRange: [0.4, 0.35],
  lRange: [Math.random() * 0.1, 0.9],
});
```

## Color Utility Functions

To keep the library small, RampenSau does not include any color conversion functions. 
However, it does provide a few utility functions to help you work with **RampenSau** or
its generated colors.

### colorToCSS(color, mode)

In 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]`).

- `color` array [0…360,0…1,0…1] → Color in format generated from **generateColorRamp** (`[0…360,0…1,0…1]`).
- `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.)

**Example**:
  
```js
import {
  colorUtils
} from "rampensau";

const { colorToCSS } = colorUtils;

console.log( 
  generateColorRamp().map(color => colorToCSS(color, 'oklch')) 
); // ['oklch(5.57% 40% 348.39)', 'oklch(14.59% 38.72% 314.74)', …]
```

### harveyHue(h)

Transforms 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.

- `h` float 0…360 → Hue value to transform, normalized to 0-360 range.

**Example**:

```js
import { colorUtils } from "rampensau";
const { harveyHue } = colorUtils;

const transformedHue = harveyHue(0.5); // Returns a transformed hue value
```

## Using RampenSau with a color library

If you are already using a color library like [culori](https://culorijs.org/api/) you can use its
`formatCSS` function instead. Just don't forget to scale the chroma value to the [adequate range](https://culorijs.org/color-spaces/).

```js
culori.formatCss({ mode: 'oklch', 
  l: color[2],
  c: color[1] * 0.4,
  h: color[0],
})

culori.formatCss({ mode: 'lch', 
  l: color[2] * 100,
  c: color[1] * 150,
  h: color[0],
});
```

## Other Utility Functions

RampenSau also provides several utility functions for working with arrays and curves:

### Usage

```js
import { utils } from 'rampensau';
const { shuffleArray, scaleSpreadArray, lerp, pointOnCurve, makeCurveEasings } = utils;
```

### shuffleArray(array, rndFn)

Returns a new shuffled array based on the input array.

- `array` array      → The array to shuffle
- `rndFn` function() → Random function. Defaults to `Math.random`

### scaleSpreadArray(valuesToFill, targetSize, padding, fillFunction)

Scales and spreads an array to the target size using interpolation.
In the context of RampenSau, this is used to create a smooth transition between colors.
Let's say you have an array of 3 colors and you want to create a ramp of 10 colors.
You can use this function to fill the gaps between the colors and create a smooth transition.

- `valuesToFill` array     → Initial array of values
- `targetSize` int         → Desired size of the resulting array
- `padding` float 0…1      → Optional padding value (defaults to 0)
- `fillFunction` function  → Interpolation function (defaults to lerp)

### lerp(amt, from, to)

Linearly interpolates between two values. 
Mainly used for the `fillFunction` in `scaleSpreadArray`.

- `amt` float 0…1   → The interpolation amount
- `from` number     → The starting value
- `to` number       → The ending value

### pointOnCurve(curveMethod, curveAccent)

Returns a function that calculates the point on a curve at a given t value.

- `curveMethod` string or function → The curve method to use
- `curveAccent` float              → The accent of the curve

### makeCurveEasings(curveMethod, curveAccent)

Generates saturation and lightness easing functions based on a curve method.

- `curveMethod` string or function → The curve method to use
- `curveAccent` float              → The accent of the curve

## About the Name

For 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.

## License

Rampensau is distributed under the [MIT License](./LICENSE).
Feel 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.


================================================
FILE: build.js
================================================
import { build } from "esbuild";

// Bundled CJS
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  format: "cjs",
  outfile: "dist/index.cjs",
});

// Bundled CJS, minified
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  minify: true,
  format: "cjs",
  outfile: "dist/index.min.cjs",
});

// Bundled ESM
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  format: "esm",
  target: "es2019",
  outfile: "dist/index.mjs",
});

// Bundled ESM, minified
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  minify: true,
  format: "esm",
  target: "es2019",
  outfile: "dist/index.min.mjs",
});

// Bundled IIFE
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  format: "iife",
  target: "node14",
  globalName: "rampensau",
  outfile: "dist/index.js",
});

// Bundled IIFE, minified
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  minify: true,
  format: "iife",
  target: "es6",
  globalName: "rampensau",
  outfile: "dist/index.min.js",
});

// Bundled UMD
// Adapted from: https://github.com/umdjs/umd/blob/master/templates/returnExports.js
build({
  entryPoints: ["./src/index.ts"],
  logLevel: "info",
  bundle: true,
  format: "iife",
  target: "es6",
  globalName: "rampensau",
  banner: {
    js: `(function(root, factory) {
      if (typeof define === 'function' && define.amd) {
      	define([], factory);
      } else if (typeof module === 'object' && module.exports) {
      	module.exports = factory();
      } else {
      	root.rampensau = factory();
      }
    }
    (typeof self !== 'undefined' ? self : this, function() {`,
  },
  footer: {
    js: `return rampensau; }));`,
  },
  outfile: "dist/index.umd.js",
});


================================================
FILE: dist/colorUtils.d.ts
================================================
export declare type Vector2 = [number, number];
export declare type Vector3 = [...Vector2, number];
/**
 * Converts a color from HSL to HSV.
 * @param {Array} hsl - The HSL color values.
 * @returns {Array} - The HSV color values.
 */
export declare function normalizeHue(h: number): number;
/**
 * Get a more evenly distributed spectrum without the over abundance of green and ultramarine
 * https://twitter.com/harvey_rayner/status/1748159440010809665
 * @param h - The hue value to be converted 0-360
 * @returns h
 */
export declare function harveyHue(h: number): number;
export declare type colorHarmony = "complementary" | "splitComplementary" | "triadic" | "tetradic" | "pentadic" | "hexadic" | "monochromatic" | "doubleComplementary" | "compound" | "analogous";
export declare type colorHarmonyFn = (h: number) => number[];
/**
 * Generates a list of hues based on a color harmony.
 * @param {number} h - The base hue.
 * @param {colorHarmony} harmony - The color harmony.
 * @returns {Array<number>} - The list of hues.
 */
export declare const colorHarmonies: {
    [key in colorHarmony]: colorHarmonyFn;
};
export declare type uniqueRandomHuesArguments = {
    startHue?: number;
    total?: number;
    minHueDiffAngle?: number;
    rndFn?: () => number;
};
/**
 * Generates a list of unique hues.
 * @param {uniqueRandomHuesArguments} args - The arguments to generate the hues.
 * @returns {Array<number>} - The list of hues.
 */
export declare function uniqueRandomHues({ startHue, total, minHueDiffAngle, rndFn, }?: {
    startHue?: number | undefined;
    total?: number | undefined;
    minHueDiffAngle?: number | undefined;
    rndFn?: (() => number) | undefined;
}): number[];
/**
 * Converts a color from HSV to HSL.
 * @param {Array} hsv - The HSV color values.
 * @returns {Array} - The HSL color values.
 */
export declare const hsv2hsl: ([h, s, v]: [number, number, number]) => [number, number, number];
export declare type colorToCSSMode = "oklch" | "lch" | "hsl" | "hsv";
/**
 * Converts color values to a CSS color function string.
 *
 * @param {Vector3} color - Array of three color values based on the color mode.
 * @param {colorToCSSMode} mode - The color mode to use (oklch, lch, hsl, or hsv).
 * @returns {string} - The CSS color function string in the appropriate format.
 */
export declare const colorToCSS: (color: [number, number, number], mode?: colorToCSSMode) => string;


================================================
FILE: dist/core.d.ts
================================================
import type { Vector2, Vector3 } from "./colorUtils";
import type { CurveMethod } from "./utils";
export declare type ModifiedEasingFn = (x: number, fr?: number) => number;
export declare type hueArguments = {
    hStart?: number;
    hStartCenter?: number;
    hCycles?: number;
    hEasing?: ModifiedEasingFn;
};
export declare type presetHues = {
    hueList: number[];
};
export declare type saturationArguments = {
    sRange?: Vector2;
    sEasing?: ModifiedEasingFn;
};
export declare type lightnessArguments = {
    lRange?: Vector2;
    lEasing?: ModifiedEasingFn;
};
declare type BaseGenerateColorRampArgument = {
    total?: number;
    transformFn?: (hsl: Vector3, i?: number) => Vector3 | string;
} & hueArguments & saturationArguments & lightnessArguments;
export declare type GenerateColorRampArgument = BaseGenerateColorRampArgument & {
    hueList?: never;
};
export declare type GenerateColorRampArgumentFixedHues = BaseGenerateColorRampArgument & presetHues;
/**
 * Generates a color ramp based on the HSL color space.
 * @param {GenerateColorRampArgument} args - The arguments to generate the ramp.
 * @returns {Array<number>} - The color ramp.
 */
export declare function generateColorRamp({ total, hStart, hStartCenter, hEasing, hCycles, sRange, sEasing, lRange, lEasing, transformFn, hueList, }?: GenerateColorRampArgument | GenerateColorRampArgumentFixedHues): Vector3[];
export declare const generateColorRampWithCurve: ({ total, hStart, hStartCenter, hCycles, sRange, lRange, hueList, curveMethod, curveAccent, transformFn, }?: (GenerateColorRampArgument | GenerateColorRampArgumentFixedHues) & {
    curveMethod?: CurveMethod | undefined;
    curveAccent?: number | undefined;
}) => Vector3[];
export {};


================================================
FILE: dist/highlighter.html
================================================
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>RampenSau Syntax Highlighter Demo</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <style>
      body {
        margin: 0;
        padding: 0;
        font-family: system-ui, -apple-system, sans-serif;
        background: #18181a;
        color: #f8f8f2;
        min-height: 100vh;
      }
      h1 {
        text-align: center;
      }
      #syntax-container {
        transition: background-color 0.3s ease;
      }
      button {
        font-size: 1rem;
        border-radius: 0.375rem;
        transition: all 0.2s ease;
        display: block;
        width: 100%;
        margin-bottom: 2rem;
        padding: 0.5rem 1.5rem;
        color: white;
        border: none;
        border-radius: 0.25rem;
        cursor: pointer;
        font-weight: 600;
      }
      button:hover {
        opacity: 0.9;
      }
      .code-block {
        transition: background-color 0.3s ease;
        border-radius: 0.5rem;
        padding: 1.5rem;
        margin-bottom: 2rem;
      }
      pre {
        margin: 0;
        line-height: 1.5;
        font-family: monospace;
        overflow: auto;
      }
      h3 {
        margin-bottom: 1rem;
        margin-top: 0;
        font-weight: 300;
      }
      .wrapper {
        max-width: 64rem;
        margin: 0 auto;
        padding: 2rem;
      }
    </style>
  </head>
  <body>
    <div id="syntax-container"></div>
    <script type="module">
      import {
        generateColorRamp,
        generateColorRampWithCurve,
        generateColorRampParams,
        colorUtils,
        utils,
      } from "./index.mjs";

      const { uniqueRandomHues, colorHarmonies, colorToCSS, harveyHue } =
        colorUtils;

      const { scaleSpreadArray, shuffleArray } = utils;

      const random = (min, max) => Math.random() * (max - min) + min;

      function getRampensauColors(isLightMode = false, colorCount = 24) {
        return generateColorRamp({
          total: colorCount,
          hCycles: random(0.1, 2),
          hStartCenter: 0.5,
          sRange: [random(0.9, 0.7), random(0.5, 0.3)],
          lRange: isLightMode
            ? [random(0.85, 1), random(0, 0.15)]
            : [random(0, 0.15), random(0.8, 0.99)],
          lEasing: isLightMode
            ? (x) => -(Math.cos(Math.PI * x) - 1) / 2
            : (x, fr) => Math.pow(x, 1.5),
        }).map((hsl) => colorToCSS(hsl, "hsl"));
      }

      class SyntaxHighlighter {
        constructor() {
          this.colors = [];

          // Semantic color indices - organize by purpose
          this.colorMap = {
            // Background and UI elements
            background: 2,
            codeBackground: 0,
            buttonBackground: 3,

            // Syntax elements
            comment: 5,
            string: random(-20, -15),
            space: 15,
            keyword: -16,
            number: -14,
            function: -12,
            method: -10,
            property: -9,
            operator: -7,
            builtin: -5,
            variable: -3,
            default: -1,

            // Always use the last color for text
            text: -1,
          };

          // Language keywords and syntax definitions
          this.keywords = [
            "class",
            "def",
            "return",
            "for",
            "in",
            "if",
            "const",
            "let",
            "var",
            "async",
            "await",
            "try",
            "catch",
            "throw",
            "new",
            "function",
            "typeof",
            "instanceof",
            "this",
            "super",
            "extends",
            "static",
          ];
          this.builtins = [
            "console",
            "Math",
            "Object",
            "Array",
            "String",
            "Number",
            "Promise",
            "Date",
            "RegExp",
            "Map",
            "Set",
            "JSON",
            "Error",
          ];
          this.operators = [
            "+",
            "-",
            "*",
            "/",
            "%",
            "=",
            "==",
            "===",
            "!=",
            "!==",
            ">",
            "<",
            ">=",
            "<=",
            "&&",
            "||",
            "!",
            "??",
            "?.",
          ];
        }
        tokenizeLine(line) {
          const tokens = [];
          let current = 0;
          let buffer = "";
          const pushBuffer = (type = "default") => {
            if (buffer) {
              if (this.keywords.includes(buffer)) {
                tokens.push({ type: "keyword", content: buffer });
              } else if (this.builtins.includes(buffer)) {
                tokens.push({ type: "builtin", content: buffer });
              } else {
                const nextChar = line[current];
                if (nextChar === "(") {
                  tokens.push({ type: "function", content: buffer });
                } else if (nextChar === ".") {
                  tokens.push({ type: "property", content: buffer });
                } else {
                  tokens.push({ type, content: buffer });
                }
              }
              buffer = "";
            }
          };
          while (current < line.length) {
            if (line[current] === '"' || line[current] === "'") {
              pushBuffer();
              const quote = line[current];
              let string = quote;
              current++;
              while (current < line.length && line[current] !== quote) {
                string += line[current];
                current++;
              }
              if (current < line.length) {
                string += quote;
                current++;
              }
              tokens.push({ type: "string", content: string });
              continue;
            }
            if (line[current] === "/" && line[current + 1] === "/") {
              pushBuffer();
              tokens.push({ type: "comment", content: line.slice(current) });
              break;
            }
            const possibleOperator = this.operators.find((op) =>
              line.slice(current).startsWith(op)
            );
            if (possibleOperator) {
              pushBuffer();
              tokens.push({ type: "operator", content: possibleOperator });
              current += possibleOperator.length;
              continue;
            }
            if (line[current] === ".") {
              pushBuffer();
              tokens.push({ type: "operator", content: "." });
              current++;
              while (current < line.length && /[\w$_]/.test(line[current])) {
                buffer += line[current];
                current++;
              }
              if (buffer) {
                tokens.push({ type: "method", content: buffer });
                buffer = "";
              }
              continue;
            }
            if (/[0-9]/.test(line[current])) {
              pushBuffer();
              let number = "";
              while (current < line.length && /[0-9.]/.test(line[current])) {
                number += line[current];
                current++;
              }
              tokens.push({ type: "number", content: number });
              continue;
            }
            if (/\s/.test(line[current])) {
              pushBuffer();
              let spaces = "";
              while (current < line.length && /\s/.test(line[current])) {
                spaces += line[current] === " " ? "·" : line[current];
                current++;
              }
              tokens.push({ type: "space", content: spaces });
              continue;
            }
            if (/[\w$_]/.test(line[current])) {
              buffer += line[current];
              current++;
              continue;
            }
            pushBuffer();
            tokens.push({ type: "operator", content: line[current] });
            current++;
          }
          pushBuffer();
          return tokens;
        }
        createTokenSpan(token) {
          const span = document.createElement("span");
          span.textContent = token.content;
          let colorIndex = this.colorMap[token.type];
          if (typeof colorIndex === "undefined")
            colorIndex = this.colorMap.default;
          // Support negative indices for end of array
          const idx =
            colorIndex < 0 ? this.colors.length + colorIndex : colorIndex;
          span.style.color =
            this.colors[idx] || this.colors[this.colors.length - 1];
          return span;
        }
        renderLine(line) {
          const div = document.createElement("div");
          const tokens = this.tokenizeLine(line);
          tokens.forEach((token) => {
            div.appendChild(this.createTokenSpan(token));
          });
          return div;
        }

        createCodeBlocks() {
          const codeContainer = document.createElement("div");
          codeContainer.style.display = "flex";
          codeContainer.style.flexDirection = "column";
          codeContainer.style.gap = "2rem";
          codeContainer.appendChild(
            this.createCodeBlock("JavaScript Example", javascriptCode)
          );
          codeContainer.appendChild(
            this.createCodeBlock("Python Example", pythonCode)
          );
          return codeContainer;
        }

        createShuffleButton() {
          const shuffleButton = document.createElement("button");
          shuffleButton.textContent = "Generate Theme";
          shuffleButton.style.background = this.colors[3];
          shuffleButton.style.color =
            this.colors[this.colors.length - 1] || "#fff";
          shuffleButton.addEventListener("click", () => {
            this.shuffle();
            this.updateColors();
          });
          return shuffleButton;
        }

        applyContainerStyles(container) {
          container.style.background = this.colors[2];
          container.style.color = this.colors[this.colors.length - 1];
          return container;
        }

        render(container) {
          container.innerHTML = "";
          container.className = "";

          // Apply base styles
          this.applyContainerStyles(container);

          // Create and style wrapper
          const wrapper = document.createElement("div");
          wrapper.className = "wrapper";
          wrapper.style.color = this.colors[this.colors.length - 1];

          const title = document.createElement("h1");
          title.textContent = "RampenSau Syntax Highlighter";
          title.style.color = "currentColor";
          // Add button and code blocks
          wrapper.appendChild(title);
          wrapper.appendChild(this.createShuffleButton());
          wrapper.appendChild(this.createCodeBlocks());

          // Add everything to container
          container.appendChild(wrapper);
        }

        createCodeBlock(title, code) {
          const block = document.createElement("div");
          block.classList.add("code-block");
          block.style.background = this.colors[0];
          block.style.color = this.colors[this.colors.length - 1];
          const heading = document.createElement("h3");
          heading.textContent = title;
          heading.style.color = this.colors[this.colors.length - 1];
          block.appendChild(heading);
          const pre = document.createElement("pre");
          pre.style.color = this.colors[this.colors.length - 1];
          code.split("\n").forEach((line) => {
            pre.appendChild(this.renderLine(line));
          });
          block.appendChild(pre);
          return block;
        }

        shuffle() {
          this.colors = getRampensauColors(random(0, 1) < 0.5, 40);
        }

        updateColors() {
          const container = document.querySelector("#syntax-container");
          this.applyContainerStyles(container);

          const wrapper = container.querySelector(".wrapper");
          wrapper.style.color = this.colors[this.colors.length - 1];

          // Update button styles
          const button = wrapper.querySelector("button");
          button.style.background = this.colors[3];
          button.style.color = this.colors[this.colors.length - 1];

          // Replace code container with new one
          const oldCodeContainer = wrapper.querySelector("div");
          const newCodeContainer = this.createCodeBlocks();
          wrapper.replaceChild(newCodeContainer, oldCodeContainer);
        }
      }

      const pythonCode = `class DataProcessor:
    def __init__(self, data: List[Dict]):
        self.data = data
        self._processed = False

    def process(self) -> None:
        """Process the data and update internal state."""
        for item in self.data:
            if item['status'] == 'active':
                self._transform(item)
        self._processed = True

    @property
    def is_processed(self) -> bool:
        return self._processed`;

      const javascriptCode = `const fetchUserData = async (userId) => {
  try {
    const response = await api.get(${"`/users/${userId}`"});
    const { data } = response;
    
    if (!data.isActive) {
      throw new Error('User account inactive');
    }

    return {
      ...data,
      lastLogin: new Date(data.lastLogin)
    };
  } catch (error) {
    console.error('Failed to fetch user:', error);
    return null;
  }
};`;

      // Mount highlighter
      const container = document.getElementById("syntax-container");
      const highlighter = new SyntaxHighlighter();
      highlighter.shuffle();
      highlighter.render(container);
    </script>
  </body>
</html>


================================================
FILE: dist/index.cjs
================================================
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
  if (from && typeof from === "object" || typeof from === "function") {
    for (let key of __getOwnPropNames(from))
      if (!__hasOwnProp.call(to, key) && key !== except)
        __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
  }
  return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

// src/index.ts
var index_exports = {};
__export(index_exports, {
  colorUtils: () => colorUtils_exports,
  generateColorRamp: () => generateColorRamp,
  generateColorRampParams: () => generateColorRampParams,
  generateColorRampWithCurve: () => generateColorRampWithCurve,
  utils: () => utils_exports
});
module.exports = __toCommonJS(index_exports);

// src/utils.ts
var utils_exports = {};
__export(utils_exports, {
  lerp: () => lerp,
  makeCurveEasings: () => makeCurveEasings,
  pointOnCurve: () => pointOnCurve,
  scaleSpreadArray: () => scaleSpreadArray,
  shuffleArray: () => shuffleArray
});
function shuffleArray(array, rndFn = Math.random) {
  const copy = [...array];
  let currentIndex = copy.length, randomIndex;
  while (currentIndex != 0) {
    randomIndex = Math.floor(rndFn() * currentIndex);
    currentIndex--;
    [copy[currentIndex], copy[randomIndex]] = [
      copy[randomIndex],
      copy[currentIndex]
    ];
  }
  return copy;
}
var lerp = (amt, from, to) => from + amt * (to - from);
var scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {
  if (!valuesToFill || valuesToFill.length < 2) {
    throw new Error("valuesToFill array must have at least two values.");
  }
  if (targetSize < 1 && padding > 0) {
    throw new Error("Target size must be at least 1");
  }
  if (targetSize < valuesToFill.length && padding === 0) {
    throw new Error(
      "Target size must be greater than or equal to the valuesToFill array length."
    );
  }
  const result = new Array(targetSize);
  if (padding <= 0) {
    const len = valuesToFill.length;
    const lastIdx = len - 1;
    const totalAdded = targetSize - len;
    const baseAdds = Math.floor(totalAdded / lastIdx);
    const remainder = totalAdded % lastIdx;
    let currentResultIdx = 0;
    for (let i = 0; i < lastIdx; i++) {
      const startVal = valuesToFill[i];
      const endVal = valuesToFill[i + 1];
      const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);
      for (let j = 0; j < segmentLen; j++) {
        const t = j / segmentLen;
        result[currentResultIdx++] = fillFunction(t, startVal, endVal);
      }
    }
    result[currentResultIdx] = valuesToFill[lastIdx];
    return result;
  }
  const domainStart = padding;
  const domainEnd = 1 - padding;
  const lenMinus1 = valuesToFill.length - 1;
  const normalizedPositions = new Float64Array(valuesToFill.length);
  for (let i = 0; i < valuesToFill.length; i++) {
    normalizedPositions[i] = i / lenMinus1;
  }
  let segmentIndex = 0;
  for (let i = 0; i < targetSize; i++) {
    const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);
    const adjustedT = domainStart + t * (domainEnd - domainStart);
    while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {
      segmentIndex++;
    }
    const segmentStart = normalizedPositions[segmentIndex];
    const segmentEnd = normalizedPositions[segmentIndex + 1];
    let segmentT = 0;
    if (segmentEnd > segmentStart) {
      segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);
    }
    const fromValue = valuesToFill[segmentIndex];
    const toValue = valuesToFill[segmentIndex + 1];
    result[i] = fillFunction(segmentT, fromValue, toValue);
  }
  return result;
};
var pointOnCurve = (curveMethod, curveAccent) => {
  return (t) => {
    const limit = Math.PI / 2;
    const slice = limit / 1;
    const percentile = t;
    let x = 0, y = 0;
    if (curveMethod === "lam\xE9") {
      const t2 = percentile * limit;
      const exp = 2 / (2 + 20 * curveAccent);
      const cosT = Math.cos(t2);
      const sinT = Math.sin(t2);
      x = Math.sign(cosT) * Math.abs(cosT) ** exp;
      y = Math.sign(sinT) * Math.abs(sinT) ** exp;
    } else if (curveMethod === "arc") {
      y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);
      x = Math.sin(Math.PI / 2 + t * slice - curveAccent);
    } else if (curveMethod === "pow") {
      x = Math.pow(1 - percentile, 1 - curveAccent);
      y = Math.pow(percentile, 1 - curveAccent);
    } else if (curveMethod === "powY") {
      x = Math.pow(1 - percentile, curveAccent);
      y = Math.pow(percentile, 1 - curveAccent);
    } else if (curveMethod === "powX") {
      x = Math.pow(percentile, curveAccent);
      y = Math.pow(percentile, 1 - curveAccent);
    } else if (typeof curveMethod === "function") {
      const [xFunc, yFunc] = curveMethod(t, curveAccent);
      x = xFunc;
      y = yFunc;
    } else {
      throw new Error(
        `pointOnCurve() curveMethod parameter is expected to be "lam\xE9" | "arc" | "pow" | "powY" | "powX" or a function but \`${curveMethod}\` given.`
      );
    }
    return { x, y };
  };
};
var makeCurveEasings = (curveMethod, curveAccent) => {
  const point = pointOnCurve(curveMethod, curveAccent);
  return {
    sEasing: (t) => point(t).x,
    lEasing: (t) => point(t).y
  };
};

// src/colorUtils.ts
var colorUtils_exports = {};
__export(colorUtils_exports, {
  colorHarmonies: () => colorHarmonies,
  colorToCSS: () => colorToCSS,
  harveyHue: () => harveyHue,
  hsv2hsl: () => hsv2hsl,
  normalizeHue: () => normalizeHue,
  uniqueRandomHues: () => uniqueRandomHues
});
function normalizeHue(h) {
  return (h % 360 + 360) % 360;
}
function harveyHue(h) {
  h = normalizeHue(h) / 360;
  if (h === 1 || h === 0) return h;
  h = 1 + h % 1;
  const seg = 1 / 6;
  const a = h % seg / seg * Math.PI / 2;
  const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];
  const i = Math.floor(h * 6);
  const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];
  return cases[i % 6] * 360;
}
var colorHarmonies = {
  complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],
  splitComplementary: (h) => [
    normalizeHue(h),
    normalizeHue(h + 150),
    normalizeHue(h - 150)
  ],
  triadic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 120),
    normalizeHue(h + 240)
  ],
  tetradic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 90),
    normalizeHue(h + 180),
    normalizeHue(h + 270)
  ],
  pentadic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 72),
    normalizeHue(h + 144),
    normalizeHue(h + 216),
    normalizeHue(h + 288)
  ],
  hexadic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 60),
    normalizeHue(h + 120),
    normalizeHue(h + 180),
    normalizeHue(h + 240),
    normalizeHue(h + 300)
  ],
  monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],
  // min 2 for RampenSau
  doubleComplementary: (h) => [
    normalizeHue(h),
    normalizeHue(h + 180),
    normalizeHue(h + 30),
    normalizeHue(h + 210)
  ],
  compound: (h) => [
    normalizeHue(h),
    normalizeHue(h + 180),
    normalizeHue(h + 60),
    normalizeHue(h + 240)
  ],
  analogous: (h) => [
    normalizeHue(h),
    normalizeHue(h + 30),
    normalizeHue(h + 60),
    normalizeHue(h + 90),
    normalizeHue(h + 120),
    normalizeHue(h + 150)
  ]
};
function uniqueRandomHues({
  startHue = 0,
  total = 9,
  minHueDiffAngle = 60,
  rndFn = Math.random
} = {}) {
  minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);
  const baseHue = startHue ?? rndFn() * 360;
  const huesToPickFrom = Array.from(
    {
      length: Math.round(360 / minHueDiffAngle)
    },
    (_, i) => (baseHue + i * minHueDiffAngle) % 360
  );
  let randomizedHues = shuffleArray(huesToPickFrom, rndFn);
  if (randomizedHues.length > total) {
    randomizedHues = randomizedHues.slice(0, total);
  }
  return randomizedHues;
}
var hsv2hsl = ([h, s, v]) => {
  const l = v - v * s / 2;
  const m = Math.min(l, 1 - l);
  const s_hsl = m === 0 ? 0 : (v - l) / m;
  return [h, s_hsl, l];
};
var colorModsCSS = {
  oklch: (color) => [
    color[2] * 100 + "%",
    color[1] * 100 + "%",
    color[0]
  ],
  lch: (color) => [
    color[2] * 100 + "%",
    color[1] * 100 + "%",
    color[0]
  ],
  hsl: (color) => [
    color[0],
    color[1] * 100 + "%",
    color[2] * 100 + "%"
  ],
  hsv: (color) => {
    const [h, s, l] = hsv2hsl(color);
    return [h, s * 100 + "%", l * 100 + "%"];
  }
};
var colorToCSS = (color, mode = "oklch") => {
  const cssMode = mode === "hsv" ? "hsl" : mode;
  return `${cssMode}(${colorModsCSS[mode](color).join(" ")})`;
};

// src/core.ts
function generateColorRamp({
  total = 9,
  hStart = Math.random() * 360,
  hStartCenter = 0.5,
  hEasing = (x) => x,
  hCycles = 1,
  sRange = [0.4, 0.35],
  sEasing = (x) => Math.pow(x, 2),
  lRange = [Math.random() * 0.1, 0.9],
  lEasing = (x) => Math.pow(x, 1.5),
  transformFn = ([h, s, l]) => [h, s, l],
  hueList
} = {}) {
  const lDiff = lRange[1] - lRange[0];
  const sDiff = sRange[1] - sRange[0];
  const length = hueList && hueList.length > 0 ? hueList.length : total;
  return Array.from({ length }, (_, i) => {
    const relI = length > 1 ? i / (length - 1) : 0;
    const fraction = 1 / length;
    const hue = hueList ? hueList[i] : normalizeHue(
      hStart + // Add the starting hue
      (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)
      // Calculate the hue based on the easing function
    );
    const saturation = sRange[0] + sDiff * sEasing(relI, fraction);
    const lightness = lRange[0] + lDiff * lEasing(relI, fraction);
    return transformFn([hue, saturation, lightness], i);
  });
}
var generateColorRampWithCurve = ({
  total = 9,
  hStart = Math.random() * 360,
  hStartCenter = 0.5,
  hCycles = 1,
  sRange = [0.4, 0.35],
  lRange = [Math.random() * 0.1, 0.9],
  hueList,
  curveMethod = "lam\xE9",
  curveAccent = 0.5,
  transformFn = ([h, s, l]) => [h, s, l]
} = {}) => {
  const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);
  return generateColorRamp({
    total,
    hStart,
    hStartCenter,
    hCycles,
    sRange,
    lRange,
    sEasing,
    lEasing,
    transformFn,
    hueList
  });
};

// src/index.ts
var generateColorRampParams = {
  total: {
    default: 5,
    props: { min: 4, max: 50, step: 1 }
  },
  hStart: {
    default: 0,
    props: { min: 0, max: 360, step: 0.1 }
  },
  hCycles: {
    default: 1,
    props: { min: -2, max: 2, step: 1e-3 }
  },
  hStartCenter: {
    default: 0.5,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  minLight: {
    default: Math.random() * 0.2,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  maxLight: {
    default: 0.89 + Math.random() * 0.11,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  minSaturation: {
    default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  maxSaturation: {
    default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  curveMethod: {
    default: "lam\xE9",
    props: { options: ["lam\xE9", "sine", "power", "linear"] }
  },
  curveAccent: {
    default: 0.5,
    props: { min: 0, max: 5, step: 0.01 }
  }
};


================================================
FILE: dist/index.d.ts
================================================
export * as utils from "./utils";
export * as colorUtils from "./colorUtils";
export { generateColorRamp, generateColorRampWithCurve } from "./core";
/**
 * A set of default parameters and sane ranges to use with `generateColorRamp`
 * when coming up with random color ramps.
 */
export declare const generateColorRampParams: {
    total: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    hStart: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    hCycles: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    hStartCenter: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    minLight: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    maxLight: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    minSaturation: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    maxSaturation: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
    curveMethod: {
        default: string;
        props: {
            options: string[];
        };
    };
    curveAccent: {
        default: number;
        props: {
            min: number;
            max: number;
            step: number;
        };
    };
};


================================================
FILE: dist/index.html
================================================
<!DOCTYPE html>

<html lang="en">
  <head>
    <meta charset="utf-8" />
    <!-- Primary Meta Tags -->
    <title>
      RampenSau — Color ramp generator using curves within the HSL color model
    </title>
    <meta
      name="title"
      content="RampenSau — Color ramp generator using curves within the HSL color model"
    />
    <meta
      name="description"
      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."
    />
    <meta
      name="keywords"
      content="color, colour, generative, generative-art, generative-design, palette, colorpalette"
    />

    <!-- Open Graph / Facebook -->
    <meta property="og:type" content="website" />
    <meta property="og:url" content="https://meodai.github.io/rampensau/" />
    <meta
      property="og:title"
      content="RampenSau — Color ramp generator using curves within the HSL color model"
    />
    <meta
      property="og:description"
      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."
    />
    <meta
      property="og:image"
      content="https://meodai.github.io/rampensau/socialfb.png"
    />

    <!-- Twitter -->
    <meta property="twitter:card" content="summary_large_image" />
    <meta
      property="twitter:url"
      content="https://meodai.github.io/rampensau/"
    />
    <meta
      property="twitter:title"
      content="RampenSau — Color ramp generator using curves within the HSL color model"
    />
    <meta
      property="twitter:description"
      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."
    />
    <meta
      property="twitter:image"
      content="https://meodai.github.io/rampensau/socialfb.png"
    />

    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="icon" type="image/png" />
    <link rel="preconnect" href="https://fonts.googleapis.com" crossorigin />

    <link
      href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:opsz,wght@12..96,400;12..96,800&display=swap"
      rel="stylesheet"
    />
    <link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/meyer-reset/2.0/reset.min.css"
    />
    <script type="module">
      // Pre-load token-beam for use later
      window.tokenBeamPromise = import('https://esm.sh/token-beam@1.6.0/es2022/token-beam.mjs');
    </script>
    <style>
      :root {
        font-family: "Bricolage Grotesque", sans-serif;
        --light: var(--clast);
        --dark: var(--c0);
        --highlight: var(--csecondlast);

        background: var(--dark);
        color: var(--dark);

        --sidebarwidth: max(22rem, 20vw);

        scrollbar-color: var(--dark) var(--light);
        scrollbar-width: thin;
      }

      ::selection {
        background: var(--dark);
        color: var(--highlight);
      }

      a,
      [data-code] i {
        color: var(--highlight);
      }

      .ellogo__font {
        fill: var(--light);
      }

      .ellogo__logo {
        stroke: var(--highlight);
      }

      .sidebar {
        position: fixed;
        width: var(--sidebarwidth);
        order: 1;
        left: 0;
        top: 0;
        bottom: 0;

        z-index: 2;

        border-right: 1px solid var(--light);
      }

      .sidebar__button {
        display: none;
        position: absolute;
        right: 0;
        top: 0;
        transform: translate(calc(100% + 1em), 1em);
        width: 1.6rem;
        height: 1.3rem;
        --line-color: var(--dark);
      }

      .sidebar__button i {
        position: absolute;
        top: 0;
        left: 0;
        right: 0;
        height: 2px;
        background: var(--line-color);
      }

      .sidebar__button i:nth-child(2) {
        top: 50%;
        transform: translateY(-50%);
      }

      .sidebar__button i:nth-child(3) {
        bottom: 0;
        top: auto;
      }

      .code-title {
        margin-top: 0;
      }

      .settings {
        position: absolute;
        padding: 2rem 2rem 0;
        background: var(--dark);
        color: var(--light);
        inset: 0;
        overflow-y: auto;
        box-sizing: border-box;

        /* style scrollbar */
        scrollbar-color: var(--highlight) var(--dark);
        scrollbar-width: thin;

        overscroll-behavior: contain;
        scroll-behavior: smooth;
      }

      .button {
        display: block;
        margin-bottom: 1rem;
        padding: 0.8rem 1rem 0.65rem;
        color: var(--highlight);
        border: 2px solid currentColor;
        display: block;
        width: 100%;
        font-weight: 900;
      }

      .button--main {
        background: var(--dark);
        color: var(--light);
        border-color: var(--light);
        display: none;
      }

      @media (max-width: 500px) {
        .button--main {
          display: block;
        }
      }

      .button:hover {
        background: var(--highlight);
        color: var(--dark);
        border-color: var(--highlight);
      }

      .projectlink {
        display: inline-block;
        color: var(--dark);
        margin-top: 0.75em;
        font-size: 0.8em;
        position: relative;
        z-index: 10;
        text-decoration: none;
      }

      .projectlink::after {
        content: "↗";
        display: inline-block;
        margin-left: 0.5em;
        font-size: 0.8em;
      }

      figcaption {
        display: none;
      }

      .tabs {
        width: 100%;
        background: var(--dark);
        box-shadow: 0 0 0 2px var(--dark), 0 0 0.6rem var(--dark),
          0 -2rem 0 2rem var(--dark);
      }

      .tabs__contents {
        border: 2px solid var(--light);
        overflow: hidden;
        background: var(--dark);
        z-index: 1;
        position: relative;
      }

      .tabs__slider {
        display: flex;
        gap: 0;
        width: calc(var(--tabs-length, 1) * 100%);
        transform: translateX(
          calc(var(--active-tab, 0) * calc(-100% / var(--tabs-length, 1)))
        );
        transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1);
      }

      .tabs__content {
        flex: 0 0 calc(100% / var(--tabs-length, 1));
        width: calc(100% / var(--tabs-length, 1));
        box-sizing: border-box;
        padding: 0.5rem;
        pointer-events: none;
        transform: scale(0.8);
        opacity: 0;

        transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1) 50ms,
          opacity 300ms cubic-bezier(0.7, 0.3, 0, 1) 50ms;

        will-change: transform, opacity;

        position: relative;
      }

      .tabs__content--active {
        pointer-events: auto;

        transform: scale(1);
        opacity: 1;

        transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1) 100ms,
          opacity 300ms cubic-bezier(0.7, 0.3, 0, 1) 100ms;
      }

      .tabs__controls {
        display: flex;
        margin-top: -2px;
      }

      .tabs__control {
        border: 2px solid var(--light);
        padding: 0.8rem 0.5rem;

        font-size: 0.8rem;
        font-weight: 800;
        line-height: 1;

        display: flex;
        align-items: center;
        justify-content: center;
        flex-grow: 1;

        cursor: pointer;
      }

      .tabs__control:hover,
      .tabs__control--active {
        background: var(--light);
        color: var(--dark);
      }

      .tabs__control--active {
        z-index: 1;
      }

      .tabs__control + .tabs__control {
        margin-left: -2px;
      }

      .tabs__icon {
        display: inline-block;
        height: 1.25em;
        aspect-ratio: 1;
        margin-right: 0.5em;
      }

      .tabs__label {
        display: inline-block;
        font-weight: 800;
        line-height: 1;
        margin-top: 0.1em;
        white-space: nowrap;
        overflow: hidden;
        text-overflow: ellipsis;
      }

      .icon::before {
        content: "";
        position: absolute;
        inset: 0;
      }

      .icon {
        position: relative;
      }

      .icon--hue::before {
        border-radius: 50%;
        box-shadow: inset 0 0 0 1.5px currentColor;
      }

      .icon--sl::before {
        box-shadow: inset 0 0 0 1.5px currentColor;
      }

      .icon--fncall::before {
        content: "{}";
        font-weight: 100;
        font-size: 1.25em;
        line-height: 1;
      }

      .disc--mini {
        position: absolute;
        top: 50%;
        left: 50%;
        height: calc(100% + 3px);
        width: 1px;

        transform: translate(-50%, -50%) rotate(calc(var(--c-h) * 1deg));
      }

      .disc--mini::before {
        position: absolute;
        content: "";
        top: 0;
        left: 50%;
        width: 100%;
        height: 1px;
        background: var(--light);
      }

      .disc--mini-sl {
        left: calc(4px + var(--c-s) * calc(100% - 8px));
        top: calc(4px + (1 - var(--c-l)) * calc(100% - 8px));

        position: absolute;
        width: 1.5px;
        height: 1.5px;
        transform: translate(-50%, -50%);
      }

      .disc--mini-sl::before {
        position: absolute;
        width: 1px;
        height: 1px;
      }

      .settings__top {
        margin-bottom: 2rem;
        padding: 2rem;
        padding-top: 0.5rem;
        margin: -2rem -2rem 2rem;

        position: sticky;
        top: 0;
        z-index: 1;
      }

      @media (max-width: 500px) {
        .settings__top {
          padding-top: 1.5rem;
        }
      }

      .h-viz {
        position: relative;
        aspect-ratio: 1;
        perspective: 400px;
        will-change: perspective;
        transition: perspective 300ms cubic-bezier(0.7, 0.3, 0, 1);
      }

      .h-viz:hover {
        perspective: 1000px;
      }

      .h-viz .disc {
        position: absolute;
        bottom: 50%;
        left: 50%;
        width: 7rem;
        height: 7rem;

        transform: translate(-50%, 100%) translateY(calc(-100% * var(--c-i)))
          rotateX(34deg) scale(calc(sin(0.1 + var(--c-i2) * 1.5))) scale(1.5);
        transition: calc(300ms - 50ms + var(--c-i2) * 200ms) transform
          cubic-bezier(0.175, 0.885, 0.32, 1.275);
      }

      .h-viz .disc::before {
        content: "";
        position: absolute;
        top: 0;
        left: 0;
        width: 100%;
        height: 100%;
        border-radius: 50%;
        background: conic-gradient(
          hsl(360, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(315, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(270, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(225, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(180, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(135, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(90, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(45, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%)),
          hsl(0, calc(var(--c-s) * 100%), calc(var(--c-l) * 100%))
        );
        transform: scaleX(-1); /* lazy flip the gradient */
        opacity: 0.55;
      }

      .h-viz .disc::after {
        content: "";
        position: absolute;
        top: 50%;
        left: 50%;
        width: 0.6rem;
        border-radius: 50%;
        height: 0.6rem;
        transform: translate(-50%, -50%) rotate(calc(var(--c-h) * 1deg))
          translateY(-3.3rem) scale(calc(3 - var(--c-i2) * 4.5));
        transform-origin: 50% 50%;
        box-shadow: 0 0 0 1px var(--dark),
          0 0 0 2px
            hsl(var(--c-h), calc(var(--c-s) * 100%), calc(var(--c-l) * 100%));
        background: hsl(
          var(--c-h),
          calc(var(--c-s) * 100%),
          calc(var(--c-l) * 100%)
        );
        transition: calc(300ms - 50ms + var(--c-i2) * 200ms) transform
          cubic-bezier(0.175, 0.885, 0.32, 1.275);
        transition-duration: calc(100ms + var(--c-i2) * 200ms);
      }

      .h-viz:hover .disc {
        transform: translate(-50%, 100%) translateY(calc(-100% * var(--c-i)))
          rotateX(34deg) scale(1);
      }
      .h-viz:hover .disc::after {
        transform: translate(-50%, -50%) rotate(calc(var(--c-h) * 1deg))
          translateY(-3.3rem) scale(1);
      }
      .h-viz:hover .disc::before {
        mask-image: radial-gradient(
          circle farthest-side at center,
          transparent 90%,
          white 70%
        );
        -webkit-mask-image: radial-gradient(
          circle farthest-side at center,
          transparent 90%,
          white 70%
        );
      }

      .sl-viz {
        position: relative;
        aspect-ratio: 1;
      }

      .sl-viz .disc--sl {
        position: absolute;
        width: 100%;
        height: 100%;
      }

      .sl-viz .disc--sl::after {
        content: "";
        position: absolute;
        inset: 0;
      }

      .sl-viz .disc--sl::before {
        left: calc(5% + var(--c-s) * 90%);
        top: calc(5% + (1 - var(--c-l)) * 90%);

        content: "";
        position: absolute;

        width: 0.6rem;
        aspect-ratio: 1;
        border-radius: 50%;

        box-shadow: 0 0 0 1px var(--light),
          0 0 0 2px
            hsl(var(--c-h), calc(var(--c-s) * 100%), calc(var(--c-l) * 100%));
        background: hsl(
          var(--c-h),
          calc(var(--c-s) * 100%),
          calc(var(--c-l) * 100%)
        );
        transform: translate(-50%, -50%);
      }

      .main {
        margin-left: var(--sidebarwidth);
        background: var(--light);
        padding: 4rem;
      }

      .section {
        display: flex;
        margin-top: 4rem;
        max-width: calc(50rem + 12.5vw);
      }
      .section--first {
        margin-top: 0;
      }
      .section--vertical {
        flex-direction: column;
      }
      .section__text {
        flex: 1 0 calc(100% - 25rem - 4rem);
        order: 1;
        width: calc(100% - 25rem - 4rem);
      }
      .section--vertical .section__text {
        flex: 1 0 calc(100% - 10rem - 4rem);
        order: 1;
        width: calc(100% - 10rem - 4rem);
      }

      .section__fig {
        flex: 1 1 25rem;
        order: 0;
        margin-right: 4rem;
      }
      .section code {
        font-family: monospace;
        background: var(--dark);
        color: var(--light);
        padding: 0 0.75ex 0.2ex;
      }

      .intro {
        margin-bottom: 4rem;
      }

      h1 {
        font-size: calc(0.64rem + 5vw);
        font-weight: 900;
        letter-spacing: -0.025em;
        margin-top: 0;
        line-height: 0.85;
        margin-left: -0.045em;
        margin-bottom: 2rem;
        text-shadow: var(--tsss);
        transition: text-shadow 300ms cubic-bezier(0.7, 0.3, 0, 1);
      }

      h1:hover {
        text-shadow: var(--ts);
      }

      h2,
      h3,
      [data-names] strong {
        margin: 0 0 2rem;
        font-size: calc(1.5rem + 2vw);
        font-weight: 800;
        letter-spacing: -0.02em;
        line-height: 0.9;
      }

      h2 + p {
        margin-bottom: 1rem;
      }

      h2 span {
        font-size: 0.9rem;
        line-height: 1.2;
        font-weight: 400;
      }

      h3 {
        margin-top: 1.5em;
        font-size: calc(0.5rem + 1vw);
      }
      pre {
        font-family: monospace;
        font-size: 0.75rem;
        max-width: 100%;
        overflow: hidden;
      }

      .fncall {
        position: absolute;
        inset: 0;
        padding: 2rem;
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
      }

      .code-sample {
        display: block;
        max-width: 100%;
      }

      a {
        font-size: 1em;
        margin-top: 0.4em;
        font-weight: 700;
      }

      button {
        display: block;
        background: none;
        padding: 0;
        border: none;
        border-radius: 0;
        font: inherit;
        font-size: inherit;
        color: inherit;
      }
      .main button:not(.button) {
        cursor: pointer;
        font-weight: bold;
        display: inline;
        text-decoration: underline;
        margin: 0 0.2em;
      }

      .main button:not(.button):hover {
        text-decoration-color: var(--highlight);
      }

      [data-colors] {
        position: relative;
        display: flex;
        flex-wrap: wrap;
        width: 100%;
        height: calc(100vmin - 8rem);
        margin-bottom: 4rem;
        flex-direction: column-reverse;
        box-shadow: 0 0 0 2px var(--dark);
        transition: box-shadow 300ms cubic-bezier(0.7, 0.3, 0, 1);
        transition-delay: 300ms;
        background: var(--dark);
        overflow: hidden;
      }
      [data-colors]::after {
        content: "";
        position: absolute;
        inset: -1vmin;
        background: linear-gradient(0deg, var(--gradient));
        opacity: 1;
        z-index: 1;
        filter: blur(10vmin);
        transform: scale(0.9) translateX(-50%);
        transition: opacity 200ms linear;
        transition-delay: 500ms;
        will-change: opacity;
      }
      [data-colors]:hover::after {
        opacity: 0;
        transition-delay: 0ms;
      }

      [data-colors] i {
        flex: 1 0 auto;
        width: 100%;
        background: var(--color);
        position: relative;

        font-size: calc(20vmin * 1 / var(--total));
        transition: transform 460ms cubic-bezier(0.7, 0.3, 0, 1);
        transition-delay: calc(100ms + var(--i) * 200ms);
      }
      [data-colors] span {
        position: absolute;
        left: 1em;
        top: 50%;
        background: var(--color);
        width: 0.8em;
        height: 0.8em;
        border-radius: 50%;
        transform: translateY(-50%) rotate(calc(var(--h) * 1deg));
        z-index: 2;
        box-shadow: 0 0 0 2px var(--c);
        overflow: hidden;
        display: none;
      }
      [data-colors] span::before {
        content: "";
        position: absolute;
        inset: 0;
        transform-origin: 50% 50%;
        transform: rotate(calc(var(--h) * -1deg))
          translateY(calc(-1 * var(--l) * 100%));
        background: var(--c);
        display: none;
      }
      [data-colors] span::after {
        content: "";
        position: absolute;
        top: 0;
        left: 50%;
        width: 30%;
        aspect-ratio: 1;
        background: var(--c);
        transform: translate(-50%, -50%) rotate(45deg);
      }

      [data-colors] b {
        position: absolute;
        top: 50%;
        right: 1em;
        transform: translateY(-50%);
        color: var(--c);
        font-weight: 800;
        filter: hue-rotate(180deg);
        z-index: 2;
      }

      [data-palette] {
        display: flex;
        flex-wrap: wrap;
        gap: 2px;
        cursor: pointer;
      }
      [data-palette] .palette-sample {
        --rotation: round(calc(var(--rnd) * 360deg), 90deg);
      }
      .palette-sample {
        position: relative;
        background: var(--col-0);
        aspect-ratio: 1;
        margin: 0;
        user-select: none;
        flex: 0 0 calc(100% / var(--x) - 2px);
      }
      .palette-sample b {
        position: absolute;
        top: 50%;
        left: 50%;
        width: 50%;
        height: 50%;
        transform: translate(-50%, -50%) rotate(var(--rotation));
        background: var(--col-1);
      }
      .palette-sample i {
        position: absolute;
        width: 50%;
        height: 50%;
        right: 0;
      }
      .palette-sample i:first-child {
        background: var(--col-2);
      }
      .palette-sample i:last-child {
        bottom: 0;
        background: var(--col-3);
      }

      figure {
        margin: 0;
        padding: 0;
      }
      [data-figure] {
        background-image: linear-gradient(to top, black, rgba(0, 0, 0, 0)),
          linear-gradient(
            to left,
            hsl(var(--deg, 0deg), 100%, 50%),
            hsl(var(--deg, 0deg), 0%, 100%)
          );
      }
      [data-ramp] {
        height: 10rem;
        box-shadow: 0 0 0 2px var(--dark);
      }
      [data-list] {
        margin: 0 0 2rem;
        display: flex;
        gap: 4px;
      }

      .swatch {
        position: relative;
        font-size: 0.8rem;
        line-height: 1.2;
        padding-bottom: 0.5em;
      }

      [data-list] h3 {
        display: none;
        margin-top: 0;
      }

      .swatch::before {
        user-select: none;
        content: "";
        background: var(--col);
        display: block;
        width: 100%;
        height: 1.2em;

        box-shadow: 0 0 0 2px var(--dark);
      }

      .swatch div {
        transform: translateY(-100%);
        color: var(--coltext);
        margin-left: 0.2em;
      }

      .color-info {
        padding: 0.5rem;
      }

      .color-info strong {
        font-size: 1rem;
        display: block;
        margin-bottom: 1ex;
      }
      .color-info button {
        font-size: 0.6rem;
        margin-top: 0.5em;

        text-align: left;
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        max-width: 100%;
      }
      .color-info__contrast {
        position: absolute;
        top: 8.7rem;
        left: 0.5rem;
        color: var(--colbg);
        font-size: 0.8em;
      }
      .color-info__contrast h5 {
        display: none;
      }
      [data-list] > div {
        flex: 1;
      }
      [data-copy] {
        cursor: pointer;
      }
      p {
        margin-top: 1em;
        max-width: 42ch;
        font-size: 0.9em;
        letter-spacing: 0.002em;
        line-height: 1.45;
        font-weight: 400;
      }

      .pane__section,
      .pane__label {
        display: block;
      }

      .pane__label {
        font-size: 0.8rem;
        font-weight: 800;
        line-height: 1;
        margin-bottom: 0.3em;
      }

      .pane {
        box-sizing: border-box;
        background: var(--color-bg);
        display: block;
        cursor: default;
        background: var(--dark);
        color: var(--light);
        --size-gutter: 1rem;
        --color-inverted: var(--light);

        margin-top: calc(var(--size-gutter) * 2);
        select,
        option {
          /* windows does not apply it correctly otherwise */
          color: var(--light);
        }
      }
      .pane__section {
        display: block;
      }
      .pane__section + .pane__section {
        margin-top: calc(var(--size-gutter) * 2);
      }
      .pane__section--hidden {
        display: none;
      }
      .pane__inputs {
        display: flex;
        touch-action: manipulation;
        gap: calc(var(--size-gutter) * 0.5);
      }
      .pane__input--number {
        flex-grow: 1;
      }
      .pane .pane__input--number + input[type="text"] {
        display: block;
        flex-basis: 3.3rem;
        width: 3.3rem;
      }
      .pane__desc {
        margin: 1em 0 3em;
        font-size: 0.6em;
      }
      .pane select {
        font-size: 0.8em;
        border-radius: 2rem;
        padding: 0.2rem;
      }
      .pane input,
      .pane select {
        display: block;
        box-sizing: border-box;
        touch-action: manipulation;
        font-family: "Space Mono", monospace;
        border: none;
        width: auto;
      }
      .pane select option {
        color: var(--light);
      }
      .pane input[type="text"],
      .pane select[type="number"] {
        color: var(--color-inverted);
        background: none;
        border: none;
        text-align: right;
        font-size: 0.8em;
        flex: 0 0 3rem;
        width: 3rem;
      }
      .pane input {
        background-color: transparent;
        accent-color: var(--highlight);
      }
      .pane input[type="range"] {
        -webkit-appearance: none;
      }
      .pane input[type="range"] {
        margin: 0;
        padding-top: 0.7em;
        margin-top: -0.7em;
      }
      .pane input[type="range"]:focus {
        outline: none;
      }
      .pane input[type="range"]:hover::-webkit-slider-thumb {
        background-color: var(--color-inverted);
        clip-path: polygon(100% 0%, 0% 0%, 50% 100%, 50% 100%);
      }
      .pane input[type="range"]::-webkit-slider-runnable-track {
        width: 100%;
        height: 1rem;
        background: transparent;
        color: var(--dark);
        border-radius: 0;
        border: solid var(--light);
        border-width: 0 0 2px;
      }
      .pane input[type="range"]::-webkit-slider-thumb {
        border: 2px solid transparent;
        height: 0.75rem;
        width: 0.5rem;
        border-radius: 0;
        background: var(--light);
        -webkit-appearance: none;
        margin-top: 0.25rem;
        transition: 150ms background-color, 200ms clip-path,
          200ms -webkit-clip-path;
        clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
      }
      .pane input[type="range"]::-moz-range-track {
        width: 100%;
        height: 1rem;
        background: transparent;
        color: var(--dark);
        border-radius: 0;
        border: solid var(--light);
        border-width: 0 0 2px;
      }
      .pane input[type="range"]::-moz-range-thumb {
        border: 2px solid transparent;
        height: 0.75rem;
        width: 0.5rem;
        border-radius: 0;
        background: var(--light);
        -webkit-appearance: none;
        margin-top: 0.25rem;
        transition: 150ms background-color, 200ms clip-path,
          200ms -webkit-clip-path;
        clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
      }
      .pane input[type="range"]::-ms-track {
        width: 100%;
        height: 1rem;
        background: transparent;
        color: var(--dark);
        border-radius: 0;
        border: solid var(--light);
        border-width: 0 0 2px;
      }
      .pane input[type="range"]::-ms-fill-lower {
        background: var(--light);
        border: none;
        border-radius: 100%;
      }
      .pane input[type="range"]::-ms-fill-upper {
        background: var(--light);
        border-radius: 100%;
        box-shadow: none;
      }
      .pane input[type="range"]::-ms-thumb {
        border: 2px solid transparent;
        height: 0.75rem;
        width: 0.5rem;
        border-radius: 0;
        background: var(--light);
        -webkit-appearance: none;
        margin-top: 0.25rem;
        transition: 150ms background-color, 200ms clip-path,
          200ms -webkit-clip-path;
        clip-path: polygon(0 0, 100% 0, 100% 100%, 0% 100%);
      }
      .pane select {
        color: var(--light);
        width: 100%;
        box-sizing: border-box;
        -webkit-appearance: none;
        border: 0;
        box-shadow: 0 2px 0 0 var(--light);
        border-radius: 0;
        padding: 0.25rem 1rem 0.25rem 0rem;
        background-color: transparent;
        background-size: 1.25em 1.25em;
        background-image: conic-gradient(
          var(--light) 5%,
          transparent 0 95%,
          var(--light) 0
        );
        background-repeat: no-repeat;
        background-position: right 0% top 120%;
      }
      .pane select:focus {
        outline: none;
        background-color: transparent;
      }

      svg:not(:root) {
        overflow: visible;
      }
      [data-names] ol {
        display: flex;
        margin-top: 4rem;
        flex-wrap: wrap;
        gap: 2px;
      }

      [data-names] li {
        position: relative;
        width: 10rem;
        background: var(--light);
        color: var(--dark);
      }

      [data-names] li::before {
        content: "";
        display: block;
        padding-top: 100%;
        background: var(--col);
      }

      .color-names {
        /*position: relative;
    z-index: 10;*/
        padding: 4rem;
        padding-left: calc(var(--sidebarwidth) + 4rem);
        color: var(--light);
      }

      .color-names .section__text {
        margin-top: 4rem;
      }

      .color-names__copy {
        margin-top: 2rem;
        width: max-content;
      }

      /* Token Beam Widget Styles */
      .token-beam-widget {
        margin-top: 2rem;
        margin-bottom: 1rem;
        padding: 0;
        border: 2px solid var(--light);
        display: flex;
        align-items: center;
        max-width: 400px;
        background: var(--dark);
        position: relative;
      }

      .token-beam-widget__label {
        position: relative;
        font-size: 0.65rem;
        font-weight: 600;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        color: var(--light);
        white-space: nowrap;
        padding: 0.6rem 1.5rem 0.6rem 3em;
        line-height: 1;
        margin: 0;
      }

      .token-beam-widget__label::before {
        content: '⊷';
        font-size: 2em;
        transform: translateY(-50%) rotate(45deg);
        position: absolute;
        top: 50%;
        left: 0.5em;
      }

      .token-beam-widget__token-wrap {
        position: relative;
        flex: 1;
        display: flex;
        align-items: center;
      }

      .token-beam-widget__token {
        flex: 1;
        padding: 0.6rem;
        background: var(--dark);
        border: 2px solid var(--light);
        border-top: 0;
        border-bottom: 0;
        font-size: 0.75rem;
        letter-spacing: 0.18em;
        text-align: left;
        cursor: pointer;
        font-family: 'Courier New', monospace;
        color: var(--light);
        transition: all 0.2s ease;
      }

      .token-beam-widget__token:hover {
        background: var(--highlight);
        color: var(--dark);
      }

      .token-beam-widget__help {
        position: relative;
        display: inline-flex;
        align-items: center;
      }

      .token-beam-widget__help-btn {
        width: 1.95rem;
        height: 1.95rem;
        border-radius: 0;
        background: var(--light);
        border: 2px solid var(--light);
        color: var(--dark);
        font-size: 0.8rem;
        font-weight: 700;
        cursor: pointer;
        display: flex;
        align-items: center;
        justify-content: center;
        transition: all 0.2s ease;
      }

      .token-beam-widget__help-btn:hover {
        background: var(--highlight);
        border-color: var(--highlight);
      }

      .token-beam-widget__help-dropdown {
        position: absolute;
        bottom: calc(100% + 0.5rem);
        right: 0;
        background: var(--light);
        color: var(--dark);
        padding: 0.8rem;
        border: 2px solid var(--highlight);
        min-width: 280px;
        font-size: 0.7rem;
        line-height: 1.4;
        display: none;
        z-index: 100;
      }

      .token-beam-widget__help-dropdown::after {
        content: '';
        position: absolute;
        top: 100%;
        right: 0.5rem;
        width: 0;
        height: 0;
        border-left: 6px solid transparent;
        border-right: 6px solid transparent;
        border-top: 6px solid var(--highlight);
      }

      .token-beam-widget__help.is-open .token-beam-widget__help-dropdown {
        display: block;
      }

      .token-beam-widget__help-dropdown a {
        text-decoration: underline;
      }

      /* Hide help button when connected */
      .token-beam-widget--connected .token-beam-widget__help {
        display: none;
      }

      .token-beam-widget__unlink {
        display: inline-flex;
        align-items: center;
        gap: 0.3rem;
        background: var(--light);
        border: 2px solid var(--light);
        padding: 0.6rem;
        font-size: 0.6rem;
        text-transform: uppercase;
        letter-spacing: 0.08em;
        cursor: pointer;
        color: var(--dark);
        transition: all 0.2s ease;
        border-left: 0;
      }

      .token-beam-widget__dot {
        width: 8px;
        height: 8px;
        border-radius: 50%;
        background: #1b8a2d;
        flex-shrink: 0;
      }

      @keyframes pulse {
        0%, 100% { opacity: 1; transform: scale(1); }
        50% { opacity: 0.55; transform: scale(0.85); }
      }

      .token-beam-widget__dot--active {
        animation: pulse 2s ease infinite;
      }

      .token-beam-widget__unlink:hover {
        background: #c4342d;
        border-color: #c4342d;
        color: var(--light);
      }

      .token-beam-widget__unlink:hover .token-beam-widget__dot {
        background: var(--light);
      }

      .token-beam-widget--waiting .token-beam-widget__token {
        cursor: default;
      }

      .color-names h2 {
        color: var(--light);
      }

      .color-names h3 {
        margin-top: 4rem;
      }

      .ellogo {
        display: block;
        width: 40%;
        margin: 4rem 0 2rem;
      }

      [data-rndsamples] {
        display: flex;
        flex-wrap: wrap;
        gap: 2px;
        min-height: 100vh;
        margin-bottom: 4rem;
      }
      .palette {
        position: relative;
        aspect-ratio: 2;
        flex: 1 1 20%;
        background: var(--crnd);
        display: flex;
        align-items: center;
        justify-content: center;
      }
      .palette::after {
        content: "";
        position: absolute;
        inset: 0;
        z-index: -1;
        background: linear-gradient(90deg, var(--gc));
        transform: translateY(min(1vw, 4px));
        border-radius: 0.3rem;
      }
      .palette:hover {
        background: linear-gradient(90deg, var(--gs));
        /*box-shadow: inset 0 0 0 min(0.3vw, 2px) var(--c4), inset 0 0 0 min(0.5vw, 4px) var(--c0);*/
      }
      .palette:hover > * {
        display: none;
      }
      .palette:hover::after {
        background: var(--c4);
      }
      .palette__swatch {
        width: 16%;
        aspect-ratio: 1;
        background: var(--color);
        border-radius: 50%;
        margin-left: -8%;
        position: relative;
        left: 0.5vw;
        /*transform: rotate(-45deg);*/
        box-shadow: 0 0 0 min(0.2vw, 2px) var(--c0),
          0 0 0 min(0.4vw, 4px) var(--color);
      }

      @media (max-width: 500px) {
        .sidebar {
          width: 100%;
          order: 0;
          z-index: 2;
          transition: transform 300ms cubic-bezier(0.7, 0.3, 0, 1);
          transform: translateX(-100%);
        }

        .color-names {
          padding-left: 4rem;
        }

        .sidebar-open .sidebar {
          transform: translateX(0%);
        }
        .sidebar__button {
          display: block;
        }
        .sidebar-open .sidebar__button {
          transform: translate(calc(-100% - 0.8rem), 1em);
          --line-color: var(--light);
          z-index: 3;
        }
        .sidebar-open .sidebar__button i {
          top: 50%;
          transform: translateY(-50%);
        }
        .sidebar-open .sidebar__button i:nth-child(1) {
          transform: rotate(45deg);
        }

        .sidebar-open .sidebar__button i:nth-child(3) {
          transform: rotate(-45deg);
        }
        .sidebar-open .sidebar__button i:nth-child(2) {
          display: none;
        }
        .main {
          transform: translateX(0);
          margin-left: 0;
        }
        .section__fig {
          margin-right: 0;
        }
        .section--vertical .section__text,
        .section__text {
          flex: 0 0 100%;
          order: 1;
          width: 100%;
        }

        .palette {
          aspect-ratio: 1.5;
          flex: 1 1 40%;
        }

        .section {
          flex-direction: column;
        }

        .section h2 {
          margin-top: 1em;
        }

        .section__fig {
          flex: 0 0 100%;
        }

        [data-names] li {
          width: 45%;
        }

        .color-info__contrast {
          top: 51%;
        }
      }
    </style>
  </head>

  <body>
    <article class="app">
      <div
        class="sidebar"
        id="sidebar"
        role="complementary"
        aria-label="Settings Sidebar"
      >
        <button
          class="sidebar__button"
          aria-expanded="true"
          aria-controls="sidebar"
          aria-label="Toggle sidebar"
          tabindex="0"
        >
          <i class="line"></i>
          <i class="line"></i>
          <i class="line"></i>
        </button>
        <section class="settings" aria-label="Color Ramp Settings">
          <div class="settings__top">
            <div class="tabs" role="tablist" aria-label="Visualization Tabs">
              <div class="tabs__contents" data-tabswrap>
                <div class="tabs__slider">
                  <div
                    class="tabs__content"
                    data-tab="viz-h"
                    role="tabpanel"
                    aria-labelledby="tab-viz-h"
                  >
                    <figure data-viz class="h-viz"></figure>
                  </div>
                  <div
                    class="tabs__content"
                    data-tab="viz-sl"
                    role="tabpanel"
                    aria-labelledby="tab-viz-sl"
                  >
                    <figure data-slviz class="sl-viz"></figure>
                  </div>
                  <div
                    class="tabs__content"
                    data-tab="fncall"
                    role="tabpanel"
                    aria-labelledby="tab-fncall"
                  >
                    <aside class="fncall" aria-label="Function call">
                      <code class="code-sample">
                        <pre data-code data-copy></pre>
                      </code>
                    </aside>
                  </div>
                </div>
              </div>
              <div class="tabs__controls">
                <button
                  class="tabs__control tabs__control--active"
                  data-tabtarget="viz-h"
                  id="tab-viz-h"
                  role="tab"
                  aria-selected="true"
                  aria-controls="viz-h"
                  tabindex="0"
                >
                  <i data-icon="hue" class="tabs__icon icon icon--hue"></i>
                  <span class="tabs__label"><span>Hue</span></span>
                </button>
                <button
                  class="tabs__control"
                  data-tabtarget="viz-sl"
                  id="tab-viz-sl"
                  role="tab"
                  aria-selected="false"
                  aria-controls="viz-sl"
                  tabindex="-1"
                >
                  <i data-icon="sl" class="tabs__icon icon icon--sl"></i>
                  <span class="tabs__label"
                    ><span title="Saturation &amp; Lightness">S/L</span></span
                  >
                </button>
                <button
                  class="tabs__control"
                  data-tabtarget="fncall"
                  id="tab-fncall"
                  role="tab"
                  aria-selected="false"
                  aria-controls="fncall"
                  tabindex="-1"
                >
                  <i
                    data-icon="fncall"
                    class="tabs__icon icon icon--fncall"
                  ></i>
                  <span class="tabs__label"><span>Function</span></span>
                </button>
              </div>
            </div>
            <div class="vizes"></div>
          </div>
          <aside aria-label="settings" class="settings__inner">
            <!-- h3>Settings</h3 -->
            <button class="button" data-randomize>Randomize Settings</button>
            <div class="settings__inner">
              <div data-pane></div>
            </div>
          </aside>
          <footer>
            <a href="https://www.elastiq.ch/" hreflang="en" class="ellogo"
              ><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 352 185">
                <g fill="none" fill-rule="evenodd" transform="translate(0 6)">
                  <path
                    class="ellogo__font"
                    fill-rule="nonzero"
                    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"
                  ></path>
                  <path
                    class="ellogo__logo"
                    stroke-linecap="round"
                    stroke-linejoin="round"
                    stroke-width="11.38"
                    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"
                  ></path>
                  <path
                    class="ellogo__font"
                    fill-rule="nonzero"
                    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"
                  ></path>
                </g></svg
            ></a>
          </footer>
        </section>
      </div>
      <section class="main">
        <aside class="section section--vertical section--first">
          <aside class="intro">
            <h1>RampenSau</h1>
            <p>
              RampenSau is a lightweight, dependency-free and blazingly fast
              color generation library. It makes use of hue cycling and easing
              functions to generate pleasing color ramps.
            </p>
            <a class="projectlink" href="https://github.com/meodai/rampensau"
              >Github</a
            >
          </aside>
          <div class="section__text">
            <h2>Generating a Color-Ramp</h2>
            <p>
              The illustration above shows the full color ramp generated by the
              function.
              <code>hStart</code> [<button
                data-pantrigger="hStart"
                data-panvalue="0"
              >
                0° (red)</button
              >,
              <button data-pantrigger="hStart" data-panvalue="45">
                45° (yellow)</button
              >,
              <button data-pantrigger="hStart" data-panvalue="90">
                90° (green)</button
              >,
              <button data-pantrigger="hStart" data-panvalue="180">
                180° (teal)
              </button>
              etc.] sets the starting hue, at the darkest color in generated
              ramp.
            </p>
            <p>
              The color set using <code>hStart</code> can be positioned anywhere
              in the ramp by using <code>hStartCenter</code>.
              <button data-pantrigger="hStartCenter" data-panvalue="0">
                0
              </button>
              is the start of the ramp,
              <button data-pantrigger="hStartCenter" data-panvalue="1">
                1
              </button>
              is the end of the ramp. By default <code>hStartCenter</code> is
              set to
              <button data-pantrigger="hStartCenter" data-panvalue="0.5">
                0.5
              </button>
              which means the starting hue is in the middle of the ramp.
            </p>
            <p>
              <code>hCycles</code> [<button
                data-pantrigger="hCycles"
                data-panvalue="0"
              >
                0</button
              >,
              <button data-pantrigger="hCycles" data-panvalue="0.5">0.5</button
              >, <button data-pantrigger="hCycles" data-panvalue="1">1</button>,
              <button data-pantrigger="hCycles" data-panvalue="-0.5">
                -0.5</button
              >] Defines the about of hue variation in the ramp.
              <code>1</code> is a full cycle around the starting hue,
              <code>0.5</code> is half a cycle (complementary color),
              <code>-0.5</code> is half a cycle in the opposite direction.
            </p>
          </div>
          <figure class="section__fig">
            <div data-colors></div>
            <figcaption>Full Color Ramp</figcaption>
          </figure>
          <button class="button button--main" data-randomize>
            Randomize Settings
          </button>
        </aside>

        <aside class="section section--vertical">
          <div class="section__text">
            <h2>Randomly Generated Samples</h2>
            <p>
              All samples use the same <code>maxLight</code>,
              <code>maxLight</code>, <code>maxSaturation</code> and
              <code>minSaturation</code> values but randomizes the other
              settings.
            </p>
          </div>
          <div class="section__fig">
            <div data-rndsamples></div>
          </div>
        </aside>

        <aside class="section">
          <div class="section__text">
            <h2>Example Use</h2>
            <p>click & hold to re-generate</p>
            <p>
              Each of those squares shows four random entires from the generated
              colors. In a single square every color is unique.
            </p>
          </div>
          <div class="section__fig">
            <div data-palette></div>
          </div>
        </aside>

        <aside class="section">
          <div class="section__text">
            <h2>Color Ramp</h2>
            <p>All generated colors shown as a continuous gradient.</p>
          </div>
          <div class="section__fig">
            <div data-ramp></div>
          </div>
        </aside>

        <aside class="section">
          <div class="section__text">
            <h2>HSL Colors</h2>
            <p>Full list of the generated colors.</p>
            <p>
              We used
              <a title="culori" href="https://culorijs.org/">a library</a> to
              convert the the HSL colors into different color models. We
              deliberately choose to only deliver HSL
              <code>[0…360, 0…1, 0…1]</code> to keep the function fast and
              lightweight as possible. There are plenty of awesome color
              libraries if you need to have the colors converted to an other
              color model.
            </p>
          </div>
          <div class="section__fig">
            <div data-list></div>
          </div>
        </aside>

        <p>
          fork on
          <a href="https://github.com/meodai/rampensau">github</a> made by
          <a href="https://www.elastiq.ch/">elastiq</a>.
        </p>
      </section>
      <aside class="color-names">
        <h2>For Curious Minds <span>and Desperate Deadlines</span></h2>
        <div data-names></div>
        
        <button class="button color-names__copy" data-copy data-copycolors>
          Copy Palette to Clipboard
        </button>
        
        <!-- Token Beam Widget -->
        <div class="token-beam-widget token-beam-widget--waiting" data-token-beam-widget>
          <h4 class="token-beam-widget__label">Token Beam</h4>
          <div class="token-beam-widget__token-wrap" data-token-wrap>
            <button class="token-beam-widget__token" data-token-button>
              Generating Token...
            </button>
          </div>
          <div class="token-beam-widget__help" data-help>
            <button class="token-beam-widget__help-btn" data-help-btn title="What is this?">?</button>
            <div class="token-beam-widget__help-dropdown" data-help-dropdown>
              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.
              <br><br>
              <a href="https://github.com/meodai/token-beam" target="_blank">Learn more →</a>
            </div>
          </div>
        </div>
        
        <div class="section__text">
          <p>
            I got requests to export palettes—so here you go. Still, I’d gently
            suggest looking into integrating the library or
            <a href="https://meodai.github.io/poline/">something similar</a>
            into your workflow.
          </p>
          <p>
            A palette can be more than just colors—it can be a living system.
            One that responds to different data, brands, themes, or even future
            you, exploring new directions without starting from scratch.
          </p>
          <p>
            Thinking in systems doesn’t limit creativity—it gives it room to
            grow. There’s a quiet kind of magic in flexible tools.
          </p>
        </div>
      </aside>
    </article>
    <script src="https://cdn.jsdelivr.net/npm/culori@4.0.1/bundled/culori.umd.js"></script>

    <script type="module">
      import {
        generateColorRamp,
        generateColorRampWithCurve,
        generateColorRampParams,
        colorUtils,
        utils,
      } from "./index.mjs";

      const { uniqueRandomHues, colorHarmonies, colorToCSS, harveyHue } =
        colorUtils;

      const { scaleSpreadArray, shuffleArray } = utils;

      import { rybHsl2rgb } from "https://esm.sh/rybitten/";

      console.clear();

      const $favicon = document.querySelector('link[rel="icon"]');
      const $favCanvas = document.createElement("canvas");
      const favCtx = $favCanvas.getContext("2d");

      function updateFavicon(colors) {
        $favCanvas.width = 64;
        $favCanvas.height = 64;
        favCtx.clearRect(0, 0, 64, 64);
        colors.forEach((c, i) => {
          favCtx.fillStyle = c;
          favCtx.fillRect(0, (i * 64) / colors.length, 64, 64 / colors.length);
        });
        // add a border
        favCtx.strokeStyle = "#000";
        favCtx.lineWidth = 2;
        favCtx.strokeRect(2, 2, 60, 60);
        $favicon.href = $favCanvas.toDataURL();
      }

      const easingFunctions = {
        linear: (x) => x,
        easeInSine: (x) => 1 - Math.cos((x * Math.PI) / 2),
        easeOutSine: (x) => Math.sin((x * Math.PI) / 2),
        easeInOutSine: (x) => -(Math.cos(Math.PI * x) - 1) / 2,
        easeInQuad: (x) => x * x,
        easeOutQuad: (x) => 1 - (1 - x) * (1 - x),
        easeInOutQuad: (x) =>
          x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2,
        easeInCubic: (x) => x * x * x,
        easeOutCubic: (x) => 1 - Math.pow(1 - x, 3),
        easeInOutCubic: (x) =>
          x < 0.5 ? 4 * x * x * x : 1 - Math.pow(-2 * x + 2, 3) / 2,
        easeInQuart: (x) => x * x * x * x,
        easeOutQuart: (x) => 1 - Math.pow(1 - x, 4),
        easeInOutQuart: (x) =>
          x < 0.5 ? 8 * x * x * x * x : 1 - Math.pow(-2 * x + 2, 4) / 2,
        easeInQuint: (x) => x * x * x * x * x,
        easeOutQuint: (x) => 1 - Math.pow(1 - x, 5),
        easeInOutQuint: (x) =>
          x < 0.5 ? 16 * x * x * x * x * x : 1 - Math.pow(-2 * x + 2, 5) / 2,
        easeInExpo: (x) => (x === 0 ? 0 : Math.pow(2, 10 * x - 10)),
        easeOutExpo: (x) => (x === 1 ? 1 : 1 - Math.pow(2, -10 * x)),
        easeInOutExpo: (x) => {
          if (x === 0) {
            return 0;
          }
          if (x === 1) {
            return 1;
          }
          if (x < 0.5) {
            return Math.pow(2, 20 * x - 10) / 2;
          }
          return (2 - Math.pow(2, -20 * x + 10)) / 2;
        },
        easeInCirc: (x) => 1 - Math.sqrt(1 - Math.pow(x, 2)),
        easeOutCirc: (x) => Math.sqrt(1 - Math.pow(x - 1, 2)),
        easeInOutCirc: (x) => {
          if (x < 0.5) {
            return (1 - Math.sqrt(1 - Math.pow(2 * x, 2))) / 2;
          }
          return (Math.sqrt(1 - Math.pow(-2 * x + 2, 2)) + 1) / 2;
        },
        relativeRandom50: (x, fr) => x + (-fr + Math.random() * fr * 2) * 0.5,
        random: (x) => Math.random(),
      };

      const adjustmentFunctions = {
        none: (hsl) => hsl,
        harveyHue: ([h, s, l]) => [harveyHue(h), s, l],
        muted: ([h, s, l]) => [h, s * 0.5, l],
        pastel: ([h, s, l]) => [h, s, 0.2 + l * 0.8],
        pseudoPrint: ([h, s, l]) => {
          // if light is above .9 make the hue yellowish
          if (l > 0.9) {
            h = 36;
          }
          // clamp white to 95
          l = Math.min(l, 0.92);
          // make sure black is never too dark
          l = Math.max(l, 0.11);

          // make sure none of the colors are too saturated
          s = Math.min(s, 0.7);
          // when greenish lower saturation more
          if (h > 80 && h < 160) {
            s = Math.min(s, 0.5);
          }

          // if light teal make darker
          if (h > 172 && h < 195) {
            l *= 0.8;
            s += 0.15;
          }

          return [h, s, l];
        },
      };

      const easingFunctionsKeys = Object.keys(easingFunctions);
      const adjustmentFunctionsKeys = Object.keys(adjustmentFunctions);

      const newOptions = (overwrites = {}) => {
        const lPow = 0.1 + Math.random() * 0.9;
        const cPow = 0.2 + Math.random() * 0.2;

        const lRange = [
          Math.pow(0.05 + Math.random() * 0.2, lPow),
          Math.pow(1 - Math.random() * 0.15, lPow),
        ];

        return {
          ...{
            total: 5 + Math.floor(Math.random() * 5),
            hStart: Math.random() * 360,
            hStartCenter: 0.5,
            hEasing: easingFunctions["linear"],
            hCycles: Math.random() * 1.25,
            sRange: [
              Math.pow(0.005 + Math.random() * 0.25, cPow),
              Math.pow(1 - Math.random(), cPow),
            ],
            sEasing: easingFunctions["easeInOutCubic"],
            lRange,
            lEasing: easingFunctions["easeInOutSine"],
            transformFn: adjustmentFunctions["none"],
          },
          ...overwrites,
        };
      };

      const palette = (colors, method) => {
        let allColors = shuffleArray(colors);

        let localcolors = [...allColors];
        $pal.innerHTML = "";

        $pal.appendChild(paletteDom(localcolors, 1));
        localcolors = shuffleArray(localcolors);

        for (let i = 0; i < 2; i++) {
          $pal.appendChild(paletteDom(localcolors, 2));
          localcolors = shuffleArray(localcolors);
        }

        for (let i = 0; i < 4 * 2; i++) {
          $pal.appendChild(paletteDom(localcolors, 4));
          localcolors = shuffleArray(localcolors);
        }

        $ramp.style.setProperty(
          "background",
          `linear-gradient(90deg, ${colors.map((c) => c.css).join(",")})`
        );
      };

      const roundTo = (num, dec = 2) => {
        return Math.round(num * Math.pow(10, dec)) / Math.pow(10, dec);
      };

      class Pane {
        constructor($dom, nameSpace = "pane", throttleTimer = 50) {
          this.watches = new Map();
          this.nameSpace = nameSpace;

          this.$dom = $dom;
          this.$el = document.createElement("div");
          this.$el.classList.add(nameSpace);
          this.$el.addEventListener(
            "input",
            this._onChange.bind(this),
            {
              captures: true,
              passive: true,
            },
            true
          );

          $dom.appendChild(this.$el);

          this.events = {};
          this.timer;
        }

        _callCallbacks(ref, key) {
          // Added key parameter
          if (this.events.hasOwnProperty("change")) {
            clearTimeout(this.timer);
            this.events["change"].forEach((fn) => {
              this.timer = setTimeout(() => {
                // Pass both ref and key to the callback function
                fn.call(ref || this, { reference: ref, key: key });
              }, this.throttleTimer);
            });
          }
        }

        _onChange(event) {
          const $target = event.target;

          if ("key" in $target.dataset) {
            const key = $target.dataset.key;
            const ref = this.watches.get(key).reference;

            if ($target.dataset.type === "number") {
              ref[key] = parseFloat($target.value);
            } else {
              ref[key] = $target.value;
            }

            if ("siblingid" in $target.dataset) {
              if (Number($target.value) && $target.value !== "") {
                // validate that the values is within the min and max range of the input if present
                const $sibling = document.getElementById(
                  $target.dataset.siblingid
                );

                if ($sibling) {
                  const min = $sibling.getAttribute("min");
                  const max = $sibling.getAttribute("max");

                  if (min && Number($sibling.value) < Number(min)) {
                    $sibling.value = min;
                  } else if (max && Number($sibling.value) > Number(max)) {
                    $sibling.value = max;
                  } else {
                    $sibling.value = roundTo($target.value, 2);
                  }
                }
              }
            }

            // Pass the key to the callbacks
            this._callCallbacks(ref, key);
          }
        }

        on(event, fn) {
          if (!this.events.hasOwnProperty(event)) {
            this.events[event] = [];
          }
          this.events[event].push(fn);
        }

        addInput(reference, key, options) {
          const $inputs = this._appendInput(reference, key, options);
          this.watches.set(key, { reference, $inputs, key });
        }

        _inputToValue($input, ref, key, value) {
          if (!$input) {
            return;
          }

          if ($input.matches("select")) {
            Array.from($input.querySelectorAll("option")).forEach(
              ($input) =>
                ($input.selected = $input.value === (value || ref[key]))
            );
          } else if ($input.matches('[inputmode="numeric"]')) {
            $input.value = value || roundTo(ref[key], 2);
          } else {
            $input.value = value || ref[key];
          }
        }

        updateInputs(key, value) {
          if (key) {
            const watcher = this.watches.get(key);
            watcher.$inputs.forEach(($input) =>
              this._inputToValue($input, watcher.reference, watcher.key, value)
            );
          } else {
            this.watches.forEach((watcher) => {
              watcher.$inputs.forEach(($input) =>
                this._inputToValue($input, watcher.reference, watcher.key)
              );
            });
          }

          this._callCallbacks();
        }

        _appendInput(reference, key, options) {
          let type = typeof reference[key];

          const $e = document.createElement("label");
          const $label = document.createElement("strong");
          const $section = document.createElement("div");

          let $i, $in;

          if (type === "number") {
            $i = document.createElement("input");
            $in = document.createElement("input");

            $i.setAttribute("type", "range");
            $in.setAttribute("type", "text");
            $in.setAttribute("inputmode", "numeric");
            $in.setAttribute("pattern", "[0-9]*");

            // type="text" inputmode="numeric" pattern="[0-9]*"

            if (options.hasOwnProperty("min")) {
              $i.setAttribute("min", options.min);
              $in.setAttribute("min", options.min);
            }
            if (options.hasOwnProperty("max")) {
              $i.setAttribute("max", options.max);
              $in.setAttribute("max", options.max);
            }
            if (options.hasOwnProperty("step")) {
              $i.setAttribute("step", options.step);
              $in.setAttribute("step", options.step);
            }
          } else if (type === "string") {
            if (options.hasOwnProperty("options")) {
              $i = document.createElement("select");

              options.options.forEach((option) => {
                const $opt = document.createElement("option");
                $opt.setAttribute("value", option);
                if (option === reference[key]) {
                  $opt.setAttribute("selected", true);
                }
                $opt.innerHTML = option;
                $i.appendChild($opt);
              });
            } else {
              $i = document.createElement("input");
              $i.setAttribute("type", "text");
            }
          }

          $i.dataset.key = key;
          $i.dataset.type = type;
          $i.value = reference[key];
          $i.id = `${this.nameSpace}--${key}`;
          $i.classList.add(
            `${this.nameSpace}__input`,
            `${this.nameSpace}__input--${type}`
          );

          $section.appendChild($i);
          $section.classList.add(
            `${this.nameSpace}__inputs`,
            `${this.nameSpace}__inputs--${type}`
          );

          if ($in) {
            $in.dataset.key = key;
            $in.dataset.type = type;
            $in.value = roundTo(reference[key], 2);
            $in.id = `${this.nameSpace}--${key}--value`;
            $i.classList.add(
              `${this.nameSpace}__input`,
              `${this.nameSpace}__input--${type}`
            );
            $in.dataset.siblingid = `${this.nameSpace}--${key}`;
            $i.dataset.siblingid = `${this.nameSpace}--${key}--value`;
            $section.appendChild($in);
          }

          $label.innerHTML = key;
          $label.classList.add(`${this.nameSpace}__label`);

          // make a css compatible version of the key
          const cssKey = key.replace(/[^a-z0-9]/gi, "_").toLowerCase();
          $e.classList.add(
            `${this.nameSpace}__section`,
            `${this.nameSpace}__section--${type}`,
            `${this.nameSpace}__section--${cssKey}`
          );
          $e.appendChild($label);
          $e.append($section);

          this.$el.appendChild($e);

          return [$i, $in];
        }
      }

      const pane = new Pane(document.querySelector("[data-pane]"));

      const PARAMS = {};

      let darkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;

      if (darkMode) {
        generateColorRampParams.maxLight.default = Math.random() * 0.2;
        generateColorRampParams.minLight.default = 0.89 + Math.random() * 0.11;
      }

      const defaultParams = {
        hueGenerator: {
          default: "Default",
          props: {
            options: ["Default", "Unique Random", "Color Harmony"],
          },
        },
        colorHarmony: {
          default: "splitComplementary",
          props: {
            options: Object.keys(colorHarmonies).reverse(),
          },
        },
        hMinDiffAngle: {
          default: 60,
          props: {
            min: 15,
            max: 180,
            step: 1,
          },
        },
        hueSpread: {
          default: 0,
          props: {
            min: 0,
            max: 1,
            step: 0.01,
          },
        },
        ...generateColorRampParams,
        easingMode: {
          default: "Individual Axis",
          props: {
            options: ["Individual Axis", "S/L Curve"],
          },
        },
        curveMethod: {
          default: "lamé",
          props: {
            options: ["lamé", "arc", "pow", "powX", "powY"],
          },
        },
        curveAccent: {
          default: 0,
          props: {
            min: -0.05,
            max: 1,
            step: 0.01,
          },
        },
        hEasing: {
          default: "linear",
          props: {
            options: easingFunctionsKeys,
          },
        },
        sEasing: {
          default: "easeInOutCubic",
          props: {
            options: easingFunctionsKeys,
          },
        },
        lEasing: {
          default: "easeInOutSine",
          props: {
            options: easingFunctionsKeys,
          },
        },
        transformFn: {
          default: "none",
          props: {
            options: adjustmentFunctionsKeys,
          },
        },
      };

      Object.keys(defaultParams).forEach((key) => {
        const param = defaultParams[key];
        PARAMS[key] = param.default;
        pane.addInput(PARAMS, key, param.props);
      });

      PARAMS["Color Mode"] = Math.random() < 0.9 ? "hsl" : "RYBItten";

      pane.addInput(PARAMS, "Color Mode", {
        options: [
          "hsl",
          "okhsl",
          "hsv",
          "RYBItten",
          "okhsv",
          "hsi",
          "hwb",
          "lch",
          "oklch",
        ],
      });

      const createPalette = (hueList = null) => {
        let colors = generateColorRamp(
          newOptions({
            hueList,
            sRange: [PARAMS.minSaturation, PARAMS.maxSaturation],
            lRange: [PARAMS.minLight, PARAMS.maxLight],
            transformFn: adjustmentFunctions[PARAMS.transformFn],
          })
        );

        colors = hslToColorObj(colors);

        const cssHSL = colors.map((color) => color.css);

        const $w = document.createElement("article");
        $w.classList.add("palette");
        $w.style.setProperty("--gc", cssHSL.join());
        $w.style.setProperty(
          "--gs",
          cssHSL
            .map(
              (c, i) =>
                `${c} ${(i / colors.length) * 100}%, ${c} ${
                  ((i + 1) / colors.length) * 100
                }%`
            )
            .join()
        );
        $w.style.setProperty(
          "--crnd",
          cssHSL[~~(cssHSL.length * Math.random())]
        );

        cssHSL.forEach((color, i) => {
          $w.style.setProperty(`--c${i}`, color);
          const $swatch = document.createElement("div");
          $swatch.classList.add("palette__swatch");
          $swatch.style.setProperty("--color", color);
          $w.appendChild($swatch);
        });

        return $w;
      };

      const $pal = document.querySelector("[data-palette]");
      const $ramp = document.querySelector("[data-ramp]");
      const $rndsamples = document.querySelector("[data-rndsamples]");
      const $rnd = document.querySelectorAll("[data-randomize]");

      const np = (huelist = null) => {
        $rndsamples.innerHTML = "";
        new Array(28).fill(0).forEach((_) => {
          $rndsamples.appendChild(createPalette(huelist));
        });
      };

      np();

      const newPalette = () => {
        const newParams = newOptions();

        Object.keys(newParams).forEach((key) => {
          PARAMS[key] = newParams[key];
        });

        pane.updateInputs();
      };
      let timers = [];

      $rnd.forEach(($b) => {
        $b.addEventListener("click", (e) => {
          e.preventDefault();
          if (timers.length) {
            timers.forEach((t) => clearTimeout(t));
            timers = [];
          }
          newPalette();
        });
      });

      let colors = [];

      function paletteDom(colorArr, x = 1) {
        const $div = document.createElement("div");
        $div.classList.add("palette-sample");
        $div.style.setProperty("--x", x);
        $div.style.setProperty("--rnd", Math.random());
        $div.innerHTML = "<b><i></i><i></i></b>";

        for (let i = 0; i < 4; i++) {
          $div.style.setProperty(
            `--col-${i}`,
            colorArr[i % (colorArr.length - 1)].css
          );
        }

        return $div;
      }

      function hslToColorObj(colors) {
        const colorMode = PARAMS["Color Mode"];
        // Mutate colors object
        return colors.map(([h, s, l]) => {
          const channels = {};
          let css;
          if (colorMode === "hsl" || colorMode === "okhsl") {
            channels.h = h;
            channels.s = s;
            channels.l = l;
            if (colorMode === "hsl") {
              css = colorToCSS([h, s, l], "hsl");
            }
          } else if (colorMode === "hsv" || colorMode === "okhsv") {
            channels.h = h;
            channels.s = s;
            channels.v = l + (s * Math.min(l, 1 - l)) / 2;
          } else if (colorMode === "hsi") {
            channels.h = h;
            channels.s = s;
            channels.i = l * 2;
          } else if (colorMode === "hwb") {
            channels.h = h;
            channels.w = l;
            channels.b = s;
          } else if (colorMode === "oklch") {
            channels.l = l;
            channels.c = s * 0.4;
            channels.h = h;
            css = colorToCSS([h, s, l], "oklch");
          } else if (colorMode === "lch") {
            channels.l = l * 100;
            channels.c = s * 150;
            channels.h = h;
            css = colorToCSS([h, s, l], "lch");
          } else if (colorMode === "RYBItten") {
            const [ry, gy, by] = rybHsl2rgb([h, s, l]);
            channels.r = ry;
            channels.g = gy;
            channels.b = by;
          }

          const computedColorMode =
            colorMode === "RYBItten" ? "rgb" : colorMode;

          const { r, g, b } = culori.converter("rgb")({
            mode: computedColorMode,
            ...channels,
          });

          return {
            hsl: [h, s, l],
            rgb: [r * 255, g * 255, b * 255],
            hex: culori.formatHex({ mode: computedColorMode, ...channels }),
            css:
              css || culori.formatHex({ mode: computedColorMode, ...channels }),
            contrast: {
              white: culori.wcagContrast("#ffffff", {
                mode: "hsl",
                ...channels,
              }),
              black: culori.wcagContrast("#000000", {
                mode: "hsl",
                ...channels,
              }),
            },
          };
        });
      }

      const $viz = document.querySelector("[data-viz]");
      const $slViz = document.querySelector("[data-slviz]");
      const $vizMini = document.querySelector("[data-icon='hue']");
      const $vizMiniSl = document.querySelector("[data-icon='sl']");

      function createDisc(color, i) {
        const $disc = document.createElement("div");
        $disc.classList.add("disc");
        $disc.style.setProperty("--c-h", color.hsl[0]);
        $disc.style.setProperty("--c-s", color.hsl[1]);
        $disc.style.setProperty("--c-l", color.hsl[2]);
        $disc.style.setProperty("--c-i", i);
        // number going from 0 to 0.5 and back to 0
        $disc.style.setProperty("--c-i2", i > 0.5 ? 1 - i : i);

        return $disc;
      }

      function viz(colors) {
        $viz.innerHTML = "";
        $slViz.innerHTML = "";
        $vizMini.innerHTML = "";
        $vizMiniSl.innerHTML = "";

        colors.forEach((color, i) => {
          $viz.appendChild(createDisc(color, i / colors.length));

          const $slDisc = createDisc(color, i / colors.length);
          $slDisc.classList.add("disc--sl");
          $slViz.appendChild($slDisc);

          const $miniDisc = createDisc(color, i / colors.length);
          $miniDisc.classList.add("disc--mini");
          $vizMini.appendChild($miniDisc);

          const $miniDiscSl = createDisc(color, i / colors.length);
          $miniDiscSl.classList.add("disc--mini");
          $miniDiscSl.classList.add("disc--mini-sl");
          $vizMiniSl.appendChild($miniDiscSl);
        });
      }

      function bam(eventData) {
        // Accept eventData object
        const changedKey = eventData ? eventData.key : null; // Extract the key

        // --- TAB SWITCHING LOGIC MOVED HERE ---
        if (changedKey) {
          const activeTabButton = document.querySelector(
            ".tabs__control--active"
          );
          // Check if the 'fncall' tab is currently active
          if (
            !activeTabButton ||
            activeTabButton.dataset.tabtarget !== "fncall"
          ) {
            const slKeys = [
              "minLight",
              "maxLight",
              "minSaturation",
              "maxSaturation",
              "sEasing",
              "lEasing",
              "curveAccent",
              "curveMethod",
            ];
            const hKeys = [
              "hueGenerator",
              "hStart",
              "hCycles",
              "hStartCenter",
              "hMinDiffAngle",
              "colorHarmony",
            ];

            let targetTabId = null;
            if (slKeys.includes(changedKey)) {
              targetTabId = "viz-sl";
            } else if (hKeys.includes(changedKey)) {
              targetTabId = "viz-h";
            }

            if (targetTabId) {
              const tabButton = document.querySelector(
                `.tabs__control[data-tabtarget="${targetTabId}"]`
              );
              // Check if the tab isn't already active before clicking to avoid unnecessary actions
              if (
                tabButton &&
                !tabButton.classList.contains("tabs__control--active")
              ) {
                tabButton.click(); // Consider calling activateTab directly if possible
              }
            }
          }
        }
        // --- END TAB SWITCHING LOGIC ---

        document
          .querySelector(".pane__section--hstart")
          .classList.remove("pane__section--hidden");
        document
          .querySelector(".pane__section--hstartcenter")
          .classList.remove("pane__section--hidden");
        document
          .querySelector(".pane__section--hcycles")
          .classList.remove("pane__section--hidden");
        document
          .querySelector(".pane__section--heasing")
          .classList.remove("pane__section--hidden");
        document
          .querySelector(".pane__section--colorharmony")
          .classList.add("pane__section--hidden");
        document
          .querySelector(".pane__section--hmindiffangle")
          .classList.add("pane__section--hidden");

        // Show hueSpread only for 'Unique Random' or 'Color Harmony'
        if (
          PARAMS.hueGenerator === "Unique Random" ||
          PARAMS.hueGenerator === "Color Harmony"
        ) {
          document
            .querySelector(".pane__section--huespread")
            .classList.remove("pane__section--hidden");
        } else {
          document
            .querySelector(".pane__section--huespread")
            .classList.add("pane__section--hidden");
        }

        let hueList;

        if (PARAMS.hueGenerator === "Default") {
        } else if (PARAMS.hueGenerator === "Color Harmony") {
          document
            .querySelector(".pane__section--hstartcenter")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--hcycles")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--heasing")
            .classList.add("pane__section--hidden");

          document
            .querySelector(".pane__section--colorharmony")
            .classList.remove("pane__section--hidden");

          hueList = colorHarmonies[PARAMS.colorHarmony](PARAMS.hStart);
        } else if (PARAMS.hueGenerator === "Unique Random") {
          document
            .querySelector(".pane__section--hmindiffangle")
            .classList.remove("pane__section--hidden");
          document
            .querySelector(".pane__section--hstart")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--hstartcenter")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--hcycles")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--heasing")
            .classList.add("pane__section--hidden");

          let totalRandomHues = 4;

          if (PARAMS.total > 4 && PARAMS.total < 8) {
            totalRandomHues = 3;
          }

          hueList = uniqueRandomHues({
            startHue: PARAMS.hStart,
            total: totalRandomHues,
          });
        }

        if (PARAMS.easingMode === "S/L Curve") {
          document
            .querySelector(".pane__section--seasing")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--leasing")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--curvemethod")
            .classList.remove("pane__section--hidden");
          document
            .querySelector(".pane__section--curveaccent")
            .classList.remove("pane__section--hidden");
        } else {
          document
            .querySelector(".pane__section--seasing")
            .classList.remove("pane__section--hidden");
          document
            .querySelector(".pane__section--leasing")
            .classList.remove("pane__section--hidden");
          document
            .querySelector(".pane__section--curvemethod")
            .classList.add("pane__section--hidden");
          document
            .querySelector(".pane__section--curveaccent")
            .classList.add("pane__section--hidden");
        }

        if (hueList && hueList.length > PARAMS.total) {
          hueList = hueList.slice(0, PARAMS.total);
        } else if (hueList && hueList.length < PARAMS.total) {
          hueList = scaleSpreadArray(hueList, PARAMS.total, PARAMS.hueSpread);
        }

        const generatorFn =
          PARAMS.easingMode === "S/L Curve"
            ? generateColorRampWithCurve
            : generateColorRamp;

        colors = generatorFn({
          total: PARAMS.total,
          hStart: PARAMS.hStart,
          hCycles: PARAMS.hCycles,
          hStartCenter: PARAMS.hStartCenter,
          hEasing:
            typeof PARAMS.hEasing === "string"
              ? easingFunctions[PARAMS.hEasing]
              : PARAMS.hEasing,
          sRange: [PARAMS.minSaturation, PARAMS.maxSaturation],
          sEasing:
            typeof PARAMS.sEasing === "string"
              ? easingFunctions[PARAMS.sEasing]
              : PARAMS.sEasing,
          lRange: [PARAMS.minLight, PARAMS.maxLight],
          lEasing:
            typeof PARAMS.lEasing === "string"
              ? easingFunctions[PARAMS.lEasing]
              : PARAMS.lEasing,

          curveMethod: PARAMS.curveMethod,
          curveAccent: PARAMS.curveAccent,

          hueList: hueList,
          transformFn: adjustmentFunctions[PARAMS.transformFn],
        });

        document.documentElement.style.setProperty("--total", colors.length);
        document.documentElement.style.setProperty("--hStart", PARAMS.hStart);
        document.documentElement.style.setProperty(
          "--hStartCenter",
          PARAMS.hStartCenter
        );
        document.documentElement.style.setProperty("--hCycles", PARAMS.hCycles);
        document.documentElement.style.setProperty(
          "--sRangeMin",
          PARAMS.minSaturation
        );
        document.documentElement.style.setProperty(
          "--sRangeMax",
          PARAMS.maxSaturation
        );
        document.documentElement.style.setProperty(
          "--lRangeMin",
          PARAMS.minLight
        );
        document.documentElement.style.setProperty(
          "--lRangeMax",
          PARAMS.maxLight
        );

        const colorMode = PARAMS["Color Mode"];
        // Mutate colors object
        colors = hslToColorObj(colors);

        viz(colors);

        let hueParams = `  total: <i>${PARAMS.total}</i>,
  hStart: <i>${PARAMS.hStart.toFixed(3)}</i>,
  hStartCenter: <i>${PARAMS.hStartCenter.toFixed(3)}</i>,
  hEasing: 
    <i>${
      typeof PARAMS.hEasing === "string"
        ? easingFunctions[PARAMS.hEasing]
        : PARAMS.hEasing
    }</i>,
  hCycles: <i>${PARAMS.hCycles.toFixed(3)}</i>,
`;

        if (PARAMS.hueGenerator != "Default") {
          if (PARAMS.hueGenerator === "Color Harmony") {
            hueParams = `  hueList: <i>colorHarmonies
    .${PARAMS.colorHarmony}(${PARAMS.hStart.toFixed(1)})</i>,`;
          } else if (PARAMS.hueGenerator === "Unique Random") {
            hueParams = `  hueList: <i>uniqueRandomHues({
    startHue: ${PARAMS.hStart.toFixed(2)},
    total: ${PARAMS.total > 4 && PARAMS.total < 8 ? 3 : 4},
    minDistance: ${PARAMS.hMinDiffAngle},
  })</i>,`;
          }
        }
        const codeString = `${
          PARAMS.easingMode === "S/L Curve"
            ? "generateColorRampWithCurve"
            : "generateColorRamp"
        }({
${hueParams}
  sRange: <i>[${PARAMS.minSaturation.toFixed(
    3
  )}, ${PARAMS.maxSaturation.toFixed(3)}]</i>,
  lRange: <i>[${PARAMS.minLight.toFixed(3)}, ${PARAMS.maxLight.toFixed(3)}]</i>,
${
  PARAMS.easingMode === "S/L Curve"
    ? `  curveMethod: <i>${PARAMS.curveMethod}</i>,
  curveAccent: <i>${PARAMS.curveAccent.toFixed(3)}</i>,`
    : `
  sEasing:
    <i>${
      typeof PARAMS.sEasing === "string"
        ? easingFunctions[PARAMS.sEasing]
        : PARAMS.sEasing
    }</i>,
  lEasing: 
    <i>${
      typeof PARAMS.lEasing === "string"
        ? easingFunctions[PARAMS.lEasing]
        : PARAMS.lEasing
    }</i>,`
}
});
`;
        const $code = document.querySelector("[data-code]");

        $code.innerHTML = codeString;
        $code.dataset.copy = codeString
          .replace(/<i>/g, "")
          .replace(/<\/i>/g, "");

        document.documentElement.style.setProperty(
          "--gradient",
          colors.map((c) => c.css).join(",")
        );
        document.querySelector("[data-colors]").innerHTML = colors.reduce(
          (r, c, i) => {
            const relI = i / PARAMS.total;
            const bestcontrast =
              c.contrast.white < c.contrast.black ? "black" : "white";
            return `${r}<i style="--w: ${1 / PARAMS.total}; --color: ${
              c.css
            }; --c: ${bestcontrast}; --h: ${c.hsl[0]}; --l: ${
              c.hsl[2]
            }; --rnd: ${-1 + Math.random() * 2};  --rnd2: ${
              -1 + Math.random() * 2
            }; --i: ${relI};">
            <b>
              ${c.hsl
                .map((c, i) => Math.round(i ? c * 100 : c) + (i ? "%" : "°"))
                .join(" ")}
            </b>
            <span></span>
          </i>`;
          },
          ""
        );

        palette(colors, "random");
        list(colors);

        const $body = document.querySelector("body");

        colors.forEach((c, i) => {
          document.documentElement.style.setProperty(
            `--c${i}`,
            `hsl(${c.hsl[0]}, ${c.hsl[1] * 100}%, ${c.hsl[2] * 100}%)`
          );
        });

        document.documentElement.style.setProperty(
          `--clast`,
          colors[colors.length - 1].css
        );

        const remColors = [...colors];

        const bgc = remColors.shift();
        remColors.pop();
        const bestContrastTobgc = remColors.sort(
          (a, b) =>
            culori.wcagContrast(a.hex, bgc.hex) -
            culori.wcagContrast(b.hex, bgc.hex)
        );

        if (bestContrastTobgc.length) {
          document.documentElement.style.setProperty(
            `--csecondlast`,
            bestContrastTobgc.at(-2).css
          );
        }

        document.documentElement.style.setProperty(
          `--ts`,
          colors.map((c, i) => `${-i * 1}px ${-i * 1}px ${0}px ${c.css}`).join()
        );

        document.documentElement.style.setProperty(
          `--tsss`,
          colors.map((c, i) => `0 ${0}px ${0}px ${c.css}`).join()
        );

        document.documentElement.style.setProperty(
          `--tss`,
          -colors.length * 4 + "px"
        );

        names(colors);
        updateFavicon(colors.map((c) => c.hex));
        np(hueList);
        
        // Sync palette to Token Beam
        if (window.tokenBeamSync && typeof window.tokenBeamSync === 'function') {
          window.tokenBeamSync(colors);
        }
      }

      const xmlns = "http://www.w3.org/2000/svg";

      const printColors = (arr, simple, names) =>
        arr
          .map(({ hsl, rgb, hex, css, contrast }, i) => {
            if (simple) {
              return `
        <li class="swatch swatch--simple" style="--col:${hex}; --coltext: ${
                contrast.black > contrast.white ? "#202125" : "#fff"
              }">
          <div data-copy title="Click to copy">${Math.floor(
            hsl[0]
          )}&deg; ${Math.floor(hsl[1] * 100)}% ${Math.floor(
                hsl[2] * 100
              )}%</div>
        </li>`;
            } else {
              return `
          <li class="full" style="--col:${hex}; --coltext: ${
                contrast.black < contrast.white ? "#202125" : "#fff"
              }; --colbg: ${
                contrast.black > contrast.white ? "#202125" : "#fff"
              }">
            <div class="color-info">
              <strong>${names[i].name}</strong>
              <button data-copy title="Click to copy">hsl(${Math.floor(
                hsl[0]
              )},${Math.floor(hsl[1] * 100)}%,${Math.floor(
                hsl[2] * 100
              )}%)</button>
              <button data-copy title="Click to copy">rgb(${Math.floor(
                rgb[0]
              )},${Math.floor(rgb[1])},${Math.floor(rgb[2])})</button>
              <button data-copy title="Click to copy">${css}</button>
              <div class="color-info__contrast">
                <h5>wcag contrast:</h5>
                <em><span style="color: #ffffff;"">Aa</span> ${contrast.white.toFixed(
                  2
                )}</em>
                <span style="color: #000000;">Aa</span> ${contrast.black.toFixed(
                  2
                )}</em>
              </div>
            </div>
          </li>
        `;
            }
          })
          .join("");

      function list(colors) {
        document.querySelector("[data-list]").innerHTML = `
      <div>
        <h3>Colors</h3>
        <ol>${printColors(colors, true)}</ol>
      </div>
    `;
      }

      let timer;
      const $names = document.querySelector("[data-names]");
      const $copyButton = document.querySelector("[data-copycolors]");

      function names(colors) {
        $names.innerHTML = `<strong style="--col: #fff">…</strong>`;
        clearTimeout(timer);
        timer = setTimeout(() => {
          const hexes = colors.map((c) => c.hex.replace("#", ""));
          fetch(
            `https://api.color.pizza/v1/?values=${hexes.join()}&goodnamesonly=true&noduplicates=true`,
            {
              method: "GET",
              headers: {
                "Content-Type": "application/json",
                "X-Referrer": "https://meodai.github.io/rampensau/",
              },
            }
          )
            .then((r) => r.json())
            .then((d) => {
              $names.innerHTML = `<ol>${printColors(
                colors,
                false,
                d.colors
              )}</ol>`;
            });
        }, 1000);

        $copyButton.setAttribute(
          "data-copy",
          colors.map((c) => c.hex).join(", ")
        );
      }

      $pal.addEventListener("pointerdown", (e) => {
        palette(colors, "random");
        timer = setInterval(() => palette(colors, "random"), 100);
      });
      $pal.addEventListener("pointerup", () => clearInterval(timer));

      pane.on("change", bam);

      document.documentElement.addEventListener("pointerdown", (e) => {
        const $target = e.target;
        if ($target.matches(`[data-pantrigger]`)) {
          let value = !isNaN($target.dataset.panvalue)
            ? new Number($target.dataset.panvalue)
            : $target.dataset.panvalue;
          PARAMS[$target.dataset.pantrigger] = value;
          pane.updateInputs(
            $target.dataset.pantrigger,
            $target.dataset.panvalue
          );
        }
      });

      if (navigator.clipboard) {
        document.querySelector("body").addEventListener("click", (e) => {
          const $el = e.target;
          if ($el.matches("[data-copy]")) {
            let oldText = $el.innerText;
            let text;
            // check if data copy has a value
            if (!$el.dataset.copy) {
              text = $el.innerText;
            } else {
              text = $el.dataset.copy;
            }

            navigator.clipboard.writeText(text).then(() => {
              $el.innerText = "copied!";
              setTimeout(() => {
                $el.innerText = oldText;
              }, 500);
            });
          }
        });
      }

      document
        .querySelector('[aria-controls="sidebar"]')
        .addEventListener("click", () => {
          document.documentElement.classList.toggle("sidebar-open");
        });

      newPalette();
      bam();

      // tabs logic
      const $tabs = document.querySelectorAll("[data-tab]");
      const $tabTriggers = document.querySelectorAll("[data-tabtarget]");
      const $tabsWrap = document.querySelector("[data-tabswrap]");

      let activeTabIndex = 0;

      $tabsWrap.style.setProperty("--tabs-length", $tabs.length);

      function activateTab($tab) {
        $tabs.forEach(($t) => {
          $t.classList.remove("tabs__content--active");
          $t.setAttribute("aria-hidden", true);
        });
        $tabTriggers.forEach(($trigger) => {
          $trigger.setAttribute("aria-selected", false);
          $trigger.classList.remove("tabs__control--active");
        });

        const $trigger = document.querySelector(
          `[data-tabtarget="${$tab.dataset.tab}"]`
        );
        $trigger.setAttribute("aria-selected", true);
        $trigger.classList.add("tabs__control--active");

        $tab.classList.add("tabs__content--active");
        $tab.setAttribute("aria-hidden", false);
        activeTabIndex = [...$tabs].indexOf($tab);
        $tabsWrap.style.setProperty("--active-tab", activeTabIndex);
      }

      activateTab($tabs[activeTabIndex]);

      $tabTriggers.forEach(($trigger) => {
        $trigger.addEventListener("click", (e) => {
          e.preventDefault();
          const target = $trigger.dataset.tabtarget;
          const $target = document.querySelector(`[data-tab="${target}"]`);
          activateTab($target);
        });
      });

      // Token Beam Integration
      (async function initTokenBeam() {
        try {
          const { SourceSession, createCollection } = await window.tokenBeamPromise;
          
          const widget = document.querySelector('[data-token-beam-widget]');
          const tokenButton = document.querySelector('[data-token-button]');
          const tokenWrap = document.querySelector('[data-token-wrap]');
          const helpEl = document.querySelector('[data-help]');
          const helpBtn = document.querySelector('[data-help-btn]');
          const helpDropdown = document.querySelector('[data-help-dropdown]');
          
          let syncClient = null;
          let sessionToken = null;
          
          // Help dropdown toggle
          helpBtn.addEventListener('click', (e) => {
            e.stopPropagation();
            helpEl.classList.toggle('is-open');
          });
          
          document.addEventListener('click', (e) => {
            if (!helpEl.contains(e.target)) {
              helpEl.classList.remove('is-open');
            }
          });
          
          const SYNC_SERVER_URL = 'wss://tokenbeam.dev';
          
          // Helper to show/hide unlink button and help
          function showUnlinkButton() {
            widget.classList.add('token-beam-widget--connected');
            const existingBtn = tokenWrap.querySelector('.token-beam-widget__unlink');
            if (!existingBtn) {
              const unlinkBtn = document.createElement('button');
              unlinkBtn.className = 'token-beam-widget__unlink';
              unlinkBtn.innerHTML = '<span class="token-beam-widget__dot token-beam-widget__dot--active"></span>';
              unlinkBtn.title = 'Click to disconnect';
              unlinkBtn.addEventListener('click', handleUnlink);
              tokenWrap.appendChild(unlinkBtn);
            }
          }
          
          function hideUnlinkButton() {
            widget.classList.remove('token-beam-widget--connected');
            const unlinkBtn = tokenWrap.querySelector('.token-beam-widget__unlink');
            if (unlinkBtn) unlinkBtn.remove();
          }
          
          // Initialize sync client - it handles token creation automatically
          function initSyncClient() {
            if (syncClient) {
              syncClient.disconnect();
            }
            
            syncClient = new SourceSession({
              serverUrl: SYNC_SERVER_URL,
              clientType: 'web',
              origin: 'RampenSau Color Generator',
              icon: { type: 'unicode', value: '🎨' },
            });

            syncClient.on('paired', ({ sessionToken: token }) => {
              sessionToken = token;
              widget.classList.remove('token-beam-widget--waiting');
              tokenButton.textContent = sessionToken;
              tokenButton.title = 'Click to copy token - waiting for design tool...';
            });

            syncClient.on('peer-connected', ({ clientType }) => {
              showUnlinkButton();
              tokenButton.title = `Connected to ${clientType}`;
              if (colors && colors.length > 0) {
                syncPalette(colors);
              }
            });

            syncClient.on('peer-disconnected', () => {
              if (!syncClient.hasPeers()) {
                hideUnlinkButton();
                tokenButton.title = 'Design tool disconnected';
              }
            });

            syncClient.on('connected', () => {
              tokenButton.title = 'Connecting to sync server...';
            });

            syncClient.on('disconnected', () => {
              hideUnlinkButton();
              tokenButton.title = 'Disconnected - click unlink to reconnect';
            });

            syncClient.on('error', ({ message }) => {
              console.error('Sync error:', message);
              tokenButton.title = 'Sync error - check console';
            });
            
            syncClient.connect().catch((error) => {
              console.error('Failed to connect to sync server:', error);
              tokenButton.textContent = 'Connection failed';
              tokenButton.title = 'Failed to connect to sync server';
            });
          }
          
          // Handle token click
          tokenButton.addEventListener('click', () => {
            if (!sessionToken) {
              // Token not yet ready
              return;
            }
            // Copy token to clipboard
            navigator.clipboard.writeText(sessionToken).then(() => {
              const oldText = tokenButton.textContent;
              tokenButton.textContent = 'Copied!';
              setTimeout(() => {
                tokenButton.textContent = oldText;
              }, 500);
            });
          });
          
          // Handle unlink
          function handleUnlink() {
            if (syncClient) {
              syncClient.disconnect();
              syncClient = null;
            }
            sessionToken = null;
            
            // Update UI
            widget.classList.add('token-beam-widget--waiting');
            hideUnlinkButton();
            tokenButton.textContent = 'Generating Token...';
            tokenButton.title = 'Generating sync token...';
            
            // Reinitialize
            initSyncClient();
          }
          
          // Sync current palette to design tool
          function syncPalette(colorArray) {
            if (!syncClient || !syncClient.hasPeers()) return;
            
            try {
              const collection = createCollection('rampensau', 
                colorArray.map((c, i) => ({
                  name: String(i + 1).padStart(2, '0'),
                  type: 'color',
                  value: c.hex,
                }))
              );
              
              syncClient.sync(collection);
            } catch (error) {
              console.error('Failed to sync palette:', error);
            }
          }
          
          // Expose sync function globally so bam() can call it
          window.tokenBeamSync = syncPalette;
          
          // Auto-initialize on page load
          initSyncClient();
          
        } catch (error) {
          console.error('Token Beam initialization error:', error);
          const tokenButton = document.querySelector('[data-token-button]');
          if (tokenButton) {
            tokenButton.textContent = 'Token Beam Unavailable';
            tokenButton.disabled = true;
            tokenButton.title = 'Failed to load Token Beam library';
          }
        }
      })();
    </script>
  </body>
</html>


================================================
FILE: dist/index.js
================================================
"use strict";
var rampensau = (() => {
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, { get: all[name], enumerable: true });
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

  // src/index.ts
  var index_exports = {};
  __export(index_exports, {
    colorUtils: () => colorUtils_exports,
    generateColorRamp: () => generateColorRamp,
    generateColorRampParams: () => generateColorRampParams,
    generateColorRampWithCurve: () => generateColorRampWithCurve,
    utils: () => utils_exports
  });

  // src/utils.ts
  var utils_exports = {};
  __export(utils_exports, {
    lerp: () => lerp,
    makeCurveEasings: () => makeCurveEasings,
    pointOnCurve: () => pointOnCurve,
    scaleSpreadArray: () => scaleSpreadArray,
    shuffleArray: () => shuffleArray
  });
  function shuffleArray(array, rndFn = Math.random) {
    const copy = [...array];
    let currentIndex = copy.length, randomIndex;
    while (currentIndex != 0) {
      randomIndex = Math.floor(rndFn() * currentIndex);
      currentIndex--;
      [copy[currentIndex], copy[randomIndex]] = [
        copy[randomIndex],
        copy[currentIndex]
      ];
    }
    return copy;
  }
  var lerp = (amt, from, to) => from + amt * (to - from);
  var scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {
    if (!valuesToFill || valuesToFill.length < 2) {
      throw new Error("valuesToFill array must have at least two values.");
    }
    if (targetSize < 1 && padding > 0) {
      throw new Error("Target size must be at least 1");
    }
    if (targetSize < valuesToFill.length && padding === 0) {
      throw new Error(
        "Target size must be greater than or equal to the valuesToFill array length."
      );
    }
    const result = new Array(targetSize);
    if (padding <= 0) {
      const len = valuesToFill.length;
      const lastIdx = len - 1;
      const totalAdded = targetSize - len;
      const baseAdds = Math.floor(totalAdded / lastIdx);
      const remainder = totalAdded % lastIdx;
      let currentResultIdx = 0;
      for (let i = 0; i < lastIdx; i++) {
        const startVal = valuesToFill[i];
        const endVal = valuesToFill[i + 1];
        const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);
        for (let j = 0; j < segmentLen; j++) {
          const t = j / segmentLen;
          result[currentResultIdx++] = fillFunction(t, startVal, endVal);
        }
      }
      result[currentResultIdx] = valuesToFill[lastIdx];
      return result;
    }
    const domainStart = padding;
    const domainEnd = 1 - padding;
    const lenMinus1 = valuesToFill.length - 1;
    const normalizedPositions = new Float64Array(valuesToFill.length);
    for (let i = 0; i < valuesToFill.length; i++) {
      normalizedPositions[i] = i / lenMinus1;
    }
    let segmentIndex = 0;
    for (let i = 0; i < targetSize; i++) {
      const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);
      const adjustedT = domainStart + t * (domainEnd - domainStart);
      while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {
        segmentIndex++;
      }
      const segmentStart = normalizedPositions[segmentIndex];
      const segmentEnd = normalizedPositions[segmentIndex + 1];
      let segmentT = 0;
      if (segmentEnd > segmentStart) {
        segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);
      }
      const fromValue = valuesToFill[segmentIndex];
      const toValue = valuesToFill[segmentIndex + 1];
      result[i] = fillFunction(segmentT, fromValue, toValue);
    }
    return result;
  };
  var pointOnCurve = (curveMethod, curveAccent) => {
    return (t) => {
      const limit = Math.PI / 2;
      const slice = limit / 1;
      const percentile = t;
      let x = 0, y = 0;
      if (curveMethod === "lam\xE9") {
        const t2 = percentile * limit;
        const exp = 2 / (2 + 20 * curveAccent);
        const cosT = Math.cos(t2);
        const sinT = Math.sin(t2);
        x = Math.sign(cosT) * Math.abs(cosT) ** exp;
        y = Math.sign(sinT) * Math.abs(sinT) ** exp;
      } else if (curveMethod === "arc") {
        y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);
        x = Math.sin(Math.PI / 2 + t * slice - curveAccent);
      } else if (curveMethod === "pow") {
        x = Math.pow(1 - percentile, 1 - curveAccent);
        y = Math.pow(percentile, 1 - curveAccent);
      } else if (curveMethod === "powY") {
        x = Math.pow(1 - percentile, curveAccent);
        y = Math.pow(percentile, 1 - curveAccent);
      } else if (curveMethod === "powX") {
        x = Math.pow(percentile, curveAccent);
        y = Math.pow(percentile, 1 - curveAccent);
      } else if (typeof curveMethod === "function") {
        const [xFunc, yFunc] = curveMethod(t, curveAccent);
        x = xFunc;
        y = yFunc;
      } else {
        throw new Error(
          `pointOnCurve() curveMethod parameter is expected to be "lam\xE9" | "arc" | "pow" | "powY" | "powX" or a function but \`${curveMethod}\` given.`
        );
      }
      return { x, y };
    };
  };
  var makeCurveEasings = (curveMethod, curveAccent) => {
    const point = pointOnCurve(curveMethod, curveAccent);
    return {
      sEasing: (t) => point(t).x,
      lEasing: (t) => point(t).y
    };
  };

  // src/colorUtils.ts
  var colorUtils_exports = {};
  __export(colorUtils_exports, {
    colorHarmonies: () => colorHarmonies,
    colorToCSS: () => colorToCSS,
    harveyHue: () => harveyHue,
    hsv2hsl: () => hsv2hsl,
    normalizeHue: () => normalizeHue,
    uniqueRandomHues: () => uniqueRandomHues
  });
  function normalizeHue(h) {
    return (h % 360 + 360) % 360;
  }
  function harveyHue(h) {
    h = normalizeHue(h) / 360;
    if (h === 1 || h === 0) return h;
    h = 1 + h % 1;
    const seg = 1 / 6;
    const a = h % seg / seg * Math.PI / 2;
    const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];
    const i = Math.floor(h * 6);
    const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];
    return cases[i % 6] * 360;
  }
  var colorHarmonies = {
    complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],
    splitComplementary: (h) => [
      normalizeHue(h),
      normalizeHue(h + 150),
      normalizeHue(h - 150)
    ],
    triadic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 120),
      normalizeHue(h + 240)
    ],
    tetradic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 90),
      normalizeHue(h + 180),
      normalizeHue(h + 270)
    ],
    pentadic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 72),
      normalizeHue(h + 144),
      normalizeHue(h + 216),
      normalizeHue(h + 288)
    ],
    hexadic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 60),
      normalizeHue(h + 120),
      normalizeHue(h + 180),
      normalizeHue(h + 240),
      normalizeHue(h + 300)
    ],
    monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],
    // min 2 for RampenSau
    doubleComplementary: (h) => [
      normalizeHue(h),
      normalizeHue(h + 180),
      normalizeHue(h + 30),
      normalizeHue(h + 210)
    ],
    compound: (h) => [
      normalizeHue(h),
      normalizeHue(h + 180),
      normalizeHue(h + 60),
      normalizeHue(h + 240)
    ],
    analogous: (h) => [
      normalizeHue(h),
      normalizeHue(h + 30),
      normalizeHue(h + 60),
      normalizeHue(h + 90),
      normalizeHue(h + 120),
      normalizeHue(h + 150)
    ]
  };
  function uniqueRandomHues({
    startHue = 0,
    total = 9,
    minHueDiffAngle = 60,
    rndFn = Math.random
  } = {}) {
    minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);
    const baseHue = startHue ?? rndFn() * 360;
    const huesToPickFrom = Array.from(
      {
        length: Math.round(360 / minHueDiffAngle)
      },
      (_, i) => (baseHue + i * minHueDiffAngle) % 360
    );
    let randomizedHues = shuffleArray(huesToPickFrom, rndFn);
    if (randomizedHues.length > total) {
      randomizedHues = randomizedHues.slice(0, total);
    }
    return randomizedHues;
  }
  var hsv2hsl = ([h, s, v]) => {
    const l = v - v * s / 2;
    const m = Math.min(l, 1 - l);
    const s_hsl = m === 0 ? 0 : (v - l) / m;
    return [h, s_hsl, l];
  };
  var colorModsCSS = {
    oklch: (color) => [
      color[2] * 100 + "%",
      color[1] * 100 + "%",
      color[0]
    ],
    lch: (color) => [
      color[2] * 100 + "%",
      color[1] * 100 + "%",
      color[0]
    ],
    hsl: (color) => [
      color[0],
      color[1] * 100 + "%",
      color[2] * 100 + "%"
    ],
    hsv: (color) => {
      const [h, s, l] = hsv2hsl(color);
      return [h, s * 100 + "%", l * 100 + "%"];
    }
  };
  var colorToCSS = (color, mode = "oklch") => {
    const cssMode = mode === "hsv" ? "hsl" : mode;
    return `${cssMode}(${colorModsCSS[mode](color).join(" ")})`;
  };

  // src/core.ts
  function generateColorRamp({
    total = 9,
    hStart = Math.random() * 360,
    hStartCenter = 0.5,
    hEasing = (x) => x,
    hCycles = 1,
    sRange = [0.4, 0.35],
    sEasing = (x) => Math.pow(x, 2),
    lRange = [Math.random() * 0.1, 0.9],
    lEasing = (x) => Math.pow(x, 1.5),
    transformFn = ([h, s, l]) => [h, s, l],
    hueList
  } = {}) {
    const lDiff = lRange[1] - lRange[0];
    const sDiff = sRange[1] - sRange[0];
    const length = hueList && hueList.length > 0 ? hueList.length : total;
    return Array.from({ length }, (_, i) => {
      const relI = length > 1 ? i / (length - 1) : 0;
      const fraction = 1 / length;
      const hue = hueList ? hueList[i] : normalizeHue(
        hStart + // Add the starting hue
        (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)
        // Calculate the hue based on the easing function
      );
      const saturation = sRange[0] + sDiff * sEasing(relI, fraction);
      const lightness = lRange[0] + lDiff * lEasing(relI, fraction);
      return transformFn([hue, saturation, lightness], i);
    });
  }
  var generateColorRampWithCurve = ({
    total = 9,
    hStart = Math.random() * 360,
    hStartCenter = 0.5,
    hCycles = 1,
    sRange = [0.4, 0.35],
    lRange = [Math.random() * 0.1, 0.9],
    hueList,
    curveMethod = "lam\xE9",
    curveAccent = 0.5,
    transformFn = ([h, s, l]) => [h, s, l]
  } = {}) => {
    const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);
    return generateColorRamp({
      total,
      hStart,
      hStartCenter,
      hCycles,
      sRange,
      lRange,
      sEasing,
      lEasing,
      transformFn,
      hueList
    });
  };

  // src/index.ts
  var generateColorRampParams = {
    total: {
      default: 5,
      props: { min: 4, max: 50, step: 1 }
    },
    hStart: {
      default: 0,
      props: { min: 0, max: 360, step: 0.1 }
    },
    hCycles: {
      default: 1,
      props: { min: -2, max: 2, step: 1e-3 }
    },
    hStartCenter: {
      default: 0.5,
      props: { min: 0, max: 1, step: 1e-3 }
    },
    minLight: {
      default: Math.random() * 0.2,
      props: { min: 0, max: 1, step: 1e-3 }
    },
    maxLight: {
      default: 0.89 + Math.random() * 0.11,
      props: { min: 0, max: 1, step: 1e-3 }
    },
    minSaturation: {
      default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,
      props: { min: 0, max: 1, step: 1e-3 }
    },
    maxSaturation: {
      default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,
      props: { min: 0, max: 1, step: 1e-3 }
    },
    curveMethod: {
      default: "lam\xE9",
      props: { options: ["lam\xE9", "sine", "power", "linear"] }
    },
    curveAccent: {
      default: 0.5,
      props: { min: 0, max: 5, step: 0.01 }
    }
  };
  return __toCommonJS(index_exports);
})();


================================================
FILE: dist/index.min.cjs
================================================
"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}}};


================================================
FILE: dist/index.min.mjs
================================================
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};


================================================
FILE: dist/index.mjs
================================================
var __defProp = Object.defineProperty;
var __export = (target, all) => {
  for (var name in all)
    __defProp(target, name, { get: all[name], enumerable: true });
};

// src/utils.ts
var utils_exports = {};
__export(utils_exports, {
  lerp: () => lerp,
  makeCurveEasings: () => makeCurveEasings,
  pointOnCurve: () => pointOnCurve,
  scaleSpreadArray: () => scaleSpreadArray,
  shuffleArray: () => shuffleArray
});
function shuffleArray(array, rndFn = Math.random) {
  const copy = [...array];
  let currentIndex = copy.length, randomIndex;
  while (currentIndex != 0) {
    randomIndex = Math.floor(rndFn() * currentIndex);
    currentIndex--;
    [copy[currentIndex], copy[randomIndex]] = [
      copy[randomIndex],
      copy[currentIndex]
    ];
  }
  return copy;
}
var lerp = (amt, from, to) => from + amt * (to - from);
var scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {
  if (!valuesToFill || valuesToFill.length < 2) {
    throw new Error("valuesToFill array must have at least two values.");
  }
  if (targetSize < 1 && padding > 0) {
    throw new Error("Target size must be at least 1");
  }
  if (targetSize < valuesToFill.length && padding === 0) {
    throw new Error(
      "Target size must be greater than or equal to the valuesToFill array length."
    );
  }
  const result = new Array(targetSize);
  if (padding <= 0) {
    const len = valuesToFill.length;
    const lastIdx = len - 1;
    const totalAdded = targetSize - len;
    const baseAdds = Math.floor(totalAdded / lastIdx);
    const remainder = totalAdded % lastIdx;
    let currentResultIdx = 0;
    for (let i = 0; i < lastIdx; i++) {
      const startVal = valuesToFill[i];
      const endVal = valuesToFill[i + 1];
      const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);
      for (let j = 0; j < segmentLen; j++) {
        const t = j / segmentLen;
        result[currentResultIdx++] = fillFunction(t, startVal, endVal);
      }
    }
    result[currentResultIdx] = valuesToFill[lastIdx];
    return result;
  }
  const domainStart = padding;
  const domainEnd = 1 - padding;
  const lenMinus1 = valuesToFill.length - 1;
  const normalizedPositions = new Float64Array(valuesToFill.length);
  for (let i = 0; i < valuesToFill.length; i++) {
    normalizedPositions[i] = i / lenMinus1;
  }
  let segmentIndex = 0;
  for (let i = 0; i < targetSize; i++) {
    const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);
    const adjustedT = domainStart + t * (domainEnd - domainStart);
    while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {
      segmentIndex++;
    }
    const segmentStart = normalizedPositions[segmentIndex];
    const segmentEnd = normalizedPositions[segmentIndex + 1];
    let segmentT = 0;
    if (segmentEnd > segmentStart) {
      segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);
    }
    const fromValue = valuesToFill[segmentIndex];
    const toValue = valuesToFill[segmentIndex + 1];
    result[i] = fillFunction(segmentT, fromValue, toValue);
  }
  return result;
};
var pointOnCurve = (curveMethod, curveAccent) => {
  return (t) => {
    const limit = Math.PI / 2;
    const slice = limit / 1;
    const percentile = t;
    let x = 0, y = 0;
    if (curveMethod === "lam\xE9") {
      const t2 = percentile * limit;
      const exp = 2 / (2 + 20 * curveAccent);
      const cosT = Math.cos(t2);
      const sinT = Math.sin(t2);
      x = Math.sign(cosT) * Math.abs(cosT) ** exp;
      y = Math.sign(sinT) * Math.abs(sinT) ** exp;
    } else if (curveMethod === "arc") {
      y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);
      x = Math.sin(Math.PI / 2 + t * slice - curveAccent);
    } else if (curveMethod === "pow") {
      x = Math.pow(1 - percentile, 1 - curveAccent);
      y = Math.pow(percentile, 1 - curveAccent);
    } else if (curveMethod === "powY") {
      x = Math.pow(1 - percentile, curveAccent);
      y = Math.pow(percentile, 1 - curveAccent);
    } else if (curveMethod === "powX") {
      x = Math.pow(percentile, curveAccent);
      y = Math.pow(percentile, 1 - curveAccent);
    } else if (typeof curveMethod === "function") {
      const [xFunc, yFunc] = curveMethod(t, curveAccent);
      x = xFunc;
      y = yFunc;
    } else {
      throw new Error(
        `pointOnCurve() curveMethod parameter is expected to be "lam\xE9" | "arc" | "pow" | "powY" | "powX" or a function but \`${curveMethod}\` given.`
      );
    }
    return { x, y };
  };
};
var makeCurveEasings = (curveMethod, curveAccent) => {
  const point = pointOnCurve(curveMethod, curveAccent);
  return {
    sEasing: (t) => point(t).x,
    lEasing: (t) => point(t).y
  };
};

// src/colorUtils.ts
var colorUtils_exports = {};
__export(colorUtils_exports, {
  colorHarmonies: () => colorHarmonies,
  colorToCSS: () => colorToCSS,
  harveyHue: () => harveyHue,
  hsv2hsl: () => hsv2hsl,
  normalizeHue: () => normalizeHue,
  uniqueRandomHues: () => uniqueRandomHues
});
function normalizeHue(h) {
  return (h % 360 + 360) % 360;
}
function harveyHue(h) {
  h = normalizeHue(h) / 360;
  if (h === 1 || h === 0) return h;
  h = 1 + h % 1;
  const seg = 1 / 6;
  const a = h % seg / seg * Math.PI / 2;
  const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];
  const i = Math.floor(h * 6);
  const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];
  return cases[i % 6] * 360;
}
var colorHarmonies = {
  complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],
  splitComplementary: (h) => [
    normalizeHue(h),
    normalizeHue(h + 150),
    normalizeHue(h - 150)
  ],
  triadic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 120),
    normalizeHue(h + 240)
  ],
  tetradic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 90),
    normalizeHue(h + 180),
    normalizeHue(h + 270)
  ],
  pentadic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 72),
    normalizeHue(h + 144),
    normalizeHue(h + 216),
    normalizeHue(h + 288)
  ],
  hexadic: (h) => [
    normalizeHue(h),
    normalizeHue(h + 60),
    normalizeHue(h + 120),
    normalizeHue(h + 180),
    normalizeHue(h + 240),
    normalizeHue(h + 300)
  ],
  monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],
  // min 2 for RampenSau
  doubleComplementary: (h) => [
    normalizeHue(h),
    normalizeHue(h + 180),
    normalizeHue(h + 30),
    normalizeHue(h + 210)
  ],
  compound: (h) => [
    normalizeHue(h),
    normalizeHue(h + 180),
    normalizeHue(h + 60),
    normalizeHue(h + 240)
  ],
  analogous: (h) => [
    normalizeHue(h),
    normalizeHue(h + 30),
    normalizeHue(h + 60),
    normalizeHue(h + 90),
    normalizeHue(h + 120),
    normalizeHue(h + 150)
  ]
};
function uniqueRandomHues({
  startHue = 0,
  total = 9,
  minHueDiffAngle = 60,
  rndFn = Math.random
} = {}) {
  minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);
  const baseHue = startHue != null ? startHue : rndFn() * 360;
  const huesToPickFrom = Array.from(
    {
      length: Math.round(360 / minHueDiffAngle)
    },
    (_, i) => (baseHue + i * minHueDiffAngle) % 360
  );
  let randomizedHues = shuffleArray(huesToPickFrom, rndFn);
  if (randomizedHues.length > total) {
    randomizedHues = randomizedHues.slice(0, total);
  }
  return randomizedHues;
}
var hsv2hsl = ([h, s, v]) => {
  const l = v - v * s / 2;
  const m = Math.min(l, 1 - l);
  const s_hsl = m === 0 ? 0 : (v - l) / m;
  return [h, s_hsl, l];
};
var colorModsCSS = {
  oklch: (color) => [
    color[2] * 100 + "%",
    color[1] * 100 + "%",
    color[0]
  ],
  lch: (color) => [
    color[2] * 100 + "%",
    color[1] * 100 + "%",
    color[0]
  ],
  hsl: (color) => [
    color[0],
    color[1] * 100 + "%",
    color[2] * 100 + "%"
  ],
  hsv: (color) => {
    const [h, s, l] = hsv2hsl(color);
    return [h, s * 100 + "%", l * 100 + "%"];
  }
};
var colorToCSS = (color, mode = "oklch") => {
  const cssMode = mode === "hsv" ? "hsl" : mode;
  return `${cssMode}(${colorModsCSS[mode](color).join(" ")})`;
};

// src/core.ts
function generateColorRamp({
  total = 9,
  hStart = Math.random() * 360,
  hStartCenter = 0.5,
  hEasing = (x) => x,
  hCycles = 1,
  sRange = [0.4, 0.35],
  sEasing = (x) => Math.pow(x, 2),
  lRange = [Math.random() * 0.1, 0.9],
  lEasing = (x) => Math.pow(x, 1.5),
  transformFn = ([h, s, l]) => [h, s, l],
  hueList
} = {}) {
  const lDiff = lRange[1] - lRange[0];
  const sDiff = sRange[1] - sRange[0];
  const length = hueList && hueList.length > 0 ? hueList.length : total;
  return Array.from({ length }, (_, i) => {
    const relI = length > 1 ? i / (length - 1) : 0;
    const fraction = 1 / length;
    const hue = hueList ? hueList[i] : normalizeHue(
      hStart + // Add the starting hue
      (1 - hEasing(relI, fraction) - hStartCenter) * (360 * hCycles)
      // Calculate the hue based on the easing function
    );
    const saturation = sRange[0] + sDiff * sEasing(relI, fraction);
    const lightness = lRange[0] + lDiff * lEasing(relI, fraction);
    return transformFn([hue, saturation, lightness], i);
  });
}
var generateColorRampWithCurve = ({
  total = 9,
  hStart = Math.random() * 360,
  hStartCenter = 0.5,
  hCycles = 1,
  sRange = [0.4, 0.35],
  lRange = [Math.random() * 0.1, 0.9],
  hueList,
  curveMethod = "lam\xE9",
  curveAccent = 0.5,
  transformFn = ([h, s, l]) => [h, s, l]
} = {}) => {
  const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent);
  return generateColorRamp({
    total,
    hStart,
    hStartCenter,
    hCycles,
    sRange,
    lRange,
    sEasing,
    lEasing,
    transformFn,
    hueList
  });
};

// src/index.ts
var generateColorRampParams = {
  total: {
    default: 5,
    props: { min: 4, max: 50, step: 1 }
  },
  hStart: {
    default: 0,
    props: { min: 0, max: 360, step: 0.1 }
  },
  hCycles: {
    default: 1,
    props: { min: -2, max: 2, step: 1e-3 }
  },
  hStartCenter: {
    default: 0.5,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  minLight: {
    default: Math.random() * 0.2,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  maxLight: {
    default: 0.89 + Math.random() * 0.11,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  minSaturation: {
    default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  maxSaturation: {
    default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1,
    props: { min: 0, max: 1, step: 1e-3 }
  },
  curveMethod: {
    default: "lam\xE9",
    props: { options: ["lam\xE9", "sine", "power", "linear"] }
  },
  curveAccent: {
    default: 0.5,
    props: { min: 0, max: 5, step: 0.01 }
  }
};
export {
  colorUtils_exports as colorUtils,
  generateColorRamp,
  generateColorRampParams,
  generateColorRampWithCurve,
  utils_exports as utils
};


================================================
FILE: dist/index.umd.js
================================================
(function(root, factory) {
      if (typeof define === 'function' && define.amd) {
      	define([], factory);
      } else if (typeof module === 'object' && module.exports) {
      	module.exports = factory();
      } else {
      	root.rampensau = factory();
      }
    }
    (typeof self !== 'undefined' ? self : this, function() {
"use strict";
var rampensau = (() => {
  var __defProp = Object.defineProperty;
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
  var __getOwnPropNames = Object.getOwnPropertyNames;
  var __hasOwnProp = Object.prototype.hasOwnProperty;
  var __pow = Math.pow;
  var __export = (target, all) => {
    for (var name in all)
      __defProp(target, name, { get: all[name], enumerable: true });
  };
  var __copyProps = (to, from, except, desc) => {
    if (from && typeof from === "object" || typeof from === "function") {
      for (let key of __getOwnPropNames(from))
        if (!__hasOwnProp.call(to, key) && key !== except)
          __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
    }
    return to;
  };
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);

  // src/index.ts
  var index_exports = {};
  __export(index_exports, {
    colorUtils: () => colorUtils_exports,
    generateColorRamp: () => generateColorRamp,
    generateColorRampParams: () => generateColorRampParams,
    generateColorRampWithCurve: () => generateColorRampWithCurve,
    utils: () => utils_exports
  });

  // src/utils.ts
  var utils_exports = {};
  __export(utils_exports, {
    lerp: () => lerp,
    makeCurveEasings: () => makeCurveEasings,
    pointOnCurve: () => pointOnCurve,
    scaleSpreadArray: () => scaleSpreadArray,
    shuffleArray: () => shuffleArray
  });
  function shuffleArray(array, rndFn = Math.random) {
    const copy = [...array];
    let currentIndex = copy.length, randomIndex;
    while (currentIndex != 0) {
      randomIndex = Math.floor(rndFn() * currentIndex);
      currentIndex--;
      [copy[currentIndex], copy[randomIndex]] = [
        copy[randomIndex],
        copy[currentIndex]
      ];
    }
    return copy;
  }
  var lerp = (amt, from, to) => from + amt * (to - from);
  var scaleSpreadArray = (valuesToFill, targetSize, padding = 0, fillFunction = lerp) => {
    if (!valuesToFill || valuesToFill.length < 2) {
      throw new Error("valuesToFill array must have at least two values.");
    }
    if (targetSize < 1 && padding > 0) {
      throw new Error("Target size must be at least 1");
    }
    if (targetSize < valuesToFill.length && padding === 0) {
      throw new Error(
        "Target size must be greater than or equal to the valuesToFill array length."
      );
    }
    const result = new Array(targetSize);
    if (padding <= 0) {
      const len = valuesToFill.length;
      const lastIdx = len - 1;
      const totalAdded = targetSize - len;
      const baseAdds = Math.floor(totalAdded / lastIdx);
      const remainder = totalAdded % lastIdx;
      let currentResultIdx = 0;
      for (let i = 0; i < lastIdx; i++) {
        const startVal = valuesToFill[i];
        const endVal = valuesToFill[i + 1];
        const segmentLen = 1 + baseAdds + (i < remainder ? 1 : 0);
        for (let j = 0; j < segmentLen; j++) {
          const t = j / segmentLen;
          result[currentResultIdx++] = fillFunction(t, startVal, endVal);
        }
      }
      result[currentResultIdx] = valuesToFill[lastIdx];
      return result;
    }
    const domainStart = padding;
    const domainEnd = 1 - padding;
    const lenMinus1 = valuesToFill.length - 1;
    const normalizedPositions = new Float64Array(valuesToFill.length);
    for (let i = 0; i < valuesToFill.length; i++) {
      normalizedPositions[i] = i / lenMinus1;
    }
    let segmentIndex = 0;
    for (let i = 0; i < targetSize; i++) {
      const t = targetSize === 1 ? 0.5 : i / (targetSize - 1);
      const adjustedT = domainStart + t * (domainEnd - domainStart);
      while (segmentIndex < lenMinus1 && adjustedT > normalizedPositions[segmentIndex + 1]) {
        segmentIndex++;
      }
      const segmentStart = normalizedPositions[segmentIndex];
      const segmentEnd = normalizedPositions[segmentIndex + 1];
      let segmentT = 0;
      if (segmentEnd > segmentStart) {
        segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart);
      }
      const fromValue = valuesToFill[segmentIndex];
      const toValue = valuesToFill[segmentIndex + 1];
      result[i] = fillFunction(segmentT, fromValue, toValue);
    }
    return result;
  };
  var pointOnCurve = (curveMethod, curveAccent) => {
    return (t) => {
      const limit = Math.PI / 2;
      const slice = limit / 1;
      const percentile = t;
      let x = 0, y = 0;
      if (curveMethod === "lam\xE9") {
        const t2 = percentile * limit;
        const exp = 2 / (2 + 20 * curveAccent);
        const cosT = Math.cos(t2);
        const sinT = Math.sin(t2);
        x = Math.sign(cosT) * __pow(Math.abs(cosT), exp);
        y = Math.sign(sinT) * __pow(Math.abs(sinT), exp);
      } else if (curveMethod === "arc") {
        y = Math.cos(-Math.PI / 2 + t * slice + curveAccent);
        x = Math.sin(Math.PI / 2 + t * slice - curveAccent);
      } else if (curveMethod === "pow") {
        x = Math.pow(1 - percentile, 1 - curveAccent);
        y = Math.pow(percentile, 1 - curveAccent);
      } else if (curveMethod === "powY") {
        x = Math.pow(1 - percentile, curveAccent);
        y = Math.pow(percentile, 1 - curveAccent);
      } else if (curveMethod === "powX") {
        x = Math.pow(percentile, curveAccent);
        y = Math.pow(percentile, 1 - curveAccent);
      } else if (typeof curveMethod === "function") {
        const [xFunc, yFunc] = curveMethod(t, curveAccent);
        x = xFunc;
        y = yFunc;
      } else {
        throw new Error(
          `pointOnCurve() curveMethod parameter is expected to be "lam\xE9" | "arc" | "pow" | "powY" | "powX" or a function but \`${curveMethod}\` given.`
        );
      }
      return { x, y };
    };
  };
  var makeCurveEasings = (curveMethod, curveAccent) => {
    const point = pointOnCurve(curveMethod, curveAccent);
    return {
      sEasing: (t) => point(t).x,
      lEasing: (t) => point(t).y
    };
  };

  // src/colorUtils.ts
  var colorUtils_exports = {};
  __export(colorUtils_exports, {
    colorHarmonies: () => colorHarmonies,
    colorToCSS: () => colorToCSS,
    harveyHue: () => harveyHue,
    hsv2hsl: () => hsv2hsl,
    normalizeHue: () => normalizeHue,
    uniqueRandomHues: () => uniqueRandomHues
  });
  function normalizeHue(h) {
    return (h % 360 + 360) % 360;
  }
  function harveyHue(h) {
    h = normalizeHue(h) / 360;
    if (h === 1 || h === 0) return h;
    h = 1 + h % 1;
    const seg = 1 / 6;
    const a = h % seg / seg * Math.PI / 2;
    const [b, c] = [seg * Math.cos(a), seg * Math.sin(a)];
    const i = Math.floor(h * 6);
    const cases = [c, 1 / 3 - b, 1 / 3 + c, 2 / 3 - b, 2 / 3 + c, 1 - b];
    return cases[i % 6] * 360;
  }
  var colorHarmonies = {
    complementary: (h) => [normalizeHue(h), normalizeHue(h + 180)],
    splitComplementary: (h) => [
      normalizeHue(h),
      normalizeHue(h + 150),
      normalizeHue(h - 150)
    ],
    triadic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 120),
      normalizeHue(h + 240)
    ],
    tetradic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 90),
      normalizeHue(h + 180),
      normalizeHue(h + 270)
    ],
    pentadic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 72),
      normalizeHue(h + 144),
      normalizeHue(h + 216),
      normalizeHue(h + 288)
    ],
    hexadic: (h) => [
      normalizeHue(h),
      normalizeHue(h + 60),
      normalizeHue(h + 120),
      normalizeHue(h + 180),
      normalizeHue(h + 240),
      normalizeHue(h + 300)
    ],
    monochromatic: (h) => [normalizeHue(h), normalizeHue(h)],
    // min 2 for RampenSau
    doubleComplementary: (h) => [
      normalizeHue(h),
      normalizeHue(h + 180),
      normalizeHue(h + 30),
      normalizeHue(h + 210)
    ],
    compound: (h) => [
      normalizeHue(h),
      normalizeHue(h + 180),
      normalizeHue(h + 60),
      normalizeHue(h + 240)
    ],
    analogous: (h) => [
      normalizeHue(h),
      normalizeHue(h + 30),
      normalizeHue(h + 60),
      normalizeHue(h + 90),
      normalizeHue(h + 120),
      normalizeHue(h + 150)
    ]
  };
  function uniqueRandomHues({
    startHue = 0,
    total = 9,
    minHueDiffAngle = 60,
    rndFn = Math.random
  } = {}) {
    minHueDiffAngle = Math.min(minHueDiffAngle, 360 / total);
    const baseHue = startHue != null ? startHue : rndFn() * 360;
    const huesToPickFrom = Array.from(
      {
        length: Math.round(360 / minHueDiffAngle)
      },
      (_, i) => (baseHue + i * minHueDiffAngle) % 360
    );
    let randomizedHues = shuffleArray(huesToPickFrom, rndFn);
    if (randomizedHues.length > total) {
      randomizedHues = randomizedHues.slice(0, total);
    }
    return randomizedHues;
  }
  var hsv2hsl = ([h, s, v]) => {
    const l = v - v * s / 2;
    const m = Math.min(l, 1 - l);
    const s_hsl = m === 0 ? 0 : (v - l) / m;
    return [h, s_hsl, l];
  };
  var colorModsCSS = {
    oklch: (color) => [
      color[2] * 100 + "%",
      color[1] * 100 + "%",
      color[0]
    ],
    lch: (color) => [
      color[2] * 100 + "%",
      color[1] * 100 + "%",
      color[0]
    ],
    hsl: (color) => [
      color[0],
      color[1] * 100 + "%",
      color[2] * 100 + "%"
    ],
    hsv: (color) => {
      const [h, s, l] = hsv2hsl(color);
      return [h, s * 100 + "%", l * 100 + "%"];
    }
  };
  var colorToCSS = (color, mode = "oklch") => {
    const cssMode = mode === "hsv" ? "hsl" : mode;
    return `${cssMode}(${colorModsCSS[mode](color).join(" ")})`;
  };

  // src/core.ts
  function generateColorRamp({
    total = 9,
    hStart = Math.random() * 360,
    hStartCenter = 0.5,
    hEasing = (x) => x,
    hCycles = 1,
    sRange = [0.4, 0.35],
    sEasing = (x) => Math.pow(x, 2),
    lRange = [Math.random() * 0.1, 0.9],
    lEasing = (x) => Math.p
Download .txt
gitextract_ws2pvfso/

├── .eslintrc
├── .github/
│   └── workflows/
│       └── main.yml
├── .gitignore
├── .prettierrc.json
├── LICENSE
├── README.md
├── build.js
├── dist/
│   ├── colorUtils.d.ts
│   ├── core.d.ts
│   ├── highlighter.html
│   ├── index.cjs
│   ├── index.d.ts
│   ├── index.html
│   ├── index.js
│   ├── index.min.cjs
│   ├── index.min.mjs
│   ├── index.mjs
│   ├── index.umd.js
│   └── utils.d.ts
├── package.json
├── src/
│   ├── colorUtils.ts
│   ├── core.ts
│   ├── index.ts
│   └── utils.ts
├── tea.yaml
├── test/
│   ├── colorUtils.test.ts
│   ├── core.test.ts
│   └── utils.test.ts
├── tsconfig.json
└── vitest.config.ts
Download .txt
SYMBOL INDEX (67 symbols across 12 files)

FILE: dist/colorUtils.d.ts
  type Vector2 (line 1) | type Vector2 = [number, number];
  type Vector3 (line 2) | type Vector3 = [...Vector2, number];
  type colorHarmony (line 16) | type colorHarmony = "complementary" | "splitComplementary" | "triadic" |...
  type colorHarmonyFn (line 17) | type colorHarmonyFn = (h: number) => number[];
  type uniqueRandomHuesArguments (line 27) | type uniqueRandomHuesArguments = {
  type colorToCSSMode (line 50) | type colorToCSSMode = "oklch" | "lch" | "hsl" | "hsv";

FILE: dist/core.d.ts
  type ModifiedEasingFn (line 3) | type ModifiedEasingFn = (x: number, fr?: number) => number;
  type hueArguments (line 4) | type hueArguments = {
  type presetHues (line 10) | type presetHues = {
  type saturationArguments (line 13) | type saturationArguments = {
  type lightnessArguments (line 17) | type lightnessArguments = {
  type BaseGenerateColorRampArgument (line 21) | type BaseGenerateColorRampArgument = {
  type GenerateColorRampArgument (line 25) | type GenerateColorRampArgument = BaseGenerateColorRampArgument & {
  type GenerateColorRampArgumentFixedHues (line 28) | type GenerateColorRampArgumentFixedHues = BaseGenerateColorRampArgument ...

FILE: dist/index.cjs
  function shuffleArray (line 40) | function shuffleArray(array, rndFn = Math.random) {
  function normalizeHue (line 167) | function normalizeHue(h) {
  function harveyHue (line 170) | function harveyHue(h) {
  function uniqueRandomHues (line 237) | function uniqueRandomHues({
  function generateColorRamp (line 290) | function generateColorRamp({

FILE: dist/index.js
  function shuffleArray (line 40) | function shuffleArray(array, rndFn = Math.random) {
  function normalizeHue (line 167) | function normalizeHue(h) {
  function harveyHue (line 170) | function harveyHue(h) {
  function uniqueRandomHues (line 237) | function uniqueRandomHues({
  function generateColorRamp (line 290) | function generateColorRamp({

FILE: dist/index.min.cjs
  function V (line 1) | function V(e,t=Math.random){let r=[...e],o=r.length,a;for(;o!=0;)a=Math....
  function n (line 1) | function n(e){return(e%360+360)%360}
  function _ (line 1) | function _(e){if(e=n(e)/360,e===1||e===0)return e;e=1+e%1;let t=1/6,r=e%...
  function O (line 1) | function O({startHue:e=0,total:t=9,minHueDiffAngle:r=60,rndFn:o=Math.ran...
  function R (line 1) | function R({total:e=9,hStart:t=Math.random()*360,hStartCenter:r=.5,hEasi...

FILE: dist/index.min.mjs
  function w (line 1) | function w(e,t=Math.random){let r=[...e],o=r.length,a;for(;o!=0;)a=Math....
  function n (line 1) | function n(e){return(e%360+360)%360}
  function P (line 1) | function P(e){if(e=n(e)/360,e===1||e===0)return e;e=1+e%1;let t=1/6,r=e%...
  function q (line 1) | function q({startHue:e=0,total:t=9,minHueDiffAngle:r=60,rndFn:o=Math.ran...
  function S (line 1) | function S({total:e=9,hStart:t=Math.random()*360,hStartCenter:r=.5,hEasi...

FILE: dist/index.mjs
  function shuffleArray (line 16) | function shuffleArray(array, rndFn = Math.random) {
  function normalizeHue (line 143) | function normalizeHue(h) {
  function harveyHue (line 146) | function harveyHue(h) {
  function uniqueRandomHues (line 213) | function uniqueRandomHues({
  function generateColorRamp (line 266) | function generateColorRamp({

FILE: dist/index.umd.js
  function shuffleArray (line 51) | function shuffleArray(array, rndFn = Math.random) {
  function normalizeHue (line 178) | function normalizeHue(h) {
  function harveyHue (line 181) | function harveyHue(h) {
  function uniqueRandomHues (line 248) | function uniqueRandomHues({
  function generateColorRamp (line 301) | function generateColorRamp({

FILE: dist/utils.d.ts
  type FillFunction (line 8) | type FillFunction<T> = T extends number ? (amt: number, from: T, to: T) ...
  type CurveMethod (line 43) | type CurveMethod = "lamé" | "arc" | "pow" | "powY" | "powX" | ((i: numbe...

FILE: src/colorUtils.ts
  type Vector2 (line 3) | type Vector2 = [number, number];
  type Vector3 (line 4) | type Vector3 = [...Vector2, number];
  function normalizeHue (line 11) | function normalizeHue(h: number): number {
  function harveyHue (line 21) | function harveyHue(h: number): number {
  type colorHarmony (line 37) | type colorHarmony =
  type colorHarmonyFn (line 48) | type colorHarmonyFn = (h: number) => number[];
  type uniqueRandomHuesArguments (line 114) | type uniqueRandomHuesArguments = {
  function uniqueRandomHues (line 125) | function uniqueRandomHues({
  type colorToCSSMode (line 184) | type colorToCSSMode = "oklch" | "lch" | "hsl" | "hsv";

FILE: src/core.ts
  type ModifiedEasingFn (line 7) | type ModifiedEasingFn = (x: number, fr?: number) => number;
  type hueArguments (line 9) | type hueArguments = {
  type presetHues (line 15) | type presetHues = {
  type saturationArguments (line 18) | type saturationArguments = {
  type lightnessArguments (line 22) | type lightnessArguments = {
  type BaseGenerateColorRampArgument (line 27) | type BaseGenerateColorRampArgument = {
  type GenerateColorRampArgument (line 34) | type GenerateColorRampArgument = BaseGenerateColorRampArgument & {
  type GenerateColorRampArgumentFixedHues (line 38) | type GenerateColorRampArgumentFixedHues = BaseGenerateColorRampArgument &
  function generateColorRamp (line 46) | function generateColorRamp({

FILE: src/utils.ts
  function shuffleArray (line 8) | function shuffleArray<T>(array: readonly T[], rndFn = Math.random): T[] {
  type FillFunction (line 28) | type FillFunction<T> = T extends number
  type CurveMethod (line 179) | type CurveMethod =
Condensed preview — 30 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (250K chars).
[
  {
    "path": ".eslintrc",
    "chars": 239,
    "preview": "{\n  \"root\": true,\n  \"parser\": \"@typescript-eslint/parser\",\n  \"plugins\": [\"@typescript-eslint\"],\n  \"extends\": [\n    \"esli"
  },
  {
    "path": ".github/workflows/main.yml",
    "chars": 878,
    "preview": "name: Build and Deploy\non: [push]\njobs:\n  build-and-deploy:\n    concurrency: ci-${{ github.ref }} # Recommended if you i"
  },
  {
    "path": ".gitignore",
    "chars": 1605,
    "preview": "# Logs\nlogs\n*.log\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\nlerna-debug.log*\n\n# Diagnostic reports (https://nodejs."
  },
  {
    "path": ".prettierrc.json",
    "chars": 3,
    "preview": "{}\n"
  },
  {
    "path": "LICENSE",
    "chars": 1068,
    "preview": "MIT License\n\nCopyright (c) 2021 David Aerne\n\nPermission is hereby granted, free of charge, to any person obtaining a cop"
  },
  {
    "path": "README.md",
    "chars": 16477,
    "preview": "# RampenSau 🎢🐷🎨\n\n**RampenSau** is a color palette generation library that utilizes **hue cycling** and\n**easing function"
  },
  {
    "path": "build.js",
    "chars": 1819,
    "preview": "import { build } from \"esbuild\";\n\n// Bundled CJS\nbuild({\n  entryPoints: [\"./src/index.ts\"],\n  logLevel: \"info\",\n  bundle"
  },
  {
    "path": "dist/colorUtils.d.ts",
    "chars": 2411,
    "preview": "export declare type Vector2 = [number, number];\nexport declare type Vector3 = [...Vector2, number];\n/**\n * Converts a co"
  },
  {
    "path": "dist/core.d.ts",
    "chars": 1732,
    "preview": "import type { Vector2, Vector3 } from \"./colorUtils\";\nimport type { CurveMethod } from \"./utils\";\nexport declare type Mo"
  },
  {
    "path": "dist/highlighter.html",
    "chars": 13808,
    "preview": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <title>RampenSau Syntax Highlighter Demo</tit"
  },
  {
    "path": "dist/index.cjs",
    "chars": 11565,
    "preview": "\"use strict\";\nvar __defProp = Object.defineProperty;\nvar __getOwnPropDesc = Object.getOwnPropertyDescriptor;\nvar __getOw"
  },
  {
    "path": "dist/index.d.ts",
    "chars": 1820,
    "preview": "export * as utils from \"./utils\";\nexport * as colorUtils from \"./colorUtils\";\nexport { generateColorRamp, generateColorR"
  },
  {
    "path": "dist/index.html",
    "chars": 101117,
    "preview": "<!DOCTYPE html>\n\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\" />\n    <!-- Primary Meta Tags -->\n    <title>\n     "
  },
  {
    "path": "dist/index.js",
    "chars": 12350,
    "preview": "\"use strict\";\nvar rampensau = (() => {\n  var __defProp = Object.defineProperty;\n  var __getOwnPropDesc = Object.getOwnPr"
  },
  {
    "path": "dist/index.min.cjs",
    "chars": 5175,
    "preview": "\"use strict\";var w=Object.defineProperty;var P=Object.getOwnPropertyDescriptor;var j=Object.getOwnPropertyNames;var q=Ob"
  },
  {
    "path": "dist/index.min.mjs",
    "chars": 4814,
    "preview": "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,makeC"
  },
  {
    "path": "dist/index.mjs",
    "chars": 10748,
    "preview": "var __defProp = Object.defineProperty;\nvar __export = (target, all) => {\n  for (var name in all)\n    __defProp(target, n"
  },
  {
    "path": "dist/index.umd.js",
    "chars": 12761,
    "preview": "(function(root, factory) {\n      if (typeof define === 'function' && define.amd) {\n      \tdefine([], factory);\n      } e"
  },
  {
    "path": "dist/utils.d.ts",
    "chars": 3342,
    "preview": "/**\n * returns a new shuffled array\n * @param {Array} array - The array to shuffle.\n * @param {function} rndFn - The ran"
  },
  {
    "path": "package.json",
    "chars": 1400,
    "preview": "{\n  \"name\": \"rampensau\",\n  \"version\": \"2.3.0\",\n  \"description\": \"Color ramp generator using curves within the HSL color "
  },
  {
    "path": "src/colorUtils.ts",
    "chars": 5294,
    "preview": "import { shuffleArray } from \"./utils.js\";\n\nexport type Vector2 = [number, number];\nexport type Vector3 = [...Vector2, n"
  },
  {
    "path": "src/core.ts",
    "chars": 3265,
    "preview": "import { makeCurveEasings } from \"./utils\";\nimport { normalizeHue } from \"./colorUtils\";\n\nimport type { Vector2, Vector3"
  },
  {
    "path": "src/index.ts",
    "chars": 1302,
    "preview": "export * as utils from \"./utils\";\nexport * as colorUtils from \"./colorUtils\";\n\nexport { generateColorRamp, generateColor"
  },
  {
    "path": "src/utils.ts",
    "chars": 8995,
    "preview": "/**\n * returns a new shuffled array\n * @param {Array} array - The array to shuffle.\n * @param {function} rndFn - The ran"
  },
  {
    "path": "tea.yaml",
    "chars": 126,
    "preview": "# https://tea.xyz/what-is-this-file\n---\nversion: 1.0.0\ncodeOwners:\n  - '0xFA64435d1281921E36b90CeA9a1fbf0e5c408e65'\nquor"
  },
  {
    "path": "test/colorUtils.test.ts",
    "chars": 6370,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { normalizeHue, harveyHue, colorHarmonies, uniqueRandomHues, hsv2h"
  },
  {
    "path": "test/core.test.ts",
    "chars": 3538,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { generateColorRamp, generateColorRampWithCurve } from '../src/cor"
  },
  {
    "path": "test/utils.test.ts",
    "chars": 4831,
    "preview": "import { describe, it, expect } from 'vitest';\nimport { shuffleArray, scaleSpreadArray, makeCurveEasings, pointOnCurve }"
  },
  {
    "path": "tsconfig.json",
    "chars": 325,
    "preview": "{\n  \"compilerOptions\": {\n    \"noImplicitAny\": true,\n    \"noEmitOnError\": true,\n    \"removeComments\": false,\n    \"sourceM"
  },
  {
    "path": "vitest.config.ts",
    "chars": 223,
    "preview": "import { defineConfig } from 'vitest/config';\n\nexport default defineConfig({\n  test: {\n    // environment: 'jsdom', // u"
  }
]

About this extraction

This page contains the full source code of the meodai/rampensau GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 30 files (233.8 KB), approximately 69.1k tokens, and a symbol index with 67 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!