Skip to content

Commit 8da9c2c

Browse files
committed
feat: add clampToCircle functionality to Poline class
- Introduced clampToCircle method to constrain (x, y) coordinates within a color wheel circle. - Updated PolineOptions to include clampToCircle option. - Modified ColorPoint creation and update methods to respect the clampToCircle setting. - Added documentation for clampToCircle method and its usage examples.
1 parent 762612f commit 8da9c2c

18 files changed

+456
-79
lines changed

README.md

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,49 @@ poline.addAnchorPoint({
9999
});
100100
```
101101

102+
## Clamping to Circle
103+
104+
When working with XYZ coordinates, you may want to ensure anchor points stay within the valid color wheel (a circle of radius 0.5 centered at 0.5, 0.5). The `clampToCircle` option constrains coordinates to remain inside this boundary.
105+
106+
You can set a default behavior for all anchor operations:
107+
108+
```js
109+
// During initialization
110+
const poline = new Poline({
111+
anchorColors: [...],
112+
clampToCircle: true // All anchor operations will clamp by default
113+
});
114+
115+
// Or change it later
116+
poline.clampToCircle = true;
117+
```
118+
119+
You can also override the default on a per-call basis:
120+
121+
```js
122+
// Clamp this specific anchor point
123+
poline.addAnchorPoint({
124+
xyz: [0.9, 0.9, 0.5],
125+
clamp: true // Will be clamped to stay within color wheel
126+
});
127+
128+
// Override the default to disable clamping for this call
129+
poline.updateAnchorPoint({
130+
pointIndex: 0,
131+
xyz: [1.0, 1.0, 0.5],
132+
clamp: false // Explicitly disable clamping
133+
});
134+
```
135+
136+
The `clampToCircle` helper function is also exported for use in your own code:
137+
138+
```js
139+
import { clampToCircle } from 'poline';
140+
141+
const [clampedX, clampedY] = clampToCircle(0.9, 0.9);
142+
// Returns coordinates clamped to circle boundary
143+
```
144+
102145
## Updating Anchors
103146

104147
With this feature, you have the power to fine-tune your palette and make adjustments as your creative vision evolves. So whether you are looking to make subtle changes or bold alterations, "**Poline**" is always ready to help you achieve your desired result.
@@ -437,6 +480,7 @@ type PolineOptions = {
437480
positionFunctionZ?: PositionFunction;
438481
invertedLightness?: boolean;
439482
closedLoop?: boolean;
483+
clampToCircle?: boolean; // Optional: clamp anchor points to color wheel
440484
};
441485

442486
// Color point collection
@@ -469,6 +513,7 @@ constructor(options?: PolineOptions)
469513
- `anchorPoints: ColorPoint[]` - Get/set the anchor points
470514
- `closedLoop: boolean` - Get/set whether the palette forms a closed loop
471515
- `invertedLightness: boolean` - Get/set whether lightness calculation is inverted
516+
- `clampToCircle: boolean` - Get/set whether anchor point coordinates are clamped to the color wheel
472517
- `flattenedPoints: ColorPoint[]` - Get all points in a flat array
473518
- `colors: Vector3[]` - Get all colors as HSL arrays
474519
- `colorsCSS: string[]` - Get all colors as CSS HSL strings
@@ -478,9 +523,9 @@ constructor(options?: PolineOptions)
478523
#### Methods of the ColorPoint Class
479524

480525
- `updateAnchorPairs(): void` - Update internal anchor pairs
481-
- `addAnchorPoint(options: ColorPointCollection & { insertAtIndex?: number }): ColorPoint` - Add a new anchor point
526+
- `addAnchorPoint(options: ColorPointCollection & { insertAtIndex?: number; clamp?: boolean }): ColorPoint` - Add a new anchor point
482527
- `removeAnchorPoint(options: { point?: ColorPoint; index?: number }): void` - Remove an anchor point
483-
- `updateAnchorPoint(options: { point?: ColorPoint; pointIndex?: number } & ColorPointCollection): ColorPoint` - Update an anchor point
528+
- `updateAnchorPoint(options: { point?: ColorPoint; pointIndex?: number; clamp?: boolean } & ColorPointCollection): ColorPoint` - Update an anchor point
484529
- `getClosestAnchorPoint(options: { xyz?: PartialVector3; hsl?: PartialVector3; maxDistance?: number }): ColorPoint | null` - Find closest anchor point
485530
- `getColorAt(t: number): ColorPoint` - Get color at a specific position (0-1) along the entire palette
486531
- `shiftHue(hShift?: number): void` - Shift the hue of all colors

dist/index.cjs

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ var src_exports = {};
2222
__export(src_exports, {
2323
ColorPoint: () => ColorPoint,
2424
Poline: () => Poline,
25+
clampToCircle: () => clampToCircle,
2526
hslToPoint: () => hslToPoint,
2627
pointToHSL: () => pointToHSL,
2728
positionFunctions: () => positionFunctions,
@@ -56,6 +57,17 @@ var randomHSLPair = (startHue = Math.random() * 360, saturations = [Math.random(
5657
[startHue, saturations[0], lightnesses[0]],
5758
[(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]]
5859
];
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+
};
5971
var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [
6072
0.75 + Math.random() * 0.2,
6173
Math.random() * 0.2,
@@ -263,7 +275,8 @@ var Poline = class {
263275
positionFunctionY,
264276
positionFunctionZ,
265277
closedLoop,
266-
invertedLightness
278+
invertedLightness,
279+
clampToCircle: clampToCircle2
267280
} = {
268281
anchorColors: randomHSLPair(),
269282
numPoints: 4,
@@ -276,6 +289,7 @@ var Poline = class {
276289
this.connectLastAndFirstAnchor = false;
277290
this._animationFrame = null;
278291
this._invertedLightness = false;
292+
this._clampToCircle = false;
279293
if (!anchorColors || anchorColors.length < 2) {
280294
throw new Error("Must have at least two anchor colors");
281295
}
@@ -288,6 +302,7 @@ var Poline = class {
288302
this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition;
289303
this.connectLastAndFirstAnchor = closedLoop || false;
290304
this._invertedLightness = invertedLightness || false;
305+
this._clampToCircle = clampToCircle2 || false;
291306
this.updateAnchorPairs();
292307
}
293308
get numPoints() {
@@ -349,6 +364,12 @@ var Poline = class {
349364
get positionFunctionZ() {
350365
return this._positionFunctionZ;
351366
}
367+
get clampToCircle() {
368+
return this._clampToCircle;
369+
}
370+
set clampToCircle(clamp) {
371+
this._clampToCircle = clamp;
372+
}
352373
get anchorPoints() {
353374
return this._anchorPoints;
354375
}
@@ -386,10 +407,18 @@ var Poline = class {
386407
addAnchorPoint({
387408
xyz,
388409
color,
389-
insertAtIndex
410+
insertAtIndex,
411+
clamp
390412
}) {
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+
}
391420
const newAnchor = new ColorPoint({
392-
xyz,
421+
xyz: finalXyz,
393422
color,
394423
invertedLightness: this._invertedLightness
395424
});
@@ -428,7 +457,8 @@ var Poline = class {
428457
point,
429458
pointIndex,
430459
xyz,
431-
color
460+
color,
461+
clamp
432462
}) {
433463
if (pointIndex !== void 0) {
434464
point = this.anchorPoints[pointIndex];
@@ -439,8 +469,16 @@ var Poline = class {
439469
if (!xyz && !color) {
440470
throw new Error("Must provide a new xyz position or color");
441471
}
442-
if (xyz)
443-
point.position = xyz;
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+
}
444482
if (color)
445483
point.hsl = color;
446484
this.updateAnchorPairs();

dist/index.d.ts

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,19 @@ export declare const pointToHSL: (xyz: [number, number, number], invertedLightne
2929
**/
3030
export declare const hslToPoint: (hsl: [number, number, number], invertedLightness: boolean) => [number, number, number];
3131
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;
3245
export declare const randomHSLTriple: (startHue?: number, saturations?: [number, number, number], lightnesses?: [number, number, number]) => [Vector3, Vector3, Vector3];
3346
export type PositionFunction = (t: number, reverse?: boolean) => number;
3447
export declare const positionFunctions: {
@@ -75,6 +88,7 @@ export type PolineOptions = {
7588
positionFunctionZ?: (t: number, invert?: boolean) => number;
7689
invertedLightness?: boolean;
7790
closedLoop?: boolean;
91+
clampToCircle?: boolean;
7892
};
7993
export declare class Poline {
8094
private _anchorPoints;
@@ -87,7 +101,8 @@ export declare class Poline {
87101
private connectLastAndFirstAnchor;
88102
private _animationFrame;
89103
private _invertedLightness;
90-
constructor({ anchorColors, numPoints, positionFunction, positionFunctionX, positionFunctionY, positionFunctionZ, closedLoop, invertedLightness, }?: PolineOptions);
104+
private _clampToCircle;
105+
constructor({ anchorColors, numPoints, positionFunction, positionFunctionX, positionFunctionY, positionFunctionZ, closedLoop, invertedLightness, clampToCircle, }?: PolineOptions);
91106
get numPoints(): number;
92107
set numPoints(numPoints: number);
93108
set positionFunction(positionFunction: PositionFunction | PositionFunction[]);
@@ -98,19 +113,23 @@ export declare class Poline {
98113
get positionFunctionY(): PositionFunction;
99114
set positionFunctionZ(positionFunctionZ: PositionFunction);
100115
get positionFunctionZ(): PositionFunction;
116+
get clampToCircle(): boolean;
117+
set clampToCircle(clamp: boolean);
101118
get anchorPoints(): ColorPoint[];
102119
set anchorPoints(anchorPoints: ColorPoint[]);
103120
updateAnchorPairs(): void;
104-
addAnchorPoint({ xyz, color, insertAtIndex, }: ColorPointCollection & {
121+
addAnchorPoint({ xyz, color, insertAtIndex, clamp, }: ColorPointCollection & {
105122
insertAtIndex?: number;
123+
clamp?: boolean;
106124
}): ColorPoint;
107125
removeAnchorPoint({ point, index, }: {
108126
point?: ColorPoint;
109127
index?: number;
110128
}): void;
111-
updateAnchorPoint({ point, pointIndex, xyz, color, }: {
129+
updateAnchorPoint({ point, pointIndex, xyz, color, clamp, }: {
112130
point?: ColorPoint;
113131
pointIndex?: number;
132+
clamp?: boolean;
114133
} & ColorPointCollection): ColorPoint;
115134
getClosestAnchorPoint({ xyz, hsl, maxDistance, }: {
116135
xyz?: PartialVector3;

dist/index.html

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2775,18 +2775,7 @@ <h2 class="export__title">${paletteTitle}</h2>
27752775
}
27762776

27772777
if (e.key === 'p') {
2778-
// Clamp position to be within the hue wheel (circle with radius 0.5 centered at 0.5, 0.5)
2779-
let px = lastX;
2780-
let py = lastY;
2781-
const dx = px - 0.5;
2782-
const dy = py - 0.5;
2783-
const dist = Math.hypot(dx, dy);
2784-
if (dist > 0.5) {
2785-
// Project to edge of circle
2786-
px = 0.5 + (dx / dist) * 0.5;
2787-
py = 0.5 + (dy / dist) * 0.5;
2788-
}
2789-
lastSelectedPoint = poline.addAnchorPoint({xyz: [px, py, py]});
2778+
lastSelectedPoint = poline.addAnchorPoint({xyz: [lastX, lastY, lastY], clamp: true});
27902779
updateSVG();
27912780
updateFullCode();
27922781
}

dist/index.js

Lines changed: 44 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ var poline = (() => {
2323
__export(src_exports, {
2424
ColorPoint: () => ColorPoint,
2525
Poline: () => Poline,
26+
clampToCircle: () => clampToCircle,
2627
hslToPoint: () => hslToPoint,
2728
pointToHSL: () => pointToHSL,
2829
positionFunctions: () => positionFunctions,
@@ -56,6 +57,17 @@ var poline = (() => {
5657
[startHue, saturations[0], lightnesses[0]],
5758
[(startHue + 60 + Math.random() * 180) % 360, saturations[1], lightnesses[1]]
5859
];
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+
};
5971
var randomHSLTriple = (startHue = Math.random() * 360, saturations = [Math.random(), Math.random(), Math.random()], lightnesses = [
6072
0.75 + Math.random() * 0.2,
6173
Math.random() * 0.2,
@@ -263,7 +275,8 @@ var poline = (() => {
263275
positionFunctionY,
264276
positionFunctionZ,
265277
closedLoop,
266-
invertedLightness
278+
invertedLightness,
279+
clampToCircle: clampToCircle2
267280
} = {
268281
anchorColors: randomHSLPair(),
269282
numPoints: 4,
@@ -276,6 +289,7 @@ var poline = (() => {
276289
this.connectLastAndFirstAnchor = false;
277290
this._animationFrame = null;
278291
this._invertedLightness = false;
292+
this._clampToCircle = false;
279293
if (!anchorColors || anchorColors.length < 2) {
280294
throw new Error("Must have at least two anchor colors");
281295
}
@@ -288,6 +302,7 @@ var poline = (() => {
288302
this._positionFunctionZ = positionFunctionZ || positionFunction || sinusoidalPosition;
289303
this.connectLastAndFirstAnchor = closedLoop || false;
290304
this._invertedLightness = invertedLightness || false;
305+
this._clampToCircle = clampToCircle2 || false;
291306
this.updateAnchorPairs();
292307
}
293308
get numPoints() {
@@ -349,6 +364,12 @@ var poline = (() => {
349364
get positionFunctionZ() {
350365
return this._positionFunctionZ;
351366
}
367+
get clampToCircle() {
368+
return this._clampToCircle;
369+
}
370+
set clampToCircle(clamp) {
371+
this._clampToCircle = clamp;
372+
}
352373
get anchorPoints() {
353374
return this._anchorPoints;
354375
}
@@ -386,10 +407,18 @@ var poline = (() => {
386407
addAnchorPoint({
387408
xyz,
388409
color,
389-
insertAtIndex
410+
insertAtIndex,
411+
clamp
390412
}) {
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+
}
391420
const newAnchor = new ColorPoint({
392-
xyz,
421+
xyz: finalXyz,
393422
color,
394423
invertedLightness: this._invertedLightness
395424
});
@@ -428,7 +457,8 @@ var poline = (() => {
428457
point,
429458
pointIndex,
430459
xyz,
431-
color
460+
color,
461+
clamp
432462
}) {
433463
if (pointIndex !== void 0) {
434464
point = this.anchorPoints[pointIndex];
@@ -439,8 +469,16 @@ var poline = (() => {
439469
if (!xyz && !color) {
440470
throw new Error("Must provide a new xyz position or color");
441471
}
442-
if (xyz)
443-
point.position = xyz;
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+
}
444482
if (color)
445483
point.hsl = color;
446484
this.updateAnchorPairs();

0 commit comments

Comments
 (0)