Files
valknar 00af8edce6 refactor: replace pastel-wasm with pure TypeScript color engine
Removes the @valknarthing/pastel-wasm WASM dependency and replaces it
with a self-contained TypeScript implementation of all color operations
(parsing, conversion, manipulation, gradients, palette generation).
Adds eslint-plugin-react-hooks which was missing from devDependencies.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-31 08:24:25 +02:00

254 lines
8.2 KiB
TypeScript

import {
parse_color,
lighten_color,
darken_color,
saturate_color,
desaturate_color,
rotate_hue,
complement_color,
generate_random_colors,
generate_gradient,
generate_palette,
version,
} from './color-engine';
import type {
ApiResponse,
ColorInfoRequest,
ColorInfoData,
ConvertFormatRequest,
ConvertFormatData,
ColorManipulationRequest,
ColorManipulationData,
RandomColorsRequest,
RandomColorsData,
GradientRequest,
GradientData,
HealthData,
CapabilitiesData,
PaletteGenerateRequest,
PaletteGenerateData,
} from './types';
/**
* Color client backed by a pure TypeScript color engine.
* Zero network latency, works offline — no WASM required.
*/
export class ColorWASMClient {
private request<T>(fn: () => T): Promise<ApiResponse<T>> {
try {
return Promise.resolve({ success: true, data: fn() });
} catch (error) {
return Promise.resolve({
success: false,
error: {
code: 'COLOR_ERROR',
message: error instanceof Error ? error.message : 'Unknown error',
},
});
}
}
// Color Information
async getColorInfo(request: ColorInfoRequest): Promise<ApiResponse<ColorInfoData>> {
return this.request(() => {
const colors = request.colors.map((colorStr) => {
const info = parse_color(colorStr);
return {
input: info.input,
hex: info.hex,
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] },
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) => {
const p = parse_color(colorStr);
let output: string;
switch (request.format) {
case 'hex':
output = p.hex;
break;
case 'rgb':
output = `rgb(${p.rgb[0]}, ${p.rgb[1]}, ${p.rgb[2]})`;
break;
case 'hsl':
output = `hsl(${p.hsl[0].toFixed(1)}, ${(p.hsl[1] * 100).toFixed(1)}%, ${(p.hsl[2] * 100).toFixed(1)}%)`;
break;
case 'hsv':
output = `hsv(${p.hsv[0].toFixed(1)}, ${(p.hsv[1] * 100).toFixed(1)}%, ${(p.hsv[2] * 100).toFixed(1)}%)`;
break;
case 'lab':
output = `lab(${p.lab[0].toFixed(2)}, ${p.lab[1].toFixed(2)}, ${p.lab[2].toFixed(2)})`;
break;
case 'lch':
output = `lch(${p.lch[0].toFixed(2)}, ${p.lch[1].toFixed(2)}, ${p.lch[2].toFixed(2)})`;
break;
case 'oklab':
output = `oklab(${(p.oklab[0] * 100).toFixed(1)}% ${p.oklab[1].toFixed(3)} ${p.oklab[2].toFixed(3)})`;
break;
case 'oklch':
output = `oklch(${(p.oklch[0] * 100).toFixed(1)}% ${p.oklch[1].toFixed(3)} ${p.oklch[2].toFixed(2)})`;
break;
default:
output = p.hex;
}
return { input: colorStr, output };
});
return { conversions };
});
}
// Color Manipulation
async lighten(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'lighten',
amount: request.amount,
colors: request.colors.map((c) => ({ input: c, output: lighten_color(c, request.amount) })),
}));
}
async darken(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'darken',
amount: request.amount,
colors: request.colors.map((c) => ({ input: c, output: darken_color(c, request.amount) })),
}));
}
async saturate(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'saturate',
amount: request.amount,
colors: request.colors.map((c) => ({ input: c, output: saturate_color(c, request.amount) })),
}));
}
async desaturate(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'desaturate',
amount: request.amount,
colors: request.colors.map((c) => ({ input: c, output: desaturate_color(c, request.amount) })),
}));
}
async rotate(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'rotate',
amount: request.amount,
colors: request.colors.map((c) => ({ input: c, output: rotate_hue(c, request.amount) })),
}));
}
async complement(colors: string[]): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'complement',
colors: colors.map((c) => ({ input: c, output: complement_color(c) })),
}));
}
async grayscale(colors: string[]): Promise<ApiResponse<ColorManipulationData>> {
return this.request(() => ({
operation: 'grayscale',
colors: colors.map((c) => ({ input: c, output: desaturate_color(c, 1.0) })),
}));
}
// Color Generation
async generateRandom(request: RandomColorsRequest): Promise<ApiResponse<RandomColorsData>> {
return this.request(() => {
const vivid = request.strategy === 'vivid' || request.strategy === 'lch';
return { colors: generate_random_colors(request.count, vivid) };
});
}
async generateGradient(request: GradientRequest): Promise<ApiResponse<GradientData>> {
return this.request(() => {
if (request.stops.length < 2) throw new Error('At least 2 color stops are required');
if (request.stops.length === 2) {
return {
stops: request.stops,
count: request.count,
gradient: generate_gradient(request.stops[0], request.stops[1], request.count),
};
}
// Multi-stop: interpolate segment by segment
const segments = request.stops.length - 1;
const colorsPerSegment = Math.floor(request.count / segments);
const gradient: string[] = [];
for (let i = 0; i < segments; i++) {
const segColors = generate_gradient(
request.stops[i],
request.stops[i + 1],
i === segments - 1 ? request.count - gradient.length : colorsPerSegment,
);
gradient.push(...segColors.slice(0, -1));
}
gradient.push(request.stops[request.stops.length - 1]);
return { stops: request.stops, count: request.count, gradient };
});
}
// System
async getHealth(): Promise<ApiResponse<HealthData>> {
return this.request(() => ({ status: 'healthy', version: version() }));
}
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,
palette: { primary: colors[0], secondary: colors.slice(1) },
};
});
}
}
// Export singleton instance
export const colorWASM = new ColorWASMClient();