Library / SDK
meodai/poline avatar
meodai/poline

poline: an esoteric colour palette generator in TypeScript

Esoteric Palette Generator Mico-Lib Interpolating HSL Color in cartesian space

1,261 stars70 forksTypeScriptMIT

At a glance

What is it?
poline is a small TypeScript library that builds a colour palette by drawing lines between anchor points in polar coordinates and sampling along them, with the position along each line set by a function you choose. It ships as an npm package with ES module, UMD and CommonJS builds, plus a custom element that puts the whole thing in a browser widget.
Who is it for?
Adopt poline if you want palettes that are generated rather than sampled from a photograph, if you need the same generator in a Node script and in a browser, and if you are willing to tune a couple of anchors by eye. Do not adopt it for anything requiring perceptually uniform colour, for accessible contrast guarantees, or for palettes that must match a brand specification, because the interpolation happens in HSL space between anchors you choose.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 117 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Palettes from lines between anchor points

The project's own description is deliberately theatrical. Poline is an esoteric color palette generator that harnesses polar coordinates, and its methodology is described as defying conventional color science, drawing lines between anchors to produce visually striking and otherworldly palettes.

Underneath the wording is a small, understandable idea. You give the library two or more anchor colours, expressed as HSL triples, and it places them on a colour wheel in polar space, draws a line between each pair of anchors, and samples points along those lines. Each sampled point is one colour in the resulting palette, and the palette is the whole set of samples.

The consequence is that the palette is defined by the geometry rather than by a hue rotation rule. Two anchors that are far apart on the wheel produce a line that passes through a particular region of the colour space, and the intermediate colours follow that path rather than taking the shortest hue interpolation. If you have ever looked at a palette and thought that the in-between colours were a bit arbitrary, this is the alternative: the intermediate colours are a straight line in the coordinate system, and the path is a deliberate choice.

The library is a micro-lib in the npm sense, and the repository is correspondingly small, with a src/ directory, a dist/ with the built bundles, a tests/ directory, a build.js script, an eslint configuration, a prettier configuration and a TypeScript configuration.

Anchors, points, and how the palette grows

The API is small and the terminology is consistent enough to be worth learning in one pass.

Anchors are the fixed points at the heart of the palette, represented as a list of HSL values with three components, hue in the range 0 to 360, saturation in the range 0 to 1, and lightness in the range 0 to 1. If you omit the anchorColors argument, the library picks a random selection, and if you provide your own you get a palette with a predictable character.

Points determine the richness of the palette, with each connection producing one colour. Four points are used by default, and the numPoints property changes it:

js
new Poline({
  numPoints: 6,
});

The resulting palette is the product of the number of points and the number of anchor pairs, which is the relationship that makes the sizing predictable.

Anchors can also be added after construction, either as a colour or as an XYZ position:

js
poline.addAnchorPoint({
  color: [100, 0.91, 0.8],
});

// or

poline.addAnchorPoint({
  xyz: [0.43, 0.89, 0.91],
});

The XYZ form is the interesting one, because it lets you place an anchor by position on the wheel rather than by hue, and the addAnchorPoint method accepts an insertAtIndex parameter so you can control where the new anchor lands in the list, which matters because the anchor order determines which pairs are connected.

The clampToCircle detail, and the trap inside it

When you work in XYZ coordinates, you can end up outside the valid colour wheel, which is a circle of radius 0.5 centred at 0.5, 0.5. The clampToCircle option constrains coordinates to remain inside that boundary.

There is a sharp edge in the behaviour that the README spells out: this only affects future addAnchorPoint and updateAnchorPoint calls, and existing anchor points are not retroactively clamped. So a palette that already contains an out-of-range anchor stays out of range, and the fix is to update or remove that anchor rather than to expect the option to tidy things up.

The default can be set at construction, changed later, or overridden per call:

js
const poline = new Poline({
  anchorColors: [...],
  clampToCircle: true  // All anchor operations will clamp by default
});

// Or change it later
poline.clampToCircle = true;

and a single call can opt out, which is what you want when you deliberately want a point outside the wheel:

js
poline.updateAnchorPoint({
  pointIndex: 0,
  xyz: [1.0, 1.0, 0.5],
  clamp: false, // Explicitly disable clamping
});

The helper is also exported on its own, so you can clamp coordinates in your own code, which is useful if you are building a UI that lets a user drag an anchor around and needs to keep it inside the wheel:

js
import { clampToCircle } from "poline";

const [clampedX, clampedY] = clampToCircle(0.9, 0.9);

Updating anchors, by reference or by index

Palettes are not fixed at construction, and the updateAnchorPoint method takes a reference to the anchor you want to change, or an index into the anchor list, and either a colour in HSL form or an XYZ position array.

js
poline.updateAnchorPoint({
  point: poline.anchorPoints[0],
  color: [286, 0.22, 0.22],
});

The index form is the same operation with a different way of identifying the target:

js
poline.updateAnchorPoint({
  pointIndex: 1,
  color: [120, 0.8, 0.5],
});

The two-parameter style is worth pausing on, because it is a common source of subtle bugs in a library with a small API. The method is called updateAnchorPoint, singular, and the property is pointIndex, so a call that passes both a point and a pointIndex has two ways of saying the same thing. A caller who updates by index while the list has been reordered in between will silently edit a different anchor than intended.

For an interactive tool, the by-reference form is safer, because the reference follows the object even if the list order changes. For a script, the index form is more readable. The library supports both, and the choice is yours.

