Pixiv鼠標預覽

滑鼠移入縮圖時顯示預覽視窗,滑動滾輪切換圖片,單圖、多圖、動圖預覽與下載原圖功能

  1. // ==UserScript==
  2. // @name Pixiv鼠標預覽
  3. // @namespace Pixiv鼠標預覽
  4. // @version 2.0.5
  5. // @description 滑鼠移入縮圖時顯示預覽視窗,滑動滾輪切換圖片,單圖、多圖、動圖預覽與下載原圖功能
  6. // @author Eltair
  7. // @match https://www.pixiv.net/*
  8. // @connect i.pximg.net
  9. // @grant GM_xmlhttpRequest
  10. // @license MIT
  11. // ==/UserScript==
  12.  
  13. (function() {
  14. 'use strict';
  15.  
  16. // ========== 下面是可更改的數值 ========== //
  17. const BOXSIZE = 0.75; // 預覽框寬度,視窗可視區域短邊的倍數(預設 0.75 倍),ex:-視窗可視區域寬高是1920x1080,那麼預覽框會是 1080 * 0.75 = 810
  18. const SPEED = 0.6; // 鼠標移動到縮圖上多久後顯示預覽框(預設 0.6 秒)
  19. const ADV = 2; // 多張圖片時預先背景載入的張數(預設 2 張),ex:開啟預覽視窗時預設同時載入 3 張圖片並顯示第 1 張
  20.  
  21. // [預覽插畫品質]
  22. // 0 = 原圖
  23. // 1 = 大圖(1200x1200)
  24. // 2 = 小圖(540x540)
  25. const QUALITY = 1
  26.  
  27. // [預覽動圖品質]
  28. // 0 = 原圖(1920x1080)
  29. // 1 = 小圖(600x600)
  30. const QUALITY_AM = 1
  31. // ========== 上面是可更改的數值 ========== //
  32.  
  33. let t = null; // 計時器
  34. let zp = null; // ZipImagePlayer
  35. let css = document.createElement("style"); // css
  36. let line = document.createElement("div"); // 底線
  37.  
  38. // [需要捕捉的 class 名稱,用於判斷是否為可預覽的縮圖,如有缺少可以自行新增]
  39. // sc-rp5asc-10: / => 首頁
  40. // sc-cc54fdaf-3: /discovery/users => 發現頁
  41. // sc-f7489a5-10: /dashboard => 創作儀錶板
  42. // _thumbnail: /ranking.php => 排行榜
  43. // _work: /stacc => 個人動態
  44. // d949E3qlwcA9QJIe: /idea => 創作點子
  45. const targetClass = [".sc-rp5asc-10", ".sc-cc54fdaf-3", ".sc-f7489a5-10", ".sc-4a85edf0-2", "._thumbnail", "._work", ".d949E3qlwcA9QJIe"].join(",");
  46.  
  47. // [設定CSS]
  48. // under css
  49. css.innerText = `@keyframes showUnder { from { width: 0%; } to { width: 100%; } } .under { background-color: #0096fa; height: 2px; position: absolute; bottom: 0; left: 0; width: 0%; } .showUnder { animation: showUnder ${SPEED}s linear; width: 100%; }` ;
  50. // box css
  51. css.innerText += ".imgbox{z-index: 10001; position: absolute; text-align: center; border: 1px solid #cecece; background-color: #f5f5f5; border-radius: 8px; white-space: nowrap;}.imgbox .topbar,.imgbox .toolbar{display: flex; justify-content: space-between; padding: 3px 6px; gap: 8px;}.imgbox .tags{overflow: hidden; text-overflow: ellipsis;}.imgbox img{vertical-align: middle;}.imgbox .toolbar > *{width: 33%; overflow: hidden; text-overflow: ellipsis;}.imgbox .toolbar > :first-child{text-align: left;}.imgbox .toolbar > :last-child{text-align: right;}.imgbox .r18{color: #ff386b; font-weight: bold;}.imgbox .ai{color: #3d7699; font-weight: bold;}.imgbox .dl,.imgbox .fixed-pb,.imgbox .next,.imgbox .prev{font-weight: 700; color: #1f1f1f; cursor: pointer;}.imgbox .next,.imgbox .prev{position: absolute; width: 50%; top: 24px; bottom: 24px;}.imgbox .next{right: 0;}.imgbox .prev{left: 0;}.imgbox .toolbar a,.dl:visited{color: #adadad;}.imgbox .toolbar a,.dl:hover{color: #5c5c5c;}.imgpv{overflow: hidden;}.am{display: flex; align-items: center; justify-content: center;}";
  52. document.head.appendChild(css);
  53. line.className = "under showUnder";
  54.  
  55. // [綁定監聽器]
  56. document.addEventListener('mouseover', in_img);
  57. document.addEventListener('mouseout', out_img);
  58.  
  59. // ========== 主要方法 ========== //
  60. // [滑鼠移入圖片]
  61. function in_img(e) {
  62. if (e.target.matches(targetClass)) {
  63. if (e.target.nodeName == "A") e.target.appendChild(line);
  64. else e.target.parentNode.appendChild(line);
  65. window.clearTimeout(t);
  66. t = window.setTimeout(function() {
  67. window.clearTimeout(t);
  68. new previewbox(e.target);
  69. }, SPEED * 1000)
  70. }
  71. }
  72. // [滑鼠移出圖片]
  73. function out_img(e) {
  74. if (e.target.matches(targetClass)) {
  75. if (e.target.nodeName == "A") e.target.removeChild(line);
  76. else e.target.parentNode.removeChild(line);
  77. window.clearTimeout(t);
  78. }
  79. }
  80. // [預覽框]
  81. function previewbox(target) {
  82. this.size = Math.min(document.documentElement.clientWidth, document.documentElement.clientHeight) * BOXSIZE; // 彈出框寬度,單位px
  83. this.adv = ADV;
  84. this.target = target;
  85. this.title = "";
  86. this.fixed = false;
  87. this.pid = null;
  88. this.box = null;
  89. this.box_html = "<div class='topbar'><span class='restrict'></span><p class='tags'>取得標籤中..</p><span class='fixed-pb'>固定</span></div><div class='imgpv' data-cursor='0' style='width: {size}px; height: {size}px;'></div><div style='position: relative;' class='under'></div><div class='prev'></div><div class='next'></div><div class='toolbar'><div><a href=''></a></div><p class='count'>1/1</p><div><span class='dl' data-src=''>下載</span></div></div>";
  90. this.img_html = "<img src='{imgsrc}' style='max-width: {size}px; max-height: {size}px;'>";
  91. this._createBox();
  92. return this;
  93. }
  94. // [加入彈出框]
  95. previewbox.prototype._createBox = function() {
  96. let pos = this._calcPosition();
  97. this.pid = this._getId();
  98. this.box = document.createElement("div");
  99. this.box.className = "imgbox";
  100. this.box.innerHTML = this.box_html.replace(/{size}/g, this.size);
  101. this.box.style.top = `${pos.top}px`;
  102. this.box.style.left = `${pos.left}px`;
  103. this.box.style.width = `${this.size + 2}px`;
  104. this.box.addEventListener("mouseleave", function (e) {
  105. if (!this.fixed) {
  106. this._removeBox.bind(e.target)()
  107. }
  108. }.bind(this));
  109.  
  110. // 固定按鈕
  111. this.box.querySelector(".fixed-pb").addEventListener("click", function (e) {
  112. this.fixed = !this.fixed
  113. e.target.innerText = this.fixed ? "關閉" : "固定"
  114. }.bind(this));
  115. document.querySelector("body").appendChild(this.box);
  116. this._getData(function(json) {
  117. let body = json.body;
  118. // 設定標題
  119. this.title = body.title
  120. // 動圖或是一般插畫
  121. if(body.illustType == 2) {
  122. this._addAm();
  123. } else if(body.illustType == 0 || body.illustType == 1) {
  124. this._addImgs(this._getImgurls(body.urls, body.pageCount));
  125. }
  126. // 設定標籤
  127. this._addTags(body.tags.tags, body.xRestrict, body.aiType == 2);
  128. }.bind(this));
  129. }
  130. // [刪除彈出框]
  131. previewbox.prototype._removeBox = function() {
  132. this != null && this.remove();
  133. zp && zp.loadAbort();
  134. zp && window.clearTimeout(zp._loadTimer);
  135. zp && window.clearTimeout(zp._timer);
  136. zp = null;
  137. }
  138. // [計算彈出框位置]
  139. previewbox.prototype._calcPosition = function() {
  140. let windowWidth = document.documentElement.clientWidth,
  141. windowHeight = document.documentElement.clientHeight,
  142. bounding = this.target.getBoundingClientRect(),
  143. scrollTop = document.documentElement.scrollTop,
  144. scrollLeft = document.documentElement.scrollLeft,
  145. top = 0,
  146. left = 0;
  147.  
  148. left = scrollLeft + bounding.left - (this.size - bounding.width) / 2;
  149. top = scrollTop + bounding.top - (this.size - bounding.height) / 2;
  150. // 如果上邊超出視窗
  151. if (top < scrollTop) {
  152. top = scrollTop + 2;
  153. }
  154. // 如果下邊超出視窗
  155. if (top + (this.size + 44) > scrollTop + windowHeight) {
  156. top = windowHeight - (this.size + 44) + scrollTop - 4;
  157. }
  158. // 如果左邊超出視窗
  159. if (left < scrollLeft) {
  160. left = scrollLeft + 2;
  161. }
  162. // 如果右邊超出視窗
  163. if (left + this.size > scrollLeft + windowWidth) {
  164. left = windowWidth - this.size + scrollLeft - 4;
  165. }
  166. return { top: top, left: left };
  167. }
  168. // [插入圖片方法]
  169. previewbox.prototype._addImg = function(url) {
  170. let div = document.createElement("div");
  171. div.style.width = `${this.size}px`;
  172. div.style.height = `${this.size}px`;
  173. div.style.lineHeight = `${this.size}px`;
  174. div.innerHTML = this.img_html.replace(/{imgsrc}/g, url[1]).replace(/{size}/g, this.size);
  175. this.box.querySelector(".imgpv").appendChild(div);
  176. }
  177. // [插入多圖方法]
  178. previewbox.prototype._addImgs = function(urls) {
  179. let cursor = 0; // 當前圖片位置
  180. let imgpv = this.box.querySelector(".imgpv"); // 圖片視窗
  181. let count = this.box.querySelector(".count"); // 頁數
  182. let dl = this.box.querySelector(".dl"); // 下載
  183. let next = this.box.querySelector(".next"); // 下一張按鈕
  184. let prev = this.box.querySelector(".prev"); // 上一張按鈕
  185. let imgLink = this.box.querySelector(".toolbar a"); // 圖片連結
  186. let under = this.box.querySelector(".under"); // 進度條
  187.  
  188. imgLink.innerText = imgLink.title = this.title
  189. imgLink.href = urls[0]
  190. // 圖片數量標籤
  191. count.innerText = `1/${urls[1].length}`;
  192. // 圖片下載按鈕
  193. dl.dataset.src = urls[2][cursor];
  194. dl.addEventListener("click", function(e) {
  195. const filename = e.target.dataset.src.match(/[0-9]+\_p[0-9]+\..+/g)[0];
  196. this._downloadImg(e.target.dataset.src, filename);
  197. }.bind(this));
  198.  
  199. // 單張插圖
  200. if(urls[1].length == 1) {
  201. this._addImg([urls[0], urls[1][0]]);
  202. this.box.querySelector(".next").style.display = "none"; // 下一張按鈕
  203. this.box.querySelector(".prev").style.display = "none"; // 上一張按鈕
  204. return;
  205. }
  206.  
  207. // 切換圖片
  208. const changImg = (e)=> {
  209. e.preventDefault();
  210. // 滾輪往下或往上
  211. if (e.deltaY > 0 && cursor < urls[1].length - 1) {
  212. cursor++;
  213. if (cursor == imgpv.childNodes.length - this.adv && cursor < urls[1].length - this.adv) {
  214. this._addImg([urls[0], urls[1][cursor + this.adv]]);
  215. }
  216. imgpv.childNodes[cursor - 1].style.display = "none";
  217. imgpv.childNodes[cursor].style.display = "block";
  218. } else if (e.deltaY < 0 && cursor > 0) {
  219. cursor--;
  220. imgpv.childNodes[cursor + 1].style.display = "none";
  221. imgpv.childNodes[cursor].style.display = "block";
  222. }
  223. under.style.width = `${cursor / (urls[1].length - 1) * 100}%`;
  224. count.innerText = `${cursor + 1}/${urls[1].length}`;
  225. dl.dataset.src = urls[2][cursor];
  226. }
  227. under.style.width = `${cursor / (urls[1].length - 1) * 100}%`;
  228.  
  229. // 提前載入圖片
  230. for(let i = 0; i <= this.adv && i < urls[1].length; i++) {
  231. this._addImg([urls[0], urls[1][i]]);
  232. }
  233.  
  234. // 點擊右區塊下一張圖片
  235. next.addEventListener("click", function(e) {
  236. e.deltaY = 100
  237. changImg(e)
  238. });
  239. // 右區塊滾輪移動監聽器
  240. next.addEventListener("mousewheel", changImg);
  241.  
  242. // 點擊左區塊上一張圖片
  243. prev.addEventListener("click", function(e) {
  244. e.deltaY = -100
  245. changImg(e)
  246. });
  247. // 左區塊滾輪移動監聽器
  248. prev.addEventListener("mousewheel", changImg);
  249. }
  250. // [插入動圖方法]
  251. previewbox.prototype._addAm = function() {
  252. let canvas = document.createElement("canvas");
  253. let resizeBtn = document.createElement("span");
  254. let isLoading = document.createElement("span");
  255. let dl = this.box.querySelector(".dl"); // 下載
  256. let count = this.box.querySelector(".count"); // 頁數
  257. let under = this.box.querySelector(".under"); // 進度條
  258.  
  259. this.box.querySelector(".next").style.display = "none"; // 下一張按鈕
  260. this.box.querySelector(".prev").style.display = "none"; // 上一張按鈕
  261. this.box.querySelector(".imgpv").appendChild(canvas);
  262. this.box.querySelector(".imgpv").appendChild(resizeBtn);
  263. this.box.querySelector(".imgpv").appendChild(isLoading);
  264.  
  265. // 抓取第一張動圖設定畫布寬高,如果動圖下載超過五秒則暫停計時,顯示重製大小按鈕
  266. const resize = () => {
  267. let time = 0
  268. resizeBtn.style.display = "none";
  269. const t = window.setInterval(function () {
  270. time += 100
  271. if (zp?._frameImages?.length) {
  272. canvas.width = zp._frameImages[0]?.width || canvas.width;
  273. canvas.height = zp._frameImages[0]?.height || canvas.width;
  274. isLoading.style.display = "none";
  275. canvas.style.display = "block"
  276. window.clearInterval(t);
  277. } else if (time >= 5000) {
  278. resizeBtn.style.display = "block"
  279. canvas.style.display = "block"
  280. window.clearInterval(t);
  281. }
  282. }, 100);
  283. }
  284.  
  285. canvas.style.display = "none";
  286. canvas.style.maxWidth = `${this.size}px`;
  287. canvas.style.maxHeight = `${this.size}px`;
  288. canvas.parentNode.classList.add("am");
  289. canvas.addEventListener("click", () => {
  290. if (zp._paused) {
  291. zp.play()
  292. } else {
  293. zp._paused = true
  294. }
  295. })
  296.  
  297. resizeBtn.style.display = "none";
  298. resizeBtn.style.cursor = "pointer";
  299. resizeBtn.style.position = "absolute";
  300. resizeBtn.innerText = "重製動圖大小"
  301. resizeBtn.addEventListener("click", resize)
  302.  
  303. isLoading.innerText = "動圖載入中..."
  304.  
  305. fetch(`https://www.pixiv.net/ajax/illust/${this.pid}/ugoira_meta`).then(function(response) {
  306. if (!response.ok) throw new Error(response.statusText);
  307. return response.json();
  308. }).then(function(json) {
  309. let options = {
  310. canvas: canvas,
  311. source: { 0: json.body.originalSrc, 1: json.body.src }[QUALITY_AM],
  312. metadata: json.body,
  313. chunkSize: 300000,
  314. loop: true,
  315. };
  316.  
  317. zp = new ZipImagePlayer(options);
  318. resize()
  319.  
  320. document.addEventListener("frame", function(e) {
  321. under.style.width = `${e.detail.frame / e.detail.count * 100}%`;
  322. count.innerText = `${e.detail.frame}/${e.detail.count}`;
  323. });
  324.  
  325. // 動畫下載按鈕
  326. dl.dataset.src = json.body.originalSrc; // json.body.src(小圖) or json.body.originalSrc(原圖)
  327. dl.addEventListener("click", function(e) {
  328. const filename = e.target.dataset.src.match(/[0-9]+_ugoira/g)[0] + ".zip";
  329. this._downloadImg(e.target.dataset.src, filename);
  330. }.bind(this));
  331. }.bind(this)).catch(function(err) {
  332. console.log("抓取動圖資料發生問題:", err);
  333. })
  334. }
  335. // [設定標籤]
  336. previewbox.prototype._addTags = function(tags, isRestrict, isAI) {
  337. const restrict_html = this.box.querySelector(".restrict")
  338. const tags_html = this.box.querySelector(".tags")
  339. const fixex_html = this.box.querySelector(".fixex")
  340.  
  341. // 是否為 AI 生成
  342. const ai_tag = isAI ? "<span class='ai'>AI</span>" : "";
  343. // 是否為限制級
  344. const r18_tag = {
  345. 0: "",
  346. 1: "<span class='r18'>R-18</span>",
  347. 2: "<span class='r18'>R-18G</span>",
  348. }[isRestrict];
  349.  
  350. // 檢查一般標籤中的限制級標籤
  351. let title = ""
  352. const normal_tags = tags.map((tag) => {
  353. title += `${tag.tag} `
  354. if (tag.tag.match(/R-18G|R-18|R18|R18G/g)) {
  355. return `<span class='r18'>${tag.tag}</span>`;
  356. } else {
  357. return tag.tag;
  358. }
  359. }).join(", ");
  360.  
  361. restrict_html.innerHTML = `${r18_tag} ${ai_tag}`;
  362. tags_html.innerHTML = normal_tags;
  363. tags_html.title = title;
  364. }
  365. // [取得圖片ID]
  366. previewbox.prototype._getId = function() {
  367. if (this.target.nodeName == "A") {
  368. // return /illust_id=([0-9]+)/g.exec(this.target.href)[1];
  369. return /([0-9]+)/g.exec(this.target.href)[1];
  370. } else if (this.target.nodeName == "IMG") {
  371. return /\/img\/.+\/([0-9]+)/g.exec(this.target.src)[1];
  372. } else if (this.target.nodeName == "DIV") {
  373. return /\/img\/.+\/([0-9]+)/g.exec(this.target.style.backgroundImage)[1];
  374. }
  375. }
  376. // [取得圖片連結]
  377. previewbox.prototype._getImgurls = function(urls, count) {
  378. let urls_preview = [];
  379. let urls_original = [];
  380. let quality = { 0: "original", 1: "regular", 2: "small" }[QUALITY];
  381. for(let i = 0; i < count; i++) {
  382. urls_preview.push(urls[quality].replace(/p0/, `p${i}`));
  383. urls_original.push(urls.original.replace(/p0/, `p${i}`));
  384. }
  385. return [`https://www.pixiv.net/member_illust.php?mode=medium&illust_id=${this.pid}`, urls_preview, urls_original];
  386. }
  387. // [抓取圖片資料]
  388. // https://www.pixiv.net/ajax/illust/:illustId
  389. // https://www.pixiv.net/ajax/illust/:illustId/pages
  390. // https://www.pixiv.net/ajax/tags/illust/:illustId
  391. // https://www.pixiv.net/ajax/user/:userId
  392. previewbox.prototype._getData = function(fun) {
  393. fetch(`https://www.pixiv.net/ajax/illust/${this.pid}`).then(function(response) {
  394. if (!response.ok) throw new Error(response.statusText);
  395. return response.json();
  396. }).then(fun).catch(function(err) {
  397. console.log("抓取圖片資料發生問題:", err)
  398. })
  399. }
  400. // [下載圖片]
  401. previewbox.prototype._downloadImg = function(url, filename) {
  402. GM_xmlhttpRequest({
  403. method: "GET",
  404. url: url,
  405. headers: { referer: "https://www.pixiv.net/" },
  406. responseType: "blob",
  407. onload: function ({response}) {
  408. const blobUrl = window.URL.createObjectURL(response);
  409. const a = document.createElement("a");
  410. a.download = filename;
  411. a.href = blobUrl;
  412. a.click();
  413. window.URL.revokeObjectURL(blobUrl);
  414. },
  415. });
  416. }
  417.  
  418. // ========== pixiv zipplayer ========== //
  419. // 原始碼: https://github.com/pixiv/zip_player
  420. function ZipImagePlayer(options) {
  421. this._Uint8Array = (window.Uint8Array || window.WebKitUint8Array || window.MozUint8Array || window.MSUint8Array);
  422. this._DataView = (window.DataView || window.WebKitDataView || window.MozDataView || window.MSDataView);
  423. this._ArrayBuffer = (window.ArrayBuffer || window.WebKitArrayBuffer || window.MozArrayBuffer || window.MSArrayBuffer);
  424. this.op = options;
  425. this._loadingState = 0;
  426. this._context = options.canvas.getContext("2d");
  427. this._files = {};
  428. this._frameCount = this.op.metadata.frames.length;
  429. this._frame = 0;
  430. this._loadFrame = 0;
  431. this._frameImages = [];
  432. this._paused = false;
  433. this._loadTimer = null;
  434. this._startLoad();
  435. this._loadXhr = null;
  436. }
  437. ZipImagePlayer.prototype = {
  438. _trailerBytes: 30000,
  439. _load: function(offset, length, callback) {
  440. var _this = this;
  441. var xhr = new XMLHttpRequest();
  442. xhr.addEventListener("load", function(ev) {
  443. if (!zp) return;
  444. if (xhr.status == 200) {
  445. offset = 0;
  446. length = xhr.response.byteLength;
  447. _this._len = length;
  448. _this._buf = xhr.response;
  449. _this._bytes = new _this._Uint8Array(_this._buf);
  450. } else {
  451. _this._bytes.set(new _this._Uint8Array(xhr.response), offset);
  452. }
  453. if (callback) {
  454. callback.apply(_this, [offset, length]);
  455. }
  456. }, false);
  457. xhr.open("GET", this.op.source);
  458. xhr.responseType = "arraybuffer";
  459. if (offset != null && length != null) {
  460. var end = offset + length;
  461. xhr.setRequestHeader("Range", "bytes=" + offset + "-" + (end - 1));
  462. if (this._isSafari) {
  463. xhr.setRequestHeader("Cache-control", "no-cache");
  464. xhr.setRequestHeader("If-None-Match", Math.random().toString());
  465. }
  466. }
  467. this._loadXhr = xhr;
  468. this._loadXhr.send();
  469. },
  470. _startLoad: function() {
  471. var _this = this;
  472. var http = new XMLHttpRequest();
  473. http.open('HEAD', this.op.source);
  474. http.onreadystatechange = function() {
  475. if (this.readyState == this.DONE) {
  476. _this._pHead = 0;
  477. _this._pNextHead = 0;
  478. _this._pFetch = 0;
  479. var len = parseInt(this.getResponseHeader("Content-Length"));
  480. _this._len = len;
  481. _this._buf = new _this._ArrayBuffer(len);
  482. _this._bytes = new _this._Uint8Array(_this._buf);
  483. var off = len - _this._trailerBytes;
  484. if (off < 0) {
  485. off = 0;
  486. }
  487. _this._pTail = len;
  488. _this._load(off, len - off, function(off, len) {
  489. _this._pTail = off;
  490. _this._findCentralDirectory();
  491. });
  492. }
  493. };
  494. http.send();
  495. },
  496. _findCentralDirectory: function() {
  497. var dv = new this._DataView(this._buf, this._len - 22, 22);
  498. var cd_count = dv.getUint16(10, true);
  499. var cd_size = dv.getUint32(12, true);
  500. var cd_off = dv.getUint32(16, true);
  501. if (cd_off < this._pTail) {
  502. this._load(cd_off, this._pTail - cd_off, function() {
  503. this._pTail = cd_off;
  504. this._readCentralDirectory(cd_off, cd_size, cd_count);
  505. });
  506. } else {
  507. this._readCentralDirectory(cd_off, cd_size, cd_count);
  508. }
  509. },
  510. _readCentralDirectory: function(offset, size, count) {
  511. var dv = new this._DataView(this._buf, offset, size);
  512. var p = 0;
  513. for (var i = 0; i < count; i++ ) {
  514. var compMethod = dv.getUint16(p + 10, true);
  515. var uncompSize = dv.getUint32(p + 24, true);
  516. var nameLen = dv.getUint16(p + 28, true);
  517. var extraLen = dv.getUint16(p + 30, true);
  518. var cmtLen = dv.getUint16(p + 32, true);
  519. var off = dv.getUint32(p + 42, true);
  520. p += 46;
  521. var nameView = new this._Uint8Array(this._buf, offset + p, nameLen);
  522. var name = "";
  523. for (var j = 0; j < nameLen; j++) {
  524. name += String.fromCharCode(nameView[j]);
  525. }
  526. p += nameLen + extraLen + cmtLen;
  527. this._files[name] = {off: off, len: uncompSize};
  528. }
  529. if (this._pHead >= this._pTail) {
  530. this._pHead = this._len;
  531. document.dispatchEvent(new CustomEvent("loadProgress", {detail: this._pHead / this._len}));
  532. this._loadNextFrame();
  533. } else {
  534. this._loadNextChunk();
  535. this._loadNextChunk();
  536. }
  537. },
  538. _loadNextChunk: function() {
  539. if (this._pFetch >= this._pTail) {
  540. return;
  541. }
  542. var off = this._pFetch;
  543. var len = this.op.chunkSize;
  544. if (this._pFetch + len > this._pTail) {
  545. len = this._pTail - this._pFetch;
  546. }
  547. this._pFetch += len;
  548. this._load(off, len, function() {
  549. if (off == this._pHead) {
  550. if (this._pNextHead) {
  551. this._pHead = this._pNextHead;
  552. this._pNextHead = 0;
  553. } else {
  554. this._pHead = off + len;
  555. }
  556. if (this._pHead >= this._pTail) {
  557. this._pHead = this._len;
  558. }
  559. document.dispatchEvent(new CustomEvent("loadProgress", {detail: this._pHead / this._len}));
  560. if (!this._loadTimer) {
  561. this._loadNextFrame();
  562. }
  563. } else {
  564. this._pNextHead = off + len;
  565. }
  566. this._loadNextChunk();
  567. });
  568. },
  569. _fileDataStart: function(offset) {
  570. var dv = new DataView(this._buf, offset, 30);
  571. var nameLen = dv.getUint16(26, true);
  572. var extraLen = dv.getUint16(28, true);
  573. return offset + 30 + nameLen + extraLen;
  574. },
  575. _isFileAvailable: function(name) {
  576. var info = this._files[name];
  577. if (!info) {
  578. this._error("File " + name + " not found in ZIP");
  579. }
  580. if (this._pHead < (info.off + 30)) {
  581. return false;
  582. }
  583. return this._pHead >= (this._fileDataStart(info.off) + info.len);
  584. },
  585. _loadNextFrame: function() {
  586. if (this._dead) {
  587. return;
  588. }
  589. var frame = this._loadFrame;
  590. if (frame >= this._frameCount) {
  591. return;
  592. }
  593. var meta = this.op.metadata.frames[frame];
  594. if (!this.op.source) {
  595. // Unpacked mode (individiual frame URLs)
  596. this._loadFrame += 1;
  597. this._loadImage(frame, meta.file, false);
  598. return;
  599. }
  600. if (!this._isFileAvailable(meta.file)) {
  601. return;
  602. }
  603. this._loadFrame += 1;
  604. var off = this._fileDataStart(this._files[meta.file].off);
  605. var end = off + this._files[meta.file].len;
  606. var url;
  607. var mime_type = this.op.metadata.mime_type || "image/png";
  608. if (this._URL) {
  609. var slice;
  610. if (!this._buf.slice) {
  611. slice = new this._ArrayBuffer(this._files[meta.file].len);
  612. var view = new this._Uint8Array(slice);
  613. view.set(this._bytes.subarray(off, end));
  614. } else {
  615. slice = this._buf.slice(off, end);
  616. }
  617. var blob;
  618. try {
  619. blob = new this._Blob([slice], {type: mime_type});
  620. }
  621. catch (err) {
  622. var bb = new this._BlobBuilder();
  623. bb.append(slice);
  624. blob = bb.getBlob();
  625. }
  626. url = this._URL.createObjectURL(blob);
  627. this._loadImage(frame, url, true);
  628. } else {
  629. url = ("data:" + mime_type + ";base64,"
  630. + base64ArrayBuffer(this._buf, off, end - off));
  631. this._loadImage(frame, url, false);
  632. }
  633. },
  634. _loadImage: function(frame, url, isBlob) {
  635. var _this = this;
  636. var image = new Image();
  637. var meta = this.op.metadata.frames[frame];
  638. image.addEventListener('load', function() {
  639. if (isBlob) {
  640. _this._URL.revokeObjectURL(url);
  641. }
  642. if (_this._dead) {
  643. return;
  644. }
  645. _this._frameImages[frame] = image;
  646. document.dispatchEvent(new CustomEvent("frameLoaded", [frame]));
  647. if (_this._loadingState == 0) {
  648. _this._displayFrame.apply(_this);
  649. }
  650. if (frame >= (_this._frameCount - 1)) {
  651. _this._setLoadingState(2);
  652. _this._buf = null;
  653. _this._bytes = null;
  654. } else {
  655. if (!_this._maxLoadAhead ||
  656. (frame - _this._frame) < _this._maxLoadAhead) {
  657. _this._loadNextFrame();
  658. } else if (!_this._loadTimer) {
  659. _this._loadTimer = setTimeout(function() {
  660. _this._loadTimer = null;
  661. _this._loadNextFrame();
  662. }, 200);
  663. }
  664. }
  665. });
  666. image.src = url;
  667. },
  668. _setLoadingState: function(state) {
  669. if (this._loadingState != state) {
  670. this._loadingState = state;
  671. document.dispatchEvent(new CustomEvent("loadingStateChanged", [state]));
  672. }
  673. },
  674. _displayFrame: function() {
  675. if (this._dead) {
  676. return;
  677. }
  678. var _this = this;
  679. var meta = this.op.metadata.frames[this._frame];
  680. var image = this._frameImages[this._frame];
  681. if (!image) {
  682. this._setLoadingState(0);
  683. return;
  684. }
  685. if (this._loadingState != 2) {
  686. this._setLoadingState(1);
  687. }
  688. if (this.op.autosize) {
  689. if (this._context.canvas.width != image.width || this._context.canvas.height != image.height) {
  690. // make the canvas autosize itself according to the images drawn on it
  691. // should set it once, since we don't have variable sized frames
  692. this._context.canvas.width = image.width;
  693. this._context.canvas.height = image.height;
  694. }
  695. };
  696. this._context.clearRect(0, 0, this.op.canvas.width,
  697. this.op.canvas.height);
  698. this._context.drawImage(image, 0, 0);
  699. document.dispatchEvent(new CustomEvent("frame", { detail: { frame: this._frame, count: this._frameCount } }));
  700. if (!this._paused) {
  701. this._timer = setTimeout(function() {
  702. _this._timer = null;
  703. _this._nextFrame.apply(_this);
  704. }, meta.delay);
  705. }
  706. },
  707. _nextFrame: function(frame) {
  708. if (this._frame >= (this._frameCount - 1)) {
  709. if (this.op.loop) {
  710. this._frame = 0;
  711. } else {
  712. this.pause();
  713. return;
  714. }
  715. } else {
  716. this._frame += 1;
  717. }
  718. this._displayFrame();
  719. },
  720. play: function() {
  721. if (this._dead) {
  722. return;
  723. }
  724. if (this._paused) {
  725. document.dispatchEvent(new CustomEvent("play", [this._frame]));
  726. this._paused = false;
  727. this._displayFrame();
  728. }
  729. },
  730. getCurrentFrame: function() {
  731. return this._frame;
  732. },
  733. getLoadedFrames: function() {
  734. return this._frameImages.length;
  735. },
  736. getFrameCount: function() {
  737. return this._frameCount;
  738. },
  739. loadAbort: function() {
  740. this._loadXhr.abort()
  741. this._loadXhr = null;
  742. }
  743. }
  744. function base64ArrayBuffer(arrayBuffer, off, byteLength) {
  745. var base64 = '';
  746. var encodings = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
  747. var bytes = new Uint8Array(arrayBuffer);
  748. var byteRemainder = byteLength % 3;
  749. var mainLength = off + byteLength - byteRemainder;
  750. var a, b, c, d;
  751. var chunk;
  752. // Main loop deals with bytes in chunks of 3
  753. for (var i = off; i < mainLength; i = i + 3) {
  754. // Combine the three bytes into a single integer
  755. chunk = (bytes[i] << 16) | (bytes[i + 1] << 8) | bytes[i + 2];
  756. // Use bitmasks to extract 6-bit segments from the triplet
  757. a = (chunk & 16515072) >> 18; // 16515072 = (2^6 - 1) << 18
  758. b = (chunk & 258048) >> 12; // 258048 = (2^6 - 1) << 12
  759. c = (chunk & 4032) >> 6; // 4032 = (2^6 - 1) << 6
  760. d = chunk & 63; // 63 = 2^6 - 1
  761. // Convert the raw binary segments to the appropriate ASCII encoding
  762. base64 += encodings[a] + encodings[b] + encodings[c] + encodings[d];
  763. }
  764. // Deal with the remaining bytes and padding
  765. if (byteRemainder == 1) {
  766. chunk = bytes[mainLength];
  767. a = (chunk & 252) >> 2; // 252 = (2^6 - 1) << 2
  768. // Set the 4 least significant bits to zero
  769. b = (chunk & 3) << 4; // 3 = 2^2 - 1
  770. base64 += encodings[a] + encodings[b] + '==';
  771. } else if (byteRemainder == 2) {
  772. chunk = (bytes[mainLength] << 8) | bytes[mainLength + 1];
  773. a = (chunk & 64512) >> 10; // 64512 = (2^6 - 1) << 10
  774. b = (chunk & 1008) >> 4; // 1008 = (2^6 - 1) << 4
  775. // Set the 2 least significant bits to zero
  776. c = (chunk & 15) << 2; // 15 = 2^4 - 1
  777. base64 += encodings[a] + encodings[b] + encodings[c] + '=';
  778. }
  779. return base64;
  780. }
  781. })();

QingJ © 2025

镜像随时可能失效,请加Q群300939539或关注我们的公众号极客氢云获取最新地址