@rog0x/mcp-color-tools 1.0.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.
@@ -0,0 +1,165 @@
1
+ // Color Mixer — mix colors, blend, lighten, darken, saturate, desaturate.
2
+
3
+ import {
4
+ parseColor,
5
+ rgbToHex,
6
+ rgbToHsl,
7
+ hslToRgb,
8
+ type RGB,
9
+ type HSL,
10
+ } from "./color-converter.js";
11
+
12
+ interface MixedColor {
13
+ hex: string;
14
+ rgb: RGB;
15
+ hsl: HSL;
16
+ }
17
+
18
+ function formatResult(rgb: RGB): MixedColor {
19
+ return {
20
+ hex: rgbToHex(rgb),
21
+ rgb,
22
+ hsl: rgbToHsl(rgb),
23
+ };
24
+ }
25
+
26
+ function clampRgb(rgb: RGB): RGB {
27
+ return {
28
+ r: Math.max(0, Math.min(255, Math.round(rgb.r))),
29
+ g: Math.max(0, Math.min(255, Math.round(rgb.g))),
30
+ b: Math.max(0, Math.min(255, Math.round(rgb.b))),
31
+ };
32
+ }
33
+
34
+ function clampHsl(hsl: HSL): HSL {
35
+ return {
36
+ h: ((hsl.h % 360) + 360) % 360,
37
+ s: Math.max(0, Math.min(100, hsl.s)),
38
+ l: Math.max(0, Math.min(100, hsl.l)),
39
+ };
40
+ }
41
+
42
+ // Mix two or more colors in specified ratios (weighted average in RGB space)
43
+ export function mixColors(
44
+ colors: string[],
45
+ weights?: number[]
46
+ ): MixedColor {
47
+ if (colors.length < 2) {
48
+ throw new Error("Provide at least 2 colors to mix");
49
+ }
50
+
51
+ const rgbColors = colors.map((c) => parseColor(c));
52
+
53
+ // Default to equal weights
54
+ const w = weights && weights.length === colors.length
55
+ ? weights
56
+ : colors.map(() => 1);
57
+
58
+ const totalWeight = w.reduce((sum, v) => sum + v, 0);
59
+ if (totalWeight === 0) throw new Error("Total weight cannot be zero");
60
+
61
+ let r = 0, g = 0, b = 0;
62
+ for (let i = 0; i < rgbColors.length; i++) {
63
+ const ratio = w[i] / totalWeight;
64
+ r += rgbColors[i].r * ratio;
65
+ g += rgbColors[i].g * ratio;
66
+ b += rgbColors[i].b * ratio;
67
+ }
68
+
69
+ const result = clampRgb({ r, g, b });
70
+ return formatResult(result);
71
+ }
72
+
73
+ // Blend two colors at a given ratio (0 = color1, 1 = color2)
74
+ export function blendColors(
75
+ color1: string,
76
+ color2: string,
77
+ ratio: number = 0.5
78
+ ): MixedColor {
79
+ const t = Math.max(0, Math.min(1, ratio));
80
+ const rgb1 = parseColor(color1);
81
+ const rgb2 = parseColor(color2);
82
+
83
+ const result = clampRgb({
84
+ r: rgb1.r + (rgb2.r - rgb1.r) * t,
85
+ g: rgb1.g + (rgb2.g - rgb1.g) * t,
86
+ b: rgb1.b + (rgb2.b - rgb1.b) * t,
87
+ });
88
+
89
+ return formatResult(result);
90
+ }
91
+
92
+ // Lighten a color by a percentage (0-100)
93
+ export function lightenColor(color: string, amount: number = 10): MixedColor {
94
+ const rgb = parseColor(color);
95
+ const hsl = rgbToHsl(rgb);
96
+ const adjusted = clampHsl({ ...hsl, l: hsl.l + amount });
97
+ const result = hslToRgb(adjusted);
98
+ return formatResult(result);
99
+ }
100
+
101
+ // Darken a color by a percentage (0-100)
102
+ export function darkenColor(color: string, amount: number = 10): MixedColor {
103
+ const rgb = parseColor(color);
104
+ const hsl = rgbToHsl(rgb);
105
+ const adjusted = clampHsl({ ...hsl, l: hsl.l - amount });
106
+ const result = hslToRgb(adjusted);
107
+ return formatResult(result);
108
+ }
109
+
110
+ // Saturate a color by a percentage (0-100)
111
+ export function saturateColor(color: string, amount: number = 10): MixedColor {
112
+ const rgb = parseColor(color);
113
+ const hsl = rgbToHsl(rgb);
114
+ const adjusted = clampHsl({ ...hsl, s: hsl.s + amount });
115
+ const result = hslToRgb(adjusted);
116
+ return formatResult(result);
117
+ }
118
+
119
+ // Desaturate a color by a percentage (0-100)
120
+ export function desaturateColor(color: string, amount: number = 10): MixedColor {
121
+ const rgb = parseColor(color);
122
+ const hsl = rgbToHsl(rgb);
123
+ const adjusted = clampHsl({ ...hsl, s: hsl.s - amount });
124
+ const result = hslToRgb(adjusted);
125
+ return formatResult(result);
126
+ }
127
+
128
+ export type MixOperation =
129
+ | "mix"
130
+ | "blend"
131
+ | "lighten"
132
+ | "darken"
133
+ | "saturate"
134
+ | "desaturate";
135
+
136
+ export interface MixRequest {
137
+ operation: MixOperation;
138
+ colors: string[];
139
+ weights?: number[];
140
+ ratio?: number;
141
+ amount?: number;
142
+ }
143
+
144
+ export function colorMix(req: MixRequest): MixedColor {
145
+ switch (req.operation) {
146
+ case "mix":
147
+ return mixColors(req.colors, req.weights);
148
+ case "blend":
149
+ if (req.colors.length < 2)
150
+ throw new Error("Blend requires exactly 2 colors");
151
+ return blendColors(req.colors[0], req.colors[1], req.ratio ?? 0.5);
152
+ case "lighten":
153
+ return lightenColor(req.colors[0], req.amount ?? 10);
154
+ case "darken":
155
+ return darkenColor(req.colors[0], req.amount ?? 10);
156
+ case "saturate":
157
+ return saturateColor(req.colors[0], req.amount ?? 10);
158
+ case "desaturate":
159
+ return desaturateColor(req.colors[0], req.amount ?? 10);
160
+ default:
161
+ throw new Error(
162
+ `Unknown operation: "${req.operation}". Use: mix, blend, lighten, darken, saturate, desaturate`
163
+ );
164
+ }
165
+ }
@@ -0,0 +1,80 @@
1
+ // Contrast Checker — check WCAG contrast ratio between two colors.
2
+ // Reports AA/AAA compliance for normal and large text.
3
+
4
+ import { parseColor, rgbToHex, type RGB } from "./color-converter.js";
5
+
6
+ interface ContrastResult {
7
+ color1: { hex: string; rgb: RGB };
8
+ color2: { hex: string; rgb: RGB };
9
+ ratio: number;
10
+ ratioString: string;
11
+ wcag: {
12
+ aa: {
13
+ normalText: boolean;
14
+ largeText: boolean;
15
+ uiComponents: boolean;
16
+ };
17
+ aaa: {
18
+ normalText: boolean;
19
+ largeText: boolean;
20
+ };
21
+ };
22
+ rating: string;
23
+ }
24
+
25
+ function relativeLuminance(rgb: RGB): number {
26
+ const toLinear = (c: number): number => {
27
+ const srgb = c / 255;
28
+ return srgb <= 0.04045
29
+ ? srgb / 12.92
30
+ : Math.pow((srgb + 0.055) / 1.055, 2.4);
31
+ };
32
+
33
+ const r = toLinear(rgb.r);
34
+ const g = toLinear(rgb.g);
35
+ const b = toLinear(rgb.b);
36
+
37
+ return 0.2126 * r + 0.7152 * g + 0.0722 * b;
38
+ }
39
+
40
+ function contrastRatio(lum1: number, lum2: number): number {
41
+ const lighter = Math.max(lum1, lum2);
42
+ const darker = Math.min(lum1, lum2);
43
+ return (lighter + 0.05) / (darker + 0.05);
44
+ }
45
+
46
+ function getRating(ratio: number): string {
47
+ if (ratio >= 7) return "Excellent (AAA)";
48
+ if (ratio >= 4.5) return "Good (AA normal text)";
49
+ if (ratio >= 3) return "Acceptable (AA large text only)";
50
+ return "Poor (fails WCAG AA)";
51
+ }
52
+
53
+ export function checkContrast(color1: string, color2: string): ContrastResult {
54
+ const rgb1 = parseColor(color1);
55
+ const rgb2 = parseColor(color2);
56
+
57
+ const lum1 = relativeLuminance(rgb1);
58
+ const lum2 = relativeLuminance(rgb2);
59
+ const ratio = contrastRatio(lum1, lum2);
60
+ const rounded = Math.round(ratio * 100) / 100;
61
+
62
+ return {
63
+ color1: { hex: rgbToHex(rgb1), rgb: rgb1 },
64
+ color2: { hex: rgbToHex(rgb2), rgb: rgb2 },
65
+ ratio: rounded,
66
+ ratioString: `${rounded}:1`,
67
+ wcag: {
68
+ aa: {
69
+ normalText: ratio >= 4.5,
70
+ largeText: ratio >= 3,
71
+ uiComponents: ratio >= 3,
72
+ },
73
+ aaa: {
74
+ normalText: ratio >= 7,
75
+ largeText: ratio >= 4.5,
76
+ },
77
+ },
78
+ rating: getRating(ratio),
79
+ };
80
+ }
@@ -0,0 +1,143 @@
1
+ // CSS Gradient Generator — generate CSS gradient code from colors.
2
+ // Supports linear, radial, and conic gradients with direction, stops, and browser prefixes.
3
+
4
+ import { parseColor, rgbToHex } from "./color-converter.js";
5
+
6
+ export type GradientType = "linear" | "radial" | "conic";
7
+
8
+ export interface ColorStop {
9
+ color: string;
10
+ position?: number; // percentage 0-100
11
+ }
12
+
13
+ export interface GradientRequest {
14
+ type: GradientType;
15
+ colors: (string | ColorStop)[];
16
+ direction?: string; // e.g. "to right", "45deg", "circle", "from 0deg"
17
+ includePrefix?: boolean; // include -webkit- prefix (default true)
18
+ }
19
+
20
+ interface GradientResult {
21
+ type: GradientType;
22
+ css: string;
23
+ cssWithPrefix: string;
24
+ cssProperty: string;
25
+ stops: { hex: string; position: string }[];
26
+ }
27
+
28
+ function normalizeStops(
29
+ colors: (string | ColorStop)[]
30
+ ): { hex: string; position: string }[] {
31
+ if (colors.length < 2) {
32
+ throw new Error("Gradient requires at least 2 colors");
33
+ }
34
+
35
+ return colors.map((entry, index) => {
36
+ let colorStr: string;
37
+ let pos: number | undefined;
38
+
39
+ if (typeof entry === "string") {
40
+ colorStr = entry;
41
+ } else {
42
+ colorStr = entry.color;
43
+ pos = entry.position;
44
+ }
45
+
46
+ const rgb = parseColor(colorStr);
47
+ const hex = rgbToHex(rgb);
48
+
49
+ // Auto-distribute if no position given
50
+ if (pos === undefined) {
51
+ pos = colors.length === 1 ? 0 : (index / (colors.length - 1)) * 100;
52
+ }
53
+
54
+ return {
55
+ hex,
56
+ position: `${Math.round(pos)}%`,
57
+ };
58
+ });
59
+ }
60
+
61
+ function buildStopsString(stops: { hex: string; position: string }[]): string {
62
+ return stops.map((s) => `${s.hex} ${s.position}`).join(", ");
63
+ }
64
+
65
+ function buildLinearGradient(
66
+ stops: { hex: string; position: string }[],
67
+ direction: string
68
+ ): { standard: string; prefixed: string } {
69
+ const stopsStr = buildStopsString(stops);
70
+ const standard = `linear-gradient(${direction}, ${stopsStr})`;
71
+ const prefixed = `-webkit-linear-gradient(${direction}, ${stopsStr})`;
72
+ return { standard, prefixed };
73
+ }
74
+
75
+ function buildRadialGradient(
76
+ stops: { hex: string; position: string }[],
77
+ shape: string
78
+ ): { standard: string; prefixed: string } {
79
+ const stopsStr = buildStopsString(stops);
80
+ const standard = `radial-gradient(${shape}, ${stopsStr})`;
81
+ const prefixed = `-webkit-radial-gradient(${shape}, ${stopsStr})`;
82
+ return { standard, prefixed };
83
+ }
84
+
85
+ function buildConicGradient(
86
+ stops: { hex: string; position: string }[],
87
+ direction: string
88
+ ): { standard: string; prefixed: string } {
89
+ const stopsStr = buildStopsString(stops);
90
+ const standard = `conic-gradient(${direction}, ${stopsStr})`;
91
+ // conic-gradient has limited prefix support but include for completeness
92
+ const prefixed = `-webkit-conic-gradient(${direction}, ${stopsStr})`;
93
+ return { standard, prefixed };
94
+ }
95
+
96
+ export function generateGradient(req: GradientRequest): GradientResult {
97
+ const stops = normalizeStops(req.colors);
98
+ const includePrefix = req.includePrefix !== false;
99
+
100
+ let standard: string;
101
+ let prefixed: string;
102
+
103
+ switch (req.type) {
104
+ case "linear": {
105
+ const direction = req.direction || "to right";
106
+ const result = buildLinearGradient(stops, direction);
107
+ standard = result.standard;
108
+ prefixed = result.prefixed;
109
+ break;
110
+ }
111
+ case "radial": {
112
+ const shape = req.direction || "circle at center";
113
+ const result = buildRadialGradient(stops, shape);
114
+ standard = result.standard;
115
+ prefixed = result.prefixed;
116
+ break;
117
+ }
118
+ case "conic": {
119
+ const direction = req.direction || "from 0deg at center";
120
+ const result = buildConicGradient(stops, direction);
121
+ standard = result.standard;
122
+ prefixed = result.prefixed;
123
+ break;
124
+ }
125
+ default:
126
+ throw new Error(
127
+ `Unknown gradient type: "${req.type}". Use: linear, radial, conic`
128
+ );
129
+ }
130
+
131
+ const cssProperty = `background: ${standard};`;
132
+ const cssWithPrefix = includePrefix
133
+ ? `background: ${prefixed};\nbackground: ${standard};`
134
+ : cssProperty;
135
+
136
+ return {
137
+ type: req.type,
138
+ css: standard,
139
+ cssWithPrefix,
140
+ cssProperty,
141
+ stops,
142
+ };
143
+ }
@@ -0,0 +1,126 @@
1
+ // Palette Generator — generate color palettes from a base color.
2
+ // Supports: complementary, analogous, triadic, split-complementary, monochromatic.
3
+
4
+ import {
5
+ parseColor,
6
+ rgbToHsl,
7
+ hslToRgb,
8
+ rgbToHex,
9
+ type RGB,
10
+ type HSL,
11
+ } from "./color-converter.js";
12
+
13
+ export type PaletteType =
14
+ | "complementary"
15
+ | "analogous"
16
+ | "triadic"
17
+ | "split-complementary"
18
+ | "monochromatic";
19
+
20
+ interface PaletteColor {
21
+ hex: string;
22
+ rgb: { r: number; g: number; b: number };
23
+ hsl: { h: number; s: number; l: number };
24
+ role: string;
25
+ }
26
+
27
+ interface PaletteResult {
28
+ type: PaletteType;
29
+ base: string;
30
+ colors: PaletteColor[];
31
+ }
32
+
33
+ function hslColor(h: number, s: number, l: number, role: string): PaletteColor {
34
+ const hNorm = ((h % 360) + 360) % 360;
35
+ const sNorm = Math.max(0, Math.min(100, s));
36
+ const lNorm = Math.max(0, Math.min(100, l));
37
+ const hsl: HSL = { h: Math.round(hNorm * 100) / 100, s: Math.round(sNorm * 100) / 100, l: Math.round(lNorm * 100) / 100 };
38
+ const rgb = hslToRgb(hsl);
39
+ return {
40
+ hex: rgbToHex(rgb),
41
+ rgb,
42
+ hsl,
43
+ role,
44
+ };
45
+ }
46
+
47
+ function generateComplementary(hsl: HSL): PaletteColor[] {
48
+ return [
49
+ hslColor(hsl.h, hsl.s, hsl.l, "base"),
50
+ hslColor(hsl.h + 180, hsl.s, hsl.l, "complementary"),
51
+ ];
52
+ }
53
+
54
+ function generateAnalogous(hsl: HSL): PaletteColor[] {
55
+ return [
56
+ hslColor(hsl.h - 30, hsl.s, hsl.l, "analogous -30"),
57
+ hslColor(hsl.h - 15, hsl.s, hsl.l, "analogous -15"),
58
+ hslColor(hsl.h, hsl.s, hsl.l, "base"),
59
+ hslColor(hsl.h + 15, hsl.s, hsl.l, "analogous +15"),
60
+ hslColor(hsl.h + 30, hsl.s, hsl.l, "analogous +30"),
61
+ ];
62
+ }
63
+
64
+ function generateTriadic(hsl: HSL): PaletteColor[] {
65
+ return [
66
+ hslColor(hsl.h, hsl.s, hsl.l, "base"),
67
+ hslColor(hsl.h + 120, hsl.s, hsl.l, "triadic +120"),
68
+ hslColor(hsl.h + 240, hsl.s, hsl.l, "triadic +240"),
69
+ ];
70
+ }
71
+
72
+ function generateSplitComplementary(hsl: HSL): PaletteColor[] {
73
+ return [
74
+ hslColor(hsl.h, hsl.s, hsl.l, "base"),
75
+ hslColor(hsl.h + 150, hsl.s, hsl.l, "split-complementary +150"),
76
+ hslColor(hsl.h + 210, hsl.s, hsl.l, "split-complementary +210"),
77
+ ];
78
+ }
79
+
80
+ function generateMonochromatic(hsl: HSL): PaletteColor[] {
81
+ return [
82
+ hslColor(hsl.h, hsl.s, Math.max(hsl.l - 30, 5), "darkest"),
83
+ hslColor(hsl.h, hsl.s, Math.max(hsl.l - 15, 10), "darker"),
84
+ hslColor(hsl.h, hsl.s, hsl.l, "base"),
85
+ hslColor(hsl.h, hsl.s, Math.min(hsl.l + 15, 90), "lighter"),
86
+ hslColor(hsl.h, hsl.s, Math.min(hsl.l + 30, 95), "lightest"),
87
+ ];
88
+ }
89
+
90
+ export function generatePalette(
91
+ color: string,
92
+ type: PaletteType
93
+ ): PaletteResult {
94
+ const rgb = parseColor(color);
95
+ const hsl = rgbToHsl(rgb);
96
+
97
+ let colors: PaletteColor[];
98
+
99
+ switch (type) {
100
+ case "complementary":
101
+ colors = generateComplementary(hsl);
102
+ break;
103
+ case "analogous":
104
+ colors = generateAnalogous(hsl);
105
+ break;
106
+ case "triadic":
107
+ colors = generateTriadic(hsl);
108
+ break;
109
+ case "split-complementary":
110
+ colors = generateSplitComplementary(hsl);
111
+ break;
112
+ case "monochromatic":
113
+ colors = generateMonochromatic(hsl);
114
+ break;
115
+ default:
116
+ throw new Error(
117
+ `Unknown palette type: "${type}". Use: complementary, analogous, triadic, split-complementary, monochromatic`
118
+ );
119
+ }
120
+
121
+ return {
122
+ type,
123
+ base: rgbToHex(rgb),
124
+ colors,
125
+ };
126
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "outDir": "./dist",
7
+ "rootDir": "./src",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true
12
+ },
13
+ "include": ["src/**/*"]
14
+ }