GIFGrooverSource.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473
  1. function GIFGroover() {
  2. var interlacedBufSize, deinterlaceBuf, pixelBufSize, pixelBuf, st, timerID, currentFrame, currentTime, playing, loading, complete, cancel, disposalMethod, transparencyGiven, delayTime, transparencyIndex, gifWidth, gifHeight, duration, frameTime, playSpeed, nextFrameTime, nextFrame, lastFrame, bgColorCSS, gifSrc, paused, colorRes, globalColourCount, bgColourIndex, globalColourTable;
  3. const bitValues = new Uint32Array([1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024, 2048, 4096]);
  4. const interlaceOffsets = [0, 4, 2, 1];
  5. const interlaceSteps = [8, 8, 4, 2];
  6. const frames = [];
  7. const comments = [];
  8. const events = [];
  9. nextFrameTime = undefined;
  10. nextFrame = null;
  11. playSpeed = 1;
  12. frameTime = duration = gifHeight = gifWidth = 0;
  13. cancel = complete = loading = playing = false;
  14. const GIF_FILE = { // gif file data block headers
  15. GCExt : 249,
  16. COMMENT : 254,
  17. APPExt : 255,
  18. UNKNOWN : 1, // not sure what this is but need to skip it in parser
  19. IMAGE : 44, // This block contains compressed image data
  20. EOF : 59, // This is entered as decimal
  21. EXT : 33,
  22. };
  23. function Stream(data) {
  24. var pos = this.pos = 0;
  25. const dat = this.data = new Uint8Array(data);
  26. const len = this.data.length;
  27. this.getString = function (count) { // returns a string from current pos
  28. var s = "";
  29. pos = this.pos;
  30. while (count--) { s += String.fromCharCode(dat[pos++]) }
  31. this.pos = pos;
  32. return s;
  33. };
  34. this.readSubBlocks = function () { // reads a set of blocks as a string
  35. var size, count, data = "";
  36. pos = this.pos;
  37. while (size !== 0 && pos < len) {
  38. count = size = dat[pos++];
  39. while (count--) { data += String.fromCharCode(dat[pos++]) }
  40. }
  41. this.pos = pos;
  42. return data;
  43. }
  44. this.readSubBlocksB = function () { // reads a set of blocks as binary
  45. var size, count, data = [], idx = 0;
  46. pos = this.pos;
  47. while (size !== 0 && pos < len) {
  48. count = size = dat[pos++];
  49. while (count--) { data[idx++] = dat[pos++] }
  50. }
  51. this.pos = pos;
  52. return data;
  53. }
  54. }
  55. function decodePixels(minSize, data) {
  56. var i, pixelPos, pos, clear, end, size, busy, key, last, plen, len;
  57. const bitVals = bitValues;
  58. const dic = [];
  59. pos = pixelPos = 0;
  60. clear = bitVals[minSize];
  61. end = clear + 1;
  62. size = minSize + 1;
  63. busy = true;
  64. for (i = 0; i < clear; i++) { dic[i] = [i] }
  65. len = end + 1;
  66. while (busy) {
  67. last = key;
  68. key = 0;
  69. for (i = 0; i < size; i++) {
  70. if (data[pos >> 3] & bitVals[pos & 7]) { key |= bitVals[i] }
  71. pos++;
  72. }
  73. if (key === clear) { // reset the dictionary
  74. size = minSize + 1;
  75. len = end + 1;
  76. for (i = 0; i < end; i++) { dic[i] = [i] }
  77. dic[end] = [0];
  78. dic[clear] = [0];
  79. } else {
  80. if (key === end) { break } // THIS IS EXIT POINT
  81. if (key >= len) { dic[len ++] = [...dic[last], dic[last][0]] }
  82. else if (last !== clear) { dic[len ++] = [...dic[last], dic[key][0]] }
  83. plen = dic[key].length;
  84. for (i = 0; i < plen; i++) { pixelBuf[pixelPos++] = dic[key][i] }
  85. if (size < 12 && len === bitVals[size]) { size += 1 }
  86. }
  87. }
  88. };
  89. function createColourTable(count) {
  90. var i = 0;
  91. count <<= 2;
  92. const colours = new Uint8Array(count);
  93. while (i < count) {
  94. colours[i++] = st.data[st.pos++];
  95. colours[i++] = st.data[st.pos++];
  96. colours[i++] = st.data[st.pos++];
  97. colours[i++] = 255;
  98. }
  99. return new Uint32Array(colours.buffer);
  100. }
  101. function parse (){ // read the header. This is the starting point of the decode and async calls parseBlock
  102. st.pos += 6;
  103. gifWidth = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  104. gifHeight = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  105. pixelBuf = new Uint8Array(gifWidth * gifHeight);
  106. const bitField = st.data[st.pos++];
  107. gif.colorRes = (bitField & 112) >> 4; //0b1110000
  108. globalColourCount = 1 << ((bitField & 7) + 1);
  109. bgColourIndex = st.data[st.pos++];
  110. st.pos++; // ignoring pixel aspect ratio. if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
  111. if (bitField & 128) { // global colour flag
  112. globalColourTable = createColourTable(globalColourCount);
  113. const bg = globalColourTable[bgColourIndex];
  114. bgColorCSS = bg !== undefined ? `rgb(${bg&255},${(bg>>8)&255},${(bg>>16)&255})` : `black`;
  115. }
  116. fireEvent("decodestart", { width : gifWidth, height : gifHeight}, true);
  117. setTimeout(parseBlock,0);
  118. }
  119. function parseAppExt() { // get application specific data.
  120. st.pos += 1;
  121. if ('NETSCAPE' === st.getString(8)) { st.pos += 8 } // ignoring this data. iterations (word) and terminator (byte)
  122. else { st.pos += 3; st.readSubBlocks() } // 3 bytes of string usually "2.0"
  123. };
  124. function parseGCExt() { // get GC data
  125. st.pos++;
  126. const bitField = st.data[st.pos++];
  127. disposalMethod = (bitField & 28) >> 2;
  128. transparencyGiven = bitField & 1 ? true : false; // ignoring bit two that is marked as userInput???
  129. delayTime = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  130. transparencyIndex = st.data[st.pos++];
  131. st.pos++;
  132. };
  133. function parseImg() { // decodes image data to create the indexed pixel image
  134. function deinterlace(width) { // de interlace pixel data if needed
  135. var fromLine, pass, toLine;
  136. const lines = pixelBufSize / width;
  137. fromLine = 0;
  138. if (interlacedBufSize !== pixelBufSize) {
  139. deinterlaceBuf = new Uint8Array(pixelBufSize);
  140. interlacedBufSize = pixelBufSize;
  141. }
  142. for (pass = 0; pass < 4; pass++) {
  143. for (toLine = interlaceOffsets[pass]; toLine < lines; toLine += interlaceSteps[pass]) {
  144. deinterlaceBuf.set(pixelBuf.subarray(fromLine, fromLine + width), toLine * width);
  145. fromLine += width;
  146. }
  147. }
  148. };
  149. const frame = {}
  150. frames.push(frame);
  151. frame.disposalMethod = disposalMethod;
  152. frame.time = duration;
  153. frame.delay = delayTime * 10;
  154. duration += frame.delay;
  155. frame.leftPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  156. frame.topPos = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  157. frame.width = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  158. frame.height = (st.data[st.pos++]) + ((st.data[st.pos++]) << 8);
  159. const bitField = st.data[st.pos++];
  160. frame.localColourTableFlag = bitField & 128 ? true : false;
  161. if (frame.localColourTableFlag) { frame.localColourTable = createColourTable(1 << ((bitField & 7) + 1)) }
  162. if (pixelBufSize !== frame.width * frame.height) { pixelBufSize = frame.width * frame.height }
  163. if (transparencyGiven) { frame.transparencyIndex = transparencyIndex }
  164. else { frame.transparencyIndex = undefined }
  165. decodePixels(st.data[st.pos++], st.readSubBlocksB());
  166. if (bitField & 64) {
  167. frame.interlaced = true;
  168. deinterlace(frame.width);
  169. } else { frame.interlaced = false }
  170. processFrame(frame);
  171. };
  172. function processFrame(frame) { // creates a RGBA canvas image from the indexed pixel data.
  173. var useT, i, pixel, pDat, col;
  174. frame.image = document.createElement('canvas');
  175. frame.image.width = gifWidth;
  176. frame.image.height = gifHeight;
  177. frame.image.ctx = frame.image.getContext("2d");
  178. const ct = frame.localColourTableFlag ? frame.localColourTable : globalColourTable;
  179. lastFrame = lastFrame ? lastFrame : frame;
  180. useT = (lastFrame.disposalMethod === 2 || lastFrame.disposalMethod === 3) ? true : false;
  181. if (!useT) { frame.image.ctx.drawImage(lastFrame.image, 0, 0, gifWidth, gifHeight) }
  182. const cData = frame.image.ctx.getImageData(frame.leftPos, frame.topPos, frame.width, frame.height);
  183. const ti = frame.transparencyIndex;
  184. const dat = new Uint32Array(cData.data.buffer);
  185. if (frame.interlaced) { pDat = deinterlaceBuf }
  186. else { pDat = pixelBuf }
  187. for (i = 0; i < pixelBufSize; i++) {
  188. pixel = pDat[i];
  189. if (ti !== pixel) { dat[i] = ct[pixel] }
  190. else if (useT) { dat[i] = 0 }
  191. }
  192. frame.image.ctx.putImageData(cData, frame.leftPos, frame.topPos);
  193. if (!playing) { gif.image = frame.image }
  194. lastFrame = frame;
  195. };
  196. function cleanup() { // a little house keeping
  197. lastFrame = null;
  198. st = undefined;
  199. disposalMethod = undefined;
  200. transparencyGiven = undefined;
  201. delayTime = undefined;
  202. transparencyIndex = undefined;
  203. pixelBuf = undefined;
  204. deinterlaceBuf = undefined;
  205. pixelBufSize = undefined;
  206. deinterlaceBuf = undefined;
  207. complete = true;
  208. }
  209. function finnished() { // called when the load has completed
  210. loading = false;
  211. if (!playing) {
  212. currentTime = currentFrame = 0;
  213. if (frames.length > 0) { gif.image = frames[0].image }
  214. }
  215. doOnloadEvent();
  216. cleanup();
  217. }
  218. function canceled () { finnished() }
  219. function parseExt() { // parse extended blocks
  220. const blockID = st.data[st.pos++];
  221. if (blockID === GIF_FILE.GCExt) { parseGCExt() }
  222. else if (blockID === GIF_FILE.COMMENT) { comments.push(st.readSubBlocks()) }
  223. else if (blockID === GIF_FILE.APPExt) { parseAppExt() }
  224. else {
  225. if (blockID === GIF_FILE.UNKNOWN) { st.pos += 13 } // skip unknown block
  226. st.readSubBlocks();
  227. }
  228. }
  229. function parseBlock() { // parsing the blocks
  230. if (cancel === true) { return canceled() }
  231. const blockId = st.data[st.pos++];
  232. if (blockId === GIF_FILE.IMAGE ) {
  233. parseImg();
  234. fireEvent("progress", { progress : ((st.pos / st.data.length) * 1000 | 0) / 10, frameCount : frames.length });
  235. if (gif.firstFrameOnly) { return finnished() }
  236. } else if (blockId === GIF_FILE.EOF) { return finnished() }
  237. else { parseExt() }
  238. setTimeout(parseBlock,0);
  239. };
  240. function cancelLoad() { // cancels the loading. This will cancel the load before the next frame is decoded
  241. if (complete) { return false }
  242. return cancel = true;
  243. }
  244. function error(message) {
  245. fireEvent("error", {message : message}, false);
  246. //events.decodestart = events.onload = undefined;
  247. loading = false;
  248. }
  249. function doOnloadEvent() { // fire onload event if set
  250. currentTime = currentFrame = 0;
  251. fireEvent("load", {frameCount : frames.length}, true);
  252. if (gif.playOnLoad) { gif.play() }
  253. }
  254. function dataLoaded(data) { // Data loaded create stream and parse
  255. st = new Stream(data);
  256. parse();
  257. }
  258. function loadGif(filename) { // starts the load
  259. var ajax = new XMLHttpRequest();
  260. gifSrc = filename;
  261. loading = true;
  262. ajax.responseType = "arraybuffer";
  263. ajax.onload = function (e) {
  264. if (e.target.status === 404) {
  265. gifSrc = undefined;
  266. error("File not found")
  267. } else if (e.target.status >= 200 && e.target.status < 300 ) { dataLoaded(ajax.response) }
  268. else {
  269. gifSrc = undefined;
  270. error("Loading error : " + e.target.status)
  271. }
  272. };
  273. ajax.onerror = function (e) {
  274. gifSrc = undefined;
  275. error("File error " + e.message)
  276. };
  277. ajax.open('GET', filename, true);
  278. ajax.send();
  279. }
  280. function startLoad(filename) {
  281. if (gifSrc === undefined) {
  282. gifSrc = filename;
  283. setTimeout(()=>loadGif(gifSrc),0);
  284. } else {
  285. const message = "GIF is limited to a single load. Create a new GIF object to load another gif."
  286. error(message);
  287. console.warn(message);
  288. }
  289. }
  290. function setPlaySpeed(speed) {
  291. playSpeed = (speed * 100 | 0) / 100;
  292. nextFrameTime = undefined;
  293. if (Math.abs(playSpeed) === 0) {
  294. playSpeed = 0;
  295. if (playing) { pause() }
  296. }
  297. }
  298. function play() { // starts play if paused
  299. if (!playing) {
  300. if (playSpeed === 0) { playSpeed = 1 }
  301. paused = false;
  302. playing = true;
  303. tick();
  304. }
  305. }
  306. function pause() { // stops play
  307. paused = true;
  308. playing = false;
  309. clearTimeout(timerID);
  310. nextFrameTime = undefined;
  311. }
  312. function togglePlay(){
  313. if (paused || !playing) { gif.play() }
  314. else { gif.pause() }
  315. }
  316. function seekFrame(index) { // seeks to frame number.
  317. clearTimeout(timerID);
  318. nextFrameTime = undefined;
  319. nextFrame = null;
  320. currentFrame = ((index % frames.length) + frames.length) % frames.length;
  321. if (playing) { tick() }
  322. else {
  323. gif.image = frames[currentFrame].image;
  324. currentTime = frames[currentFrame].time;
  325. }
  326. }
  327. function getFrameAtTime(timeMs) { // returns frame that is displayed at timeMs (ms 1/1000th)
  328. if (timeMs < 0) { timeMs = 0 }
  329. timeMs %= duration;
  330. var frame = 0;
  331. while (frame < frames.length && timeMs > frames[frame].time + frames[frame].delay) { frame += 1 }
  332. return frame;
  333. }
  334. function seek(time) { // time in Seconds // seek to frame that would be displayed at time
  335. clearTimeout(timerID);
  336. nextFrameTime = undefined;
  337. nextFrame = null;
  338. currentFrame = getFrameAtTime(time * 1000);
  339. if (playing) { tick() }
  340. else {
  341. currentTime = frames[currentFrame].time;
  342. gif.image = frames[currentFrame].image;
  343. }
  344. }
  345. function tick() {
  346. var delay, frame, framesSkipped = false, delayFix = 0;
  347. if (playSpeed === 0) {
  348. gif.pause();
  349. return;
  350. } else {
  351. if (nextFrameTime !== undefined && nextFrame === null){
  352. const behind = nextFrameTime - performance.now();
  353. if (behind < -frameTime / 2) {
  354. framesSkipped = true;
  355. nextFrameTime = ((nextFrameTime + behind / playSpeed) % duration) + duration; // normalize to positive
  356. currentFrame = getFrameAtTime(nextFrameTime);
  357. if (playSpeed < 0) { frame = currentFrame === 0 ? frames.length - 1 : currentFrame - 1 }
  358. else { frame = currentFrame }
  359. } else if (behind < 0) { delayFix = behind } // always behind as code take time to execute;
  360. }
  361. if (! framesSkipped){
  362. if (playSpeed < 0) {
  363. if (nextFrame !== null) { currentFrame = nextFrame }
  364. else { currentFrame = currentFrame === 0 ? frames.length - 1 : currentFrame - 1 }
  365. frame = currentFrame === 0 ? frames.length - 1 : currentFrame - 1;
  366. } else {
  367. if (nextFrame !== null) { currentFrame = nextFrame }
  368. frame = currentFrame = (currentFrame + 1) % frames.length;
  369. }
  370. }
  371. delay = Math.abs(frames[frame].delay / playSpeed) + delayFix;
  372. frameTime = Math.abs(frames[frame].delay / playSpeed);
  373. nextFrameTime = performance.now() + delay;
  374. gif.image = frames[currentFrame].image;
  375. currentTime = frames[currentFrame].time;
  376. timerID = setTimeout(tick, delay);
  377. nextFrame = null;
  378. }
  379. }
  380. function fireEvent(name, data, clearEvent = false) {
  381. if (events["on" + name]) {
  382. setTimeout(() => {
  383. data.type = name;
  384. data.gif = gif;
  385. events["on" + name](data);
  386. if (clearEvent) { _removeEventListener(name) }
  387. }, 0);
  388. }
  389. }
  390. function _addEventListener(name, func) {
  391. if (typeof func === "function") {
  392. if (name !== "progress") { func = func.bind(gif) }
  393. events["on" + name] = func
  394. };
  395. }
  396. function _removeEventListener(name) {
  397. if (events["on" + name] !== undefined) { events["on" + name] = undefined }
  398. }
  399. const gif = { // the gif image object
  400. image : null, // the current image at the currentFrame
  401. comments : comments,
  402. //==============================================================================================================
  403. // Play status
  404. get paused() { return paused }, // true if paused
  405. get playing() { return playing }, // true if playing
  406. get loading() { return loading }, // true if still loading
  407. get complete() { return complete }, // true when loading complete. Does not mean success
  408. //==============================================================================================================
  409. // Use to load the gif.
  410. set src(URL) { startLoad(URL) }, // load the gif from URL. Note that the gif will only start loading after current execution is complete.
  411. cancel : cancelLoad, // Stop loading cancel() returns true if cancels, false if already loaded
  412. //==============================================================================================================
  413. // General properties getters or functions
  414. get backgroundColor() { return bgColorCSS },// returns the background colour as a CSS color value
  415. get src() { return gifSrc }, // get the gif URL
  416. get width() { return gifWidth }, // Read only. Width in pixels
  417. get height() { return gifHeight }, // Read only. Height in pixels
  418. get naturalWidth() { return gifWidth }, // Read only. Height in pixels
  419. get naturalHeight(){ return gifHeight }, // Read only. Height in pixels
  420. get allFrames() { return frames.map(frame => frame.image) }, // returns array of frames as images (canvas).
  421. get duration() { return duration }, // Read only. gif duration in ms (1/1000 second)
  422. get currentFrame() { return currentFrame }, // gets the current frame index
  423. get currentTime() { return currentTime }, // gets the current frame index
  424. get frameCount() { return frames.length },// Read only. Current frame count, during load is number of frames loaded
  425. get playSpeed() { return playSpeed }, // play speed 1 normal, 2 twice 0.5 half, -1 reverse etc...
  426. getFrame(index) { // return the frame at index 0. If index is outside range closet first or last frame is returned
  427. return frames[index < 0 ? 0 : index >= frames.length ? frames.length-1 : index].image;
  428. },
  429. //==============================================================================================================
  430. // Shuttle control setters
  431. set currentFrame(index) { seekFrame(index) }, // seeks to frame index
  432. set currentTime(time) { seek(time) }, // seeks to time
  433. set playSpeed(speed) { setPlaySpeed(speed) }, // set the play speed. NOTE speed will not take affect if playing until the current frame duration is up.
  434. //==============================================================================================================
  435. // load control flags
  436. playOnLoad : true, // if true starts playback when loaded
  437. firstFrameOnly : false, // if true only load the first frame
  438. //==============================================================================================================
  439. // events. Please note setting to a non function will be ignored.
  440. set onload(func) { _addEventListener("load",func) }, // fires when gif loaded and decode. Will fire if you cancel before all frames are decode.
  441. set onerror(func) { _addEventListener("error",func) }, // fires on error
  442. set onprogress(func) { _addEventListener("progress",func) }, // fires a load progress event
  443. set ondecodestart(func) { _addEventListener("decodestart",func) },// event fires when gif file content has been read and basic header info is read (width, and height) and before decoding of frames begins.
  444. //==============================================================================================================
  445. // play controls
  446. play : play, // Start playback of gif
  447. pause : pause, // Pauses gif at currentframe
  448. seek : seek, // Moves current time to position seek(time) time in seconds
  449. seekFrame : seekFrame, // Moves time to frame number time is set to frame start.
  450. togglePlay : togglePlay, // toggles play state
  451. };
  452. return gif;
  453. }