@edelstone/tints-and-shades 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +72 -68
- package/dist/color.d.ts +19 -0
- package/dist/color.js +108 -0
- package/dist/generator.js +2 -15
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @edelstone/tints-and-shades
|
|
2
2
|
|
|
3
|
-
Deterministic
|
|
3
|
+
Deterministic color toolkit for tints, shades, color-wheel relationships, hex normalization, and hex/RGB/HSL conversion.
|
|
4
4
|
Used internally by the [Tint & Shade Generator](https://maketintsandshades.com) and published here as a standalone API.
|
|
5
5
|
|
|
6
6
|
## Install
|
|
@@ -9,14 +9,48 @@ Used internally by the [Tint & Shade Generator](https://maketintsandshades.com)
|
|
|
9
9
|
npm install @edelstone/tints-and-shades
|
|
10
10
|
```
|
|
11
11
|
|
|
12
|
+
## Quick example
|
|
13
|
+
|
|
14
|
+
```js
|
|
15
|
+
import {
|
|
16
|
+
normalizeHex,
|
|
17
|
+
calculateTints,
|
|
18
|
+
calculateShades,
|
|
19
|
+
getComplementaryHex
|
|
20
|
+
} from "@edelstone/tints-and-shades";
|
|
21
|
+
|
|
22
|
+
const baseHex = normalizeHex("#3bf"); // "33bbff"
|
|
23
|
+
if (!baseHex) throw new Error("Invalid color");
|
|
24
|
+
|
|
25
|
+
const tints = calculateTints(baseHex, [0, 0.5, 1]);
|
|
26
|
+
const shades = calculateShades(baseHex, [0, 0.5, 1]);
|
|
27
|
+
const complementary = getComplementaryHex(baseHex);
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
{
|
|
32
|
+
"complementary": "ff7733",
|
|
33
|
+
"tints": [
|
|
34
|
+
{ "hex": "33bbff", "ratio": 0, "percent": 0 },
|
|
35
|
+
{ "hex": "99ddff", "ratio": 0.5, "percent": 50 },
|
|
36
|
+
{ "hex": "ffffff", "ratio": 1, "percent": 100 }
|
|
37
|
+
],
|
|
38
|
+
"shades": [
|
|
39
|
+
{ "hex": "33bbff", "ratio": 0, "percent": 0 },
|
|
40
|
+
{ "hex": "1a5e80", "ratio": 0.5, "percent": 50 },
|
|
41
|
+
{ "hex": "000000", "ratio": 1, "percent": 100 }
|
|
42
|
+
]
|
|
43
|
+
}
|
|
44
|
+
```
|
|
45
|
+
|
|
12
46
|
## API
|
|
13
47
|
|
|
48
|
+
### Generation
|
|
49
|
+
|
|
14
50
|
```ts
|
|
15
51
|
calculateTints(colorValue: string, steps?: number[]): ScaleColor[]
|
|
16
52
|
calculateShades(colorValue: string, steps?: number[]): ScaleColor[]
|
|
17
|
-
```
|
|
18
53
|
|
|
19
|
-
```ts
|
|
20
54
|
type ScaleColor = {
|
|
21
55
|
hex: string;
|
|
22
56
|
ratio: number;
|
|
@@ -24,83 +58,53 @@ type ScaleColor = {
|
|
|
24
58
|
};
|
|
25
59
|
```
|
|
26
60
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
- **colorValue**
|
|
30
|
-
6-character hex string without `#`, e.g. `3b82f6`
|
|
31
|
-
Must be a valid 6-character hex value.
|
|
32
|
-
|
|
33
|
-
- **steps** (optional)
|
|
34
|
-
Array of finite numeric mix ratios.
|
|
35
|
-
Example: `[0, 0.1, 0.2, 0.3]`
|
|
61
|
+
Default steps: `[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]`
|
|
36
62
|
|
|
37
|
-
|
|
63
|
+
### Normalization
|
|
38
64
|
|
|
39
|
-
```
|
|
40
|
-
|
|
65
|
+
```ts
|
|
66
|
+
normalizeHex(value: string): string | null
|
|
41
67
|
```
|
|
42
68
|
|
|
43
|
-
|
|
69
|
+
Returns canonical 6-character lowercase hex without `#` (3-character hex is expanded), or `null` if invalid.
|
|
44
70
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
- `hex`: 6-character hex string (without `#`)
|
|
48
|
-
- `ratio`: numeric step ratio used for the mix
|
|
49
|
-
- `percent`: `ratio` expressed as a percentage (rounded to 1 decimal)
|
|
50
|
-
|
|
51
|
-
## Example
|
|
52
|
-
|
|
53
|
-
```js
|
|
54
|
-
import { calculateTints, calculateShades }
|
|
55
|
-
from "@edelstone/tints-and-shades";
|
|
71
|
+
### Relationships
|
|
56
72
|
|
|
57
|
-
|
|
58
|
-
|
|
73
|
+
```ts
|
|
74
|
+
getComplementaryHex(colorValue: string): string
|
|
75
|
+
getSplitComplementaryHexes(colorValue: string): [string, string]
|
|
76
|
+
getAnalogousHexes(colorValue: string): [string, string]
|
|
77
|
+
getTriadicHexes(colorValue: string): [string, string]
|
|
59
78
|
```
|
|
60
79
|
|
|
61
|
-
|
|
80
|
+
Hue is rotated by standard offsets while preserving saturation and lightness.
|
|
62
81
|
|
|
63
|
-
|
|
64
|
-
[
|
|
65
|
-
{ "hex": "000000", "ratio": 0, "percent": 0 },
|
|
66
|
-
{ "hex": "808080", "ratio": 0.5, "percent": 50 },
|
|
67
|
-
{ "hex": "ffffff", "ratio": 1, "percent": 100 }
|
|
68
|
-
]
|
|
69
|
-
```
|
|
82
|
+
### Conversions
|
|
70
83
|
|
|
71
|
-
|
|
84
|
+
```ts
|
|
85
|
+
hexToRgb(colorValue: string): RGB
|
|
86
|
+
rgbToHex(rgb: RGB): string
|
|
87
|
+
rgbToHsl(rgb: RGB): HSL
|
|
88
|
+
hslToRgb(hsl: HSL): RGB
|
|
89
|
+
|
|
90
|
+
type RGB = {
|
|
91
|
+
red: number;
|
|
92
|
+
green: number;
|
|
93
|
+
blue: number;
|
|
94
|
+
};
|
|
72
95
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
]
|
|
96
|
+
type HSL = {
|
|
97
|
+
hue: number;
|
|
98
|
+
saturation: number;
|
|
99
|
+
lightness: number;
|
|
100
|
+
};
|
|
79
101
|
```
|
|
80
102
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
- `colorValue` must be a 6-character hex string (no `#`).
|
|
84
|
-
- Invalid values throw a `TypeError`.
|
|
85
|
-
- `steps` must be an array of finite numbers.
|
|
86
|
-
- Invalid `steps` input throws a `TypeError`.
|
|
87
|
-
|
|
88
|
-
## Learn more
|
|
103
|
+
Converts between hex, RGB, and HSL using deterministic channel clamping and standard HSL conversion math.
|
|
89
104
|
|
|
90
|
-
|
|
105
|
+
## Validation rules
|
|
91
106
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
```bash
|
|
97
|
-
npm run build:api
|
|
98
|
-
npm run test:api
|
|
99
|
-
```
|
|
100
|
-
|
|
101
|
-
From package directory:
|
|
102
|
-
|
|
103
|
-
```bash
|
|
104
|
-
npm run build
|
|
105
|
-
npm run test
|
|
106
|
-
```
|
|
107
|
+
- Generation requires a 6-character hex string (no `#`) and finite numeric steps.
|
|
108
|
+
- Relationship and conversion helpers accept valid 3- or 6-character hex (optional `#`).
|
|
109
|
+
- `normalizeHex` returns `null` for invalid input.
|
|
110
|
+
- Other helpers throw `TypeError` for invalid input.
|
package/dist/color.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export type RGB = {
|
|
2
|
+
red: number;
|
|
3
|
+
green: number;
|
|
4
|
+
blue: number;
|
|
5
|
+
};
|
|
6
|
+
export type HSL = {
|
|
7
|
+
hue: number;
|
|
8
|
+
saturation: number;
|
|
9
|
+
lightness: number;
|
|
10
|
+
};
|
|
11
|
+
export declare const normalizeHex: (value: string) => string | null;
|
|
12
|
+
export declare const hexToRgb: (colorValue: string) => RGB;
|
|
13
|
+
export declare const rgbToHex: (rgb: RGB) => string;
|
|
14
|
+
export declare const rgbToHsl: (rgb: RGB) => HSL;
|
|
15
|
+
export declare const hslToRgb: ({ hue, saturation, lightness }: HSL) => RGB;
|
|
16
|
+
export declare const getComplementaryHex: (colorValue: string) => string;
|
|
17
|
+
export declare const getSplitComplementaryHexes: (colorValue: string) => [string, string];
|
|
18
|
+
export declare const getAnalogousHexes: (colorValue: string) => [string, string];
|
|
19
|
+
export declare const getTriadicHexes: (colorValue: string) => [string, string];
|
package/dist/color.js
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
const HEX_3_RE = /^[0-9a-fA-F]{3}$/;
|
|
2
|
+
const HEX_6_RE = /^[0-9a-fA-F]{6}$/;
|
|
3
|
+
const clampChannel = (value) => Math.min(Math.max(Math.round(value), 0), 255);
|
|
4
|
+
const toChannelHex = (value) => clampChannel(value).toString(16).padStart(2, "0");
|
|
5
|
+
const normalizeHue = (value) => ((value % 360) + 360) % 360;
|
|
6
|
+
const assertNormalizedHex = (value) => {
|
|
7
|
+
const normalized = normalizeHex(value);
|
|
8
|
+
if (!normalized) {
|
|
9
|
+
throw new TypeError("colorValue must be a valid 3- or 6-character hex string with optional '#'.");
|
|
10
|
+
}
|
|
11
|
+
return normalized;
|
|
12
|
+
};
|
|
13
|
+
export const normalizeHex = (value) => {
|
|
14
|
+
if (typeof value !== "string")
|
|
15
|
+
return null;
|
|
16
|
+
const stripped = value.trim().replace(/^#/, "").toLowerCase();
|
|
17
|
+
if (HEX_6_RE.test(stripped))
|
|
18
|
+
return stripped;
|
|
19
|
+
if (HEX_3_RE.test(stripped)) {
|
|
20
|
+
return stripped.split("").map((char) => char + char).join("");
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
};
|
|
24
|
+
export const hexToRgb = (colorValue) => {
|
|
25
|
+
const normalized = assertNormalizedHex(colorValue);
|
|
26
|
+
return {
|
|
27
|
+
red: parseInt(normalized.slice(0, 2), 16),
|
|
28
|
+
green: parseInt(normalized.slice(2, 4), 16),
|
|
29
|
+
blue: parseInt(normalized.slice(4, 6), 16)
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
export const rgbToHex = (rgb) => toChannelHex(rgb.red) + toChannelHex(rgb.green) + toChannelHex(rgb.blue);
|
|
33
|
+
export const rgbToHsl = (rgb) => {
|
|
34
|
+
const r = clampChannel(rgb.red) / 255;
|
|
35
|
+
const g = clampChannel(rgb.green) / 255;
|
|
36
|
+
const b = clampChannel(rgb.blue) / 255;
|
|
37
|
+
const max = Math.max(r, g, b);
|
|
38
|
+
const min = Math.min(r, g, b);
|
|
39
|
+
let hue = 0;
|
|
40
|
+
let saturation = 0;
|
|
41
|
+
const lightness = (max + min) / 2;
|
|
42
|
+
if (max !== min) {
|
|
43
|
+
const delta = max - min;
|
|
44
|
+
saturation = lightness > 0.5 ? delta / (2 - max - min) : delta / (max + min);
|
|
45
|
+
switch (max) {
|
|
46
|
+
case r:
|
|
47
|
+
hue = ((g - b) / delta + (g < b ? 6 : 0)) * 60;
|
|
48
|
+
break;
|
|
49
|
+
case g:
|
|
50
|
+
hue = ((b - r) / delta + 2) * 60;
|
|
51
|
+
break;
|
|
52
|
+
default:
|
|
53
|
+
hue = ((r - g) / delta + 4) * 60;
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
hue: normalizeHue(hue),
|
|
59
|
+
saturation,
|
|
60
|
+
lightness
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
const hueToRgb = (p, q, t) => {
|
|
64
|
+
let channel = t;
|
|
65
|
+
if (channel < 0)
|
|
66
|
+
channel += 1;
|
|
67
|
+
if (channel > 1)
|
|
68
|
+
channel -= 1;
|
|
69
|
+
if (channel < 1 / 6)
|
|
70
|
+
return p + (q - p) * 6 * channel;
|
|
71
|
+
if (channel < 1 / 2)
|
|
72
|
+
return q;
|
|
73
|
+
if (channel < 2 / 3)
|
|
74
|
+
return p + (q - p) * (2 / 3 - channel) * 6;
|
|
75
|
+
return p;
|
|
76
|
+
};
|
|
77
|
+
export const hslToRgb = ({ hue, saturation, lightness }) => {
|
|
78
|
+
const h = normalizeHue(hue) / 360;
|
|
79
|
+
const s = Math.min(Math.max(saturation, 0), 1);
|
|
80
|
+
const l = Math.min(Math.max(lightness, 0), 1);
|
|
81
|
+
if (s === 0) {
|
|
82
|
+
const value = clampChannel(l * 255);
|
|
83
|
+
return { red: value, green: value, blue: value };
|
|
84
|
+
}
|
|
85
|
+
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
86
|
+
const p = 2 * l - q;
|
|
87
|
+
return {
|
|
88
|
+
red: clampChannel(hueToRgb(p, q, h + 1 / 3) * 255),
|
|
89
|
+
green: clampChannel(hueToRgb(p, q, h) * 255),
|
|
90
|
+
blue: clampChannel(hueToRgb(p, q, h - 1 / 3) * 255)
|
|
91
|
+
};
|
|
92
|
+
};
|
|
93
|
+
const rotateHueHexes = (colorValue, offsets) => {
|
|
94
|
+
const normalized = assertNormalizedHex(colorValue);
|
|
95
|
+
const baseHsl = rgbToHsl(hexToRgb(normalized));
|
|
96
|
+
return offsets.map((offset) => {
|
|
97
|
+
const rgb = hslToRgb({
|
|
98
|
+
hue: normalizeHue(baseHsl.hue + offset),
|
|
99
|
+
saturation: baseHsl.saturation,
|
|
100
|
+
lightness: baseHsl.lightness
|
|
101
|
+
});
|
|
102
|
+
return rgbToHex(rgb);
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
export const getComplementaryHex = (colorValue) => rotateHueHexes(colorValue, [180])[0];
|
|
106
|
+
export const getSplitComplementaryHexes = (colorValue) => rotateHueHexes(colorValue, [150, 210]);
|
|
107
|
+
export const getAnalogousHexes = (colorValue) => rotateHueHexes(colorValue, [-30, 30]);
|
|
108
|
+
export const getTriadicHexes = (colorValue) => rotateHueHexes(colorValue, [120, 240]);
|
package/dist/generator.js
CHANGED
|
@@ -1,17 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
let str = number.toString();
|
|
3
|
-
while (str.length < length) {
|
|
4
|
-
str = "0" + str;
|
|
5
|
-
}
|
|
6
|
-
return str;
|
|
7
|
-
};
|
|
8
|
-
const hexToRGB = (colorValue) => ({
|
|
9
|
-
red: parseInt(colorValue.slice(0, 2), 16),
|
|
10
|
-
green: parseInt(colorValue.slice(2, 4), 16),
|
|
11
|
-
blue: parseInt(colorValue.slice(4, 6), 16)
|
|
12
|
-
});
|
|
13
|
-
const intToHex = (rgbint) => pad(Math.min(Math.max(Math.round(rgbint), 0), 255).toString(16), 2);
|
|
14
|
-
const rgbToHex = (rgb) => intToHex(rgb.red) + intToHex(rgb.green) + intToHex(rgb.blue);
|
|
1
|
+
import { hexToRgb, rgbToHex } from "./color.js";
|
|
15
2
|
const DEFAULT_STEPS = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
|
16
3
|
const validateColorValue = (colorValue) => {
|
|
17
4
|
if (typeof colorValue !== "string" || colorValue.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(colorValue)) {
|
|
@@ -33,7 +20,7 @@ const mixChannel = (from, to, ratio) => from + (to - from) * ratio;
|
|
|
33
20
|
const calculateScale = (colorValue, steps, mixFn) => {
|
|
34
21
|
validateColorValue(colorValue);
|
|
35
22
|
const stepRatios = resolveSteps(steps);
|
|
36
|
-
const color =
|
|
23
|
+
const color = hexToRgb(colorValue);
|
|
37
24
|
const values = [];
|
|
38
25
|
for (const ratio of stepRatios) {
|
|
39
26
|
const rgb = mixFn(color, ratio);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,4 @@
|
|
|
1
1
|
export { calculateTints, calculateShades } from "./generator.js";
|
|
2
2
|
export type { ScaleColor } from "./generator.js";
|
|
3
|
+
export { normalizeHex, hexToRgb, rgbToHex, rgbToHsl, hslToRgb, getComplementaryHex, getSplitComplementaryHexes, getAnalogousHexes, getTriadicHexes } from "./color.js";
|
|
4
|
+
export type { RGB, HSL } from "./color.js";
|
package/dist/index.js
CHANGED
package/package.json
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
"engines": {
|
|
13
13
|
"node": ">=18"
|
|
14
14
|
},
|
|
15
|
-
"version": "0.
|
|
16
|
-
"description": "Deterministic
|
|
15
|
+
"version": "0.2.0",
|
|
16
|
+
"description": "Deterministic color toolkit for tints, shades, color-wheel relationships, hex normalization, and hex/RGB/HSL conversion.",
|
|
17
17
|
"license": "MIT",
|
|
18
18
|
"type": "module",
|
|
19
19
|
"publishConfig": {
|