This guide explains the technical details of the react-bitmap engine, outlining the mathematical algorithms for dithering, GPU optimizations, and strategies to maintain 60 FPS in production WebGL applications.
Dithering mimics color depth by distributing quantization errors. react-bitmap implements ordered dithering using mathematical Bayer matrices and high-frequency wave patterns directly on the GPU.
Ordered dithering compares the brightness of a downsampled pixel with a threshold from a recurring matrix grid.
- Bayer 2x2 Matrix: A simple 4-step grid. $$\mathbf{M_2} = \frac{1}{4} \begin{bmatrix} 0 & 2 \ 3 & 1 \end{bmatrix}$$
- Bayer 4x4 Matrix: A 16-step grid, balancing grain and structure. $$\mathbf{M_4} = \frac{1}{16} \begin{bmatrix} 0 & 8 & 2 & 10 \ 12 & 4 & 14 & 6 \ 3 & 11 & 1 & 9 \ 15 & 7 & 13 & 5 \end{bmatrix}$$
- Bayer 8x8 Matrix: A 64-step grid, offering high-fidelity vintage rendering.
In the shader, the dither threshold value
Halftone dithering simulates print screening by generating a grid of circular dots. In the GLSL fragment shader, this is achieved by computing a high-frequency grid using rotating vectors and sine waves:
Standard CPU-based dithering (like Canvas2D getImageData loop) is extremely slow because it requires reading pixel data back from the GPU to the CPU, running Javascript loops on millions of pixels, and sending it back. This can clog the main thread and drop frame rates to 5-10 FPS for HD videos.
react-bitmap solves this with the following optimizations:
Using WebGL 2.0 (GLSL ES 3.00), we define Bayer matrices as uniform lookups or localized function arrays:
float bayer8[64] = float[](
0.0, 48.0, 12.0, 60.0, ...
);This is compiled directly into GPU instructions, performing matrix index lookup in
To map a dithered RGB color to the closest color in a custom palette, the shader computes the 3D distance between vectors in the RGB color space:
distance(vec3, vec3) function enables hardware-level vector optimization. The shader iterates up to 32 colors in a single pass to select the pixel color.
Re-initializing canvas widths and heights on each frame triggers GPU buffer re-allocation, leading to frame drops. react-bitmap monitors dimensions and only updates the canvas viewport when the source resolution changes.
To render videos smoothly without audio-visual desync or visual stuttering, we implement two primary loops:
graph TD
A[Start Video / Camera Stream] --> B{Is requestVideoFrameCallback supported?}
B -- Yes --> C[Register RVFC Callback]
B -- No --> D[Fallback to requestAnimationFrame Loop]
C --> E[Upload Video Frame to WebGL Texture]
D --> E
E --> F[Render Dithered Frame to Screen Canvas]
F --> G{Is video playing?}
G -- Yes --> B
G -- No --> H[Pause Loop]
If available, BitVideo registers an RVFC callback on the HTML5 <video> element. This instructs the browser to notify the Javascript thread only when a new video frame is ready to be painted, matching the GPU render loop exactly to the video stream frequency.
Adjusting settings (such as pixel size or palette) while a video is paused should update the viewport instantly. Both components hook React state changes to a static renderSingleFrame() execution, rendering immediately even when the video timeline is stationary.
In modern SPA applications (like React), components are mounted and unmounted frequently. If WebGL contexts are not disposed of properly, the browser can exceed the maximum hardware WebGL context limit (typically 8 to 16 concurrent contexts), throwing warnings and crashing canvas elements.
react-bitmap addresses this by implementing a complete cleanup sequence:
- Deleting vertex arrays (
gl.deleteVertexArray). - Deleting positions and texture coordinate buffers (
gl.deleteBuffer). - Releasing loaded source textures (
gl.deleteTexture). - Deleting compiled shader programs (
gl.deleteProgram). - Forcing Context Release: Invoking
WEBGL_lose_contextextension programmatically:
const ext = gl.getExtension('WEBGL_lose_context');
if (ext) {
ext.loseContext(); // Reclaims all hardware memory instantly
}To build a custom palette, supply an array of hex colors (minimum 1, maximum 32 colors) to the palette prop.
- Keep it small: Shaders resolve colors faster with smaller arrays (e.g. 4-8 colors).
- Maximize contrast: Dithering looks best when colors cover the full range of light and dark values. Include both dark colors (shadows) and bright colors (highlights).
- Match saturation: Adjust the
saturationandcontrastprops on the component to fit your palette's style (e.g., set saturation to0for monochrome palettes to prevent color-fringing, or set it to1.5for neon palettes).