Initial commit — Bradly branding editor platform

This commit is contained in:
2026-06-02 03:27:03 -05:00
commit b135a70cc7
180 changed files with 43160 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
/**
* 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;
}