├── .prettierrc.json ├── dist ├── socialfb.png ├── poline-logo.png ├── poline-wheel.png ├── p5.html ├── webcomponent.d.ts ├── index.d.ts ├── index.min.mjs ├── index.min.cjs ├── index.min.js ├── picker.html ├── picker.min.mjs ├── picker.min.cjs ├── picker.min.js ├── index.mjs ├── index.cjs ├── index.js └── index.umd.js ├── .github ├── FUNDING.yml └── workflows │ └── static.yml ├── .eslintrc ├── tsconfig.json ├── tea.yaml ├── LICENSE ├── package.json └── .gitignore /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /dist/socialfb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meodai/poline/HEAD/dist/socialfb.png -------------------------------------------------------------------------------- /dist/poline-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meodai/poline/HEAD/dist/poline-logo.png -------------------------------------------------------------------------------- /dist/poline-wheel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meodai/poline/HEAD/dist/poline-wheel.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [meodai] 4 | ko_fi: colorparrot 5 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "noImplicitAny": false, 4 | "noEmitOnError": true, 5 | "removeComments": false, 6 | "sourceMap": true, 7 | "target": "es2016", 8 | "outDir": "dist", 9 | "declaration": true, 10 | "emitDeclarationOnly": true, 11 | "noUncheckedIndexedAccess": true, 12 | "strict": true, 13 | "strictPropertyInitialization": false, 14 | "skipLibCheck": true 15 | }, 16 | "include": ["src/**/*", "demo-src"] 17 | } -------------------------------------------------------------------------------- /dist/p5.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 24 | 25 | -------------------------------------------------------------------------------- /tea.yaml: -------------------------------------------------------------------------------- 1 | # https://tea.xyz/what-is-this-file 2 | # 3 | # DO NOT REMOVE OR EDIT THIS WARNING: 4 | # 5 | # This file is auto-generated by the TEA app. It is intended to validate ownership of your repository. 6 | # DO NOT commit this file or accept any PR if you don't know what this is. 7 | # We are aware that spammers will try to use this file to try to profit off others' work. 8 | # We take this very seriously and will take action against any malicious actors. 9 | # 10 | # If you are not the owner of this repository, and someone maliciously opens a commit with this file 11 | # please report it to us at support@tea.xyz. 12 | # 13 | # A constitution without this header is invalid. 14 | --- 15 | version: 2.0.0 16 | codeOwners: 17 | - '0x2220923A4190a1A2FEE4545Ad86c15998c58C15B' 18 | quorum: 1 19 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /dist/webcomponent.d.ts: -------------------------------------------------------------------------------- 1 | import { Poline, positionFunctions, ColorPoint } from "./index"; 2 | export { Poline, positionFunctions }; 3 | export declare class PolinePicker extends HTMLElement { 4 | private poline; 5 | private svg; 6 | private interactive; 7 | private line; 8 | private wheel; 9 | private anchors; 10 | private points; 11 | private saturationRings; 12 | private currentPoint; 13 | private allowAddPoints; 14 | private ringAdjust; 15 | private ringHoverIndex; 16 | private boundPointerDown; 17 | private boundPointerMove; 18 | private boundPointerUp; 19 | constructor(); 20 | connectedCallback(): void; 21 | disconnectedCallback(): void; 22 | setPoline(poline: Poline): void; 23 | setAllowAddPoints(allow: boolean): void; 24 | addPointAtPosition(x: number, y: number): ColorPoint | null; 25 | private updateLightnessBackground; 26 | private render; 27 | private createSVG; 28 | updateSVG(): void; 29 | private updateSaturationRings; 30 | private describeArc; 31 | private pointToCartesian; 32 | private addEventListeners; 33 | private removeEventListeners; 34 | private handlePointerDown; 35 | private handlePointerMove; 36 | private handlePointerUp; 37 | private pickRing; 38 | private clamp01; 39 | private pointerToNormalizedCoordinates; 40 | private dispatchPolineChange; 41 | } 42 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "poline", 3 | "version": "0.13.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 | ".": { 12 | "require": "./dist/index.cjs", 13 | "import": "./dist/index.mjs", 14 | "types": "./dist/index.d.ts" 15 | }, 16 | "./picker": { 17 | "require": "./dist/picker.cjs", 18 | "import": "./dist/picker.mjs", 19 | "types": "./dist/webcomponent.d.ts" 20 | } 21 | }, 22 | "types": "dist/index.d.ts", 23 | "scripts": { 24 | "build": "npm test && npm run lint && tsc --build && node ./build.js", 25 | "test": "vitest run", 26 | "lint": "eslint . --ext .ts && npx prettier --check ./src/", 27 | "prettier": "npx prettier --write ./src/", 28 | "bsc": "browser-sync start --server 'dist' --files 'dist'", 29 | "watch": "esbuild ./src/index.ts --bundle --sourcemap --outfile=./dist/index.mjs --format=esm --watch", 30 | "dev": "npm-run-all --parallel watch bsc" 31 | }, 32 | "repository": { 33 | "type": "git", 34 | "url": "git+https://github.com/meodai/poline.git" 35 | }, 36 | "keywords": [ 37 | "color", 38 | "generative-art", 39 | "colour", 40 | "palette-generation", 41 | "generative" 42 | ], 43 | "author": "meodai@gmail.com", 44 | "license": "MIT", 45 | "bugs": { 46 | "url": "https://github.com/meodai/poline/issues" 47 | }, 48 | "homepage": "https://github.com/meodai/poline#readme", 49 | "devDependencies": { 50 | "@typescript-eslint/eslint-plugin": "^5.48.0", 51 | "@typescript-eslint/parser": "^5.48.0", 52 | "browser-sync": "^2.27.11", 53 | "esbuild": "^0.16.14", 54 | "eslint": "^8.31.0", 55 | "npm-run-all": "^4.1.5", 56 | "prettier": "^2.8.1", 57 | "typescript": "^4.9.4", 58 | "vitest": "^3.2.4" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /.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 -------------------------------------------------------------------------------- /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 | /** 33 | * Clamps an (x, y) position to be within the color wheel circle 34 | * The circle has radius 0.5 centered at (0.5, 0.5) 35 | * If the point is outside the circle, it projects it to the edge 36 | * @param x The x coordinate (0-1) 37 | * @param y The y coordinate (0-1) 38 | * @returns [x, y] clamped to be within the circle 39 | * @example 40 | * clampToCircle(0.5, 0.5) // [0.5, 0.5] - center, unchanged 41 | * clampToCircle(1, 0.5) // [1, 0.5] - edge, unchanged 42 | * clampToCircle(1.5, 0.5) // [1, 0.5] - outside, clamped to edge 43 | */ 44 | export declare const clampToCircle: (x: number, y: number) => Vector2; 45 | export declare const randomHSLTriple: (startHue?: number, saturations?: [number, number, number], lightnesses?: [number, number, number]) => [Vector3, Vector3, Vector3]; 46 | export type PositionFunction = (t: number, reverse?: boolean) => number; 47 | export declare const positionFunctions: { 48 | linearPosition: PositionFunction; 49 | exponentialPosition: PositionFunction; 50 | quadraticPosition: PositionFunction; 51 | cubicPosition: PositionFunction; 52 | quarticPosition: PositionFunction; 53 | sinusoidalPosition: PositionFunction; 54 | asinusoidalPosition: PositionFunction; 55 | arcPosition: PositionFunction; 56 | smoothStepPosition: PositionFunction; 57 | }; 58 | export type ColorPointCollection = { 59 | xyz?: Vector3; 60 | color?: Vector3; 61 | invertedLightness?: boolean; 62 | }; 63 | export declare class ColorPoint { 64 | x: number; 65 | y: number; 66 | z: number; 67 | color: Vector3; 68 | private _invertedLightness; 69 | constructor({ xyz, color, invertedLightness, }?: ColorPointCollection); 70 | positionOrColor({ xyz, color, invertedLightness, }: ColorPointCollection): void; 71 | set position([x, y, z]: Vector3); 72 | get position(): Vector3; 73 | set hsl([h, s, l]: Vector3); 74 | get hsl(): Vector3; 75 | get hslCSS(): string; 76 | get oklchCSS(): string; 77 | get lchCSS(): string; 78 | set invertedLightness(val: boolean); 79 | get invertedLightness(): boolean; 80 | shiftHue(angle: number): void; 81 | } 82 | export type PolineOptions = { 83 | anchorColors?: Vector3[]; 84 | numPoints?: number; 85 | positionFunction?: (t: number, invert?: boolean) => number; 86 | positionFunctionX?: (t: number, invert?: boolean) => number; 87 | positionFunctionY?: (t: number, invert?: boolean) => number; 88 | positionFunctionZ?: (t: number, invert?: boolean) => number; 89 | invertedLightness?: boolean; 90 | closedLoop?: boolean; 91 | clampToCircle?: boolean; 92 | }; 93 | export declare class Poline { 94 | private _anchorPoints; 95 | private _numPoints; 96 | private points; 97 | private _positionFunctionX; 98 | private _positionFunctionY; 99 | private _positionFunctionZ; 100 | private _anchorPairs; 101 | private connectLastAndFirstAnchor; 102 | private _animationFrame; 103 | private _invertedLightness; 104 | private _clampToCircle; 105 | constructor({ anchorColors, numPoints, positionFunction, positionFunctionX, positionFunctionY, positionFunctionZ, closedLoop, invertedLightness, clampToCircle, }?: PolineOptions); 106 | get numPoints(): number; 107 | set numPoints(numPoints: number); 108 | set positionFunction(positionFunction: PositionFunction | PositionFunction[]); 109 | get positionFunction(): PositionFunction | PositionFunction[]; 110 | set positionFunctionX(positionFunctionX: PositionFunction); 111 | get positionFunctionX(): PositionFunction; 112 | set positionFunctionY(positionFunctionY: PositionFunction); 113 | get positionFunctionY(): PositionFunction; 114 | set positionFunctionZ(positionFunctionZ: PositionFunction); 115 | get positionFunctionZ(): PositionFunction; 116 | get clampToCircle(): boolean; 117 | set clampToCircle(clamp: boolean); 118 | get anchorPoints(): ColorPoint[]; 119 | set anchorPoints(anchorPoints: ColorPoint[]); 120 | updateAnchorPairs(): void; 121 | addAnchorPoint({ xyz, color, insertAtIndex, clamp, }: ColorPointCollection & { 122 | insertAtIndex?: number; 123 | clamp?: boolean; 124 | }): ColorPoint; 125 | removeAnchorPoint({ point, index, }: { 126 | point?: ColorPoint; 127 | index?: number; 128 | }): void; 129 | updateAnchorPoint({ point, pointIndex, xyz, color, clamp, }: { 130 | point?: ColorPoint; 131 | pointIndex?: number; 132 | clamp?: boolean; 133 | } & ColorPointCollection): ColorPoint; 134 | getClosestAnchorPoint({ xyz, hsl, maxDistance, }: { 135 | xyz?: PartialVector3; 136 | hsl?: PartialVector3; 137 | maxDistance?: number; 138 | }): ColorPoint | null; 139 | set closedLoop(newStatus: boolean); 140 | get closedLoop(): boolean; 141 | set invertedLightness(newStatus: boolean); 142 | get invertedLightness(): boolean; 143 | /** 144 | * Returns a flattened array of all points across all segments, 145 | * removing duplicated anchor points at segment boundaries. 146 | * 147 | * Since anchor points exist at both the end of one segment and 148 | * the beginning of the next, this method keeps only one instance of each. 149 | * The filter logic keeps the first point (index 0) and then filters out 150 | * points whose indices are multiples of the segment size (_numPoints), 151 | * which are the anchor points at the start of each segment (except the first). 152 | * 153 | * This approach ensures we get all unique points in the correct order 154 | * while avoiding duplicated anchor points. 155 | * 156 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 157 | */ 158 | get flattenedPoints(): ColorPoint[]; 159 | get colors(): [number, number, number][]; 160 | cssColors(mode?: "hsl" | "oklch" | "lch"): string[]; 161 | get colorsCSS(): string[]; 162 | get colorsCSSlch(): string[]; 163 | get colorsCSSoklch(): string[]; 164 | shiftHue(hShift?: number): void; 165 | /** 166 | * Returns a color at a specific position along the entire color line (0-1) 167 | * Treats all segments as one continuous path, respecting easing functions 168 | * @param t Position along the line (0-1), where 0 is start and 1 is end 169 | * @returns ColorPoint at the specified position 170 | * @example 171 | * getColorAt(0) // Returns color at the very beginning 172 | * getColorAt(0.5) // Returns color at the middle of the entire journey 173 | * getColorAt(1) // Returns color at the very end 174 | */ 175 | getColorAt(t: number): ColorPoint; 176 | /** 177 | * Determines whether easing should be inverted for a given segment 178 | * @param segmentIndex The index of the segment 179 | * @returns Whether easing should be inverted 180 | */ 181 | private shouldInvertEaseForSegment; 182 | } 183 | -------------------------------------------------------------------------------- /dist/index.min.mjs: -------------------------------------------------------------------------------- 1 | var m=(i,t)=>{let[o,n,e]=i,c=.5,h=.5,r=Math.atan2(n-h,o-c)*(180/Math.PI);r=(360+r)%360;let l=e,u=Math.sqrt(Math.pow(n-h,2)+Math.pow(o-c,2))/c;return[r,l,t?1-u:u]},f=(i,t)=>{let[o,n,e]=i,c=.5,h=.5,s=o/(180/Math.PI),r=(t?1-e:e)*c,l=c+r*Math.cos(s),a=h+r*Math.sin(s);return[l,a,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]]],_=(i,t)=>{let e=i-.5,c=t-.5,h=Math.hypot(e,c);return h<=.5?[i,t]:[.5+e/h*.5,.5+c/h*.5]},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]]],v=(i,t,o,n=!1,e=(s,r)=>r?1-s:s,c=(s,r)=>r?1-s:s,h=(s,r)=>r?1-s:s)=>{let s=e(i,n),r=c(i,n),l=h(i,n),a=(1-s)*t[0]+s*o[0],u=(1-r)*t[1]+r*o[1],P=(1-l)*t[2]+l*o[2];return[a,u,P]},x=(i,t,o=4,n=!1,e=(s,r)=>r?1-s:s,c=(s,r)=>r?1-s:s,h=(s,r)=>r?1-s:s)=>{let s=[];for(let r=0;ri,L=(i,t=!1)=>t?1-(1-i)**2:i**2,A=(i,t=!1)=>t?1-(1-i)**3:i**3,y=(i,t=!1)=>t?1-(1-i)**4:i**4,w=(i,t=!1)=>t?1-(1-i)**5:i**5,p=(i,t=!1)=>t?1-Math.sin((1-i)*Math.PI/2):Math.sin(i*Math.PI/2),S=(i,t=!1)=>t?1-Math.asin(1-i)/(Math.PI/2):Math.asin(i)/(Math.PI/2),E=(i,t=!1)=>t?1-Math.sqrt(1-i**2):1-Math.sqrt(1-i),T=i=>i**2*(3-2*i),I={linearPosition:V,exponentialPosition:L,quadraticPosition:A,cubicPosition:y,quarticPosition:w,sinusoidalPosition:p,asinusoidalPosition:S,arcPosition:E,smoothStepPosition:T},C=(i,t,o=!1)=>{let n=i[0],e=t[0],c=0;o&&n!==null&&e!==null?(c=Math.min(Math.abs(n-e),360-Math.abs(n-e)),c=c/360):c=n===null||e===null?0:n-e;let h=c,s=i[1]===null||t[1]===null?0:t[1]-i[1],r=i[2]===null||t[2]===null?0:t[2]-i[2];return Math.sqrt(h*h+s*s+r*r)},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(this._invertedLightness=n,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=m([this.x,this.y,this.z],n)):o&&(this.color=o,[this.x,this.y,this.z]=f(o,n))}set position([t,o,n]){this.x=t,this.y=o,this.z=n,this.color=m([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]=f(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)})`}set invertedLightness(t){this._invertedLightness=t,this.color=m([this.x,this.y,this.z],this._invertedLightness)}get invertedLightness(){return this._invertedLightness}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=f(this.color,this._invertedLightness)}},g=class{constructor({anchorColors:t=F(),numPoints:o=4,positionFunction:n=p,positionFunctionX:e,positionFunctionY:c,positionFunctionZ:h,closedLoop:s,invertedLightness:r,clampToCircle:l}={anchorColors:F(),numPoints:4,positionFunction:p,closedLoop:!1}){this._positionFunctionX=p;this._positionFunctionY=p;this._positionFunctionZ=p;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;this._clampToCircle=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(a=>new d({color:a,invertedLightness:r})),this._numPoints=o+2,this._positionFunctionX=e||n||p,this._positionFunctionY=c||n||p,this._positionFunctionZ=h||n||p,this.connectLastAndFirstAnchor=s||!1,this._invertedLightness=r||!1,this._clampToCircle=l||!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 clampToCircle(){return this._clampToCircle}set clampToCircle(t){this._clampToCircle=t}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 e=o[0]?o[0].position:[0,0,0],c=o[1]?o[1].position:[0,0,0],h=this.shouldInvertEaseForSegment(n);return x(e,c,this._numPoints,!!h,this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(s=>new d({xyz:s,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:o,insertAtIndex:n,clamp:e}){let c=t;if((e??this._clampToCircle)&&t){let[r,l,a]=t,[u,P]=_(r,l);c=[u,P,a]}let s=new d({xyz:c,color:o,invertedLightness:this._invertedLightness});return n!==void 0?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&&nC(s.position,t)):o&&(e=this.anchorPoints.map(s=>C(s.hsl,o,!0)));let c=Math.min(...e);if(c>n)return null;let h=e.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.anchorPoints.forEach(o=>o.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&&this._anchorPoints.length!==2&&t.pop(),t}cssColors(t="hsl"){let o={hsl:e=>e.hslCSS,oklch:e=>e.oklchCSS,lch:e=>e.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()}getColorAt(t){if(t<0||t>1)throw new Error("Position must be between 0 and 1");if(this.anchorPoints.length===0)throw new Error("No anchor points available");let o=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1,n=this.connectLastAndFirstAnchor&&this.anchorPoints.length===2?2:o,e=t*n,c=Math.floor(e),h=e-c,s=c>=n?n-1:c,r=c>=n?1:h,l=this._anchorPairs[s];if(!l||l.length<2||!l[0]||!l[1])return new d({color:this.anchorPoints[0]?.color||[0,0,0],invertedLightness:this._invertedLightness});let a=l[0].position,u=l[1].position,P=this.shouldInvertEaseForSegment(s),M=v(r,a,u,P,this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ);return new d({xyz:M,invertedLightness:this._invertedLightness})}shouldInvertEaseForSegment(t){return!!(t%2||this.connectLastAndFirstAnchor&&this.anchorPoints.length===2&&t===0)}},{p5:b}=globalThis;if(b&&b.VERSION&&b.VERSION.startsWith("1.")){console.info("p5 < 1.x detected, adding poline to p5 prototype");let i=new g;b.prototype.poline=i;let t=()=>i.colors.map(o=>`hsl(${Math.round(o[0])},${o[1]*100}%,${o[2]*100}%)`);b.prototype.registerMethod("polineColors",t),globalThis.poline=i,globalThis.polineColors=t}export{d as ColorPoint,g as Poline,_ as clampToCircle,f as hslToPoint,m as pointToHSL,I as positionFunctions,F as randomHSLPair,X as randomHSLTriple}; 2 | -------------------------------------------------------------------------------- /dist/index.min.cjs: -------------------------------------------------------------------------------- 1 | "use strict";var F=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var w=(i,t)=>{for(var o in t)F(i,o,{get:t[o],enumerable:!0})},S=(i,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of A(t))!y.call(i,s)&&s!==o&&F(i,s,{get:()=>t[s],enumerable:!(n=L(t,s))||n.enumerable});return i};var E=i=>S(F({},"__esModule",{value:!0}),i);var H={};w(H,{ColorPoint:()=>d,Poline:()=>g,clampToCircle:()=>C,hslToPoint:()=>f,pointToHSL:()=>m,positionFunctions:()=>N,randomHSLPair:()=>_,randomHSLTriple:()=>T});module.exports=E(H);var m=(i,t)=>{let[o,n,s]=i,r=.5,h=.5,c=Math.atan2(n-h,o-r)*(180/Math.PI);c=(360+c)%360;let l=s,u=Math.sqrt(Math.pow(n-h,2)+Math.pow(o-r,2))/r;return[c,l,t?1-u:u]},f=(i,t)=>{let[o,n,s]=i,r=.5,h=.5,e=o/(180/Math.PI),c=(t?1-s:s)*r,l=r+c*Math.cos(e),a=h+c*Math.sin(e);return[l,a,n]},_=(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]]],C=(i,t)=>{let s=i-.5,r=t-.5,h=Math.hypot(s,r);return h<=.5?[i,t]:[.5+s/h*.5,.5+r/h*.5]},T=(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]]],x=(i,t,o,n=!1,s=(e,c)=>c?1-e:e,r=(e,c)=>c?1-e:e,h=(e,c)=>c?1-e:e)=>{let e=s(i,n),c=r(i,n),l=h(i,n),a=(1-e)*t[0]+e*o[0],u=(1-c)*t[1]+c*o[1],P=(1-l)*t[2]+l*o[2];return[a,u,P]},X=(i,t,o=4,n=!1,s=(e,c)=>c?1-e:e,r=(e,c)=>c?1-e:e,h=(e,c)=>c?1-e:e)=>{let e=[];for(let c=0;ci,z=(i,t=!1)=>t?1-(1-i)**2:i**2,Y=(i,t=!1)=>t?1-(1-i)**3:i**3,Z=(i,t=!1)=>t?1-(1-i)**4:i**4,$=(i,t=!1)=>t?1-(1-i)**5:i**5,p=(i,t=!1)=>t?1-Math.sin((1-i)*Math.PI/2):Math.sin(i*Math.PI/2),O=(i,t=!1)=>t?1-Math.asin(1-i)/(Math.PI/2):Math.asin(i)/(Math.PI/2),k=(i,t=!1)=>t?1-Math.sqrt(1-i**2):1-Math.sqrt(1-i),q=i=>i**2*(3-2*i),N={linearPosition:I,exponentialPosition:z,quadraticPosition:Y,cubicPosition:Z,quarticPosition:$,sinusoidalPosition:p,asinusoidalPosition:O,arcPosition:k,smoothStepPosition:q},M=(i,t,o=!1)=>{let n=i[0],s=t[0],r=0;o&&n!==null&&s!==null?(r=Math.min(Math.abs(n-s),360-Math.abs(n-s)),r=r/360):r=n===null||s===null?0:n-s;let h=r,e=i[1]===null||t[1]===null?0:t[1]-i[1],c=i[2]===null||t[2]===null?0:t[2]-i[2];return Math.sqrt(h*h+e*e+c*c)},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(this._invertedLightness=n,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=m([this.x,this.y,this.z],n)):o&&(this.color=o,[this.x,this.y,this.z]=f(o,n))}set position([t,o,n]){this.x=t,this.y=o,this.z=n,this.color=m([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]=f(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)})`}set invertedLightness(t){this._invertedLightness=t,this.color=m([this.x,this.y,this.z],this._invertedLightness)}get invertedLightness(){return this._invertedLightness}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=f(this.color,this._invertedLightness)}},g=class{constructor({anchorColors:t=_(),numPoints:o=4,positionFunction:n=p,positionFunctionX:s,positionFunctionY:r,positionFunctionZ:h,closedLoop:e,invertedLightness:c,clampToCircle:l}={anchorColors:_(),numPoints:4,positionFunction:p,closedLoop:!1}){this._positionFunctionX=p;this._positionFunctionY=p;this._positionFunctionZ=p;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;this._clampToCircle=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(a=>new d({color:a,invertedLightness:c})),this._numPoints=o+2,this._positionFunctionX=s||n||p,this._positionFunctionY=r||n||p,this._positionFunctionZ=h||n||p,this.connectLastAndFirstAnchor=e||!1,this._invertedLightness=c||!1,this._clampToCircle=l||!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 clampToCircle(){return this._clampToCircle}set clampToCircle(t){this._clampToCircle=t}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],r=o[1]?o[1].position:[0,0,0],h=this.shouldInvertEaseForSegment(n);return X(s,r,this._numPoints,!!h,this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(e=>new d({xyz:e,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:o,insertAtIndex:n,clamp:s}){let r=t;if((s!=null?s:this._clampToCircle)&&t){let[c,l,a]=t,[u,P]=C(c,l);r=[u,P,a]}let e=new d({xyz:r,color:o,invertedLightness:this._invertedLightness});return n!==void 0?this.anchorPoints.splice(n,0,e):this.anchorPoints.push(e),this.updateAnchorPairs(),e}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&&nM(e.position,t)):o&&(s=this.anchorPoints.map(e=>M(e.hsl,o,!0)));let r=Math.min(...s);if(r>n)return null;let h=s.indexOf(r);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.anchorPoints.forEach(o=>o.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&&this._anchorPoints.length!==2&&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()}getColorAt(t){var v;if(t<0||t>1)throw new Error("Position must be between 0 and 1");if(this.anchorPoints.length===0)throw new Error("No anchor points available");let o=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1,n=this.connectLastAndFirstAnchor&&this.anchorPoints.length===2?2:o,s=t*n,r=Math.floor(s),h=s-r,e=r>=n?n-1:r,c=r>=n?1:h,l=this._anchorPairs[e];if(!l||l.length<2||!l[0]||!l[1])return new d({color:((v=this.anchorPoints[0])==null?void 0:v.color)||[0,0,0],invertedLightness:this._invertedLightness});let a=l[0].position,u=l[1].position,P=this.shouldInvertEaseForSegment(e),V=x(c,a,u,P,this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ);return new d({xyz:V,invertedLightness:this._invertedLightness})}shouldInvertEaseForSegment(t){return!!(t%2||this.connectLastAndFirstAnchor&&this.anchorPoints.length===2&&t===0)}},{p5:b}=globalThis;if(b&&b.VERSION&&b.VERSION.startsWith("1.")){console.info("p5 < 1.x detected, adding poline to p5 prototype");let i=new g;b.prototype.poline=i;let t=()=>i.colors.map(o=>`hsl(${Math.round(o[0])},${o[1]*100}%,${o[2]*100}%)`);b.prototype.registerMethod("polineColors",t),globalThis.poline=i,globalThis.polineColors=t} 2 | -------------------------------------------------------------------------------- /dist/index.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var poline=(()=>{var F=Object.defineProperty;var L=Object.getOwnPropertyDescriptor;var A=Object.getOwnPropertyNames;var y=Object.prototype.hasOwnProperty;var w=(i,t)=>{for(var o in t)F(i,o,{get:t[o],enumerable:!0})},S=(i,t,o,n)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of A(t))!y.call(i,s)&&s!==o&&F(i,s,{get:()=>t[s],enumerable:!(n=L(t,s))||n.enumerable});return i};var E=i=>S(F({},"__esModule",{value:!0}),i);var H={};w(H,{ColorPoint:()=>d,Poline:()=>g,clampToCircle:()=>C,hslToPoint:()=>f,pointToHSL:()=>m,positionFunctions:()=>N,randomHSLPair:()=>_,randomHSLTriple:()=>T});var m=(i,t)=>{let[o,n,s]=i,r=.5,h=.5,c=Math.atan2(n-h,o-r)*(180/Math.PI);c=(360+c)%360;let l=s,u=Math.sqrt(Math.pow(n-h,2)+Math.pow(o-r,2))/r;return[c,l,t?1-u:u]},f=(i,t)=>{let[o,n,s]=i,r=.5,h=.5,e=o/(180/Math.PI),c=(t?1-s:s)*r,l=r+c*Math.cos(e),a=h+c*Math.sin(e);return[l,a,n]},_=(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]]],C=(i,t)=>{let s=i-.5,r=t-.5,h=Math.hypot(s,r);return h<=.5?[i,t]:[.5+s/h*.5,.5+r/h*.5]},T=(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]]],x=(i,t,o,n=!1,s=(e,c)=>c?1-e:e,r=(e,c)=>c?1-e:e,h=(e,c)=>c?1-e:e)=>{let e=s(i,n),c=r(i,n),l=h(i,n),a=(1-e)*t[0]+e*o[0],u=(1-c)*t[1]+c*o[1],P=(1-l)*t[2]+l*o[2];return[a,u,P]},X=(i,t,o=4,n=!1,s=(e,c)=>c?1-e:e,r=(e,c)=>c?1-e:e,h=(e,c)=>c?1-e:e)=>{let e=[];for(let c=0;ci,z=(i,t=!1)=>t?1-(1-i)**2:i**2,Y=(i,t=!1)=>t?1-(1-i)**3:i**3,Z=(i,t=!1)=>t?1-(1-i)**4:i**4,$=(i,t=!1)=>t?1-(1-i)**5:i**5,p=(i,t=!1)=>t?1-Math.sin((1-i)*Math.PI/2):Math.sin(i*Math.PI/2),O=(i,t=!1)=>t?1-Math.asin(1-i)/(Math.PI/2):Math.asin(i)/(Math.PI/2),k=(i,t=!1)=>t?1-Math.sqrt(1-i**2):1-Math.sqrt(1-i),q=i=>i**2*(3-2*i),N={linearPosition:I,exponentialPosition:z,quadraticPosition:Y,cubicPosition:Z,quarticPosition:$,sinusoidalPosition:p,asinusoidalPosition:O,arcPosition:k,smoothStepPosition:q},M=(i,t,o=!1)=>{let n=i[0],s=t[0],r=0;o&&n!==null&&s!==null?(r=Math.min(Math.abs(n-s),360-Math.abs(n-s)),r=r/360):r=n===null||s===null?0:n-s;let h=r,e=i[1]===null||t[1]===null?0:t[1]-i[1],c=i[2]===null||t[2]===null?0:t[2]-i[2];return Math.sqrt(h*h+e*e+c*c)},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(this._invertedLightness=n,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=m([this.x,this.y,this.z],n)):o&&(this.color=o,[this.x,this.y,this.z]=f(o,n))}set position([t,o,n]){this.x=t,this.y=o,this.z=n,this.color=m([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]=f(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)})`}set invertedLightness(t){this._invertedLightness=t,this.color=m([this.x,this.y,this.z],this._invertedLightness)}get invertedLightness(){return this._invertedLightness}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=f(this.color,this._invertedLightness)}},g=class{constructor({anchorColors:t=_(),numPoints:o=4,positionFunction:n=p,positionFunctionX:s,positionFunctionY:r,positionFunctionZ:h,closedLoop:e,invertedLightness:c,clampToCircle:l}={anchorColors:_(),numPoints:4,positionFunction:p,closedLoop:!1}){this._positionFunctionX=p;this._positionFunctionY=p;this._positionFunctionZ=p;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;this._clampToCircle=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(a=>new d({color:a,invertedLightness:c})),this._numPoints=o+2,this._positionFunctionX=s||n||p,this._positionFunctionY=r||n||p,this._positionFunctionZ=h||n||p,this.connectLastAndFirstAnchor=e||!1,this._invertedLightness=c||!1,this._clampToCircle=l||!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 clampToCircle(){return this._clampToCircle}set clampToCircle(t){this._clampToCircle=t}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],r=o[1]?o[1].position:[0,0,0],h=this.shouldInvertEaseForSegment(n);return X(s,r,this._numPoints,!!h,this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(e=>new d({xyz:e,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:o,insertAtIndex:n,clamp:s}){let r=t;if((s!=null?s:this._clampToCircle)&&t){let[c,l,a]=t,[u,P]=C(c,l);r=[u,P,a]}let e=new d({xyz:r,color:o,invertedLightness:this._invertedLightness});return n!==void 0?this.anchorPoints.splice(n,0,e):this.anchorPoints.push(e),this.updateAnchorPairs(),e}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&&nM(e.position,t)):o&&(s=this.anchorPoints.map(e=>M(e.hsl,o,!0)));let r=Math.min(...s);if(r>n)return null;let h=s.indexOf(r);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.anchorPoints.forEach(o=>o.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&&this._anchorPoints.length!==2&&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()}getColorAt(t){var v;if(t<0||t>1)throw new Error("Position must be between 0 and 1");if(this.anchorPoints.length===0)throw new Error("No anchor points available");let o=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1,n=this.connectLastAndFirstAnchor&&this.anchorPoints.length===2?2:o,s=t*n,r=Math.floor(s),h=s-r,e=r>=n?n-1:r,c=r>=n?1:h,l=this._anchorPairs[e];if(!l||l.length<2||!l[0]||!l[1])return new d({color:((v=this.anchorPoints[0])==null?void 0:v.color)||[0,0,0],invertedLightness:this._invertedLightness});let a=l[0].position,u=l[1].position,P=this.shouldInvertEaseForSegment(e),V=x(c,a,u,P,this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ);return new d({xyz:V,invertedLightness:this._invertedLightness})}shouldInvertEaseForSegment(t){return!!(t%2||this.connectLastAndFirstAnchor&&this.anchorPoints.length===2&&t===0)}},{p5:b}=globalThis;if(b&&b.VERSION&&b.VERSION.startsWith("1.")){console.info("p5 < 1.x detected, adding poline to p5 prototype");let i=new g;b.prototype.poline=i;let t=()=>i.colors.map(o=>`hsl(${Math.round(o[0])},${o[1]*100}%,${o[2]*100}%)`);b.prototype.registerMethod("polineColors",t),globalThis.poline=i,globalThis.polineColors=t}return E(H);})(); 2 | -------------------------------------------------------------------------------- /dist/picker.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Poline Picker Example 7 | 8 | 9 | 11 | 195 | 196 | 197 |
198 |

