@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @edelstone/tints-and-shades
2
2
 
3
- Deterministic tint and shade generator for 6-character hex colors.
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
- ### Parameters
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
- If omitted, the default steps are:
63
+ ### Normalization
38
64
 
39
- ```js
40
- [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
65
+ ```ts
66
+ normalizeHex(value: string): string | null
41
67
  ```
42
68
 
43
- ## Returns
69
+ Returns canonical 6-character lowercase hex without `#` (3-character hex is expanded), or `null` if invalid.
44
70
 
45
- An array of `ScaleColor` objects:
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
- const tints = calculateTints("000000", [0, 0.5, 1]);
58
- const shades = calculateShades("ffffff", [0, 0.5, 1]);
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
- Tints output:
80
+ Hue is rotated by standard offsets while preserving saturation and lightness.
62
81
 
63
- ```json
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
- Shades output:
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
- ```json
74
- [
75
- { "hex": "ffffff", "ratio": 0, "percent": 0 },
76
- { "hex": "808080", "ratio": 0.5, "percent": 50 },
77
- { "hex": "000000", "ratio": 1, "percent": 100 }
78
- ]
96
+ type HSL = {
97
+ hue: number;
98
+ saturation: number;
99
+ lightness: number;
100
+ };
79
101
  ```
80
102
 
81
- ## Validation
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
- - Calculation method and rationale available on the [Tint & Shade Generator docs](https://maketintsandshades.com/about/#calculation-method).
105
+ ## Validation rules
91
106
 
92
- ## Development
93
-
94
- From repo root:
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.
@@ -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
- const pad = (number, length) => {
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 = hexToRGB(colorValue);
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
@@ -1 +1,2 @@
1
1
  export { calculateTints, calculateShades } from "./generator.js";
2
+ export { normalizeHex, hexToRgb, rgbToHex, rgbToHsl, hslToRgb, getComplementaryHex, getSplitComplementaryHexes, getAnalogousHexes, getTriadicHexes } from "./color.js";
package/package.json CHANGED
@@ -12,8 +12,8 @@
12
12
  "engines": {
13
13
  "node": ">=18"
14
14
  },
15
- "version": "0.1.6",
16
- "description": "Deterministic tint and shade generator for 6-character hex colors.",
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": {