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 ``` ## 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} - 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} - 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} - 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 ================================================ RampenSau Syntax Highlighter Demo
================================================ 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 ================================================ RampenSau — Color ramp generator using curves within the HSL color model

fork on github made by elastiq.

================================================ 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(tl[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(tl[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.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); })(); return rampensau; })); ================================================ FILE: dist/utils.d.ts ================================================ /** * returns a new shuffled array * @param {Array} array - The array to shuffle. * @param {function} rndFn - The random function to use. * @returns {Array} - The shuffled array. */ export declare function shuffleArray(array: readonly T[], rndFn?: () => number): T[]; declare type FillFunction = T extends number ? (amt: number, from: T, to: T) => T : (amt: number, from: T | null, to: T | null) => T; /** * Linearly interpolates between two values. * * @param {number} amt - The interpolation amount (usually between 0 and 1). * @param {number} from - The starting value. * @param {number} to - The ending value. * @returns {number} - The interpolated value. */ export declare const lerp: FillFunction; /** * Scales and spreads an array to the target size using interpolation, with optional padding. * * This function takes an initial array of values, a target size, an optional padding value, * and an interpolation function (defaults to `lerp`). It returns a scaled and spread * version of the initial array to the target size using the specified interpolation function. * * The padding parameter (between 0 and 1) compresses the normalized domain from both ends, * matching the behavior of chroma.js's scale() function. This is particularly useful for * color scales to prevent the endpoints from being too extreme. * * When padding is 0 (default), the original algorithm is used where values are distributed * and interpolated across segments. * * When padding > 0, the normalized domain (0-1) is compressed to [padding, 1-padding], * allowing for more graceful handling of extreme values. * * @param {Array} valuesToFill - The initial array of values. * @param {number} targetSize - The desired size of the resulting array. * @param {number} padding - Optional padding value between 0 and 1 (default: 0). * @param {FillFunction} fillFunction - The interpolation function (default is lerp). * @returns {Array} The scaled and spread array. * @throws {Error} If the initial array is invalid or target size is invalid. */ export declare const scaleSpreadArray: (valuesToFill: T[], targetSize: number, padding?: number, fillFunction?: FillFunction) => T[]; export declare type CurveMethod = "lamé" | "arc" | "pow" | "powY" | "powX" | ((i: number, curveAccent: number) => [number, number]); /** * function pointOnCurve * @param curveMethod {String|Function} Defines how the curve is drawn * @param curveAccent {Number} Defines the accent of the curve * @returns {Function} A function that takes a number between 0 and 1 and returns the x and y coordinates of the curve at that point * @throws {Error} If the curveMethod is not a valid type */ export declare const pointOnCurve: (curveMethod: CurveMethod, curveAccent: number) => (t: number) => { x: number; y: number; }; /** * makeCurveEasings generates two easing functions based on a curve method and accent. * @param {CurveMethod} curveMethod - The method used to generate the curve. * @param {number} curveAccent - The accent of the curve. * @returns {Object} An object containing two easing functions: sEasing and lEasing. */ export declare const makeCurveEasings: (curveMethod: CurveMethod, curveAccent: number) => { sEasing: (t: number) => number; lEasing: (t: number) => number; }; export {}; ================================================ FILE: package.json ================================================ { "name": "rampensau", "version": "2.3.0", "description": "Color ramp generator using curves within the HSL color model", "type": "module", "main": "./dist/index.cjs", "module": "./dist/index.mjs", "browser": "./dist/index.min.js", "jsdelivr": "./dist/index.umd.js", "exports": { "require": { "types": "./dist/index.d.ts", "default": "./dist/index.cjs" }, "import": { "types": "./dist/index.d.ts", "default": "./dist/index.mjs" } }, "scripts": { "build": "npm run lint && tsc --build && node ./build.js", "dev": "tsc --build --watch", "test": "vitest", "lint": "eslint . --ext .ts && npx prettier --check ./src/", "prettier": "npx prettier --write ./src/" }, "repository": { "type": "git", "url": "git+https://github.com/meodai/rampensau.git" }, "keywords": [ "color", "generative-art", "colour", "palette-generation", "generative" ], "author": "David Aerne @meodai", "license": "MIT", "bugs": { "url": "https://github.com/meodai/fettepalette/issues" }, "homepage": "https://github.com/meodai/fettepalette#readme", "devDependencies": { "@typescript-eslint/eslint-plugin": "^4.31.2", "@typescript-eslint/parser": "^4.31.2", "esbuild": "^0.25.3", "eslint": "^7.32.0", "prettier": "2.4.1", "typescript": "^4.4.3", "vitest": "^3.1.2" } } ================================================ FILE: src/colorUtils.ts ================================================ import { shuffleArray } from "./utils.js"; export type Vector2 = [number, number]; export 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 function normalizeHue(h: number): number { return ((h % 360) + 360) % 360; } /** * 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 function harveyHue(h: number): number { // modified this part to make it more usable with rampenSau h = normalizeHue(h) / 360; // this ensures the value stays within the 0-360 range and normalizes it to 0-1 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] as number) * 360; } export type colorHarmony = | "complementary" | "splitComplementary" | "triadic" | "tetradic" | "pentadic" | "hexadic" | "monochromatic" | "doubleComplementary" | "compound" | "analogous"; export 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} - The list of hues. */ export const colorHarmonies: { [key in colorHarmony]: colorHarmonyFn; } = { 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), ], }; export 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} - The list of hues. */ export function uniqueRandomHues({ startHue = 0, total = 9, minHueDiffAngle = 60, rndFn = Math.random, } = {}): number[] { 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; } /** * Converts a color from HSV to HSL. * @param {Array} hsv - The HSV color values. * @returns {Array} - The HSL color values. */ export const hsv2hsl = ([h, s, v]: Vector3): Vector3 => { 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]; }; /** * functions to convert from the ramp's colors values to CSS color functions. */ const colorModsCSS = { oklch: (color: Vector3) => [ color[2] * 100 + "%", color[1] * 100 + "%", color[0], ], lch: (color: Vector3) => [ color[2] * 100 + "%", color[1] * 100 + "%", color[0], ], hsl: (color: Vector3) => [ color[0], color[1] * 100 + "%", color[2] * 100 + "%", ], hsv: (color: Vector3) => { const [h, s, l] = hsv2hsl(color); return [h, s * 100 + "%", l * 100 + "%"]; }, }; export 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 const colorToCSS = ( color: Vector3, mode: colorToCSSMode = "oklch" ): string => { const cssMode = mode === "hsv" ? "hsl" : mode; // Use HSL for HSV input return `${cssMode}(${colorModsCSS[mode](color).join(" ")})`; }; ================================================ FILE: src/core.ts ================================================ import { makeCurveEasings } from "./utils"; import { normalizeHue } from "./colorUtils"; import type { Vector2, Vector3 } from "./colorUtils"; import type { CurveMethod } from "./utils"; export type ModifiedEasingFn = (x: number, fr?: number) => number; export type hueArguments = { hStart?: number; hStartCenter?: number; hCycles?: number; hEasing?: ModifiedEasingFn; }; export type presetHues = { hueList: number[]; }; export type saturationArguments = { sRange?: Vector2; sEasing?: ModifiedEasingFn; }; export type lightnessArguments = { lRange?: Vector2; lEasing?: ModifiedEasingFn; }; type BaseGenerateColorRampArgument = { total?: number; transformFn?: (hsl: Vector3, i?: number) => Vector3 | string; } & hueArguments & saturationArguments & lightnessArguments; export type GenerateColorRampArgument = BaseGenerateColorRampArgument & { hueList?: never; }; export 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} - The color ramp. */ export 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, }: | GenerateColorRampArgument | GenerateColorRampArgumentFixedHues = {}): Vector3[] { // creates a range of lightness and saturation based on the corresponding min and max values const lDiff: number = lRange[1] - lRange[0]; const sDiff: number = sRange[1] - sRange[0]; // if hueList is provided, use it's length as the length of the ramp 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] as number) : 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) as Vector3; // Ensure the array is of type Vector3 }); } export const 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é", curveAccent = 0.5, transformFn = ([h, s, l]) => [h, s, l], }: (GenerateColorRampArgument | GenerateColorRampArgumentFixedHues) & { curveMethod?: CurveMethod; curveAccent?: number; } = {}): Vector3[] => { const { sEasing, lEasing } = makeCurveEasings(curveMethod, curveAccent); return generateColorRamp({ total, hStart, hStartCenter, hCycles, sRange, lRange, sEasing, lEasing, transformFn, hueList, }); }; ================================================ FILE: src/index.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 const 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: 0.001 }, }, hStartCenter: { default: 0.5, props: { min: 0, max: 1, step: 0.001 }, }, minLight: { default: Math.random() * 0.2, props: { min: 0, max: 1, step: 0.001 }, }, maxLight: { default: 0.89 + Math.random() * 0.11, props: { min: 0, max: 1, step: 0.001 }, }, minSaturation: { default: Math.random() < 0.5 ? 0.4 : 0.8 + Math.random() * 0.2, props: { min: 0, max: 1, step: 0.001 }, }, maxSaturation: { default: Math.random() < 0.5 ? 0.35 : 0.9 + Math.random() * 0.1, props: { min: 0, max: 1, step: 0.001 }, }, curveMethod: { default: "lamé", props: { options: ["lamé", "sine", "power", "linear"] }, }, curveAccent: { default: 0.5, props: { min: 0, max: 5, step: 0.01 }, }, }; ================================================ FILE: src/utils.ts ================================================ /** * returns a new shuffled array * @param {Array} array - The array to shuffle. * @param {function} rndFn - The random function to use. * @returns {Array} - The shuffled array. */ export function shuffleArray(array: readonly T[], rndFn = Math.random): T[] { // Create a copy of the input array const copy = [...array]; let currentIndex = copy.length, randomIndex; while (currentIndex != 0) { randomIndex = Math.floor(rndFn() * currentIndex); currentIndex--; [copy[currentIndex], copy[randomIndex]] = [ copy[randomIndex] as T, copy[currentIndex] as T, ]; } return copy; } type FillFunction = T extends number ? (amt: number, from: T, to: T) => T : (amt: number, from: T | null, to: T | null) => T; /** * Linearly interpolates between two values. * * @param {number} amt - The interpolation amount (usually between 0 and 1). * @param {number} from - The starting value. * @param {number} to - The ending value. * @returns {number} - The interpolated value. */ export const lerp: FillFunction = (amt, from, to) => from + amt * (to - from); /** * Scales and spreads an array to the target size using interpolation, with optional padding. * * This function takes an initial array of values, a target size, an optional padding value, * and an interpolation function (defaults to `lerp`). It returns a scaled and spread * version of the initial array to the target size using the specified interpolation function. * * The padding parameter (between 0 and 1) compresses the normalized domain from both ends, * matching the behavior of chroma.js's scale() function. This is particularly useful for * color scales to prevent the endpoints from being too extreme. * * When padding is 0 (default), the original algorithm is used where values are distributed * and interpolated across segments. * * When padding > 0, the normalized domain (0-1) is compressed to [padding, 1-padding], * allowing for more graceful handling of extreme values. * * @param {Array} valuesToFill - The initial array of values. * @param {number} targetSize - The desired size of the resulting array. * @param {number} padding - Optional padding value between 0 and 1 (default: 0). * @param {FillFunction} fillFunction - The interpolation function (default is lerp). * @returns {Array} The scaled and spread array. * @throws {Error} If the initial array is invalid or target size is invalid. */ export const scaleSpreadArray = ( valuesToFill: T[], targetSize: number, padding = 0, fillFunction: FillFunction = lerp as unknown as FillFunction ): T[] => { // Validation checks 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); // For case without padding, use the original algorithm optimized if (padding <= 0) { const len = valuesToFill.length; const lastIdx = len - 1; const totalAdded = targetSize - len; // Calculate how many items are added per segment // The original logic distributed 'valuesToAdd' in a round-robin fashion // starting from the first segment. const baseAdds = Math.floor(totalAdded / lastIdx); const remainder = totalAdded % lastIdx; let currentResultIdx = 0; for (let i = 0; i < lastIdx; i++) { const startVal = valuesToFill[i] as T; const endVal = valuesToFill[i + 1] as T; // A segment consists of the start value + any added intermediate values. // If i < remainder, this segment gets an extra slot (matching the original round-robin). 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); } } // The loop above handles [start, ...intermediates] for every segment. // We must manually add the very last value of the input array, // as it is never a 'start' value for a segment. result[currentResultIdx] = valuesToFill[lastIdx]; return result; } // Implement chroma.js style padding (Optimized) // The padding essentially shifts the start and end of the normalized range const domainStart = padding; const domainEnd = 1 - padding; const lenMinus1 = valuesToFill.length - 1; // Optimization: Pre-calculate normalized positions once // This avoids creating a new array and iterating it inside the main loop (O(N) -> O(1)) const normalizedPositions = new Float64Array(valuesToFill.length); for (let i = 0; i < valuesToFill.length; i++) { normalizedPositions[i] = i / lenMinus1; } let segmentIndex = 0; // Generate evenly spaced positions in the target array for (let i = 0; i < targetSize; i++) { // Generate normalized position (0-1) const t = targetSize === 1 ? 0.5 : i / (targetSize - 1); // Apply padding by adjusting t const adjustedT = domainStart + t * (domainEnd - domainStart); // Optimization: Monotonic search. // Since 'i' increases, 'adjustedT' increases. We can continue searching // from the previous segmentIndex instead of searching from 0 every time. while ( segmentIndex < lenMinus1 && adjustedT > (normalizedPositions[segmentIndex + 1] as number) ) { segmentIndex++; } // Get the segment boundaries in normalized space const segmentStart = normalizedPositions[segmentIndex] as number; const segmentEnd = normalizedPositions[segmentIndex + 1] as number; // Calculate relative position within segment (0-1) let segmentT = 0; if (segmentEnd > segmentStart) { segmentT = (adjustedT - segmentStart) / (segmentEnd - segmentStart); } // Get the values from the segments const fromValue = valuesToFill[segmentIndex] as T; const toValue = valuesToFill[segmentIndex + 1] as T; // Get the interpolated value from the correct segment result[i] = fillFunction(segmentT, fromValue, toValue); } return result; }; export type CurveMethod = | "lamé" | "arc" | "pow" | "powY" | "powX" | ((i: number, curveAccent: number) => [number, number]); /** * function pointOnCurve * @param curveMethod {String|Function} Defines how the curve is drawn * @param curveAccent {Number} Defines the accent of the curve * @returns {Function} A function that takes a number between 0 and 1 and returns the x and y coordinates of the curve at that point * @throws {Error} If the curveMethod is not a valid type */ export const pointOnCurve = (curveMethod: CurveMethod, curveAccent: number) => { return (t: number): { x: number; y: number } => { const limit = Math.PI / 2; const slice = limit / 1; const percentile = t; let x = 0, y = 0; if (curveMethod === "lamé") { const t = percentile * limit; const exp = 2 / (2 + 20 * curveAccent); const cosT = Math.cos(t); const sinT = Math.sin(t); 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) as [number, number]; x = xFunc; y = yFunc; } else { throw new Error( `pointOnCurve() curveMethod parameter is expected to be "lamé" | "arc" | "pow" | "powY" | "powX" or a function but \`${curveMethod}\` given.` ); } return { x, y }; }; }; /** * makeCurveEasings generates two easing functions based on a curve method and accent. * @param {CurveMethod} curveMethod - The method used to generate the curve. * @param {number} curveAccent - The accent of the curve. * @returns {Object} An object containing two easing functions: sEasing and lEasing. */ export const makeCurveEasings = ( curveMethod: CurveMethod, curveAccent: number ): { sEasing: (t: number) => number; lEasing: (t: number) => number; } => { const point = pointOnCurve(curveMethod, curveAccent); return { sEasing: (t: number) => point(t).x, lEasing: (t: number) => point(t).y, }; }; ================================================ FILE: tea.yaml ================================================ # https://tea.xyz/what-is-this-file --- version: 1.0.0 codeOwners: - '0xFA64435d1281921E36b90CeA9a1fbf0e5c408e65' quorum: 1 ================================================ FILE: test/colorUtils.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { normalizeHue, harveyHue, colorHarmonies, uniqueRandomHues, hsv2hsl, colorToCSS } from '../src/colorUtils'; import type { Vector3 } from '../src/colorUtils'; describe('normalizeHue', () => { it('should normalize positive hues within the 0-360 range', () => { expect(normalizeHue(0)).toBe(0); expect(normalizeHue(360)).toBe(0); // 360 is normalized to 0 expect(normalizeHue(720)).toBe(0); // 720 is normalized to 0 expect(normalizeHue(450)).toBe(90); // 450 is normalized to 90 }); it('should normalize negative hues within the 0-360 range', () => { expect(normalizeHue(-360)).toBe(0); // -360 is normalized to 0 expect(normalizeHue(-90)).toBe(270); // -90 is normalized to 270 expect(normalizeHue(-450)).toBe(270); // -450 is normalized to 270 }); it('should handle edge cases correctly', () => { expect(normalizeHue(0)).toBe(0); expect(normalizeHue(-0)).toBe(0); // Negative zero should also normalize to 0 expect(normalizeHue(360)).toBe(0); // 360 is normalized to 0 expect(normalizeHue(-360)).toBe(0); // -360 is normalized to 0 }); }); describe('harveyHue', () => { it('should return a value between 0 and 360', () => { expect(harveyHue(0)).toBeGreaterThanOrEqual(0); expect(harveyHue(0)).toBeLessThanOrEqual(360); expect(harveyHue(360)).toBeGreaterThanOrEqual(0); expect(harveyHue(360)).toBeLessThanOrEqual(360); }); it('should normalize input hue to the 0-360 range', () => { expect(harveyHue(-360)).toBeGreaterThanOrEqual(0); expect(harveyHue(-360)).toBeLessThanOrEqual(360); expect(harveyHue(720)).toBeGreaterThanOrEqual(0); expect(harveyHue(720)).toBeLessThanOrEqual(360); }); it('should handle edge cases correctly', () => { expect(harveyHue(0)).toBe(0); expect(harveyHue(360)).toBe(0); // 360 is normalized to 0 }); }); describe('colorHarmonies', () => { it('complementary should return two hues 180 degrees apart', () => { const baseHue = 60; const harmony = colorHarmonies.complementary(baseHue); expect(harmony).toHaveLength(2); expect(harmony[0]).toBe(baseHue); expect(harmony[1]).toBe((baseHue + 180) % 360); }); it('triadic should return three hues 120 degrees apart', () => { const baseHue = 30; const harmony = colorHarmonies.triadic(baseHue); expect(harmony).toHaveLength(3); expect(harmony[0]).toBe(baseHue); expect(harmony[1]).toBe((baseHue + 120) % 360); expect(harmony[2]).toBe((baseHue + 240) % 360); }); // TODO: Add tests for other harmonies (splitComplementary, tetradic, etc.) }); describe('uniqueRandomHues', () => { it('should generate the correct number of hues', () => { const total = 5; const hues = uniqueRandomHues({ total }); expect(hues).toHaveLength(total); }); it('should generate hues within the 0-360 range', () => { const hues = uniqueRandomHues({ total: 10 }); hues.forEach(hue => { expect(hue).toBeGreaterThanOrEqual(0); expect(hue).toBeLessThan(360); }); }); it('should generate unique hues', () => { const hues = uniqueRandomHues({ total: 6, minHueDiffAngle: 1 }); // Ensure uniqueness is likely const uniqueHues = new Set(hues); expect(hues.length).toBe(uniqueHues.size); }); it('should respect minHueDiffAngle (probabilistically)', () => { const total = 4; const minHueDiffAngle = 90; const hues = uniqueRandomHues({ total, minHueDiffAngle }); // Check differences between sorted hues const sortedHues = [...hues].sort((a, b) => a - b); for (let i = 0; i < sortedHues.length; i++) { const h1 = sortedHues[i] as number; const h2 = sortedHues[(i + 1) % sortedHues.length] as number; let diff = Math.round(Math.abs(h1 - h2)); if (diff > 180) diff = 360 - diff; // Handle wrap-around difference // Because it picks from pre-calculated slots, the difference should be >= minHueDiffAngle expect(diff).toBeGreaterThanOrEqual(minHueDiffAngle); } }); }); describe('hsv2hsl', () => { it('should convert HSV black to HSL black', () => { const hsv: Vector3 = [0, 0, 0]; const hsl = hsv2hsl(hsv); expect(hsl[0]).toBe(0); // Hue is irrelevant for black expect(hsl[1]).toBe(0); // Saturation expect(hsl[2]).toBe(0); // Lightness }); it('should convert HSV white to HSL white', () => { const hsv: Vector3 = [0, 0, 1]; const hsl = hsv2hsl(hsv); expect(hsl[0]).toBe(0); // Hue is irrelevant for white expect(hsl[1]).toBe(0); // Saturation expect(hsl[2]).toBe(1); // Lightness }); it('should convert HSV red to HSL red', () => { const hsv: Vector3 = [0, 1, 1]; // Max saturation, max value const hsl = hsv2hsl(hsv); expect(hsl[0]).toBe(0); // Hue expect(hsl[1]).toBe(1); // Saturation expect(hsl[2]).toBe(0.5); // Lightness }); it('should convert HSV gray to HSL gray', () => { const hsv: Vector3 = [0, 0, 0.5]; // No saturation, mid value const hsl = hsv2hsl(hsv); expect(hsl[0]).toBe(0); // Hue expect(hsl[1]).toBe(0); // Saturation expect(hsl[2]).toBe(0.5); // Lightness }); }); describe('colorToCSS', () => { it('should format HSL correctly', () => { const color: Vector3 = [120, 0.5, 0.75]; // H, S, L expect(colorToCSS(color, 'hsl')).toBe('hsl(120 50% 75%)'); }); it('should format LCH correctly', () => { // Note: Direct conversion from HSL to LCH isn't done here, // assuming input is already LCH-like for formatting purposes const color: Vector3 = [240, .8, .6]; // H, C, L (example values) expect(colorToCSS(color, 'lch')).toBe('lch(60% 80% 240)'); }); it('should format OKLCH correctly', () => { // Assuming input is OKLCH-like const color: Vector3 = [180, 0.2, 0.8]; // H, C, L (example values) expect(colorToCSS(color, 'oklch')).toBe('oklch(80% 20% 180)'); }); it('should format HSV correctly (by converting to HSL)', () => { const color: Vector3 = [0, 1, 1]; // HSV Red // Expected HSL: [0, 1, 0.5] => hsl(0 100% 50%) expect(colorToCSS(color, 'hsv')).toBe('hsl(0 100% 50%)'); }); it('should default to oklch if mode is omitted', () => { // Assuming input is OKLCH-like const color: Vector3 = [180, 0.2, 0.8]; expect(colorToCSS(color)).toBe('oklch(80% 20% 180)'); }); }); ================================================ FILE: test/core.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { generateColorRamp, generateColorRampWithCurve } from '../src/core'; import type { Vector3 } from '../src/colorUtils'; describe('generateColorRamp', () => { it('should generate the correct number of colors', () => { const colors = generateColorRamp({ total: 5 }); expect(colors).toHaveLength(5); }); it('should generate colors within HSL constraints', () => { const colors = generateColorRamp({ total: 10 }); colors.forEach((color: Vector3) => { expect(color[0]).toBeGreaterThanOrEqual(0); // Hue expect(color[0]).toBeLessThanOrEqual(360); expect(color[1]).toBeGreaterThanOrEqual(0); // Saturation expect(color[1]).toBeLessThanOrEqual(1); expect(color[2]).toBeGreaterThanOrEqual(0); // Lightness expect(color[2]).toBeLessThanOrEqual(1); }); }); it('should respect hStart parameter', () => { const hStart = 180; const colors = generateColorRamp({ total: 5, hStart, hStartCenter: 0 }); // The first color's hue should be close to hStart when hStartCenter is 0 expect(colors[0]?.[0]).toBeCloseTo(hStart); }); it('should respect hCycles parameter', () => { const hStart = 0; const hCycles = 1; const total = 5; const colors = generateColorRamp({ total, hStart, hCycles, hStartCenter: 0 }); // With 1 cycle and hStartCenter 0, the last color should be close to the first expect(colors[total - 1]?.[0]).toBeCloseTo(colors[0]?.[0] as number, 0); // Allow some tolerance }); it('should use hueList when provided', () => { const hueList = [0, 120, 240]; const colors = generateColorRamp({ hueList }); expect(colors).toHaveLength(hueList.length); expect(colors[0]?.[0]).toBe(hueList[0]); expect(colors[1]?.[0]).toBe(hueList[1]); expect(colors[2]?.[0]).toBe(hueList[2]); }); it('should apply transformFn', () => { const transformFn = ([h, s, l]: Vector3): Vector3 => [h, s * 0.5, l]; const colors = generateColorRamp({ total: 3, transformFn, sRange: [1, 1] }); colors.forEach(color => { expect(color[1]).toBeLessThanOrEqual(0.5); // Saturation should be halved }); }); }); describe('generateColorRampWithCurve', () => { it('should generate the correct number of colors', () => { const colors = generateColorRampWithCurve({ total: 7 }); expect(colors).toHaveLength(7); }); it('should generate colors within HSL constraints', () => { const colors = generateColorRampWithCurve({ total: 10 }); colors.forEach((color: Vector3) => { expect(color[0]).toBeGreaterThanOrEqual(0); expect(color[0]).toBeLessThanOrEqual(360); expect(color[1]).toBeGreaterThanOrEqual(0); expect(color[1]).toBeLessThanOrEqual(1); expect(color[2]).toBeGreaterThanOrEqual(0); expect(color[2]).toBeLessThanOrEqual(1); }); }); it('should use curveMethod for easing', () => { // This test is a bit indirect, checking if output differs from default generateColorRamp // A more robust test would mock makeCurveEasings or check specific curve outputs const colorsCurve = generateColorRampWithCurve({ total: 5, curveMethod: 'lamé', curveAccent: 0.5 }); const colorsDefault = generateColorRamp({ total: 5 }); // Uses default easings // Expecting the results to be different due to different easing functions // Note: This might fail if default easings happen to produce the same result for a specific input expect(colorsCurve).not.toEqual(colorsDefault); }); }); ================================================ FILE: test/utils.test.ts ================================================ import { describe, it, expect } from 'vitest'; import { shuffleArray, scaleSpreadArray, makeCurveEasings, pointOnCurve } from '../src/utils'; // Define lerp function locally for testing const lerp = (amt: number, from: number, to: number): number => from + amt * (to - from); describe('shuffleArray', () => { it('should return an array with the same length', () => { const original = [1, 2, 3, 4, 5]; const shuffled = shuffleArray(original); expect(shuffled).toHaveLength(original.length); }); it('should contain the same elements', () => { const original = [1, 2, 3, 4, 5]; const shuffled = shuffleArray(original); expect(shuffled).toEqual(expect.arrayContaining(original)); expect(original).toEqual(expect.arrayContaining(shuffled)); }); it('should shuffle the array (usually)', () => { const original = Array.from({ length: 100 }, (_, i) => i); const shuffled = shuffleArray(original); // It's statistically improbable but possible for the array to remain unchanged expect(shuffled).not.toEqual(original); }); it('should use the provided random function', () => { const original = [1, 2, 3]; // A deterministic "random" function that cycles through 0.5, 0.2, 0.8 const mockRndFn = (() => { const sequence = [0.5, 0.2, 0.8]; let index = 0; return () => sequence[index++ % sequence.length]; })(); const shuffled = shuffleArray(original, mockRndFn); // With this mock, the shuffle should produce a predictable result expect(shuffled).toEqual([3, 1, 2]); }); }); describe('lerp', () => { it('should interpolate correctly', () => { expect(lerp(0, 0, 10)).toBe(0); expect(lerp(1, 0, 10)).toBe(10); expect(lerp(0.5, 0, 10)).toBe(5); expect(lerp(0.25, 10, 20)).toBe(12.5); expect(lerp(0.75, -10, 10)).toBe(5); }); }); describe('scaleSpreadArray', () => { it('should scale and spread the array correctly with lerp', () => { const initial = [0, 10]; const targetSize = 5; const expected = [0, 2.5, 5, 7.5, 10]; const result = scaleSpreadArray(initial, targetSize, 0, lerp); expect(result).toHaveLength(targetSize); result.forEach((val, i) => expect(val).toBeCloseTo(expected[i] as number)); }); it('should handle arrays with more than two elements', () => { const initial = [0, 10, 5]; const targetSize = 7; // Expected: [0, 5, 10, 7.5, 5, ?, ?] - Calculation gets complex, focus on length and bounds const result = scaleSpreadArray(initial, targetSize, 0, lerp); expect(result).toHaveLength(targetSize); expect(result[0]).toBe(0); expect(result[2]).toBe(6.666666666666666); // Second original element position expect(result[4]).toBe(8.333333333333334); // Third original element position }); it('should throw error if initial array is too small', () => { expect(() => scaleSpreadArray([1], 8.333333333333334)).toThrow(); }); it('should throw error if target size is smaller than initial size', () => { expect(() => scaleSpreadArray([1, 2, 3], 2)).toThrow(); }); }); describe('pointOnCurve', () => { it('should return points for "lamé" curve', () => { const poc = pointOnCurve('lamé', 0.5); const point = poc(0.5); expect(point.x).toBeGreaterThanOrEqual(0); expect(point.x).toBeLessThanOrEqual(1); expect(point.y).toBeGreaterThanOrEqual(0); expect(point.y).toBeLessThanOrEqual(1); }); it('should return points for "arc" curve', () => { const poc = pointOnCurve('arc', 0.1); const point = poc(0.2); expect(point.x).toBeGreaterThanOrEqual(0); expect(point.x).toBeLessThanOrEqual(1); expect(point.y).toBeGreaterThanOrEqual(0); expect(point.y).toBeLessThanOrEqual(1); }); /* it('should throw error for invalid curve method', () => { // @ts-expect-error Testing invalid input expect(() => pointOnCurve('invalidMethod', 0.5)).toThrow( 'pointOnCurve() curveAccent parameter is expected to be "lamé" | "arc" | "pow" | "powY" | "powX" or a function but `invalidMethod` given.' ); });*/ }); describe('makeCurveEasings', () => { it('should return sEasing and lEasing functions', () => { const easings = makeCurveEasings('lamé', 0.5); expect(easings.sEasing).toBeInstanceOf(Function); expect(easings.lEasing).toBeInstanceOf(Function); }); it('easing functions should return values between 0 and 1 for t between 0 and 1', () => { const easings = makeCurveEasings('lamé', 0.5); const tValues = [0, 0.25, 0.5, 0.75, 1]; tValues.forEach(t => { const sVal = easings.sEasing(t); const lVal = easings.lEasing(t); expect(sVal).toBeGreaterThanOrEqual(0); expect(sVal).toBeLessThanOrEqual(1); expect(lVal).toBeGreaterThanOrEqual(0); expect(lVal).toBeLessThanOrEqual(1); }); }); }); ================================================ FILE: tsconfig.json ================================================ { "compilerOptions": { "noImplicitAny": true, "noEmitOnError": true, "removeComments": false, "sourceMap": true, "target": "es2019", "outDir": "dist", "declaration": true, "emitDeclarationOnly": true, "noUncheckedIndexedAccess": true, "strict": true }, "include": ["src/**/*"] } ================================================ FILE: vitest.config.ts ================================================ import { defineConfig } from 'vitest/config'; export default defineConfig({ test: { // environment: 'jsdom', // uncomment if you need DOM APIs globals: true, // Use global APIs like describe, it, expect }, });