feat(photos): инструмент «Цвет» с WebGL-превью и пипеткой
- неразрушающие правки цвета: баланс белого, вибрация/насыщенность, цветовой микшер, колористика и поканальная коррекция - серверный рендер цвета в Go и паритетная реализация в WASM - живое превью на WebGL2 (кадр считает GPU), CPU/WASM-фолбэк - пипетка баланса белого по нейтральной точке - новые контролы: слайдеры с градиентами, цветовые колёса, двойной клик по контролу сбрасывает значение - тесты: параметры, рендер, паритет JS/WASM, e2e сохранения и сброса
This commit is contained in:
@@ -108,3 +108,272 @@ fn adjust(
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user