feat(web): правки применяются на фронте, фильтры в WASM
Новый алгоритм редактирования: обработка идёт только на клиенте. - Фильтры «Свет» считает WASM-модуль на Rust (frontend/wasm/light, 1.5 КБ): он строит тональные кривые и обрабатывает пиксели. При недоступности модуля используется JS-реализация с тем же результатом — расхождение проверяется тестом. - Просмотр рисует видимую область фото с правками на canvas поверх исходной версии, поэтому изменения видны сразу, без ожидания рендера. - Правки уходят на сервер не сразу: через десять секунд без изменений или при переключении фото. Сервер сохраняет параметры и готовит варианты сетки и полноразмерную версию в фоне, о готовности сообщает через SSE — клиент подтягивает готовую версию.
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { beforeAll, describe, expect, it } from 'vitest';
|
||||
import { applyLightToPixels, emptyEditParams, type LightParams } from './edit';
|
||||
|
||||
interface LightExports {
|
||||
memory: WebAssembly.Memory;
|
||||
build_lut(points: number, count: number, channel: number): void;
|
||||
apply(
|
||||
data: number,
|
||||
len: number,
|
||||
exposureMul: number,
|
||||
contrastK: number,
|
||||
highlights: number,
|
||||
shadows: number,
|
||||
whites: number,
|
||||
blacks: number
|
||||
): void;
|
||||
}
|
||||
|
||||
let wasm: LightExports;
|
||||
|
||||
beforeAll(async () => {
|
||||
const bytes = await readFile(new URL('../../static/light.wasm', import.meta.url));
|
||||
const { instance } = await WebAssembly.instantiate(bytes, {});
|
||||
wasm = instance.exports as unknown as LightExports;
|
||||
});
|
||||
|
||||
/** Применяет правки модулем WASM к копии пикселей. */
|
||||
function applyWasm(pixels: Uint8ClampedArray, light: LightParams): Uint8ClampedArray {
|
||||
const points = light.curve?.rgb ?? [];
|
||||
const floats = new Float32Array(points.length * 2);
|
||||
points.forEach((p, i) => {
|
||||
floats[i * 2] = p.x;
|
||||
floats[i * 2 + 1] = p.y;
|
||||
});
|
||||
const mem = wasm.memory;
|
||||
const need = Math.ceil((4096 + pixels.length) / 65536);
|
||||
if (need > mem.buffer.byteLength / 65536) mem.grow(need - mem.buffer.byteLength / 65536);
|
||||
new Float32Array(mem.buffer, 0, floats.length).set(floats);
|
||||
wasm.build_lut(0, points.length, 0);
|
||||
wasm.build_lut(0, 0, 1);
|
||||
wasm.build_lut(0, 0, 2);
|
||||
wasm.build_lut(0, 0, 3);
|
||||
new Uint8ClampedArray(mem.buffer, 4096, pixels.length).set(pixels);
|
||||
wasm.apply(
|
||||
4096,
|
||||
pixels.length,
|
||||
Math.pow(2, light.exposure),
|
||||
1 + light.contrast / 100,
|
||||
(light.highlights / 100) * 80,
|
||||
(light.shadows / 100) * 80,
|
||||
(light.whites / 100) * 80,
|
||||
(light.blacks / 100) * 80
|
||||
);
|
||||
return new Uint8ClampedArray(mem.buffer, 4096, pixels.length).slice();
|
||||
}
|
||||
|
||||
describe('WASM-модуль фильтров', () => {
|
||||
it('совпадает с JS-реализацией', () => {
|
||||
const light: LightParams = {
|
||||
...emptyEditParams().light,
|
||||
exposure: 0.8,
|
||||
contrast: 25,
|
||||
highlights: -30,
|
||||
shadows: 40,
|
||||
whites: 10,
|
||||
blacks: -15,
|
||||
curve: { rgb: [{ x: 0, y: 0 }, { x: 0.5, y: 0.4 }, { x: 1, y: 1 }] }
|
||||
};
|
||||
const source = new Uint8ClampedArray(4 * 64);
|
||||
for (let i = 0; i < source.length; i += 4) {
|
||||
source[i] = (i * 7) % 256;
|
||||
source[i + 1] = (i * 13) % 256;
|
||||
source[i + 2] = (i * 29) % 256;
|
||||
source[i + 3] = 255;
|
||||
}
|
||||
const expected = source.slice();
|
||||
applyLightToPixels(expected, light);
|
||||
const actual = applyWasm(source.slice(), light);
|
||||
expect(Array.from(actual)).toEqual(Array.from(expected));
|
||||
});
|
||||
|
||||
it('не меняет прозрачные пиксели', () => {
|
||||
const light: LightParams = { ...emptyEditParams().light, exposure: 2 };
|
||||
const pixels = new Uint8ClampedArray([10, 20, 30, 0]);
|
||||
const out = applyWasm(pixels, light);
|
||||
expect(Array.from(out)).toEqual([10, 20, 30, 0]);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user