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>
This commit is contained in:
+73
-170
@@ -1,5 +1,4 @@
|
||||
import {
|
||||
init,
|
||||
parse_color,
|
||||
lighten_color,
|
||||
darken_color,
|
||||
@@ -11,7 +10,7 @@ import {
|
||||
generate_gradient,
|
||||
generate_palette,
|
||||
version,
|
||||
} from '@valknarthing/pastel-wasm';
|
||||
} from './color-engine';
|
||||
import type {
|
||||
ApiResponse,
|
||||
ColorInfoRequest,
|
||||
@@ -30,43 +29,22 @@ import type {
|
||||
PaletteGenerateData,
|
||||
} from './types';
|
||||
|
||||
// Initialize WASM module
|
||||
let wasmInitialized = false;
|
||||
|
||||
async function ensureWasmInit() {
|
||||
if (!wasmInitialized) {
|
||||
init(); // Initialize panic hook
|
||||
wasmInitialized = true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* WASM-based Color client
|
||||
* Provides the same interface as ColorAPIClient but uses WebAssembly
|
||||
* Zero network latency, works offline!
|
||||
* Color client backed by a pure TypeScript color engine.
|
||||
* Zero network latency, works offline — no WASM required.
|
||||
*/
|
||||
export class ColorWASMClient {
|
||||
constructor() {
|
||||
// Initialize WASM eagerly
|
||||
ensureWasmInit().catch(console.error);
|
||||
}
|
||||
|
||||
private async request<T>(fn: () => T): Promise<ApiResponse<T>> {
|
||||
private request<T>(fn: () => T): Promise<ApiResponse<T>> {
|
||||
try {
|
||||
await ensureWasmInit();
|
||||
const data = fn();
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
};
|
||||
return Promise.resolve({ success: true, data: fn() });
|
||||
} catch (error) {
|
||||
return {
|
||||
return Promise.resolve({
|
||||
success: false,
|
||||
error: {
|
||||
code: 'WASM_ERROR',
|
||||
code: 'COLOR_ERROR',
|
||||
message: error instanceof Error ? error.message : 'Unknown error',
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -74,51 +52,18 @@ export class ColorWASMClient {
|
||||
async getColorInfo(request: ColorInfoRequest): Promise<ApiResponse<ColorInfoData>> {
|
||||
return this.request(() => {
|
||||
const colors = request.colors.map((colorStr) => {
|
||||
const info = parse_color(colorStr) as any;
|
||||
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 ? info.oklab[0] : info.lab[0] / 100.0,
|
||||
a: info.oklab ? info.oklab[1] : info.lab[1] / 100.0,
|
||||
b: info.oklab ? info.oklab[2] : info.lab[2] / 100.0,
|
||||
},
|
||||
lch: {
|
||||
l: info.lch[0],
|
||||
c: info.lch[1],
|
||||
h: info.lch[2],
|
||||
},
|
||||
oklch: {
|
||||
l: info.oklch ? info.oklch[0] : info.lch[0] / 100.0,
|
||||
c: info.oklch ? info.oklch[1] : info.lch[1] / 100.0,
|
||||
h: info.oklch ? info.oklch[2] : info.lch[2],
|
||||
},
|
||||
cmyk: {
|
||||
c: 0,
|
||||
m: 0,
|
||||
y: 0,
|
||||
k: 0,
|
||||
},
|
||||
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,
|
||||
@@ -132,50 +77,39 @@ export class ColorWASMClient {
|
||||
async convertFormat(request: ConvertFormatRequest): Promise<ApiResponse<ConvertFormatData>> {
|
||||
return this.request(() => {
|
||||
const conversions = request.colors.map((colorStr) => {
|
||||
const parsed = parse_color(colorStr) as any;
|
||||
const p = parse_color(colorStr);
|
||||
let output: string;
|
||||
|
||||
switch (request.format) {
|
||||
case 'hex':
|
||||
output = parsed.hex;
|
||||
output = p.hex;
|
||||
break;
|
||||
case 'rgb':
|
||||
output = `rgb(${parsed.rgb[0]}, ${parsed.rgb[1]}, ${parsed.rgb[2]})`;
|
||||
output = `rgb(${p.rgb[0]}, ${p.rgb[1]}, ${p.rgb[2]})`;
|
||||
break;
|
||||
case 'hsl':
|
||||
output = `hsl(${parsed.hsl[0].toFixed(1)}, ${(parsed.hsl[1] * 100).toFixed(1)}%, ${(parsed.hsl[2] * 100).toFixed(1)}%)`;
|
||||
output = `hsl(${p.hsl[0].toFixed(1)}, ${(p.hsl[1] * 100).toFixed(1)}%, ${(p.hsl[2] * 100).toFixed(1)}%)`;
|
||||
break;
|
||||
case 'hsv':
|
||||
output = `hsv(${parsed.hsv[0].toFixed(1)}, ${(parsed.hsv[1] * 100).toFixed(1)}%, ${(parsed.hsv[2] * 100).toFixed(1)}%)`;
|
||||
output = `hsv(${p.hsv[0].toFixed(1)}, ${(p.hsv[1] * 100).toFixed(1)}%, ${(p.hsv[2] * 100).toFixed(1)}%)`;
|
||||
break;
|
||||
case 'lab':
|
||||
output = `lab(${parsed.lab[0].toFixed(2)}, ${parsed.lab[1].toFixed(2)}, ${parsed.lab[2].toFixed(2)})`;
|
||||
output = `lab(${p.lab[0].toFixed(2)}, ${p.lab[1].toFixed(2)}, ${p.lab[2].toFixed(2)})`;
|
||||
break;
|
||||
case 'lch':
|
||||
output = `lch(${parsed.lch[0].toFixed(2)}, ${parsed.lch[1].toFixed(2)}, ${parsed.lch[2].toFixed(2)})`;
|
||||
output = `lch(${p.lch[0].toFixed(2)}, ${p.lch[1].toFixed(2)}, ${p.lch[2].toFixed(2)})`;
|
||||
break;
|
||||
case 'oklab': {
|
||||
const l = parsed.oklab ? parsed.oklab[0] : parsed.lab[0] / 100.0;
|
||||
const a = parsed.oklab ? parsed.oklab[1] : parsed.lab[1] / 100.0;
|
||||
const b = parsed.oklab ? parsed.oklab[2] : parsed.lab[2] / 100.0;
|
||||
output = `oklab(${(l * 100).toFixed(1)}% ${a.toFixed(3)} ${b.toFixed(3)})`;
|
||||
case 'oklab':
|
||||
output = `oklab(${(p.oklab[0] * 100).toFixed(1)}% ${p.oklab[1].toFixed(3)} ${p.oklab[2].toFixed(3)})`;
|
||||
break;
|
||||
}
|
||||
case 'oklch': {
|
||||
const l = parsed.oklch ? parsed.oklch[0] : parsed.lch[0] / 100.0;
|
||||
const c = parsed.oklch ? parsed.oklch[1] : parsed.lch[1] / 100.0;
|
||||
const h = parsed.oklch ? parsed.oklch[2] : parsed.lch[2];
|
||||
output = `oklch(${(l * 100).toFixed(1)}% ${c.toFixed(3)} ${h.toFixed(2)})`;
|
||||
case 'oklch':
|
||||
output = `oklch(${(p.oklch[0] * 100).toFixed(1)}% ${p.oklch[1].toFixed(3)} ${p.oklch[2].toFixed(2)})`;
|
||||
break;
|
||||
}
|
||||
default:
|
||||
output = parsed.hex;
|
||||
output = p.hex;
|
||||
}
|
||||
|
||||
return {
|
||||
input: colorStr,
|
||||
output,
|
||||
};
|
||||
return { input: colorStr, output };
|
||||
});
|
||||
return { conversions };
|
||||
});
|
||||
@@ -183,129 +117,101 @@ export class ColorWASMClient {
|
||||
|
||||
// Color Manipulation
|
||||
async lighten(request: ColorManipulationRequest): Promise<ApiResponse<ColorManipulationData>> {
|
||||
return this.request(() => {
|
||||
const colors = request.colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: lighten_color(colorStr, request.amount),
|
||||
}));
|
||||
return { operation: 'lighten', amount: request.amount, colors };
|
||||
});
|
||||
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(() => {
|
||||
const colors = request.colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: darken_color(colorStr, request.amount),
|
||||
}));
|
||||
return { operation: 'darken', amount: request.amount, colors };
|
||||
});
|
||||
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(() => {
|
||||
const colors = request.colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: saturate_color(colorStr, request.amount),
|
||||
}));
|
||||
return { operation: 'saturate', amount: request.amount, colors };
|
||||
});
|
||||
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(() => {
|
||||
const colors = request.colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: desaturate_color(colorStr, request.amount),
|
||||
}));
|
||||
return { operation: 'desaturate', amount: request.amount, colors };
|
||||
});
|
||||
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(() => {
|
||||
const colors = request.colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: rotate_hue(colorStr, request.amount),
|
||||
}));
|
||||
return { operation: 'rotate', amount: request.amount, colors };
|
||||
});
|
||||
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(() => {
|
||||
const results = colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: complement_color(colorStr),
|
||||
}));
|
||||
return { operation: 'complement', colors: results };
|
||||
});
|
||||
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(() => {
|
||||
const results = colors.map((colorStr) => ({
|
||||
input: colorStr,
|
||||
output: desaturate_color(colorStr, 1.0),
|
||||
}));
|
||||
return { operation: 'grayscale', colors: results };
|
||||
});
|
||||
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';
|
||||
const colors = generate_random_colors(request.count, vivid);
|
||||
return { colors };
|
||||
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) throw new Error('At least 2 color stops are required');
|
||||
|
||||
// For 2 stops, use the WASM gradient function
|
||||
if (request.stops.length === 2) {
|
||||
const gradient = generate_gradient(request.stops[0], request.stops[1], request.count);
|
||||
return {
|
||||
stops: request.stops,
|
||||
count: request.count,
|
||||
gradient,
|
||||
gradient: generate_gradient(request.stops[0], request.stops[1], request.count),
|
||||
};
|
||||
}
|
||||
|
||||
// For multiple stops, interpolate segments
|
||||
// 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 segmentColors = generate_gradient(
|
||||
const segColors = generate_gradient(
|
||||
request.stops[i],
|
||||
request.stops[i + 1],
|
||||
i === segments - 1 ? request.count - gradient.length : colorsPerSegment
|
||||
i === segments - 1 ? request.count - gradient.length : colorsPerSegment,
|
||||
);
|
||||
gradient.push(...segmentColors.slice(0, -1)); // Avoid duplicates
|
||||
gradient.push(...segColors.slice(0, -1));
|
||||
}
|
||||
gradient.push(request.stops[request.stops.length - 1]);
|
||||
|
||||
return {
|
||||
stops: request.stops,
|
||||
count: request.count,
|
||||
gradient,
|
||||
};
|
||||
return { stops: request.stops, count: request.count, gradient };
|
||||
});
|
||||
}
|
||||
|
||||
// System
|
||||
async getHealth(): Promise<ApiResponse<HealthData>> {
|
||||
return this.request(() => ({
|
||||
status: 'healthy',
|
||||
version: version(),
|
||||
}));
|
||||
return this.request(() => ({ status: 'healthy', version: version() }));
|
||||
}
|
||||
|
||||
async getCapabilities(): Promise<ApiResponse<CapabilitiesData>> {
|
||||
@@ -337,10 +243,7 @@ export class ColorWASMClient {
|
||||
return {
|
||||
base: request.base,
|
||||
scheme: request.scheme,
|
||||
palette: {
|
||||
primary: colors[0],
|
||||
secondary: colors.slice(1),
|
||||
},
|
||||
palette: { primary: colors[0], secondary: colors.slice(1) },
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user