mattdesl / gifenc

fast GIF encoding
MIT License
277 stars 19 forks source link

Some colors are being pixelated(? #17

Open jrafaaael opened 4 months ago

jrafaaael commented 4 months ago

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);
});
mattdesl commented 3 weeks ago

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: