feat(web): правки применяются на фронте, фильтры в WASM
Новый алгоритм редактирования: обработка идёт только на клиенте. - Фильтры «Свет» считает WASM-модуль на Rust (frontend/wasm/light, 1.5 КБ): он строит тональные кривые и обрабатывает пиксели. При недоступности модуля используется JS-реализация с тем же результатом — расхождение проверяется тестом. - Просмотр рисует видимую область фото с правками на canvas поверх исходной версии, поэтому изменения видны сразу, без ожидания рендера. - Правки уходят на сервер не сразу: через десять секунд без изменений или при переключении фото. Сервер сохраняет параметры и готовит варианты сетки и полноразмерную версию в фоне, о готовности сообщает через SSE — клиент подтягивает готовую версию.
This commit is contained in:
@@ -183,3 +183,80 @@ export function photoPreviewUrl(photo: Photo): string | undefined {
|
||||
export function photoFullUrl(photo: Photo): string | undefined {
|
||||
return photo.edit_url ?? photo.original_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Строит 256-элементную таблицу соответствия по точкам кривой. Математика
|
||||
* повторяет серверную (backend/internal/photos/edit.go), поэтому предпросмотр
|
||||
* в браузере совпадает с результатом рендера.
|
||||
*/
|
||||
export function buildCurveLUT(points?: CurvePoint[]): Uint8Array {
|
||||
const lut = new Uint8Array(256);
|
||||
const pts = normalizeCurve(points);
|
||||
if (!pts || pts.length < 2) {
|
||||
for (let i = 0; i < 256; i++) lut[i] = i;
|
||||
return lut;
|
||||
}
|
||||
for (let i = 0; i < 256; i++) {
|
||||
lut[i] = Math.round(clamp01(curveEval(pts, i / 255)) * 255);
|
||||
}
|
||||
return lut;
|
||||
}
|
||||
|
||||
function curveEval(points: CurvePoint[], x: number): number {
|
||||
if (x <= points[0].x) return points[0].y;
|
||||
const last = points[points.length - 1];
|
||||
if (x >= last.x) return last.y;
|
||||
for (let i = 1; i < points.length; i++) {
|
||||
if (x > points[i].x) continue;
|
||||
const prev = points[i - 1];
|
||||
const cur = points[i];
|
||||
if (cur.x - prev.x < 1e-9) return cur.y;
|
||||
const t = (x - prev.x) / (cur.x - prev.x);
|
||||
return prev.y + t * (cur.y - prev.y);
|
||||
}
|
||||
return last.y;
|
||||
}
|
||||
|
||||
function adjustChannel(
|
||||
value: number,
|
||||
light: LightParams,
|
||||
master: Uint8Array,
|
||||
channel: Uint8Array
|
||||
): number {
|
||||
// Экспозиция (стопы).
|
||||
let v = value * Math.pow(2, light.exposure);
|
||||
// Контраст вокруг средней точки.
|
||||
const k = 1 + light.contrast / 100;
|
||||
v = 128 + (v - 128) * k;
|
||||
// Света: верхняя часть диапазона.
|
||||
let w = Math.pow(clamp01((v - 128) / 127), 2);
|
||||
v += (light.highlights / 100) * 80 * w;
|
||||
// Тени: нижняя часть диапазона.
|
||||
w = Math.pow(clamp01((128 - v) / 128), 2);
|
||||
v += (light.shadows / 100) * 80 * w;
|
||||
// Белые: самый верх диапазона.
|
||||
w = clamp01((v - 192) / 63);
|
||||
v += (light.whites / 100) * 80 * w;
|
||||
// Чёрные: самый низ диапазона.
|
||||
w = clamp01((64 - v) / 64);
|
||||
v += (light.blacks / 100) * 80 * w;
|
||||
// Тоновая кривая: общая, затем канальная.
|
||||
let idx = Math.min(255, Math.max(0, Math.round(v)));
|
||||
v = master[idx];
|
||||
idx = Math.min(255, Math.max(0, Math.round(v)));
|
||||
return channel[idx];
|
||||
}
|
||||
|
||||
/** Применяет правки «Свет» к пикселям изображения (в том же массиве). */
|
||||
export function applyLightToPixels(data: Uint8ClampedArray, light: LightParams): void {
|
||||
const master = buildCurveLUT(light.curve?.rgb);
|
||||
const red = buildCurveLUT(light.curve?.red);
|
||||
const green = buildCurveLUT(light.curve?.green);
|
||||
const blue = buildCurveLUT(light.curve?.blue);
|
||||
for (let i = 0; i + 3 < data.length; i += 4) {
|
||||
if (data[i + 3] === 0) continue;
|
||||
data[i] = adjustChannel(data[i], light, master, red);
|
||||
data[i + 1] = adjustChannel(data[i + 1], light, master, green);
|
||||
data[i + 2] = adjustChannel(data[i + 2], light, master, blue);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user