Skip to content

Instantly share code, notes, and snippets.

@Mr-Bossman
Created January 23, 2022 01:24
Show Gist options
  • Save Mr-Bossman/6fcc7d2dc5a386616b500018bc496630 to your computer and use it in GitHub Desktop.
Save Mr-Bossman/6fcc7d2dc5a386616b500018bc496630 to your computer and use it in GitHub Desktop.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="shortcut icon" type="image/jpg" href="/"/>
<script>
const GIF = function () {
// **NOT** for commercial use.
var timerID; // timer handle for set time out usage
var st; // holds the stream object when loading.
var interlaceOffsets = [0, 4, 2, 1]; // used in de-interlacing.
var interlaceSteps = [8, 8, 4, 2];
var interlacedBufSize; // this holds a buffer to de interlace. Created on the first frame and when size changed
var deinterlaceBuf;
var pixelBufSize; // this holds a buffer for pixels. Created on the first frame and when size changed
var pixelBuf;
const GIF_FILE = {
// gif file data headers
GCExt: 0xf9,
COMMENT: 0xfe,
APPExt: 0xff,
UNKNOWN: 0x01, // not sure what this is but need to skip it in parser
IMAGE: 0x2c,
EOF: 59, // This is entered as decimal
EXT: 0x21,
};
// simple buffered stream used to read from the file
var Stream = function (data) {
this.data = new Uint8ClampedArray(data);
this.pos = 0;
var len = this.data.length;
this.getString = function (count) {
// returns a string from current pos of len count
var s = "";
while (count--) {
s += String.fromCharCode(this.data[this.pos++]);
}
return s;
};
this.readSubBlocks = function () {
// reads a set of blocks as a string
var size,
count,
data = "";
do {
count = size = this.data[this.pos++];
while (count--) {
data += String.fromCharCode(this.data[this.pos++]);
}
} while (size !== 0 && this.pos < len);
return data;
};
this.readSubBlocksB = function () {
// reads a set of blocks as binary
var size,
count,
data = [];
do {
count = size = this.data[this.pos++];
while (count--) {
data.push(this.data[this.pos++]);
}
} while (size !== 0 && this.pos < len);
return data;
};
};
// LZW decoder uncompressed each frames pixels
// this needs to be optimised.
// minSize is the min dictionary as powers of two
// size and data is the compressed pixels
function lzwDecode(minSize, data) {
var i, pixelPos, pos, clear, eod, size, done, dic, code, last, d, len;
pos = pixelPos = 0;
dic = [];
clear = 1 << minSize;
eod = clear + 1;
size = minSize + 1;
done = false;
while (!done) {
// JavaScript optimisers like a clear exit though I never use 'done' apart from fooling the optimiser
last = code;
code = 0;
for (i = 0; i < size; i++) {
if (data[pos >> 3] & (1 << (pos & 7))) {
code |= 1 << i;
}
pos++;
}
if (code === clear) {
// clear and reset the dictionary
dic = [];
size = minSize + 1;
for (i = 0; i < clear; i++) {
dic[i] = [i];
}
dic[clear] = [];
dic[eod] = null;
} else {
if (code === eod) {
done = true;
return;
}
if (code >= dic.length) {
dic.push(dic[last].concat(dic[last][0]));
} else if (last !== clear) {
dic.push(dic[last].concat(dic[code][0]));
}
d = dic[code];
len = d.length;
for (i = 0; i < len; i++) {
pixelBuf[pixelPos++] = d[i];
}
if (dic.length === 1 << size && size < 12) {
size++;
}
}
}
}
function parseColourTable(count) {
// get a colour table of length count Each entry is 3 bytes, for RGB.
var colours = [];
for (var i = 0; i < count; i++) {
colours.push([
st.data[st.pos++],
st.data[st.pos++],
st.data[st.pos++],
]);
}
return colours;
}
function parse() {
// read the header. This is the starting point of the decode and async calls parseBlock
var bitField;
st.pos += 6;
gif.width = st.data[st.pos++] + (st.data[st.pos++] << 8);
gif.height = st.data[st.pos++] + (st.data[st.pos++] << 8);
bitField = st.data[st.pos++];
gif.colorRes = (bitField & 0b1110000) >> 4;
gif.globalColourCount = 1 << ((bitField & 0b111) + 1);
gif.bgColourIndex = st.data[st.pos++];
st.pos++; // ignoring pixel aspect ratio. if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
if (bitField & 0b10000000) {
gif.globalColourTable = parseColourTable(gif.globalColourCount);
} // global colour flag
setTimeout(parseBlock, 0);
}
function parseAppExt() {
// get application specific data. Netscape added iterations and terminator. Ignoring that
st.pos += 1;
if ("NETSCAPE" === st.getString(8)) {
st.pos += 8;
} // ignoring this data. iterations (word) and terminator (byte)
else {
st.pos += 3; // 3 bytes of string usually "2.0" when identifier is NETSCAPE
st.readSubBlocks(); // unknown app extension
}
}
function parseGCExt() {
// get GC data
var bitField;
st.pos++;
bitField = st.data[st.pos++];
gif.disposalMethod = (bitField & 0b11100) >> 2;
gif.transparencyGiven = bitField & 0b1 ? true : false; // ignoring bit two that is marked as userInput???
gif.delayTime = st.data[st.pos++] + (st.data[st.pos++] << 8);
gif.transparencyIndex = st.data[st.pos++];
st.pos++;
}
function parseImg() {
// decodes image data to create the indexed pixel image
var deinterlace, frame, bitField;
deinterlace = function (width) {
// de interlace pixel data if needed
var lines, fromLine, pass, toline;
lines = pixelBufSize / width;
fromLine = 0;
if (interlacedBufSize !== pixelBufSize) {
// create the buffer if size changed or undefined.
deinterlaceBuf = new Uint8Array(pixelBufSize);
interlacedBufSize = pixelBufSize;
}
for (pass = 0; pass < 4; pass++) {
for (
toLine = interlaceOffsets[pass];
toLine < lines;
toLine += interlaceSteps[pass]
) {
deinterlaceBuf.set(
pixelBuf.subarray(fromLine, fromLine + width),
toLine * width
);
fromLine += width;
}
}
};
frame = {};
gif.frames.push(frame);
frame.disposalMethod = gif.disposalMethod;
frame.time = gif.length;
frame.delay = gif.delayTime * 10;
gif.length += frame.delay;
if (gif.transparencyGiven) {
frame.transparencyIndex = gif.transparencyIndex;
} else {
frame.transparencyIndex = undefined;
}
frame.leftPos = st.data[st.pos++] + (st.data[st.pos++] << 8);
frame.topPos = st.data[st.pos++] + (st.data[st.pos++] << 8);
frame.width = st.data[st.pos++] + (st.data[st.pos++] << 8);
frame.height = st.data[st.pos++] + (st.data[st.pos++] << 8);
bitField = st.data[st.pos++];
frame.localColourTableFlag = bitField & 0b10000000 ? true : false;
if (frame.localColourTableFlag) {
frame.localColourTable = parseColourTable(
1 << ((bitField & 0b111) + 1)
);
}
if (pixelBufSize !== frame.width * frame.height) {
// create a pixel buffer if not yet created or if current frame size is different from previous
pixelBuf = new Uint8Array(frame.width * frame.height);
pixelBufSize = frame.width * frame.height;
}
lzwDecode(st.data[st.pos++], st.readSubBlocksB()); // decode the pixels
if (bitField & 0b1000000) {
// de interlace if needed
frame.interlaced = true;
deinterlace(frame.width);
} else {
frame.interlaced = false;
}
processFrame(frame); // convert to canvas image
}
function processFrame(frame) {
// creates a RGBA canvas image from the indexed pixel data.
var ct,
cData,
dat,
pixCount,
ind,
useT,
i,
pixel,
pDat,
col,
frame,
ti;
frame.image = document.createElement("canvas");
frame.image.width = gif.width;
frame.image.height = gif.height;
frame.image.ctx = frame.image.getContext("2d");
ct = frame.localColourTableFlag
? frame.localColourTable
: gif.globalColourTable;
if (gif.lastFrame === null) {
gif.lastFrame = frame;
}
useT =
gif.lastFrame.disposalMethod === 2 ||
gif.lastFrame.disposalMethod === 3
? true
: false;
if (!useT) {
frame.image.ctx.drawImage(
gif.lastFrame.image,
0,
0,
gif.width,
gif.height
);
}
cData = frame.image.ctx.getImageData(
frame.leftPos,
frame.topPos,
frame.width,
frame.height
);
ti = frame.transparencyIndex;
dat = cData.data;
if (frame.interlaced) {
pDat = deinterlaceBuf;
} else {
pDat = pixelBuf;
}
pixCount = pDat.length;
ind = 0;
for (i = 0; i < pixCount; i++) {
pixel = pDat[i];
col = ct[pixel];
if (ti !== pixel) {
dat[ind++] = col[0];
dat[ind++] = col[1];
dat[ind++] = col[2];
dat[ind++] = 255; // Opaque.
} else if (useT) {
dat[ind + 3] = 0; // Transparent.
ind += 4;
} else {
ind += 4;
}
}
frame.image.ctx.putImageData(cData, frame.leftPos, frame.topPos);
gif.lastFrame = frame;
if (!gif.waitTillDone && typeof gif.onload === "function") {
doOnloadEvent();
} // if !waitTillDone the call onload now after first frame is loaded
}
// **NOT** for commercial use.
function finnished() {
// called when the load has completed
gif.loading = false;
gif.frameCount = gif.frames.length;
gif.lastFrame = null;
st = undefined;
gif.complete = true;
gif.disposalMethod = undefined;
gif.transparencyGiven = undefined;
gif.delayTime = undefined;
gif.transparencyIndex = undefined;
gif.waitTillDone = undefined;
pixelBuf = undefined; // dereference pixel buffer
deinterlaceBuf = undefined; // dereference interlace buff (may or may not be used);
pixelBufSize = undefined;
deinterlaceBuf = undefined;
gif.currentFrame = 0;
if (gif.frames.length > 0) {
gif.image = gif.frames[0].image;
}
doOnloadEvent();
if (typeof gif.onloadall === "function") {
gif.onloadall.bind(gif)({ type: "loadall", path: [gif] });
}
}
function canceled() {
// called if the load has been cancelled
finnished();
if (typeof gif.cancelCallback === "function") {
gif.cancelCallback.bind(gif)({ type: "canceled", path: [gif] });
}
}
function parseExt() {
// parse extended blocks
const blockID = st.data[st.pos++];
if (blockID === GIF_FILE.GCExt) {
parseGCExt();
} else if (blockID === GIF_FILE.COMMENT) {
gif.comment += st.readSubBlocks();
} else if (blockID === GIF_FILE.APPExt) {
parseAppExt();
} else {
if (blockID === GIF_FILE.UNKNOWN) {
st.pos += 13;
} // skip unknow block
st.readSubBlocks();
}
}
function parseBlock() {
// parsing the blocks
if (gif.cancel !== undefined && gif.cancel === true) {
canceled();
return;
}
const blockId = st.data[st.pos++];
if (blockId === GIF_FILE.IMAGE) {
// image block
parseImg();
if (gif.firstFrameOnly) {
finnished();
return;
}
} else if (blockId === GIF_FILE.EOF) {
finnished();
return;
} else {
parseExt();
}
if (typeof gif.onprogress === "function") {
gif.onprogress({
bytesRead: st.pos,
totalBytes: st.data.length,
frame: gif.frames.length,
});
}
setTimeout(parseBlock, 0); // parsing frame async so processes can get some time in.
}
function cancelLoad(callback) {
// cancels the loading. This will cancel the load before the next frame is decoded
if (gif.complete) {
return false;
}
gif.cancelCallback = callback;
gif.cancel = true;
return true;
}
function error(type) {
if (typeof gif.onerror === "function") {
gif.onerror.bind(this)({ type: type, path: [this] });
}
gif.onload = gif.onerror = undefined;
gif.loading = false;
}
function doOnloadEvent() {
// fire onload event if set
gif.currentFrame = 0;
gif.nextFrameAt = gif.lastFrameAt = new Date().valueOf(); // just sets the time now
if (typeof gif.onload === "function") {
gif.onload.bind(gif)({ type: "load", path: [gif] });
}
gif.onerror = gif.onload = undefined;
}
function dataLoaded(data) {
// Data loaded create stream and parse
st = new Stream(data);
parse();
}
function loadGif(filename) {
// starts the load
var ajax = new XMLHttpRequest();
ajax.responseType = "arraybuffer";
ajax.onload = function (e) {
if (e.target.status === 404) {
error("File not found");
} else if (e.target.status >= 200 && e.target.status < 300) {
dataLoaded(ajax.response);
} else {
error("Loading error : " + e.target.status);
}
};
ajax.open("GET", filename, true);
ajax.send();
ajax.onerror = function (e) {
error("File error");
};
this.src = filename;
this.loading = true;
}
gif = {
// the gif image object
onload: null, // fire on load. Use waitTillDone = true to have load fire at end or false to fire on first frame
onerror: null, // fires on error
onprogress: null, // fires a load progress event
onloadall: null, // event fires when all frames have loaded and gif is ready
waitTillDone: true, // If true onload will fire when all frames loaded, if false, onload will fire when first frame has loaded
loading: false, // true if still loading
firstFrameOnly: false, // if true only load the first frame
width: null, // width in pixels
height: null, // height in pixels
frames: [], // array of frames
comment: "", // comments if found in file. Note I remember that some gifs have comments per frame if so this will be all comment concatenated
length: 0, // gif length in ms (1/1000 second)
currentFrame: 0, // current frame.
frameCount: 0, // number of frames
lastFrame: null, // temp hold last frame loaded so you can display the gif as it loads
image: null, // the current image at the currentFrame
// functions
load: loadGif, // call this to load a file
cancel: cancelLoad, // call to stop loading
};
return gif;
};
</script>
</head>
<body>
<canvas id="canvas">
<script language="javascript">
const faviconVar = document.querySelector('link[rel*="icon"]');
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
const gifURL = "https://cdn.discordapp.com/emojis/399376764952117258.gif";
let myGif = GIF();
myGif.onerror = function (e) {
console.log("Gif loading error " + e.type);
};
myGif.load(gifURL);
var framec = 0;
var skips = 0;
const skipamm = 20;
function update(timer) {
if(skips == skipamm){
if (myGif) {
if (
myGif.frames[framec] != undefined &&
myGif.frames.length > framec
) {
// Shows frames as they load
if (framec == myGif.frames.length - 1) {
framec = 0;
}
canvas.width = myGif.width;
canvas.height = myGif.height;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(myGif.frames[framec].image, 0, 0);
framec += 1;
faviconVar.href = canvas.toDataURL("image/png");
}
}
skips = 0;
}
skips += 1;
requestAnimationFrame(update);
}
requestAnimationFrame(update);
</script>
</canvas>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment