├── .eslintrc ├── .github ├── FUNDING.yml └── workflows │ └── static.yml ├── .gitignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── build.js ├── dist ├── index.cjs ├── index.d.ts ├── index.html ├── index.js ├── index.min.cjs ├── index.min.js ├── index.min.mjs ├── index.mjs ├── index.mjs.map ├── index.umd.js ├── p5.html ├── poline-logo.png ├── poline-wheel.png └── socialfb.png ├── package-lock.json ├── package.json ├── src └── index.ts ├── tea.yaml └── tsconfig.json /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [meodai] 4 | ko_fi: colorparrot 5 | -------------------------------------------------------------------------------- /.github/workflows/static.yml: -------------------------------------------------------------------------------- 1 | # Simple workflow for deploying static content to GitHub Pages 2 | name: Deploy static content to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow one concurrent deployment 19 | concurrency: 20 | group: "pages" 21 | cancel-in-progress: true 22 | 23 | jobs: 24 | # Single deploy job since we're just deploying 25 | deploy: 26 | environment: 27 | name: github-pages 28 | url: ${{ steps.deployment.outputs.page_url }} 29 | runs-on: ubuntu-latest 30 | steps: 31 | - name: Checkout 32 | uses: actions/checkout@v3 33 | - name: Setup Pages 34 | uses: actions/configure-pages@v2 35 | - name: Use Node.js ${{ matrix.node-version }} 36 | uses: actions/setup-node@v3 37 | with: 38 | node-version: ${{ matrix.node-version }} 39 | - run: npm ci 40 | - run: npm run build --if-present 41 | - name: Upload artifact 42 | uses: actions/upload-pages-artifact@v3 43 | with: 44 | # Upload entire repository 45 | path: './dist' 46 | - name: Deploy to GitHub Pages 47 | id: deployment 48 | uses: actions/deploy-pages@v4 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | src/_color.js 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | 11 | # Diagnostic reports (https://nodejs.org/api/report.html) 12 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 13 | 14 | # Runtime data 15 | pids 16 | *.pid 17 | *.seed 18 | *.pid.lock 19 | 20 | # Directory for instrumented libs generated by jscoverage/JSCover 21 | lib-cov 22 | 23 | # Coverage directory used by tools like istanbul 24 | coverage 25 | *.lcov 26 | 27 | # nyc test coverage 28 | .nyc_output 29 | 30 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 31 | .grunt 32 | 33 | # Bower dependency directory (https://bower.io/) 34 | bower_components 35 | 36 | # node-waf configuration 37 | .lock-wscript 38 | 39 | # Compiled binary addons (https://nodejs.org/api/addons.html) 40 | build/Release 41 | 42 | # Dependency directories 43 | node_modules/ 44 | jspm_packages/ 45 | 46 | # TypeScript v1 declaration files 47 | typings/ 48 | 49 | # TypeScript cache 50 | *.tsbuildinfo 51 | 52 | # Optional npm cache directory 53 | .npm 54 | 55 | # Optional eslint cache 56 | .eslintcache 57 | 58 | # Microbundle cache 59 | .rpt2_cache/ 60 | .rts2_cache_cjs/ 61 | .rts2_cache_es/ 62 | .rts2_cache_umd/ 63 | 64 | # Optional REPL history 65 | .node_repl_history 66 | 67 | # Output of 'npm pack' 68 | *.tgz 69 | 70 | # Yarn Integrity file 71 | .yarn-integrity 72 | 73 | # dotenv environment variables file 74 | .env 75 | .env.test 76 | 77 | # parcel-bundler cache (https://parceljs.org/) 78 | .cache 79 | 80 | # Next.js build output 81 | .next 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | 86 | # Gatsby files 87 | .cache/ 88 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 89 | # https://nextjs.org/blog/next-9-1#public-directory-support 90 | # public 91 | 92 | # vuepress build output 93 | .vuepress/dist 94 | 95 | # Serverless directories 96 | .serverless/ 97 | 98 | # FuseBox cache 99 | .fusebox/ 100 | 101 | # DynamoDB Local files 102 | .dynamodb/ 103 | 104 | # TernJS port file 105 | .tern-port -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 David Aerne 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

poline

