poline
Version:
color palette generator mico-lib
599 lines (597 loc) • 18.7 kB
JavaScript
"use strict";
var __defProp = Object.defineProperty;
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
var __getOwnPropNames = Object.getOwnPropertyNames;
var __hasOwnProp = Object.prototype.hasOwnProperty;
var __pow = Math.pow;
var __export = (target, all) => {
for (var name in all)
__defProp(target, name, { get: all[name], enumerable: true });
};
var __copyProps = (to, from, except, desc) => {
if (from && typeof from === "object" || typeof from === "function") {
for (let key of __getOwnPropNames(from))
if (!__hasOwnProp.call(to, key) && key !== except)
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
}
return to;
};
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
// src/index.ts
var src_exports = {};
__export(src_exports, {
ColorPoint: () => ColorPoint,
Poline: () => Poline,
hslToPoint: () => hslToPoint,
pointToHSL: () => pointToHSL,
positionFunctions: () => positionFunctions,
randomHSLPair: () => randomHSLPair,
randomHSLTriple: () => randomHSLTriple
});
module.exports = __toCommonJS(src_exports);
var pointToHSL = (xyz, invertedLightness) => {
const [x, y, z] = xyz;
const cx = 0.5;
const cy = 0.5;
const radians = Math.atan2(y - cy, x - cx);
let deg = radians * (180 / Math.PI);
deg = (360 + deg) % 360;
const s = z;
const dist = Math.sqrt(Math.pow(y - cy, 2) + Math.pow(x - cx, 2));
const l = dist / cx;
return [deg, s, invertedLightness ? 1 - l : l];
};
var hslToPoint = (hsl, invertedLightness) => {
const [h, s, l] = hsl;
const cx = 0.5;
const cy = 0.5;
const radians = h / (180 / Math.PI);
const dist = (invertedLightness ? 1 - l : l) * cx;
const x = cx + dist * Math.cos(radians);
const y = cy + dist * Math.sin(radians);
const z = s;
return [x, y, z];
};
var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random()], lightnesses = [0.75 + Math.random() * 0.2, 0.3 + Math.random() * 0.2]) => [
[startHue, saturations[0], lightnesses[0]],
[(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]]
];
var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [
0.75 + Math.random() * 0.2,
Math.random() * 0.2,
0.75 + Math.random() * 0.2
]) => [
[startHue, saturations[0], lightnesses[0]],
[(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]],
[(startHue + 60 + Math.random() * 180) % 360, saturations[2], lightnesses[2]]
];
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) => {
const tModifiedX = fx(t, invert);
const tModifiedY = fy(t, invert);
const tModifiedZ = fz(t, invert);
const x = (1 - tModifiedX) * p1[0] + tModifiedX * p2[0];
const y = (1 - tModifiedY) * p1[1] + tModifiedY * p2[1];
const z = (1 - tModifiedZ) * p1[2] + tModifiedZ * p2[2];
return [x, y, z];
};
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) => {
const points = [];
for (let i = 0; i < numPoints; i++) {
const [x, y, z] = vectorOnLine(
i / (numPoints - 1),
p1,
p2,
invert,
fx,
fy,
fz
);
points.push([x, y, z]);
}
return points;
};
var linearPosition = (t) => {
return t;
};
var exponentialPosition = (t, reverse = false) => {
if (reverse) {
return 1 - __pow(1 - t, 2);
}
return __pow(t, 2);
};
var quadraticPosition = (t, reverse = false) => {
if (reverse) {
return 1 - __pow(1 - t, 3);
}
return __pow(t, 3);
};
var cubicPosition = (t, reverse = false) => {
if (reverse) {
return 1 - __pow(1 - t, 4);
}
return __pow(t, 4);
};
var quarticPosition = (t, reverse = false) => {
if (reverse) {
return 1 - __pow(1 - t, 5);
}
return __pow(t, 5);
};
var sinusoidalPosition = (t, reverse = false) => {
if (reverse) {
return 1 - Math.sin((1 - t) * Math.PI / 2);
}
return Math.sin(t * Math.PI / 2);
};
var asinusoidalPosition = (t, reverse = false) => {
if (reverse) {
return 1 - Math.asin(1 - t) / (Math.PI / 2);
}
return Math.asin(t) / (Math.PI / 2);
};
var arcPosition = (t, reverse = false) => {
if (reverse) {
return 1 - Math.sqrt(1 - __pow(t, 2));
}
return 1 - Math.sqrt(1 - t);
};
var smoothStepPosition = (t) => {
return __pow(t, 2) * (3 - 2 * t);
};
var positionFunctions = {
linearPosition,
exponentialPosition,
quadraticPosition,
cubicPosition,
quarticPosition,
sinusoidalPosition,
asinusoidalPosition,
arcPosition,
smoothStepPosition
};
var distance = (p1, p2, hueMode = false) => {
const a1 = p1[0];
const a2 = p2[0];
let diffA = 0;
if (hueMode && a1 !== null && a2 !== null) {
diffA = Math.min(Math.abs(a1 - a2), 360 - Math.abs(a1 - a2));
diffA = diffA / 360;
} else {
diffA = a1 === null || a2 === null ? 0 : a1 - a2;
}
const a = diffA;
const b = p1[1] === null || p2[1] === null ? 0 : p2[1] - p1[1];
const c = p1[2] === null || p2[2] === null ? 0 : p2[2] - p1[2];
return Math.sqrt(a * a + b * b + c * c);
};
var ColorPoint = class {
constructor({
xyz,
color,
invertedLightness = false
} = {}) {
this.x = 0;
this.y = 0;
this.z = 0;
this.color = [0, 0, 0];
this._invertedLightness = false;
this._invertedLightness = invertedLightness;
this.positionOrColor({ xyz, color, invertedLightness });
}
positionOrColor({
xyz,
color,
invertedLightness = false
}) {
if (xyz && color || !xyz && !color) {
throw new Error("Point must be initialized with either x,y,z or hsl");
} else if (xyz) {
this.x = xyz[0];
this.y = xyz[1];
this.z = xyz[2];
this.color = pointToHSL([this.x, this.y, this.z], invertedLightness);
} else if (color) {
this.color = color;
[this.x, this.y, this.z] = hslToPoint(color, invertedLightness);
}
}
set position([x, y, z]) {
this.x = x;
this.y = y;
this.z = z;
this.color = pointToHSL(
[this.x, this.y, this.z],
this._invertedLightness
);
}
get position() {
return [this.x, this.y, this.z];
}
set hsl([h, s, l]) {
this.color = [h, s, l];
[this.x, this.y, this.z] = hslToPoint(
this.color,
this._invertedLightness
);
}
get hsl() {
return this.color;
}
get hslCSS() {
const [h, s, l] = this.color;
return `hsl(${h.toFixed(2)}, ${(s * 100).toFixed(2)}%, ${(l * 100).toFixed(
2
)}%)`;
}
get oklchCSS() {
const [h, s, l] = this.color;
return `oklch(${(l * 100).toFixed(2)}% ${(s * 0.4).toFixed(3)} ${h.toFixed(
2
)})`;
}
get lchCSS() {
const [h, s, l] = this.color;
return `lch(${(l * 100).toFixed(2)}% ${(s * 150).toFixed(2)} ${h.toFixed(
2
)})`;
}
shiftHue(angle) {
this.color[0] = (360 + (this.color[0] + angle)) % 360;
[this.x, this.y, this.z] = hslToPoint(
this.color,
this._invertedLightness
);
}
};
var Poline = class {
constructor({
anchorColors = randomHSLPair(),
numPoints = 4,
positionFunction = sinusoidalPosition,
positionFunctionX,
positionFunctionY,
positionFunctionZ,
closedLoop,
invertedLightness
} = {
anchorColors: randomHSLPair(),
numPoints: 4,
positionFunction: sinusoidalPosition,
closedLoop: false
}) {
this._needsUpdate = true;
this._positionFunctionX = sinusoidalPosition;
this._positionFunctionY = sinusoidalPosition;
this._positionFunctionZ = sinusoidalPosition;
this.connectLastAndFirstAnchor = false;
this._animationFrame = null;
this._invertedLightness = false;
if (!anchorColors || anchorColors.length < 2) {
throw new Error("Must have at least two anchor colors");
}
this._anchorPoints = anchorColors.map(
(point) => new ColorPoint({ color: point, invertedLightness })
);
this._numPoints = numPoints + 2;
this._positionFunctionX = positionFunctionX || positionFunction || sinusoidalPosition;
this._positionFunctionY = positionFunctionY || positionFunction || sinusoidalPosition;
this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition;
this.connectLastAndFirstAnchor = closedLoop || false;
this._invertedLightness = invertedLightness || false;
this.updateAnchorPairs();
}
get numPoints() {
return this._numPoints - 2;
}
set numPoints(numPoints) {
if (numPoints < 1) {
throw new Error("Must have at least one point");
}
this._numPoints = numPoints + 2;
this.updateAnchorPairs();
}
set positionFunction(positionFunction) {
if (Array.isArray(positionFunction)) {
if (positionFunction.length !== 3) {
throw new Error("Position function array must have 3 elements");
}
if (typeof positionFunction[0] !== "function" || typeof positionFunction[1] !== "function" || typeof positionFunction[2] !== "function") {
throw new Error("Position function array must have 3 functions");
}
this._positionFunctionX = positionFunction[0];
this._positionFunctionY = positionFunction[1];
this._positionFunctionZ = positionFunction[2];
} else {
this._positionFunctionX = positionFunction;
this._positionFunctionY = positionFunction;
this._positionFunctionZ = positionFunction;
}
this.updateAnchorPairs();
}
get positionFunction() {
if (this._positionFunctionX === this._positionFunctionY && this._positionFunctionX === this._positionFunctionZ) {
return this._positionFunctionX;
}
return [
this._positionFunctionX,
this._positionFunctionY,
this._positionFunctionZ
];
}
set positionFunctionX(positionFunctionX) {
this._positionFunctionX = positionFunctionX;
this.updateAnchorPairs();
}
get positionFunctionX() {
return this._positionFunctionX;
}
set positionFunctionY(positionFunctionY) {
this._positionFunctionY = positionFunctionY;
this.updateAnchorPairs();
}
get positionFunctionY() {
return this._positionFunctionY;
}
set positionFunctionZ(positionFunctionZ) {
this._positionFunctionZ = positionFunctionZ;
this.updateAnchorPairs();
}
get positionFunctionZ() {
return this._positionFunctionZ;
}
get anchorPoints() {
return this._anchorPoints;
}
set anchorPoints(anchorPoints) {
this._anchorPoints = anchorPoints;
this.updateAnchorPairs();
}
updateAnchorPairs() {
this._anchorPairs = [];
const anchorPointsLength = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1;
for (let i = 0; i < anchorPointsLength; i++) {
const pair = [
this.anchorPoints[i],
this.anchorPoints[(i + 1) % this.anchorPoints.length]
];
this._anchorPairs.push(pair);
}
this.points = this._anchorPairs.map((pair, i) => {
const p1position = pair[0] ? pair[0].position : [0, 0, 0];
const p2position = pair[1] ? pair[1].position : [0, 0, 0];
const shouldInvertEase = this.shouldInvertEaseForSegment(i);
return vectorsOnLine(
p1position,
p2position,
this._numPoints,
shouldInvertEase ? true : false,
this.positionFunctionX,
this.positionFunctionY,
this.positionFunctionZ
).map(
(p) => new ColorPoint({ xyz: p, invertedLightness: this._invertedLightness })
);
});
}
addAnchorPoint({
xyz,
color,
insertAtIndex
}) {
const newAnchor = new ColorPoint({
xyz,
color,
invertedLightness: this._invertedLightness
});
if (insertAtIndex !== void 0) {
this.anchorPoints.splice(insertAtIndex, 0, newAnchor);
} else {
this.anchorPoints.push(newAnchor);
}
this.updateAnchorPairs();
return newAnchor;
}
removeAnchorPoint({
point,
index
}) {
if (!point && index === 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 apid;
if (index !== void 0) {
apid = index;
} else if (point) {
apid = this.anchorPoints.indexOf(point);
}
if (apid > -1 && apid < this.anchorPoints.length) {
this.anchorPoints.splice(apid, 1);
this.updateAnchorPairs();
} else {
throw new Error("Point not found");
}
}
updateAnchorPoint({
point,
pointIndex,
xyz,
color
}) {
if (pointIndex !== void 0) {
point = this.anchorPoints[pointIndex];
}
if (!point) {
throw new Error("Must provide a point or pointIndex");
}
if (!xyz && !color) {
throw new Error("Must provide a new xyz position or color");
}
if (xyz)
point.position = xyz;
if (color)
point.hsl = color;
this.updateAnchorPairs();
return point;
}
getClosestAnchorPoint({
xyz,
hsl,
maxDistance = 1
}) {
if (!xyz && !hsl) {
throw new Error("Must provide a xyz or hsl");
}
let distances;
if (xyz) {
distances = this.anchorPoints.map(
(anchor) => distance(anchor.position, xyz)
);
} else if (hsl) {
distances = this.anchorPoints.map(
(anchor) => distance(anchor.hsl, hsl, true)
);
}
const minDistance = Math.min(...distances);
if (minDistance > maxDistance) {
return null;
}
const closestAnchorIndex = distances.indexOf(minDistance);
return this.anchorPoints[closestAnchorIndex] || null;
}
set closedLoop(newStatus) {
this.connectLastAndFirstAnchor = newStatus;
this.updateAnchorPairs();
}
get closedLoop() {
return this.connectLastAndFirstAnchor;
}
set invertedLightness(newStatus) {
this._invertedLightness = newStatus;
this.updateAnchorPairs();
}
get invertedLightness() {
return this._invertedLightness;
}
/**
* Returns a flattened array of all points across all segments,
* removing duplicated anchor points at segment boundaries.
*
* Since anchor points exist at both the end of one segment and
* the beginning of the next, this method keeps only one instance of each.
* The filter logic keeps the first point (index 0) and then filters out
* points whose indices are multiples of the segment size (_numPoints),
* which are the anchor points at the start of each segment (except the first).
*
* This approach ensures we get all unique points in the correct order
* while avoiding duplicated anchor points.
*
* @returns {ColorPoint[]} A flat array of unique ColorPoint instances
*/
get flattenedPoints() {
return this.points.flat().filter((p, i) => i != 0 ? i % this._numPoints : true);
}
get colors() {
const colors = this.flattenedPoints.map((p) => p.color);
if (this.connectLastAndFirstAnchor && this._anchorPoints.length !== 2) {
colors.pop();
}
return colors;
}
cssColors(mode = "hsl") {
const methods = {
hsl: (p) => p.hslCSS,
oklch: (p) => p.oklchCSS,
lch: (p) => p.lchCSS
};
const cssColors = this.flattenedPoints.map(methods[mode]);
if (this.connectLastAndFirstAnchor) {
cssColors.pop();
}
return cssColors;
}
get colorsCSS() {
return this.cssColors("hsl");
}
get colorsCSSlch() {
return this.cssColors("lch");
}
get colorsCSSoklch() {
return this.cssColors("oklch");
}
shiftHue(hShift = 20) {
this.anchorPoints.forEach((p) => p.shiftHue(hShift));
this.updateAnchorPairs();
}
/**
* Returns a color at a specific position along the entire color line (0-1)
* Treats all segments as one continuous path, respecting easing functions
* @param t Position along the line (0-1), where 0 is start and 1 is end
* @returns ColorPoint at the specified position
* @example
* getColorAt(0) // Returns color at the very beginning
* getColorAt(0.5) // Returns color at the middle of the entire journey
* getColorAt(1) // Returns color at the very end
*/
getColorAt(t) {
var _a;
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");
}
const totalSegments = this.connectLastAndFirstAnchor ? this.anchorPoints.length : this.anchorPoints.length - 1;
const effectiveSegments = this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 ? 2 : totalSegments;
const segmentPosition = t * effectiveSegments;
const segmentIndex = Math.floor(segmentPosition);
const localT = segmentPosition - segmentIndex;
const actualSegmentIndex = segmentIndex >= effectiveSegments ? effectiveSegments - 1 : segmentIndex;
const actualLocalT = segmentIndex >= effectiveSegments ? 1 : localT;
const pair = this._anchorPairs[actualSegmentIndex];
if (!pair || pair.length < 2 || !pair[0] || !pair[1]) {
return new ColorPoint({
color: ((_a = this.anchorPoints[0]) == null ? void 0 : _a.color) || [0, 0, 0],
invertedLightness: this._invertedLightness
});
}
const p1position = pair[0].position;
const p2position = pair[1].position;
const shouldInvertEase = this.shouldInvertEaseForSegment(actualSegmentIndex);
const xyz = vectorOnLine(
actualLocalT,
p1position,
p2position,
shouldInvertEase,
this._positionFunctionX,
this._positionFunctionY,
this._positionFunctionZ
);
return new ColorPoint({
xyz,
invertedLightness: this._invertedLightness
});
}
/**
* Determines whether easing should be inverted for a given segment
* @param segmentIndex The index of the segment
* @returns Whether easing should be inverted
*/
shouldInvertEaseForSegment(segmentIndex) {
return !!(segmentIndex % 2 || this.connectLastAndFirstAnchor && this.anchorPoints.length === 2 && segmentIndex === 0);
}
};
var { p5 } = globalThis;
if (p5 && p5.VERSION && p5.VERSION.startsWith("1.")) {
console.info("p5 < 1.x detected, adding poline to p5 prototype");
const poline = new Poline();
p5.prototype.poline = poline;
const polineColors = () => poline.colors.map(
(c) => `hsl(${Math.round(c[0])},${c[1] * 100}%,${c[2] * 100}%)`
);
p5.prototype.registerMethod("polineColors", polineColors);
globalThis.poline = poline;
globalThis.polineColors = polineColors;
}