114 lines
3.7 KiB
TypeScript
114 lines
3.7 KiB
TypeScript
/**
|
|
* Chroma Key Utilities
|
|
*
|
|
* Core pixel-processing algorithm for removing solid-color backgrounds
|
|
* from images and video frames. Operates on Canvas 2D ImageData.
|
|
*/
|
|
|
|
export interface ChromaKeyParams {
|
|
/** RGB tuple of the key color to remove */
|
|
keyColor: [number, number, number];
|
|
/** Tolerance in color distance (0-255 range, mapped from 0-100%) */
|
|
tolerance: number;
|
|
/** Edge softness in color distance units */
|
|
softness: number;
|
|
}
|
|
|
|
/** Preset colors for quick selection — optimized for common use cases */
|
|
export const CHROMA_KEY_PRESETS = [
|
|
{ color: '#ffffff', label: 'Blanco', icon: '⬜' },
|
|
{ color: '#000000', label: 'Negro', icon: '⬛' },
|
|
{ color: '#00ff00', label: 'Verde', icon: '🟩' },
|
|
{ color: '#0000ff', label: 'Azul', icon: '🟦' },
|
|
] as const;
|
|
|
|
/**
|
|
* Parse a hex color string to an RGB tuple.
|
|
* Supports #RGB, #RRGGBB, and plain RRGGBB formats.
|
|
*/
|
|
export function hexToRgb(hex: string): [number, number, number] {
|
|
let clean = hex.replace('#', '');
|
|
if (clean.length === 3) {
|
|
clean = clean[0] + clean[0] + clean[1] + clean[1] + clean[2] + clean[2];
|
|
}
|
|
const num = parseInt(clean, 16);
|
|
return [
|
|
(num >> 16) & 0xff,
|
|
(num >> 8) & 0xff,
|
|
num & 0xff,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Convert RGB to a hex color string (#RRGGBB).
|
|
*/
|
|
export function rgbToHex(r: number, g: number, b: number): string {
|
|
return '#' + [r, g, b].map(c => c.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
/**
|
|
* Apply chroma key to canvas ImageData in-place.
|
|
*
|
|
* Uses Euclidean distance in RGB color space to determine how close
|
|
* each pixel is to the key color. Pixels within tolerance become fully
|
|
* transparent; pixels in the softness zone get partial transparency for
|
|
* smooth edges.
|
|
*
|
|
* @param imageData - The ImageData to process (modified in-place)
|
|
* @param params - Chroma key configuration
|
|
*/
|
|
export function applyChromaKey(
|
|
imageData: ImageData,
|
|
params: ChromaKeyParams
|
|
): void {
|
|
const { keyColor, tolerance, softness } = params;
|
|
const data = imageData.data;
|
|
const [kr, kg, kb] = keyColor;
|
|
|
|
for (let i = 0; i < data.length; i += 4) {
|
|
const r = data[i];
|
|
const g = data[i + 1];
|
|
const b = data[i + 2];
|
|
|
|
// Euclidean distance in RGB space
|
|
const dr = r - kr;
|
|
const dg = g - kg;
|
|
const db = b - kb;
|
|
const distance = Math.sqrt(dr * dr + dg * dg + db * db);
|
|
|
|
if (distance < tolerance) {
|
|
// Fully transparent — within the key color range
|
|
data[i + 3] = 0;
|
|
} else if (softness > 0 && distance < tolerance + softness) {
|
|
// Partial transparency — smooth edge transition
|
|
const alpha = Math.round(255 * ((distance - tolerance) / softness));
|
|
data[i + 3] = Math.min(data[i + 3], alpha);
|
|
|
|
// Spill suppression: reduce the key color influence on edge pixels
|
|
// This prevents a colored "halo" around the subject
|
|
const spillFactor = 1 - ((tolerance + softness - distance) / softness) * 0.5;
|
|
data[i] = Math.round(r + (r - kr) * (1 - spillFactor));
|
|
data[i + 1] = Math.round(g + (g - kg) * (1 - spillFactor));
|
|
data[i + 2] = Math.round(b + (b - kb) * (1 - spillFactor));
|
|
|
|
// Clamp values
|
|
data[i] = Math.max(0, Math.min(255, data[i]));
|
|
data[i + 1] = Math.max(0, Math.min(255, data[i + 1]));
|
|
data[i + 2] = Math.max(0, Math.min(255, data[i + 2]));
|
|
}
|
|
// else: pixel is outside the range, keep as-is
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Map user-facing percentage values (0-100) to internal distance values.
|
|
* The max RGB distance is ~441 (sqrt(255² + 255² + 255²)).
|
|
*/
|
|
export function mapToleranceToDistance(tolerancePercent: number): number {
|
|
return (tolerancePercent / 100) * 441;
|
|
}
|
|
|
|
export function mapSoftnessToDistance(softnessPercent: number): number {
|
|
return (softnessPercent / 100) * 150;
|
|
}
|