Poline Picker

199 |

200 | Experience the magic of "poline" for yourself, dear color explorer. 201 | Drag existing anchor points to adjust colors. 202 | Drag the outer ring of an anchor point to adjust its saturation. 203 | Press P to add a new point at cursor position, 204 | and press to remove the last selected anchor point. 205 | Try using the and keys to change the hue of all colors. 206 | Hold Shift while adjusting saturation for finer control. 207 |

208 | 209 |
210 | 214 | 218 | 222 | 226 | 231 | 236 | 241 | 242 |
243 | 244 | 245 | 246 | 247 | 248 |
249 |

HSL

250 |
251 |

OKLch

252 |
253 |

Lch

254 |
255 |
256 |
257 | 258 | 440 | 441 | 442 | -------------------------------------------------------------------------------- /dist/picker.min.mjs: -------------------------------------------------------------------------------- 1 | var L=(c,t)=>{let[n,i,o]=c,e=.5,a=.5,r=Math.atan2(i-a,n-e)*(180/Math.PI);r=(360+r)%360;let h=o,p=Math.sqrt(Math.pow(i-a,2)+Math.pow(n-e,2))/e;return[r,h,t?1-p:p]},x=(c,t)=>{let[n,i,o]=c,e=.5,a=.5,s=n/(180/Math.PI),r=(t?1-o:o)*e,h=e+r*Math.cos(s),u=a+r*Math.sin(s);return[h,u,i]},I=(c=Math.random()*360,t=[Math.random(),Math.random()],n=[.75+Math.random()*.2,.3+Math.random()*.2])=>[[c,t[0],n[0]],[(c+60+Math.random()*180)%360,t[1],n[1]]],G=(c,t)=>{let o=c-.5,e=t-.5,a=Math.hypot(o,e);return a<=.5?[c,t]:[.5+o/a*.5,.5+e/a*.5]};var z=(c,t,n,i=!1,o=(s,r)=>r?1-s:s,e=(s,r)=>r?1-s:s,a=(s,r)=>r?1-s:s)=>{let s=o(c,i),r=e(c,i),h=a(c,i),u=(1-s)*t[0]+s*n[0],p=(1-r)*t[1]+r*n[1],l=(1-h)*t[2]+h*n[2];return[u,p,l]},$=(c,t,n=4,i=!1,o=(s,r)=>r?1-s:s,e=(s,r)=>r?1-s:s,a=(s,r)=>r?1-s:s)=>{let s=[];for(let r=0;rc,H=(c,t=!1)=>t?1-(1-c)**2:c**2,N=(c,t=!1)=>t?1-(1-c)**3:c**3,q=(c,t=!1)=>t?1-(1-c)**4:c**4,O=(c,t=!1)=>t?1-(1-c)**5:c**5,b=(c,t=!1)=>t?1-Math.sin((1-c)*Math.PI/2):Math.sin(c*Math.PI/2),Z=(c,t=!1)=>t?1-Math.asin(1-c)/(Math.PI/2):Math.asin(c)/(Math.PI/2),U=(c,t=!1)=>t?1-Math.sqrt(1-c**2):1-Math.sqrt(1-c),D=c=>c**2*(3-2*c),B={linearPosition:j,exponentialPosition:H,quadraticPosition:N,cubicPosition:q,quarticPosition:O,sinusoidalPosition:b,asinusoidalPosition:Z,arcPosition:U,smoothStepPosition:D},R=(c,t,n=!1)=>{let i=c[0],o=t[0],e=0;n&&i!==null&&o!==null?(e=Math.min(Math.abs(i-o),360-Math.abs(i-o)),e=e/360):e=i===null||o===null?0:i-o;let a=e,s=c[1]===null||t[1]===null?0:t[1]-c[1],r=c[2]===null||t[2]===null?0:t[2]-c[2];return Math.sqrt(a*a+s*s+r*r)},A=class{constructor({xyz:t,color:n,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:n,invertedLightness:i})}positionOrColor({xyz:t,color:n,invertedLightness:i=!1}){if(this._invertedLightness=i,t&&n||!t&&!n)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=L([this.x,this.y,this.z],i)):n&&(this.color=n,[this.x,this.y,this.z]=x(n,i))}set position([t,n,i]){this.x=t,this.y=n,this.z=i,this.color=L([this.x,this.y,this.z],this._invertedLightness)}get position(){return[this.x,this.y,this.z]}set hsl([t,n,i]){this.color=[t,n,i],[this.x,this.y,this.z]=x(this.color,this._invertedLightness)}get hsl(){return this.color}get hslCSS(){let[t,n,i]=this.color;return`hsl(${t.toFixed(2)}, ${(n*100).toFixed(2)}%, ${(i*100).toFixed(2)}%)`}get oklchCSS(){let[t,n,i]=this.color;return`oklch(${(i*100).toFixed(2)}% ${(n*.4).toFixed(3)} ${t.toFixed(2)})`}get lchCSS(){let[t,n,i]=this.color;return`lch(${(i*100).toFixed(2)}% ${(n*150).toFixed(2)} ${t.toFixed(2)})`}set invertedLightness(t){this._invertedLightness=t,this.color=L([this.x,this.y,this.z],this._invertedLightness)}get invertedLightness(){return this._invertedLightness}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=x(this.color,this._invertedLightness)}},M=class{constructor({anchorColors:t=I(),numPoints:n=4,positionFunction:i=b,positionFunctionX:o,positionFunctionY:e,positionFunctionZ:a,closedLoop:s,invertedLightness:r,clampToCircle:h}={anchorColors:I(),numPoints:4,positionFunction:b,closedLoop:!1}){this._positionFunctionX=b;this._positionFunctionY=b;this._positionFunctionZ=b;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;this._clampToCircle=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(u=>new A({color:u,invertedLightness:r})),this._numPoints=n+2,this._positionFunctionX=o||i||b,this._positionFunctionY=e||i||b,this._positionFunctionZ=a||i||b,this.connectLastAndFirstAnchor=s||!1,this._invertedLightness=r||!1,this._clampToCircle=h||!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 clampToCircle(){return this._clampToCircle}set clampToCircle(t){this._clampToCircle=t}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 n=0;n{let o=n[0]?n[0].position:[0,0,0],e=n[1]?n[1].position:[0,0,0],a=this.shouldInvertEaseForSegment(i);return $(o,e,this._numPoints,!!a,this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(s=>new A({xyz:s,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:n,insertAtIndex:i,clamp:o}){let e=t;if((o??this._clampToCircle)&&t){let[r,h,u]=t,[p,l]=G(r,h);e=[p,l,u]}let s=new A({xyz:e,color:n,invertedLightness:this._invertedLightness});return i!==void 0?this.anchorPoints.splice(i,0,s):this.anchorPoints.push(s),this.updateAnchorPairs(),s}removeAnchorPoint({point:t,index:n}){if(!t&&n===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(n!==void 0?i=n:t&&(i=this.anchorPoints.indexOf(t)),i>-1&&iR(s.position,t)):n&&(o=this.anchorPoints.map(s=>R(s.hsl,n,!0)));let e=Math.min(...o);if(e>i)return null;let a=o.indexOf(e);return this.anchorPoints[a]||null}set closedLoop(t){this.connectLastAndFirstAnchor=t,this.updateAnchorPairs()}get closedLoop(){return this.connectLastAndFirstAnchor}set invertedLightness(t){this._invertedLightness=t,this.anchorPoints.forEach(n=>n.invertedLightness=t),this.updateAnchorPairs()}get invertedLightness(){return this._invertedLightness}get flattenedPoints(){return this.points.flat().filter((t,n)=>n!=0?n%this._numPoints:!0)}get colors(){let t=this.flattenedPoints.map(n=>n.color);return this.connectLastAndFirstAnchor&&this._anchorPoints.length!==2&&t.pop(),t}cssColors(t="hsl"){let n={hsl:o=>o.hslCSS,oklch:o=>o.oklchCSS,lch:o=>o.lchCSS},i=this.flattenedPoints.map(n[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(n=>n.shiftHue(t)),this.updateAnchorPairs()}getColorAt(t){if(t<0||t>1)throw new Error("Position must be between 0 and 1");if(this.anchorPoints.length===0)throw new Error("No anchor points available");let n=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1,i=this.connectLastAndFirstAnchor&&this.anchorPoints.length===2?2:n,o=t*i,e=Math.floor(o),a=o-e,s=e>=i?i-1:e,r=e>=i?1:a,h=this._anchorPairs[s];if(!h||h.length<2||!h[0]||!h[1])return new A({color:this.anchorPoints[0]?.color||[0,0,0],invertedLightness:this._invertedLightness});let u=h[0].position,p=h[1].position,l=this.shouldInvertEaseForSegment(s),m=z(r,u,p,l,this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ);return new A({xyz:m,invertedLightness:this._invertedLightness})}shouldInvertEaseForSegment(t){return!!(t%2||this.connectLastAndFirstAnchor&&this.anchorPoints.length===2&&t===0)}},{p5:C}=globalThis;if(C&&C.VERSION&&C.VERSION.startsWith("1.")){console.info("p5 < 1.x detected, adding poline to p5 prototype");let c=new M;C.prototype.poline=c;let t=()=>c.colors.map(n=>`hsl(${Math.round(n[0])},${n[1]*100}%,${n[2]*100}%)`);C.prototype.registerMethod("polineColors",t),globalThis.poline=c,globalThis.polineColors=t}var S="http://www.w3.org/2000/svg",g=100,f={anchorRadius:2,ringOuterRadius:5,ringThickness:1,ringThicknessHover:2,ringGap:.5,tickLength:1.5,tickGap:.5},W=1,K=2.5,y=class extends HTMLElement{constructor(){super();this.currentPoint=null;this.allowAddPoints=!1;this.ringAdjust=null;this.ringHoverIndex=null;this.boundPointerDown=this.handlePointerDown.bind(this);this.boundPointerMove=this.handlePointerMove.bind(this);this.boundPointerUp=this.handlePointerUp.bind(this);this.attachShadow({mode:"open"}),this.interactive=this.hasAttribute("interactive"),this.allowAddPoints=this.hasAttribute("allow-add-points")}connectedCallback(){this.interactive=this.hasAttribute("interactive"),this.allowAddPoints=this.hasAttribute("allow-add-points"),this.render(),this.interactive&&this.addEventListeners()}disconnectedCallback(){this.removeEventListeners()}setPoline(n){this.poline=n,this.updateSVG(),this.updateLightnessBackground()}setAllowAddPoints(n){this.allowAddPoints=n}addPointAtPosition(n,i){if(!this.poline)return null;let o=n/this.svg.clientWidth,e=i/this.svg.clientHeight,a=this.poline.addAnchorPoint({xyz:[o,e,e]});return this.updateSVG(),this.dispatchPolineChange(),a}updateLightnessBackground(){let n=this.shadowRoot?.querySelector(".picker");n&&this.poline&&(this.poline.invertedLightness?(n.style.setProperty("--maxL","#000"),n.style.setProperty("--minL","#fff")):(n.style.setProperty("--maxL","#fff"),n.style.setProperty("--minL","#000")))}render(){if(!this.shadowRoot)return;this.shadowRoot.innerHTML=` 2 | 92 | `,this.svg=this.createSVG();let n=document.createElement("div");n.className="picker",n.appendChild(this.svg),this.shadowRoot.appendChild(n),this.wheel=this.svg.querySelector(".wheel"),this.line=this.svg.querySelector(".wheel__line"),this.saturationRings=this.svg.querySelector(".wheel__saturation-rings"),this.anchors=this.svg.querySelector(".wheel__anchors"),this.points=this.svg.querySelector(".wheel__points"),this.poline&&(this.updateSVG(),this.updateLightnessBackground())}createSVG(){let n=document.createElementNS(S,"svg");return n.setAttribute("viewBox",`0 0 ${g} ${g}`),n.innerHTML=` 93 | 94 | 95 | 96 | 97 | 98 | 99 | `,n}updateSVG(){if(!this.poline||!this.svg)return;let n=this.poline.flattenedPoints,i=n.map(e=>{let a=this.pointToCartesian(e);if(!a)return"";let[s,r]=a;return`${s},${r}`}).filter(e=>e!=="").join(" ");this.line.setAttribute("points",i);let o=(e,a,s,r)=>{let h=e.children;for(;h.length>a.length;){let u=h[h.length-1];u&&e.removeChild(u)}a.forEach((u,p)=>{let l=h[p],m=this.pointToCartesian(u);if(!m)return;let[v=0,P=0]=m,d=r(u),_=u.hslCSS;l||(l=document.createElementNS(S,"circle"),l.setAttribute("class",s),e.appendChild(l)),l.setAttribute("cx",v.toString()),l.setAttribute("cy",P.toString()),l.setAttribute("r",d.toString()),l.setAttribute("fill",_)})};this.updateSaturationRings(),o(this.anchors,this.poline.anchorPoints,"wheel__anchor",()=>f.anchorRadius),o(this.points,n,"wheel__point",e=>.5+e.color[1])}updateSaturationRings(){if(!this.poline||!this.saturationRings)return;let n=this.poline.anchorPoints,i=Array.from(this.saturationRings.querySelectorAll(".wheel__ring-group"));for(;i.length>n.length;){let o=i.pop();o&&o.remove()}n.forEach((o,e)=>{let a=this.pointToCartesian(o);if(!a)return;let[s,r]=a,h=o.z,u=f.anchorRadius+f.ringGap+1,p=this.ringHoverIndex===e||this.ringAdjust&&this.ringAdjust.anchorIndex===e,l=i[e];if(!l){l=document.createElementNS(S,"g"),l.setAttribute("class","wheel__ring-group");let V=document.createElementNS(S,"circle");V.setAttribute("class","wheel__ring-bg"),l.appendChild(V);let E=document.createElementNS(S,"path");E.setAttribute("class","wheel__saturation-ring"),l.appendChild(E);let T=document.createElementNS(S,"line");T.setAttribute("class","wheel__ring-tick"),l.appendChild(T),this.saturationRings.appendChild(l),i.push(l)}l.classList.toggle("wheel__ring-group--hover",!!p);let m=l.querySelector(".wheel__ring-bg");m.setAttribute("cx",s.toString()),m.setAttribute("cy",r.toString()),m.setAttribute("r",u.toString());let v=l.querySelector(".wheel__saturation-ring"),P=-Math.PI/2,d=P+h*Math.PI*2,_=this.describeArc(s,r,u,P,d);v.setAttribute("d",_);let w=l.querySelector(".wheel__ring-tick"),F=s+(u+f.tickGap)*Math.cos(d),k=r+(u+f.tickGap)*Math.sin(d),X=F+Math.cos(d)*f.tickLength,Y=k+Math.sin(d)*f.tickLength;w.setAttribute("x1",F.toString()),w.setAttribute("y1",k.toString()),w.setAttribute("x2",X.toString()),w.setAttribute("y2",Y.toString())})}describeArc(n,i,o,e,a){let s=a-e;if(Math.abs(s)<.001)return"";if(Math.abs(s)>Math.PI*2-.01){let v=e+Math.PI,P=n+o*Math.cos(e),d=i+o*Math.sin(e),_=n+o*Math.cos(v),w=i+o*Math.sin(v);return`M ${P} ${d} A ${o} ${o} 0 1 1 ${_} ${w} A ${o} ${o} 0 1 1 ${P} ${d}`}let r=n+o*Math.cos(e),h=i+o*Math.sin(e),u=n+o*Math.cos(a),p=i+o*Math.sin(a),l=s>Math.PI?1:0;return`M ${r} ${h} A ${o} ${o} 0 ${l} ${1} ${u} ${p}`}pointToCartesian(n){let i=g/2,o=i+(n.x-.5)*g,e=i+(n.y-.5)*g;return[o,e]}addEventListeners(){this.svg&&(this.svg.addEventListener("pointerdown",this.boundPointerDown),this.svg.addEventListener("pointermove",this.boundPointerMove),this.svg.addEventListener("pointerup",this.boundPointerUp))}removeEventListeners(){this.svg&&(this.svg.removeEventListener("pointerdown",this.boundPointerDown),this.svg.removeEventListener("pointermove",this.boundPointerMove),this.svg.removeEventListener("pointerup",this.boundPointerUp))}handlePointerDown(n){n.stopPropagation();let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n),a=window.matchMedia("(pointer: coarse)").matches?null:this.pickRing(i,o);if(a!==null){let r=this.poline.anchorPoints[a];if(!r)return;let h=this.pointToCartesian(r);if(!h)return;let[u,p]=h,l=i*g,m=o*g,v=Math.atan2(m-p,l-u);this.ringAdjust={anchorIndex:a,startSaturation:r.color[1],startAngle:v,prevAngle:v,accumulatedAngle:0},this.ringHoverIndex=a,this.classList.add("ring-adjusting"),this.updateSaturationRings();try{this.svg.setPointerCapture(n.pointerId)}catch{}return}let s=this.poline.getClosestAnchorPoint({xyz:[i,o,null],maxDistance:.05});s?this.currentPoint=s:this.allowAddPoints&&(this.currentPoint=this.poline.addAnchorPoint({xyz:[i,o,o]}),this.updateSVG(),this.dispatchPolineChange())}handlePointerMove(n){let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n);if(this.ringAdjust){let e=this.poline.anchorPoints[this.ringAdjust.anchorIndex];if(!e)return;let a=this.pointToCartesian(e);if(!a)return;let[s,r]=a,h=i*g,u=o*g,p=Math.atan2(u-r,h-s),l=p-this.ringAdjust.prevAngle;l>Math.PI?l-=Math.PI*2:l<-Math.PI&&(l+=Math.PI*2),this.ringAdjust.accumulatedAngle+=l,this.ringAdjust.prevAngle=p;let m=this.ringAdjust.accumulatedAngle/(Math.PI*2),v=n.shiftKey?K:W,P=m/v,d=this.clamp01(this.ringAdjust.startSaturation+P);d>.99&&(d=1),d<.01&&(d=0);let _=d===0||d===1,w=d===1&&P>0||d===0&&P<0;_&&w&&(this.ringAdjust.startSaturation=d,this.ringAdjust.accumulatedAngle=0,this.ringAdjust.prevAngle=p),this.poline.updateAnchorPoint({point:e,color:[e.color[0],d,e.color[2]]}),this.updateSVG(),this.dispatchPolineChange();return}if(this.currentPoint){this.poline.updateAnchorPoint({point:this.currentPoint,xyz:[i,o,this.currentPoint.z]}),this.updateSVG(),this.dispatchPolineChange();return}if(!window.matchMedia("(pointer: coarse)").matches){let e=this.pickRing(i,o);e!==this.ringHoverIndex&&(this.ringHoverIndex=e,this.classList.toggle("ring-hover",e!==null),this.updateSaturationRings())}}handlePointerUp(n){if(this.ringAdjust){try{this.svg.releasePointerCapture(n.pointerId)}catch{}this.classList.remove("ring-adjusting")}this.ringAdjust=null,this.currentPoint=null;let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n),e=this.pickRing(i,o);e!==this.ringHoverIndex&&(this.ringHoverIndex=e,this.classList.toggle("ring-hover",e!==null),this.updateSaturationRings())}pickRing(n,i){if(!this.poline)return null;let o=n*g,e=i*g;for(let a=0;af.anchorRadius&&p<=f.ringOuterRadius)return a}return null}clamp01(n){return Math.max(0,Math.min(1,n))}pointerToNormalizedCoordinates(n){let i=this.svg.getBoundingClientRect(),o=(n.clientX-i.left)/i.width*g,e=(n.clientY-i.top)/i.height*g;return{normalizedX:o/g,normalizedY:e/g}}dispatchPolineChange(){this.dispatchEvent(new CustomEvent("poline-change",{detail:{poline:this.poline}}))}};customElements.define("poline-picker",y);export{M as Poline,y as PolinePicker,B as positionFunctions}; 100 | -------------------------------------------------------------------------------- /dist/picker.min.cjs: -------------------------------------------------------------------------------- 1 | "use strict";var x=Object.defineProperty;var H=Object.getOwnPropertyDescriptor;var N=Object.getOwnPropertyNames;var q=Object.prototype.hasOwnProperty;var O=(r,t)=>{for(var n in t)x(r,n,{get:t[n],enumerable:!0})},Z=(r,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of N(t))!q.call(r,o)&&o!==n&&x(r,o,{get:()=>t[o],enumerable:!(i=H(t,o))||i.enumerable});return r};var U=r=>Z(x({},"__esModule",{value:!0}),r);var st={};O(st,{Poline:()=>M,PolinePicker:()=>L,positionFunctions:()=>Y});module.exports=U(st);var y=(r,t)=>{let[n,i,o]=r,e=.5,a=.5,c=Math.atan2(i-a,n-e)*(180/Math.PI);c=(360+c)%360;let h=o,p=Math.sqrt(Math.pow(i-a,2)+Math.pow(n-e,2))/e;return[c,h,t?1-p:p]},F=(r,t)=>{let[n,i,o]=r,e=.5,a=.5,s=n/(180/Math.PI),c=(t?1-o:o)*e,h=e+c*Math.cos(s),u=a+c*Math.sin(s);return[h,u,i]},G=(r=Math.random()*360,t=[Math.random(),Math.random()],n=[.75+Math.random()*.2,.3+Math.random()*.2])=>[[r,t[0],n[0]],[(r+60+Math.random()*180)%360,t[1],n[1]]],R=(r,t)=>{let o=r-.5,e=t-.5,a=Math.hypot(o,e);return a<=.5?[r,t]:[.5+o/a*.5,.5+e/a*.5]};var X=(r,t,n,i=!1,o=(s,c)=>c?1-s:s,e=(s,c)=>c?1-s:s,a=(s,c)=>c?1-s:s)=>{let s=o(r,i),c=e(r,i),h=a(r,i),u=(1-s)*t[0]+s*n[0],p=(1-c)*t[1]+c*n[1],l=(1-h)*t[2]+h*n[2];return[u,p,l]},D=(r,t,n=4,i=!1,o=(s,c)=>c?1-s:s,e=(s,c)=>c?1-s:s,a=(s,c)=>c?1-s:s)=>{let s=[];for(let c=0;cr,W=(r,t=!1)=>t?1-(1-r)**2:r**2,K=(r,t=!1)=>t?1-(1-r)**3:r**3,J=(r,t=!1)=>t?1-(1-r)**4:r**4,Q=(r,t=!1)=>t?1-(1-r)**5:r**5,b=(r,t=!1)=>t?1-Math.sin((1-r)*Math.PI/2):Math.sin(r*Math.PI/2),tt=(r,t=!1)=>t?1-Math.asin(1-r)/(Math.PI/2):Math.asin(r)/(Math.PI/2),nt=(r,t=!1)=>t?1-Math.sqrt(1-r**2):1-Math.sqrt(1-r),it=r=>r**2*(3-2*r),Y={linearPosition:B,exponentialPosition:W,quadraticPosition:K,cubicPosition:J,quarticPosition:Q,sinusoidalPosition:b,asinusoidalPosition:tt,arcPosition:nt,smoothStepPosition:it},z=(r,t,n=!1)=>{let i=r[0],o=t[0],e=0;n&&i!==null&&o!==null?(e=Math.min(Math.abs(i-o),360-Math.abs(i-o)),e=e/360):e=i===null||o===null?0:i-o;let a=e,s=r[1]===null||t[1]===null?0:t[1]-r[1],c=r[2]===null||t[2]===null?0:t[2]-r[2];return Math.sqrt(a*a+s*s+c*c)},A=class{constructor({xyz:t,color:n,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:n,invertedLightness:i})}positionOrColor({xyz:t,color:n,invertedLightness:i=!1}){if(this._invertedLightness=i,t&&n||!t&&!n)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=y([this.x,this.y,this.z],i)):n&&(this.color=n,[this.x,this.y,this.z]=F(n,i))}set position([t,n,i]){this.x=t,this.y=n,this.z=i,this.color=y([this.x,this.y,this.z],this._invertedLightness)}get position(){return[this.x,this.y,this.z]}set hsl([t,n,i]){this.color=[t,n,i],[this.x,this.y,this.z]=F(this.color,this._invertedLightness)}get hsl(){return this.color}get hslCSS(){let[t,n,i]=this.color;return`hsl(${t.toFixed(2)}, ${(n*100).toFixed(2)}%, ${(i*100).toFixed(2)}%)`}get oklchCSS(){let[t,n,i]=this.color;return`oklch(${(i*100).toFixed(2)}% ${(n*.4).toFixed(3)} ${t.toFixed(2)})`}get lchCSS(){let[t,n,i]=this.color;return`lch(${(i*100).toFixed(2)}% ${(n*150).toFixed(2)} ${t.toFixed(2)})`}set invertedLightness(t){this._invertedLightness=t,this.color=y([this.x,this.y,this.z],this._invertedLightness)}get invertedLightness(){return this._invertedLightness}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=F(this.color,this._invertedLightness)}},M=class{constructor({anchorColors:t=G(),numPoints:n=4,positionFunction:i=b,positionFunctionX:o,positionFunctionY:e,positionFunctionZ:a,closedLoop:s,invertedLightness:c,clampToCircle:h}={anchorColors:G(),numPoints:4,positionFunction:b,closedLoop:!1}){this._positionFunctionX=b;this._positionFunctionY=b;this._positionFunctionZ=b;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;this._clampToCircle=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(u=>new A({color:u,invertedLightness:c})),this._numPoints=n+2,this._positionFunctionX=o||i||b,this._positionFunctionY=e||i||b,this._positionFunctionZ=a||i||b,this.connectLastAndFirstAnchor=s||!1,this._invertedLightness=c||!1,this._clampToCircle=h||!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 clampToCircle(){return this._clampToCircle}set clampToCircle(t){this._clampToCircle=t}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 n=0;n{let o=n[0]?n[0].position:[0,0,0],e=n[1]?n[1].position:[0,0,0],a=this.shouldInvertEaseForSegment(i);return D(o,e,this._numPoints,!!a,this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(s=>new A({xyz:s,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:n,insertAtIndex:i,clamp:o}){let e=t;if((o!=null?o:this._clampToCircle)&&t){let[c,h,u]=t,[p,l]=R(c,h);e=[p,l,u]}let s=new A({xyz:e,color:n,invertedLightness:this._invertedLightness});return i!==void 0?this.anchorPoints.splice(i,0,s):this.anchorPoints.push(s),this.updateAnchorPairs(),s}removeAnchorPoint({point:t,index:n}){if(!t&&n===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(n!==void 0?i=n:t&&(i=this.anchorPoints.indexOf(t)),i>-1&&iz(s.position,t)):n&&(o=this.anchorPoints.map(s=>z(s.hsl,n,!0)));let e=Math.min(...o);if(e>i)return null;let a=o.indexOf(e);return this.anchorPoints[a]||null}set closedLoop(t){this.connectLastAndFirstAnchor=t,this.updateAnchorPairs()}get closedLoop(){return this.connectLastAndFirstAnchor}set invertedLightness(t){this._invertedLightness=t,this.anchorPoints.forEach(n=>n.invertedLightness=t),this.updateAnchorPairs()}get invertedLightness(){return this._invertedLightness}get flattenedPoints(){return this.points.flat().filter((t,n)=>n!=0?n%this._numPoints:!0)}get colors(){let t=this.flattenedPoints.map(n=>n.color);return this.connectLastAndFirstAnchor&&this._anchorPoints.length!==2&&t.pop(),t}cssColors(t="hsl"){let n={hsl:o=>o.hslCSS,oklch:o=>o.oklchCSS,lch:o=>o.lchCSS},i=this.flattenedPoints.map(n[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(n=>n.shiftHue(t)),this.updateAnchorPairs()}getColorAt(t){var v;if(t<0||t>1)throw new Error("Position must be between 0 and 1");if(this.anchorPoints.length===0)throw new Error("No anchor points available");let n=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1,i=this.connectLastAndFirstAnchor&&this.anchorPoints.length===2?2:n,o=t*i,e=Math.floor(o),a=o-e,s=e>=i?i-1:e,c=e>=i?1:a,h=this._anchorPairs[s];if(!h||h.length<2||!h[0]||!h[1])return new A({color:((v=this.anchorPoints[0])==null?void 0:v.color)||[0,0,0],invertedLightness:this._invertedLightness});let u=h[0].position,p=h[1].position,l=this.shouldInvertEaseForSegment(s),m=X(c,u,p,l,this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ);return new A({xyz:m,invertedLightness:this._invertedLightness})}shouldInvertEaseForSegment(t){return!!(t%2||this.connectLastAndFirstAnchor&&this.anchorPoints.length===2&&t===0)}},{p5:C}=globalThis;if(C&&C.VERSION&&C.VERSION.startsWith("1.")){console.info("p5 < 1.x detected, adding poline to p5 prototype");let r=new M;C.prototype.poline=r;let t=()=>r.colors.map(n=>`hsl(${Math.round(n[0])},${n[1]*100}%,${n[2]*100}%)`);C.prototype.registerMethod("polineColors",t),globalThis.poline=r,globalThis.polineColors=t}var S="http://www.w3.org/2000/svg",g=100,f={anchorRadius:2,ringOuterRadius:5,ringThickness:1,ringThicknessHover:2,ringGap:.5,tickLength:1.5,tickGap:.5},ot=1,et=2.5,L=class extends HTMLElement{constructor(){super();this.currentPoint=null;this.allowAddPoints=!1;this.ringAdjust=null;this.ringHoverIndex=null;this.boundPointerDown=this.handlePointerDown.bind(this);this.boundPointerMove=this.handlePointerMove.bind(this);this.boundPointerUp=this.handlePointerUp.bind(this);this.attachShadow({mode:"open"}),this.interactive=this.hasAttribute("interactive"),this.allowAddPoints=this.hasAttribute("allow-add-points")}connectedCallback(){this.interactive=this.hasAttribute("interactive"),this.allowAddPoints=this.hasAttribute("allow-add-points"),this.render(),this.interactive&&this.addEventListeners()}disconnectedCallback(){this.removeEventListeners()}setPoline(n){this.poline=n,this.updateSVG(),this.updateLightnessBackground()}setAllowAddPoints(n){this.allowAddPoints=n}addPointAtPosition(n,i){if(!this.poline)return null;let o=n/this.svg.clientWidth,e=i/this.svg.clientHeight,a=this.poline.addAnchorPoint({xyz:[o,e,e]});return this.updateSVG(),this.dispatchPolineChange(),a}updateLightnessBackground(){var i;let n=(i=this.shadowRoot)==null?void 0:i.querySelector(".picker");n&&this.poline&&(this.poline.invertedLightness?(n.style.setProperty("--maxL","#000"),n.style.setProperty("--minL","#fff")):(n.style.setProperty("--maxL","#fff"),n.style.setProperty("--minL","#000")))}render(){if(!this.shadowRoot)return;this.shadowRoot.innerHTML=` 2 | 92 | `,this.svg=this.createSVG();let n=document.createElement("div");n.className="picker",n.appendChild(this.svg),this.shadowRoot.appendChild(n),this.wheel=this.svg.querySelector(".wheel"),this.line=this.svg.querySelector(".wheel__line"),this.saturationRings=this.svg.querySelector(".wheel__saturation-rings"),this.anchors=this.svg.querySelector(".wheel__anchors"),this.points=this.svg.querySelector(".wheel__points"),this.poline&&(this.updateSVG(),this.updateLightnessBackground())}createSVG(){let n=document.createElementNS(S,"svg");return n.setAttribute("viewBox",`0 0 ${g} ${g}`),n.innerHTML=` 93 | 94 | 95 | 96 | 97 | 98 | 99 | `,n}updateSVG(){if(!this.poline||!this.svg)return;let n=this.poline.flattenedPoints,i=n.map(e=>{let a=this.pointToCartesian(e);if(!a)return"";let[s,c]=a;return`${s},${c}`}).filter(e=>e!=="").join(" ");this.line.setAttribute("points",i);let o=(e,a,s,c)=>{let h=e.children;for(;h.length>a.length;){let u=h[h.length-1];u&&e.removeChild(u)}a.forEach((u,p)=>{let l=h[p],m=this.pointToCartesian(u);if(!m)return;let[v=0,P=0]=m,d=c(u),_=u.hslCSS;l||(l=document.createElementNS(S,"circle"),l.setAttribute("class",s),e.appendChild(l)),l.setAttribute("cx",v.toString()),l.setAttribute("cy",P.toString()),l.setAttribute("r",d.toString()),l.setAttribute("fill",_)})};this.updateSaturationRings(),o(this.anchors,this.poline.anchorPoints,"wheel__anchor",()=>f.anchorRadius),o(this.points,n,"wheel__point",e=>.5+e.color[1])}updateSaturationRings(){if(!this.poline||!this.saturationRings)return;let n=this.poline.anchorPoints,i=Array.from(this.saturationRings.querySelectorAll(".wheel__ring-group"));for(;i.length>n.length;){let o=i.pop();o&&o.remove()}n.forEach((o,e)=>{let a=this.pointToCartesian(o);if(!a)return;let[s,c]=a,h=o.z,u=f.anchorRadius+f.ringGap+1,p=this.ringHoverIndex===e||this.ringAdjust&&this.ringAdjust.anchorIndex===e,l=i[e];if(!l){l=document.createElementNS(S,"g"),l.setAttribute("class","wheel__ring-group");let E=document.createElementNS(S,"circle");E.setAttribute("class","wheel__ring-bg"),l.appendChild(E);let T=document.createElementNS(S,"path");T.setAttribute("class","wheel__saturation-ring"),l.appendChild(T);let I=document.createElementNS(S,"line");I.setAttribute("class","wheel__ring-tick"),l.appendChild(I),this.saturationRings.appendChild(l),i.push(l)}l.classList.toggle("wheel__ring-group--hover",!!p);let m=l.querySelector(".wheel__ring-bg");m.setAttribute("cx",s.toString()),m.setAttribute("cy",c.toString()),m.setAttribute("r",u.toString());let v=l.querySelector(".wheel__saturation-ring"),P=-Math.PI/2,d=P+h*Math.PI*2,_=this.describeArc(s,c,u,P,d);v.setAttribute("d",_);let w=l.querySelector(".wheel__ring-tick"),k=s+(u+f.tickGap)*Math.cos(d),V=c+(u+f.tickGap)*Math.sin(d),$=k+Math.cos(d)*f.tickLength,j=V+Math.sin(d)*f.tickLength;w.setAttribute("x1",k.toString()),w.setAttribute("y1",V.toString()),w.setAttribute("x2",$.toString()),w.setAttribute("y2",j.toString())})}describeArc(n,i,o,e,a){let s=a-e;if(Math.abs(s)<.001)return"";if(Math.abs(s)>Math.PI*2-.01){let v=e+Math.PI,P=n+o*Math.cos(e),d=i+o*Math.sin(e),_=n+o*Math.cos(v),w=i+o*Math.sin(v);return`M ${P} ${d} A ${o} ${o} 0 1 1 ${_} ${w} A ${o} ${o} 0 1 1 ${P} ${d}`}let c=n+o*Math.cos(e),h=i+o*Math.sin(e),u=n+o*Math.cos(a),p=i+o*Math.sin(a),l=s>Math.PI?1:0;return`M ${c} ${h} A ${o} ${o} 0 ${l} ${1} ${u} ${p}`}pointToCartesian(n){let i=g/2,o=i+(n.x-.5)*g,e=i+(n.y-.5)*g;return[o,e]}addEventListeners(){this.svg&&(this.svg.addEventListener("pointerdown",this.boundPointerDown),this.svg.addEventListener("pointermove",this.boundPointerMove),this.svg.addEventListener("pointerup",this.boundPointerUp))}removeEventListeners(){this.svg&&(this.svg.removeEventListener("pointerdown",this.boundPointerDown),this.svg.removeEventListener("pointermove",this.boundPointerMove),this.svg.removeEventListener("pointerup",this.boundPointerUp))}handlePointerDown(n){n.stopPropagation();let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n),a=window.matchMedia("(pointer: coarse)").matches?null:this.pickRing(i,o);if(a!==null){let c=this.poline.anchorPoints[a];if(!c)return;let h=this.pointToCartesian(c);if(!h)return;let[u,p]=h,l=i*g,m=o*g,v=Math.atan2(m-p,l-u);this.ringAdjust={anchorIndex:a,startSaturation:c.color[1],startAngle:v,prevAngle:v,accumulatedAngle:0},this.ringHoverIndex=a,this.classList.add("ring-adjusting"),this.updateSaturationRings();try{this.svg.setPointerCapture(n.pointerId)}catch(P){}return}let s=this.poline.getClosestAnchorPoint({xyz:[i,o,null],maxDistance:.05});s?this.currentPoint=s:this.allowAddPoints&&(this.currentPoint=this.poline.addAnchorPoint({xyz:[i,o,o]}),this.updateSVG(),this.dispatchPolineChange())}handlePointerMove(n){let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n);if(this.ringAdjust){let e=this.poline.anchorPoints[this.ringAdjust.anchorIndex];if(!e)return;let a=this.pointToCartesian(e);if(!a)return;let[s,c]=a,h=i*g,u=o*g,p=Math.atan2(u-c,h-s),l=p-this.ringAdjust.prevAngle;l>Math.PI?l-=Math.PI*2:l<-Math.PI&&(l+=Math.PI*2),this.ringAdjust.accumulatedAngle+=l,this.ringAdjust.prevAngle=p;let m=this.ringAdjust.accumulatedAngle/(Math.PI*2),v=n.shiftKey?et:ot,P=m/v,d=this.clamp01(this.ringAdjust.startSaturation+P);d>.99&&(d=1),d<.01&&(d=0);let _=d===0||d===1,w=d===1&&P>0||d===0&&P<0;_&&w&&(this.ringAdjust.startSaturation=d,this.ringAdjust.accumulatedAngle=0,this.ringAdjust.prevAngle=p),this.poline.updateAnchorPoint({point:e,color:[e.color[0],d,e.color[2]]}),this.updateSVG(),this.dispatchPolineChange();return}if(this.currentPoint){this.poline.updateAnchorPoint({point:this.currentPoint,xyz:[i,o,this.currentPoint.z]}),this.updateSVG(),this.dispatchPolineChange();return}if(!window.matchMedia("(pointer: coarse)").matches){let e=this.pickRing(i,o);e!==this.ringHoverIndex&&(this.ringHoverIndex=e,this.classList.toggle("ring-hover",e!==null),this.updateSaturationRings())}}handlePointerUp(n){if(this.ringAdjust){try{this.svg.releasePointerCapture(n.pointerId)}catch(a){}this.classList.remove("ring-adjusting")}this.ringAdjust=null,this.currentPoint=null;let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n),e=this.pickRing(i,o);e!==this.ringHoverIndex&&(this.ringHoverIndex=e,this.classList.toggle("ring-hover",e!==null),this.updateSaturationRings())}pickRing(n,i){if(!this.poline)return null;let o=n*g,e=i*g;for(let a=0;af.anchorRadius&&p<=f.ringOuterRadius)return a}return null}clamp01(n){return Math.max(0,Math.min(1,n))}pointerToNormalizedCoordinates(n){let i=this.svg.getBoundingClientRect(),o=(n.clientX-i.left)/i.width*g,e=(n.clientY-i.top)/i.height*g;return{normalizedX:o/g,normalizedY:e/g}}dispatchPolineChange(){this.dispatchEvent(new CustomEvent("poline-change",{detail:{poline:this.poline}}))}};customElements.define("poline-picker",L); 100 | -------------------------------------------------------------------------------- /dist/picker.min.js: -------------------------------------------------------------------------------- 1 | "use strict";var polinePicker=(()=>{var y=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var q=Object.getOwnPropertyNames;var O=Object.prototype.hasOwnProperty;var b=Math.pow;var Z=(r,t)=>{for(var n in t)y(r,n,{get:t[n],enumerable:!0})},U=(r,t,n,i)=>{if(t&&typeof t=="object"||typeof t=="function")for(let o of q(t))!O.call(r,o)&&o!==n&&y(r,o,{get:()=>t[o],enumerable:!(i=N(t,o))||i.enumerable});return r};var D=r=>U(y({},"__esModule",{value:!0}),r);var rt={};Z(rt,{Poline:()=>L,PolinePicker:()=>x,positionFunctions:()=>$});var F=(r,t)=>{let[n,i,o]=r,e=.5,a=.5,c=Math.atan2(i-a,n-e)*(180/Math.PI);c=(360+c)%360;let h=o,p=Math.sqrt(Math.pow(i-a,2)+Math.pow(n-e,2))/e;return[c,h,t?1-p:p]},k=(r,t)=>{let[n,i,o]=r,e=.5,a=.5,s=n/(180/Math.PI),c=(t?1-o:o)*e,h=e+c*Math.cos(s),u=a+c*Math.sin(s);return[h,u,i]},R=(r=Math.random()*360,t=[Math.random(),Math.random()],n=[.75+Math.random()*.2,.3+Math.random()*.2])=>[[r,t[0],n[0]],[(r+60+Math.random()*180)%360,t[1],n[1]]],z=(r,t)=>{let o=r-.5,e=t-.5,a=Math.hypot(o,e);return a<=.5?[r,t]:[.5+o/a*.5,.5+e/a*.5]};var Y=(r,t,n,i=!1,o=(s,c)=>c?1-s:s,e=(s,c)=>c?1-s:s,a=(s,c)=>c?1-s:s)=>{let s=o(r,i),c=e(r,i),h=a(r,i),u=(1-s)*t[0]+s*n[0],p=(1-c)*t[1]+c*n[1],l=(1-h)*t[2]+h*n[2];return[u,p,l]},B=(r,t,n=4,i=!1,o=(s,c)=>c?1-s:s,e=(s,c)=>c?1-s:s,a=(s,c)=>c?1-s:s)=>{let s=[];for(let c=0;cr,K=(r,t=!1)=>t?1-b(1-r,2):b(r,2),J=(r,t=!1)=>t?1-b(1-r,3):b(r,3),Q=(r,t=!1)=>t?1-b(1-r,4):b(r,4),tt=(r,t=!1)=>t?1-b(1-r,5):b(r,5),f=(r,t=!1)=>t?1-Math.sin((1-r)*Math.PI/2):Math.sin(r*Math.PI/2),nt=(r,t=!1)=>t?1-Math.asin(1-r)/(Math.PI/2):Math.asin(r)/(Math.PI/2),it=(r,t=!1)=>t?1-Math.sqrt(1-b(r,2)):1-Math.sqrt(1-r),ot=r=>b(r,2)*(3-2*r),$={linearPosition:W,exponentialPosition:K,quadraticPosition:J,cubicPosition:Q,quarticPosition:tt,sinusoidalPosition:f,asinusoidalPosition:nt,arcPosition:it,smoothStepPosition:ot},X=(r,t,n=!1)=>{let i=r[0],o=t[0],e=0;n&&i!==null&&o!==null?(e=Math.min(Math.abs(i-o),360-Math.abs(i-o)),e=e/360):e=i===null||o===null?0:i-o;let a=e,s=r[1]===null||t[1]===null?0:t[1]-r[1],c=r[2]===null||t[2]===null?0:t[2]-r[2];return Math.sqrt(a*a+s*s+c*c)},S=class{constructor({xyz:t,color:n,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:n,invertedLightness:i})}positionOrColor({xyz:t,color:n,invertedLightness:i=!1}){if(this._invertedLightness=i,t&&n||!t&&!n)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],i)):n&&(this.color=n,[this.x,this.y,this.z]=k(n,i))}set position([t,n,i]){this.x=t,this.y=n,this.z=i,this.color=F([this.x,this.y,this.z],this._invertedLightness)}get position(){return[this.x,this.y,this.z]}set hsl([t,n,i]){this.color=[t,n,i],[this.x,this.y,this.z]=k(this.color,this._invertedLightness)}get hsl(){return this.color}get hslCSS(){let[t,n,i]=this.color;return`hsl(${t.toFixed(2)}, ${(n*100).toFixed(2)}%, ${(i*100).toFixed(2)}%)`}get oklchCSS(){let[t,n,i]=this.color;return`oklch(${(i*100).toFixed(2)}% ${(n*.4).toFixed(3)} ${t.toFixed(2)})`}get lchCSS(){let[t,n,i]=this.color;return`lch(${(i*100).toFixed(2)}% ${(n*150).toFixed(2)} ${t.toFixed(2)})`}set invertedLightness(t){this._invertedLightness=t,this.color=F([this.x,this.y,this.z],this._invertedLightness)}get invertedLightness(){return this._invertedLightness}shiftHue(t){this.color[0]=(360+(this.color[0]+t))%360,[this.x,this.y,this.z]=k(this.color,this._invertedLightness)}},L=class{constructor({anchorColors:t=R(),numPoints:n=4,positionFunction:i=f,positionFunctionX:o,positionFunctionY:e,positionFunctionZ:a,closedLoop:s,invertedLightness:c,clampToCircle:h}={anchorColors:R(),numPoints:4,positionFunction:f,closedLoop:!1}){this._positionFunctionX=f;this._positionFunctionY=f;this._positionFunctionZ=f;this.connectLastAndFirstAnchor=!1;this._animationFrame=null;this._invertedLightness=!1;this._clampToCircle=!1;if(!t||t.length<2)throw new Error("Must have at least two anchor colors");this._anchorPoints=t.map(u=>new S({color:u,invertedLightness:c})),this._numPoints=n+2,this._positionFunctionX=o||i||f,this._positionFunctionY=e||i||f,this._positionFunctionZ=a||i||f,this.connectLastAndFirstAnchor=s||!1,this._invertedLightness=c||!1,this._clampToCircle=h||!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 clampToCircle(){return this._clampToCircle}set clampToCircle(t){this._clampToCircle=t}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 n=0;n{let o=n[0]?n[0].position:[0,0,0],e=n[1]?n[1].position:[0,0,0],a=this.shouldInvertEaseForSegment(i);return B(o,e,this._numPoints,!!a,this.positionFunctionX,this.positionFunctionY,this.positionFunctionZ).map(s=>new S({xyz:s,invertedLightness:this._invertedLightness}))})}addAnchorPoint({xyz:t,color:n,insertAtIndex:i,clamp:o}){let e=t;if((o!=null?o:this._clampToCircle)&&t){let[c,h,u]=t,[p,l]=z(c,h);e=[p,l,u]}let s=new S({xyz:e,color:n,invertedLightness:this._invertedLightness});return i!==void 0?this.anchorPoints.splice(i,0,s):this.anchorPoints.push(s),this.updateAnchorPairs(),s}removeAnchorPoint({point:t,index:n}){if(!t&&n===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(n!==void 0?i=n:t&&(i=this.anchorPoints.indexOf(t)),i>-1&&iX(s.position,t)):n&&(o=this.anchorPoints.map(s=>X(s.hsl,n,!0)));let e=Math.min(...o);if(e>i)return null;let a=o.indexOf(e);return this.anchorPoints[a]||null}set closedLoop(t){this.connectLastAndFirstAnchor=t,this.updateAnchorPairs()}get closedLoop(){return this.connectLastAndFirstAnchor}set invertedLightness(t){this._invertedLightness=t,this.anchorPoints.forEach(n=>n.invertedLightness=t),this.updateAnchorPairs()}get invertedLightness(){return this._invertedLightness}get flattenedPoints(){return this.points.flat().filter((t,n)=>n!=0?n%this._numPoints:!0)}get colors(){let t=this.flattenedPoints.map(n=>n.color);return this.connectLastAndFirstAnchor&&this._anchorPoints.length!==2&&t.pop(),t}cssColors(t="hsl"){let n={hsl:o=>o.hslCSS,oklch:o=>o.oklchCSS,lch:o=>o.lchCSS},i=this.flattenedPoints.map(n[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(n=>n.shiftHue(t)),this.updateAnchorPairs()}getColorAt(t){var v;if(t<0||t>1)throw new Error("Position must be between 0 and 1");if(this.anchorPoints.length===0)throw new Error("No anchor points available");let n=this.connectLastAndFirstAnchor?this.anchorPoints.length:this.anchorPoints.length-1,i=this.connectLastAndFirstAnchor&&this.anchorPoints.length===2?2:n,o=t*i,e=Math.floor(o),a=o-e,s=e>=i?i-1:e,c=e>=i?1:a,h=this._anchorPairs[s];if(!h||h.length<2||!h[0]||!h[1])return new S({color:((v=this.anchorPoints[0])==null?void 0:v.color)||[0,0,0],invertedLightness:this._invertedLightness});let u=h[0].position,p=h[1].position,l=this.shouldInvertEaseForSegment(s),m=Y(c,u,p,l,this._positionFunctionX,this._positionFunctionY,this._positionFunctionZ);return new S({xyz:m,invertedLightness:this._invertedLightness})}shouldInvertEaseForSegment(t){return!!(t%2||this.connectLastAndFirstAnchor&&this.anchorPoints.length===2&&t===0)}},{p5:M}=globalThis;if(M&&M.VERSION&&M.VERSION.startsWith("1.")){console.info("p5 < 1.x detected, adding poline to p5 prototype");let r=new L;M.prototype.poline=r;let t=()=>r.colors.map(n=>`hsl(${Math.round(n[0])},${n[1]*100}%,${n[2]*100}%)`);M.prototype.registerMethod("polineColors",t),globalThis.poline=r,globalThis.polineColors=t}var C="http://www.w3.org/2000/svg",g=100,w={anchorRadius:2,ringOuterRadius:5,ringThickness:1,ringThicknessHover:2,ringGap:.5,tickLength:1.5,tickGap:.5},et=1,st=2.5,x=class extends HTMLElement{constructor(){super();this.currentPoint=null;this.allowAddPoints=!1;this.ringAdjust=null;this.ringHoverIndex=null;this.boundPointerDown=this.handlePointerDown.bind(this);this.boundPointerMove=this.handlePointerMove.bind(this);this.boundPointerUp=this.handlePointerUp.bind(this);this.attachShadow({mode:"open"}),this.interactive=this.hasAttribute("interactive"),this.allowAddPoints=this.hasAttribute("allow-add-points")}connectedCallback(){this.interactive=this.hasAttribute("interactive"),this.allowAddPoints=this.hasAttribute("allow-add-points"),this.render(),this.interactive&&this.addEventListeners()}disconnectedCallback(){this.removeEventListeners()}setPoline(n){this.poline=n,this.updateSVG(),this.updateLightnessBackground()}setAllowAddPoints(n){this.allowAddPoints=n}addPointAtPosition(n,i){if(!this.poline)return null;let o=n/this.svg.clientWidth,e=i/this.svg.clientHeight,a=this.poline.addAnchorPoint({xyz:[o,e,e]});return this.updateSVG(),this.dispatchPolineChange(),a}updateLightnessBackground(){var i;let n=(i=this.shadowRoot)==null?void 0:i.querySelector(".picker");n&&this.poline&&(this.poline.invertedLightness?(n.style.setProperty("--maxL","#000"),n.style.setProperty("--minL","#fff")):(n.style.setProperty("--maxL","#fff"),n.style.setProperty("--minL","#000")))}render(){if(!this.shadowRoot)return;this.shadowRoot.innerHTML=` 2 | 92 | `,this.svg=this.createSVG();let n=document.createElement("div");n.className="picker",n.appendChild(this.svg),this.shadowRoot.appendChild(n),this.wheel=this.svg.querySelector(".wheel"),this.line=this.svg.querySelector(".wheel__line"),this.saturationRings=this.svg.querySelector(".wheel__saturation-rings"),this.anchors=this.svg.querySelector(".wheel__anchors"),this.points=this.svg.querySelector(".wheel__points"),this.poline&&(this.updateSVG(),this.updateLightnessBackground())}createSVG(){let n=document.createElementNS(C,"svg");return n.setAttribute("viewBox",`0 0 ${g} ${g}`),n.innerHTML=` 93 | 94 | 95 | 96 | 97 | 98 | 99 | `,n}updateSVG(){if(!this.poline||!this.svg)return;let n=this.poline.flattenedPoints,i=n.map(e=>{let a=this.pointToCartesian(e);if(!a)return"";let[s,c]=a;return`${s},${c}`}).filter(e=>e!=="").join(" ");this.line.setAttribute("points",i);let o=(e,a,s,c)=>{let h=e.children;for(;h.length>a.length;){let u=h[h.length-1];u&&e.removeChild(u)}a.forEach((u,p)=>{let l=h[p],m=this.pointToCartesian(u);if(!m)return;let[v=0,P=0]=m,d=c(u),A=u.hslCSS;l||(l=document.createElementNS(C,"circle"),l.setAttribute("class",s),e.appendChild(l)),l.setAttribute("cx",v.toString()),l.setAttribute("cy",P.toString()),l.setAttribute("r",d.toString()),l.setAttribute("fill",A)})};this.updateSaturationRings(),o(this.anchors,this.poline.anchorPoints,"wheel__anchor",()=>w.anchorRadius),o(this.points,n,"wheel__point",e=>.5+e.color[1])}updateSaturationRings(){if(!this.poline||!this.saturationRings)return;let n=this.poline.anchorPoints,i=Array.from(this.saturationRings.querySelectorAll(".wheel__ring-group"));for(;i.length>n.length;){let o=i.pop();o&&o.remove()}n.forEach((o,e)=>{let a=this.pointToCartesian(o);if(!a)return;let[s,c]=a,h=o.z,u=w.anchorRadius+w.ringGap+1,p=this.ringHoverIndex===e||this.ringAdjust&&this.ringAdjust.anchorIndex===e,l=i[e];if(!l){l=document.createElementNS(C,"g"),l.setAttribute("class","wheel__ring-group");let T=document.createElementNS(C,"circle");T.setAttribute("class","wheel__ring-bg"),l.appendChild(T);let I=document.createElementNS(C,"path");I.setAttribute("class","wheel__saturation-ring"),l.appendChild(I);let G=document.createElementNS(C,"line");G.setAttribute("class","wheel__ring-tick"),l.appendChild(G),this.saturationRings.appendChild(l),i.push(l)}l.classList.toggle("wheel__ring-group--hover",!!p);let m=l.querySelector(".wheel__ring-bg");m.setAttribute("cx",s.toString()),m.setAttribute("cy",c.toString()),m.setAttribute("r",u.toString());let v=l.querySelector(".wheel__saturation-ring"),P=-Math.PI/2,d=P+h*Math.PI*2,A=this.describeArc(s,c,u,P,d);v.setAttribute("d",A);let _=l.querySelector(".wheel__ring-tick"),V=s+(u+w.tickGap)*Math.cos(d),E=c+(u+w.tickGap)*Math.sin(d),j=V+Math.cos(d)*w.tickLength,H=E+Math.sin(d)*w.tickLength;_.setAttribute("x1",V.toString()),_.setAttribute("y1",E.toString()),_.setAttribute("x2",j.toString()),_.setAttribute("y2",H.toString())})}describeArc(n,i,o,e,a){let s=a-e;if(Math.abs(s)<.001)return"";if(Math.abs(s)>Math.PI*2-.01){let v=e+Math.PI,P=n+o*Math.cos(e),d=i+o*Math.sin(e),A=n+o*Math.cos(v),_=i+o*Math.sin(v);return`M ${P} ${d} A ${o} ${o} 0 1 1 ${A} ${_} A ${o} ${o} 0 1 1 ${P} ${d}`}let c=n+o*Math.cos(e),h=i+o*Math.sin(e),u=n+o*Math.cos(a),p=i+o*Math.sin(a),l=s>Math.PI?1:0;return`M ${c} ${h} A ${o} ${o} 0 ${l} ${1} ${u} ${p}`}pointToCartesian(n){let i=g/2,o=i+(n.x-.5)*g,e=i+(n.y-.5)*g;return[o,e]}addEventListeners(){this.svg&&(this.svg.addEventListener("pointerdown",this.boundPointerDown),this.svg.addEventListener("pointermove",this.boundPointerMove),this.svg.addEventListener("pointerup",this.boundPointerUp))}removeEventListeners(){this.svg&&(this.svg.removeEventListener("pointerdown",this.boundPointerDown),this.svg.removeEventListener("pointermove",this.boundPointerMove),this.svg.removeEventListener("pointerup",this.boundPointerUp))}handlePointerDown(n){n.stopPropagation();let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n),a=window.matchMedia("(pointer: coarse)").matches?null:this.pickRing(i,o);if(a!==null){let c=this.poline.anchorPoints[a];if(!c)return;let h=this.pointToCartesian(c);if(!h)return;let[u,p]=h,l=i*g,m=o*g,v=Math.atan2(m-p,l-u);this.ringAdjust={anchorIndex:a,startSaturation:c.color[1],startAngle:v,prevAngle:v,accumulatedAngle:0},this.ringHoverIndex=a,this.classList.add("ring-adjusting"),this.updateSaturationRings();try{this.svg.setPointerCapture(n.pointerId)}catch(P){}return}let s=this.poline.getClosestAnchorPoint({xyz:[i,o,null],maxDistance:.05});s?this.currentPoint=s:this.allowAddPoints&&(this.currentPoint=this.poline.addAnchorPoint({xyz:[i,o,o]}),this.updateSVG(),this.dispatchPolineChange())}handlePointerMove(n){let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n);if(this.ringAdjust){let e=this.poline.anchorPoints[this.ringAdjust.anchorIndex];if(!e)return;let a=this.pointToCartesian(e);if(!a)return;let[s,c]=a,h=i*g,u=o*g,p=Math.atan2(u-c,h-s),l=p-this.ringAdjust.prevAngle;l>Math.PI?l-=Math.PI*2:l<-Math.PI&&(l+=Math.PI*2),this.ringAdjust.accumulatedAngle+=l,this.ringAdjust.prevAngle=p;let m=this.ringAdjust.accumulatedAngle/(Math.PI*2),v=n.shiftKey?st:et,P=m/v,d=this.clamp01(this.ringAdjust.startSaturation+P);d>.99&&(d=1),d<.01&&(d=0);let A=d===0||d===1,_=d===1&&P>0||d===0&&P<0;A&&_&&(this.ringAdjust.startSaturation=d,this.ringAdjust.accumulatedAngle=0,this.ringAdjust.prevAngle=p),this.poline.updateAnchorPoint({point:e,color:[e.color[0],d,e.color[2]]}),this.updateSVG(),this.dispatchPolineChange();return}if(this.currentPoint){this.poline.updateAnchorPoint({point:this.currentPoint,xyz:[i,o,this.currentPoint.z]}),this.updateSVG(),this.dispatchPolineChange();return}if(!window.matchMedia("(pointer: coarse)").matches){let e=this.pickRing(i,o);e!==this.ringHoverIndex&&(this.ringHoverIndex=e,this.classList.toggle("ring-hover",e!==null),this.updateSaturationRings())}}handlePointerUp(n){if(this.ringAdjust){try{this.svg.releasePointerCapture(n.pointerId)}catch(a){}this.classList.remove("ring-adjusting")}this.ringAdjust=null,this.currentPoint=null;let{normalizedX:i,normalizedY:o}=this.pointerToNormalizedCoordinates(n),e=this.pickRing(i,o);e!==this.ringHoverIndex&&(this.ringHoverIndex=e,this.classList.toggle("ring-hover",e!==null),this.updateSaturationRings())}pickRing(n,i){if(!this.poline)return null;let o=n*g,e=i*g;for(let a=0;aw.anchorRadius&&p<=w.ringOuterRadius)return a}return null}clamp01(n){return Math.max(0,Math.min(1,n))}pointerToNormalizedCoordinates(n){let i=this.svg.getBoundingClientRect(),o=(n.clientX-i.left)/i.width*g,e=(n.clientY-i.top)/i.height*g;return{normalizedX:o/g,normalizedY:e/g}}dispatchPolineChange(){this.dispatchEvent(new CustomEvent("poline-change",{detail:{poline:this.poline}}))}};customElements.define("poline-picker",x);return D(rt);})(); 100 | -------------------------------------------------------------------------------- /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 clampToCircle = (x, y) => { 30 | const cx = 0.5; 31 | const cy = 0.5; 32 | const dx = x - cx; 33 | const dy = y - cy; 34 | const dist = Math.hypot(dx, dy); 35 | if (dist <= 0.5) { 36 | return [x, y]; 37 | } 38 | return [cx + dx / dist * 0.5, cy + dy / dist * 0.5]; 39 | }; 40 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 41 | 0.75 + Math.random() * 0.2, 42 | Math.random() * 0.2, 43 | 0.75 + Math.random() * 0.2 44 | ]) => [ 45 | [startHue, saturations[0], lightnesses[0]], 46 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 47 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 48 | ]; 49 | 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) => { 50 | const tModifiedX = fx(t, invert); 51 | const tModifiedY = fy(t, invert); 52 | const tModifiedZ = fz(t, invert); 53 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 54 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 55 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 56 | return [x, y, z]; 57 | }; 58 | 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) => { 59 | const points = []; 60 | for (let i = 0; i < numPoints; i++) { 61 | const [x, y, z] = vectorOnLine( 62 | i / (numPoints - 1), 63 | p1, 64 | p2, 65 | invert, 66 | fx, 67 | fy, 68 | fz 69 | ); 70 | points.push([x, y, z]); 71 | } 72 | return points; 73 | }; 74 | var linearPosition = (t) => { 75 | return t; 76 | }; 77 | var exponentialPosition = (t, reverse = false) => { 78 | if (reverse) { 79 | return 1 - (1 - t) ** 2; 80 | } 81 | return t ** 2; 82 | }; 83 | var quadraticPosition = (t, reverse = false) => { 84 | if (reverse) { 85 | return 1 - (1 - t) ** 3; 86 | } 87 | return t ** 3; 88 | }; 89 | var cubicPosition = (t, reverse = false) => { 90 | if (reverse) { 91 | return 1 - (1 - t) ** 4; 92 | } 93 | return t ** 4; 94 | }; 95 | var quarticPosition = (t, reverse = false) => { 96 | if (reverse) { 97 | return 1 - (1 - t) ** 5; 98 | } 99 | return t ** 5; 100 | }; 101 | var sinusoidalPosition = (t, reverse = false) => { 102 | if (reverse) { 103 | return 1 - Math.sin((1 - t) * Math.PI / 2); 104 | } 105 | return Math.sin(t * Math.PI / 2); 106 | }; 107 | var asinusoidalPosition = (t, reverse = false) => { 108 | if (reverse) { 109 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 110 | } 111 | return Math.asin(t) / (Math.PI / 2); 112 | }; 113 | var arcPosition = (t, reverse = false) => { 114 | if (reverse) { 115 | return 1 - Math.sqrt(1 - t ** 2); 116 | } 117 | return 1 - Math.sqrt(1 - t); 118 | }; 119 | var smoothStepPosition = (t) => { 120 | return t ** 2 * (3 - 2 * t); 121 | }; 122 | var positionFunctions = { 123 | linearPosition, 124 | exponentialPosition, 125 | quadraticPosition, 126 | cubicPosition, 127 | quarticPosition, 128 | sinusoidalPosition, 129 | asinusoidalPosition, 130 | arcPosition, 131 | smoothStepPosition 132 | }; 133 | var distance = (p1, p2, hueMode = false) => { 134 | const a1 = p1[0]; 135 | const a2 = p2[0]; 136 | let diffA = 0; 137 | if (hueMode && a1 !== null && a2 !== null) { 138 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 139 | diffA = diffA / 360; 140 | } else { 141 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 142 | } 143 | const a = diffA; 144 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 145 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 146 | return Math.sqrt(a * a + b * b + c * c); 147 | }; 148 | var ColorPoint = class { 149 | constructor({ 150 | xyz, 151 | color, 152 | invertedLightness = false 153 | } = {}) { 154 | this.x = 0; 155 | this.y = 0; 156 | this.z = 0; 157 | this.color = [0, 0, 0]; 158 | this._invertedLightness = false; 159 | this._invertedLightness = invertedLightness; 160 | this.positionOrColor({ xyz, color, invertedLightness }); 161 | } 162 | positionOrColor({ 163 | xyz, 164 | color, 165 | invertedLightness = false 166 | }) { 167 | this._invertedLightness = invertedLightness; 168 | if (xyz && color || !xyz && !color) { 169 | throw new Error("Point must be initialized with either x,y,z or hsl"); 170 | } else if (xyz) { 171 | this.x = xyz[0]; 172 | this.y = xyz[1]; 173 | this.z = xyz[2]; 174 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 175 | } else if (color) { 176 | this.color = color; 177 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 178 | } 179 | } 180 | set position([x, y, z]) { 181 | this.x = x; 182 | this.y = y; 183 | this.z = z; 184 | this.color = pointToHSL( 185 | [this.x, this.y, this.z], 186 | this._invertedLightness 187 | ); 188 | } 189 | get position() { 190 | return [this.x, this.y, this.z]; 191 | } 192 | set hsl([h, s, l]) { 193 | this.color = [h, s, l]; 194 | [this.x, this.y, this.z] = hslToPoint( 195 | this.color, 196 | this._invertedLightness 197 | ); 198 | } 199 | get hsl() { 200 | return this.color; 201 | } 202 | get hslCSS() { 203 | const [h, s, l] = this.color; 204 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 205 | 2 206 | )}%)`; 207 | } 208 | get oklchCSS() { 209 | const [h, s, l] = this.color; 210 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 211 | 2 212 | )})`; 213 | } 214 | get lchCSS() { 215 | const [h, s, l] = this.color; 216 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 217 | 2 218 | )})`; 219 | } 220 | set invertedLightness(val) { 221 | this._invertedLightness = val; 222 | this.color = pointToHSL( 223 | [this.x, this.y, this.z], 224 | this._invertedLightness 225 | ); 226 | } 227 | get invertedLightness() { 228 | return this._invertedLightness; 229 | } 230 | shiftHue(angle) { 231 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 232 | [this.x, this.y, this.z] = hslToPoint( 233 | this.color, 234 | this._invertedLightness 235 | ); 236 | } 237 | }; 238 | var Poline = class { 239 | constructor({ 240 | anchorColors = randomHSLPair(), 241 | numPoints = 4, 242 | positionFunction = sinusoidalPosition, 243 | positionFunctionX, 244 | positionFunctionY, 245 | positionFunctionZ, 246 | closedLoop, 247 | invertedLightness, 248 | clampToCircle: clampToCircle2 249 | } = { 250 | anchorColors: randomHSLPair(), 251 | numPoints: 4, 252 | positionFunction: sinusoidalPosition, 253 | closedLoop: false 254 | }) { 255 | this._positionFunctionX = sinusoidalPosition; 256 | this._positionFunctionY = sinusoidalPosition; 257 | this._positionFunctionZ = sinusoidalPosition; 258 | this.connectLastAndFirstAnchor = false; 259 | this._animationFrame = null; 260 | this._invertedLightness = false; 261 | this._clampToCircle = false; 262 | if (!anchorColors || anchorColors.length < 2) { 263 | throw new Error("Must have at least two anchor colors"); 264 | } 265 | this._anchorPoints = anchorColors.map( 266 | (point) => new ColorPoint({ color: point, invertedLightness }) 267 | ); 268 | this._numPoints = numPoints + 2; 269 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 270 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 271 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 272 | this.connectLastAndFirstAnchor = closedLoop || false; 273 | this._invertedLightness = invertedLightness || false; 274 | this._clampToCircle = clampToCircle2 || false; 275 | this.updateAnchorPairs(); 276 | } 277 | get numPoints() { 278 | return this._numPoints - 2; 279 | } 280 | set numPoints(numPoints) { 281 | if (numPoints < 1) { 282 | throw new Error("Must have at least one point"); 283 | } 284 | this._numPoints = numPoints + 2; 285 | this.updateAnchorPairs(); 286 | } 287 | set positionFunction(positionFunction) { 288 | if (Array.isArray(positionFunction)) { 289 | if (positionFunction.length !== 3) { 290 | throw new Error("Position function array must have 3 elements"); 291 | } 292 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 293 | throw new Error("Position function array must have 3 functions"); 294 | } 295 | this._positionFunctionX = positionFunction[0]; 296 | this._positionFunctionY = positionFunction[1]; 297 | this._positionFunctionZ = positionFunction[2]; 298 | } else { 299 | this._positionFunctionX = positionFunction; 300 | this._positionFunctionY = positionFunction; 301 | this._positionFunctionZ = positionFunction; 302 | } 303 | this.updateAnchorPairs(); 304 | } 305 | get positionFunction() { 306 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 307 | return this._positionFunctionX; 308 | } 309 | return [ 310 | this._positionFunctionX, 311 | this._positionFunctionY, 312 | this._positionFunctionZ 313 | ]; 314 | } 315 | set positionFunctionX(positionFunctionX) { 316 | this._positionFunctionX = positionFunctionX; 317 | this.updateAnchorPairs(); 318 | } 319 | get positionFunctionX() { 320 | return this._positionFunctionX; 321 | } 322 | set positionFunctionY(positionFunctionY) { 323 | this._positionFunctionY = positionFunctionY; 324 | this.updateAnchorPairs(); 325 | } 326 | get positionFunctionY() { 327 | return this._positionFunctionY; 328 | } 329 | set positionFunctionZ(positionFunctionZ) { 330 | this._positionFunctionZ = positionFunctionZ; 331 | this.updateAnchorPairs(); 332 | } 333 | get positionFunctionZ() { 334 | return this._positionFunctionZ; 335 | } 336 | get clampToCircle() { 337 | return this._clampToCircle; 338 | } 339 | set clampToCircle(clamp) { 340 | this._clampToCircle = clamp; 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 | const shouldInvertEase = this.shouldInvertEaseForSegment(i); 363 | return vectorsOnLine( 364 | p1position, 365 | p2position, 366 | this._numPoints, 367 | shouldInvertEase ? true : false, 368 | this.positionFunctionX, 369 | this.positionFunctionY, 370 | this.positionFunctionZ 371 | ).map( 372 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 373 | ); 374 | }); 375 | } 376 | addAnchorPoint({ 377 | xyz, 378 | color, 379 | insertAtIndex, 380 | clamp 381 | }) { 382 | let finalXyz = xyz; 383 | const shouldClamp = clamp ?? this._clampToCircle; 384 | if (shouldClamp && xyz) { 385 | const [x, y, z] = xyz; 386 | const [cx, cy] = clampToCircle(x, y); 387 | finalXyz = [cx, cy, z]; 388 | } 389 | const newAnchor = new ColorPoint({ 390 | xyz: finalXyz, 391 | color, 392 | invertedLightness: this._invertedLightness 393 | }); 394 | if (insertAtIndex !== void 0) { 395 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 396 | } else { 397 | this.anchorPoints.push(newAnchor); 398 | } 399 | this.updateAnchorPairs(); 400 | return newAnchor; 401 | } 402 | removeAnchorPoint({ 403 | point, 404 | index 405 | }) { 406 | if (!point && index === void 0) { 407 | throw new Error("Must provide a point or index"); 408 | } 409 | if (this.anchorPoints.length < 3) { 410 | throw new Error("Must have at least two anchor points"); 411 | } 412 | let apid; 413 | if (index !== void 0) { 414 | apid = index; 415 | } else if (point) { 416 | apid = this.anchorPoints.indexOf(point); 417 | } 418 | if (apid > -1 && apid < this.anchorPoints.length) { 419 | this.anchorPoints.splice(apid, 1); 420 | this.updateAnchorPairs(); 421 | } else { 422 | throw new Error("Point not found"); 423 | } 424 | } 425 | updateAnchorPoint({ 426 | point, 427 | pointIndex, 428 | xyz, 429 | color, 430 | clamp 431 | }) { 432 | if (pointIndex !== void 0) { 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 | const shouldClamp = clamp ?? this._clampToCircle; 443 | if (shouldClamp) { 444 | const [x, y, z] = xyz; 445 | const [cx, cy] = clampToCircle(x, y); 446 | point.position = [cx, cy, z]; 447 | } else { 448 | point.position = xyz; 449 | } 450 | } 451 | if (color) 452 | point.hsl = color; 453 | this.updateAnchorPairs(); 454 | return point; 455 | } 456 | getClosestAnchorPoint({ 457 | xyz, 458 | hsl, 459 | maxDistance = 1 460 | }) { 461 | if (!xyz && !hsl) { 462 | throw new Error("Must provide a xyz or hsl"); 463 | } 464 | let distances; 465 | if (xyz) { 466 | distances = this.anchorPoints.map( 467 | (anchor) => distance(anchor.position, xyz) 468 | ); 469 | } else if (hsl) { 470 | distances = this.anchorPoints.map( 471 | (anchor) => distance(anchor.hsl, hsl, true) 472 | ); 473 | } 474 | const minDistance = Math.min(...distances); 475 | if (minDistance > maxDistance) { 476 | return null; 477 | } 478 | const closestAnchorIndex = distances.indexOf(minDistance); 479 | return this.anchorPoints[closestAnchorIndex] || null; 480 | } 481 | set closedLoop(newStatus) { 482 | this.connectLastAndFirstAnchor = newStatus; 483 | this.updateAnchorPairs(); 484 | } 485 | get closedLoop() { 486 | return this.connectLastAndFirstAnchor; 487 | } 488 | set invertedLightness(newStatus) { 489 | this._invertedLightness = newStatus; 490 | this.anchorPoints.forEach((p) => p.invertedLightness = newStatus); 491 | this.updateAnchorPairs(); 492 | } 493 | get invertedLightness() { 494 | return this._invertedLightness; 495 | } 496 | /** 497 | * Returns a flattened array of all points across all segments, 498 | * removing duplicated anchor points at segment boundaries. 499 | * 500 | * Since anchor points exist at both the end of one segment and 501 | * the beginning of the next, this method keeps only one instance of each. 502 | * The filter logic keeps the first point (index 0) and then filters out 503 | * points whose indices are multiples of the segment size (_numPoints), 504 | * which are the anchor points at the start of each segment (except the first). 505 | * 506 | * This approach ensures we get all unique points in the correct order 507 | * while avoiding duplicated anchor points. 508 | * 509 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 510 | */ 511 | get flattenedPoints() { 512 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 513 | } 514 | get colors() { 515 | const colors = this.flattenedPoints.map((p) => p.color); 516 | if (this.connectLastAndFirstAnchor && this._anchorPoints.length !== 2) { 517 | colors.pop(); 518 | } 519 | return colors; 520 | } 521 | cssColors(mode = "hsl") { 522 | const methods = { 523 | hsl: (p) => p.hslCSS, 524 | oklch: (p) => p.oklchCSS, 525 | lch: (p) => p.lchCSS 526 | }; 527 | const cssColors = this.flattenedPoints.map(methods[mode]); 528 | if (this.connectLastAndFirstAnchor) { 529 | cssColors.pop(); 530 | } 531 | return cssColors; 532 | } 533 | get colorsCSS() { 534 | return this.cssColors("hsl"); 535 | } 536 | get colorsCSSlch() { 537 | return this.cssColors("lch"); 538 | } 539 | get colorsCSSoklch() { 540 | return this.cssColors("oklch"); 541 | } 542 | shiftHue(hShift = 20) { 543 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 544 | this.updateAnchorPairs(); 545 | } 546 | /** 547 | * Returns a color at a specific position along the entire color line (0-1) 548 | * Treats all segments as one continuous path, respecting easing functions 549 | * @param t Position along the line (0-1), where 0 is start and 1 is end 550 | * @returns ColorPoint at the specified position 551 | * @example 552 | * getColorAt(0) // Returns color at the very beginning 553 | * getColorAt(0.5) // Returns color at the middle of the entire journey 554 | * getColorAt(1) // Returns color at the very end 555 | */ 556 | getColorAt(t) { 557 | if (t < 0 || t > 1) { 558 | throw new Error("Position must be between 0 and 1"); 559 | } 560 | if (this.anchorPoints.length === 0) { 561 | throw new Error("No anchor points available"); 562 | } 563 | const totalSegments = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 564 | const effectiveSegments = this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 ? 2 : totalSegments; 565 | const segmentPosition = t * effectiveSegments; 566 | const segmentIndex = Math.floor(segmentPosition); 567 | const localT = segmentPosition - segmentIndex; 568 | const actualSegmentIndex = segmentIndex >= effectiveSegments ? effectiveSegments - 1 : segmentIndex; 569 | const actualLocalT = segmentIndex >= effectiveSegments ? 1 : localT; 570 | const pair = this._anchorPairs[actualSegmentIndex]; 571 | if (!pair || pair.length < 2 || !pair[0] || !pair[1]) { 572 | return new ColorPoint({ 573 | color: this.anchorPoints[0]?.color || [0, 0, 0], 574 | invertedLightness: this._invertedLightness 575 | }); 576 | } 577 | const p1position = pair[0].position; 578 | const p2position = pair[1].position; 579 | const shouldInvertEase = this.shouldInvertEaseForSegment(actualSegmentIndex); 580 | const xyz = vectorOnLine( 581 | actualLocalT, 582 | p1position, 583 | p2position, 584 | shouldInvertEase, 585 | this._positionFunctionX, 586 | this._positionFunctionY, 587 | this._positionFunctionZ 588 | ); 589 | return new ColorPoint({ 590 | xyz, 591 | invertedLightness: this._invertedLightness 592 | }); 593 | } 594 | /** 595 | * Determines whether easing should be inverted for a given segment 596 | * @param segmentIndex The index of the segment 597 | * @returns Whether easing should be inverted 598 | */ 599 | shouldInvertEaseForSegment(segmentIndex) { 600 | return !!(segmentIndex % 2 || this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 && segmentIndex === 0); 601 | } 602 | }; 603 | var { p5 } = globalThis; 604 | if (p5 && p5.VERSION && p5.VERSION.startsWith("1.")) { 605 | console.info("p5 < 1.x detected, adding poline to p5 prototype"); 606 | const poline = new Poline(); 607 | p5.prototype.poline = poline; 608 | const polineColors = () => poline.colors.map( 609 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 610 | ); 611 | p5.prototype.registerMethod("polineColors", polineColors); 612 | globalThis.poline = poline; 613 | globalThis.polineColors = polineColors; 614 | } 615 | export { 616 | ColorPoint, 617 | Poline, 618 | clampToCircle, 619 | hslToPoint, 620 | pointToHSL, 621 | positionFunctions, 622 | randomHSLPair, 623 | randomHSLTriple 624 | }; 625 | -------------------------------------------------------------------------------- /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 __export = (target, all) => { 7 | for (var name in all) 8 | __defProp(target, name, { get: all[name], enumerable: true }); 9 | }; 10 | var __copyProps = (to, from, except, desc) => { 11 | if (from && typeof from === "object" || typeof from === "function") { 12 | for (let key of __getOwnPropNames(from)) 13 | if (!__hasOwnProp.call(to, key) && key !== except) 14 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 15 | } 16 | return to; 17 | }; 18 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 19 | 20 | // src/index.ts 21 | var src_exports = {}; 22 | __export(src_exports, { 23 | ColorPoint: () => ColorPoint, 24 | Poline: () => Poline, 25 | clampToCircle: () => clampToCircle, 26 | hslToPoint: () => hslToPoint, 27 | pointToHSL: () => pointToHSL, 28 | positionFunctions: () => positionFunctions, 29 | randomHSLPair: () => randomHSLPair, 30 | randomHSLTriple: () => randomHSLTriple 31 | }); 32 | module.exports = __toCommonJS(src_exports); 33 | var pointToHSL = (xyz, invertedLightness) => { 34 | const [x, y, z] = xyz; 35 | const cx = 0.5; 36 | const cy = 0.5; 37 | const radians = Math.atan2(y - cy, x - cx); 38 | let deg = radians * (180 / Math.PI); 39 | deg = (360 + deg) % 360; 40 | const s = z; 41 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 42 | const l = dist / cx; 43 | return [deg, s, invertedLightness ? 1 - l : l]; 44 | }; 45 | var hslToPoint = (hsl, invertedLightness) => { 46 | const [h, s, l] = hsl; 47 | const cx = 0.5; 48 | const cy = 0.5; 49 | const radians = h / (180 / Math.PI); 50 | const dist = (invertedLightness ? 1 - l : l) * cx; 51 | const x = cx + dist * Math.cos(radians); 52 | const y = cy + dist * Math.sin(radians); 53 | const z = s; 54 | return [x, y, z]; 55 | }; 56 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 57 | [startHue, saturations[0], lightnesses[0]], 58 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 59 | ]; 60 | var clampToCircle = (x, y) => { 61 | const cx = 0.5; 62 | const cy = 0.5; 63 | const dx = x - cx; 64 | const dy = y - cy; 65 | const dist = Math.hypot(dx, dy); 66 | if (dist <= 0.5) { 67 | return [x, y]; 68 | } 69 | return [cx + dx / dist * 0.5, cy + dy / dist * 0.5]; 70 | }; 71 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 72 | 0.75 + Math.random() * 0.2, 73 | Math.random() * 0.2, 74 | 0.75 + Math.random() * 0.2 75 | ]) => [ 76 | [startHue, saturations[0], lightnesses[0]], 77 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 78 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 79 | ]; 80 | 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) => { 81 | const tModifiedX = fx(t, invert); 82 | const tModifiedY = fy(t, invert); 83 | const tModifiedZ = fz(t, invert); 84 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 85 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 86 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 87 | return [x, y, z]; 88 | }; 89 | 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) => { 90 | const points = []; 91 | for (let i = 0; i < numPoints; i++) { 92 | const [x, y, z] = vectorOnLine( 93 | i / (numPoints - 1), 94 | p1, 95 | p2, 96 | invert, 97 | fx, 98 | fy, 99 | fz 100 | ); 101 | points.push([x, y, z]); 102 | } 103 | return points; 104 | }; 105 | var linearPosition = (t) => { 106 | return t; 107 | }; 108 | var exponentialPosition = (t, reverse = false) => { 109 | if (reverse) { 110 | return 1 - (1 - t) ** 2; 111 | } 112 | return t ** 2; 113 | }; 114 | var quadraticPosition = (t, reverse = false) => { 115 | if (reverse) { 116 | return 1 - (1 - t) ** 3; 117 | } 118 | return t ** 3; 119 | }; 120 | var cubicPosition = (t, reverse = false) => { 121 | if (reverse) { 122 | return 1 - (1 - t) ** 4; 123 | } 124 | return t ** 4; 125 | }; 126 | var quarticPosition = (t, reverse = false) => { 127 | if (reverse) { 128 | return 1 - (1 - t) ** 5; 129 | } 130 | return t ** 5; 131 | }; 132 | var sinusoidalPosition = (t, reverse = false) => { 133 | if (reverse) { 134 | return 1 - Math.sin((1 - t) * Math.PI / 2); 135 | } 136 | return Math.sin(t * Math.PI / 2); 137 | }; 138 | var asinusoidalPosition = (t, reverse = false) => { 139 | if (reverse) { 140 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 141 | } 142 | return Math.asin(t) / (Math.PI / 2); 143 | }; 144 | var arcPosition = (t, reverse = false) => { 145 | if (reverse) { 146 | return 1 - Math.sqrt(1 - t ** 2); 147 | } 148 | return 1 - Math.sqrt(1 - t); 149 | }; 150 | var smoothStepPosition = (t) => { 151 | return t ** 2 * (3 - 2 * t); 152 | }; 153 | var positionFunctions = { 154 | linearPosition, 155 | exponentialPosition, 156 | quadraticPosition, 157 | cubicPosition, 158 | quarticPosition, 159 | sinusoidalPosition, 160 | asinusoidalPosition, 161 | arcPosition, 162 | smoothStepPosition 163 | }; 164 | var distance = (p1, p2, hueMode = false) => { 165 | const a1 = p1[0]; 166 | const a2 = p2[0]; 167 | let diffA = 0; 168 | if (hueMode && a1 !== null && a2 !== null) { 169 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 170 | diffA = diffA / 360; 171 | } else { 172 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 173 | } 174 | const a = diffA; 175 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 176 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 177 | return Math.sqrt(a * a + b * b + c * c); 178 | }; 179 | var ColorPoint = class { 180 | constructor({ 181 | xyz, 182 | color, 183 | invertedLightness = false 184 | } = {}) { 185 | this.x = 0; 186 | this.y = 0; 187 | this.z = 0; 188 | this.color = [0, 0, 0]; 189 | this._invertedLightness = false; 190 | this._invertedLightness = invertedLightness; 191 | this.positionOrColor({ xyz, color, invertedLightness }); 192 | } 193 | positionOrColor({ 194 | xyz, 195 | color, 196 | invertedLightness = false 197 | }) { 198 | this._invertedLightness = invertedLightness; 199 | if (xyz && color || !xyz && !color) { 200 | throw new Error("Point must be initialized with either x,y,z or hsl"); 201 | } else if (xyz) { 202 | this.x = xyz[0]; 203 | this.y = xyz[1]; 204 | this.z = xyz[2]; 205 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 206 | } else if (color) { 207 | this.color = color; 208 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 209 | } 210 | } 211 | set position([x, y, z]) { 212 | this.x = x; 213 | this.y = y; 214 | this.z = z; 215 | this.color = pointToHSL( 216 | [this.x, this.y, this.z], 217 | this._invertedLightness 218 | ); 219 | } 220 | get position() { 221 | return [this.x, this.y, this.z]; 222 | } 223 | set hsl([h, s, l]) { 224 | this.color = [h, s, l]; 225 | [this.x, this.y, this.z] = hslToPoint( 226 | this.color, 227 | this._invertedLightness 228 | ); 229 | } 230 | get hsl() { 231 | return this.color; 232 | } 233 | get hslCSS() { 234 | const [h, s, l] = this.color; 235 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 236 | 2 237 | )}%)`; 238 | } 239 | get oklchCSS() { 240 | const [h, s, l] = this.color; 241 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 242 | 2 243 | )})`; 244 | } 245 | get lchCSS() { 246 | const [h, s, l] = this.color; 247 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 248 | 2 249 | )})`; 250 | } 251 | set invertedLightness(val) { 252 | this._invertedLightness = val; 253 | this.color = pointToHSL( 254 | [this.x, this.y, this.z], 255 | this._invertedLightness 256 | ); 257 | } 258 | get invertedLightness() { 259 | return this._invertedLightness; 260 | } 261 | shiftHue(angle) { 262 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 263 | [this.x, this.y, this.z] = hslToPoint( 264 | this.color, 265 | this._invertedLightness 266 | ); 267 | } 268 | }; 269 | var Poline = class { 270 | constructor({ 271 | anchorColors = randomHSLPair(), 272 | numPoints = 4, 273 | positionFunction = sinusoidalPosition, 274 | positionFunctionX, 275 | positionFunctionY, 276 | positionFunctionZ, 277 | closedLoop, 278 | invertedLightness, 279 | clampToCircle: clampToCircle2 280 | } = { 281 | anchorColors: randomHSLPair(), 282 | numPoints: 4, 283 | positionFunction: sinusoidalPosition, 284 | closedLoop: false 285 | }) { 286 | this._positionFunctionX = sinusoidalPosition; 287 | this._positionFunctionY = sinusoidalPosition; 288 | this._positionFunctionZ = sinusoidalPosition; 289 | this.connectLastAndFirstAnchor = false; 290 | this._animationFrame = null; 291 | this._invertedLightness = false; 292 | this._clampToCircle = false; 293 | if (!anchorColors || anchorColors.length < 2) { 294 | throw new Error("Must have at least two anchor colors"); 295 | } 296 | this._anchorPoints = anchorColors.map( 297 | (point) => new ColorPoint({ color: point, invertedLightness }) 298 | ); 299 | this._numPoints = numPoints + 2; 300 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 301 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 302 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 303 | this.connectLastAndFirstAnchor = closedLoop || false; 304 | this._invertedLightness = invertedLightness || false; 305 | this._clampToCircle = clampToCircle2 || false; 306 | this.updateAnchorPairs(); 307 | } 308 | get numPoints() { 309 | return this._numPoints - 2; 310 | } 311 | set numPoints(numPoints) { 312 | if (numPoints < 1) { 313 | throw new Error("Must have at least one point"); 314 | } 315 | this._numPoints = numPoints + 2; 316 | this.updateAnchorPairs(); 317 | } 318 | set positionFunction(positionFunction) { 319 | if (Array.isArray(positionFunction)) { 320 | if (positionFunction.length !== 3) { 321 | throw new Error("Position function array must have 3 elements"); 322 | } 323 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 324 | throw new Error("Position function array must have 3 functions"); 325 | } 326 | this._positionFunctionX = positionFunction[0]; 327 | this._positionFunctionY = positionFunction[1]; 328 | this._positionFunctionZ = positionFunction[2]; 329 | } else { 330 | this._positionFunctionX = positionFunction; 331 | this._positionFunctionY = positionFunction; 332 | this._positionFunctionZ = positionFunction; 333 | } 334 | this.updateAnchorPairs(); 335 | } 336 | get positionFunction() { 337 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 338 | return this._positionFunctionX; 339 | } 340 | return [ 341 | this._positionFunctionX, 342 | this._positionFunctionY, 343 | this._positionFunctionZ 344 | ]; 345 | } 346 | set positionFunctionX(positionFunctionX) { 347 | this._positionFunctionX = positionFunctionX; 348 | this.updateAnchorPairs(); 349 | } 350 | get positionFunctionX() { 351 | return this._positionFunctionX; 352 | } 353 | set positionFunctionY(positionFunctionY) { 354 | this._positionFunctionY = positionFunctionY; 355 | this.updateAnchorPairs(); 356 | } 357 | get positionFunctionY() { 358 | return this._positionFunctionY; 359 | } 360 | set positionFunctionZ(positionFunctionZ) { 361 | this._positionFunctionZ = positionFunctionZ; 362 | this.updateAnchorPairs(); 363 | } 364 | get positionFunctionZ() { 365 | return this._positionFunctionZ; 366 | } 367 | get clampToCircle() { 368 | return this._clampToCircle; 369 | } 370 | set clampToCircle(clamp) { 371 | this._clampToCircle = clamp; 372 | } 373 | get anchorPoints() { 374 | return this._anchorPoints; 375 | } 376 | set anchorPoints(anchorPoints) { 377 | this._anchorPoints = anchorPoints; 378 | this.updateAnchorPairs(); 379 | } 380 | updateAnchorPairs() { 381 | this._anchorPairs = []; 382 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 383 | for (let i = 0; i < anchorPointsLength; i++) { 384 | const pair = [ 385 | this.anchorPoints[i], 386 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 387 | ]; 388 | this._anchorPairs.push(pair); 389 | } 390 | this.points = this._anchorPairs.map((pair, i) => { 391 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 392 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 393 | const shouldInvertEase = this.shouldInvertEaseForSegment(i); 394 | return vectorsOnLine( 395 | p1position, 396 | p2position, 397 | this._numPoints, 398 | shouldInvertEase ? true : false, 399 | this.positionFunctionX, 400 | this.positionFunctionY, 401 | this.positionFunctionZ 402 | ).map( 403 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 404 | ); 405 | }); 406 | } 407 | addAnchorPoint({ 408 | xyz, 409 | color, 410 | insertAtIndex, 411 | clamp 412 | }) { 413 | let finalXyz = xyz; 414 | const shouldClamp = clamp != null ? clamp : this._clampToCircle; 415 | if (shouldClamp && xyz) { 416 | const [x, y, z] = xyz; 417 | const [cx, cy] = clampToCircle(x, y); 418 | finalXyz = [cx, cy, z]; 419 | } 420 | const newAnchor = new ColorPoint({ 421 | xyz: finalXyz, 422 | color, 423 | invertedLightness: this._invertedLightness 424 | }); 425 | if (insertAtIndex !== void 0) { 426 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 427 | } else { 428 | this.anchorPoints.push(newAnchor); 429 | } 430 | this.updateAnchorPairs(); 431 | return newAnchor; 432 | } 433 | removeAnchorPoint({ 434 | point, 435 | index 436 | }) { 437 | if (!point && index === void 0) { 438 | throw new Error("Must provide a point or index"); 439 | } 440 | if (this.anchorPoints.length < 3) { 441 | throw new Error("Must have at least two anchor points"); 442 | } 443 | let apid; 444 | if (index !== void 0) { 445 | apid = index; 446 | } else if (point) { 447 | apid = this.anchorPoints.indexOf(point); 448 | } 449 | if (apid > -1 && apid < this.anchorPoints.length) { 450 | this.anchorPoints.splice(apid, 1); 451 | this.updateAnchorPairs(); 452 | } else { 453 | throw new Error("Point not found"); 454 | } 455 | } 456 | updateAnchorPoint({ 457 | point, 458 | pointIndex, 459 | xyz, 460 | color, 461 | clamp 462 | }) { 463 | if (pointIndex !== void 0) { 464 | point = this.anchorPoints[pointIndex]; 465 | } 466 | if (!point) { 467 | throw new Error("Must provide a point or pointIndex"); 468 | } 469 | if (!xyz && !color) { 470 | throw new Error("Must provide a new xyz position or color"); 471 | } 472 | if (xyz) { 473 | const shouldClamp = clamp != null ? clamp : this._clampToCircle; 474 | if (shouldClamp) { 475 | const [x, y, z] = xyz; 476 | const [cx, cy] = clampToCircle(x, y); 477 | point.position = [cx, cy, z]; 478 | } else { 479 | point.position = xyz; 480 | } 481 | } 482 | if (color) 483 | point.hsl = color; 484 | this.updateAnchorPairs(); 485 | return point; 486 | } 487 | getClosestAnchorPoint({ 488 | xyz, 489 | hsl, 490 | maxDistance = 1 491 | }) { 492 | if (!xyz && !hsl) { 493 | throw new Error("Must provide a xyz or hsl"); 494 | } 495 | let distances; 496 | if (xyz) { 497 | distances = this.anchorPoints.map( 498 | (anchor) => distance(anchor.position, xyz) 499 | ); 500 | } else if (hsl) { 501 | distances = this.anchorPoints.map( 502 | (anchor) => distance(anchor.hsl, hsl, true) 503 | ); 504 | } 505 | const minDistance = Math.min(...distances); 506 | if (minDistance > maxDistance) { 507 | return null; 508 | } 509 | const closestAnchorIndex = distances.indexOf(minDistance); 510 | return this.anchorPoints[closestAnchorIndex] || null; 511 | } 512 | set closedLoop(newStatus) { 513 | this.connectLastAndFirstAnchor = newStatus; 514 | this.updateAnchorPairs(); 515 | } 516 | get closedLoop() { 517 | return this.connectLastAndFirstAnchor; 518 | } 519 | set invertedLightness(newStatus) { 520 | this._invertedLightness = newStatus; 521 | this.anchorPoints.forEach((p) => p.invertedLightness = newStatus); 522 | this.updateAnchorPairs(); 523 | } 524 | get invertedLightness() { 525 | return this._invertedLightness; 526 | } 527 | /** 528 | * Returns a flattened array of all points across all segments, 529 | * removing duplicated anchor points at segment boundaries. 530 | * 531 | * Since anchor points exist at both the end of one segment and 532 | * the beginning of the next, this method keeps only one instance of each. 533 | * The filter logic keeps the first point (index 0) and then filters out 534 | * points whose indices are multiples of the segment size (_numPoints), 535 | * which are the anchor points at the start of each segment (except the first). 536 | * 537 | * This approach ensures we get all unique points in the correct order 538 | * while avoiding duplicated anchor points. 539 | * 540 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 541 | */ 542 | get flattenedPoints() { 543 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 544 | } 545 | get colors() { 546 | const colors = this.flattenedPoints.map((p) => p.color); 547 | if (this.connectLastAndFirstAnchor && this._anchorPoints.length !== 2) { 548 | colors.pop(); 549 | } 550 | return colors; 551 | } 552 | cssColors(mode = "hsl") { 553 | const methods = { 554 | hsl: (p) => p.hslCSS, 555 | oklch: (p) => p.oklchCSS, 556 | lch: (p) => p.lchCSS 557 | }; 558 | const cssColors = this.flattenedPoints.map(methods[mode]); 559 | if (this.connectLastAndFirstAnchor) { 560 | cssColors.pop(); 561 | } 562 | return cssColors; 563 | } 564 | get colorsCSS() { 565 | return this.cssColors("hsl"); 566 | } 567 | get colorsCSSlch() { 568 | return this.cssColors("lch"); 569 | } 570 | get colorsCSSoklch() { 571 | return this.cssColors("oklch"); 572 | } 573 | shiftHue(hShift = 20) { 574 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 575 | this.updateAnchorPairs(); 576 | } 577 | /** 578 | * Returns a color at a specific position along the entire color line (0-1) 579 | * Treats all segments as one continuous path, respecting easing functions 580 | * @param t Position along the line (0-1), where 0 is start and 1 is end 581 | * @returns ColorPoint at the specified position 582 | * @example 583 | * getColorAt(0) // Returns color at the very beginning 584 | * getColorAt(0.5) // Returns color at the middle of the entire journey 585 | * getColorAt(1) // Returns color at the very end 586 | */ 587 | getColorAt(t) { 588 | var _a; 589 | if (t < 0 || t > 1) { 590 | throw new Error("Position must be between 0 and 1"); 591 | } 592 | if (this.anchorPoints.length === 0) { 593 | throw new Error("No anchor points available"); 594 | } 595 | const totalSegments = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 596 | const effectiveSegments = this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 ? 2 : totalSegments; 597 | const segmentPosition = t * effectiveSegments; 598 | const segmentIndex = Math.floor(segmentPosition); 599 | const localT = segmentPosition - segmentIndex; 600 | const actualSegmentIndex = segmentIndex >= effectiveSegments ? effectiveSegments - 1 : segmentIndex; 601 | const actualLocalT = segmentIndex >= effectiveSegments ? 1 : localT; 602 | const pair = this._anchorPairs[actualSegmentIndex]; 603 | if (!pair || pair.length < 2 || !pair[0] || !pair[1]) { 604 | return new ColorPoint({ 605 | color: ((_a = this.anchorPoints[0]) == null ? void 0 : _a.color) || [0, 0, 0], 606 | invertedLightness: this._invertedLightness 607 | }); 608 | } 609 | const p1position = pair[0].position; 610 | const p2position = pair[1].position; 611 | const shouldInvertEase = this.shouldInvertEaseForSegment(actualSegmentIndex); 612 | const xyz = vectorOnLine( 613 | actualLocalT, 614 | p1position, 615 | p2position, 616 | shouldInvertEase, 617 | this._positionFunctionX, 618 | this._positionFunctionY, 619 | this._positionFunctionZ 620 | ); 621 | return new ColorPoint({ 622 | xyz, 623 | invertedLightness: this._invertedLightness 624 | }); 625 | } 626 | /** 627 | * Determines whether easing should be inverted for a given segment 628 | * @param segmentIndex The index of the segment 629 | * @returns Whether easing should be inverted 630 | */ 631 | shouldInvertEaseForSegment(segmentIndex) { 632 | return !!(segmentIndex % 2 || this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 && segmentIndex === 0); 633 | } 634 | }; 635 | var { p5 } = globalThis; 636 | if (p5 && p5.VERSION && p5.VERSION.startsWith("1.")) { 637 | console.info("p5 < 1.x detected, adding poline to p5 prototype"); 638 | const poline = new Poline(); 639 | p5.prototype.poline = poline; 640 | const polineColors = () => poline.colors.map( 641 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 642 | ); 643 | p5.prototype.registerMethod("polineColors", polineColors); 644 | globalThis.poline = poline; 645 | globalThis.polineColors = polineColors; 646 | } 647 | -------------------------------------------------------------------------------- /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 | ColorPoint: () => ColorPoint, 25 | Poline: () => Poline, 26 | clampToCircle: () => clampToCircle, 27 | hslToPoint: () => hslToPoint, 28 | pointToHSL: () => pointToHSL, 29 | positionFunctions: () => positionFunctions, 30 | randomHSLPair: () => randomHSLPair, 31 | randomHSLTriple: () => randomHSLTriple 32 | }); 33 | var pointToHSL = (xyz, invertedLightness) => { 34 | const [x, y, z] = xyz; 35 | const cx = 0.5; 36 | const cy = 0.5; 37 | const radians = Math.atan2(y - cy, x - cx); 38 | let deg = radians * (180 / Math.PI); 39 | deg = (360 + deg) % 360; 40 | const s = z; 41 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 42 | const l = dist / cx; 43 | return [deg, s, invertedLightness ? 1 - l : l]; 44 | }; 45 | var hslToPoint = (hsl, invertedLightness) => { 46 | const [h, s, l] = hsl; 47 | const cx = 0.5; 48 | const cy = 0.5; 49 | const radians = h / (180 / Math.PI); 50 | const dist = (invertedLightness ? 1 - l : l) * cx; 51 | const x = cx + dist * Math.cos(radians); 52 | const y = cy + dist * Math.sin(radians); 53 | const z = s; 54 | return [x, y, z]; 55 | }; 56 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 57 | [startHue, saturations[0], lightnesses[0]], 58 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 59 | ]; 60 | var clampToCircle = (x, y) => { 61 | const cx = 0.5; 62 | const cy = 0.5; 63 | const dx = x - cx; 64 | const dy = y - cy; 65 | const dist = Math.hypot(dx, dy); 66 | if (dist <= 0.5) { 67 | return [x, y]; 68 | } 69 | return [cx + dx / dist * 0.5, cy + dy / dist * 0.5]; 70 | }; 71 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 72 | 0.75 + Math.random() * 0.2, 73 | Math.random() * 0.2, 74 | 0.75 + Math.random() * 0.2 75 | ]) => [ 76 | [startHue, saturations[0], lightnesses[0]], 77 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 78 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 79 | ]; 80 | 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) => { 81 | const tModifiedX = fx(t, invert); 82 | const tModifiedY = fy(t, invert); 83 | const tModifiedZ = fz(t, invert); 84 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 85 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 86 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 87 | return [x, y, z]; 88 | }; 89 | 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) => { 90 | const points = []; 91 | for (let i = 0; i < numPoints; i++) { 92 | const [x, y, z] = vectorOnLine( 93 | i / (numPoints - 1), 94 | p1, 95 | p2, 96 | invert, 97 | fx, 98 | fy, 99 | fz 100 | ); 101 | points.push([x, y, z]); 102 | } 103 | return points; 104 | }; 105 | var linearPosition = (t) => { 106 | return t; 107 | }; 108 | var exponentialPosition = (t, reverse = false) => { 109 | if (reverse) { 110 | return 1 - (1 - t) ** 2; 111 | } 112 | return t ** 2; 113 | }; 114 | var quadraticPosition = (t, reverse = false) => { 115 | if (reverse) { 116 | return 1 - (1 - t) ** 3; 117 | } 118 | return t ** 3; 119 | }; 120 | var cubicPosition = (t, reverse = false) => { 121 | if (reverse) { 122 | return 1 - (1 - t) ** 4; 123 | } 124 | return t ** 4; 125 | }; 126 | var quarticPosition = (t, reverse = false) => { 127 | if (reverse) { 128 | return 1 - (1 - t) ** 5; 129 | } 130 | return t ** 5; 131 | }; 132 | var sinusoidalPosition = (t, reverse = false) => { 133 | if (reverse) { 134 | return 1 - Math.sin((1 - t) * Math.PI / 2); 135 | } 136 | return Math.sin(t * Math.PI / 2); 137 | }; 138 | var asinusoidalPosition = (t, reverse = false) => { 139 | if (reverse) { 140 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 141 | } 142 | return Math.asin(t) / (Math.PI / 2); 143 | }; 144 | var arcPosition = (t, reverse = false) => { 145 | if (reverse) { 146 | return 1 - Math.sqrt(1 - t ** 2); 147 | } 148 | return 1 - Math.sqrt(1 - t); 149 | }; 150 | var smoothStepPosition = (t) => { 151 | return t ** 2 * (3 - 2 * t); 152 | }; 153 | var positionFunctions = { 154 | linearPosition, 155 | exponentialPosition, 156 | quadraticPosition, 157 | cubicPosition, 158 | quarticPosition, 159 | sinusoidalPosition, 160 | asinusoidalPosition, 161 | arcPosition, 162 | smoothStepPosition 163 | }; 164 | var distance = (p1, p2, hueMode = false) => { 165 | const a1 = p1[0]; 166 | const a2 = p2[0]; 167 | let diffA = 0; 168 | if (hueMode && a1 !== null && a2 !== null) { 169 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 170 | diffA = diffA / 360; 171 | } else { 172 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 173 | } 174 | const a = diffA; 175 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 176 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 177 | return Math.sqrt(a * a + b * b + c * c); 178 | }; 179 | var ColorPoint = class { 180 | constructor({ 181 | xyz, 182 | color, 183 | invertedLightness = false 184 | } = {}) { 185 | this.x = 0; 186 | this.y = 0; 187 | this.z = 0; 188 | this.color = [0, 0, 0]; 189 | this._invertedLightness = false; 190 | this._invertedLightness = invertedLightness; 191 | this.positionOrColor({ xyz, color, invertedLightness }); 192 | } 193 | positionOrColor({ 194 | xyz, 195 | color, 196 | invertedLightness = false 197 | }) { 198 | this._invertedLightness = invertedLightness; 199 | if (xyz && color || !xyz && !color) { 200 | throw new Error("Point must be initialized with either x,y,z or hsl"); 201 | } else if (xyz) { 202 | this.x = xyz[0]; 203 | this.y = xyz[1]; 204 | this.z = xyz[2]; 205 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 206 | } else if (color) { 207 | this.color = color; 208 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 209 | } 210 | } 211 | set position([x, y, z]) { 212 | this.x = x; 213 | this.y = y; 214 | this.z = z; 215 | this.color = pointToHSL( 216 | [this.x, this.y, this.z], 217 | this._invertedLightness 218 | ); 219 | } 220 | get position() { 221 | return [this.x, this.y, this.z]; 222 | } 223 | set hsl([h, s, l]) { 224 | this.color = [h, s, l]; 225 | [this.x, this.y, this.z] = hslToPoint( 226 | this.color, 227 | this._invertedLightness 228 | ); 229 | } 230 | get hsl() { 231 | return this.color; 232 | } 233 | get hslCSS() { 234 | const [h, s, l] = this.color; 235 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 236 | 2 237 | )}%)`; 238 | } 239 | get oklchCSS() { 240 | const [h, s, l] = this.color; 241 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 242 | 2 243 | )})`; 244 | } 245 | get lchCSS() { 246 | const [h, s, l] = this.color; 247 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 248 | 2 249 | )})`; 250 | } 251 | set invertedLightness(val) { 252 | this._invertedLightness = val; 253 | this.color = pointToHSL( 254 | [this.x, this.y, this.z], 255 | this._invertedLightness 256 | ); 257 | } 258 | get invertedLightness() { 259 | return this._invertedLightness; 260 | } 261 | shiftHue(angle) { 262 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 263 | [this.x, this.y, this.z] = hslToPoint( 264 | this.color, 265 | this._invertedLightness 266 | ); 267 | } 268 | }; 269 | var Poline = class { 270 | constructor({ 271 | anchorColors = randomHSLPair(), 272 | numPoints = 4, 273 | positionFunction = sinusoidalPosition, 274 | positionFunctionX, 275 | positionFunctionY, 276 | positionFunctionZ, 277 | closedLoop, 278 | invertedLightness, 279 | clampToCircle: clampToCircle2 280 | } = { 281 | anchorColors: randomHSLPair(), 282 | numPoints: 4, 283 | positionFunction: sinusoidalPosition, 284 | closedLoop: false 285 | }) { 286 | this._positionFunctionX = sinusoidalPosition; 287 | this._positionFunctionY = sinusoidalPosition; 288 | this._positionFunctionZ = sinusoidalPosition; 289 | this.connectLastAndFirstAnchor = false; 290 | this._animationFrame = null; 291 | this._invertedLightness = false; 292 | this._clampToCircle = false; 293 | if (!anchorColors || anchorColors.length < 2) { 294 | throw new Error("Must have at least two anchor colors"); 295 | } 296 | this._anchorPoints = anchorColors.map( 297 | (point) => new ColorPoint({ color: point, invertedLightness }) 298 | ); 299 | this._numPoints = numPoints + 2; 300 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 301 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 302 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 303 | this.connectLastAndFirstAnchor = closedLoop || false; 304 | this._invertedLightness = invertedLightness || false; 305 | this._clampToCircle = clampToCircle2 || false; 306 | this.updateAnchorPairs(); 307 | } 308 | get numPoints() { 309 | return this._numPoints - 2; 310 | } 311 | set numPoints(numPoints) { 312 | if (numPoints < 1) { 313 | throw new Error("Must have at least one point"); 314 | } 315 | this._numPoints = numPoints + 2; 316 | this.updateAnchorPairs(); 317 | } 318 | set positionFunction(positionFunction) { 319 | if (Array.isArray(positionFunction)) { 320 | if (positionFunction.length !== 3) { 321 | throw new Error("Position function array must have 3 elements"); 322 | } 323 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 324 | throw new Error("Position function array must have 3 functions"); 325 | } 326 | this._positionFunctionX = positionFunction[0]; 327 | this._positionFunctionY = positionFunction[1]; 328 | this._positionFunctionZ = positionFunction[2]; 329 | } else { 330 | this._positionFunctionX = positionFunction; 331 | this._positionFunctionY = positionFunction; 332 | this._positionFunctionZ = positionFunction; 333 | } 334 | this.updateAnchorPairs(); 335 | } 336 | get positionFunction() { 337 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 338 | return this._positionFunctionX; 339 | } 340 | return [ 341 | this._positionFunctionX, 342 | this._positionFunctionY, 343 | this._positionFunctionZ 344 | ]; 345 | } 346 | set positionFunctionX(positionFunctionX) { 347 | this._positionFunctionX = positionFunctionX; 348 | this.updateAnchorPairs(); 349 | } 350 | get positionFunctionX() { 351 | return this._positionFunctionX; 352 | } 353 | set positionFunctionY(positionFunctionY) { 354 | this._positionFunctionY = positionFunctionY; 355 | this.updateAnchorPairs(); 356 | } 357 | get positionFunctionY() { 358 | return this._positionFunctionY; 359 | } 360 | set positionFunctionZ(positionFunctionZ) { 361 | this._positionFunctionZ = positionFunctionZ; 362 | this.updateAnchorPairs(); 363 | } 364 | get positionFunctionZ() { 365 | return this._positionFunctionZ; 366 | } 367 | get clampToCircle() { 368 | return this._clampToCircle; 369 | } 370 | set clampToCircle(clamp) { 371 | this._clampToCircle = clamp; 372 | } 373 | get anchorPoints() { 374 | return this._anchorPoints; 375 | } 376 | set anchorPoints(anchorPoints) { 377 | this._anchorPoints = anchorPoints; 378 | this.updateAnchorPairs(); 379 | } 380 | updateAnchorPairs() { 381 | this._anchorPairs = []; 382 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 383 | for (let i = 0; i < anchorPointsLength; i++) { 384 | const pair = [ 385 | this.anchorPoints[i], 386 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 387 | ]; 388 | this._anchorPairs.push(pair); 389 | } 390 | this.points = this._anchorPairs.map((pair, i) => { 391 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 392 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 393 | const shouldInvertEase = this.shouldInvertEaseForSegment(i); 394 | return vectorsOnLine( 395 | p1position, 396 | p2position, 397 | this._numPoints, 398 | shouldInvertEase ? true : false, 399 | this.positionFunctionX, 400 | this.positionFunctionY, 401 | this.positionFunctionZ 402 | ).map( 403 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 404 | ); 405 | }); 406 | } 407 | addAnchorPoint({ 408 | xyz, 409 | color, 410 | insertAtIndex, 411 | clamp 412 | }) { 413 | let finalXyz = xyz; 414 | const shouldClamp = clamp ?? this._clampToCircle; 415 | if (shouldClamp && xyz) { 416 | const [x, y, z] = xyz; 417 | const [cx, cy] = clampToCircle(x, y); 418 | finalXyz = [cx, cy, z]; 419 | } 420 | const newAnchor = new ColorPoint({ 421 | xyz: finalXyz, 422 | color, 423 | invertedLightness: this._invertedLightness 424 | }); 425 | if (insertAtIndex !== void 0) { 426 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 427 | } else { 428 | this.anchorPoints.push(newAnchor); 429 | } 430 | this.updateAnchorPairs(); 431 | return newAnchor; 432 | } 433 | removeAnchorPoint({ 434 | point, 435 | index 436 | }) { 437 | if (!point && index === void 0) { 438 | throw new Error("Must provide a point or index"); 439 | } 440 | if (this.anchorPoints.length < 3) { 441 | throw new Error("Must have at least two anchor points"); 442 | } 443 | let apid; 444 | if (index !== void 0) { 445 | apid = index; 446 | } else if (point) { 447 | apid = this.anchorPoints.indexOf(point); 448 | } 449 | if (apid > -1 && apid < this.anchorPoints.length) { 450 | this.anchorPoints.splice(apid, 1); 451 | this.updateAnchorPairs(); 452 | } else { 453 | throw new Error("Point not found"); 454 | } 455 | } 456 | updateAnchorPoint({ 457 | point, 458 | pointIndex, 459 | xyz, 460 | color, 461 | clamp 462 | }) { 463 | if (pointIndex !== void 0) { 464 | point = this.anchorPoints[pointIndex]; 465 | } 466 | if (!point) { 467 | throw new Error("Must provide a point or pointIndex"); 468 | } 469 | if (!xyz && !color) { 470 | throw new Error("Must provide a new xyz position or color"); 471 | } 472 | if (xyz) { 473 | const shouldClamp = clamp ?? this._clampToCircle; 474 | if (shouldClamp) { 475 | const [x, y, z] = xyz; 476 | const [cx, cy] = clampToCircle(x, y); 477 | point.position = [cx, cy, z]; 478 | } else { 479 | point.position = xyz; 480 | } 481 | } 482 | if (color) 483 | point.hsl = color; 484 | this.updateAnchorPairs(); 485 | return point; 486 | } 487 | getClosestAnchorPoint({ 488 | xyz, 489 | hsl, 490 | maxDistance = 1 491 | }) { 492 | if (!xyz && !hsl) { 493 | throw new Error("Must provide a xyz or hsl"); 494 | } 495 | let distances; 496 | if (xyz) { 497 | distances = this.anchorPoints.map( 498 | (anchor) => distance(anchor.position, xyz) 499 | ); 500 | } else if (hsl) { 501 | distances = this.anchorPoints.map( 502 | (anchor) => distance(anchor.hsl, hsl, true) 503 | ); 504 | } 505 | const minDistance = Math.min(...distances); 506 | if (minDistance > maxDistance) { 507 | return null; 508 | } 509 | const closestAnchorIndex = distances.indexOf(minDistance); 510 | return this.anchorPoints[closestAnchorIndex] || null; 511 | } 512 | set closedLoop(newStatus) { 513 | this.connectLastAndFirstAnchor = newStatus; 514 | this.updateAnchorPairs(); 515 | } 516 | get closedLoop() { 517 | return this.connectLastAndFirstAnchor; 518 | } 519 | set invertedLightness(newStatus) { 520 | this._invertedLightness = newStatus; 521 | this.anchorPoints.forEach((p) => p.invertedLightness = newStatus); 522 | this.updateAnchorPairs(); 523 | } 524 | get invertedLightness() { 525 | return this._invertedLightness; 526 | } 527 | /** 528 | * Returns a flattened array of all points across all segments, 529 | * removing duplicated anchor points at segment boundaries. 530 | * 531 | * Since anchor points exist at both the end of one segment and 532 | * the beginning of the next, this method keeps only one instance of each. 533 | * The filter logic keeps the first point (index 0) and then filters out 534 | * points whose indices are multiples of the segment size (_numPoints), 535 | * which are the anchor points at the start of each segment (except the first). 536 | * 537 | * This approach ensures we get all unique points in the correct order 538 | * while avoiding duplicated anchor points. 539 | * 540 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 541 | */ 542 | get flattenedPoints() { 543 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 544 | } 545 | get colors() { 546 | const colors = this.flattenedPoints.map((p) => p.color); 547 | if (this.connectLastAndFirstAnchor && this._anchorPoints.length !== 2) { 548 | colors.pop(); 549 | } 550 | return colors; 551 | } 552 | cssColors(mode = "hsl") { 553 | const methods = { 554 | hsl: (p) => p.hslCSS, 555 | oklch: (p) => p.oklchCSS, 556 | lch: (p) => p.lchCSS 557 | }; 558 | const cssColors = this.flattenedPoints.map(methods[mode]); 559 | if (this.connectLastAndFirstAnchor) { 560 | cssColors.pop(); 561 | } 562 | return cssColors; 563 | } 564 | get colorsCSS() { 565 | return this.cssColors("hsl"); 566 | } 567 | get colorsCSSlch() { 568 | return this.cssColors("lch"); 569 | } 570 | get colorsCSSoklch() { 571 | return this.cssColors("oklch"); 572 | } 573 | shiftHue(hShift = 20) { 574 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 575 | this.updateAnchorPairs(); 576 | } 577 | /** 578 | * Returns a color at a specific position along the entire color line (0-1) 579 | * Treats all segments as one continuous path, respecting easing functions 580 | * @param t Position along the line (0-1), where 0 is start and 1 is end 581 | * @returns ColorPoint at the specified position 582 | * @example 583 | * getColorAt(0) // Returns color at the very beginning 584 | * getColorAt(0.5) // Returns color at the middle of the entire journey 585 | * getColorAt(1) // Returns color at the very end 586 | */ 587 | getColorAt(t) { 588 | var _a; 589 | if (t < 0 || t > 1) { 590 | throw new Error("Position must be between 0 and 1"); 591 | } 592 | if (this.anchorPoints.length === 0) { 593 | throw new Error("No anchor points available"); 594 | } 595 | const totalSegments = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 596 | const effectiveSegments = this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 ? 2 : totalSegments; 597 | const segmentPosition = t * effectiveSegments; 598 | const segmentIndex = Math.floor(segmentPosition); 599 | const localT = segmentPosition - segmentIndex; 600 | const actualSegmentIndex = segmentIndex >= effectiveSegments ? effectiveSegments - 1 : segmentIndex; 601 | const actualLocalT = segmentIndex >= effectiveSegments ? 1 : localT; 602 | const pair = this._anchorPairs[actualSegmentIndex]; 603 | if (!pair || pair.length < 2 || !pair[0] || !pair[1]) { 604 | return new ColorPoint({ 605 | color: ((_a = this.anchorPoints[0]) == null ? void 0 : _a.color) || [0, 0, 0], 606 | invertedLightness: this._invertedLightness 607 | }); 608 | } 609 | const p1position = pair[0].position; 610 | const p2position = pair[1].position; 611 | const shouldInvertEase = this.shouldInvertEaseForSegment(actualSegmentIndex); 612 | const xyz = vectorOnLine( 613 | actualLocalT, 614 | p1position, 615 | p2position, 616 | shouldInvertEase, 617 | this._positionFunctionX, 618 | this._positionFunctionY, 619 | this._positionFunctionZ 620 | ); 621 | return new ColorPoint({ 622 | xyz, 623 | invertedLightness: this._invertedLightness 624 | }); 625 | } 626 | /** 627 | * Determines whether easing should be inverted for a given segment 628 | * @param segmentIndex The index of the segment 629 | * @returns Whether easing should be inverted 630 | */ 631 | shouldInvertEaseForSegment(segmentIndex) { 632 | return !!(segmentIndex % 2 || this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 && segmentIndex === 0); 633 | } 634 | }; 635 | var { p5 } = globalThis; 636 | if (p5 && p5.VERSION && p5.VERSION.startsWith("1.")) { 637 | console.info("p5 < 1.x detected, adding poline to p5 prototype"); 638 | const poline = new Poline(); 639 | p5.prototype.poline = poline; 640 | const polineColors = () => poline.colors.map( 641 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 642 | ); 643 | p5.prototype.registerMethod("polineColors", polineColors); 644 | globalThis.poline = poline; 645 | globalThis.polineColors = polineColors; 646 | } 647 | return __toCommonJS(src_exports); 648 | })(); 649 | -------------------------------------------------------------------------------- /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 __export = (target, all) => { 18 | for (var name in all) 19 | __defProp(target, name, { get: all[name], enumerable: true }); 20 | }; 21 | var __copyProps = (to, from, except, desc) => { 22 | if (from && typeof from === "object" || typeof from === "function") { 23 | for (let key of __getOwnPropNames(from)) 24 | if (!__hasOwnProp.call(to, key) && key !== except) 25 | __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); 26 | } 27 | return to; 28 | }; 29 | var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); 30 | 31 | // src/index.ts 32 | var src_exports = {}; 33 | __export(src_exports, { 34 | ColorPoint: () => ColorPoint, 35 | Poline: () => Poline, 36 | clampToCircle: () => clampToCircle, 37 | hslToPoint: () => hslToPoint, 38 | pointToHSL: () => pointToHSL, 39 | positionFunctions: () => positionFunctions, 40 | randomHSLPair: () => randomHSLPair, 41 | randomHSLTriple: () => randomHSLTriple 42 | }); 43 | var pointToHSL = (xyz, invertedLightness) => { 44 | const [x, y, z] = xyz; 45 | const cx = 0.5; 46 | const cy = 0.5; 47 | const radians = Math.atan2(y - cy, x - cx); 48 | let deg = radians * (180 / Math.PI); 49 | deg = (360 + deg) % 360; 50 | const s = z; 51 | const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2)); 52 | const l = dist / cx; 53 | return [deg, s, invertedLightness ? 1 - l : l]; 54 | }; 55 | var hslToPoint = (hsl, invertedLightness) => { 56 | const [h, s, l] = hsl; 57 | const cx = 0.5; 58 | const cy = 0.5; 59 | const radians = h / (180 / Math.PI); 60 | const dist = (invertedLightness ? 1 - l : l) * cx; 61 | const x = cx + dist * Math.cos(radians); 62 | const y = cy + dist * Math.sin(radians); 63 | const z = s; 64 | return [x, y, z]; 65 | }; 66 | var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [ 67 | [startHue, saturations[0], lightnesses[0]], 68 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]] 69 | ]; 70 | var clampToCircle = (x, y) => { 71 | const cx = 0.5; 72 | const cy = 0.5; 73 | const dx = x - cx; 74 | const dy = y - cy; 75 | const dist = Math.hypot(dx, dy); 76 | if (dist <= 0.5) { 77 | return [x, y]; 78 | } 79 | return [cx + dx / dist * 0.5, cy + dy / dist * 0.5]; 80 | }; 81 | var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [ 82 | 0.75 + Math.random() * 0.2, 83 | Math.random() * 0.2, 84 | 0.75 + Math.random() * 0.2 85 | ]) => [ 86 | [startHue, saturations[0], lightnesses[0]], 87 | [(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]], 88 | [(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]] 89 | ]; 90 | 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) => { 91 | const tModifiedX = fx(t, invert); 92 | const tModifiedY = fy(t, invert); 93 | const tModifiedZ = fz(t, invert); 94 | const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0]; 95 | const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1]; 96 | const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2]; 97 | return [x, y, z]; 98 | }; 99 | 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) => { 100 | const points = []; 101 | for (let i = 0; i < numPoints; i++) { 102 | const [x, y, z] = vectorOnLine( 103 | i / (numPoints - 1), 104 | p1, 105 | p2, 106 | invert, 107 | fx, 108 | fy, 109 | fz 110 | ); 111 | points.push([x, y, z]); 112 | } 113 | return points; 114 | }; 115 | var linearPosition = (t) => { 116 | return t; 117 | }; 118 | var exponentialPosition = (t, reverse = false) => { 119 | if (reverse) { 120 | return 1 - (1 - t) ** 2; 121 | } 122 | return t ** 2; 123 | }; 124 | var quadraticPosition = (t, reverse = false) => { 125 | if (reverse) { 126 | return 1 - (1 - t) ** 3; 127 | } 128 | return t ** 3; 129 | }; 130 | var cubicPosition = (t, reverse = false) => { 131 | if (reverse) { 132 | return 1 - (1 - t) ** 4; 133 | } 134 | return t ** 4; 135 | }; 136 | var quarticPosition = (t, reverse = false) => { 137 | if (reverse) { 138 | return 1 - (1 - t) ** 5; 139 | } 140 | return t ** 5; 141 | }; 142 | var sinusoidalPosition = (t, reverse = false) => { 143 | if (reverse) { 144 | return 1 - Math.sin((1 - t) * Math.PI / 2); 145 | } 146 | return Math.sin(t * Math.PI / 2); 147 | }; 148 | var asinusoidalPosition = (t, reverse = false) => { 149 | if (reverse) { 150 | return 1 - Math.asin(1 - t) / (Math.PI / 2); 151 | } 152 | return Math.asin(t) / (Math.PI / 2); 153 | }; 154 | var arcPosition = (t, reverse = false) => { 155 | if (reverse) { 156 | return 1 - Math.sqrt(1 - t ** 2); 157 | } 158 | return 1 - Math.sqrt(1 - t); 159 | }; 160 | var smoothStepPosition = (t) => { 161 | return t ** 2 * (3 - 2 * t); 162 | }; 163 | var positionFunctions = { 164 | linearPosition, 165 | exponentialPosition, 166 | quadraticPosition, 167 | cubicPosition, 168 | quarticPosition, 169 | sinusoidalPosition, 170 | asinusoidalPosition, 171 | arcPosition, 172 | smoothStepPosition 173 | }; 174 | var distance = (p1, p2, hueMode = false) => { 175 | const a1 = p1[0]; 176 | const a2 = p2[0]; 177 | let diffA = 0; 178 | if (hueMode && a1 !== null && a2 !== null) { 179 | diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2)); 180 | diffA = diffA / 360; 181 | } else { 182 | diffA = a1 === null || a2 === null ? 0 : a1 - a2; 183 | } 184 | const a = diffA; 185 | const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1]; 186 | const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2]; 187 | return Math.sqrt(a * a + b * b + c * c); 188 | }; 189 | var ColorPoint = class { 190 | constructor({ 191 | xyz, 192 | color, 193 | invertedLightness = false 194 | } = {}) { 195 | this.x = 0; 196 | this.y = 0; 197 | this.z = 0; 198 | this.color = [0, 0, 0]; 199 | this._invertedLightness = false; 200 | this._invertedLightness = invertedLightness; 201 | this.positionOrColor({ xyz, color, invertedLightness }); 202 | } 203 | positionOrColor({ 204 | xyz, 205 | color, 206 | invertedLightness = false 207 | }) { 208 | this._invertedLightness = invertedLightness; 209 | if (xyz && color || !xyz && !color) { 210 | throw new Error("Point must be initialized with either x,y,z or hsl"); 211 | } else if (xyz) { 212 | this.x = xyz[0]; 213 | this.y = xyz[1]; 214 | this.z = xyz[2]; 215 | this.color = pointToHSL([this.x, this.y, this.z], invertedLightness); 216 | } else if (color) { 217 | this.color = color; 218 | [this.x, this.y, this.z] = hslToPoint(color, invertedLightness); 219 | } 220 | } 221 | set position([x, y, z]) { 222 | this.x = x; 223 | this.y = y; 224 | this.z = z; 225 | this.color = pointToHSL( 226 | [this.x, this.y, this.z], 227 | this._invertedLightness 228 | ); 229 | } 230 | get position() { 231 | return [this.x, this.y, this.z]; 232 | } 233 | set hsl([h, s, l]) { 234 | this.color = [h, s, l]; 235 | [this.x, this.y, this.z] = hslToPoint( 236 | this.color, 237 | this._invertedLightness 238 | ); 239 | } 240 | get hsl() { 241 | return this.color; 242 | } 243 | get hslCSS() { 244 | const [h, s, l] = this.color; 245 | return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed( 246 | 2 247 | )}%)`; 248 | } 249 | get oklchCSS() { 250 | const [h, s, l] = this.color; 251 | return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed( 252 | 2 253 | )})`; 254 | } 255 | get lchCSS() { 256 | const [h, s, l] = this.color; 257 | return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed( 258 | 2 259 | )})`; 260 | } 261 | set invertedLightness(val) { 262 | this._invertedLightness = val; 263 | this.color = pointToHSL( 264 | [this.x, this.y, this.z], 265 | this._invertedLightness 266 | ); 267 | } 268 | get invertedLightness() { 269 | return this._invertedLightness; 270 | } 271 | shiftHue(angle) { 272 | this.color[0] = (360 + (this.color[0] + angle)) % 360; 273 | [this.x, this.y, this.z] = hslToPoint( 274 | this.color, 275 | this._invertedLightness 276 | ); 277 | } 278 | }; 279 | var Poline = class { 280 | constructor({ 281 | anchorColors = randomHSLPair(), 282 | numPoints = 4, 283 | positionFunction = sinusoidalPosition, 284 | positionFunctionX, 285 | positionFunctionY, 286 | positionFunctionZ, 287 | closedLoop, 288 | invertedLightness, 289 | clampToCircle: clampToCircle2 290 | } = { 291 | anchorColors: randomHSLPair(), 292 | numPoints: 4, 293 | positionFunction: sinusoidalPosition, 294 | closedLoop: false 295 | }) { 296 | this._positionFunctionX = sinusoidalPosition; 297 | this._positionFunctionY = sinusoidalPosition; 298 | this._positionFunctionZ = sinusoidalPosition; 299 | this.connectLastAndFirstAnchor = false; 300 | this._animationFrame = null; 301 | this._invertedLightness = false; 302 | this._clampToCircle = false; 303 | if (!anchorColors || anchorColors.length < 2) { 304 | throw new Error("Must have at least two anchor colors"); 305 | } 306 | this._anchorPoints = anchorColors.map( 307 | (point) => new ColorPoint({ color: point, invertedLightness }) 308 | ); 309 | this._numPoints = numPoints + 2; 310 | this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition; 311 | this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition; 312 | this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition; 313 | this.connectLastAndFirstAnchor = closedLoop || false; 314 | this._invertedLightness = invertedLightness || false; 315 | this._clampToCircle = clampToCircle2 || false; 316 | this.updateAnchorPairs(); 317 | } 318 | get numPoints() { 319 | return this._numPoints - 2; 320 | } 321 | set numPoints(numPoints) { 322 | if (numPoints < 1) { 323 | throw new Error("Must have at least one point"); 324 | } 325 | this._numPoints = numPoints + 2; 326 | this.updateAnchorPairs(); 327 | } 328 | set positionFunction(positionFunction) { 329 | if (Array.isArray(positionFunction)) { 330 | if (positionFunction.length !== 3) { 331 | throw new Error("Position function array must have 3 elements"); 332 | } 333 | if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") { 334 | throw new Error("Position function array must have 3 functions"); 335 | } 336 | this._positionFunctionX = positionFunction[0]; 337 | this._positionFunctionY = positionFunction[1]; 338 | this._positionFunctionZ = positionFunction[2]; 339 | } else { 340 | this._positionFunctionX = positionFunction; 341 | this._positionFunctionY = positionFunction; 342 | this._positionFunctionZ = positionFunction; 343 | } 344 | this.updateAnchorPairs(); 345 | } 346 | get positionFunction() { 347 | if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) { 348 | return this._positionFunctionX; 349 | } 350 | return [ 351 | this._positionFunctionX, 352 | this._positionFunctionY, 353 | this._positionFunctionZ 354 | ]; 355 | } 356 | set positionFunctionX(positionFunctionX) { 357 | this._positionFunctionX = positionFunctionX; 358 | this.updateAnchorPairs(); 359 | } 360 | get positionFunctionX() { 361 | return this._positionFunctionX; 362 | } 363 | set positionFunctionY(positionFunctionY) { 364 | this._positionFunctionY = positionFunctionY; 365 | this.updateAnchorPairs(); 366 | } 367 | get positionFunctionY() { 368 | return this._positionFunctionY; 369 | } 370 | set positionFunctionZ(positionFunctionZ) { 371 | this._positionFunctionZ = positionFunctionZ; 372 | this.updateAnchorPairs(); 373 | } 374 | get positionFunctionZ() { 375 | return this._positionFunctionZ; 376 | } 377 | get clampToCircle() { 378 | return this._clampToCircle; 379 | } 380 | set clampToCircle(clamp) { 381 | this._clampToCircle = clamp; 382 | } 383 | get anchorPoints() { 384 | return this._anchorPoints; 385 | } 386 | set anchorPoints(anchorPoints) { 387 | this._anchorPoints = anchorPoints; 388 | this.updateAnchorPairs(); 389 | } 390 | updateAnchorPairs() { 391 | this._anchorPairs = []; 392 | const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 393 | for (let i = 0; i < anchorPointsLength; i++) { 394 | const pair = [ 395 | this.anchorPoints[i], 396 | this.anchorPoints[(i + 1) % this.anchorPoints.length] 397 | ]; 398 | this._anchorPairs.push(pair); 399 | } 400 | this.points = this._anchorPairs.map((pair, i) => { 401 | const p1position = pair[0] ? pair[0].position : [0, 0, 0]; 402 | const p2position = pair[1] ? pair[1].position : [0, 0, 0]; 403 | const shouldInvertEase = this.shouldInvertEaseForSegment(i); 404 | return vectorsOnLine( 405 | p1position, 406 | p2position, 407 | this._numPoints, 408 | shouldInvertEase ? true : false, 409 | this.positionFunctionX, 410 | this.positionFunctionY, 411 | this.positionFunctionZ 412 | ).map( 413 | (p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness }) 414 | ); 415 | }); 416 | } 417 | addAnchorPoint({ 418 | xyz, 419 | color, 420 | insertAtIndex, 421 | clamp 422 | }) { 423 | let finalXyz = xyz; 424 | const shouldClamp = clamp != null ? clamp : this._clampToCircle; 425 | if (shouldClamp && xyz) { 426 | const [x, y, z] = xyz; 427 | const [cx, cy] = clampToCircle(x, y); 428 | finalXyz = [cx, cy, z]; 429 | } 430 | const newAnchor = new ColorPoint({ 431 | xyz: finalXyz, 432 | color, 433 | invertedLightness: this._invertedLightness 434 | }); 435 | if (insertAtIndex !== void 0) { 436 | this.anchorPoints.splice(insertAtIndex, 0, newAnchor); 437 | } else { 438 | this.anchorPoints.push(newAnchor); 439 | } 440 | this.updateAnchorPairs(); 441 | return newAnchor; 442 | } 443 | removeAnchorPoint({ 444 | point, 445 | index 446 | }) { 447 | if (!point && index === void 0) { 448 | throw new Error("Must provide a point or index"); 449 | } 450 | if (this.anchorPoints.length < 3) { 451 | throw new Error("Must have at least two anchor points"); 452 | } 453 | let apid; 454 | if (index !== void 0) { 455 | apid = index; 456 | } else if (point) { 457 | apid = this.anchorPoints.indexOf(point); 458 | } 459 | if (apid > -1 && apid < this.anchorPoints.length) { 460 | this.anchorPoints.splice(apid, 1); 461 | this.updateAnchorPairs(); 462 | } else { 463 | throw new Error("Point not found"); 464 | } 465 | } 466 | updateAnchorPoint({ 467 | point, 468 | pointIndex, 469 | xyz, 470 | color, 471 | clamp 472 | }) { 473 | if (pointIndex !== void 0) { 474 | point = this.anchorPoints[pointIndex]; 475 | } 476 | if (!point) { 477 | throw new Error("Must provide a point or pointIndex"); 478 | } 479 | if (!xyz && !color) { 480 | throw new Error("Must provide a new xyz position or color"); 481 | } 482 | if (xyz) { 483 | const shouldClamp = clamp != null ? clamp : this._clampToCircle; 484 | if (shouldClamp) { 485 | const [x, y, z] = xyz; 486 | const [cx, cy] = clampToCircle(x, y); 487 | point.position = [cx, cy, z]; 488 | } else { 489 | point.position = xyz; 490 | } 491 | } 492 | if (color) 493 | point.hsl = color; 494 | this.updateAnchorPairs(); 495 | return point; 496 | } 497 | getClosestAnchorPoint({ 498 | xyz, 499 | hsl, 500 | maxDistance = 1 501 | }) { 502 | if (!xyz && !hsl) { 503 | throw new Error("Must provide a xyz or hsl"); 504 | } 505 | let distances; 506 | if (xyz) { 507 | distances = this.anchorPoints.map( 508 | (anchor) => distance(anchor.position, xyz) 509 | ); 510 | } else if (hsl) { 511 | distances = this.anchorPoints.map( 512 | (anchor) => distance(anchor.hsl, hsl, true) 513 | ); 514 | } 515 | const minDistance = Math.min(...distances); 516 | if (minDistance > maxDistance) { 517 | return null; 518 | } 519 | const closestAnchorIndex = distances.indexOf(minDistance); 520 | return this.anchorPoints[closestAnchorIndex] || null; 521 | } 522 | set closedLoop(newStatus) { 523 | this.connectLastAndFirstAnchor = newStatus; 524 | this.updateAnchorPairs(); 525 | } 526 | get closedLoop() { 527 | return this.connectLastAndFirstAnchor; 528 | } 529 | set invertedLightness(newStatus) { 530 | this._invertedLightness = newStatus; 531 | this.anchorPoints.forEach((p) => p.invertedLightness = newStatus); 532 | this.updateAnchorPairs(); 533 | } 534 | get invertedLightness() { 535 | return this._invertedLightness; 536 | } 537 | /** 538 | * Returns a flattened array of all points across all segments, 539 | * removing duplicated anchor points at segment boundaries. 540 | * 541 | * Since anchor points exist at both the end of one segment and 542 | * the beginning of the next, this method keeps only one instance of each. 543 | * The filter logic keeps the first point (index 0) and then filters out 544 | * points whose indices are multiples of the segment size (_numPoints), 545 | * which are the anchor points at the start of each segment (except the first). 546 | * 547 | * This approach ensures we get all unique points in the correct order 548 | * while avoiding duplicated anchor points. 549 | * 550 | * @returns {ColorPoint[]} A flat array of unique ColorPoint instances 551 | */ 552 | get flattenedPoints() { 553 | return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true); 554 | } 555 | get colors() { 556 | const colors = this.flattenedPoints.map((p) => p.color); 557 | if (this.connectLastAndFirstAnchor && this._anchorPoints.length !== 2) { 558 | colors.pop(); 559 | } 560 | return colors; 561 | } 562 | cssColors(mode = "hsl") { 563 | const methods = { 564 | hsl: (p) => p.hslCSS, 565 | oklch: (p) => p.oklchCSS, 566 | lch: (p) => p.lchCSS 567 | }; 568 | const cssColors = this.flattenedPoints.map(methods[mode]); 569 | if (this.connectLastAndFirstAnchor) { 570 | cssColors.pop(); 571 | } 572 | return cssColors; 573 | } 574 | get colorsCSS() { 575 | return this.cssColors("hsl"); 576 | } 577 | get colorsCSSlch() { 578 | return this.cssColors("lch"); 579 | } 580 | get colorsCSSoklch() { 581 | return this.cssColors("oklch"); 582 | } 583 | shiftHue(hShift = 20) { 584 | this.anchorPoints.forEach((p) => p.shiftHue(hShift)); 585 | this.updateAnchorPairs(); 586 | } 587 | /** 588 | * Returns a color at a specific position along the entire color line (0-1) 589 | * Treats all segments as one continuous path, respecting easing functions 590 | * @param t Position along the line (0-1), where 0 is start and 1 is end 591 | * @returns ColorPoint at the specified position 592 | * @example 593 | * getColorAt(0) // Returns color at the very beginning 594 | * getColorAt(0.5) // Returns color at the middle of the entire journey 595 | * getColorAt(1) // Returns color at the very end 596 | */ 597 | getColorAt(t) { 598 | var _a; 599 | if (t < 0 || t > 1) { 600 | throw new Error("Position must be between 0 and 1"); 601 | } 602 | if (this.anchorPoints.length === 0) { 603 | throw new Error("No anchor points available"); 604 | } 605 | const totalSegments = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1; 606 | const effectiveSegments = this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 ? 2 : totalSegments; 607 | const segmentPosition = t * effectiveSegments; 608 | const segmentIndex = Math.floor(segmentPosition); 609 | const localT = segmentPosition - segmentIndex; 610 | const actualSegmentIndex = segmentIndex >= effectiveSegments ? effectiveSegments - 1 : segmentIndex; 611 | const actualLocalT = segmentIndex >= effectiveSegments ? 1 : localT; 612 | const pair = this._anchorPairs[actualSegmentIndex]; 613 | if (!pair || pair.length < 2 || !pair[0] || !pair[1]) { 614 | return new ColorPoint({ 615 | color: ((_a = this.anchorPoints[0]) == null ? void 0 : _a.color) || [0, 0, 0], 616 | invertedLightness: this._invertedLightness 617 | }); 618 | } 619 | const p1position = pair[0].position; 620 | const p2position = pair[1].position; 621 | const shouldInvertEase = this.shouldInvertEaseForSegment(actualSegmentIndex); 622 | const xyz = vectorOnLine( 623 | actualLocalT, 624 | p1position, 625 | p2position, 626 | shouldInvertEase, 627 | this._positionFunctionX, 628 | this._positionFunctionY, 629 | this._positionFunctionZ 630 | ); 631 | return new ColorPoint({ 632 | xyz, 633 | invertedLightness: this._invertedLightness 634 | }); 635 | } 636 | /** 637 | * Determines whether easing should be inverted for a given segment 638 | * @param segmentIndex The index of the segment 639 | * @returns Whether easing should be inverted 640 | */ 641 | shouldInvertEaseForSegment(segmentIndex) { 642 | return !!(segmentIndex % 2 || this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 && segmentIndex === 0); 643 | } 644 | }; 645 | var { p5 } = globalThis; 646 | if (p5 && p5.VERSION && p5.VERSION.startsWith("1.")) { 647 | console.info("p5 < 1.x detected, adding poline to p5 prototype"); 648 | const poline = new Poline(); 649 | p5.prototype.poline = poline; 650 | const polineColors = () => poline.colors.map( 651 | (c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)` 652 | ); 653 | p5.prototype.registerMethod("polineColors", polineColors); 654 | globalThis.poline = poline; 655 | globalThis.polineColors = polineColors; 656 | } 657 | return __toCommonJS(src_exports); 658 | })(); 659 | return poline; })); 660 | --------------------------------------------------------------------------------