mattdesl/gifenc

Some colors are being pixelated(?

jrafaaael opened this issue · 1 comments

The examples show better what I'm trying to say:

028f489e-372a-4d15-8b49-b17ce84c3626 29b01c27-8947-45bc-abcf-d2ba015d1774
gifenc result canvas.toDataUrl() result

Code:

function draw(frame?: CanvasImageSource) {
  return new Promise<void>((resolve) => {
    const VIDEO_NATURAL_WIDTH = videoRef?.videoWidth;
    const VIDEO_NATURAL_HEIGHT = videoRef?.videoHeight;
    const VIDEO_NATURAL_ASPECT_RATIO =
      VIDEO_NATURAL_WIDTH / VIDEO_NATURAL_HEIGHT;
    const p = 100;
    const width =
      Math.min(
        ctx.canvas.height * VIDEO_NATURAL_ASPECT_RATIO,
        ctx.canvas.width
      ) - p;
    const height = Math.min(
      width / VIDEO_NATURAL_ASPECT_RATIO,
      ctx.canvas.height
    );
    const left = (ctx.canvas.width - width) / 2;
    const top = (ctx.canvas.height - height) / 2;

    ctx?.drawImage(
      backgroundImageRef,
      0,
      0,
      ctx.canvas.width,
      ctx.canvas.height
    );

    ctx.imageSmoothingEnabled = true;
    ctx.imageSmoothingQuality = "high";
    ctx?.drawImage(frame ?? videoRef, left, top, width, height);
    resolve();
  });
}

export function exportAsGif() {
  const decodeWorker = new DecodeWorker();
  const gifEncoderWorker = new GifEncoderWorker();
  const gif = GIFEncoder({ auto: false });

  decodeWorker.addEventListener("message", async ({ data }) => {
    const { type, ...rest } = data;

    if (type === "frame") {
      const frame: VideoFrame = rest.frame;

      await draw(frame);

      frame.close();

      const uint8 = ctx?.getImageData(0, 0, 1920, 1080).data;

      gifEncoderWorker.postMessage({ type: "encode", frame: uint8 });
    }
  });

  gifEncoderWorker.addEventListener("message", ({ data }) => {
    const { type, ...rest } = data;

    if (type === "encoded") {
      const output = rest.output;

      frames.push(output);
    }
  });

  decodeWorker.postMessage({ type: "start", url: $recording?.url });

  setTimeout(async () => {
    const chunks = await Promise.all(frames);

    gif.writeHeader();

    // Now we can write each chunk
    for (let i = 0; i < chunks.length; i++) {
      gif.stream.writeBytesView(chunks[i]);
    }

    // Finish the GIF
    gif.finish();

    // Close workers
    decodeWorker.terminate();
    gifEncoderWorker.terminate();

    // Return bytes
    const buffer = gif.bytesView();
    const url = URL.createObjectURL(new Blob([buffer], { type: "image/gif" }));
    console.log(url);
  }, 50_000);
}
// gif-encoder.worker.ts

import { GIFEncoder, applyPalette, prequantize, quantize } from "gifenc";

const FORMAT = "rgb565";
const MAX_COLORS = 256;
let isFirstFrame = true;

function onEncodeFrame({ frame }: { frame: Uint8Array | Uint8ClampedArray }) {
  const encoder = GIFEncoder({ auto: false });

  prequantize(frame);

  const palette = quantize(frame, MAX_COLORS, { format: FORMAT });
  const index = applyPalette(frame, palette, FORMAT);

  encoder.writeFrame(index, 1920, 1080, { palette, first: isFirstFrame });

  const output = encoder.bytesView();

  self.postMessage({ type: "encoded", output }, { transfer: [output.buffer] });

  isFirstFrame = false;
}

const MESSAGE_HANLDER = {
  encode: onEncodeFrame,
  default: () => {
    throw new Error("This type of message is not available");
  },
};

type Handlers = keyof typeof MESSAGE_HANLDER;

self.addEventListener("message", (e) => {
  const { type, ...rest }: { type: Handlers } = e.data;
  const handler = MESSAGE_HANLDER[type] ?? MESSAGE_HANLDER.default;

  handler(rest);
});

From what I see, it looks like a standard quantization of an image with many colours down to an image with 256 colours, and that creates banding that shows on the colourful gradients. toDataURL generates an image with many more colours than 256, but GIF is limited to 256 colours.

There is not a very easy way around this, but here's some suggestions:

  • Use dithering, like floyd-steinberg dithering, as this will reduce the noticeable banding between colours. See an example here
  • Use a different quantizer than the one in gifenc; some might be better than others depending on the image. You could try some different ones here
  • If you have control over your backdrop while recording frames, you could use a flatter and less colourful one.