The position function is where the character comes from

The position function plays the crucial role in determining the distribution of colors between the anchors. It works similar to easing functions and can be imported from the Poline module.

A position function is a mathematical function that maps a value between 0 and 1 to another value between 0 and 1. By definition, the same position function for all axes means poline draws a straight line between the anchors. The chosen function determines the distribution of colors between the anchors.

js
import { Poline, positionFunctions } from "poline";

new Poline({
  positionFunction: positionFunctions.linearPosition,
});

If none is provided, the default is a sinusoidal function. That default is what gives the library its character, because a sine curve is not a straight line, so the samples are not evenly spaced along the segment, and the colours cluster towards the ends and spread in the middle.

The framing as an easing function is the useful part. If you have written a cubic bezier easing for a CSS transition, you already know the shape of the thing that is being asked for, and you can reason about where the samples land. A linear position function gives evenly spaced colours, a sine gives a different distribution, and anything else you can supply is a curve between the same two endpoints.

This is the knob that turns the generator from a fixed behaviour into a configurable one, and it is worth spending an afternoon on it, because it is where the difference between a palette that looks considered and one that looks accidental comes from.

Packaging, and the web component entry point

The package metadata shows a library that has thought about how it reaches different consumers. The published name is poline at version 0.13.1, the module type is set to module, and there are several entry points.

The main field points at the CommonJS build, the module field at the ES module build, the browser field at a minified browser build, and jsdelivr at the UMD build. The exports map is more precise still, with a require branch, an import branch and a types entry for the main package, and a second entry point called ./picker with its own require, import and types branches, where the types file is named webcomponent.d.ts.

So there is a second, separately importable component, a picker, exposed as its own subpath, and its type definition name suggests it is a web component. For a library whose main draw is a wheel of colours, a picker widget is the obvious companion, and having it in its own export entry means a bundler that only needs the generation code does not pull the UI in.

Installation is the usual npm line, with the unpkg CDN as an alternative, and the README recommends the mjs build when you want to use import syntax, while the UMD build exists for a plain script tag:

html
<script type="module">
  import { Poline } from "https://unpkg.com/poline?module";
</script>

The build scripts show how it is put together. The build target runs the tests, then the lint, then the TypeScript build, then the bundling script. Tests run under vitest, and linting is eslint with a prettier check on the source. There is a watch mode that uses esbuild directly, and a browser-sync command for serving the dist directory, which is how the demo page is served.

Where poline is the wrong tool

The first wrong-tool case is any requirement about perceptual uniformity or contrast. The interpolation happens in HSL, which is a cylindrical space, and the library says so itself in describing its method as defying conventional color science. HSL is convenient for people and wrong for hardware, and two colours that are equidistant in it can look nothing alike. If your work has an accessibility requirement, a colour-blindness simulation, or a brand specification, a generator that samples lines in HSL is the wrong instrument.

The second is a palette that has to be reproducible. If you omit the anchor colours, the library picks a random selection, and a random palette is a good thing for a demo and a bad thing for a product. The fix is to pass your own anchors, which the library supports, and to record them.

The third is a team that wants a maintained library with a stable contract. There are no GitHub releases for this project, the version lives only in package.json, and the API surface is small and informal. The repository carries a tea.yaml file and a.github directory, but the signs are of an individual project rather than an organisation. Pin the version and vendor the bundle if it matters.

There is also the question of scale. The library is designed around a handful of anchors and a few points, and there is no indication that it is meant to produce palettes with hundreds of colours or to be used as part of a real-time rendering pipeline.

Editorial conclusion

Adopt poline if you want palettes that are generated rather than sampled from a photograph, if you need the same generator in a Node script and in a browser, and if you are willing to tune a couple of anchors by eye. Do not adopt it for anything requiring perceptually uniform colour, for accessible contrast guarantees, or for palettes that must match a brand specification, because the interpolation happens in HSL space between anchors you choose. Before you depend on it, pin the version, since there are no GitHub releases, and check the packaging entries for the build format your bundler expects, because the package exposes several different files for different consumers.

Frequently asked questions

How do I install poline and generate a palette?

Run npm install poline, then import the Poline class and create an instance. Without arguments it generates a palette with two randomly selected anchors, and the result is available as poline.colors in HSL form or as poline.colorsCSS for ready-to-use CSS strings.

How many colours does a poline palette contain?

The palette is the product of the number of points and the number of anchor pairs, with each connection producing one colour. Four points are used by default and the numPoints property changes it, and it can also be changed after initialisation by setting the property on the instance.

What does clampToCircle do in poline?

It constrains XYZ coordinates to stay inside the valid colour wheel, a circle of radius 0.5 centred at 0.5, 0.5. It only affects future addAnchorPoint and updateAnchorPoint calls, existing anchors are not clamped retroactively, and a single call can override the default with a clamp option.

What is a position function in poline?

A mathematical function mapping a value between 0 and 1 to another value between 0 and 1, used to determine the distribution of colors between the anchors, in a similar way to easing functions. The default is a sinusoidal function, and positionFunctions.linearPosition is available from the module.

What licence is poline released under?

MIT, with the LICENSE file at the repository root. The npm package is at version 0.13.1, there are no GitHub releases, and the last push was on 2026-06-28.

Official sources

  1. Issues
  2. License: MIT
  3. meodai/poline on GitHub
  4. Project website
  5. README
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/meodai-poline.svg)](https://hysenlabs.com/projects/meodai-poline)

Was this useful?