GIFGroover.js 22 KB

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