Новый алгоритм редактирования: обработка идёт только на клиенте. - Фильтры «Свет» считает WASM-модуль на Rust (frontend/wasm/light, 1.5 КБ): он строит тональные кривые и обрабатывает пиксели. При недоступности модуля используется JS-реализация с тем же результатом — расхождение проверяется тестом. - Просмотр рисует видимую область фото с правками на canvas поверх исходной версии, поэтому изменения видны сразу, без ожидания рендера. - Правки уходят на сервер не сразу: через десять секунд без изменений или при переключении фото. Сервер сохраняет параметры и готовит варианты сетки и полноразмерную версию в фоне, о готовности сообщает через SSE — клиент подтягивает готовую версию.
179 lines
5.4 KiB
Rust
179 lines
5.4 KiB
Rust
//! Фильтры «Свет» для веб-интерфейса: сборка кривых и попиксельная обработка.
|
|
//!
|
|
//! Модуль собирается в WebAssembly (`wasm32-unknown-unknown`) и загружается
|
|
//! браузером, чтобы предпросмотр правок в просмотрщике не ждал серверного
|
|
//! рендера. Математика совпадает с серверной (backend/internal/photos/edit.go).
|
|
#![no_std]
|
|
|
|
use core::panic::PanicInfo;
|
|
|
|
#[panic_handler]
|
|
fn panic(_info: &PanicInfo) -> ! {
|
|
loop {}
|
|
}
|
|
|
|
/// Каналы: 0 — общая кривая, 1 — красный, 2 — зелёный, 3 — синий.
|
|
const CHANNELS: usize = 4;
|
|
|
|
static mut LUTS: [[u8; 256]; CHANNELS] = [[0; 256]; CHANNELS];
|
|
|
|
#[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
|
|
}
|
|
}
|
|
|
|
/// Строит таблицу соответствия 0..255 по точкам кривой.
|
|
///
|
|
/// `points` — пары (x, y) в диапазоне 0..1, отсортированные по x.
|
|
/// Точки вне диапазона обрезаются, дубликаты по x пропускаются.
|
|
#[no_mangle]
|
|
pub extern "C" fn build_lut(points: *const f32, count: u32, channel: u32) {
|
|
unsafe {
|
|
let luts = &mut *core::ptr::addr_of_mut!(LUTS);
|
|
let ch = (channel as usize).min(CHANNELS - 1);
|
|
let lut = &mut luts[ch];
|
|
if count < 2 {
|
|
for (i, slot) in lut.iter_mut().enumerate() {
|
|
*slot = i as u8;
|
|
}
|
|
return;
|
|
}
|
|
let n = count as usize;
|
|
let mut seg = 0usize;
|
|
for v in 0..256 {
|
|
let x = v as f32 / 255.0;
|
|
while seg + 1 < n && x > *points.add((seg + 1) * 2) {
|
|
seg += 1;
|
|
}
|
|
let x0 = *points.add(seg * 2);
|
|
let y0 = *points.add(seg * 2 + 1);
|
|
let x1 = *points.add((seg + 1) * 2);
|
|
let y1 = *points.add((seg + 1) * 2 + 1);
|
|
let y = if x <= x0 {
|
|
y0
|
|
} else if x >= x1 {
|
|
y1
|
|
} else if x1 - x0 < 1e-6 {
|
|
y1
|
|
} else {
|
|
let t = (x - x0) / (x1 - x0);
|
|
y0 + t * (y1 - y0)
|
|
};
|
|
lut[v] = (clamp01(y) * 255.0 + 0.5) as u8;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Применяет правки «Свет» к пикселям RGBA в памяти.
|
|
///
|
|
/// Коэффициенты вычисляет вызывающий код: модуль не использует transcendental
|
|
/// функции, поэтому работает без математической библиотеки.
|
|
#[no_mangle]
|
|
#[allow(clippy::too_many_arguments)]
|
|
pub extern "C" fn apply(
|
|
data: *mut u8,
|
|
len: u32,
|
|
exposure_mul: f32,
|
|
contrast_k: f32,
|
|
highlights: f32,
|
|
shadows: f32,
|
|
whites: f32,
|
|
blacks: f32,
|
|
) {
|
|
unsafe {
|
|
let luts = &*core::ptr::addr_of!(LUTS);
|
|
let master = &luts[0];
|
|
let red = &luts[1];
|
|
let green = &luts[2];
|
|
let blue = &luts[3];
|
|
let buf = 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; 256],
|
|
channel: &[u8; 256],
|
|
) -> 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]
|
|
}
|