TL;DR
Разбираем реализацию React-компонента для real-time dithering видео без использования WebGL. Компонент предлагает три API: JSX-компонент <Dither>, хук useDither() и низкоуровневую функцию ditherImageData(). В статье рассмотрим оптимизации для работы с requestAnimationFrame и обработкой ImageData.
Введение: зачем dithering в 2024?
Dithering — техника, рожденная в эпоху 8-битной графики, неожиданно получила вторую жизнь в современных UI. В отличие от тривиального применения CSS-фильтров, наш подход:
- Работает на чистом Canvas 2D API
- Не требует WebGL контекста
- Поддерживает кастомные палитры и параметры квантования
- Обрабатывает видео в реальном времени (30fps на mid-range устройствах)
Архитектура решения
Ядро обработки
Основная магия происходит в функции ditherImageData(), которая принимает:
type DitherOptions = {
palette: Color[]; // Массив RGB-цветов
algorithm: 'floyd-steinberg' | 'atkinson' | 'ordered';
quantizationFactor: number;
gamma: number;
};
Реализация алгоритма Флойда-Стейнберга:
function applyFloydSteinberg(
imageData: ImageData,
palette: Color[],
gamma: number
): ImageData {
const { data, width, height } = imageData;
const output = new Uint8ClampedArray(data.length);
// Гамма-коррекция перед обработкой
for (let i = 0; i < data.length; i += 4) {
const r = Math.pow(data[i] / 255, gamma) * 255;
const g = Math.pow(data[i + 1] / 255, gamma) * 255;
const b = Math.pow(data[i + 2] / 255, gamma) * 255;
// ...логика диффузии ошибок...
}
return new ImageData(output, width, height);
}
React-интеграция
Хук useDither реализует оптимизированный цикл обработки:
function useDither(sourceRef: RefObject<HTMLVideoElement>, options: DitherOptions) {
const canvasRef = useRef<HTMLCanvasElement>(null);
useLayoutEffect(() => {
let frameId: number;
const ctx = canvasRef.current?.getContext('2d');
const tick = () => {
if (ctx && sourceRef.current) {
ctx.drawImage(sourceRef.current, 0, 0);
const imageData = ctx.getImageData(...);
const dithered = ditherImageData(imageData, options);
ctx.putImageData(dithered, 0, 0);
}
frameId = requestAnimationFrame(tick);
};
tick();
return () => cancelAnimationFrame(frameId);
}, [options]);
return canvasRef;
}
Оптимизации производительности
- Троттлинг RAF: Для видео 30fps достаточно:
const targetFPS = 30;
let lastTime = 0;
const tick = (time: number) => {
if (time - lastTime > 1000 / targetFPS) {
// Логика рендера
lastTime = time;
}
requestAnimationFrame(tick);
};
- Web Workers: Вынос тяжелых вычислений в воркер:
const worker = new Worker('dither.worker.js');
worker.postMessage({ imageData: clampedArray }, [clampedArray.buffer]);
- Memoization палитр: Кешируем преобразованные цвета:
const memoizedPalette = useMemo(
() => options.palette.map(applyGamma),
[options.palette, options.gamma]
);
Практическое применение
Адаптация под разные источники
Компонент работает с любым CanvasImageSource:
<Dither source={ref}>
{(ditheredRef) => (
<video ref={ref} src="input.mp4" />
<canvas ref={ditheredRef} />
)}
</Dither>
Кастомизация эффектов
Пример конфига для “retro” стиля:
const retroConfig = {
palette: [
[0, 0, 0], // Black
[255, 0, 0], // Red
[0, 255, 0], // Green
[0, 0, 255], // Blue
[255, 255, 255] // White
],
algorithm: 'ordered',
gamma: 1.8,
quantizationFactor: 0.6
};
Заключение
Представленное решение демонстрирует, что сложные графические эффекты можно реализовать без тяжеловесных зависимостей. Ключевые преимущества:
- Минимальный bundle size (~12KB)
- Поддержка tree-shaking
- Расширяемая архитектура для новых алгоритмов
Для глубокого погружения изучите исходный код, где реализованы дополнительные алгоритмы и оптимизации.
Источник: https://dither.matialee.com