diff options
Diffstat (limited to 'src/client/views/ExtractColors.ts')
-rw-r--r-- | src/client/views/ExtractColors.ts | 168 |
1 files changed, 168 insertions, 0 deletions
diff --git a/src/client/views/ExtractColors.ts b/src/client/views/ExtractColors.ts new file mode 100644 index 000000000..f6928c52a --- /dev/null +++ b/src/client/views/ExtractColors.ts @@ -0,0 +1,168 @@ +import { extractColors } from 'extract-colors'; +import { FinalColor } from 'extract-colors/lib/types/Color'; + +// Manages image color extraction +export class ExtractColors { + // loads all images into img elements + static loadImages = async (imageFiles: File[]): Promise<HTMLImageElement[]> => { + try { + const imageElements = await Promise.all(imageFiles.map(file => this.loadImage(file))); + return imageElements; + } catch (error) { + console.error(error); + return []; + } + }; + + // loads a single img into an img element + static loadImage = (file: File): Promise<HTMLImageElement> => { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = () => resolve(img); + img.onerror = error => reject(error); + + const url = URL.createObjectURL(file); + img.src = url; + }); + }; + + // loads all images into img elements + static loadImagesUrl = async (imageUrls: string[]): Promise<HTMLImageElement[]> => { + try { + const imageElements = await Promise.all(imageUrls.map(url => this.loadImageUrl(url))); + return imageElements; + } catch (error) { + console.error(error); + return []; + } + }; + + // loads a single img into an img element + static loadImageUrl = (url: string): Promise<HTMLImageElement> => { + return new Promise((resolve, reject) => { + const img = new Image(); + + img.onload = () => resolve(img); + img.onerror = error => reject(error); + + img.src = url; + }); + }; + + // extracts a list of collors from an img element + static getImgColors = async (img: HTMLImageElement) => { + const colors = await extractColors(img, { distance: 0.35 }); + return colors; + }; + + static simpleSort = (colors: FinalColor[]): FinalColor[] => { + colors.sort((a, b) => { + if (a.hue !== b.hue) { + return b.hue - a.hue; + } else { + return b.saturation - a.saturation; + } + }); + return colors; + }; + + static sortColors(colors: FinalColor[]): FinalColor[] { + // Convert color from RGB to CIELAB format + const convertToLab = (color: FinalColor): number[] => { + const r = color.red / 255; + const g = color.green / 255; + const b = color.blue / 255; + + const x = r * 0.4124564 + g * 0.3575761 + b * 0.1804375; + const y = r * 0.2126729 + g * 0.7151522 + b * 0.072175; + const z = r * 0.0193339 + g * 0.119192 + b * 0.9503041; + + const pivot = 0.008856; + const factor = 903.3; + + const fx = x > pivot ? Math.cbrt(x) : (factor * x + 16) / 116; + const fy = y > pivot ? Math.cbrt(y) : (factor * y + 16) / 116; + const fz = z > pivot ? Math.cbrt(z) : (factor * z + 16) / 116; + + const L = 116 * fy - 16; + const a = (fx - fy) * 500; + const b1 = (fy - fz) * 200; + + return [L, a, b1]; + }; + + // Sort colors using CIELAB distance for smooth transitions + colors.sort((colorA, colorB) => { + const labA = convertToLab(colorA); + const labB = convertToLab(colorB); + + // Calculate Euclidean distance in CIELAB space + const distanceA = Math.sqrt(Math.pow(labA[0] - labB[0], 2) + Math.pow(labA[1] - labB[1], 2) + Math.pow(labA[2] - labB[2], 2)); + + const distanceB = Math.sqrt(Math.pow(labB[0] - labA[0], 2) + Math.pow(labB[1] - labA[1], 2) + Math.pow(labB[2] - labA[2], 2)); + + return distanceA - distanceB; // Sort by CIELAB distance + }); + + return colors; + } + + static hexToFinalColor = (hex: string): FinalColor => { + const rgb = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); + + if (!rgb) { + throw new Error('Invalid hex color format.'); + } + + const red = parseInt(rgb[1], 16); + const green = parseInt(rgb[2], 16); + const blue = parseInt(rgb[3], 16); + + const max = Math.max(red, green, blue); + const min = Math.min(red, green, blue); + const area = max - min; + const intensity = (max + min) / 2; + + let hue = 0; + let saturation = 0; + let lightness = intensity; + + if (area !== 0) { + saturation = area / (1 - Math.abs(2 * intensity - 1)); + if (max === red) { + hue = (60 * ((green - blue) / area) + 360) % 360; + } else if (max === green) { + hue = (60 * ((blue - red) / area) + 120) % 360; + } else { + hue = (60 * ((red - green) / area) + 240) % 360; + } + } + + return { + hex, + red, + green, + blue, + area, + hue, + saturation, + lightness, + intensity, + }; + }; +} + +// for reference + +// type FinalColor = { +// hex: string; +// red: number; +// green: number; +// blue: number; +// area: number; +// hue: number; +// saturation: number; +// lightness: number; +// intensity: number; +// } |