2 | 3 | ## Esoteric Color Palette Generator Micro-Lib 4 | 5 | "**poline**" is an enigmatic color palette generator, that harnesses the mystical witchcraft of polar coordinates. Its methodology, defying conventional color science, is steeped in the esoteric knowledge of the early 20th century. This magical technology defies explanation, drawing lines between anchors to produce visually striking and otherworldly palettes. It is an indispensable tool for the modern generative sorcerer, and a delight for the eye. 6 | 7 | ![Poline DEMO UI](./dist/poline-wheel.png) 8 | 9 | ## Getting Started 10 | 11 | Begin your journey with **poline** by following this simple incantation: 12 | 13 | ```js 14 | // Import the magical construct 15 | import { Poline } from 'poline'; 16 | 17 | // Summon a new palette with default settings (random anchor colors) 18 | const poline = new Poline(); 19 | 20 | // Behold the colors in HSL format 21 | console.log(poline.colors); 22 | 23 | // Or as CSS strings ready for your spells 24 | console.log(poline.colorsCSS); 25 | ``` 26 | 27 | ## Summoning 28 | The use of "**Poline**" begins with the invocation of its command, which can be performed with or without arguments. If called without, the tool will generate a mesmerizing palette featuring two randomly selected **anchors.** 29 | On the other hand, one can choose to provide their own **anchor** points, represented as a list of **hsl** values, for a more personal touch. The power to shape and mold the colors lies in your hands." 30 | 31 | ```js 32 | new Poline({ 33 | anchorColors: [ 34 | [309, 0.72, 0.80], 35 | [67, 0.32, 0.08], 36 | //... 37 | ], 38 | }); 39 | ``` 40 | 41 | ## Points 42 | The magic of "**Poline**" is revealed through its technique of drawing lines between anchor points. The richness of the palette is determined by the number of **points**, with each connection producing a unique color. 43 | 44 | Increasing the number of **points** will yield an even greater array of colors. By default, four points are used, but this can easily be adjusted through the 'numPoints' property on your Poline instance, as demonstrated in the code example. 45 | 46 | ```js 47 | new Poline({ 48 | numPoints: 6, 49 | }); 50 | ``` 51 | 52 | The resulting palette is a product of points multiplied by the number of anchor pairs. It can be changed after initialization by setting the **numPoints** property on your "**Poline**" instance. 53 | 54 | ## Anchors 55 | 56 | At the heart of "**Poline**" lies the concept of **anchors**, the fixed points that serve as the foundation for the creation of color palettes. **Anchors** are represented as a **list of hsl** values, which consist of three components: **hue** \[0…360\], **saturation** \[0…1\], and **lightness** \[0…1\]. 57 | 58 | The choice is yours, whether to provide your own anchor points during initialization or to allow "**Poline**" to generate a random selection for you by omitting the 'anchorColors' argument. The versatility of Poline extends "**Poline**" its initial setup, as you can also add anchors to your palette at any time using the '**addAnchorPoint**' method. This method accepts either a **color** as HSL array values or an array of **X, Y, Z** coordinates, further expanding the possibilities of your color creation. 59 | 60 | ```js 61 | poline.addAnchorPoint({ 62 | color: [100, 0.91, 0.80] 63 | }); 64 | 65 | // or 66 | 67 | poline.addAnchorPoint({ 68 | xyz: [0.43, 0.89, 0.91] 69 | }); 70 | ``` 71 | 72 | You can also specify where to insert the new anchor by providing an `insertAtIndex` parameter: 73 | 74 | ```js 75 | poline.addAnchorPoint({ 76 | color: [200, 0.5, 0.6], 77 | insertAtIndex: 1 // Insert after the first anchor 78 | }); 79 | ``` 80 | 81 | ## Updating Anchors 82 | 83 | With this feature, you have the power to fine-tune your palette and make adjustments as your creative vision evolves. So whether you are looking to make subtle changes or bold alterations, "**Poline**" is always ready to help you achieve your desired result. 84 | 85 | The ability to update existing anchors is made possible through the '**updateAnchorPoint**' method. This method accepts the **reference to the anchor** you wish to modify and either a color in the form of **HSL** representation or an **XYZ** position array. 86 | 87 | ```js 88 | poline.updateAnchorPoint({ 89 | point: poline.anchorPoints[0], 90 | color: [286, 0.22, 0.22] 91 | }); 92 | ``` 93 | 94 | You can also update an anchor by its index: 95 | 96 | ```js 97 | poline.updateAnchorPoint({ 98 | pointIndex: 1, 99 | color: [120, 0.8, 0.5] 100 | }); 101 | ``` 102 | 103 | ## Position Function 104 | 105 | The **position function** in "**Poline**" plays a crucial role in determining the **distribution of colors between the anchors**. It works similar to easing functions and can be imported from the "**Poline**" module. 106 | 107 | A position function is a mathematical function that maps a value **between 0 and 1** to another value between 0 and 1. By definition the same position function for all axes "**Poline**" will draw a straight line between the anchors. The chosen function will determine the distribution of colors between the anchors. 108 | 109 | ```js 110 | import { 111 | Poline, positionFunctions 112 | } from 'poline'; 113 | 114 | new Poline({ 115 | positionFunction: 116 | positionFunctions.linearPosition, 117 | }); 118 | ``` 119 | 120 | If none is provided, "**Poline**" will use the default function, which is a sinusoidal function. 121 | The following position functions are available and can be included by importing the **positionFunctions** object from the "**Poline**" module: 122 | 123 | - linearPosition 124 | - exponentialPosition 125 | - quadraticPosition 126 | - cubicPosition 127 | - quarticPosition 128 | - sinusoidalPosition **(default)** 129 | - asinusoidalPosition 130 | - arcPosition 131 | - smoothStepPosition 132 | 133 | Here's a visual representation of how these functions affect the distribution: 134 | 135 | | Function Name | Effect on Color Distribution | 136 | |---------------|------------------------------| 137 | | linearPosition | Even distribution of colors along the path | 138 | | exponentialPosition | Colors cluster near one end, spreading out toward the other | 139 | | sinusoidalPosition | Smooth acceleration and deceleration of colors | 140 | | arcPosition | Colors follow an arc-like distribution | 141 | 142 | ## Arcs 143 | By defining **different position functions for each axis**, you can control the distribution of colors along each axis (**positionFunctionX**, **positionFunctionY**, **positionFunctionZ**). This will draw different arcs and create a diverse range of color palettes. 144 | 145 | ```js 146 | new Poline({ 147 | positionFunctionX: 148 | positionFunctions.sinusoidalPosition, 149 | positionFunctionY: 150 | positionFunctions.quadraticPosition, 151 | positionFunctionZ: 152 | positionFunctions.linearPosition, 153 | }); 154 | ``` 155 | 156 | ## Palette 157 | 158 | By default, the palette is not a closed loop. This means that the last color generated is not the same as the first color. If you want the palette to be a closed loop, you can set the **closedLoop** argument to true. 159 | 160 | ```js 161 | poline.closedLoop = true; 162 | ``` 163 | 164 | It is also possible to close the loop after the fact by setting **poline.closedLoop = true|false**. 165 | 166 | ## Hue Shifting 167 | 168 | With the power of hue shifting, "**Poline**" provides yet another level of customization. This feature allows you to **shift the hue** of the colors generated by a certain amount, giving you the ability to animate your palette or create similar color combinations with different hues." 169 | 170 | "**poline**" supports hue shifting. This means that the hue of the colors will be shifted by a certain amount. This can be useful if you want to animate the palette or generate a palette that looks similar to your current palette but using different hues. 171 | 172 | ```js 173 | poline.shiftHue(1); 174 | ``` 175 | The amount is a int or float between -Infinity and Infinity. It will permanently shift the hue of all colors in the palette. 176 | 177 | ## Closest Anchor 178 | 179 | In some situations, you might want to know which anchor is closest to a certain position or color. This method is used in the visualizer to highlight to select the closest anchor on click. 180 | 181 | ```js 182 | poline.getClosestAnchorPoint( 183 | {xyz: [x, y, null], maxDistance: .1} 184 | ) 185 | ``` 186 | 187 | The **maxDistance** argument is optional and will return null if the closest anchor is further away than the maxDistance. 188 | Any of the **xyz** or **hsl** components can be null. If they are **null**, they will be ignored. 189 | 190 | ## Color List 191 | 192 | The '**poline**' instance returns all colors as an array of **hsl**, **lch** or 193 | **oklch** arrays or alternatively as an array of **CSS** strings. 194 | 195 | ```js 196 | poline.colors // Array of HSL values [[h, s, l], [h, s, l], ...] 197 | poline.colorsCSS // Array of CSS HSL strings ['hsl(h, s%, l%)', ...] 198 | poline.colorsCSSlch // Array of CSS LCH strings ['lch(l% c h)', ...] 199 | poline.colorsCSSoklch // Array of CSS OKLCH strings ['oklch(l% c h)', ...] 200 | ``` 201 | 202 | ## Remove Anchors 203 | 204 | To remove an anchor, you can use the **removeAnchorPoint** method. It either takes an **anchor** reference or an **index** as an argument. 205 | 206 | ```js 207 | poline.removeAnchorPoint({ 208 | point: poline.anchorPoints[ 209 | poline.anchorPoints.length - 1 210 | ] 211 | }); 212 | // or 213 | poline.removeAnchorPoint({ 214 | index: poline.anchorPoints.length - 1 215 | }); 216 | ``` 217 | 218 | ## Inverted Lightness 219 | 220 | The magical construct of "**poline**" offers the power to invert the lightness calculation, creating palettes with different visual characteristics. You can toggle this option during initialization or later through the instance property. 221 | 222 | ```js 223 | // During initialization 224 | const poline = new Poline({ 225 | invertedLightness: true 226 | }); 227 | 228 | // Or later 229 | poline.invertedLightness = true; 230 | ``` 231 | 232 | When inverted, colors near the center of the coordinate system will have higher lightness values, while colors at the edge will be darker, creating a different aesthetic in your palette. 233 | 234 | ## Color Model 235 | 236 | To keep the library as lightweight as possible, "**poline**" only supports the **hsl** color model out of the box. However, it is easily possible to use other color models by using a library like [culori](https://culorijs.org/api/). 237 | 238 | ```js 239 | import {Poline} from "poline"; 240 | import {formatHex} from "culori"; 241 | const poline = new Poline(/** options */); 242 | 243 | const OKHslColors = [...poline.colors].map( 244 | c => formatHex({ 245 | mode: 'okhsl', 246 | h: c[0], 247 | s: c[1], 248 | l: c[2] 249 | }) 250 | ); 251 | const LCHColors = [...poline.colors].map( 252 | c => formatHex({ 253 | mode: 'lch', 254 | h: c[0], 255 | c: c[1] * 51.484, 256 | l: c[2] * 100, 257 | }) 258 | ); 259 | ``` 260 | 261 | ## Common Use Cases 262 | 263 | ### Creating a Gradient 264 | 265 | "**poline**" can be used to generate CSS gradients with unique color distributions: 266 | 267 | ```js 268 | const poline = new Poline({ 269 | anchorColors: [ 270 | [210, 0.8, 0.6], // Blue 271 | [30, 0.8, 0.6] // Orange 272 | ], 273 | numPoints: 8 274 | }); 275 | 276 | // Generate a CSS linear gradient 277 | const colors = poline.colorsCSS; 278 | const gradient = `linear-gradient(in oklab, ${colors.join(', ')})`; 279 | 280 | // Apply to an element 281 | document.getElementById('gradient').style.background = gradient; 282 | ``` 283 | 284 | ### Generating Color Schemes for Data Visualization 285 | 286 | "**poline**" excels at creating color schemes for data visualization. In this 287 | case, this makes a great diverging color scheme for a chart: 288 | 289 | ```js 290 | // Create a palette with perceptually distinct colors 291 | const poline = new Poline({ 292 | anchorColors: [ 293 | [10, 0.70, 0.90], 294 | [70, 0.97, 0], 295 | [260, 0.70, 0.0] 296 | ], 297 | positionFunction: positionFunctions.linearPosition, 298 | numPoints: 7, 299 | closedLoop: true 300 | }); 301 | 302 | // Use the colors for chart elements 303 | const chartColors = poline.colorsCSS; 304 | ``` 305 | 306 | ### Animating Palettes 307 | 308 | You can animate your "**poline**" palette to create mesmerizing effects: 309 | 310 | ```js 311 | const poline = new Poline(); 312 | let animationFrame; 313 | 314 | function animatePalette() { 315 | // Shift the hue slightly each frame 316 | poline.shiftHue(0.5); 317 | 318 | // Update elements with new colors 319 | const elements = document.querySelectorAll('.color-element'); 320 | const colors = poline.colorsCSS; 321 | 322 | elements.forEach((el, i) => { 323 | el.style.backgroundColor = colors[i % colors.length]; 324 | }); 325 | 326 | animationFrame = requestAnimationFrame(animatePalette); 327 | } 328 | 329 | // Start/stop animation 330 | document.getElementById('toggle-animation').addEventListener('click', () => { 331 | if (animationFrame) { 332 | cancelAnimationFrame(animationFrame); 333 | animationFrame = null; 334 | } else { 335 | animatePalette(); 336 | } 337 | }); 338 | ``` 339 | 340 | ## Error Handling 341 | 342 | "**poline**" will conjure mystical errors when improper incantations are attempted. Be prepared to handle these manifestations: 343 | 344 | - When providing fewer than two anchor colors: `"Must have at least two anchor colors"` 345 | - When setting `numPoints` to less than 1: `"Must have at least one point"` 346 | - When removing too many anchors: `"Must have at least two anchor points"` 347 | - When providing invalid parameters: `"Point must be initialized with either x,y,z or hsl"` 348 | - When the anchor point is not found: `"Point not found"` 349 | 350 | Example of proper error handling: 351 | 352 | ```js 353 | try { 354 | const poline = new Poline({ 355 | anchorColors: [[100, 0.5, 0.5]] // Only one anchor color! 356 | }); 357 | } catch (error) { 358 | console.error('Failed to summon palette:', error.message); 359 | // Fallback to default settings 360 | const poline = new Poline(); 361 | } 362 | ``` 363 | 364 | ## TypeScript Support 365 | 366 | "**poline**" is written in TypeScript and provides type definitions for all its features. The main types you'll encounter: 367 | 368 | ```typescript 369 | // Basic vector types 370 | type Vector2 = [number, number]; 371 | type Vector3 = [number, ...Vector2]; 372 | type PartialVector3 = [number | null, number | null, number | null]; 373 | 374 | // Position function type 375 | type PositionFunction = (t: number, reverse?: boolean) => number; 376 | 377 | // Options for creating a Poline instance 378 | type PolineOptions = { 379 | anchorColors: Vector3[]; 380 | numPoints: number; 381 | positionFunction?: PositionFunction; 382 | positionFunctionX?: PositionFunction; 383 | positionFunctionY?: PositionFunction; 384 | positionFunctionZ?: PositionFunction; 385 | invertedLightness?: boolean; 386 | closedLoop?: boolean; 387 | }; 388 | 389 | // Color point collection 390 | type ColorPointCollection = { 391 | xyz?: Vector3; 392 | color?: Vector3; 393 | invertedLightness?: boolean; 394 | }; 395 | ``` 396 | 397 | ## Installation 398 | 399 | "**poline**" is available as an [npm package](https://www.npmjs.com/package/poline). Alternatively you can clone it on [GitHub](https://github.com/meodai/poline). 400 | 401 | ```bash 402 | npm install poline 403 | ``` 404 | 405 | You can also use the [unpkg CDN](https://unpkg.com/poline) to include the library in your project. 406 | I recommend using the **mjs** version of the library. This will allow you to use the **import** syntax. But you can also use the **umd** version if you prefer to use the **script** tag. 407 | 408 | ```html 409 | 414 | ``` 415 | 416 | ## API Reference 417 | 418 | Behold the arcane interface of "**poline**", detailed in full for your enlightenment: 419 | 420 | ### Poline Class 421 | 422 | #### Constructor (ColorPoint Class) 423 | 424 | ```typescript 425 | constructor(options?: PolineOptions) 426 | ``` 427 | 428 | #### Properties of the Poline Class 429 | 430 | - `numPoints: number` - Get/set the number of points between anchors 431 | - `positionFunction: PositionFunction | PositionFunction[]` - Get/set the position function(s) 432 | - `positionFunctionX: PositionFunction` - Get/set the X-axis position function 433 | - `positionFunctionY: PositionFunction` - Get/set the Y-axis position function 434 | - `positionFunctionZ: PositionFunction` - Get/set the Z-axis position function 435 | - `anchorPoints: ColorPoint[]` - Get/set the anchor points 436 | - `closedLoop: boolean` - Get/set whether the palette forms a closed loop 437 | - `invertedLightness: boolean` - Get/set whether lightness calculation is inverted 438 | - `flattenedPoints: ColorPoint[]` - Get all points in a flat array 439 | - `colors: Vector3[]` - Get all colors as HSL arrays 440 | - `colorsCSS: string[]` - Get all colors as CSS HSL strings 441 | - `colorsCSSlch: string[]` - Get all colors as CSS LCH strings 442 | - `colorsCSSoklch: string[]` - Get all colors as CSS OKLCH strings 443 | 444 | #### Methods of the ColorPoint Class 445 | 446 | - `updateAnchorPairs(): void` - Update internal anchor pairs 447 | - `addAnchorPoint(options: ColorPointCollection & { insertAtIndex?: number }): ColorPoint` - Add a new anchor point 448 | - `removeAnchorPoint(options: { point?: ColorPoint; index?: number }): void` - Remove an anchor point 449 | - `updateAnchorPoint(options: { point?: ColorPoint; pointIndex?: number } & ColorPointCollection): ColorPoint` - Update an anchor point 450 | - `getClosestAnchorPoint(options: { xyz?: PartialVector3; hsl?: PartialVector3; maxDistance?: number }): ColorPoint | null` - Find closest anchor point 451 | - `shiftHue(hShift?: number): void` - Shift the hue of all colors 452 | 453 | ### ColorPoint Class 454 | 455 | #### Constructor 456 | 457 | ```typescript 458 | constructor(options?: ColorPointCollection) 459 | ``` 460 | 461 | #### Properties 462 | 463 | - `position: Vector3` - Get/set the XYZ position 464 | - `hsl: Vector3` - Get/set the HSL color 465 | - `hslCSS: string` - Get the CSS HSL string 466 | - `oklchCSS: string` - Get the CSS OKLCH string 467 | - `lchCSS: string` - Get the CSS LCH string 468 | 469 | #### Methods 470 | 471 | - `positionOrColor(options: ColorPointCollection): void` - Set position or color 472 | - `shiftHue(angle: number): void` - Shift the hue of the color 473 | 474 | ### Position Functions 475 | 476 | All position functions have the signature: 477 | 478 | ```typescript 479 | (t: number, reverse?: boolean) => number 480 | ``` 481 | 482 | Available functions: 483 | 484 | - `linearPosition` 485 | - `exponentialPosition` 486 | - `quadraticPosition` 487 | - `cubicPosition` 488 | - `quarticPosition` 489 | - `sinusoidalPosition` 490 | - `asinusoidalPosition` 491 | - `arcPosition` 492 | - `smoothStepPosition` 493 | 494 | ## License 495 | 496 | And thus, the tome of "**poline**" has been written. Its mystical powers, steeped in the arcane knowledge of the ancients, now reside within these pages. May this compendium serve you in your quest for the ultimate color palette. 497 | 498 | The project is [MIT licensed](https://github.com/meodai/poline/blob/main/LICENSE) and open source. If you find any bugs or have any suggestions please open an issue on [GitHub](https://github.com/meodai/poline/issues). 499 | 500 | Inspired and created with the blessing of [Anatoly Zenkov](https://anatolyzenkov.com/) -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | import { build } from "esbuild"; 2 | 3 | // Bundled CJS 4 | build({ 5 | entryPoints: ["./src/index.ts"], 6 | logLevel: "info", 7 | bundle: true, 8 | format: "cjs", 9 | outfile: "dist/index.cjs", 10 | }); 11 | 12 | // Bundled CJS, minified 13 | build({ 14 | entryPoints: ["./src/index.ts"], 15 | logLevel: "info", 16 | bundle: true, 17 | minify: true, 18 | format: "cjs", 19 | outfile: "dist/index.min.cjs", 20 | }); 21 | 22 | // Bundled ESM 23 | build({ 24 | entryPoints: ["./src/index.ts"], 25 | logLevel: "info", 26 | bundle: true, 27 | format: "esm", 28 | target: "es2020", 29 | outfile: "dist/index.mjs", 30 | }); 31 | 32 | // Bundled ESM, minified 33 | build({ 34 | entryPoints: ["./src/index.ts"], 35 | logLevel: "info", 36 | bundle: true, 37 | minify: true, 38 | format: "esm", 39 | target: "es2020", 40 | outfile: "dist/index.min.mjs", 41 | }); 42 | 43 | // Bundled IIFE 44 | build({ 45 | entryPoints: ["./src/index.ts"], 46 | logLevel: "info", 47 | bundle: true, 48 | format: "iife", 49 | target: "node14", 50 | globalName: "poline", 51 | outfile: "dist/index.js", 52 | }); 53 | 54 | // Bundled IIFE, minified 55 | build({ 56 | entryPoints: ["./src/index.ts"], 57 | logLevel: "info", 58 | bundle: true, 59 | minify: true, 60 | format: "iife", 61 | target: "es6", 62 | globalName: "poline", 63 | outfile: "dist/index.min.js", 64 | }); 65 | 66 | // Bundled UMD 67 | // Adapted from: https://github.com/umdjs/umd/blob/master/templates/returnExports.js 68 | build({ 69 | entryPoints: ["./src/index.ts"], 70 | logLevel: "info", 71 | bundle: true, 72 | format: "iife", 73 | target: "es6", 74 | globalName: "poline", 75 | banner: { 76 | js: `(function(root, factory) { 77 | if (typeof define === 'function' && define.amd) { 78 | define([], factory); 79 | } else if (typeof module === 'object' && module.exports) { 80 | module.exports = factory(); 81 | } else { 82 | root.poline = factory(); 83 | } 84 | } 85 | (typeof self !== 'undefined' ? self : this, function() {`, 86 | }, 87 | footer: { 88 | js: `return poline; }));`, 89 | }, 90 | outfile: "dist/index.umd.js", 91 | }); 92 | -------------------------------------------------------------------------------- /dist/index.cjs: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var __defProp = Object.defineProperty; 3 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 4 | var __getOwnPropNames = Object.getOwnPropertyNames; 5 | var __hasOwnProp = Object.prototype.hasOwnProperty; 6 | var __pow = Math.pow; 7 | var __export = (target, all) => { 8 | for (var name in all) 9 | __defProp(target, name, { get: all[name], enumerable: true }); 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 20 | 21 | // src/index.ts 22 | var src_exports = {}; 23 | __export(src_exports, { 24 | Poline: () => Poline, 25 | hslToPoint: () => hslToPoint, 26 | pointToHSL: () => pointToHSL, 27 | positionFunctions: () => positionFunctions, 28 | randomHSLPair: () => randomHSLPair, 29 | randomHSLTriple: () => randomHSLTriple 30 | }); 31 | module.exports = __toCommonJS(src_exports); 32 | var pointToHSL = (xyz, invertedLightness) => { 33 | const [x, y, z] = xyz; 34 | const cx = 0.5; 35 | const cy = 0.5; 36 | const radians = Math.atan2(y - cy, x - cx); 37 | let deg = radians * (180 / Math.PI); 38 | deg = (360 + deg) % 360; 39 | const s = z; 40 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 41 | const l = dist / cx; 42 | return [deg, s, invertedLightness ? 1 - l : l]; 43 | }; 44 | var hslToPoint = (hsl, invertedLightness) => { 45 | const [h, s, l] = hsl; 46 | const cx = 0.5; 47 | const cy = 0.5; 48 | const radians = h / (180 / Math.PI); 49 | const dist = (invertedLightness ? 1 - l : l) * cx; 50 | const x = cx + dist * Math.cos(radians); 51 | const y = cy + dist * Math.sin(radians); 52 | const z = s; 53 | return [x, y, z]; 54 | }; 55 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 56 | [startHue, saturations[0], lightnesses[0]], 57 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 58 | ]; 59 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 60 | 0.75 + Math.random() * 0.2, 61 | Math.random() * 0.2, 62 | 0.75 + Math.random() * 0.2 63 | ]) => [ 64 | [startHue, saturations[0], lightnesses[0]], 65 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 66 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 67 | ]; 68 | var vectorOnLine = (t, p1, p2, invert = false, fx = (t2, invert2) => invert2 ? 1 - t2 : t2, fy = (t2, invert2) => invert2 ? 1 - t2 : t2, fz = (t2, invert2) => invert2 ? 1 - t2 : t2) => { 69 | const tModifiedX = fx(t, invert); 70 | const tModifiedY = fy(t, invert); 71 | const tModifiedZ = fz(t, invert); 72 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 73 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 74 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 75 | return [x, y, z]; 76 | }; 77 | var vectorsOnLine = (p1, p2, numPoints = 4, invert = false, fx = (t, invert2) => invert2 ? 1 - t : t, fy = (t, invert2) => invert2 ? 1 - t : t, fz = (t, invert2) => invert2 ? 1 - t : t) => { 78 | const points = []; 79 | for (let i = 0; i < numPoints; i++) { 80 | const [x, y, z] = vectorOnLine( 81 | i / (numPoints - 1), 82 | p1, 83 | p2, 84 | invert, 85 | fx, 86 | fy, 87 | fz 88 | ); 89 | points.push([x, y, z]); 90 | } 91 | return points; 92 | }; 93 | var linearPosition = (t) => { 94 | return t; 95 | }; 96 | var exponentialPosition = (t, reverse = false) => { 97 | if (reverse) { 98 | return 1 - __pow(1 - t, 2); 99 | } 100 | return __pow(t, 2); 101 | }; 102 | var quadraticPosition = (t, reverse = false) => { 103 | if (reverse) { 104 | return 1 - __pow(1 - t, 3); 105 | } 106 | return __pow(t, 3); 107 | }; 108 | var cubicPosition = (t, reverse = false) => { 109 | if (reverse) { 110 | return 1 - __pow(1 - t, 4); 111 | } 112 | return __pow(t, 4); 113 | }; 114 | var quarticPosition = (t, reverse = false) => { 115 | if (reverse) { 116 | return 1 - __pow(1 - t, 5); 117 | } 118 | return __pow(t, 5); 119 | }; 120 | var sinusoidalPosition = (t, reverse = false) => { 121 | if (reverse) { 122 | return 1 - Math.sin((1 - t) * Math.PI / 2); 123 | } 124 | return Math.sin(t * Math.PI / 2); 125 | }; 126 | var asinusoidalPosition = (t, reverse = false) => { 127 | if (reverse) { 128 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 129 | } 130 | return Math.asin(t) / (Math.PI / 2); 131 | }; 132 | var arcPosition = (t, reverse = false) => { 133 | if (reverse) { 134 | return 1 - Math.sqrt(1 - __pow(t, 2)); 135 | } 136 | return 1 - Math.sqrt(1 - t); 137 | }; 138 | var smoothStepPosition = (t) => { 139 | return __pow(t, 2) * (3 - 2 * t); 140 | }; 141 | var positionFunctions = { 142 | linearPosition, 143 | exponentialPosition, 144 | quadraticPosition, 145 | cubicPosition, 146 | quarticPosition, 147 | sinusoidalPosition, 148 | asinusoidalPosition, 149 | arcPosition, 150 | smoothStepPosition 151 | }; 152 | var distance = (p1, p2, hueMode = false) => { 153 | const a1 = p1[0]; 154 | const a2 = p2[0]; 155 | let diffA = 0; 156 | if (hueMode && a1 !== null && a2 !== null) { 157 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 158 | diffA = diffA / 360; 159 | } else { 160 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 161 | } 162 | const a = diffA; 163 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 164 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 165 | return Math.sqrt(a * a + b * b + c * c); 166 | }; 167 | var ColorPoint = class { 168 | constructor({ 169 | xyz, 170 | color, 171 | invertedLightness = false 172 | } = {}) { 173 | this.x = 0; 174 | this.y = 0; 175 | this.z = 0; 176 | this.color = [0, 0, 0]; 177 | this._invertedLightness = false; 178 | this._invertedLightness = invertedLightness; 179 | this.positionOrColor({ xyz, color, invertedLightness }); 180 | } 181 | positionOrColor({ 182 | xyz, 183 | color, 184 | invertedLightness = false 185 | }) { 186 | if (xyz && color || !xyz && !color) { 187 | throw new Error("Point must be initialized with either x,y,z or hsl"); 188 | } else if (xyz) { 189 | this.x = xyz[0]; 190 | this.y = xyz[1]; 191 | this.z = xyz[2]; 192 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 193 | } else if (color) { 194 | this.color = color; 195 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 196 | } 197 | } 198 | set position([x, y, z]) { 199 | this.x = x; 200 | this.y = y; 201 | this.z = z; 202 | this.color = pointToHSL( 203 | [this.x, this.y, this.z], 204 | this._invertedLightness 205 | ); 206 | } 207 | get position() { 208 | return [this.x, this.y, this.z]; 209 | } 210 | set hsl([h, s, l]) { 211 | this.color = [h, s, l]; 212 | [this.x, this.y, this.z] = hslToPoint( 213 | this.color, 214 | this._invertedLightness 215 | ); 216 | } 217 | get hsl() { 218 | return this.color; 219 | } 220 | get hslCSS() { 221 | const [h, s, l] = this.color; 222 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 223 | 2 224 | )}%)`; 225 | } 226 | get oklchCSS() { 227 | const [h, s, l] = this.color; 228 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 229 | 2 230 | )})`; 231 | } 232 | get lchCSS() { 233 | const [h, s, l] = this.color; 234 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 235 | 2 236 | )})`; 237 | } 238 | shiftHue(angle) { 239 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 240 | [this.x, this.y, this.z] = hslToPoint( 241 | this.color, 242 | this._invertedLightness 243 | ); 244 | } 245 | }; 246 | var Poline = class { 247 | constructor({ 248 | anchorColors = randomHSLPair(), 249 | numPoints = 4, 250 | positionFunction = sinusoidalPosition, 251 | positionFunctionX, 252 | positionFunctionY, 253 | positionFunctionZ, 254 | closedLoop, 255 | invertedLightness 256 | } = { 257 | anchorColors: randomHSLPair(), 258 | numPoints: 4, 259 | positionFunction: sinusoidalPosition, 260 | closedLoop: false 261 | }) { 262 | this._needsUpdate = true; 263 | this._positionFunctionX = sinusoidalPosition; 264 | this._positionFunctionY = sinusoidalPosition; 265 | this._positionFunctionZ = sinusoidalPosition; 266 | this.connectLastAndFirstAnchor = false; 267 | this._animationFrame = null; 268 | this._invertedLightness = false; 269 | if (!anchorColors || anchorColors.length < 2) { 270 | throw new Error("Must have at least two anchor colors"); 271 | } 272 | this._anchorPoints = anchorColors.map( 273 | (point) => new ColorPoint({ color: point, invertedLightness }) 274 | ); 275 | this._numPoints = numPoints + 2; 276 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 277 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 278 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 279 | this.connectLastAndFirstAnchor = closedLoop || false; 280 | this._invertedLightness = invertedLightness || false; 281 | this.updateAnchorPairs(); 282 | } 283 | get numPoints() { 284 | return this._numPoints - 2; 285 | } 286 | set numPoints(numPoints) { 287 | if (numPoints < 1) { 288 | throw new Error("Must have at least one point"); 289 | } 290 | this._numPoints = numPoints + 2; 291 | this.updateAnchorPairs(); 292 | } 293 | set positionFunction(positionFunction) { 294 | if (Array.isArray(positionFunction)) { 295 | if (positionFunction.length !== 3) { 296 | throw new Error("Position function array must have 3 elements"); 297 | } 298 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 299 | throw new Error("Position function array must have 3 functions"); 300 | } 301 | this._positionFunctionX = positionFunction[0]; 302 | this._positionFunctionY = positionFunction[1]; 303 | this._positionFunctionZ = positionFunction[2]; 304 | } else { 305 | this._positionFunctionX = positionFunction; 306 | this._positionFunctionY = positionFunction; 307 | this._positionFunctionZ = positionFunction; 308 | } 309 | this.updateAnchorPairs(); 310 | } 311 | get positionFunction() { 312 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 313 | return this._positionFunctionX; 314 | } 315 | return [ 316 | this._positionFunctionX, 317 | this._positionFunctionY, 318 | this._positionFunctionZ 319 | ]; 320 | } 321 | set positionFunctionX(positionFunctionX) { 322 | this._positionFunctionX = positionFunctionX; 323 | this.updateAnchorPairs(); 324 | } 325 | get positionFunctionX() { 326 | return this._positionFunctionX; 327 | } 328 | set positionFunctionY(positionFunctionY) { 329 | this._positionFunctionY = positionFunctionY; 330 | this.updateAnchorPairs(); 331 | } 332 | get positionFunctionY() { 333 | return this._positionFunctionY; 334 | } 335 | set positionFunctionZ(positionFunctionZ) { 336 | this._positionFunctionZ = positionFunctionZ; 337 | this.updateAnchorPairs(); 338 | } 339 | get positionFunctionZ() { 340 | return this._positionFunctionZ; 341 | } 342 | get anchorPoints() { 343 | return this._anchorPoints; 344 | } 345 | set anchorPoints(anchorPoints) { 346 | this._anchorPoints = anchorPoints; 347 | this.updateAnchorPairs(); 348 | } 349 | updateAnchorPairs() { 350 | this._anchorPairs = []; 351 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 352 | for (let i = 0; i < anchorPointsLength; i++) { 353 | const pair = [ 354 | this.anchorPoints[i], 355 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 356 | ]; 357 | this._anchorPairs.push(pair); 358 | } 359 | this.points = this._anchorPairs.map((pair, i) => { 360 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 361 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 362 | return vectorsOnLine( 363 | p1position, 364 | p2position, 365 | this._numPoints, 366 | i % 2 ? true : false, 367 | this.positionFunctionX, 368 | this.positionFunctionY, 369 | this.positionFunctionZ 370 | ).map( 371 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 372 | ); 373 | }); 374 | } 375 | addAnchorPoint({ 376 | xyz, 377 | color, 378 | insertAtIndex 379 | }) { 380 | const newAnchor = new ColorPoint({ 381 | xyz, 382 | color, 383 | invertedLightness: this._invertedLightness 384 | }); 385 | if (insertAtIndex) { 386 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 387 | } else { 388 | this.anchorPoints.push(newAnchor); 389 | } 390 | this.updateAnchorPairs(); 391 | return newAnchor; 392 | } 393 | removeAnchorPoint({ 394 | point, 395 | index 396 | }) { 397 | if (!point && index === void 0) { 398 | throw new Error("Must provide a point or index"); 399 | } 400 | if (this.anchorPoints.length < 3) { 401 | throw new Error("Must have at least two anchor points"); 402 | } 403 | let apid; 404 | if (index !== void 0) { 405 | apid = index; 406 | } else if (point) { 407 | apid = this.anchorPoints.indexOf(point); 408 | } 409 | if (apid > -1 && apid < this.anchorPoints.length) { 410 | this.anchorPoints.splice(apid, 1); 411 | this.updateAnchorPairs(); 412 | } else { 413 | throw new Error("Point not found"); 414 | } 415 | } 416 | updateAnchorPoint({ 417 | point, 418 | pointIndex, 419 | xyz, 420 | color 421 | }) { 422 | if (pointIndex) { 423 | point = this.anchorPoints[pointIndex]; 424 | } 425 | if (!point) { 426 | throw new Error("Must provide a point or pointIndex"); 427 | } 428 | if (!xyz && !color) { 429 | throw new Error("Must provide a new xyz position or color"); 430 | } 431 | if (xyz) 432 | point.position = xyz; 433 | if (color) 434 | point.hsl = color; 435 | this.updateAnchorPairs(); 436 | return point; 437 | } 438 | getClosestAnchorPoint({ 439 | xyz, 440 | hsl, 441 | maxDistance = 1 442 | }) { 443 | if (!xyz && !hsl) { 444 | throw new Error("Must provide a xyz or hsl"); 445 | } 446 | let distances; 447 | if (xyz) { 448 | distances = this.anchorPoints.map( 449 | (anchor) => distance(anchor.position, xyz) 450 | ); 451 | } else if (hsl) { 452 | distances = this.anchorPoints.map( 453 | (anchor) => distance(anchor.hsl, hsl, true) 454 | ); 455 | } 456 | const minDistance = Math.min(...distances); 457 | if (minDistance > maxDistance) { 458 | return null; 459 | } 460 | const closestAnchorIndex = distances.indexOf(minDistance); 461 | return this.anchorPoints[closestAnchorIndex] || null; 462 | } 463 | set closedLoop(newStatus) { 464 | this.connectLastAndFirstAnchor = newStatus; 465 | this.updateAnchorPairs(); 466 | } 467 | get closedLoop() { 468 | return this.connectLastAndFirstAnchor; 469 | } 470 | set invertedLightness(newStatus) { 471 | this._invertedLightness = newStatus; 472 | this.updateAnchorPairs(); 473 | } 474 | get invertedLightness() { 475 | return this._invertedLightness; 476 | } 477 | /** 478 | * Returns a flattened array of all points across all segments, 479 | * removing duplicated anchor points at segment boundaries. 480 | * 481 | * Since anchor points exist at both the end of one segment and 482 | * the beginning of the next, this method keeps only one instance of each. 483 | * The filter logic keeps the first point (index 0) and then filters out 484 | * points whose indices are multiples of the segment size (_numPoints), 485 | * which are the anchor points at the start of each segment (except the first). 486 | * 487 | * This approach ensures we get all unique points in the correct order 488 | * while avoiding duplicated anchor points. 489 | * 490 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 491 | */ 492 | get flattenedPoints() { 493 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 494 | } 495 | get colors() { 496 | const colors = this.flattenedPoints.map((p) => p.color); 497 | if (this.connectLastAndFirstAnchor) { 498 | colors.pop(); 499 | } 500 | return colors; 501 | } 502 | cssColors(mode = "hsl") { 503 | const methods = { 504 | hsl: (p) => p.hslCSS, 505 | oklch: (p) => p.oklchCSS, 506 | lch: (p) => p.lchCSS 507 | }; 508 | const cssColors = this.flattenedPoints.map(methods[mode]); 509 | if (this.connectLastAndFirstAnchor) { 510 | cssColors.pop(); 511 | } 512 | return cssColors; 513 | } 514 | get colorsCSS() { 515 | return this.cssColors("hsl"); 516 | } 517 | get colorsCSSlch() { 518 | return this.cssColors("lch"); 519 | } 520 | get colorsCSSoklch() { 521 | return this.cssColors("oklch"); 522 | } 523 | shiftHue(hShift = 20) { 524 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 525 | this.updateAnchorPairs(); 526 | } 527 | }; 528 | var { p5 } = globalThis; 529 | if (p5) { 530 | console.info("p5 detected, adding poline to p5 prototype"); 531 | const poline = new Poline(); 532 | p5.prototype.poline = poline; 533 | const polineColors = () => poline.colors.map( 534 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 535 | ); 536 | p5.prototype.polineColors = polineColors; 537 | p5.prototype.registerMethod("polineColors", p5.prototype.polineColors); 538 | globalThis.poline = poline; 539 | globalThis.polineColors = polineColors; 540 | } 541 | -------------------------------------------------------------------------------- /dist/index.d.ts: -------------------------------------------------------------------------------- 1 | export type FuncNumberReturn = (arg0: number) => Vector2; 2 | export type Vector2 = [number, number]; 3 | export type Vector3 = [number, ...Vector2]; 4 | export type PartialVector3 = [number | null, number | null, number | null]; 5 | /** 6 | * Converts the given (x, y, z) coordinate to an HSL color 7 | * The (x, y) values are used to calculate the hue, while the z value is used as the saturation 8 | * The lightness value is calculated based on the distance of (x, y) from the center (0.5, 0.5) 9 | * Returns an array [hue, saturation, lightness] 10 | * @param xyz:Vector3 [x, y, z] coordinate array in (x, y, z) format (0-1, 0-1, 0-1) 11 | * @returns [hue, saturation, lightness]: Vector3 color array in HSL format (0-360, 0-1, 0-1) 12 | * @example 13 | * pointToHSL([0.5, 0.5, 1]) // [0, 1, 0.5] 14 | * pointToHSL([0.5, 0.5, 0]) // [0, 1, 0] 15 | **/ 16 | export declare const pointToHSL: (xyz: [number, number, number], invertedLightness: boolean) => [number, number, number]; 17 | /** 18 | * Converts the given HSL color to an (x, y, z) coordinate 19 | * The hue value is used to calculate the (x, y) position, while the saturation value is used as the z coordinate 20 | * The lightness value is used to calculate the distance from the center (0.5, 0.5) 21 | * Returns an array [x, y, z] 22 | * @param hsl:Vector3 [hue, saturation, lightness] color array in HSL format (0-360, 0-1, 0-1) 23 | * @returns [x, y, z]:Vector3 coordinate array in (x, y, z) format (0-1, 0-1, 0-1) 24 | * @example 25 | * hslToPoint([0, 1, 0.5]) // [0.5, 0.5, 1] 26 | * hslToPoint([0, 1, 0]) // [0.5, 0.5, 1] 27 | * hslToPoint([0, 1, 1]) // [0.5, 0.5, 1] 28 | * hslToPoint([0, 0, 0.5]) // [0.5, 0.5, 0] 29 | **/ 30 | export declare const hslToPoint: (hsl: [number, number, number], invertedLightness: boolean) => [number, number, number]; 31 | export declare const randomHSLPair: (startHue?: number, saturations?: Vector2, lightnesses?: Vector2) => [Vector3, Vector3]; 32 | export declare const randomHSLTriple: (startHue?: number, saturations?: [number, number, number], lightnesses?: [number, number, number]) => [Vector3, Vector3, Vector3]; 33 | export type PositionFunction = (t: number, reverse?: boolean) => number; 34 | export declare const positionFunctions: { 35 | linearPosition: PositionFunction; 36 | exponentialPosition: PositionFunction; 37 | quadraticPosition: PositionFunction; 38 | cubicPosition: PositionFunction; 39 | quarticPosition: PositionFunction; 40 | sinusoidalPosition: PositionFunction; 41 | asinusoidalPosition: PositionFunction; 42 | arcPosition: PositionFunction; 43 | smoothStepPosition: PositionFunction; 44 | }; 45 | export type ColorPointCollection = { 46 | xyz?: Vector3; 47 | color?: Vector3; 48 | invertedLightness?: boolean; 49 | }; 50 | declare class ColorPoint { 51 | x: number; 52 | y: number; 53 | z: number; 54 | color: Vector3; 55 | private _invertedLightness; 56 | constructor({ xyz, color, invertedLightness, }?: ColorPointCollection); 57 | positionOrColor({ xyz, color, invertedLightness, }: ColorPointCollection): void; 58 | set position([x, y, z]: Vector3); 59 | get position(): Vector3; 60 | set hsl([h, s, l]: Vector3); 61 | get hsl(): Vector3; 62 | get hslCSS(): string; 63 | get oklchCSS(): string; 64 | get lchCSS(): string; 65 | shiftHue(angle: number): void; 66 | } 67 | export type PolineOptions = { 68 | anchorColors: Vector3[]; 69 | numPoints: number; 70 | positionFunction?: (t: number, invert?: boolean) => number; 71 | positionFunctionX?: (t: number, invert?: boolean) => number; 72 | positionFunctionY?: (t: number, invert?: boolean) => number; 73 | positionFunctionZ?: (t: number, invert?: boolean) => number; 74 | invertedLightness?: boolean; 75 | closedLoop?: boolean; 76 | }; 77 | export declare class Poline { 78 | private _needsUpdate; 79 | private _anchorPoints; 80 | private _numPoints; 81 | private points; 82 | private _positionFunctionX; 83 | private _positionFunctionY; 84 | private _positionFunctionZ; 85 | private _anchorPairs; 86 | private connectLastAndFirstAnchor; 87 | private _animationFrame; 88 | private _invertedLightness; 89 | constructor({ anchorColors, numPoints, positionFunction, positionFunctionX, positionFunctionY, positionFunctionZ, closedLoop, invertedLightness, }?: PolineOptions); 90 | get numPoints(): number; 91 | set numPoints(numPoints: number); 92 | set positionFunction(positionFunction: PositionFunction | PositionFunction[]); 93 | get positionFunction(): PositionFunction | PositionFunction[]; 94 | set positionFunctionX(positionFunctionX: PositionFunction); 95 | get positionFunctionX(): PositionFunction; 96 | set positionFunctionY(positionFunctionY: PositionFunction); 97 | get positionFunctionY(): PositionFunction; 98 | set positionFunctionZ(positionFunctionZ: PositionFunction); 99 | get positionFunctionZ(): PositionFunction; 100 | get anchorPoints(): ColorPoint[]; 101 | set anchorPoints(anchorPoints: ColorPoint[]); 102 | updateAnchorPairs(): void; 103 | addAnchorPoint({ xyz, color, insertAtIndex, }: ColorPointCollection & { 104 | insertAtIndex: number; 105 | }): ColorPoint; 106 | removeAnchorPoint({ point, index, }: { 107 | point?: ColorPoint; 108 | index?: number; 109 | }): void; 110 | updateAnchorPoint({ point, pointIndex, xyz, color, }: { 111 | point?: ColorPoint; 112 | pointIndex?: number; 113 | } & ColorPointCollection): ColorPoint; 114 | getClosestAnchorPoint({ xyz, hsl, maxDistance, }: { 115 | xyz?: PartialVector3; 116 | hsl?: PartialVector3; 117 | maxDistance?: number; 118 | }): ColorPoint | null; 119 | set closedLoop(newStatus: boolean); 120 | get closedLoop(): boolean; 121 | set invertedLightness(newStatus: boolean); 122 | get invertedLightness(): boolean; 123 | /** 124 | * Returns a flattened array of all points across all segments, 125 | * removing duplicated anchor points at segment boundaries. 126 | * 127 | * Since anchor points exist at both the end of one segment and 128 | * the beginning of the next, this method keeps only one instance of each. 129 | * The filter logic keeps the first point (index 0) and then filters out 130 | * points whose indices are multiples of the segment size (_numPoints), 131 | * which are the anchor points at the start of each segment (except the first). 132 | * 133 | * This approach ensures we get all unique points in the correct order 134 | * while avoiding duplicated anchor points. 135 | * 136 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 137 | */ 138 | get flattenedPoints(): ColorPoint[]; 139 | get colors(): [number, number, number][]; 140 | cssColors(mode?: "hsl" | "oklch" | "lch"): string[]; 141 | get colorsCSS(): string[]; 142 | get colorsCSSlch(): string[]; 143 | get colorsCSSoklch(): string[]; 144 | shiftHue(hShift?: number): void; 145 | } 146 | export {}; 147 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | var poline = (() => { 3 | var __defProp = Object.defineProperty; 4 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 5 | var __getOwnPropNames = Object.getOwnPropertyNames; 6 | var __hasOwnProp = Object.prototype.hasOwnProperty; 7 | var __export = (target, all) => { 8 | for (var name in all) 9 | __defProp(target, name, { get: all[name], enumerable: true }); 10 | }; 11 | var __copyProps = (to, from, except, desc) => { 12 | if (from && typeof from === "object" || typeof from === "function") { 13 | for (let key of __getOwnPropNames(from)) 14 | if (!__hasOwnProp.call(to, key) && key !== except) 15 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 16 | } 17 | return to; 18 | }; 19 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 20 | 21 | // src/index.ts 22 | var src_exports = {}; 23 | __export(src_exports, { 24 | Poline: () => Poline, 25 | hslToPoint: () => hslToPoint, 26 | pointToHSL: () => pointToHSL, 27 | positionFunctions: () => positionFunctions, 28 | randomHSLPair: () => randomHSLPair, 29 | randomHSLTriple: () => randomHSLTriple 30 | }); 31 | var pointToHSL = (xyz, invertedLightness) => { 32 | const [x, y, z] = xyz; 33 | const cx = 0.5; 34 | const cy = 0.5; 35 | const radians = Math.atan2(y - cy, x - cx); 36 | let deg = radians * (180 / Math.PI); 37 | deg = (360 + deg) % 360; 38 | const s = z; 39 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 40 | const l = dist / cx; 41 | return [deg, s, invertedLightness ? 1 - l : l]; 42 | }; 43 | var hslToPoint = (hsl, invertedLightness) => { 44 | const [h, s, l] = hsl; 45 | const cx = 0.5; 46 | const cy = 0.5; 47 | const radians = h / (180 / Math.PI); 48 | const dist = (invertedLightness ? 1 - l : l) * cx; 49 | const x = cx + dist * Math.cos(radians); 50 | const y = cy + dist * Math.sin(radians); 51 | const z = s; 52 | return [x, y, z]; 53 | }; 54 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 55 | [startHue, saturations[0], lightnesses[0]], 56 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 57 | ]; 58 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 59 | 0.75 + Math.random() * 0.2, 60 | Math.random() * 0.2, 61 | 0.75 + Math.random() * 0.2 62 | ]) => [ 63 | [startHue, saturations[0], lightnesses[0]], 64 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 65 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 66 | ]; 67 | var vectorOnLine = (t, p1, p2, invert = false, fx = (t2, invert2) => invert2 ? 1 - t2 : t2, fy = (t2, invert2) => invert2 ? 1 - t2 : t2, fz = (t2, invert2) => invert2 ? 1 - t2 : t2) => { 68 | const tModifiedX = fx(t, invert); 69 | const tModifiedY = fy(t, invert); 70 | const tModifiedZ = fz(t, invert); 71 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 72 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 73 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 74 | return [x, y, z]; 75 | }; 76 | var vectorsOnLine = (p1, p2, numPoints = 4, invert = false, fx = (t, invert2) => invert2 ? 1 - t : t, fy = (t, invert2) => invert2 ? 1 - t : t, fz = (t, invert2) => invert2 ? 1 - t : t) => { 77 | const points = []; 78 | for (let i = 0; i < numPoints; i++) { 79 | const [x, y, z] = vectorOnLine( 80 | i / (numPoints - 1), 81 | p1, 82 | p2, 83 | invert, 84 | fx, 85 | fy, 86 | fz 87 | ); 88 | points.push([x, y, z]); 89 | } 90 | return points; 91 | }; 92 | var linearPosition = (t) => { 93 | return t; 94 | }; 95 | var exponentialPosition = (t, reverse = false) => { 96 | if (reverse) { 97 | return 1 - (1 - t) ** 2; 98 | } 99 | return t ** 2; 100 | }; 101 | var quadraticPosition = (t, reverse = false) => { 102 | if (reverse) { 103 | return 1 - (1 - t) ** 3; 104 | } 105 | return t ** 3; 106 | }; 107 | var cubicPosition = (t, reverse = false) => { 108 | if (reverse) { 109 | return 1 - (1 - t) ** 4; 110 | } 111 | return t ** 4; 112 | }; 113 | var quarticPosition = (t, reverse = false) => { 114 | if (reverse) { 115 | return 1 - (1 - t) ** 5; 116 | } 117 | return t ** 5; 118 | }; 119 | var sinusoidalPosition = (t, reverse = false) => { 120 | if (reverse) { 121 | return 1 - Math.sin((1 - t) * Math.PI / 2); 122 | } 123 | return Math.sin(t * Math.PI / 2); 124 | }; 125 | var asinusoidalPosition = (t, reverse = false) => { 126 | if (reverse) { 127 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 128 | } 129 | return Math.asin(t) / (Math.PI / 2); 130 | }; 131 | var arcPosition = (t, reverse = false) => { 132 | if (reverse) { 133 | return 1 - Math.sqrt(1 - t ** 2); 134 | } 135 | return 1 - Math.sqrt(1 - t); 136 | }; 137 | var smoothStepPosition = (t) => { 138 | return t ** 2 * (3 - 2 * t); 139 | }; 140 | var positionFunctions = { 141 | linearPosition, 142 | exponentialPosition, 143 | quadraticPosition, 144 | cubicPosition, 145 | quarticPosition, 146 | sinusoidalPosition, 147 | asinusoidalPosition, 148 | arcPosition, 149 | smoothStepPosition 150 | }; 151 | var distance = (p1, p2, hueMode = false) => { 152 | const a1 = p1[0]; 153 | const a2 = p2[0]; 154 | let diffA = 0; 155 | if (hueMode && a1 !== null && a2 !== null) { 156 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 157 | diffA = diffA / 360; 158 | } else { 159 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 160 | } 161 | const a = diffA; 162 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 163 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 164 | return Math.sqrt(a * a + b * b + c * c); 165 | }; 166 | var ColorPoint = class { 167 | constructor({ 168 | xyz, 169 | color, 170 | invertedLightness = false 171 | } = {}) { 172 | this.x = 0; 173 | this.y = 0; 174 | this.z = 0; 175 | this.color = [0, 0, 0]; 176 | this._invertedLightness = false; 177 | this._invertedLightness = invertedLightness; 178 | this.positionOrColor({ xyz, color, invertedLightness }); 179 | } 180 | positionOrColor({ 181 | xyz, 182 | color, 183 | invertedLightness = false 184 | }) { 185 | if (xyz && color || !xyz && !color) { 186 | throw new Error("Point must be initialized with either x,y,z or hsl"); 187 | } else if (xyz) { 188 | this.x = xyz[0]; 189 | this.y = xyz[1]; 190 | this.z = xyz[2]; 191 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 192 | } else if (color) { 193 | this.color = color; 194 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 195 | } 196 | } 197 | set position([x, y, z]) { 198 | this.x = x; 199 | this.y = y; 200 | this.z = z; 201 | this.color = pointToHSL( 202 | [this.x, this.y, this.z], 203 | this._invertedLightness 204 | ); 205 | } 206 | get position() { 207 | return [this.x, this.y, this.z]; 208 | } 209 | set hsl([h, s, l]) { 210 | this.color = [h, s, l]; 211 | [this.x, this.y, this.z] = hslToPoint( 212 | this.color, 213 | this._invertedLightness 214 | ); 215 | } 216 | get hsl() { 217 | return this.color; 218 | } 219 | get hslCSS() { 220 | const [h, s, l] = this.color; 221 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 222 | 2 223 | )}%)`; 224 | } 225 | get oklchCSS() { 226 | const [h, s, l] = this.color; 227 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 228 | 2 229 | )})`; 230 | } 231 | get lchCSS() { 232 | const [h, s, l] = this.color; 233 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 234 | 2 235 | )})`; 236 | } 237 | shiftHue(angle) { 238 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 239 | [this.x, this.y, this.z] = hslToPoint( 240 | this.color, 241 | this._invertedLightness 242 | ); 243 | } 244 | }; 245 | var Poline = class { 246 | constructor({ 247 | anchorColors = randomHSLPair(), 248 | numPoints = 4, 249 | positionFunction = sinusoidalPosition, 250 | positionFunctionX, 251 | positionFunctionY, 252 | positionFunctionZ, 253 | closedLoop, 254 | invertedLightness 255 | } = { 256 | anchorColors: randomHSLPair(), 257 | numPoints: 4, 258 | positionFunction: sinusoidalPosition, 259 | closedLoop: false 260 | }) { 261 | this._needsUpdate = true; 262 | this._positionFunctionX = sinusoidalPosition; 263 | this._positionFunctionY = sinusoidalPosition; 264 | this._positionFunctionZ = sinusoidalPosition; 265 | this.connectLastAndFirstAnchor = false; 266 | this._animationFrame = null; 267 | this._invertedLightness = false; 268 | if (!anchorColors || anchorColors.length < 2) { 269 | throw new Error("Must have at least two anchor colors"); 270 | } 271 | this._anchorPoints = anchorColors.map( 272 | (point) => new ColorPoint({ color: point, invertedLightness }) 273 | ); 274 | this._numPoints = numPoints + 2; 275 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 276 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 277 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 278 | this.connectLastAndFirstAnchor = closedLoop || false; 279 | this._invertedLightness = invertedLightness || false; 280 | this.updateAnchorPairs(); 281 | } 282 | get numPoints() { 283 | return this._numPoints - 2; 284 | } 285 | set numPoints(numPoints) { 286 | if (numPoints < 1) { 287 | throw new Error("Must have at least one point"); 288 | } 289 | this._numPoints = numPoints + 2; 290 | this.updateAnchorPairs(); 291 | } 292 | set positionFunction(positionFunction) { 293 | if (Array.isArray(positionFunction)) { 294 | if (positionFunction.length !== 3) { 295 | throw new Error("Position function array must have 3 elements"); 296 | } 297 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 298 | throw new Error("Position function array must have 3 functions"); 299 | } 300 | this._positionFunctionX = positionFunction[0]; 301 | this._positionFunctionY = positionFunction[1]; 302 | this._positionFunctionZ = positionFunction[2]; 303 | } else { 304 | this._positionFunctionX = positionFunction; 305 | this._positionFunctionY = positionFunction; 306 | this._positionFunctionZ = positionFunction; 307 | } 308 | this.updateAnchorPairs(); 309 | } 310 | get positionFunction() { 311 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 312 | return this._positionFunctionX; 313 | } 314 | return [ 315 | this._positionFunctionX, 316 | this._positionFunctionY, 317 | this._positionFunctionZ 318 | ]; 319 | } 320 | set positionFunctionX(positionFunctionX) { 321 | this._positionFunctionX = positionFunctionX; 322 | this.updateAnchorPairs(); 323 | } 324 | get positionFunctionX() { 325 | return this._positionFunctionX; 326 | } 327 | set positionFunctionY(positionFunctionY) { 328 | this._positionFunctionY = positionFunctionY; 329 | this.updateAnchorPairs(); 330 | } 331 | get positionFunctionY() { 332 | return this._positionFunctionY; 333 | } 334 | set positionFunctionZ(positionFunctionZ) { 335 | this._positionFunctionZ = positionFunctionZ; 336 | this.updateAnchorPairs(); 337 | } 338 | get positionFunctionZ() { 339 | return this._positionFunctionZ; 340 | } 341 | get anchorPoints() { 342 | return this._anchorPoints; 343 | } 344 | set anchorPoints(anchorPoints) { 345 | this._anchorPoints = anchorPoints; 346 | this.updateAnchorPairs(); 347 | } 348 | updateAnchorPairs() { 349 | this._anchorPairs = []; 350 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 351 | for (let i = 0; i < anchorPointsLength; i++) { 352 | const pair = [ 353 | this.anchorPoints[i], 354 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 355 | ]; 356 | this._anchorPairs.push(pair); 357 | } 358 | this.points = this._anchorPairs.map((pair, i) => { 359 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 360 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 361 | return vectorsOnLine( 362 | p1position, 363 | p2position, 364 | this._numPoints, 365 | i % 2 ? true : false, 366 | this.positionFunctionX, 367 | this.positionFunctionY, 368 | this.positionFunctionZ 369 | ).map( 370 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 371 | ); 372 | }); 373 | } 374 | addAnchorPoint({ 375 | xyz, 376 | color, 377 | insertAtIndex 378 | }) { 379 | const newAnchor = new ColorPoint({ 380 | xyz, 381 | color, 382 | invertedLightness: this._invertedLightness 383 | }); 384 | if (insertAtIndex) { 385 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 386 | } else { 387 | this.anchorPoints.push(newAnchor); 388 | } 389 | this.updateAnchorPairs(); 390 | return newAnchor; 391 | } 392 | removeAnchorPoint({ 393 | point, 394 | index 395 | }) { 396 | if (!point && index === void 0) { 397 | throw new Error("Must provide a point or index"); 398 | } 399 | if (this.anchorPoints.length < 3) { 400 | throw new Error("Must have at least two anchor points"); 401 | } 402 | let apid; 403 | if (index !== void 0) { 404 | apid = index; 405 | } else if (point) { 406 | apid = this.anchorPoints.indexOf(point); 407 | } 408 | if (apid > -1 && apid < this.anchorPoints.length) { 409 | this.anchorPoints.splice(apid, 1); 410 | this.updateAnchorPairs(); 411 | } else { 412 | throw new Error("Point not found"); 413 | } 414 | } 415 | updateAnchorPoint({ 416 | point, 417 | pointIndex, 418 | xyz, 419 | color 420 | }) { 421 | if (pointIndex) { 422 | point = this.anchorPoints[pointIndex]; 423 | } 424 | if (!point) { 425 | throw new Error("Must provide a point or pointIndex"); 426 | } 427 | if (!xyz && !color) { 428 | throw new Error("Must provide a new xyz position or color"); 429 | } 430 | if (xyz) 431 | point.position = xyz; 432 | if (color) 433 | point.hsl = color; 434 | this.updateAnchorPairs(); 435 | return point; 436 | } 437 | getClosestAnchorPoint({ 438 | xyz, 439 | hsl, 440 | maxDistance = 1 441 | }) { 442 | if (!xyz && !hsl) { 443 | throw new Error("Must provide a xyz or hsl"); 444 | } 445 | let distances; 446 | if (xyz) { 447 | distances = this.anchorPoints.map( 448 | (anchor) => distance(anchor.position, xyz) 449 | ); 450 | } else if (hsl) { 451 | distances = this.anchorPoints.map( 452 | (anchor) => distance(anchor.hsl, hsl, true) 453 | ); 454 | } 455 | const minDistance = Math.min(...distances); 456 | if (minDistance > maxDistance) { 457 | return null; 458 | } 459 | const closestAnchorIndex = distances.indexOf(minDistance); 460 | return this.anchorPoints[closestAnchorIndex] || null; 461 | } 462 | set closedLoop(newStatus) { 463 | this.connectLastAndFirstAnchor = newStatus; 464 | this.updateAnchorPairs(); 465 | } 466 | get closedLoop() { 467 | return this.connectLastAndFirstAnchor; 468 | } 469 | set invertedLightness(newStatus) { 470 | this._invertedLightness = newStatus; 471 | this.updateAnchorPairs(); 472 | } 473 | get invertedLightness() { 474 | return this._invertedLightness; 475 | } 476 | /** 477 | * Returns a flattened array of all points across all segments, 478 | * removing duplicated anchor points at segment boundaries. 479 | * 480 | * Since anchor points exist at both the end of one segment and 481 | * the beginning of the next, this method keeps only one instance of each. 482 | * The filter logic keeps the first point (index 0) and then filters out 483 | * points whose indices are multiples of the segment size (_numPoints), 484 | * which are the anchor points at the start of each segment (except the first). 485 | * 486 | * This approach ensures we get all unique points in the correct order 487 | * while avoiding duplicated anchor points. 488 | * 489 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 490 | */ 491 | get flattenedPoints() { 492 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 493 | } 494 | get colors() { 495 | const colors = this.flattenedPoints.map((p) => p.color); 496 | if (this.connectLastAndFirstAnchor) { 497 | colors.pop(); 498 | } 499 | return colors; 500 | } 501 | cssColors(mode = "hsl") { 502 | const methods = { 503 | hsl: (p) => p.hslCSS, 504 | oklch: (p) => p.oklchCSS, 505 | lch: (p) => p.lchCSS 506 | }; 507 | const cssColors = this.flattenedPoints.map(methods[mode]); 508 | if (this.connectLastAndFirstAnchor) { 509 | cssColors.pop(); 510 | } 511 | return cssColors; 512 | } 513 | get colorsCSS() { 514 | return this.cssColors("hsl"); 515 | } 516 | get colorsCSSlch() { 517 | return this.cssColors("lch"); 518 | } 519 | get colorsCSSoklch() { 520 | return this.cssColors("oklch"); 521 | } 522 | shiftHue(hShift = 20) { 523 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 524 | this.updateAnchorPairs(); 525 | } 526 | }; 527 | var { p5 } = globalThis; 528 | if (p5) { 529 | console.info("p5 detected, adding poline to p5 prototype"); 530 | const poline = new Poline(); 531 | p5.prototype.poline = poline; 532 | const polineColors = () => poline.colors.map( 533 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 534 | ); 535 | p5.prototype.polineColors = polineColors; 536 | p5.prototype.registerMethod("polineColors", p5.prototype.polineColors); 537 | globalThis.poline = poline; 538 | globalThis.polineColors = polineColors; 539 | } 540 | return __toCommonJS(src_exports); 541 | })(); 542 | -------------------------------------------------------------------------------- /dist/index.min.cjs: -------------------------------------------------------------------------------- 1 | "use strict";var F=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var l=Math.pow;var A=(n,t)=>{for(var o in t)F(n,o,{get:t[o],enumerable:!0})},w=(n,t,o,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of V(t))!x.call(n,s)&&s!==o&&F(n,s,{get:()=>t[s],enumerable:!(i=v(t,s))||i.enumerable});return n};var y=n=>w(F({},"__esModule",{value:!0}),n);var T={};A(T,{Poline:()=>f,hslToPoint:()=>m,pointToHSL:()=>g,positionFunctions:()=>q,randomHSLPair:()=>C,randomHSLTriple:()=>L});module.exports=y(T);var g=(n,t)=>{let[o,i,s]=n,c=.5,h=.5,e=Math.atan2(i-h,o-c)*(180/Math.PI);e=(360+e)%360;let a=s,p=Math.sqrt(Math.pow(i-h,2)+Math.pow(o-c,2))/c;return[e,a,t?1-p:p]},m=(n,t)=>{let[o,i,s]=n,c=.5,h=.5,r=o/(180/Math.PI),e=(t?1-s:s)*c,a=c+e*Math.cos(r),P=h+e*Math.sin(r);return[a,P,i]},C=(n=Math.random()*360,t=[Math.random(),Math.random()],o=[.75+Math.random()*.2,.3+Math.random()*.2])=>[[n,t[0],o[0]],[(n+60+Math.random()*180)%360,t[1],o[1]]],L=(n=Math.random()*360,t=[Math.random(),Math.random(),Math.random()],o=[.75+Math.random()*.2,Math.random()*.2,.75+Math.random()*.2])=>[[n,t[0],o[0]],[(n+60+Math.random()*180)%360,t[1],o[1]],[(n+60+Math.random()*180)%360,t[2],o[2]]],S=(n,t,o,i=!1,s=(r,e)=>e?1-r:r,c=(r,e)=>e?1-r:r,h=(r,e)=>e?1-r:r)=>{let r=s(n,i),e=c(n,i),a=h(n,i),P=(1-r)*t[0]+r*o[0],p=(1-e)*t[1]+e*o[1],M=(1-a)*t[2]+a*o[2];return[P,p,M]},X=(n,t,o=4,i=!1,s=(r,e)=>e?1-r:r,c=(r,e)=>e?1-r:r,h=(r,e)=>e?1-r:r)=>{let r=[];for(let e=0;en,Z=(n,t=!1)=>t?1-l(1-n,2):l(n,2),z=(n,t=!1)=>t?1-l(1-n,3):l(n,3),E=(n,t=!1)=>t?1-l(1-n,4):l(n,4),$=(n,t=!1)=>t?1-l(1-n,5):l(n,5),u=(n,t=!1)=>t?1-Math.sin((1-n)*Math.PI/2):Math.sin(n*Math.PI/2),k=(n,t=!1)=>t?1-Math.asin(1-n)/(Math.PI/2):Math.asin(n)/(Math.PI/2),I=(n,t=!1)=>t?1-Math.sqrt(1-l(n,2)):1-Math.sqrt(1-n),O=n=>l(n,2)*(3-2*n),q={linearPosition:Y,exponentialPosition:Z,quadraticPosition:z,cubicPosition:E,quarticPosition:$,sinusoidalPosition:u,asinusoidalPosition:k,arcPosition:I,smoothStepPosition:O},_=(n,t,o=!1)=>{let i=n[0],s=t[0],c=0;o&&i!==null&&s!==null?(c=Math.min(Math.abs(i-s),360-Math.abs(i-s)),c=c/360):c=i===null||s===null?0:i-s;let h=c,r=n[1]===null||t[1]===null?0:t[1]-n[1],e=n[2]===null||t[2]===null?0:t[2]-n[2];return Math.sqrt(h*h+r*r+e*e)},b=class{constructor({xyz:t,color:o,invertedLightness:i=!1}={}){this.x=0;this.y=0;this.z=0;this.color=[0,0,0];this._invertedLightness=!1;this._invertedLightness=i,this.positionOrColor({xyz:t,color:o,invertedLightness:i})}positionOrColor({xyz:t,color:o,invertedLightness:i=!1}){if(t&&o||!t&&!o)throw new Error("Point must be initialized with either x,y,z or hsl");t?(this.x=t[0],this.y=t[1],this.z=t[2],this.color=g([this.x,this.y,this.z],i)):o&&(this.color=o,[this.x,this.y,this.z]=m(o,i))}set position([t,o,i]){this.x=t,this.y=o,this.z=i,this.color=g([this.x,this.y,this.z],this._invertedLightness)}get position(){return[this.x,this.y,this.z]}set hsl([t,o,i]){this.color=[t,o,i],[this.x,this.y,this.z]=m(this.color,this._invertedLightness)}get hsl(){return this.color}get hslCSS(){let[t,o,i]=this.color;return`hsl(${t.toFixed(2)}, ${(o*100).toFixed(2)}%, ${(i*100).toFixed(2)}%)`}get oklchCSS(){let[t,o,i]=this.color;return`oklch(${(i*100).toFixed(2)}% ${(o*.4).toFixed(3)} ${t.toFixed(2)})`}get lchCSS(){let[t,o,i]=this.color;return`lch(${(i*100).toFixed(2)}% ${(o*150).toFixed(2)} ${t.toFixed(2)})`}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=m(this.color,this._invertedLightness)}},f=class{constructor({anchorColors:t=C(),numPoints:o=4,positionFunction:i=u,positionFunctionX:s,positionFunctionY:c,positionFunctionZ:h,closedLoop:r,invertedLightness:e}={anchorColors:C(),numPoints:4,positionFunction:u,closedLoop:!1}){this._needsUpdate=!0;this._positionFunctionX=u;this._positionFunctionY=u;this._positionFunctionZ=u;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(a=>new b({color:a,invertedLightness:e})),this._numPoints=o+2,this._positionFunctionX=s||i||u,this._positionFunctionY=c||i||u,this._positionFunctionZ=h||i||u,this.connectLastAndFirstAnchor=r||!1,this._invertedLightness=e||!1,this.updateAnchorPairs()}get numPoints(){return this._numPoints-2}set numPoints(t){if(t<1)throw new Error("Must have at least one point");this._numPoints=t+2,this.updateAnchorPairs()}set positionFunction(t){if(Array.isArray(t)){if(t.length!==3)throw new Error("Position function array must have 3 elements");if(typeof t[0]!="function"||typeof t[1]!="function"||typeof t[2]!="function")throw new Error("Position function array must have 3 functions");this._positionFunctionX=t[0],this._positionFunctionY=t[1],this._positionFunctionZ=t[2]}else this._positionFunctionX=t,this._positionFunctionY=t,this._positionFunctionZ=t;this.updateAnchorPairs()}get positionFunction(){return this._positionFunctionX===this._positionFunctionY&&this._positionFunctionX===this._positionFunctionZ?this._positionFunctionX:[this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ]}set positionFunctionX(t){this._positionFunctionX=t,this.updateAnchorPairs()}get positionFunctionX(){return this._positionFunctionX}set positionFunctionY(t){this._positionFunctionY=t,this.updateAnchorPairs()}get positionFunctionY(){return this._positionFunctionY}set positionFunctionZ(t){this._positionFunctionZ=t,this.updateAnchorPairs()}get positionFunctionZ(){return this._positionFunctionZ}get anchorPoints(){return this._anchorPoints}set anchorPoints(t){this._anchorPoints=t,this.updateAnchorPairs()}updateAnchorPairs(){this._anchorPairs=[];let t=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1;for(let o=0;o{let s=o[0]?o[0].position:[0,0,0],c=o[1]?o[1].position:[0,0,0];return X(s,c,this._numPoints,!!(i%2),this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(h=>new b({xyz:h,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:o,insertAtIndex:i}){let s=new b({xyz:t,color:o,invertedLightness:this._invertedLightness});return i?this.anchorPoints.splice(i,0,s):this.anchorPoints.push(s),this.updateAnchorPairs(),s}removeAnchorPoint({point:t,index:o}){if(!t&&o===void 0)throw new Error("Must provide a point or index");if(this.anchorPoints.length<3)throw new Error("Must have at least two anchor points");let i;if(o!==void 0?i=o:t&&(i=this.anchorPoints.indexOf(t)),i>-1&&i_(r.position,t)):o&&(s=this.anchorPoints.map(r=>_(r.hsl,o,!0)));let c=Math.min(...s);if(c>i)return null;let h=s.indexOf(c);return this.anchorPoints[h]||null}set closedLoop(t){this.connectLastAndFirstAnchor=t,this.updateAnchorPairs()}get closedLoop(){return this.connectLastAndFirstAnchor}set invertedLightness(t){this._invertedLightness=t,this.updateAnchorPairs()}get invertedLightness(){return this._invertedLightness}get flattenedPoints(){return this.points.flat().filter((t,o)=>o!=0?o%this._numPoints:!0)}get colors(){let t=this.flattenedPoints.map(o=>o.color);return this.connectLastAndFirstAnchor&&t.pop(),t}cssColors(t="hsl"){let o={hsl:s=>s.hslCSS,oklch:s=>s.oklchCSS,lch:s=>s.lchCSS},i=this.flattenedPoints.map(o[t]);return this.connectLastAndFirstAnchor&&i.pop(),i}get colorsCSS(){return this.cssColors("hsl")}get colorsCSSlch(){return this.cssColors("lch")}get colorsCSSoklch(){return this.cssColors("oklch")}shiftHue(t=20){this.anchorPoints.forEach(o=>o.shiftHue(t)),this.updateAnchorPairs()}},{p5:d}=globalThis;if(d){console.info("p5 detected, adding poline to p5 prototype");let n=new f;d.prototype.poline=n;let t=()=>n.colors.map(o=>`hsl(${Math.round(o[0])},${o[1]*100}%,${o[2]*100}%)`);d.prototype.polineColors=t,d.prototype.registerMethod("polineColors",d.prototype.polineColors),globalThis.poline=n,globalThis.polineColors=t} 2 | -------------------------------------------------------------------------------- /dist/index.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var poline=(()=>{var F=Object.defineProperty;var v=Object.getOwnPropertyDescriptor;var V=Object.getOwnPropertyNames;var x=Object.prototype.hasOwnProperty;var l=Math.pow;var A=(n,t)=>{for(var o in t)F(n,o,{get:t[o],enumerable:!0})},w=(n,t,o,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of V(t))!x.call(n,s)&&s!==o&&F(n,s,{get:()=>t[s],enumerable:!(i=v(t,s))||i.enumerable});return n};var y=n=>w(F({},"__esModule",{value:!0}),n);var T={};A(T,{Poline:()=>f,hslToPoint:()=>m,pointToHSL:()=>g,positionFunctions:()=>q,randomHSLPair:()=>C,randomHSLTriple:()=>L});var g=(n,t)=>{let[o,i,s]=n,c=.5,h=.5,e=Math.atan2(i-h,o-c)*(180/Math.PI);e=(360+e)%360;let a=s,p=Math.sqrt(Math.pow(i-h,2)+Math.pow(o-c,2))/c;return[e,a,t?1-p:p]},m=(n,t)=>{let[o,i,s]=n,c=.5,h=.5,r=o/(180/Math.PI),e=(t?1-s:s)*c,a=c+e*Math.cos(r),P=h+e*Math.sin(r);return[a,P,i]},C=(n=Math.random()*360,t=[Math.random(),Math.random()],o=[.75+Math.random()*.2,.3+Math.random()*.2])=>[[n,t[0],o[0]],[(n+60+Math.random()*180)%360,t[1],o[1]]],L=(n=Math.random()*360,t=[Math.random(),Math.random(),Math.random()],o=[.75+Math.random()*.2,Math.random()*.2,.75+Math.random()*.2])=>[[n,t[0],o[0]],[(n+60+Math.random()*180)%360,t[1],o[1]],[(n+60+Math.random()*180)%360,t[2],o[2]]],S=(n,t,o,i=!1,s=(r,e)=>e?1-r:r,c=(r,e)=>e?1-r:r,h=(r,e)=>e?1-r:r)=>{let r=s(n,i),e=c(n,i),a=h(n,i),P=(1-r)*t[0]+r*o[0],p=(1-e)*t[1]+e*o[1],M=(1-a)*t[2]+a*o[2];return[P,p,M]},X=(n,t,o=4,i=!1,s=(r,e)=>e?1-r:r,c=(r,e)=>e?1-r:r,h=(r,e)=>e?1-r:r)=>{let r=[];for(let e=0;en,Z=(n,t=!1)=>t?1-l(1-n,2):l(n,2),z=(n,t=!1)=>t?1-l(1-n,3):l(n,3),E=(n,t=!1)=>t?1-l(1-n,4):l(n,4),$=(n,t=!1)=>t?1-l(1-n,5):l(n,5),u=(n,t=!1)=>t?1-Math.sin((1-n)*Math.PI/2):Math.sin(n*Math.PI/2),k=(n,t=!1)=>t?1-Math.asin(1-n)/(Math.PI/2):Math.asin(n)/(Math.PI/2),I=(n,t=!1)=>t?1-Math.sqrt(1-l(n,2)):1-Math.sqrt(1-n),O=n=>l(n,2)*(3-2*n),q={linearPosition:Y,exponentialPosition:Z,quadraticPosition:z,cubicPosition:E,quarticPosition:$,sinusoidalPosition:u,asinusoidalPosition:k,arcPosition:I,smoothStepPosition:O},_=(n,t,o=!1)=>{let i=n[0],s=t[0],c=0;o&&i!==null&&s!==null?(c=Math.min(Math.abs(i-s),360-Math.abs(i-s)),c=c/360):c=i===null||s===null?0:i-s;let h=c,r=n[1]===null||t[1]===null?0:t[1]-n[1],e=n[2]===null||t[2]===null?0:t[2]-n[2];return Math.sqrt(h*h+r*r+e*e)},b=class{constructor({xyz:t,color:o,invertedLightness:i=!1}={}){this.x=0;this.y=0;this.z=0;this.color=[0,0,0];this._invertedLightness=!1;this._invertedLightness=i,this.positionOrColor({xyz:t,color:o,invertedLightness:i})}positionOrColor({xyz:t,color:o,invertedLightness:i=!1}){if(t&&o||!t&&!o)throw new Error("Point must be initialized with either x,y,z or hsl");t?(this.x=t[0],this.y=t[1],this.z=t[2],this.color=g([this.x,this.y,this.z],i)):o&&(this.color=o,[this.x,this.y,this.z]=m(o,i))}set position([t,o,i]){this.x=t,this.y=o,this.z=i,this.color=g([this.x,this.y,this.z],this._invertedLightness)}get position(){return[this.x,this.y,this.z]}set hsl([t,o,i]){this.color=[t,o,i],[this.x,this.y,this.z]=m(this.color,this._invertedLightness)}get hsl(){return this.color}get hslCSS(){let[t,o,i]=this.color;return`hsl(${t.toFixed(2)}, ${(o*100).toFixed(2)}%, ${(i*100).toFixed(2)}%)`}get oklchCSS(){let[t,o,i]=this.color;return`oklch(${(i*100).toFixed(2)}% ${(o*.4).toFixed(3)} ${t.toFixed(2)})`}get lchCSS(){let[t,o,i]=this.color;return`lch(${(i*100).toFixed(2)}% ${(o*150).toFixed(2)} ${t.toFixed(2)})`}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=m(this.color,this._invertedLightness)}},f=class{constructor({anchorColors:t=C(),numPoints:o=4,positionFunction:i=u,positionFunctionX:s,positionFunctionY:c,positionFunctionZ:h,closedLoop:r,invertedLightness:e}={anchorColors:C(),numPoints:4,positionFunction:u,closedLoop:!1}){this._needsUpdate=!0;this._positionFunctionX=u;this._positionFunctionY=u;this._positionFunctionZ=u;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(a=>new b({color:a,invertedLightness:e})),this._numPoints=o+2,this._positionFunctionX=s||i||u,this._positionFunctionY=c||i||u,this._positionFunctionZ=h||i||u,this.connectLastAndFirstAnchor=r||!1,this._invertedLightness=e||!1,this.updateAnchorPairs()}get numPoints(){return this._numPoints-2}set numPoints(t){if(t<1)throw new Error("Must have at least one point");this._numPoints=t+2,this.updateAnchorPairs()}set positionFunction(t){if(Array.isArray(t)){if(t.length!==3)throw new Error("Position function array must have 3 elements");if(typeof t[0]!="function"||typeof t[1]!="function"||typeof t[2]!="function")throw new Error("Position function array must have 3 functions");this._positionFunctionX=t[0],this._positionFunctionY=t[1],this._positionFunctionZ=t[2]}else this._positionFunctionX=t,this._positionFunctionY=t,this._positionFunctionZ=t;this.updateAnchorPairs()}get positionFunction(){return this._positionFunctionX===this._positionFunctionY&&this._positionFunctionX===this._positionFunctionZ?this._positionFunctionX:[this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ]}set positionFunctionX(t){this._positionFunctionX=t,this.updateAnchorPairs()}get positionFunctionX(){return this._positionFunctionX}set positionFunctionY(t){this._positionFunctionY=t,this.updateAnchorPairs()}get positionFunctionY(){return this._positionFunctionY}set positionFunctionZ(t){this._positionFunctionZ=t,this.updateAnchorPairs()}get positionFunctionZ(){return this._positionFunctionZ}get anchorPoints(){return this._anchorPoints}set anchorPoints(t){this._anchorPoints=t,this.updateAnchorPairs()}updateAnchorPairs(){this._anchorPairs=[];let t=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1;for(let o=0;o{let s=o[0]?o[0].position:[0,0,0],c=o[1]?o[1].position:[0,0,0];return X(s,c,this._numPoints,!!(i%2),this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(h=>new b({xyz:h,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:o,insertAtIndex:i}){let s=new b({xyz:t,color:o,invertedLightness:this._invertedLightness});return i?this.anchorPoints.splice(i,0,s):this.anchorPoints.push(s),this.updateAnchorPairs(),s}removeAnchorPoint({point:t,index:o}){if(!t&&o===void 0)throw new Error("Must provide a point or index");if(this.anchorPoints.length<3)throw new Error("Must have at least two anchor points");let i;if(o!==void 0?i=o:t&&(i=this.anchorPoints.indexOf(t)),i>-1&&i_(r.position,t)):o&&(s=this.anchorPoints.map(r=>_(r.hsl,o,!0)));let c=Math.min(...s);if(c>i)return null;let h=s.indexOf(c);return this.anchorPoints[h]||null}set closedLoop(t){this.connectLastAndFirstAnchor=t,this.updateAnchorPairs()}get closedLoop(){return this.connectLastAndFirstAnchor}set invertedLightness(t){this._invertedLightness=t,this.updateAnchorPairs()}get invertedLightness(){return this._invertedLightness}get flattenedPoints(){return this.points.flat().filter((t,o)=>o!=0?o%this._numPoints:!0)}get colors(){let t=this.flattenedPoints.map(o=>o.color);return this.connectLastAndFirstAnchor&&t.pop(),t}cssColors(t="hsl"){let o={hsl:s=>s.hslCSS,oklch:s=>s.oklchCSS,lch:s=>s.lchCSS},i=this.flattenedPoints.map(o[t]);return this.connectLastAndFirstAnchor&&i.pop(),i}get colorsCSS(){return this.cssColors("hsl")}get colorsCSSlch(){return this.cssColors("lch")}get colorsCSSoklch(){return this.cssColors("oklch")}shiftHue(t=20){this.anchorPoints.forEach(o=>o.shiftHue(t)),this.updateAnchorPairs()}},{p5:d}=globalThis;if(d){console.info("p5 detected, adding poline to p5 prototype");let n=new f;d.prototype.poline=n;let t=()=>n.colors.map(o=>`hsl(${Math.round(o[0])},${o[1]*100}%,${o[2]*100}%)`);d.prototype.polineColors=t,d.prototype.registerMethod("polineColors",d.prototype.polineColors),globalThis.poline=n,globalThis.polineColors=t}return y(T);})(); 2 | -------------------------------------------------------------------------------- /dist/index.min.mjs: -------------------------------------------------------------------------------- 1 | var f=(i,t)=>{let[o,n,s]=i,c=.5,h=.5,e=Math.atan2(n-h,o-c)*(180/Math.PI);e=(360+e)%360;let l=s,u=Math.sqrt(Math.pow(n-h,2)+Math.pow(o-c,2))/c;return[e,l,t?1-u:u]},b=(i,t)=>{let[o,n,s]=i,c=.5,h=.5,r=o/(180/Math.PI),e=(t?1-s:s)*c,l=c+e*Math.cos(r),p=h+e*Math.sin(r);return[l,p,n]},F=(i=Math.random()*360,t=[Math.random(),Math.random()],o=[.75+Math.random()*.2,.3+Math.random()*.2])=>[[i,t[0],o[0]],[(i+60+Math.random()*180)%360,t[1],o[1]]],X=(i=Math.random()*360,t=[Math.random(),Math.random(),Math.random()],o=[.75+Math.random()*.2,Math.random()*.2,.75+Math.random()*.2])=>[[i,t[0],o[0]],[(i+60+Math.random()*180)%360,t[1],o[1]],[(i+60+Math.random()*180)%360,t[2],o[2]]],_=(i,t,o,n=!1,s=(r,e)=>e?1-r:r,c=(r,e)=>e?1-r:r,h=(r,e)=>e?1-r:r)=>{let r=s(i,n),e=c(i,n),l=h(i,n),p=(1-r)*t[0]+r*o[0],u=(1-e)*t[1]+e*o[1],C=(1-l)*t[2]+l*o[2];return[p,u,C]},M=(i,t,o=4,n=!1,s=(r,e)=>e?1-r:r,c=(r,e)=>e?1-r:r,h=(r,e)=>e?1-r:r)=>{let r=[];for(let e=0;ei,V=(i,t=!1)=>t?1-(1-i)**2:i**2,x=(i,t=!1)=>t?1-(1-i)**3:i**3,A=(i,t=!1)=>t?1-(1-i)**4:i**4,w=(i,t=!1)=>t?1-(1-i)**5:i**5,a=(i,t=!1)=>t?1-Math.sin((1-i)*Math.PI/2):Math.sin(i*Math.PI/2),y=(i,t=!1)=>t?1-Math.asin(1-i)/(Math.PI/2):Math.asin(i)/(Math.PI/2),L=(i,t=!1)=>t?1-Math.sqrt(1-i**2):1-Math.sqrt(1-i),S=i=>i**2*(3-2*i),Y={linearPosition:v,exponentialPosition:V,quadraticPosition:x,cubicPosition:A,quarticPosition:w,sinusoidalPosition:a,asinusoidalPosition:y,arcPosition:L,smoothStepPosition:S},g=(i,t,o=!1)=>{let n=i[0],s=t[0],c=0;o&&n!==null&&s!==null?(c=Math.min(Math.abs(n-s),360-Math.abs(n-s)),c=c/360):c=n===null||s===null?0:n-s;let h=c,r=i[1]===null||t[1]===null?0:t[1]-i[1],e=i[2]===null||t[2]===null?0:t[2]-i[2];return Math.sqrt(h*h+r*r+e*e)},d=class{constructor({xyz:t,color:o,invertedLightness:n=!1}={}){this.x=0;this.y=0;this.z=0;this.color=[0,0,0];this._invertedLightness=!1;this._invertedLightness=n,this.positionOrColor({xyz:t,color:o,invertedLightness:n})}positionOrColor({xyz:t,color:o,invertedLightness:n=!1}){if(t&&o||!t&&!o)throw new Error("Point must be initialized with either x,y,z or hsl");t?(this.x=t[0],this.y=t[1],this.z=t[2],this.color=f([this.x,this.y,this.z],n)):o&&(this.color=o,[this.x,this.y,this.z]=b(o,n))}set position([t,o,n]){this.x=t,this.y=o,this.z=n,this.color=f([this.x,this.y,this.z],this._invertedLightness)}get position(){return[this.x,this.y,this.z]}set hsl([t,o,n]){this.color=[t,o,n],[this.x,this.y,this.z]=b(this.color,this._invertedLightness)}get hsl(){return this.color}get hslCSS(){let[t,o,n]=this.color;return`hsl(${t.toFixed(2)}, ${(o*100).toFixed(2)}%, ${(n*100).toFixed(2)}%)`}get oklchCSS(){let[t,o,n]=this.color;return`oklch(${(n*100).toFixed(2)}% ${(o*.4).toFixed(3)} ${t.toFixed(2)})`}get lchCSS(){let[t,o,n]=this.color;return`lch(${(n*100).toFixed(2)}% ${(o*150).toFixed(2)} ${t.toFixed(2)})`}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=b(this.color,this._invertedLightness)}},m=class{constructor({anchorColors:t=F(),numPoints:o=4,positionFunction:n=a,positionFunctionX:s,positionFunctionY:c,positionFunctionZ:h,closedLoop:r,invertedLightness:e}={anchorColors:F(),numPoints:4,positionFunction:a,closedLoop:!1}){this._needsUpdate=!0;this._positionFunctionX=a;this._positionFunctionY=a;this._positionFunctionZ=a;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(l=>new d({color:l,invertedLightness:e})),this._numPoints=o+2,this._positionFunctionX=s||n||a,this._positionFunctionY=c||n||a,this._positionFunctionZ=h||n||a,this.connectLastAndFirstAnchor=r||!1,this._invertedLightness=e||!1,this.updateAnchorPairs()}get numPoints(){return this._numPoints-2}set numPoints(t){if(t<1)throw new Error("Must have at least one point");this._numPoints=t+2,this.updateAnchorPairs()}set positionFunction(t){if(Array.isArray(t)){if(t.length!==3)throw new Error("Position function array must have 3 elements");if(typeof t[0]!="function"||typeof t[1]!="function"||typeof t[2]!="function")throw new Error("Position function array must have 3 functions");this._positionFunctionX=t[0],this._positionFunctionY=t[1],this._positionFunctionZ=t[2]}else this._positionFunctionX=t,this._positionFunctionY=t,this._positionFunctionZ=t;this.updateAnchorPairs()}get positionFunction(){return this._positionFunctionX===this._positionFunctionY&&this._positionFunctionX===this._positionFunctionZ?this._positionFunctionX:[this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ]}set positionFunctionX(t){this._positionFunctionX=t,this.updateAnchorPairs()}get positionFunctionX(){return this._positionFunctionX}set positionFunctionY(t){this._positionFunctionY=t,this.updateAnchorPairs()}get positionFunctionY(){return this._positionFunctionY}set positionFunctionZ(t){this._positionFunctionZ=t,this.updateAnchorPairs()}get positionFunctionZ(){return this._positionFunctionZ}get anchorPoints(){return this._anchorPoints}set anchorPoints(t){this._anchorPoints=t,this.updateAnchorPairs()}updateAnchorPairs(){this._anchorPairs=[];let t=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1;for(let o=0;o{let s=o[0]?o[0].position:[0,0,0],c=o[1]?o[1].position:[0,0,0];return M(s,c,this._numPoints,!!(n%2),this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(h=>new d({xyz:h,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:o,insertAtIndex:n}){let s=new d({xyz:t,color:o,invertedLightness:this._invertedLightness});return n?this.anchorPoints.splice(n,0,s):this.anchorPoints.push(s),this.updateAnchorPairs(),s}removeAnchorPoint({point:t,index:o}){if(!t&&o===void 0)throw new Error("Must provide a point or index");if(this.anchorPoints.length<3)throw new Error("Must have at least two anchor points");let n;if(o!==void 0?n=o:t&&(n=this.anchorPoints.indexOf(t)),n>-1&&ng(r.position,t)):o&&(s=this.anchorPoints.map(r=>g(r.hsl,o,!0)));let c=Math.min(...s);if(c>n)return null;let h=s.indexOf(c);return this.anchorPoints[h]||null}set closedLoop(t){this.connectLastAndFirstAnchor=t,this.updateAnchorPairs()}get closedLoop(){return this.connectLastAndFirstAnchor}set invertedLightness(t){this._invertedLightness=t,this.updateAnchorPairs()}get invertedLightness(){return this._invertedLightness}get flattenedPoints(){return this.points.flat().filter((t,o)=>o!=0?o%this._numPoints:!0)}get colors(){let t=this.flattenedPoints.map(o=>o.color);return this.connectLastAndFirstAnchor&&t.pop(),t}cssColors(t="hsl"){let o={hsl:s=>s.hslCSS,oklch:s=>s.oklchCSS,lch:s=>s.lchCSS},n=this.flattenedPoints.map(o[t]);return this.connectLastAndFirstAnchor&&n.pop(),n}get colorsCSS(){return this.cssColors("hsl")}get colorsCSSlch(){return this.cssColors("lch")}get colorsCSSoklch(){return this.cssColors("oklch")}shiftHue(t=20){this.anchorPoints.forEach(o=>o.shiftHue(t)),this.updateAnchorPairs()}},{p5:P}=globalThis;if(P){console.info("p5 detected, adding poline to p5 prototype");let i=new m;P.prototype.poline=i;let t=()=>i.colors.map(o=>`hsl(${Math.round(o[0])},${o[1]*100}%,${o[2]*100}%)`);P.prototype.polineColors=t,P.prototype.registerMethod("polineColors",P.prototype.polineColors),globalThis.poline=i,globalThis.polineColors=t}export{m as Poline,b as hslToPoint,f as pointToHSL,Y as positionFunctions,F as randomHSLPair,X as randomHSLTriple}; 2 | -------------------------------------------------------------------------------- /dist/index.mjs: -------------------------------------------------------------------------------- 1 | // src/index.ts 2 | var pointToHSL = (xyz, invertedLightness) => { 3 | const [x, y, z] = xyz; 4 | const cx = 0.5; 5 | const cy = 0.5; 6 | const radians = Math.atan2(y - cy, x - cx); 7 | let deg = radians * (180 / Math.PI); 8 | deg = (360 + deg) % 360; 9 | const s = z; 10 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 11 | const l = dist / cx; 12 | return [deg, s, invertedLightness ? 1 - l : l]; 13 | }; 14 | var hslToPoint = (hsl, invertedLightness) => { 15 | const [h, s, l] = hsl; 16 | const cx = 0.5; 17 | const cy = 0.5; 18 | const radians = h / (180 / Math.PI); 19 | const dist = (invertedLightness ? 1 - l : l) * cx; 20 | const x = cx + dist * Math.cos(radians); 21 | const y = cy + dist * Math.sin(radians); 22 | const z = s; 23 | return [x, y, z]; 24 | }; 25 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 26 | [startHue, saturations[0], lightnesses[0]], 27 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 28 | ]; 29 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 30 | 0.75 + Math.random() * 0.2, 31 | Math.random() * 0.2, 32 | 0.75 + Math.random() * 0.2 33 | ]) => [ 34 | [startHue, saturations[0], lightnesses[0]], 35 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 36 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 37 | ]; 38 | var vectorOnLine = (t, p1, p2, invert = false, fx = (t2, invert2) => invert2 ? 1 - t2 : t2, fy = (t2, invert2) => invert2 ? 1 - t2 : t2, fz = (t2, invert2) => invert2 ? 1 - t2 : t2) => { 39 | const tModifiedX = fx(t, invert); 40 | const tModifiedY = fy(t, invert); 41 | const tModifiedZ = fz(t, invert); 42 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 43 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 44 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 45 | return [x, y, z]; 46 | }; 47 | var vectorsOnLine = (p1, p2, numPoints = 4, invert = false, fx = (t, invert2) => invert2 ? 1 - t : t, fy = (t, invert2) => invert2 ? 1 - t : t, fz = (t, invert2) => invert2 ? 1 - t : t) => { 48 | const points = []; 49 | for (let i = 0; i < numPoints; i++) { 50 | const [x, y, z] = vectorOnLine( 51 | i / (numPoints - 1), 52 | p1, 53 | p2, 54 | invert, 55 | fx, 56 | fy, 57 | fz 58 | ); 59 | points.push([x, y, z]); 60 | } 61 | return points; 62 | }; 63 | var linearPosition = (t) => { 64 | return t; 65 | }; 66 | var exponentialPosition = (t, reverse = false) => { 67 | if (reverse) { 68 | return 1 - (1 - t) ** 2; 69 | } 70 | return t ** 2; 71 | }; 72 | var quadraticPosition = (t, reverse = false) => { 73 | if (reverse) { 74 | return 1 - (1 - t) ** 3; 75 | } 76 | return t ** 3; 77 | }; 78 | var cubicPosition = (t, reverse = false) => { 79 | if (reverse) { 80 | return 1 - (1 - t) ** 4; 81 | } 82 | return t ** 4; 83 | }; 84 | var quarticPosition = (t, reverse = false) => { 85 | if (reverse) { 86 | return 1 - (1 - t) ** 5; 87 | } 88 | return t ** 5; 89 | }; 90 | var sinusoidalPosition = (t, reverse = false) => { 91 | if (reverse) { 92 | return 1 - Math.sin((1 - t) * Math.PI / 2); 93 | } 94 | return Math.sin(t * Math.PI / 2); 95 | }; 96 | var asinusoidalPosition = (t, reverse = false) => { 97 | if (reverse) { 98 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 99 | } 100 | return Math.asin(t) / (Math.PI / 2); 101 | }; 102 | var arcPosition = (t, reverse = false) => { 103 | if (reverse) { 104 | return 1 - Math.sqrt(1 - t ** 2); 105 | } 106 | return 1 - Math.sqrt(1 - t); 107 | }; 108 | var smoothStepPosition = (t) => { 109 | return t ** 2 * (3 - 2 * t); 110 | }; 111 | var positionFunctions = { 112 | linearPosition, 113 | exponentialPosition, 114 | quadraticPosition, 115 | cubicPosition, 116 | quarticPosition, 117 | sinusoidalPosition, 118 | asinusoidalPosition, 119 | arcPosition, 120 | smoothStepPosition 121 | }; 122 | var distance = (p1, p2, hueMode = false) => { 123 | const a1 = p1[0]; 124 | const a2 = p2[0]; 125 | let diffA = 0; 126 | if (hueMode && a1 !== null && a2 !== null) { 127 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 128 | diffA = diffA / 360; 129 | } else { 130 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 131 | } 132 | const a = diffA; 133 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 134 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 135 | return Math.sqrt(a * a + b * b + c * c); 136 | }; 137 | var ColorPoint = class { 138 | constructor({ 139 | xyz, 140 | color, 141 | invertedLightness = false 142 | } = {}) { 143 | this.x = 0; 144 | this.y = 0; 145 | this.z = 0; 146 | this.color = [0, 0, 0]; 147 | this._invertedLightness = false; 148 | this._invertedLightness = invertedLightness; 149 | this.positionOrColor({ xyz, color, invertedLightness }); 150 | } 151 | positionOrColor({ 152 | xyz, 153 | color, 154 | invertedLightness = false 155 | }) { 156 | if (xyz && color || !xyz && !color) { 157 | throw new Error("Point must be initialized with either x,y,z or hsl"); 158 | } else if (xyz) { 159 | this.x = xyz[0]; 160 | this.y = xyz[1]; 161 | this.z = xyz[2]; 162 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 163 | } else if (color) { 164 | this.color = color; 165 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 166 | } 167 | } 168 | set position([x, y, z]) { 169 | this.x = x; 170 | this.y = y; 171 | this.z = z; 172 | this.color = pointToHSL( 173 | [this.x, this.y, this.z], 174 | this._invertedLightness 175 | ); 176 | } 177 | get position() { 178 | return [this.x, this.y, this.z]; 179 | } 180 | set hsl([h, s, l]) { 181 | this.color = [h, s, l]; 182 | [this.x, this.y, this.z] = hslToPoint( 183 | this.color, 184 | this._invertedLightness 185 | ); 186 | } 187 | get hsl() { 188 | return this.color; 189 | } 190 | get hslCSS() { 191 | const [h, s, l] = this.color; 192 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 193 | 2 194 | )}%)`; 195 | } 196 | get oklchCSS() { 197 | const [h, s, l] = this.color; 198 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 199 | 2 200 | )})`; 201 | } 202 | get lchCSS() { 203 | const [h, s, l] = this.color; 204 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 205 | 2 206 | )})`; 207 | } 208 | shiftHue(angle) { 209 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 210 | [this.x, this.y, this.z] = hslToPoint( 211 | this.color, 212 | this._invertedLightness 213 | ); 214 | } 215 | }; 216 | var Poline = class { 217 | constructor({ 218 | anchorColors = randomHSLPair(), 219 | numPoints = 4, 220 | positionFunction = sinusoidalPosition, 221 | positionFunctionX, 222 | positionFunctionY, 223 | positionFunctionZ, 224 | closedLoop, 225 | invertedLightness 226 | } = { 227 | anchorColors: randomHSLPair(), 228 | numPoints: 4, 229 | positionFunction: sinusoidalPosition, 230 | closedLoop: false 231 | }) { 232 | this._needsUpdate = true; 233 | this._positionFunctionX = sinusoidalPosition; 234 | this._positionFunctionY = sinusoidalPosition; 235 | this._positionFunctionZ = sinusoidalPosition; 236 | this.connectLastAndFirstAnchor = false; 237 | this._animationFrame = null; 238 | this._invertedLightness = false; 239 | if (!anchorColors || anchorColors.length < 2) { 240 | throw new Error("Must have at least two anchor colors"); 241 | } 242 | this._anchorPoints = anchorColors.map( 243 | (point) => new ColorPoint({ color: point, invertedLightness }) 244 | ); 245 | this._numPoints = numPoints + 2; 246 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 247 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 248 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 249 | this.connectLastAndFirstAnchor = closedLoop || false; 250 | this._invertedLightness = invertedLightness || false; 251 | this.updateAnchorPairs(); 252 | } 253 | get numPoints() { 254 | return this._numPoints - 2; 255 | } 256 | set numPoints(numPoints) { 257 | if (numPoints < 1) { 258 | throw new Error("Must have at least one point"); 259 | } 260 | this._numPoints = numPoints + 2; 261 | this.updateAnchorPairs(); 262 | } 263 | set positionFunction(positionFunction) { 264 | if (Array.isArray(positionFunction)) { 265 | if (positionFunction.length !== 3) { 266 | throw new Error("Position function array must have 3 elements"); 267 | } 268 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 269 | throw new Error("Position function array must have 3 functions"); 270 | } 271 | this._positionFunctionX = positionFunction[0]; 272 | this._positionFunctionY = positionFunction[1]; 273 | this._positionFunctionZ = positionFunction[2]; 274 | } else { 275 | this._positionFunctionX = positionFunction; 276 | this._positionFunctionY = positionFunction; 277 | this._positionFunctionZ = positionFunction; 278 | } 279 | this.updateAnchorPairs(); 280 | } 281 | get positionFunction() { 282 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 283 | return this._positionFunctionX; 284 | } 285 | return [ 286 | this._positionFunctionX, 287 | this._positionFunctionY, 288 | this._positionFunctionZ 289 | ]; 290 | } 291 | set positionFunctionX(positionFunctionX) { 292 | this._positionFunctionX = positionFunctionX; 293 | this.updateAnchorPairs(); 294 | } 295 | get positionFunctionX() { 296 | return this._positionFunctionX; 297 | } 298 | set positionFunctionY(positionFunctionY) { 299 | this._positionFunctionY = positionFunctionY; 300 | this.updateAnchorPairs(); 301 | } 302 | get positionFunctionY() { 303 | return this._positionFunctionY; 304 | } 305 | set positionFunctionZ(positionFunctionZ) { 306 | this._positionFunctionZ = positionFunctionZ; 307 | this.updateAnchorPairs(); 308 | } 309 | get positionFunctionZ() { 310 | return this._positionFunctionZ; 311 | } 312 | get anchorPoints() { 313 | return this._anchorPoints; 314 | } 315 | set anchorPoints(anchorPoints) { 316 | this._anchorPoints = anchorPoints; 317 | this.updateAnchorPairs(); 318 | } 319 | updateAnchorPairs() { 320 | this._anchorPairs = []; 321 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 322 | for (let i = 0; i < anchorPointsLength; i++) { 323 | const pair = [ 324 | this.anchorPoints[i], 325 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 326 | ]; 327 | this._anchorPairs.push(pair); 328 | } 329 | this.points = this._anchorPairs.map((pair, i) => { 330 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 331 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 332 | return vectorsOnLine( 333 | p1position, 334 | p2position, 335 | this._numPoints, 336 | i % 2 ? true : false, 337 | this.positionFunctionX, 338 | this.positionFunctionY, 339 | this.positionFunctionZ 340 | ).map( 341 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 342 | ); 343 | }); 344 | } 345 | addAnchorPoint({ 346 | xyz, 347 | color, 348 | insertAtIndex 349 | }) { 350 | const newAnchor = new ColorPoint({ 351 | xyz, 352 | color, 353 | invertedLightness: this._invertedLightness 354 | }); 355 | if (insertAtIndex) { 356 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 357 | } else { 358 | this.anchorPoints.push(newAnchor); 359 | } 360 | this.updateAnchorPairs(); 361 | return newAnchor; 362 | } 363 | removeAnchorPoint({ 364 | point, 365 | index 366 | }) { 367 | if (!point && index === void 0) { 368 | throw new Error("Must provide a point or index"); 369 | } 370 | if (this.anchorPoints.length < 3) { 371 | throw new Error("Must have at least two anchor points"); 372 | } 373 | let apid; 374 | if (index !== void 0) { 375 | apid = index; 376 | } else if (point) { 377 | apid = this.anchorPoints.indexOf(point); 378 | } 379 | if (apid > -1 && apid < this.anchorPoints.length) { 380 | this.anchorPoints.splice(apid, 1); 381 | this.updateAnchorPairs(); 382 | } else { 383 | throw new Error("Point not found"); 384 | } 385 | } 386 | updateAnchorPoint({ 387 | point, 388 | pointIndex, 389 | xyz, 390 | color 391 | }) { 392 | if (pointIndex) { 393 | point = this.anchorPoints[pointIndex]; 394 | } 395 | if (!point) { 396 | throw new Error("Must provide a point or pointIndex"); 397 | } 398 | if (!xyz && !color) { 399 | throw new Error("Must provide a new xyz position or color"); 400 | } 401 | if (xyz) 402 | point.position = xyz; 403 | if (color) 404 | point.hsl = color; 405 | this.updateAnchorPairs(); 406 | return point; 407 | } 408 | getClosestAnchorPoint({ 409 | xyz, 410 | hsl, 411 | maxDistance = 1 412 | }) { 413 | if (!xyz && !hsl) { 414 | throw new Error("Must provide a xyz or hsl"); 415 | } 416 | let distances; 417 | if (xyz) { 418 | distances = this.anchorPoints.map( 419 | (anchor) => distance(anchor.position, xyz) 420 | ); 421 | } else if (hsl) { 422 | distances = this.anchorPoints.map( 423 | (anchor) => distance(anchor.hsl, hsl, true) 424 | ); 425 | } 426 | const minDistance = Math.min(...distances); 427 | if (minDistance > maxDistance) { 428 | return null; 429 | } 430 | const closestAnchorIndex = distances.indexOf(minDistance); 431 | return this.anchorPoints[closestAnchorIndex] || null; 432 | } 433 | set closedLoop(newStatus) { 434 | this.connectLastAndFirstAnchor = newStatus; 435 | this.updateAnchorPairs(); 436 | } 437 | get closedLoop() { 438 | return this.connectLastAndFirstAnchor; 439 | } 440 | set invertedLightness(newStatus) { 441 | this._invertedLightness = newStatus; 442 | this.updateAnchorPairs(); 443 | } 444 | get invertedLightness() { 445 | return this._invertedLightness; 446 | } 447 | /** 448 | * Returns a flattened array of all points across all segments, 449 | * removing duplicated anchor points at segment boundaries. 450 | * 451 | * Since anchor points exist at both the end of one segment and 452 | * the beginning of the next, this method keeps only one instance of each. 453 | * The filter logic keeps the first point (index 0) and then filters out 454 | * points whose indices are multiples of the segment size (_numPoints), 455 | * which are the anchor points at the start of each segment (except the first). 456 | * 457 | * This approach ensures we get all unique points in the correct order 458 | * while avoiding duplicated anchor points. 459 | * 460 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 461 | */ 462 | get flattenedPoints() { 463 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 464 | } 465 | get colors() { 466 | const colors = this.flattenedPoints.map((p) => p.color); 467 | if (this.connectLastAndFirstAnchor) { 468 | colors.pop(); 469 | } 470 | return colors; 471 | } 472 | cssColors(mode = "hsl") { 473 | const methods = { 474 | hsl: (p) => p.hslCSS, 475 | oklch: (p) => p.oklchCSS, 476 | lch: (p) => p.lchCSS 477 | }; 478 | const cssColors = this.flattenedPoints.map(methods[mode]); 479 | if (this.connectLastAndFirstAnchor) { 480 | cssColors.pop(); 481 | } 482 | return cssColors; 483 | } 484 | get colorsCSS() { 485 | return this.cssColors("hsl"); 486 | } 487 | get colorsCSSlch() { 488 | return this.cssColors("lch"); 489 | } 490 | get colorsCSSoklch() { 491 | return this.cssColors("oklch"); 492 | } 493 | shiftHue(hShift = 20) { 494 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 495 | this.updateAnchorPairs(); 496 | } 497 | }; 498 | var { p5 } = globalThis; 499 | if (p5) { 500 | console.info("p5 detected, adding poline to p5 prototype"); 501 | const poline = new Poline(); 502 | p5.prototype.poline = poline; 503 | const polineColors = () => poline.colors.map( 504 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 505 | ); 506 | p5.prototype.polineColors = polineColors; 507 | p5.prototype.registerMethod("polineColors", p5.prototype.polineColors); 508 | globalThis.poline = poline; 509 | globalThis.polineColors = polineColors; 510 | } 511 | export { 512 | Poline, 513 | hslToPoint, 514 | pointToHSL, 515 | positionFunctions, 516 | randomHSLPair, 517 | randomHSLTriple 518 | }; 519 | -------------------------------------------------------------------------------- /dist/index.mjs.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "sources": ["../src/index.ts"], 4 | "sourcesContent": ["/* eslint-disable @typescript-eslint/ban-ts-comment */\nexport type FuncNumberReturn = (arg0: number) => Vector2;\nexport type Vector2 = [number, number];\nexport type Vector3 = [number, ...Vector2];\nexport type PartialVector3 = [number | null, number | null, number | null];\n\ntype CSSColorMethods = {\n hsl: (p: ColorPoint) => string;\n oklch: (p: ColorPoint) => string;\n lch: (p: ColorPoint) => string;\n};\n\n/**\n * Converts the given (x, y, z) coordinate to an HSL color\n * The (x, y) values are used to calculate the hue, while the z value is used as the saturation\n * The lightness value is calculated based on the distance of (x, y) from the center (0.5, 0.5)\n * Returns an array [hue, saturation, lightness]\n * @param xyz:Vector3 [x, y, z] coordinate array in (x, y, z) format (0-1, 0-1, 0-1)\n * @returns [hue, saturation, lightness]: Vector3 color array in HSL format (0-360, 0-1, 0-1)\n * @example\n * pointToHSL([0.5, 0.5, 1]) // [0, 1, 0.5]\n * pointToHSL([0.5, 0.5, 0]) // [0, 1, 0]\n **/\n\nexport const pointToHSL = (\n xyz: Vector3,\n invertedLightness: boolean\n): Vector3 => {\n const [x, y, z] = xyz;\n\n // cy and cx are the center (y and x) values\n const cx = 0.5;\n const cy = 0.5;\n\n // Calculate the angle between the point (x, y) and the center (cx, cy)\n const radians = Math.atan2(y - cy, x - cx);\n\n // Convert the angle to degrees and shift it so that it goes from 0 to 360\n let deg = radians * (180 / Math.PI);\n deg = (360 + deg) % 360;\n\n // The saturation value is taken from the z coordinate\n const s = z;\n\n // Calculate the lightness value based on the distance from the center\n const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2));\n const l = dist / cx;\n\n // Return the HSL color as an array [hue, saturation, lightness]\n return [deg, s, invertedLightness ? 1 - l : l];\n};\n\n/**\n * Converts the given HSL color to an (x, y, z) coordinate\n * The hue value is used to calculate the (x, y) position, while the saturation value is used as the z coordinate\n * The lightness value is used to calculate the distance from the center (0.5, 0.5)\n * Returns an array [x, y, z]\n * @param hsl:Vector3 [hue, saturation, lightness] color array in HSL format (0-360, 0-1, 0-1)\n * @returns [x, y, z]:Vector3 coordinate array in (x, y, z) format (0-1, 0-1, 0-1)\n * @example\n * hslToPoint([0, 1, 0.5]) // [0.5, 0.5, 1]\n * hslToPoint([0, 1, 0]) // [0.5, 0.5, 1]\n * hslToPoint([0, 1, 1]) // [0.5, 0.5, 1]\n * hslToPoint([0, 0, 0.5]) // [0.5, 0.5, 0]\n **/\nexport const hslToPoint = (\n hsl: Vector3,\n invertedLightness: boolean\n): Vector3 => {\n // Destructure the input array into separate hue, saturation, and lightness values\n const [h, s, l] = hsl;\n // cx and cy are the center (x and y) values\n const cx = 0.5;\n const cy = 0.5;\n // Calculate the angle in radians based on the hue value\n const radians = h / (180 / Math.PI);\n\n // Calculate the distance from the center based on the lightness value\n const dist = (invertedLightness ? 1 - l : l) * cx;\n\n // Calculate the x and y coordinates based on the distance and angle\n const x = cx + dist * Math.cos(radians);\n const y = cy + dist * Math.sin(radians);\n // The z coordinate is equal to the saturation value\n const z = s;\n // Return the (x, y, z) coordinate as an array [x, y, z]\n return [x, y, z];\n};\n\nexport const randomHSLPair = (\n startHue: number = Math.random() * 360,\n saturations: Vector2 = [Math.random(), Math.random()],\n lightnesses: Vector2 = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]\n): [Vector3, Vector3] => [\n [startHue, saturations[0], lightnesses[0]],\n [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]],\n];\n\nexport const randomHSLTriple = (\n startHue: number = Math.random() * 360,\n saturations: Vector3 = [Math.random(), Math.random(), Math.random()],\n lightnesses: Vector3 = [\n 0.75 + Math.random() * 0.2,\n Math.random() * 0.2,\n 0.75 + Math.random() * 0.2,\n ]\n): [Vector3, Vector3, Vector3] => [\n [startHue, saturations[0], lightnesses[0]],\n [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]],\n [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]],\n];\n\nconst vectorOnLine = (\n t: number,\n p1: Vector3,\n p2: Vector3,\n invert = false,\n fx = (t: number, invert: boolean): number => (invert ? 1 - t : t),\n fy = (t: number, invert: boolean): number => (invert ? 1 - t : t),\n fz = (t: number, invert: boolean): number => (invert ? 1 - t : t)\n): Vector3 => {\n const tModifiedX = fx(t, invert);\n const tModifiedY = fy(t, invert);\n const tModifiedZ = fz(t, invert);\n const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0];\n const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1];\n const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2];\n\n return [x, y, z];\n};\n\nconst vectorsOnLine = (\n p1: Vector3,\n p2: Vector3,\n numPoints = 4,\n invert = false,\n fx = (t: number, invert: boolean): number => (invert ? 1 - t : t),\n fy = (t: number, invert: boolean): number => (invert ? 1 - t : t),\n fz = (t: number, invert: boolean): number => (invert ? 1 - t : t)\n): Vector3[] => {\n const points: Vector3[] = [];\n\n for (let i = 0; i < numPoints; i++) {\n const [x, y, z] = vectorOnLine(\n i / (numPoints - 1),\n p1,\n p2,\n invert,\n fx,\n fy,\n fz\n );\n points.push([x, y, z]);\n }\n\n return points;\n};\n\nexport type PositionFunction = (t: number, reverse?: boolean) => number;\n\nconst linearPosition: PositionFunction = (t: number) => {\n return t;\n};\n\nconst exponentialPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - (1 - t) ** 2;\n }\n return t ** 2;\n};\n\nconst quadraticPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - (1 - t) ** 3;\n }\n return t ** 3;\n};\n\nconst cubicPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - (1 - t) ** 4;\n }\n return t ** 4;\n};\n\nconst quarticPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - (1 - t) ** 5;\n }\n return t ** 5;\n};\n\nconst sinusoidalPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - Math.sin(((1 - t) * Math.PI) / 2);\n }\n return Math.sin((t * Math.PI) / 2);\n};\n\nconst asinusoidalPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - Math.asin(1 - t) / (Math.PI / 2);\n }\n return Math.asin(t) / (Math.PI / 2);\n};\n\nconst arcPosition: PositionFunction = (t: number, reverse = false) => {\n if (reverse) {\n return 1 - Math.sqrt(1 - t ** 2);\n }\n return 1 - Math.sqrt(1 - t);\n};\n\nconst smoothStepPosition: PositionFunction = (t: number) => {\n return t ** 2 * (3 - 2 * t);\n};\n\nexport const positionFunctions = {\n linearPosition,\n exponentialPosition,\n quadraticPosition,\n cubicPosition,\n quarticPosition,\n sinusoidalPosition,\n asinusoidalPosition,\n arcPosition,\n smoothStepPosition,\n};\n\n/**\n * Calculates the distance between two points\n * @param p1 The first point\n * @param p2 The second point\n * @param hueMode Whether to use the hue distance function\n * @returns The distance between the two points\n * @example\n * const p1 = [0, 0, 0];\n * const p2 = [1, 1, 1];\n * const dist = distance(p1, p2);\n * console.log(dist); // 1.7320508075688772\n **/\nconst distance = (\n p1: PartialVector3,\n p2: PartialVector3,\n hueMode = false\n): number => {\n const a1 = p1[0];\n const a2 = p2[0];\n let diffA = 0;\n\n if (hueMode && a1 !== null && a2 !== null) {\n diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2));\n diffA = diffA / 360;\n } else {\n diffA = a1 === null || a2 === null ? 0 : a1 - a2;\n }\n\n const a = diffA;\n const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1];\n const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2];\n\n return Math.sqrt(a * a + b * b + c * c);\n};\n\nexport type ColorPointCollection = {\n xyz?: Vector3;\n color?: Vector3;\n invertedLightness?: boolean;\n};\n\nclass ColorPoint {\n public x = 0;\n public y = 0;\n public z = 0;\n public color: Vector3 = [0, 0, 0];\n private _invertedLightness = false;\n\n constructor({\n xyz,\n color,\n invertedLightness = false,\n }: ColorPointCollection = {}) {\n this._invertedLightness = invertedLightness;\n this.positionOrColor({ xyz, color, invertedLightness });\n }\n\n positionOrColor({\n xyz,\n color,\n invertedLightness = false,\n }: ColorPointCollection) {\n if ((xyz && color) || (!xyz && !color)) {\n throw new Error(\"Point must be initialized with either x,y,z or hsl\");\n } else if (xyz) {\n this.x = xyz[0];\n this.y = xyz[1];\n this.z = xyz[2];\n this.color = pointToHSL([this.x, this.y, this.z], invertedLightness);\n } else if (color) {\n this.color = color;\n [this.x, this.y, this.z] = hslToPoint(color, invertedLightness);\n }\n }\n\n set position([x, y, z]: Vector3) {\n this.x = x;\n this.y = y;\n this.z = z;\n this.color = pointToHSL(\n [this.x, this.y, this.z] as Vector3,\n this._invertedLightness\n );\n }\n\n get position(): Vector3 {\n return [this.x, this.y, this.z];\n }\n\n set hsl([h, s, l]: Vector3) {\n this.color = [h, s, l];\n [this.x, this.y, this.z] = hslToPoint(\n this.color as Vector3,\n this._invertedLightness\n );\n }\n\n get hsl(): Vector3 {\n return this.color;\n }\n\n get hslCSS(): string {\n const [h, s, l] = this.color;\n return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed(\n 2\n )}%)`;\n }\n\n get oklchCSS(): string {\n const [h, s, l] = this.color;\n return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed(\n 2\n )})`;\n }\n\n get lchCSS(): string {\n const [h, s, l] = this.color;\n return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed(\n 2\n )})`;\n }\n\n shiftHue(angle: number): void {\n this.color[0] = (360 + (this.color[0] + angle)) % 360;\n [this.x, this.y, this.z] = hslToPoint(\n this.color as Vector3,\n this._invertedLightness\n );\n }\n}\n\nexport type PolineOptions = {\n anchorColors: Vector3[];\n numPoints: number;\n positionFunction?: (t: number, invert?: boolean) => number;\n positionFunctionX?: (t: number, invert?: boolean) => number;\n positionFunctionY?: (t: number, invert?: boolean) => number;\n positionFunctionZ?: (t: number, invert?: boolean) => number;\n invertedLightness?: boolean;\n closedLoop?: boolean;\n};\nexport class Poline {\n private _needsUpdate = true;\n private _anchorPoints: ColorPoint[];\n\n private _numPoints: number;\n private points: ColorPoint[][];\n\n private _positionFunctionX = sinusoidalPosition;\n private _positionFunctionY = sinusoidalPosition;\n private _positionFunctionZ = sinusoidalPosition;\n\n private _anchorPairs: ColorPoint[][];\n\n private connectLastAndFirstAnchor = false;\n\n private _animationFrame: null | number = null;\n\n private _invertedLightness = false;\n\n constructor(\n {\n anchorColors = randomHSLPair(),\n numPoints = 4,\n positionFunction = sinusoidalPosition,\n positionFunctionX,\n positionFunctionY,\n positionFunctionZ,\n closedLoop,\n invertedLightness,\n }: PolineOptions = {\n anchorColors: randomHSLPair(),\n numPoints: 4,\n positionFunction: sinusoidalPosition,\n closedLoop: false,\n }\n ) {\n if (!anchorColors || anchorColors.length < 2) {\n throw new Error(\"Must have at least two anchor colors\");\n }\n\n this._anchorPoints = anchorColors.map(\n (point) => new ColorPoint({ color: point, invertedLightness })\n );\n\n this._numPoints = numPoints + 2; // add two for the anchor points\n\n this._positionFunctionX =\n positionFunctionX || positionFunction || sinusoidalPosition;\n this._positionFunctionY =\n positionFunctionY || positionFunction || sinusoidalPosition;\n this._positionFunctionZ =\n positionFunctionZ || positionFunction || sinusoidalPosition;\n\n this.connectLastAndFirstAnchor = closedLoop || false;\n\n this._invertedLightness = invertedLightness || false;\n\n this.updateAnchorPairs();\n }\n\n public get numPoints(): number {\n return this._numPoints - 2;\n }\n\n public set numPoints(numPoints: number) {\n if (numPoints < 1) {\n throw new Error(\"Must have at least one point\");\n }\n this._numPoints = numPoints + 2; // add two for the anchor points\n this.updateAnchorPairs();\n }\n\n public set positionFunction(\n positionFunction: PositionFunction | PositionFunction[]\n ) {\n if (Array.isArray(positionFunction)) {\n if (positionFunction.length !== 3) {\n throw new Error(\"Position function array must have 3 elements\");\n }\n if (\n typeof positionFunction[0] !== \"function\" ||\n typeof positionFunction[1] !== \"function\" ||\n typeof positionFunction[2] !== \"function\"\n ) {\n throw new Error(\"Position function array must have 3 functions\");\n }\n this._positionFunctionX = positionFunction[0];\n this._positionFunctionY = positionFunction[1];\n this._positionFunctionZ = positionFunction[2];\n } else {\n this._positionFunctionX = positionFunction;\n this._positionFunctionY = positionFunction;\n this._positionFunctionZ = positionFunction;\n }\n\n this.updateAnchorPairs();\n }\n\n public get positionFunction(): PositionFunction | PositionFunction[] {\n // not to sure what to do here, because the position function is a combination of the three\n if (\n this._positionFunctionX === this._positionFunctionY &&\n this._positionFunctionX === this._positionFunctionZ\n ) {\n return this._positionFunctionX;\n }\n\n return [\n this._positionFunctionX,\n this._positionFunctionY,\n this._positionFunctionZ,\n ];\n }\n\n public set positionFunctionX(positionFunctionX: PositionFunction) {\n this._positionFunctionX = positionFunctionX;\n this.updateAnchorPairs();\n }\n\n public get positionFunctionX(): PositionFunction {\n return this._positionFunctionX;\n }\n\n public set positionFunctionY(positionFunctionY: PositionFunction) {\n this._positionFunctionY = positionFunctionY;\n this.updateAnchorPairs();\n }\n\n public get positionFunctionY(): PositionFunction {\n return this._positionFunctionY;\n }\n\n public set positionFunctionZ(positionFunctionZ: PositionFunction) {\n this._positionFunctionZ = positionFunctionZ;\n this.updateAnchorPairs();\n }\n\n public get positionFunctionZ(): PositionFunction {\n return this._positionFunctionZ;\n }\n\n public get anchorPoints(): ColorPoint[] {\n return this._anchorPoints;\n }\n\n public set anchorPoints(anchorPoints: ColorPoint[]) {\n this._anchorPoints = anchorPoints;\n this.updateAnchorPairs();\n }\n\n public updateAnchorPairs(): void {\n this._anchorPairs = [] as ColorPoint[][];\n\n const anchorPointsLength = this.connectLastAndFirstAnchor\n ? this.anchorPoints.length\n : this.anchorPoints.length - 1;\n\n for (let i = 0; i < anchorPointsLength; i++) {\n const pair = [\n this.anchorPoints[i],\n this.anchorPoints[(i + 1) % this.anchorPoints.length],\n ] as ColorPoint[];\n\n this._anchorPairs.push(pair);\n }\n\n this.points = this._anchorPairs.map((pair, i) => {\n const p1position = pair[0] ? pair[0].position : ([0, 0, 0] as Vector3);\n const p2position = pair[1] ? pair[1].position : ([0, 0, 0] as Vector3);\n\n return vectorsOnLine(\n p1position,\n p2position,\n this._numPoints,\n i % 2 ? true : false,\n this.positionFunctionX,\n this.positionFunctionY,\n this.positionFunctionZ\n ).map(\n (p) =>\n new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness })\n );\n });\n }\n\n public addAnchorPoint({\n xyz,\n color,\n insertAtIndex,\n }: ColorPointCollection & { insertAtIndex: number }): ColorPoint {\n const newAnchor = new ColorPoint({\n xyz,\n color,\n invertedLightness: this._invertedLightness,\n });\n if (insertAtIndex) {\n this.anchorPoints.splice(insertAtIndex, 0, newAnchor);\n } else {\n this.anchorPoints.push(newAnchor);\n }\n this.updateAnchorPairs();\n return newAnchor;\n }\n\n public removeAnchorPoint({\n point,\n index,\n }: {\n point?: ColorPoint;\n index?: number;\n }): void {\n if (!point && index === undefined) {\n throw new Error(\"Must provide a point or index\");\n }\n\n if (this.anchorPoints.length < 3) {\n throw new Error(\"Must have at least two anchor points\");\n }\n\n let apid;\n\n if (index !== undefined) {\n apid = index;\n } else if (point) {\n apid = this.anchorPoints.indexOf(point);\n }\n\n if (apid > -1 && apid < this.anchorPoints.length) {\n this.anchorPoints.splice(apid, 1);\n this.updateAnchorPairs();\n } else {\n throw new Error(\"Point not found\");\n }\n }\n\n public updateAnchorPoint({\n point,\n pointIndex,\n xyz,\n color,\n }: {\n point?: ColorPoint;\n pointIndex?: number;\n } & ColorPointCollection): ColorPoint {\n if (pointIndex) {\n point = this.anchorPoints[pointIndex];\n }\n\n if (!point) {\n throw new Error(\"Must provide a point or pointIndex\");\n }\n\n if (!xyz && !color) {\n throw new Error(\"Must provide a new xyz position or color\");\n }\n\n if (xyz) point.position = xyz;\n if (color) point.hsl = color;\n\n this.updateAnchorPairs();\n\n return point;\n }\n\n public getClosestAnchorPoint({\n xyz,\n hsl,\n maxDistance = 1,\n }: {\n xyz?: PartialVector3;\n hsl?: PartialVector3;\n maxDistance?: number;\n }): ColorPoint | null {\n if (!xyz && !hsl) {\n throw new Error(\"Must provide a xyz or hsl\");\n }\n\n let distances;\n\n if (xyz) {\n distances = this.anchorPoints.map((anchor) =>\n distance(anchor.position, xyz)\n );\n } else if (hsl) {\n distances = this.anchorPoints.map((anchor) =>\n distance(anchor.hsl, hsl, true)\n );\n }\n\n const minDistance = Math.min(...distances);\n\n if (minDistance > maxDistance) {\n return null;\n }\n\n const closestAnchorIndex = distances.indexOf(minDistance);\n\n return this.anchorPoints[closestAnchorIndex] || null;\n }\n\n public set closedLoop(newStatus: boolean) {\n this.connectLastAndFirstAnchor = newStatus;\n this.updateAnchorPairs();\n }\n\n public get closedLoop(): boolean {\n return this.connectLastAndFirstAnchor;\n }\n\n public set invertedLightness(newStatus: boolean) {\n this._invertedLightness = newStatus;\n this.updateAnchorPairs();\n }\n\n public get invertedLightness(): boolean {\n return this._invertedLightness;\n }\n\n /**\n * Returns a flattened array of all points across all segments,\n * removing duplicated anchor points at segment boundaries.\n *\n * Since anchor points exist at both the end of one segment and\n * the beginning of the next, this method keeps only one instance of each.\n * The filter logic keeps the first point (index 0) and then filters out\n * points whose indices are multiples of the segment size (_numPoints),\n * which are the anchor points at the start of each segment (except the first).\n *\n * This approach ensures we get all unique points in the correct order\n * while avoiding duplicated anchor points.\n *\n * @returns {ColorPoint[]} A flat array of unique ColorPoint instances\n */\n public get flattenedPoints() {\n return this.points\n .flat()\n .filter((p, i) => (i != 0 ? i % this._numPoints : true));\n }\n\n public get colors() {\n const colors = this.flattenedPoints.map((p) => p.color);\n if (this.connectLastAndFirstAnchor) {\n colors.pop();\n }\n return colors;\n }\n\n public cssColors(mode: \"hsl\" | \"oklch\" | \"lch\" = \"hsl\"): string[] {\n const methods: CSSColorMethods = {\n hsl: (p: ColorPoint): string => p.hslCSS,\n oklch: (p: ColorPoint): string => p.oklchCSS,\n lch: (p: ColorPoint): string => p.lchCSS,\n };\n const cssColors = this.flattenedPoints.map(methods[mode]);\n if (this.connectLastAndFirstAnchor) {\n cssColors.pop();\n }\n return cssColors;\n }\n\n public get colorsCSS() {\n return this.cssColors(\"hsl\");\n }\n\n public get colorsCSSlch() {\n return this.cssColors(\"lch\");\n }\n\n public get colorsCSSoklch() {\n return this.cssColors(\"oklch\");\n }\n\n public shiftHue(hShift = 20): void {\n this.anchorPoints.forEach((p) => p.shiftHue(hShift));\n this.updateAnchorPairs();\n }\n}\n\n// eslint-disable-next-line @typescript-eslint/no-explicit-any\nconst { p5 } = globalThis as any;\n\nif (p5) {\n console.info(\"p5 detected, adding poline to p5 prototype\");\n\n const poline = new Poline();\n p5.prototype.poline = poline;\n\n const polineColors = () =>\n poline.colors.map(\n (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)`\n );\n p5.prototype.polineColors = polineColors;\n p5.prototype.registerMethod(\"polineColors\", p5.prototype.polineColors);\n\n globalThis.poline = poline;\n globalThis.polineColors = polineColors;\n}\n"], 5 | "mappings": ";;;AAwBO,IAAM,aAAa,CACxB,KACA,sBACY;AACZ,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAGlB,QAAM,KAAK;AACX,QAAM,KAAK;AAGX,QAAM,UAAU,KAAK,MAAM,IAAI,IAAI,IAAI,EAAE;AAGzC,MAAI,MAAM,WAAW,MAAM,KAAK;AAChC,SAAO,MAAM,OAAO;AAGpB,QAAM,IAAI;AAGV,QAAM,OAAO,KAAK,KAAK,KAAK,IAAI,IAAI,IAAI,CAAC,IAAI,KAAK,IAAI,IAAI,IAAI,CAAC,CAAC;AAChE,QAAM,IAAI,OAAO;AAGjB,SAAO,CAAC,KAAK,GAAG,oBAAoB,IAAI,IAAI,CAAC;AAC/C;AAeO,IAAM,aAAa,CACxB,KACA,sBACY;AAEZ,QAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAElB,QAAM,KAAK;AACX,QAAM,KAAK;AAEX,QAAM,UAAU,KAAK,MAAM,KAAK;AAGhC,QAAM,QAAQ,oBAAoB,IAAI,IAAI,KAAK;AAG/C,QAAM,IAAI,KAAK,OAAO,KAAK,IAAI,OAAO;AACtC,QAAM,IAAI,KAAK,OAAO,KAAK,IAAI,OAAO;AAEtC,QAAM,IAAI;AAEV,SAAO,CAAC,GAAG,GAAG,CAAC;AACjB;AAEO,IAAM,gBAAgB,CAC3B,WAAmB,KAAK,OAAO,IAAI,KACnC,cAAuB,CAAC,KAAK,OAAO,GAAG,KAAK,OAAO,CAAC,GACpD,cAAuB,CAAC,OAAO,KAAK,OAAO,IAAI,KAAK,MAAM,KAAK,OAAO,IAAI,GAAG,MACtD;AAAA,EACvB,CAAC,UAAU,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC;AAAA,EACzC,EAAE,WAAW,KAAK,KAAK,OAAO,IAAI,OAAO,KAAK,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC;AAC9E;AAEO,IAAM,kBAAkB,CAC7B,WAAmB,KAAK,OAAO,IAAI,KACnC,cAAuB,CAAC,KAAK,OAAO,GAAG,KAAK,OAAO,GAAG,KAAK,OAAO,CAAC,GACnE,cAAuB;AAAA,EACrB,OAAO,KAAK,OAAO,IAAI;AAAA,EACvB,KAAK,OAAO,IAAI;AAAA,EAChB,OAAO,KAAK,OAAO,IAAI;AACzB,MACgC;AAAA,EAChC,CAAC,UAAU,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC;AAAA,EACzC,EAAE,WAAW,KAAK,KAAK,OAAO,IAAI,OAAO,KAAK,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC;AAAA,EAC5E,EAAE,WAAW,KAAK,KAAK,OAAO,IAAI,OAAO,KAAK,YAAY,CAAC,GAAG,YAAY,CAAC,CAAC;AAC9E;AAEA,IAAM,eAAe,CACnB,GACA,IACA,IACA,SAAS,OACT,KAAK,CAACA,IAAWC,YAA6BA,UAAS,IAAID,KAAIA,IAC/D,KAAK,CAACA,IAAWC,YAA6BA,UAAS,IAAID,KAAIA,IAC/D,KAAK,CAACA,IAAWC,YAA6BA,UAAS,IAAID,KAAIA,OACnD;AACZ,QAAM,aAAa,GAAG,GAAG,MAAM;AAC/B,QAAM,aAAa,GAAG,GAAG,MAAM;AAC/B,QAAM,aAAa,GAAG,GAAG,MAAM;AAC/B,QAAM,KAAK,IAAI,cAAc,GAAG,CAAC,IAAI,aAAa,GAAG,CAAC;AACtD,QAAM,KAAK,IAAI,cAAc,GAAG,CAAC,IAAI,aAAa,GAAG,CAAC;AACtD,QAAM,KAAK,IAAI,cAAc,GAAG,CAAC,IAAI,aAAa,GAAG,CAAC;AAEtD,SAAO,CAAC,GAAG,GAAG,CAAC;AACjB;AAEA,IAAM,gBAAgB,CACpB,IACA,IACA,YAAY,GACZ,SAAS,OACT,KAAK,CAAC,GAAWC,YAA6BA,UAAS,IAAI,IAAI,GAC/D,KAAK,CAAC,GAAWA,YAA6BA,UAAS,IAAI,IAAI,GAC/D,KAAK,CAAC,GAAWA,YAA6BA,UAAS,IAAI,IAAI,MACjD;AACd,QAAM,SAAoB,CAAC;AAE3B,WAAS,IAAI,GAAG,IAAI,WAAW,KAAK;AAClC,UAAM,CAAC,GAAG,GAAG,CAAC,IAAI;AAAA,MAChB,KAAK,YAAY;AAAA,MACjB;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AACA,WAAO,KAAK,CAAC,GAAG,GAAG,CAAC,CAAC;AAAA,EACvB;AAEA,SAAO;AACT;AAIA,IAAM,iBAAmC,CAAC,MAAc;AACtD,SAAO;AACT;AAEA,IAAM,sBAAwC,CAAC,GAAW,UAAU,UAAU;AAC5E,MAAI,SAAS;AACX,WAAO,IAAK,UAAI,GAAM;AAAA,EACxB;AACA,SAAO,SAAK;AACd;AAEA,IAAM,oBAAsC,CAAC,GAAW,UAAU,UAAU;AAC1E,MAAI,SAAS;AACX,WAAO,IAAK,UAAI,GAAM;AAAA,EACxB;AACA,SAAO,SAAK;AACd;AAEA,IAAM,gBAAkC,CAAC,GAAW,UAAU,UAAU;AACtE,MAAI,SAAS;AACX,WAAO,IAAK,UAAI,GAAM;AAAA,EACxB;AACA,SAAO,SAAK;AACd;AAEA,IAAM,kBAAoC,CAAC,GAAW,UAAU,UAAU;AACxE,MAAI,SAAS;AACX,WAAO,IAAK,UAAI,GAAM;AAAA,EACxB;AACA,SAAO,SAAK;AACd;AAEA,IAAM,qBAAuC,CAAC,GAAW,UAAU,UAAU;AAC3E,MAAI,SAAS;AACX,WAAO,IAAI,KAAK,KAAM,IAAI,KAAK,KAAK,KAAM,CAAC;AAAA,EAC7C;AACA,SAAO,KAAK,IAAK,IAAI,KAAK,KAAM,CAAC;AACnC;AAEA,IAAM,sBAAwC,CAAC,GAAW,UAAU,UAAU;AAC5E,MAAI,SAAS;AACX,WAAO,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK,KAAK,KAAK;AAAA,EAC3C;AACA,SAAO,KAAK,KAAK,CAAC,KAAK,KAAK,KAAK;AACnC;AAEA,IAAM,cAAgC,CAAC,GAAW,UAAU,UAAU;AACpE,MAAI,SAAS;AACX,WAAO,IAAI,KAAK,KAAK,IAAI,SAAK,EAAC;AAAA,EACjC;AACA,SAAO,IAAI,KAAK,KAAK,IAAI,CAAC;AAC5B;AAEA,IAAM,qBAAuC,CAAC,MAAc;AAC1D,SAAO,SAAK,MAAK,IAAI,IAAI;AAC3B;AAEO,IAAM,oBAAoB;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAcA,IAAM,WAAW,CACf,IACA,IACA,UAAU,UACC;AACX,QAAM,KAAK,GAAG,CAAC;AACf,QAAM,KAAK,GAAG,CAAC;AACf,MAAI,QAAQ;AAEZ,MAAI,WAAW,OAAO,QAAQ,OAAO,MAAM;AACzC,YAAQ,KAAK,IAAI,KAAK,IAAI,KAAK,EAAE,GAAG,MAAM,KAAK,IAAI,KAAK,EAAE,CAAC;AAC3D,YAAQ,QAAQ;AAAA,EAClB,OAAO;AACL,YAAQ,OAAO,QAAQ,OAAO,OAAO,IAAI,KAAK;AAAA,EAChD;AAEA,QAAM,IAAI;AACV,QAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAM,OAAO,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC;AAC7D,QAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,GAAG,CAAC,MAAM,OAAO,IAAI,GAAG,CAAC,IAAI,GAAG,CAAC;AAE7D,SAAO,KAAK,KAAK,IAAI,IAAI,IAAI,IAAI,IAAI,CAAC;AACxC;AAQA,IAAM,aAAN,MAAiB;AAAA,EAOf,YAAY;AAAA,IACV;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,EACtB,IAA0B,CAAC,GAAG;AAV9B,SAAO,IAAI;AACX,SAAO,IAAI;AACX,SAAO,IAAI;AACX,SAAO,QAAiB,CAAC,GAAG,GAAG,CAAC;AAChC,SAAQ,qBAAqB;AAO3B,SAAK,qBAAqB;AAC1B,SAAK,gBAAgB,EAAE,KAAK,OAAO,kBAAkB,CAAC;AAAA,EACxD;AAAA,EAEA,gBAAgB;AAAA,IACd;AAAA,IACA;AAAA,IACA,oBAAoB;AAAA,EACtB,GAAyB;AACvB,QAAK,OAAO,SAAW,CAAC,OAAO,CAAC,OAAQ;AACtC,YAAM,IAAI,MAAM,oDAAoD;AAAA,IACtE,WAAW,KAAK;AACd,WAAK,IAAI,IAAI,CAAC;AACd,WAAK,IAAI,IAAI,CAAC;AACd,WAAK,IAAI,IAAI,CAAC;AACd,WAAK,QAAQ,WAAW,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,GAAG,iBAAiB;AAAA,IACrE,WAAW,OAAO;AAChB,WAAK,QAAQ;AACb,OAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,IAAI,WAAW,OAAO,iBAAiB;AAAA,IAChE;AAAA,EACF;AAAA,EAEA,IAAI,SAAS,CAAC,GAAG,GAAG,CAAC,GAAY;AAC/B,SAAK,IAAI;AACT,SAAK,IAAI;AACT,SAAK,IAAI;AACT,SAAK,QAAQ;AAAA,MACX,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAAA,MACvB,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,IAAI,WAAoB;AACtB,WAAO,CAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC;AAAA,EAChC;AAAA,EAEA,IAAI,IAAI,CAAC,GAAG,GAAG,CAAC,GAAY;AAC1B,SAAK,QAAQ,CAAC,GAAG,GAAG,CAAC;AACrB,KAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,IAAI;AAAA,MACzB,KAAK;AAAA,MACL,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,IAAI,MAAe;AACjB,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAI,SAAiB;AACnB,UAAM,CAAC,GAAG,GAAG,CAAC,IAAI,KAAK;AACvB,WAAO,OAAO,EAAE,QAAQ,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,QAAQ,IAAI,KAAK;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,WAAmB;AACrB,UAAM,CAAC,GAAG,GAAG,CAAC,IAAI,KAAK;AACvB,WAAO,UAAU,IAAI,KAAK,QAAQ,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,KAAK,EAAE;AAAA,MACjE;AAAA,IACF;AAAA,EACF;AAAA,EAEA,IAAI,SAAiB;AACnB,UAAM,CAAC,GAAG,GAAG,CAAC,IAAI,KAAK;AACvB,WAAO,QAAQ,IAAI,KAAK,QAAQ,CAAC,OAAO,IAAI,KAAK,QAAQ,CAAC,KAAK,EAAE;AAAA,MAC/D;AAAA,IACF;AAAA,EACF;AAAA,EAEA,SAAS,OAAqB;AAC5B,SAAK,MAAM,CAAC,KAAK,OAAO,KAAK,MAAM,CAAC,IAAI,UAAU;AAClD,KAAC,KAAK,GAAG,KAAK,GAAG,KAAK,CAAC,IAAI;AAAA,MACzB,KAAK;AAAA,MACL,KAAK;AAAA,IACP;AAAA,EACF;AACF;AAYO,IAAM,SAAN,MAAa;AAAA,EAmBlB,YACE;AAAA,IACE,eAAe,cAAc;AAAA,IAC7B,YAAY;AAAA,IACZ,mBAAmB;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,IAAmB;AAAA,IACjB,cAAc,cAAc;AAAA,IAC5B,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB,YAAY;AAAA,EACd,GACA;AAlCF,SAAQ,eAAe;AAMvB,SAAQ,qBAAqB;AAC7B,SAAQ,qBAAqB;AAC7B,SAAQ,qBAAqB;AAI7B,SAAQ,4BAA4B;AAEpC,SAAQ,kBAAiC;AAEzC,SAAQ,qBAAqB;AAmB3B,QAAI,CAAC,gBAAgB,aAAa,SAAS,GAAG;AAC5C,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAEA,SAAK,gBAAgB,aAAa;AAAA,MAChC,CAAC,UAAU,IAAI,WAAW,EAAE,OAAO,OAAO,kBAAkB,CAAC;AAAA,IAC/D;AAEA,SAAK,aAAa,YAAY;AAE9B,SAAK,qBACH,qBAAqB,oBAAoB;AAC3C,SAAK,qBACH,qBAAqB,oBAAoB;AAC3C,SAAK,qBACH,qBAAqB,oBAAoB;AAE3C,SAAK,4BAA4B,cAAc;AAE/C,SAAK,qBAAqB,qBAAqB;AAE/C,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,YAAoB;AAC7B,WAAO,KAAK,aAAa;AAAA,EAC3B;AAAA,EAEA,IAAW,UAAU,WAAmB;AACtC,QAAI,YAAY,GAAG;AACjB,YAAM,IAAI,MAAM,8BAA8B;AAAA,IAChD;AACA,SAAK,aAAa,YAAY;AAC9B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,iBACT,kBACA;AACA,QAAI,MAAM,QAAQ,gBAAgB,GAAG;AACnC,UAAI,iBAAiB,WAAW,GAAG;AACjC,cAAM,IAAI,MAAM,8CAA8C;AAAA,MAChE;AACA,UACE,OAAO,iBAAiB,CAAC,MAAM,cAC/B,OAAO,iBAAiB,CAAC,MAAM,cAC/B,OAAO,iBAAiB,CAAC,MAAM,YAC/B;AACA,cAAM,IAAI,MAAM,+CAA+C;AAAA,MACjE;AACA,WAAK,qBAAqB,iBAAiB,CAAC;AAC5C,WAAK,qBAAqB,iBAAiB,CAAC;AAC5C,WAAK,qBAAqB,iBAAiB,CAAC;AAAA,IAC9C,OAAO;AACL,WAAK,qBAAqB;AAC1B,WAAK,qBAAqB;AAC1B,WAAK,qBAAqB;AAAA,IAC5B;AAEA,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,mBAA0D;AAEnE,QACE,KAAK,uBAAuB,KAAK,sBACjC,KAAK,uBAAuB,KAAK,oBACjC;AACA,aAAO,KAAK;AAAA,IACd;AAEA,WAAO;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,MACL,KAAK;AAAA,IACP;AAAA,EACF;AAAA,EAEA,IAAW,kBAAkB,mBAAqC;AAChE,SAAK,qBAAqB;AAC1B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,oBAAsC;AAC/C,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAW,kBAAkB,mBAAqC;AAChE,SAAK,qBAAqB;AAC1B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,oBAAsC;AAC/C,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAW,kBAAkB,mBAAqC;AAChE,SAAK,qBAAqB;AAC1B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,oBAAsC;AAC/C,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAW,eAA6B;AACtC,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAW,aAAa,cAA4B;AAClD,SAAK,gBAAgB;AACrB,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEO,oBAA0B;AAC/B,SAAK,eAAe,CAAC;AAErB,UAAM,qBAAqB,KAAK,4BAC5B,KAAK,aAAa,SAClB,KAAK,aAAa,SAAS;AAE/B,aAAS,IAAI,GAAG,IAAI,oBAAoB,KAAK;AAC3C,YAAM,OAAO;AAAA,QACX,KAAK,aAAa,CAAC;AAAA,QACnB,KAAK,cAAc,IAAI,KAAK,KAAK,aAAa,MAAM;AAAA,MACtD;AAEA,WAAK,aAAa,KAAK,IAAI;AAAA,IAC7B;AAEA,SAAK,SAAS,KAAK,aAAa,IAAI,CAAC,MAAM,MAAM;AAC/C,YAAM,aAAa,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,WAAY,CAAC,GAAG,GAAG,CAAC;AACzD,YAAM,aAAa,KAAK,CAAC,IAAI,KAAK,CAAC,EAAE,WAAY,CAAC,GAAG,GAAG,CAAC;AAEzD,aAAO;AAAA,QACL;AAAA,QACA;AAAA,QACA,KAAK;AAAA,QACL,IAAI,IAAI,OAAO;AAAA,QACf,KAAK;AAAA,QACL,KAAK;AAAA,QACL,KAAK;AAAA,MACP,EAAE;AAAA,QACA,CAAC,MACC,IAAI,WAAW,EAAE,KAAK,GAAG,mBAAmB,KAAK,mBAAmB,CAAC;AAAA,MACzE;AAAA,IACF,CAAC;AAAA,EACH;AAAA,EAEO,eAAe;AAAA,IACpB;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAAiE;AAC/D,UAAM,YAAY,IAAI,WAAW;AAAA,MAC/B;AAAA,MACA;AAAA,MACA,mBAAmB,KAAK;AAAA,IAC1B,CAAC;AACD,QAAI,eAAe;AACjB,WAAK,aAAa,OAAO,eAAe,GAAG,SAAS;AAAA,IACtD,OAAO;AACL,WAAK,aAAa,KAAK,SAAS;AAAA,IAClC;AACA,SAAK,kBAAkB;AACvB,WAAO;AAAA,EACT;AAAA,EAEO,kBAAkB;AAAA,IACvB;AAAA,IACA;AAAA,EACF,GAGS;AACP,QAAI,CAAC,SAAS,UAAU,QAAW;AACjC,YAAM,IAAI,MAAM,+BAA+B;AAAA,IACjD;AAEA,QAAI,KAAK,aAAa,SAAS,GAAG;AAChC,YAAM,IAAI,MAAM,sCAAsC;AAAA,IACxD;AAEA,QAAI;AAEJ,QAAI,UAAU,QAAW;AACvB,aAAO;AAAA,IACT,WAAW,OAAO;AAChB,aAAO,KAAK,aAAa,QAAQ,KAAK;AAAA,IACxC;AAEA,QAAI,OAAO,MAAM,OAAO,KAAK,aAAa,QAAQ;AAChD,WAAK,aAAa,OAAO,MAAM,CAAC;AAChC,WAAK,kBAAkB;AAAA,IACzB,OAAO;AACL,YAAM,IAAI,MAAM,iBAAiB;AAAA,IACnC;AAAA,EACF;AAAA,EAEO,kBAAkB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,GAGsC;AACpC,QAAI,YAAY;AACd,cAAQ,KAAK,aAAa,UAAU;AAAA,IACtC;AAEA,QAAI,CAAC,OAAO;AACV,YAAM,IAAI,MAAM,oCAAoC;AAAA,IACtD;AAEA,QAAI,CAAC,OAAO,CAAC,OAAO;AAClB,YAAM,IAAI,MAAM,0CAA0C;AAAA,IAC5D;AAEA,QAAI;AAAK,YAAM,WAAW;AAC1B,QAAI;AAAO,YAAM,MAAM;AAEvB,SAAK,kBAAkB;AAEvB,WAAO;AAAA,EACT;AAAA,EAEO,sBAAsB;AAAA,IAC3B;AAAA,IACA;AAAA,IACA,cAAc;AAAA,EAChB,GAIsB;AACpB,QAAI,CAAC,OAAO,CAAC,KAAK;AAChB,YAAM,IAAI,MAAM,2BAA2B;AAAA,IAC7C;AAEA,QAAI;AAEJ,QAAI,KAAK;AACP,kBAAY,KAAK,aAAa;AAAA,QAAI,CAAC,WACjC,SAAS,OAAO,UAAU,GAAG;AAAA,MAC/B;AAAA,IACF,WAAW,KAAK;AACd,kBAAY,KAAK,aAAa;AAAA,QAAI,CAAC,WACjC,SAAS,OAAO,KAAK,KAAK,IAAI;AAAA,MAChC;AAAA,IACF;AAEA,UAAM,cAAc,KAAK,IAAI,GAAG,SAAS;AAEzC,QAAI,cAAc,aAAa;AAC7B,aAAO;AAAA,IACT;AAEA,UAAM,qBAAqB,UAAU,QAAQ,WAAW;AAExD,WAAO,KAAK,aAAa,kBAAkB,KAAK;AAAA,EAClD;AAAA,EAEA,IAAW,WAAW,WAAoB;AACxC,SAAK,4BAA4B;AACjC,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,aAAsB;AAC/B,WAAO,KAAK;AAAA,EACd;AAAA,EAEA,IAAW,kBAAkB,WAAoB;AAC/C,SAAK,qBAAqB;AAC1B,SAAK,kBAAkB;AAAA,EACzB;AAAA,EAEA,IAAW,oBAA6B;AACtC,WAAO,KAAK;AAAA,EACd;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAiBA,IAAW,kBAAkB;AAC3B,WAAO,KAAK,OACT,KAAK,EACL,OAAO,CAAC,GAAG,MAAO,KAAK,IAAI,IAAI,KAAK,aAAa,IAAK;AAAA,EAC3D;AAAA,EAEA,IAAW,SAAS;AAClB,UAAM,SAAS,KAAK,gBAAgB,IAAI,CAAC,MAAM,EAAE,KAAK;AACtD,QAAI,KAAK,2BAA2B;AAClC,aAAO,IAAI;AAAA,IACb;AACA,WAAO;AAAA,EACT;AAAA,EAEO,UAAU,OAAgC,OAAiB;AAChE,UAAM,UAA2B;AAAA,MAC/B,KAAK,CAAC,MAA0B,EAAE;AAAA,MAClC,OAAO,CAAC,MAA0B,EAAE;AAAA,MACpC,KAAK,CAAC,MAA0B,EAAE;AAAA,IACpC;AACA,UAAM,YAAY,KAAK,gBAAgB,IAAI,QAAQ,IAAI,CAAC;AACxD,QAAI,KAAK,2BAA2B;AAClC,gBAAU,IAAI;AAAA,IAChB;AACA,WAAO;AAAA,EACT;AAAA,EAEA,IAAW,YAAY;AACrB,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA,EAEA,IAAW,eAAe;AACxB,WAAO,KAAK,UAAU,KAAK;AAAA,EAC7B;AAAA,EAEA,IAAW,iBAAiB;AAC1B,WAAO,KAAK,UAAU,OAAO;AAAA,EAC/B;AAAA,EAEO,SAAS,SAAS,IAAU;AACjC,SAAK,aAAa,QAAQ,CAAC,MAAM,EAAE,SAAS,MAAM,CAAC;AACnD,SAAK,kBAAkB;AAAA,EACzB;AACF;AAGA,IAAM,EAAE,GAAG,IAAI;AAEf,IAAI,IAAI;AACN,UAAQ,KAAK,4CAA4C;AAEzD,QAAM,SAAS,IAAI,OAAO;AAC1B,KAAG,UAAU,SAAS;AAEtB,QAAM,eAAe,MACnB,OAAO,OAAO;AAAA,IACZ,CAAC,MAAM,OAAO,KAAK,MAAM,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,IAAI,QAAQ,EAAE,CAAC,IAAI;AAAA,EAC1D;AACF,KAAG,UAAU,eAAe;AAC5B,KAAG,UAAU,eAAe,gBAAgB,GAAG,UAAU,YAAY;AAErE,aAAW,SAAS;AACpB,aAAW,eAAe;AAC5B;", 6 | "names": ["t", "invert"] 7 | } 8 | -------------------------------------------------------------------------------- /dist/index.umd.js: -------------------------------------------------------------------------------- 1 | (function(root, factory) { 2 | if (typeof define === 'function' && define.amd) { 3 | define([], factory); 4 | } else if (typeof module === 'object' && module.exports) { 5 | module.exports = factory(); 6 | } else { 7 | root.poline = factory(); 8 | } 9 | } 10 | (typeof self !== 'undefined' ? self : this, function() { 11 | "use strict"; 12 | var poline = (() => { 13 | var __defProp = Object.defineProperty; 14 | var __getOwnPropDesc = Object.getOwnPropertyDescriptor; 15 | var __getOwnPropNames = Object.getOwnPropertyNames; 16 | var __hasOwnProp = Object.prototype.hasOwnProperty; 17 | var __pow = Math.pow; 18 | var __export = (target, all) => { 19 | for (var name in all) 20 | __defProp(target, name, { get: all[name], enumerable: true }); 21 | }; 22 | var __copyProps = (to, from, except, desc) => { 23 | if (from && typeof from === "object" || typeof from === "function") { 24 | for (let key of __getOwnPropNames(from)) 25 | if (!__hasOwnProp.call(to, key) && key !== except) 26 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 27 | } 28 | return to; 29 | }; 30 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 31 | 32 | // src/index.ts 33 | var src_exports = {}; 34 | __export(src_exports, { 35 | Poline: () => Poline, 36 | hslToPoint: () => hslToPoint, 37 | pointToHSL: () => pointToHSL, 38 | positionFunctions: () => positionFunctions, 39 | randomHSLPair: () => randomHSLPair, 40 | randomHSLTriple: () => randomHSLTriple 41 | }); 42 | var pointToHSL = (xyz, invertedLightness) => { 43 | const [x, y, z] = xyz; 44 | const cx = 0.5; 45 | const cy = 0.5; 46 | const radians = Math.atan2(y - cy, x - cx); 47 | let deg = radians * (180 / Math.PI); 48 | deg = (360 + deg) % 360; 49 | const s = z; 50 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 51 | const l = dist / cx; 52 | return [deg, s, invertedLightness ? 1 - l : l]; 53 | }; 54 | var hslToPoint = (hsl, invertedLightness) => { 55 | const [h, s, l] = hsl; 56 | const cx = 0.5; 57 | const cy = 0.5; 58 | const radians = h / (180 / Math.PI); 59 | const dist = (invertedLightness ? 1 - l : l) * cx; 60 | const x = cx + dist * Math.cos(radians); 61 | const y = cy + dist * Math.sin(radians); 62 | const z = s; 63 | return [x, y, z]; 64 | }; 65 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 66 | [startHue, saturations[0], lightnesses[0]], 67 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 68 | ]; 69 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 70 | 0.75 + Math.random() * 0.2, 71 | Math.random() * 0.2, 72 | 0.75 + Math.random() * 0.2 73 | ]) => [ 74 | [startHue, saturations[0], lightnesses[0]], 75 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 76 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 77 | ]; 78 | var vectorOnLine = (t, p1, p2, invert = false, fx = (t2, invert2) => invert2 ? 1 - t2 : t2, fy = (t2, invert2) => invert2 ? 1 - t2 : t2, fz = (t2, invert2) => invert2 ? 1 - t2 : t2) => { 79 | const tModifiedX = fx(t, invert); 80 | const tModifiedY = fy(t, invert); 81 | const tModifiedZ = fz(t, invert); 82 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 83 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 84 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 85 | return [x, y, z]; 86 | }; 87 | var vectorsOnLine = (p1, p2, numPoints = 4, invert = false, fx = (t, invert2) => invert2 ? 1 - t : t, fy = (t, invert2) => invert2 ? 1 - t : t, fz = (t, invert2) => invert2 ? 1 - t : t) => { 88 | const points = []; 89 | for (let i = 0; i < numPoints; i++) { 90 | const [x, y, z] = vectorOnLine( 91 | i / (numPoints - 1), 92 | p1, 93 | p2, 94 | invert, 95 | fx, 96 | fy, 97 | fz 98 | ); 99 | points.push([x, y, z]); 100 | } 101 | return points; 102 | }; 103 | var linearPosition = (t) => { 104 | return t; 105 | }; 106 | var exponentialPosition = (t, reverse = false) => { 107 | if (reverse) { 108 | return 1 - __pow(1 - t, 2); 109 | } 110 | return __pow(t, 2); 111 | }; 112 | var quadraticPosition = (t, reverse = false) => { 113 | if (reverse) { 114 | return 1 - __pow(1 - t, 3); 115 | } 116 | return __pow(t, 3); 117 | }; 118 | var cubicPosition = (t, reverse = false) => { 119 | if (reverse) { 120 | return 1 - __pow(1 - t, 4); 121 | } 122 | return __pow(t, 4); 123 | }; 124 | var quarticPosition = (t, reverse = false) => { 125 | if (reverse) { 126 | return 1 - __pow(1 - t, 5); 127 | } 128 | return __pow(t, 5); 129 | }; 130 | var sinusoidalPosition = (t, reverse = false) => { 131 | if (reverse) { 132 | return 1 - Math.sin((1 - t) * Math.PI / 2); 133 | } 134 | return Math.sin(t * Math.PI / 2); 135 | }; 136 | var asinusoidalPosition = (t, reverse = false) => { 137 | if (reverse) { 138 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 139 | } 140 | return Math.asin(t) / (Math.PI / 2); 141 | }; 142 | var arcPosition = (t, reverse = false) => { 143 | if (reverse) { 144 | return 1 - Math.sqrt(1 - __pow(t, 2)); 145 | } 146 | return 1 - Math.sqrt(1 - t); 147 | }; 148 | var smoothStepPosition = (t) => { 149 | return __pow(t, 2) * (3 - 2 * t); 150 | }; 151 | var positionFunctions = { 152 | linearPosition, 153 | exponentialPosition, 154 | quadraticPosition, 155 | cubicPosition, 156 | quarticPosition, 157 | sinusoidalPosition, 158 | asinusoidalPosition, 159 | arcPosition, 160 | smoothStepPosition 161 | }; 162 | var distance = (p1, p2, hueMode = false) => { 163 | const a1 = p1[0]; 164 | const a2 = p2[0]; 165 | let diffA = 0; 166 | if (hueMode && a1 !== null && a2 !== null) { 167 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 168 | diffA = diffA / 360; 169 | } else { 170 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 171 | } 172 | const a = diffA; 173 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 174 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 175 | return Math.sqrt(a * a + b * b + c * c); 176 | }; 177 | var ColorPoint = class { 178 | constructor({ 179 | xyz, 180 | color, 181 | invertedLightness = false 182 | } = {}) { 183 | this.x = 0; 184 | this.y = 0; 185 | this.z = 0; 186 | this.color = [0, 0, 0]; 187 | this._invertedLightness = false; 188 | this._invertedLightness = invertedLightness; 189 | this.positionOrColor({ xyz, color, invertedLightness }); 190 | } 191 | positionOrColor({ 192 | xyz, 193 | color, 194 | invertedLightness = false 195 | }) { 196 | if (xyz && color || !xyz && !color) { 197 | throw new Error("Point must be initialized with either x,y,z or hsl"); 198 | } else if (xyz) { 199 | this.x = xyz[0]; 200 | this.y = xyz[1]; 201 | this.z = xyz[2]; 202 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 203 | } else if (color) { 204 | this.color = color; 205 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 206 | } 207 | } 208 | set position([x, y, z]) { 209 | this.x = x; 210 | this.y = y; 211 | this.z = z; 212 | this.color = pointToHSL( 213 | [this.x, this.y, this.z], 214 | this._invertedLightness 215 | ); 216 | } 217 | get position() { 218 | return [this.x, this.y, this.z]; 219 | } 220 | set hsl([h, s, l]) { 221 | this.color = [h, s, l]; 222 | [this.x, this.y, this.z] = hslToPoint( 223 | this.color, 224 | this._invertedLightness 225 | ); 226 | } 227 | get hsl() { 228 | return this.color; 229 | } 230 | get hslCSS() { 231 | const [h, s, l] = this.color; 232 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 233 | 2 234 | )}%)`; 235 | } 236 | get oklchCSS() { 237 | const [h, s, l] = this.color; 238 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 239 | 2 240 | )})`; 241 | } 242 | get lchCSS() { 243 | const [h, s, l] = this.color; 244 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 245 | 2 246 | )})`; 247 | } 248 | shiftHue(angle) { 249 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 250 | [this.x, this.y, this.z] = hslToPoint( 251 | this.color, 252 | this._invertedLightness 253 | ); 254 | } 255 | }; 256 | var Poline = class { 257 | constructor({ 258 | anchorColors = randomHSLPair(), 259 | numPoints = 4, 260 | positionFunction = sinusoidalPosition, 261 | positionFunctionX, 262 | positionFunctionY, 263 | positionFunctionZ, 264 | closedLoop, 265 | invertedLightness 266 | } = { 267 | anchorColors: randomHSLPair(), 268 | numPoints: 4, 269 | positionFunction: sinusoidalPosition, 270 | closedLoop: false 271 | }) { 272 | this._needsUpdate = true; 273 | this._positionFunctionX = sinusoidalPosition; 274 | this._positionFunctionY = sinusoidalPosition; 275 | this._positionFunctionZ = sinusoidalPosition; 276 | this.connectLastAndFirstAnchor = false; 277 | this._animationFrame = null; 278 | this._invertedLightness = false; 279 | if (!anchorColors || anchorColors.length < 2) { 280 | throw new Error("Must have at least two anchor colors"); 281 | } 282 | this._anchorPoints = anchorColors.map( 283 | (point) => new ColorPoint({ color: point, invertedLightness }) 284 | ); 285 | this._numPoints = numPoints + 2; 286 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 287 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 288 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 289 | this.connectLastAndFirstAnchor = closedLoop || false; 290 | this._invertedLightness = invertedLightness || false; 291 | this.updateAnchorPairs(); 292 | } 293 | get numPoints() { 294 | return this._numPoints - 2; 295 | } 296 | set numPoints(numPoints) { 297 | if (numPoints < 1) { 298 | throw new Error("Must have at least one point"); 299 | } 300 | this._numPoints = numPoints + 2; 301 | this.updateAnchorPairs(); 302 | } 303 | set positionFunction(positionFunction) { 304 | if (Array.isArray(positionFunction)) { 305 | if (positionFunction.length !== 3) { 306 | throw new Error("Position function array must have 3 elements"); 307 | } 308 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 309 | throw new Error("Position function array must have 3 functions"); 310 | } 311 | this._positionFunctionX = positionFunction[0]; 312 | this._positionFunctionY = positionFunction[1]; 313 | this._positionFunctionZ = positionFunction[2]; 314 | } else { 315 | this._positionFunctionX = positionFunction; 316 | this._positionFunctionY = positionFunction; 317 | this._positionFunctionZ = positionFunction; 318 | } 319 | this.updateAnchorPairs(); 320 | } 321 | get positionFunction() { 322 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 323 | return this._positionFunctionX; 324 | } 325 | return [ 326 | this._positionFunctionX, 327 | this._positionFunctionY, 328 | this._positionFunctionZ 329 | ]; 330 | } 331 | set positionFunctionX(positionFunctionX) { 332 | this._positionFunctionX = positionFunctionX; 333 | this.updateAnchorPairs(); 334 | } 335 | get positionFunctionX() { 336 | return this._positionFunctionX; 337 | } 338 | set positionFunctionY(positionFunctionY) { 339 | this._positionFunctionY = positionFunctionY; 340 | this.updateAnchorPairs(); 341 | } 342 | get positionFunctionY() { 343 | return this._positionFunctionY; 344 | } 345 | set positionFunctionZ(positionFunctionZ) { 346 | this._positionFunctionZ = positionFunctionZ; 347 | this.updateAnchorPairs(); 348 | } 349 | get positionFunctionZ() { 350 | return this._positionFunctionZ; 351 | } 352 | get anchorPoints() { 353 | return this._anchorPoints; 354 | } 355 | set anchorPoints(anchorPoints) { 356 | this._anchorPoints = anchorPoints; 357 | this.updateAnchorPairs(); 358 | } 359 | updateAnchorPairs() { 360 | this._anchorPairs = []; 361 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 362 | for (let i = 0; i < anchorPointsLength; i++) { 363 | const pair = [ 364 | this.anchorPoints[i], 365 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 366 | ]; 367 | this._anchorPairs.push(pair); 368 | } 369 | this.points = this._anchorPairs.map((pair, i) => { 370 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 371 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 372 | return vectorsOnLine( 373 | p1position, 374 | p2position, 375 | this._numPoints, 376 | i % 2 ? true : false, 377 | this.positionFunctionX, 378 | this.positionFunctionY, 379 | this.positionFunctionZ 380 | ).map( 381 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 382 | ); 383 | }); 384 | } 385 | addAnchorPoint({ 386 | xyz, 387 | color, 388 | insertAtIndex 389 | }) { 390 | const newAnchor = new ColorPoint({ 391 | xyz, 392 | color, 393 | invertedLightness: this._invertedLightness 394 | }); 395 | if (insertAtIndex) { 396 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 397 | } else { 398 | this.anchorPoints.push(newAnchor); 399 | } 400 | this.updateAnchorPairs(); 401 | return newAnchor; 402 | } 403 | removeAnchorPoint({ 404 | point, 405 | index 406 | }) { 407 | if (!point && index === void 0) { 408 | throw new Error("Must provide a point or index"); 409 | } 410 | if (this.anchorPoints.length < 3) { 411 | throw new Error("Must have at least two anchor points"); 412 | } 413 | let apid; 414 | if (index !== void 0) { 415 | apid = index; 416 | } else if (point) { 417 | apid = this.anchorPoints.indexOf(point); 418 | } 419 | if (apid > -1 && apid < this.anchorPoints.length) { 420 | this.anchorPoints.splice(apid, 1); 421 | this.updateAnchorPairs(); 422 | } else { 423 | throw new Error("Point not found"); 424 | } 425 | } 426 | updateAnchorPoint({ 427 | point, 428 | pointIndex, 429 | xyz, 430 | color 431 | }) { 432 | if (pointIndex) { 433 | point = this.anchorPoints[pointIndex]; 434 | } 435 | if (!point) { 436 | throw new Error("Must provide a point or pointIndex"); 437 | } 438 | if (!xyz && !color) { 439 | throw new Error("Must provide a new xyz position or color"); 440 | } 441 | if (xyz) 442 | point.position = xyz; 443 | if (color) 444 | point.hsl = color; 445 | this.updateAnchorPairs(); 446 | return point; 447 | } 448 | getClosestAnchorPoint({ 449 | xyz, 450 | hsl, 451 | maxDistance = 1 452 | }) { 453 | if (!xyz && !hsl) { 454 | throw new Error("Must provide a xyz or hsl"); 455 | } 456 | let distances; 457 | if (xyz) { 458 | distances = this.anchorPoints.map( 459 | (anchor) => distance(anchor.position, xyz) 460 | ); 461 | } else if (hsl) { 462 | distances = this.anchorPoints.map( 463 | (anchor) => distance(anchor.hsl, hsl, true) 464 | ); 465 | } 466 | const minDistance = Math.min(...distances); 467 | if (minDistance > maxDistance) { 468 | return null; 469 | } 470 | const closestAnchorIndex = distances.indexOf(minDistance); 471 | return this.anchorPoints[closestAnchorIndex] || null; 472 | } 473 | set closedLoop(newStatus) { 474 | this.connectLastAndFirstAnchor = newStatus; 475 | this.updateAnchorPairs(); 476 | } 477 | get closedLoop() { 478 | return this.connectLastAndFirstAnchor; 479 | } 480 | set invertedLightness(newStatus) { 481 | this._invertedLightness = newStatus; 482 | this.updateAnchorPairs(); 483 | } 484 | get invertedLightness() { 485 | return this._invertedLightness; 486 | } 487 | /** 488 | * Returns a flattened array of all points across all segments, 489 | * removing duplicated anchor points at segment boundaries. 490 | * 491 | * Since anchor points exist at both the end of one segment and 492 | * the beginning of the next, this method keeps only one instance of each. 493 | * The filter logic keeps the first point (index 0) and then filters out 494 | * points whose indices are multiples of the segment size (_numPoints), 495 | * which are the anchor points at the start of each segment (except the first). 496 | * 497 | * This approach ensures we get all unique points in the correct order 498 | * while avoiding duplicated anchor points. 499 | * 500 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 501 | */ 502 | get flattenedPoints() { 503 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 504 | } 505 | get colors() { 506 | const colors = this.flattenedPoints.map((p) => p.color); 507 | if (this.connectLastAndFirstAnchor) { 508 | colors.pop(); 509 | } 510 | return colors; 511 | } 512 | cssColors(mode = "hsl") { 513 | const methods = { 514 | hsl: (p) => p.hslCSS, 515 | oklch: (p) => p.oklchCSS, 516 | lch: (p) => p.lchCSS 517 | }; 518 | const cssColors = this.flattenedPoints.map(methods[mode]); 519 | if (this.connectLastAndFirstAnchor) { 520 | cssColors.pop(); 521 | } 522 | return cssColors; 523 | } 524 | get colorsCSS() { 525 | return this.cssColors("hsl"); 526 | } 527 | get colorsCSSlch() { 528 | return this.cssColors("lch"); 529 | } 530 | get colorsCSSoklch() { 531 | return this.cssColors("oklch"); 532 | } 533 | shiftHue(hShift = 20) { 534 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 535 | this.updateAnchorPairs(); 536 | } 537 | }; 538 | var { p5 } = globalThis; 539 | if (p5) { 540 | console.info("p5 detected, adding poline to p5 prototype"); 541 | const poline = new Poline(); 542 | p5.prototype.poline = poline; 543 | const polineColors = () => poline.colors.map( 544 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 545 | ); 546 | p5.prototype.polineColors = polineColors; 547 | p5.prototype.registerMethod("polineColors", p5.prototype.polineColors); 548 | globalThis.poline = poline; 549 | globalThis.polineColors = polineColors; 550 | } 551 | return __toCommonJS(src_exports); 552 | })(); 553 | return poline; })); 554 | -------------------------------------------------------------------------------- /dist/p5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /dist/poline-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meodai/poline/f322bf66e9a4d74592f10d56004e69168312ccc6/dist/poline-logo.png -------------------------------------------------------------------------------- /dist/poline-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meodai/poline/f322bf66e9a4d74592f10d56004e69168312ccc6/dist/poline-wheel.png -------------------------------------------------------------------------------- /dist/socialfb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meodai/poline/f322bf66e9a4d74592f10d56004e69168312ccc6/dist/socialfb.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poline", 3 | "version": "0.7.0", 4 | "description": "color palette generator mico-lib", 5 | "type": "module", 6 | "main": "./dist/index.cjs", 7 | "module": "./dist/index.min.mjs", 8 | "browser": "./dist/index.min.js", 9 | "jsdelivr": "./dist/index.umd.js", 10 | "exports": { 11 | "require": "./dist/index.cjs", 12 | "import": "./dist/index.mjs", 13 | "types": "./dist/index.d.ts" 14 | }, 15 | "types": "dist/index.d.ts", 16 | "scripts": { 17 | "build": "npm run lint && tsc --build && node ./build.js", 18 | "test": "npm run lint && echo \"Error: no test specified\" && exit 1", 19 | "lint": "eslint . --ext .ts && npx prettier --check ./src/", 20 | "prettier": "npx prettier --write ./src/", 21 | "bsc": "browser-sync start --server 'dist' --files 'dist'", 22 | "watch": "esbuild ./src/index.ts --bundle --sourcemap --outfile=./dist/index.mjs --format=esm --watch", 23 | "dev": "npm-run-all --parallel watch bsc" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/meodai/poline.git" 28 | }, 29 | "keywords": [ 30 | "color", 31 | "generative-art", 32 | "colour", 33 | "palette-generation", 34 | "generative" 35 | ], 36 | "author": "meodai@gmail.com", 37 | "license": "MIT", 38 | "bugs": { 39 | "url": "https://github.com/meodai/poline/issues" 40 | }, 41 | "homepage": "https://github.com/meodai/poline#readme", 42 | "devDependencies": { 43 | "@typescript-eslint/eslint-plugin": "^5.48.0", 44 | "@typescript-eslint/parser": "^5.48.0", 45 | "browser-sync": "^2.27.11", 46 | "esbuild": "^0.16.14", 47 | "eslint": "^8.31.0", 48 | "npm-run-all": "^4.1.5", 49 | "prettier": "^2.8.1", 50 | "typescript": "^4.9.4" 51 | } 52 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 2 | export type FuncNumberReturn = (arg0: number) => Vector2; 3 | export type Vector2 = [number, number]; 4 | export type Vector3 = [number, ...Vector2]; 5 | export type PartialVector3 = [number | null, number | null, number | null]; 6 | 7 | type CSSColorMethods = { 8 | hsl: (p: ColorPoint) => string; 9 | oklch: (p: ColorPoint) => string; 10 | lch: (p: ColorPoint) => string; 11 | }; 12 | 13 | /** 14 | * Converts the given (x, y, z) coordinate to an HSL color 15 | * The (x, y) values are used to calculate the hue, while the z value is used as the saturation 16 | * The lightness value is calculated based on the distance of (x, y) from the center (0.5, 0.5) 17 | * Returns an array [hue, saturation, lightness] 18 | * @param xyz:Vector3 [x, y, z] coordinate array in (x, y, z) format (0-1, 0-1, 0-1) 19 | * @returns [hue, saturation, lightness]: Vector3 color array in HSL format (0-360, 0-1, 0-1) 20 | * @example 21 | * pointToHSL([0.5, 0.5, 1]) // [0, 1, 0.5] 22 | * pointToHSL([0.5, 0.5, 0]) // [0, 1, 0] 23 | **/ 24 | 25 | export const pointToHSL = ( 26 | xyz: Vector3, 27 | invertedLightness: boolean 28 | ): Vector3 => { 29 | const [x, y, z] = xyz; 30 | 31 | // cy and cx are the center (y and x) values 32 | const cx = 0.5; 33 | const cy = 0.5; 34 | 35 | // Calculate the angle between the point (x, y) and the center (cx, cy) 36 | const radians = Math.atan2(y - cy, x - cx); 37 | 38 | // Convert the angle to degrees and shift it so that it goes from 0 to 360 39 | let deg = radians * (180 / Math.PI); 40 | deg = (360 + deg) % 360; 41 | 42 | // The saturation value is taken from the z coordinate 43 | const s = z; 44 | 45 | // Calculate the lightness value based on the distance from the center 46 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 47 | const l = dist / cx; 48 | 49 | // Return the HSL color as an array [hue, saturation, lightness] 50 | return [deg, s, invertedLightness ? 1 - l : l]; 51 | }; 52 | 53 | /** 54 | * Converts the given HSL color to an (x, y, z) coordinate 55 | * The hue value is used to calculate the (x, y) position, while the saturation value is used as the z coordinate 56 | * The lightness value is used to calculate the distance from the center (0.5, 0.5) 57 | * Returns an array [x, y, z] 58 | * @param hsl:Vector3 [hue, saturation, lightness] color array in HSL format (0-360, 0-1, 0-1) 59 | * @returns [x, y, z]:Vector3 coordinate array in (x, y, z) format (0-1, 0-1, 0-1) 60 | * @example 61 | * hslToPoint([0, 1, 0.5]) // [0.5, 0.5, 1] 62 | * hslToPoint([0, 1, 0]) // [0.5, 0.5, 1] 63 | * hslToPoint([0, 1, 1]) // [0.5, 0.5, 1] 64 | * hslToPoint([0, 0, 0.5]) // [0.5, 0.5, 0] 65 | **/ 66 | export const hslToPoint = ( 67 | hsl: Vector3, 68 | invertedLightness: boolean 69 | ): Vector3 => { 70 | // Destructure the input array into separate hue, saturation, and lightness values 71 | const [h, s, l] = hsl; 72 | // cx and cy are the center (x and y) values 73 | const cx = 0.5; 74 | const cy = 0.5; 75 | // Calculate the angle in radians based on the hue value 76 | const radians = h / (180 / Math.PI); 77 | 78 | // Calculate the distance from the center based on the lightness value 79 | const dist = (invertedLightness ? 1 - l : l) * cx; 80 | 81 | // Calculate the x and y coordinates based on the distance and angle 82 | const x = cx + dist * Math.cos(radians); 83 | const y = cy + dist * Math.sin(radians); 84 | // The z coordinate is equal to the saturation value 85 | const z = s; 86 | // Return the (x, y, z) coordinate as an array [x, y, z] 87 | return [x, y, z]; 88 | }; 89 | 90 | export const randomHSLPair = ( 91 | startHue: number = Math.random() * 360, 92 | saturations: Vector2 = [Math.random(), Math.random()], 93 | lightnesses: Vector2 = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2] 94 | ): [Vector3, Vector3] => [ 95 | [startHue, saturations[0], lightnesses[0]], 96 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 97 | ]; 98 | 99 | export const randomHSLTriple = ( 100 | startHue: number = Math.random() * 360, 101 | saturations: Vector3 = [Math.random(), Math.random(), Math.random()], 102 | lightnesses: Vector3 = [ 103 | 0.75 + Math.random() * 0.2, 104 | Math.random() * 0.2, 105 | 0.75 + Math.random() * 0.2, 106 | ] 107 | ): [Vector3, Vector3, Vector3] => [ 108 | [startHue, saturations[0], lightnesses[0]], 109 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 110 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]], 111 | ]; 112 | 113 | const vectorOnLine = ( 114 | t: number, 115 | p1: Vector3, 116 | p2: Vector3, 117 | invert = false, 118 | fx = (t: number, invert: boolean): number => (invert ? 1 - t : t), 119 | fy = (t: number, invert: boolean): number => (invert ? 1 - t : t), 120 | fz = (t: number, invert: boolean): number => (invert ? 1 - t : t) 121 | ): Vector3 => { 122 | const tModifiedX = fx(t, invert); 123 | const tModifiedY = fy(t, invert); 124 | const tModifiedZ = fz(t, invert); 125 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 126 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 127 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 128 | 129 | return [x, y, z]; 130 | }; 131 | 132 | const vectorsOnLine = ( 133 | p1: Vector3, 134 | p2: Vector3, 135 | numPoints = 4, 136 | invert = false, 137 | fx = (t: number, invert: boolean): number => (invert ? 1 - t : t), 138 | fy = (t: number, invert: boolean): number => (invert ? 1 - t : t), 139 | fz = (t: number, invert: boolean): number => (invert ? 1 - t : t) 140 | ): Vector3[] => { 141 | const points: Vector3[] = []; 142 | 143 | for (let i = 0; i < numPoints; i++) { 144 | const [x, y, z] = vectorOnLine( 145 | i / (numPoints - 1), 146 | p1, 147 | p2, 148 | invert, 149 | fx, 150 | fy, 151 | fz 152 | ); 153 | points.push([x, y, z]); 154 | } 155 | 156 | return points; 157 | }; 158 | 159 | export type PositionFunction = (t: number, reverse?: boolean) => number; 160 | 161 | const linearPosition: PositionFunction = (t: number) => { 162 | return t; 163 | }; 164 | 165 | const exponentialPosition: PositionFunction = (t: number, reverse = false) => { 166 | if (reverse) { 167 | return 1 - (1 - t) ** 2; 168 | } 169 | return t ** 2; 170 | }; 171 | 172 | const quadraticPosition: PositionFunction = (t: number, reverse = false) => { 173 | if (reverse) { 174 | return 1 - (1 - t) ** 3; 175 | } 176 | return t ** 3; 177 | }; 178 | 179 | const cubicPosition: PositionFunction = (t: number, reverse = false) => { 180 | if (reverse) { 181 | return 1 - (1 - t) ** 4; 182 | } 183 | return t ** 4; 184 | }; 185 | 186 | const quarticPosition: PositionFunction = (t: number, reverse = false) => { 187 | if (reverse) { 188 | return 1 - (1 - t) ** 5; 189 | } 190 | return t ** 5; 191 | }; 192 | 193 | const sinusoidalPosition: PositionFunction = (t: number, reverse = false) => { 194 | if (reverse) { 195 | return 1 - Math.sin(((1 - t) * Math.PI) / 2); 196 | } 197 | return Math.sin((t * Math.PI) / 2); 198 | }; 199 | 200 | const asinusoidalPosition: PositionFunction = (t: number, reverse = false) => { 201 | if (reverse) { 202 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 203 | } 204 | return Math.asin(t) / (Math.PI / 2); 205 | }; 206 | 207 | const arcPosition: PositionFunction = (t: number, reverse = false) => { 208 | if (reverse) { 209 | return 1 - Math.sqrt(1 - t ** 2); 210 | } 211 | return 1 - Math.sqrt(1 - t); 212 | }; 213 | 214 | const smoothStepPosition: PositionFunction = (t: number) => { 215 | return t ** 2 * (3 - 2 * t); 216 | }; 217 | 218 | export const positionFunctions = { 219 | linearPosition, 220 | exponentialPosition, 221 | quadraticPosition, 222 | cubicPosition, 223 | quarticPosition, 224 | sinusoidalPosition, 225 | asinusoidalPosition, 226 | arcPosition, 227 | smoothStepPosition, 228 | }; 229 | 230 | /** 231 | * Calculates the distance between two points 232 | * @param p1 The first point 233 | * @param p2 The second point 234 | * @param hueMode Whether to use the hue distance function 235 | * @returns The distance between the two points 236 | * @example 237 | * const p1 = [0, 0, 0]; 238 | * const p2 = [1, 1, 1]; 239 | * const dist = distance(p1, p2); 240 | * console.log(dist); // 1.7320508075688772 241 | **/ 242 | const distance = ( 243 | p1: PartialVector3, 244 | p2: PartialVector3, 245 | hueMode = false 246 | ): number => { 247 | const a1 = p1[0]; 248 | const a2 = p2[0]; 249 | let diffA = 0; 250 | 251 | if (hueMode && a1 !== null && a2 !== null) { 252 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 253 | diffA = diffA / 360; 254 | } else { 255 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 256 | } 257 | 258 | const a = diffA; 259 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 260 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 261 | 262 | return Math.sqrt(a * a + b * b + c * c); 263 | }; 264 | 265 | export type ColorPointCollection = { 266 | xyz?: Vector3; 267 | color?: Vector3; 268 | invertedLightness?: boolean; 269 | }; 270 | 271 | class ColorPoint { 272 | public x = 0; 273 | public y = 0; 274 | public z = 0; 275 | public color: Vector3 = [0, 0, 0]; 276 | private _invertedLightness = false; 277 | 278 | constructor({ 279 | xyz, 280 | color, 281 | invertedLightness = false, 282 | }: ColorPointCollection = {}) { 283 | this._invertedLightness = invertedLightness; 284 | this.positionOrColor({ xyz, color, invertedLightness }); 285 | } 286 | 287 | positionOrColor({ 288 | xyz, 289 | color, 290 | invertedLightness = false, 291 | }: ColorPointCollection) { 292 | if ((xyz && color) || (!xyz && !color)) { 293 | throw new Error("Point must be initialized with either x,y,z or hsl"); 294 | } else if (xyz) { 295 | this.x = xyz[0]; 296 | this.y = xyz[1]; 297 | this.z = xyz[2]; 298 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 299 | } else if (color) { 300 | this.color = color; 301 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 302 | } 303 | } 304 | 305 | set position([x, y, z]: Vector3) { 306 | this.x = x; 307 | this.y = y; 308 | this.z = z; 309 | this.color = pointToHSL( 310 | [this.x, this.y, this.z] as Vector3, 311 | this._invertedLightness 312 | ); 313 | } 314 | 315 | get position(): Vector3 { 316 | return [this.x, this.y, this.z]; 317 | } 318 | 319 | set hsl([h, s, l]: Vector3) { 320 | this.color = [h, s, l]; 321 | [this.x, this.y, this.z] = hslToPoint( 322 | this.color as Vector3, 323 | this._invertedLightness 324 | ); 325 | } 326 | 327 | get hsl(): Vector3 { 328 | return this.color; 329 | } 330 | 331 | get hslCSS(): string { 332 | const [h, s, l] = this.color; 333 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 334 | 2 335 | )}%)`; 336 | } 337 | 338 | get oklchCSS(): string { 339 | const [h, s, l] = this.color; 340 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 341 | 2 342 | )})`; 343 | } 344 | 345 | get lchCSS(): string { 346 | const [h, s, l] = this.color; 347 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 348 | 2 349 | )})`; 350 | } 351 | 352 | shiftHue(angle: number): void { 353 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 354 | [this.x, this.y, this.z] = hslToPoint( 355 | this.color as Vector3, 356 | this._invertedLightness 357 | ); 358 | } 359 | } 360 | 361 | export type PolineOptions = { 362 | anchorColors: Vector3[]; 363 | numPoints: number; 364 | positionFunction?: (t: number, invert?: boolean) => number; 365 | positionFunctionX?: (t: number, invert?: boolean) => number; 366 | positionFunctionY?: (t: number, invert?: boolean) => number; 367 | positionFunctionZ?: (t: number, invert?: boolean) => number; 368 | invertedLightness?: boolean; 369 | closedLoop?: boolean; 370 | }; 371 | export class Poline { 372 | private _needsUpdate = true; 373 | private _anchorPoints: ColorPoint[]; 374 | 375 | private _numPoints: number; 376 | private points: ColorPoint[][]; 377 | 378 | private _positionFunctionX = sinusoidalPosition; 379 | private _positionFunctionY = sinusoidalPosition; 380 | private _positionFunctionZ = sinusoidalPosition; 381 | 382 | private _anchorPairs: ColorPoint[][]; 383 | 384 | private connectLastAndFirstAnchor = false; 385 | 386 | private _animationFrame: null | number = null; 387 | 388 | private _invertedLightness = false; 389 | 390 | constructor( 391 | { 392 | anchorColors = randomHSLPair(), 393 | numPoints = 4, 394 | positionFunction = sinusoidalPosition, 395 | positionFunctionX, 396 | positionFunctionY, 397 | positionFunctionZ, 398 | closedLoop, 399 | invertedLightness, 400 | }: PolineOptions = { 401 | anchorColors: randomHSLPair(), 402 | numPoints: 4, 403 | positionFunction: sinusoidalPosition, 404 | closedLoop: false, 405 | } 406 | ) { 407 | if (!anchorColors || anchorColors.length < 2) { 408 | throw new Error("Must have at least two anchor colors"); 409 | } 410 | 411 | this._anchorPoints = anchorColors.map( 412 | (point) => new ColorPoint({ color: point, invertedLightness }) 413 | ); 414 | 415 | this._numPoints = numPoints + 2; // add two for the anchor points 416 | 417 | this._positionFunctionX = 418 | positionFunctionX || positionFunction || sinusoidalPosition; 419 | this._positionFunctionY = 420 | positionFunctionY || positionFunction || sinusoidalPosition; 421 | this._positionFunctionZ = 422 | positionFunctionZ || positionFunction || sinusoidalPosition; 423 | 424 | this.connectLastAndFirstAnchor = closedLoop || false; 425 | 426 | this._invertedLightness = invertedLightness || false; 427 | 428 | this.updateAnchorPairs(); 429 | } 430 | 431 | public get numPoints(): number { 432 | return this._numPoints - 2; 433 | } 434 | 435 | public set numPoints(numPoints: number) { 436 | if (numPoints < 1) { 437 | throw new Error("Must have at least one point"); 438 | } 439 | this._numPoints = numPoints + 2; // add two for the anchor points 440 | this.updateAnchorPairs(); 441 | } 442 | 443 | public set positionFunction( 444 | positionFunction: PositionFunction | PositionFunction[] 445 | ) { 446 | if (Array.isArray(positionFunction)) { 447 | if (positionFunction.length !== 3) { 448 | throw new Error("Position function array must have 3 elements"); 449 | } 450 | if ( 451 | typeof positionFunction[0] !== "function" || 452 | typeof positionFunction[1] !== "function" || 453 | typeof positionFunction[2] !== "function" 454 | ) { 455 | throw new Error("Position function array must have 3 functions"); 456 | } 457 | this._positionFunctionX = positionFunction[0]; 458 | this._positionFunctionY = positionFunction[1]; 459 | this._positionFunctionZ = positionFunction[2]; 460 | } else { 461 | this._positionFunctionX = positionFunction; 462 | this._positionFunctionY = positionFunction; 463 | this._positionFunctionZ = positionFunction; 464 | } 465 | 466 | this.updateAnchorPairs(); 467 | } 468 | 469 | public get positionFunction(): PositionFunction | PositionFunction[] { 470 | // not to sure what to do here, because the position function is a combination of the three 471 | if ( 472 | this._positionFunctionX === this._positionFunctionY && 473 | this._positionFunctionX === this._positionFunctionZ 474 | ) { 475 | return this._positionFunctionX; 476 | } 477 | 478 | return [ 479 | this._positionFunctionX, 480 | this._positionFunctionY, 481 | this._positionFunctionZ, 482 | ]; 483 | } 484 | 485 | public set positionFunctionX(positionFunctionX: PositionFunction) { 486 | this._positionFunctionX = positionFunctionX; 487 | this.updateAnchorPairs(); 488 | } 489 | 490 | public get positionFunctionX(): PositionFunction { 491 | return this._positionFunctionX; 492 | } 493 | 494 | public set positionFunctionY(positionFunctionY: PositionFunction) { 495 | this._positionFunctionY = positionFunctionY; 496 | this.updateAnchorPairs(); 497 | } 498 | 499 | public get positionFunctionY(): PositionFunction { 500 | return this._positionFunctionY; 501 | } 502 | 503 | public set positionFunctionZ(positionFunctionZ: PositionFunction) { 504 | this._positionFunctionZ = positionFunctionZ; 505 | this.updateAnchorPairs(); 506 | } 507 | 508 | public get positionFunctionZ(): PositionFunction { 509 | return this._positionFunctionZ; 510 | } 511 | 512 | public get anchorPoints(): ColorPoint[] { 513 | return this._anchorPoints; 514 | } 515 | 516 | public set anchorPoints(anchorPoints: ColorPoint[]) { 517 | this._anchorPoints = anchorPoints; 518 | this.updateAnchorPairs(); 519 | } 520 | 521 | public updateAnchorPairs(): void { 522 | this._anchorPairs = [] as ColorPoint[][]; 523 | 524 | const anchorPointsLength = this.connectLastAndFirstAnchor 525 | ? this.anchorPoints.length 526 | : this.anchorPoints.length - 1; 527 | 528 | for (let i = 0; i < anchorPointsLength; i++) { 529 | const pair = [ 530 | this.anchorPoints[i], 531 | this.anchorPoints[(i + 1) % this.anchorPoints.length], 532 | ] as ColorPoint[]; 533 | 534 | this._anchorPairs.push(pair); 535 | } 536 | 537 | this.points = this._anchorPairs.map((pair, i) => { 538 | const p1position = pair[0] ? pair[0].position : ([0, 0, 0] as Vector3); 539 | const p2position = pair[1] ? pair[1].position : ([0, 0, 0] as Vector3); 540 | 541 | return vectorsOnLine( 542 | p1position, 543 | p2position, 544 | this._numPoints, 545 | i % 2 ? true : false, 546 | this.positionFunctionX, 547 | this.positionFunctionY, 548 | this.positionFunctionZ 549 | ).map( 550 | (p) => 551 | new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 552 | ); 553 | }); 554 | } 555 | 556 | public addAnchorPoint({ 557 | xyz, 558 | color, 559 | insertAtIndex, 560 | }: ColorPointCollection & { insertAtIndex: number }): ColorPoint { 561 | const newAnchor = new ColorPoint({ 562 | xyz, 563 | color, 564 | invertedLightness: this._invertedLightness, 565 | }); 566 | if (insertAtIndex) { 567 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 568 | } else { 569 | this.anchorPoints.push(newAnchor); 570 | } 571 | this.updateAnchorPairs(); 572 | return newAnchor; 573 | } 574 | 575 | public removeAnchorPoint({ 576 | point, 577 | index, 578 | }: { 579 | point?: ColorPoint; 580 | index?: number; 581 | }): void { 582 | if (!point && index === undefined) { 583 | throw new Error("Must provide a point or index"); 584 | } 585 | 586 | if (this.anchorPoints.length < 3) { 587 | throw new Error("Must have at least two anchor points"); 588 | } 589 | 590 | let apid; 591 | 592 | if (index !== undefined) { 593 | apid = index; 594 | } else if (point) { 595 | apid = this.anchorPoints.indexOf(point); 596 | } 597 | 598 | if (apid > -1 && apid < this.anchorPoints.length) { 599 | this.anchorPoints.splice(apid, 1); 600 | this.updateAnchorPairs(); 601 | } else { 602 | throw new Error("Point not found"); 603 | } 604 | } 605 | 606 | public updateAnchorPoint({ 607 | point, 608 | pointIndex, 609 | xyz, 610 | color, 611 | }: { 612 | point?: ColorPoint; 613 | pointIndex?: number; 614 | } & ColorPointCollection): ColorPoint { 615 | if (pointIndex) { 616 | point = this.anchorPoints[pointIndex]; 617 | } 618 | 619 | if (!point) { 620 | throw new Error("Must provide a point or pointIndex"); 621 | } 622 | 623 | if (!xyz && !color) { 624 | throw new Error("Must provide a new xyz position or color"); 625 | } 626 | 627 | if (xyz) point.position = xyz; 628 | if (color) point.hsl = color; 629 | 630 | this.updateAnchorPairs(); 631 | 632 | return point; 633 | } 634 | 635 | public getClosestAnchorPoint({ 636 | xyz, 637 | hsl, 638 | maxDistance = 1, 639 | }: { 640 | xyz?: PartialVector3; 641 | hsl?: PartialVector3; 642 | maxDistance?: number; 643 | }): ColorPoint | null { 644 | if (!xyz && !hsl) { 645 | throw new Error("Must provide a xyz or hsl"); 646 | } 647 | 648 | let distances; 649 | 650 | if (xyz) { 651 | distances = this.anchorPoints.map((anchor) => 652 | distance(anchor.position, xyz) 653 | ); 654 | } else if (hsl) { 655 | distances = this.anchorPoints.map((anchor) => 656 | distance(anchor.hsl, hsl, true) 657 | ); 658 | } 659 | 660 | const minDistance = Math.min(...distances); 661 | 662 | if (minDistance > maxDistance) { 663 | return null; 664 | } 665 | 666 | const closestAnchorIndex = distances.indexOf(minDistance); 667 | 668 | return this.anchorPoints[closestAnchorIndex] || null; 669 | } 670 | 671 | public set closedLoop(newStatus: boolean) { 672 | this.connectLastAndFirstAnchor = newStatus; 673 | this.updateAnchorPairs(); 674 | } 675 | 676 | public get closedLoop(): boolean { 677 | return this.connectLastAndFirstAnchor; 678 | } 679 | 680 | public set invertedLightness(newStatus: boolean) { 681 | this._invertedLightness = newStatus; 682 | this.updateAnchorPairs(); 683 | } 684 | 685 | public get invertedLightness(): boolean { 686 | return this._invertedLightness; 687 | } 688 | 689 | /** 690 | * Returns a flattened array of all points across all segments, 691 | * removing duplicated anchor points at segment boundaries. 692 | * 693 | * Since anchor points exist at both the end of one segment and 694 | * the beginning of the next, this method keeps only one instance of each. 695 | * The filter logic keeps the first point (index 0) and then filters out 696 | * points whose indices are multiples of the segment size (_numPoints), 697 | * which are the anchor points at the start of each segment (except the first). 698 | * 699 | * This approach ensures we get all unique points in the correct order 700 | * while avoiding duplicated anchor points. 701 | * 702 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 703 | */ 704 | public get flattenedPoints() { 705 | return this.points 706 | .flat() 707 | .filter((p, i) => (i != 0 ? i % this._numPoints : true)); 708 | } 709 | 710 | public get colors() { 711 | const colors = this.flattenedPoints.map((p) => p.color); 712 | if (this.connectLastAndFirstAnchor) { 713 | colors.pop(); 714 | } 715 | return colors; 716 | } 717 | 718 | public cssColors(mode: "hsl" | "oklch" | "lch" = "hsl"): string[] { 719 | const methods: CSSColorMethods = { 720 | hsl: (p: ColorPoint): string => p.hslCSS, 721 | oklch: (p: ColorPoint): string => p.oklchCSS, 722 | lch: (p: ColorPoint): string => p.lchCSS, 723 | }; 724 | const cssColors = this.flattenedPoints.map(methods[mode]); 725 | if (this.connectLastAndFirstAnchor) { 726 | cssColors.pop(); 727 | } 728 | return cssColors; 729 | } 730 | 731 | public get colorsCSS() { 732 | return this.cssColors("hsl"); 733 | } 734 | 735 | public get colorsCSSlch() { 736 | return this.cssColors("lch"); 737 | } 738 | 739 | public get colorsCSSoklch() { 740 | return this.cssColors("oklch"); 741 | } 742 | 743 | public shiftHue(hShift = 20): void { 744 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 745 | this.updateAnchorPairs(); 746 | } 747 | } 748 | 749 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 750 | const { p5 } = globalThis as any; 751 | 752 | if (p5) { 753 | console.info("p5 detected, adding poline to p5 prototype"); 754 | 755 | const poline = new Poline(); 756 | p5.prototype.poline = poline; 757 | 758 | const polineColors = () => 759 | poline.colors.map( 760 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 761 | ); 762 | p5.prototype.polineColors = polineColors; 763 | p5.prototype.registerMethod("polineColors", p5.prototype.polineColors); 764 | 765 | globalThis.poline = poline; 766 | globalThis.polineColors = polineColors; 767 | } 768 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | --- 3 | version: 1.0.0 4 | codeOwners: 5 | - '0xFA64435d1281921E36b90CeA9a1fbf0e5c408e65' 6 | quorum: 1 7 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noEmitOnError": true, 5 | "removeComments": false, 6 | "sourceMap": true, 7 | "target": "es2015", 8 | "outDir": "dist", 9 | "declaration": true, 10 | "emitDeclarationOnly": true, 11 | "noUncheckedIndexedAccess": true, 12 | "strict": true, 13 | "strictPropertyInitialization": false 14 | }, 15 | "include": ["src/**/*", "demo-src"] 16 | } --------------------------------------------------------------------------------