2026-02-22 21:35:53 +01:00
|
|
|
import {
|
|
|
|
|
parse_color,
|
|
|
|
|
lighten_color,
|
|
|
|
|
darken_color,
|
|
|
|
|
saturate_color,
|
|
|
|
|
desaturate_color,
|
|
|
|
|
rotate_hue,
|
|
|
|
|
complement_color,
|
|
|
|
|
generate_random_colors,
|
|
|
|
|
generate_gradient,
|
|
|
|
|
generate_palette,
|
|
|
|
|
version,
|
2026-03-31 08:24:25 +02:00
|
|
|
} from './color-engine';
|
2026-02-22 21:35:53 +01:00
|
|
|
import type {
|
|
|
|
|
ApiResponse,
|
|
|
|
|
ColorInfoRequest,
|
|
|
|
|
ColorInfoData,
|
|
|
|
|
ConvertFormatRequest,
|
|
|
|
|
ConvertFormatData,
|
|
|
|
|
ColorManipulationRequest,
|
|
|
|
|
ColorManipulationData,
|
|
|
|
|
RandomColorsRequest,
|
|
|
|
|
RandomColorsData,
|
|
|
|
|
GradientRequest,
|
|
|
|
|
GradientData,
|
|
|
|
|
HealthData,
|
|
|
|
|
CapabilitiesData,
|
|
|
|
|
PaletteGenerateRequest,
|
|
|
|
|
PaletteGenerateData,
|
|
|
|
|
} from './types';
|
|
|
|
|
|
|
|
|
|
/**
|
2026-03-31 08:24:25 +02:00
|
|
|
* Color client backed by a pure TypeScript color engine.
|
|
|
|
|
* Zero network latency, works offline — no WASM required.
|
2026-02-22 21:35:53 +01:00
|
|
|
*/
|
2026-02-26 12:19:22 +01:00
|
|
|
export class ColorWASMClient {
|
2026-03-31 08:24:25 +02:00
|
|
|
private request<T>(fn: () => T): Promise<ApiResponse<T>> {
|
2026-02-22 21:35:53 +01:00
|
|
|
try {
|
2026-03-31 08:24:25 +02:00
|
|
|
return Promise.resolve({ success: true, data: fn() });
|
2026-02-22 21:35:53 +01:00
|
|
|
} catch (error) {
|
2026-03-31 08:24:25 +02:00
|
|
|
return Promise.resolve({
|
2026-02-22 21:35:53 +01:00
|
|
|
success: false,
|
|
|
|
|
error: {
|
2026-03-31 08:24:25 +02:00
|
|
|
code: 'COLOR_ERROR',
|
2026-02-22 21:35:53 +01:00
|
|
|
message: error instanceof Error ? error.message : 'Unknown error',
|
|
|
|
|
},
|
2026-03-31 08:24:25 +02:00
|
|
|
});
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color Information
|
|
|
|
|
async getColorInfo(request: ColorInfoRequest): Promise<ApiResponse<ColorInfoData>> {
|
|
|
|
|
return this.request(() => {
|
|
|
|
|
const colors = request.colors.map((colorStr) => {
|
2026-03-31 08:24:25 +02:00
|
|
|
const info = parse_color(colorStr);
|
2026-02-22 21:35:53 +01:00
|
|
|
return {
|
|
|
|
|
input: info.input,
|
|
|
|
|
hex: info.hex,
|
2026-03-31 08:24:25 +02:00
|
|
|
rgb: { r: info.rgb[0], g: info.rgb[1], b: info.rgb[2] },
|
|
|
|
|
hsl: { h: info.hsl[0], s: info.hsl[1], l: info.hsl[2] },
|
|
|
|
|
hsv: { h: info.hsv[0], s: info.hsv[1], v: info.hsv[2] },
|
|
|
|
|
lab: { l: info.lab[0], a: info.lab[1], b: info.lab[2] },
|
|
|
|
|
oklab: { l: info.oklab[0], a: info.oklab[1], b: info.oklab[2] },
|
|
|
|
|
lch: { l: info.lch[0], c: info.lch[1], h: info.lch[2] },
|
|
|
|
|
oklch: { l: info.oklch[0], c: info.oklch[1], h: info.oklch[2] },
|
|
|
|
|
cmyk: { c: info.cmyk[0], m: info.cmyk[1], y: info.cmyk[2], k: info.cmyk[3] },
|
2026-02-22 21:35:53 +01:00
|
|
|
brightness: info.brightness,
|
|
|
|
|
luminance: info.luminance,
|
|
|
|
|
is_light: info.is_light,
|
|
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
return { colors };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Format Conversion
|
|
|
|
|
async convertFormat(request: ConvertFormatRequest): Promise<ApiResponse<ConvertFormatData>> {
|
|
|
|
|
return this.request(() => {
|
|
|
|
|
const conversions = request.colors.map((colorStr) => {
|
2026-03-31 08:24:25 +02:00
|
|
|
const p = parse_color(colorStr);
|
2026-02-22 21:35:53 +01:00
|
|
|
let output: string;
|
|
|
|
|
|
|
|
|
|
switch (request.format) {
|
|
|
|
|
case 'hex':
|
2026-03-31 08:24:25 +02:00
|
|
|
output = p.hex;
|
2026-02-22 21:35:53 +01:00
|
|
|
break;
|
|
|
|
|
case 'rgb':
|
2026-03-31 08:24:25 +02:00
|
|
|
output = `rgb(${p.rgb[0]}, ${p.rgb[1]}, ${p.rgb[2]})`;
|
2026-02-22 21:35:53 +01:00
|
|
|
break;
|
|
|
|
|
case 'hsl':
|
2026-03-31 08:24:25 +02:00
|
|
|
output = `hsl(${p.hsl[0].toFixed(1)}, ${(p.hsl[1] * 100).toFixed(1)}%, ${(p.hsl[2] * 100).toFixed(1)}%)`;
|
2026-02-22 21:35:53 +01:00
|
|
|
break;
|
|
|
|
|
case 'hsv':
|
2026-03-31 08:24:25 +02:00
|
|
|
output = `hsv(${p.hsv[0].toFixed(1)}, ${(p.hsv[1] * 100).toFixed(1)}%, ${(p.hsv[2] * 100).toFixed(1)}%)`;
|
2026-02-22 21:35:53 +01:00
|
|
|
break;
|
|
|
|
|
case 'lab':
|
2026-03-31 08:24:25 +02:00
|
|
|
output = `lab(${p.lab[0].toFixed(2)}, ${p.lab[1].toFixed(2)}, ${p.lab[2].toFixed(2)})`;
|
2026-02-22 21:35:53 +01:00
|
|
|
break;
|
|
|
|
|
case 'lch':
|
2026-03-31 08:24:25 +02:00
|
|
|
output = `lch(${p.lch[0].toFixed(2)}, ${p.lch[1].toFixed(2)}, ${p.lch[2].toFixed(2)})`;
|
2026-02-22 21:35:53 +01:00
|
|
|
break;
|
2026-03-31 08:24:25 +02:00
|
|
|
case 'oklab':
|
|
|
|
|
output = `oklab(${(p.oklab[0] * 100).toFixed(1)}% ${p.oklab[1].toFixed(3)} ${p.oklab[2].toFixed(3)})`;
|
2026-02-26 12:07:21 +01:00
|
|
|
break;
|
2026-03-31 08:24:25 +02:00
|
|
|
case 'oklch':
|
|
|
|
|
output = `oklch(${(p.oklch[0] * 100).toFixed(1)}% ${p.oklch[1].toFixed(3)} ${p.oklch[2].toFixed(2)})`;
|
2026-02-26 12:07:21 +01:00
|
|
|
break;
|
2026-02-22 21:35:53 +01:00
|
|
|
default:
|
2026-03-31 08:24:25 +02:00
|
|
|
output = p.hex;
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:24:25 +02:00
|
|
|
return { input: colorStr, output };
|
2026-02-22 21:35:53 +01:00
|
|
|
});
|
|
|
|
|
return { conversions };
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color Manipulation
|
|
|
|
|
async lighten(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'lighten',
|
|
|
|
|
amount: request.amount,
|
|
|
|
|
colors: request.colors.map((c) => ({ input: c, output: lighten_color(c, request.amount) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async darken(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'darken',
|
|
|
|
|
amount: request.amount,
|
|
|
|
|
colors: request.colors.map((c) => ({ input: c, output: darken_color(c, request.amount) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async saturate(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'saturate',
|
|
|
|
|
amount: request.amount,
|
|
|
|
|
colors: request.colors.map((c) => ({ input: c, output: saturate_color(c, request.amount) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async desaturate(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'desaturate',
|
|
|
|
|
amount: request.amount,
|
|
|
|
|
colors: request.colors.map((c) => ({ input: c, output: desaturate_color(c, request.amount) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async rotate(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'rotate',
|
|
|
|
|
amount: request.amount,
|
|
|
|
|
colors: request.colors.map((c) => ({ input: c, output: rotate_hue(c, request.amount) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async complement(colors: string[]): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'complement',
|
|
|
|
|
colors: colors.map((c) => ({ input: c, output: complement_color(c) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async grayscale(colors: string[]): Promise<ApiResponse<ColorManipulationData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({
|
|
|
|
|
operation: 'grayscale',
|
|
|
|
|
colors: colors.map((c) => ({ input: c, output: desaturate_color(c, 1.0) })),
|
|
|
|
|
}));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Color Generation
|
|
|
|
|
async generateRandom(request: RandomColorsRequest): Promise<ApiResponse<RandomColorsData>> {
|
|
|
|
|
return this.request(() => {
|
|
|
|
|
const vivid = request.strategy === 'vivid' || request.strategy === 'lch';
|
2026-03-31 08:24:25 +02:00
|
|
|
return { colors: generate_random_colors(request.count, vivid) };
|
2026-02-22 21:35:53 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async generateGradient(request: GradientRequest): Promise<ApiResponse<GradientData>> {
|
|
|
|
|
return this.request(() => {
|
2026-03-31 08:24:25 +02:00
|
|
|
if (request.stops.length < 2) throw new Error('At least 2 color stops are required');
|
2026-02-22 21:35:53 +01:00
|
|
|
|
|
|
|
|
if (request.stops.length === 2) {
|
|
|
|
|
return {
|
|
|
|
|
stops: request.stops,
|
|
|
|
|
count: request.count,
|
2026-03-31 08:24:25 +02:00
|
|
|
gradient: generate_gradient(request.stops[0], request.stops[1], request.count),
|
2026-02-22 21:35:53 +01:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-31 08:24:25 +02:00
|
|
|
// Multi-stop: interpolate segment by segment
|
2026-02-22 21:35:53 +01:00
|
|
|
const segments = request.stops.length - 1;
|
|
|
|
|
const colorsPerSegment = Math.floor(request.count / segments);
|
|
|
|
|
const gradient: string[] = [];
|
|
|
|
|
|
|
|
|
|
for (let i = 0; i < segments; i++) {
|
2026-03-31 08:24:25 +02:00
|
|
|
const segColors = generate_gradient(
|
2026-02-22 21:35:53 +01:00
|
|
|
request.stops[i],
|
|
|
|
|
request.stops[i + 1],
|
2026-03-31 08:24:25 +02:00
|
|
|
i === segments - 1 ? request.count - gradient.length : colorsPerSegment,
|
2026-02-22 21:35:53 +01:00
|
|
|
);
|
2026-03-31 08:24:25 +02:00
|
|
|
gradient.push(...segColors.slice(0, -1));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
gradient.push(request.stops[request.stops.length - 1]);
|
|
|
|
|
|
2026-03-31 08:24:25 +02:00
|
|
|
return { stops: request.stops, count: request.count, gradient };
|
2026-02-22 21:35:53 +01:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// System
|
|
|
|
|
async getHealth(): Promise<ApiResponse<HealthData>> {
|
2026-03-31 08:24:25 +02:00
|
|
|
return this.request(() => ({ status: 'healthy', version: version() }));
|
2026-02-22 21:35:53 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getCapabilities(): Promise<ApiResponse<CapabilitiesData>> {
|
|
|
|
|
return this.request(() => ({
|
|
|
|
|
endpoints: [
|
|
|
|
|
'colors/info',
|
|
|
|
|
'colors/convert',
|
|
|
|
|
'colors/lighten',
|
|
|
|
|
'colors/darken',
|
|
|
|
|
'colors/saturate',
|
|
|
|
|
'colors/desaturate',
|
|
|
|
|
'colors/rotate',
|
|
|
|
|
'colors/complement',
|
|
|
|
|
'colors/grayscale',
|
|
|
|
|
'colors/random',
|
|
|
|
|
'colors/gradient',
|
|
|
|
|
'colors/names',
|
|
|
|
|
],
|
|
|
|
|
formats: ['hex', 'rgb', 'hsl', 'hsv', 'lab', 'lch'],
|
|
|
|
|
distance_metrics: ['cie76', 'ciede2000'],
|
|
|
|
|
colorblindness_types: ['protanopia', 'deuteranopia', 'tritanopia'],
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Palette Generation
|
|
|
|
|
async generatePalette(request: PaletteGenerateRequest): Promise<ApiResponse<PaletteGenerateData>> {
|
|
|
|
|
return this.request(() => {
|
|
|
|
|
const colors = generate_palette(request.base, request.scheme);
|
|
|
|
|
return {
|
|
|
|
|
base: request.base,
|
|
|
|
|
scheme: request.scheme,
|
2026-03-31 08:24:25 +02:00
|
|
|
palette: { primary: colors[0], secondary: colors.slice(1) },
|
2026-02-22 21:35:53 +01:00
|
|
|
};
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Export singleton instance
|
2026-02-26 12:19:22 +01:00
|
|
|
export const colorWASM = new ColorWASMClient();
|