Реализация real-time dithering эффектов в React без WebGL

#react#image-processing#performance#typescript

TL;DR

Разбираем реализацию React-компонента для real-time dithering видео без использования WebGL. Компонент предлагает три API: JSX-компонент <Dither>, хук useDither() и низкоуровневую функцию ditherImageData(). В статье рассмотрим оптимизации для работы с requestAnimationFrame и обработкой ImageData.

Введение: зачем dithering в 2024?

Dithering — техника, рожденная в эпоху 8-битной графики, неожиданно получила вторую жизнь в современных UI. В отличие от тривиального применения CSS-фильтров, наш подход:

Архитектура решения

Ядро обработки

Основная магия происходит в функции 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;
}

Оптимизации производительности

  1. Троттлинг RAF: Для видео 30fps достаточно:
const targetFPS = 30;
let lastTime = 0;

const tick = (time: number) => {
  if (time - lastTime > 1000 / targetFPS) {
    // Логика рендера
    lastTime = time;
  }
  requestAnimationFrame(tick);
};
  1. Web Workers: Вынос тяжелых вычислений в воркер:
const worker = new Worker('dither.worker.js');
worker.postMessage({ imageData: clampedArray }, [clampedArray.buffer]);
  1. 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
};

Заключение

Представленное решение демонстрирует, что сложные графические эффекты можно реализовать без тяжеловесных зависимостей. Ключевые преимущества:

Для глубокого погружения изучите исходный код, где реализованы дополнительные алгоритмы и оптимизации.


Источник: https://dither.matialee.com