feat(web): правки применяются на фронте, фильтры в WASM
Новый алгоритм редактирования: обработка идёт только на клиенте. - Фильтры «Свет» считает WASM-модуль на Rust (frontend/wasm/light, 1.5 КБ): он строит тональные кривые и обрабатывает пиксели. При недоступности модуля используется JS-реализация с тем же результатом — расхождение проверяется тестом. - Просмотр рисует видимую область фото с правками на canvas поверх исходной версии, поэтому изменения видны сразу, без ожидания рендера. - Правки уходят на сервер не сразу: через десять секунд без изменений или при переключении фото. Сервер сохраняет параметры и готовит варианты сетки и полноразмерную версию в фоне, о готовности сообщает через SSE — клиент подтягивает готовую версию.
This commit is contained in:
@@ -0,0 +1,178 @@
|
||||
//! Фильтры «Свет» для веб-интерфейса: сборка кривых и попиксельная обработка.
|
||||
//!
|
||||
//! Модуль собирается в 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]
|
||||
}
|
||||
Reference in New Issue
Block a user