├── .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 |

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 | 
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 | }
--------------------------------------------------------------------------------