@edelstone/tints-and-shades 0.1.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 +106 -0
- package/dist/generator.d.ts +7 -0
- package/dist/generator.js +59 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/package.json +31 -0
package/README.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# @edelstone/tints-and-shades
|
|
2
|
+
|
|
3
|
+
Deterministic tint and shade generator for 6-character hex colors.
|
|
4
|
+
Used internally by the [Tint & Shade Generator](https://maketintsandshades.com) and published as a standalone API.
|
|
5
|
+
|
|
6
|
+
## Install
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
npm install @edelstone/tints-and-shades
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## API
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
calculateTints(colorValue: string, steps?: number[]): ScaleColor[]
|
|
16
|
+
calculateShades(colorValue: string, steps?: number[]): ScaleColor[]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
type ScaleColor = {
|
|
21
|
+
hex: string;
|
|
22
|
+
ratio: number;
|
|
23
|
+
percent: number;
|
|
24
|
+
};
|
|
25
|
+
```
|
|
26
|
+
|
|
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]`
|
|
36
|
+
|
|
37
|
+
If omitted, the default steps are:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
[0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Returns
|
|
44
|
+
|
|
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";
|
|
56
|
+
|
|
57
|
+
const tints = calculateTints("000000", [0, 0.5, 1]);
|
|
58
|
+
const shades = calculateShades("ffffff", [0, 0.5, 1]);
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Tints output:
|
|
62
|
+
|
|
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
|
+
```
|
|
70
|
+
|
|
71
|
+
Shades output:
|
|
72
|
+
|
|
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
|
+
]
|
|
79
|
+
```
|
|
80
|
+
|
|
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
|
|
89
|
+
|
|
90
|
+
- Calculation method and rationale: [Tint & Shade Generator docs](https://maketintsandshades.com/about/#calculation-method)
|
|
91
|
+
|
|
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
|
+
```
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type ScaleColor = {
|
|
2
|
+
hex: string;
|
|
3
|
+
ratio: number;
|
|
4
|
+
percent: number;
|
|
5
|
+
};
|
|
6
|
+
export declare const calculateShades: (colorValue: string, steps?: number[]) => ScaleColor[];
|
|
7
|
+
export declare const calculateTints: (colorValue: string, steps?: number[]) => ScaleColor[];
|
|
@@ -0,0 +1,59 @@
|
|
|
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);
|
|
15
|
+
const DEFAULT_STEPS = [0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
|
|
16
|
+
const validateColorValue = (colorValue) => {
|
|
17
|
+
if (typeof colorValue !== "string" || colorValue.length !== 6 || !/^[0-9a-fA-F]{6}$/.test(colorValue)) {
|
|
18
|
+
throw new TypeError("colorValue must be a 6-character hex string without '#'.");
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
const resolveSteps = (steps) => {
|
|
22
|
+
if (typeof steps === "undefined")
|
|
23
|
+
return DEFAULT_STEPS;
|
|
24
|
+
if (!Array.isArray(steps)) {
|
|
25
|
+
throw new TypeError("steps must be an array of numbers.");
|
|
26
|
+
}
|
|
27
|
+
if (!steps.every((step) => typeof step === "number" && Number.isFinite(step))) {
|
|
28
|
+
throw new TypeError("steps must be an array of numbers.");
|
|
29
|
+
}
|
|
30
|
+
return steps;
|
|
31
|
+
};
|
|
32
|
+
const mixChannel = (from, to, ratio) => from + (to - from) * ratio;
|
|
33
|
+
const calculateScale = (colorValue, steps, mixFn) => {
|
|
34
|
+
validateColorValue(colorValue);
|
|
35
|
+
const stepRatios = resolveSteps(steps);
|
|
36
|
+
const color = hexToRGB(colorValue);
|
|
37
|
+
const values = [];
|
|
38
|
+
for (const ratio of stepRatios) {
|
|
39
|
+
const rgb = mixFn(color, ratio);
|
|
40
|
+
values.push({
|
|
41
|
+
hex: rgbToHex(rgb),
|
|
42
|
+
ratio,
|
|
43
|
+
percent: Number((ratio * 100).toFixed(1))
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
return values;
|
|
47
|
+
};
|
|
48
|
+
const rgbShade = (rgb, ratio) => ({
|
|
49
|
+
red: mixChannel(rgb.red, 0, ratio),
|
|
50
|
+
green: mixChannel(rgb.green, 0, ratio),
|
|
51
|
+
blue: mixChannel(rgb.blue, 0, ratio)
|
|
52
|
+
});
|
|
53
|
+
const rgbTint = (rgb, ratio) => ({
|
|
54
|
+
red: mixChannel(rgb.red, 255, ratio),
|
|
55
|
+
green: mixChannel(rgb.green, 255, ratio),
|
|
56
|
+
blue: mixChannel(rgb.blue, 255, ratio)
|
|
57
|
+
});
|
|
58
|
+
export const calculateShades = (colorValue, steps) => calculateScale(colorValue, steps, rgbShade);
|
|
59
|
+
export const calculateTints = (colorValue, steps) => calculateScale(colorValue, steps, rgbTint);
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { calculateTints, calculateShades } from "./generator.js";
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@edelstone/tints-and-shades",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Core tint and shade calculation API used by tints-and-shades.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"publishConfig": {
|
|
8
|
+
"access": "public"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
13
|
+
"main": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts",
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"import": "./dist/index.js"
|
|
19
|
+
}
|
|
20
|
+
},
|
|
21
|
+
"sideEffects": false,
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.json",
|
|
24
|
+
"clean": "rm -rf dist",
|
|
25
|
+
"test": "npm run build && node --test test/*.test.mjs",
|
|
26
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"typescript": "^5.7.0"
|
|
30
|
+
}
|
|
31
|
+
}
|