//! Фильтры «Свет» для веб-интерфейса: попиксельная обработка кадра. //! //! Модуль собирается в WebAssembly (`wasm32-unknown-unknown`) и загружается //! браузером, чтобы предпросмотр правок в просмотрщике не ждал серверного //! рендера. //! //! Таблицы тональных кривых считает вызывающий код (JavaScript) и передаёт //! сюда готовыми: модуль не хранит состояние и не использует transcendental //! функции, поэтому работает без математической библиотеки. #![no_std] use core::panic::PanicInfo; #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } /// Количество каналов: 0 — общая кривая, 1 — красный, 2 — зелёный, 3 — синий. const CHANNELS: usize = 4; /// Длина таблицы соответствия одного канала. const LUT_LEN: usize = 256; #[inline(always)] fn clamp01(v: f32) -> f32 { if v < 0.0 { 0.0 } else if v > 1.0 { 1.0 } else { v } } #[inline(always)] fn clampi(v: i32, lo: i32, hi: i32) -> i32 { if v < lo { lo } else if v > hi { hi } else { v } } /// Применяет правки «Свет» к пикселям RGBA в памяти. /// /// `luts` — четыре таблицы соответствия по 256 байт (общая, красный, зелёный, /// синий), подготовленные вызывающим кодом. #[no_mangle] #[allow(clippy::too_many_arguments)] pub extern "C" fn apply( data: *mut u8, len: u32, luts: *const u8, exposure_mul: f32, contrast_k: f32, highlights: f32, shadows: f32, whites: f32, blacks: f32, ) { let tables = unsafe { core::slice::from_raw_parts(luts, CHANNELS * LUT_LEN) }; let master = &tables[0..LUT_LEN]; let red = &tables[LUT_LEN..2 * LUT_LEN]; let green = &tables[2 * LUT_LEN..3 * LUT_LEN]; let blue = &tables[3 * LUT_LEN..4 * LUT_LEN]; let buf = unsafe { core::slice::from_raw_parts_mut(data, len as usize) }; let mut i = 0usize; while i + 3 < buf.len() { if buf[i + 3] != 0 { buf[i] = adjust(buf[i] as f32, exposure_mul, contrast_k, highlights, shadows, whites, blacks, master, red); buf[i + 1] = adjust(buf[i + 1] as f32, exposure_mul, contrast_k, highlights, shadows, whites, blacks, master, green); buf[i + 2] = adjust(buf[i + 2] as f32, exposure_mul, contrast_k, highlights, shadows, whites, blacks, master, blue); } i += 4; } } /// Обрабатывает один канал: экспозиция, контраст, света, тени, белые, чёрные /// и обе тональные кривые (общая, затем канальная). #[inline(always)] #[allow(clippy::too_many_arguments)] fn adjust( value: f32, exposure_mul: f32, contrast_k: f32, highlights: f32, shadows: f32, whites: f32, blacks: f32, master: &[u8], channel: &[u8], ) -> u8 { let mut v = value * exposure_mul; v = 128.0 + (v - 128.0) * contrast_k; let mut w = clamp01((v - 128.0) / 127.0); v += highlights * w * w; w = clamp01((128.0 - v) / 128.0); v += shadows * w * w; w = clamp01((v - 192.0) / 63.0); v += whites * w; w = clamp01((64.0 - v) / 64.0); v += blacks * w; let idx = clampi((v + 0.5) as i32, 0, 255) as usize; let v = master[idx] as f32; let idx = clampi((v + 0.5) as i32, 0, 255) as usize; channel[idx] } /// Количество f64-параметров цвета. const COLOR_PARAM_COUNT: usize = 42; /// Центры полос цветового микшера в градусах. const BAND_CENTERS: [f64; 8] = [0.0, 30.0, 60.0, 120.0, 180.0, 240.0, 280.0, 320.0]; #[inline(always)] fn clampf(v: f64, lo: f64, hi: f64) -> f64 { if v < lo { lo } else if v > hi { hi } else { v } } #[inline(always)] fn clamp01d(v: f64) -> f64 { clampf(v, 0.0, 1.0) } #[inline(always)] fn to_u8(v: f64) -> u8 { (clampf(v, 0.0, 255.0) + 0.5) as u8 } /// RGB (0..255) -> HSL: оттенок в градусах, насыщенность и яркость в 0..1. fn rgb_to_hsl(r: f64, g: f64, b: f64) -> (f64, f64, f64) { let r = r / 255.0; let g = g / 255.0; let b = b / 255.0; let max = if r > g { if r > b { r } else { b } } else if g > b { g } else { b }; let min = if r < g { if r < b { r } else { b } } else if g < b { g } else { b }; let l = (max + min) / 2.0; if max == min { return (0.0, 0.0, l); } let d = max - min; let s = if l > 0.5 { d / (2.0 - max - min) } else { d / (max + min) }; let mut h; if max == r { h = (g - b) / d; if g < b { h += 6.0; } } else if max == g { h = (b - r) / d + 2.0; } else { h = (r - g) / d + 4.0; } (h * 60.0, s, l) } fn hue_to_rgb(p: f64, q: f64, mut t: f64) -> f64 { if t < 0.0 { t += 1.0; } if t > 1.0 { t -= 1.0; } if t < 1.0 / 6.0 { p + (q - p) * 6.0 * t } else if t < 1.0 / 2.0 { q } else if t < 2.0 / 3.0 { p + (q - p) * (2.0 / 3.0 - t) * 6.0 } else { p } } /// HSL -> RGB (0..255). fn hsl_to_rgb(h: f64, s: f64, l: f64) -> (f64, f64, f64) { if s == 0.0 { let v = l * 255.0; return (v, v, v); } let q = if l < 0.5 { l * (1.0 + s) } else { l + s - l * s }; let p = 2.0 * l - q; let hk = h / 360.0; ( hue_to_rgb(p, q, hk + 1.0 / 3.0) * 255.0, hue_to_rgb(p, q, hk) * 255.0, hue_to_rgb(p, q, hk - 1.0 / 3.0) * 255.0, ) } fn band_weight(h: f64, center: f64) -> f64 { let mut d = h - center; if d < 0.0 { d = -d; } if d > 180.0 { d = 360.0 - d; } clamp01d(1.0 - d / 60.0) } /// Применяет правки «Цвет» к пикселям RGBA в памяти. /// /// `params` — блок из `COLOR_PARAM_COUNT` значений f64, подготовленный /// вызывающим кодом. Порядок операций повторяет серверный рендер и JS. #[no_mangle] pub extern "C" fn apply_color(data: *mut u8, len: u32, params: *const f64) { let p = unsafe { core::slice::from_raw_parts(params, COLOR_PARAM_COUNT) }; let temperature = p[0]; let tint = p[1]; let vibrance = p[2]; let saturation = p[3]; let blending = p[37]; let balance = p[38]; let blend = blending; // Микшер часто пуст (правят только колёса) — тогда не тратим на него время. let mut has_mixer = false; let mut band = 0usize; while band < 8 { let base = 4 + band * 3; if p[base] != 0.0 || p[base + 1] != 0.0 || p[base + 2] != 0.0 { has_mixer = true; break; } band += 1; } let mut tints = [(0.0f64, 0.0f64, 0.0f64); 3]; let mut w = 0usize; while w < 3 { tints[w] = hsl_to_rgb(p[28 + w * 2], 1.0, 0.5); w += 1; } let buf = unsafe { core::slice::from_raw_parts_mut(data, len as usize) }; let mut i = 0usize; while i + 3 < buf.len() { let alpha = unsafe { *buf.get_unchecked(i + 3) }; if alpha != 0 { let mut r = unsafe { *buf.get_unchecked(i) } as f64; let mut g = unsafe { *buf.get_unchecked(i + 1) } as f64; let mut b = unsafe { *buf.get_unchecked(i + 2) } as f64; // Баланс белого. r *= 1.0 + 0.2 * temperature + 0.1 * tint; g *= 1.0 - 0.2 * tint; b *= 1.0 - 0.2 * temperature + 0.1 * tint; r = clampf(r, 0.0, 255.0); g = clampf(g, 0.0, 255.0); b = clampf(b, 0.0, 255.0); // Быстрый путь: только колёса/яркость, без микшера и насыщенности. // Тогда HSL-переход не нужен — lightness считается напрямую. let fast = !has_mixer && saturation == 0.0 && vibrance == 0.0; let weights: [f64; 3]; let mut rr: f64; let mut gg: f64; let mut bb: f64; if fast { let max = if r > g { if r > b { r } else { b } } else if g > b { g } else { b }; let min = if r < g { if r < b { r } else { b } } else if g < b { g } else { b }; let l = (max + min) / 510.0; let lb = clamp01d(l + balance * 0.5); let shadow_w = { let t = (1.0 - lb) * (1.0 - lb); t * t }; let highlight_w = { let t = lb * lb; t * t }; weights = [shadow_w, 1.0 - shadow_w - highlight_w, highlight_w]; let mut target_l = l; let mut wheel = 0usize; while wheel < 3 { target_l += p[34 + wheel] * 0.2 * weights[wheel]; wheel += 1; } target_l = clamp01d(target_l); let delta = (target_l - l) * 255.0; rr = r + delta; gg = g + delta; bb = b + delta; } else { // RGB -> HSL. let (mut h, mut s, mut l) = rgb_to_hsl(r, g, b); // Насыщенность и вибрация. if saturation != 0.0 || vibrance != 0.0 { s = clamp01d(s * (1.0 + saturation + vibrance * (1.0 - s))); } // Цветовой микшер. if has_mixer { let mut band = 0usize; while band < 8 { let base = 4 + band * 3; let weight = band_weight(h, BAND_CENTERS[band]); if weight != 0.0 { h += p[base] * 30.0 * weight; s = clamp01d(s * (1.0 + p[base + 1] * weight)); l = clamp01d(l + p[base + 2] * 0.2 * weight); } band += 1; } } h = h % 360.0; if h < 0.0 { h += 360.0; } let lb = clamp01d(l + balance * 0.5); let shadow_w = { let t = (1.0 - lb) * (1.0 - lb); t * t }; let highlight_w = { let t = lb * lb; t * t }; weights = [shadow_w, 1.0 - shadow_w - highlight_w, highlight_w]; let mut wheel = 0usize; while wheel < 3 { l = clamp01d(l + p[34 + wheel] * 0.2 * weights[wheel]); wheel += 1; } let (r2, g2, b2) = hsl_to_rgb(h, s, l); rr = r2; gg = g2; bb = b2; } // Точечный цвет: три колеса по зонам яркости. let mut wheel = 0usize; while wheel < 3 { let strength = p[28 + wheel * 2 + 1] * 0.5 * blend * weights[wheel]; if strength != 0.0 { let t = tints[wheel]; rr = rr * (1.0 - strength) + t.0 * strength; gg = gg * (1.0 - strength) + t.1 * strength; bb = bb * (1.0 - strength) + t.2 * strength; } wheel += 1; } // Цветокоррекция. rr *= 1.0 + p[39] * 0.5; gg *= 1.0 + p[40] * 0.5; bb *= 1.0 + p[41] * 0.5; unsafe { *buf.get_unchecked_mut(i) = to_u8(rr); *buf.get_unchecked_mut(i + 1) = to_u8(gg); *buf.get_unchecked_mut(i + 2) = to_u8(bb); } } i += 4; } }