Files
ds-galo/frontend/wasm/light/src/lib.rs
T
iu 520886a11d feat(web): правки применяются на фронте, фильтры в WASM
Новый алгоритм редактирования: обработка идёт только на клиенте.

- Фильтры «Свет» считает WASM-модуль на Rust (frontend/wasm/light, 1.5 КБ):
  он строит тональные кривые и обрабатывает пиксели. При недоступности модуля
  используется JS-реализация с тем же результатом — расхождение проверяется
  тестом.
- Просмотр рисует видимую область фото с правками на canvas поверх
  исходной версии, поэтому изменения видны сразу, без ожидания рендера.
- Правки уходят на сервер не сразу: через десять секунд без изменений или при
  переключении фото. Сервер сохраняет параметры и готовит варианты сетки и
  полноразмерную версию в фоне, о готовности сообщает через SSE — клиент
  подтягивает готовую версию.
2026-10-21 00:06:46 +03:00

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]
}