@lijuhong1981/jsgif 1.2.4 → 1.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +4 -652
- package/package.json +7 -7
- package/src/GIFImageLoader.js +24 -0
- package/src/GIFPlayer.js +653 -0
package/index.js
CHANGED
|
@@ -1,653 +1,5 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import Destroyable from "@lijuhong1981/jsdestroy/src/Destroyable.js";
|
|
4
|
-
import destroyHTMLElement from "@lijuhong1981/jsdestroy/src/destroyHTMLElement.js";
|
|
5
|
-
import destroyObject from "@lijuhong1981/jsdestroy/src/destroyObject.js";
|
|
6
|
-
import Event from "@lijuhong1981/jsevents/src/EventSubscriber.js";
|
|
7
|
-
import fetchArrayBuffer from "@lijuhong1981/jsload/src/fetchArrayBuffer.js";
|
|
1
|
+
import GIFPlayer from "./src/GIFPlayer.js";
|
|
2
|
+
import GIFImageLoader from "./src/GifImageLoader.js";
|
|
8
3
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
let byteArr = [];
|
|
12
|
-
for (let i = 7; i >= 0; i--) {
|
|
13
|
-
byteArr.push(!!(bite & (1 << i)));
|
|
14
|
-
}
|
|
15
|
-
return byteArr;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
// Generic functions
|
|
19
|
-
function bitsToNum(ba) {
|
|
20
|
-
return ba.reduce(function (s, n) {
|
|
21
|
-
return s * 2 + n;
|
|
22
|
-
}, 0);
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
class Stream extends Destroyable {
|
|
26
|
-
constructor(data) {
|
|
27
|
-
super();
|
|
28
|
-
this.data = data;
|
|
29
|
-
this.len = data.length;
|
|
30
|
-
this.pos = 0;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
readByte() {
|
|
34
|
-
if (this.pos >= this.data.length) {
|
|
35
|
-
throw new Error('Attempted to read past end of stream.');
|
|
36
|
-
}
|
|
37
|
-
if (this.data instanceof Uint8Array)
|
|
38
|
-
return this.data[this.pos++];
|
|
39
|
-
else
|
|
40
|
-
return this.data.charCodeAt(this.pos++) & 0xFF;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
readBytes(n) {
|
|
44
|
-
let bytes = [];
|
|
45
|
-
for (let i = 0; i < n; i++) {
|
|
46
|
-
bytes.push(this.readByte());
|
|
47
|
-
}
|
|
48
|
-
return bytes;
|
|
49
|
-
};
|
|
50
|
-
|
|
51
|
-
read(n) {
|
|
52
|
-
let chars = '';
|
|
53
|
-
for (let i = 0; i < n; i++) {
|
|
54
|
-
chars += String.fromCharCode(this.readByte());
|
|
55
|
-
}
|
|
56
|
-
return chars;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
readUnsigned() { // Little-endian.
|
|
60
|
-
let unsigned = this.readBytes(2);
|
|
61
|
-
return (unsigned[1] << 8) + unsigned[0];
|
|
62
|
-
};
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
function lzwDecode(minCodeSize, data) {
|
|
66
|
-
// TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
|
|
67
|
-
let pos = 0; // Maybe this streaming thing should be merged with the Stream?
|
|
68
|
-
|
|
69
|
-
function readCode(size) {
|
|
70
|
-
let code = 0;
|
|
71
|
-
for (let i = 0; i < size; i++) {
|
|
72
|
-
if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
|
|
73
|
-
code |= 1 << i;
|
|
74
|
-
}
|
|
75
|
-
pos++;
|
|
76
|
-
}
|
|
77
|
-
return code;
|
|
78
|
-
};
|
|
79
|
-
|
|
80
|
-
let output = [],
|
|
81
|
-
clearCode = 1 << minCodeSize,
|
|
82
|
-
eoiCode = clearCode + 1,
|
|
83
|
-
codeSize = minCodeSize + 1,
|
|
84
|
-
dict = [];
|
|
85
|
-
|
|
86
|
-
function clear() {
|
|
87
|
-
dict = [];
|
|
88
|
-
codeSize = minCodeSize + 1;
|
|
89
|
-
for (let i = 0; i < clearCode; i++) {
|
|
90
|
-
dict[i] = [i];
|
|
91
|
-
}
|
|
92
|
-
dict[clearCode] = [];
|
|
93
|
-
dict[eoiCode] = null;
|
|
94
|
-
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
let code = null, last = null;
|
|
98
|
-
while (true) {
|
|
99
|
-
last = code;
|
|
100
|
-
code = readCode(codeSize);
|
|
101
|
-
|
|
102
|
-
if (code === clearCode) {
|
|
103
|
-
clear();
|
|
104
|
-
continue;
|
|
105
|
-
}
|
|
106
|
-
if (code === eoiCode) {
|
|
107
|
-
break
|
|
108
|
-
};
|
|
109
|
-
if (code < dict.length) {
|
|
110
|
-
if (last !== clearCode) {
|
|
111
|
-
dict.push(dict[last].concat(dict[code][0]));
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
else {
|
|
115
|
-
if (code !== dict.length) {
|
|
116
|
-
throw new Error('Invalid LZW code.');
|
|
117
|
-
}
|
|
118
|
-
dict.push(dict[last].concat(dict[last][0]));
|
|
119
|
-
}
|
|
120
|
-
output.push.apply(output, dict[code]);
|
|
121
|
-
|
|
122
|
-
if (dict.length === (1 << codeSize) && codeSize < 12) {
|
|
123
|
-
// If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
|
|
124
|
-
codeSize++;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
return output;
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const defaultOptions = {
|
|
131
|
-
autoPlay: true,
|
|
132
|
-
reverse: false,
|
|
133
|
-
timeScale: 1.0,
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
function GifPlayer(options = {}) {
|
|
137
|
-
options = Object.assign({}, defaultOptions, options);
|
|
138
|
-
console.log('options:', options);
|
|
139
|
-
const CANVAS = document.createElement("canvas");
|
|
140
|
-
const CANVAS_CTX = CANVAS.getContext('2d');
|
|
141
|
-
const TEMP_CANVAS = document.createElement("canvas");//用来拿 imageData 的工具人
|
|
142
|
-
const TEMP_CANVAS_CTX = TEMP_CANVAS.getContext('2d', { willReadFrequently: true, }); // 工具人的 getContext('2d')
|
|
143
|
-
const ATLAS_CANVAS = document.createElement("canvas");
|
|
144
|
-
const FRAME_LIST = []; // 存放每一帧数据以及对应的延时
|
|
145
|
-
let GIF_INFO = {}; // GIF 的一些信息
|
|
146
|
-
let STREAM = null;
|
|
147
|
-
let LAST_DISPOSA_METHOD = null;
|
|
148
|
-
let CURRENT_FRAME_INDEX = -1; //当前帧的下标
|
|
149
|
-
let TRANSPARENCY = null;
|
|
150
|
-
let isPlaying = false;
|
|
151
|
-
let currentFrameIndex = 0;
|
|
152
|
-
const frameChanged = new Event();
|
|
153
|
-
const player = {};
|
|
154
|
-
|
|
155
|
-
function reset() {
|
|
156
|
-
FRAME_LIST.length = 0;
|
|
157
|
-
GIF_INFO = {};
|
|
158
|
-
LAST_DISPOSA_METHOD = null;
|
|
159
|
-
CURRENT_FRAME_INDEX = -1;
|
|
160
|
-
TRANSPARENCY = null;
|
|
161
|
-
STREAM && STREAM.destroy();
|
|
162
|
-
};
|
|
163
|
-
|
|
164
|
-
function readSubBlocks() {
|
|
165
|
-
let size = null, data = '';
|
|
166
|
-
do {
|
|
167
|
-
size = STREAM.readByte();
|
|
168
|
-
data += STREAM.read(size);
|
|
169
|
-
} while (size !== 0);
|
|
170
|
-
return data;
|
|
171
|
-
};
|
|
172
|
-
|
|
173
|
-
function doImg(img) {
|
|
174
|
-
// if (!TEMP_CANVAS_CTX) {
|
|
175
|
-
// // 没有就创建
|
|
176
|
-
// TEMP_CANVAS_CTX = TEMP_CANVAS.getContext('2d');
|
|
177
|
-
// }
|
|
178
|
-
const currIdx = FRAME_LIST.length,
|
|
179
|
-
ct = img.lctFlag ? img.lct : GIF_INFO.gct;
|
|
180
|
-
if (currIdx > 0) {
|
|
181
|
-
// 这块不要动
|
|
182
|
-
if (LAST_DISPOSA_METHOD === 3) {
|
|
183
|
-
// Restore to previous
|
|
184
|
-
// If we disposed every TEMP_CANVAS_CTX including first TEMP_CANVAS_CTX up to this point, then we have
|
|
185
|
-
// no composited TEMP_CANVAS_CTX to restore to. In this case, restore to background instead.
|
|
186
|
-
if (CURRENT_FRAME_INDEX > -1) {
|
|
187
|
-
TEMP_CANVAS_CTX.putImageData(FRAME_LIST[CURRENT_FRAME_INDEX].data, 0, 0);
|
|
188
|
-
} else {
|
|
189
|
-
TEMP_CANVAS_CTX.clearRect(0, 0, TEMP_CANVAS.width, TEMP_CANVAS.height);
|
|
190
|
-
}
|
|
191
|
-
} else {
|
|
192
|
-
CURRENT_FRAME_INDEX = currIdx - 1;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
if (LAST_DISPOSA_METHOD === 2) {
|
|
196
|
-
// Restore to background color
|
|
197
|
-
// Browser implementations historically restore to transparent; we do the same.
|
|
198
|
-
// http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
|
|
199
|
-
TEMP_CANVAS_CTX.clearRect(0, 0, TEMP_CANVAS.width, TEMP_CANVAS.height);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
let imgData = TEMP_CANVAS_CTX.getImageData(img.leftPos, img.topPos, img.width, img.height);
|
|
203
|
-
img.pixels.forEach(function (pixel, i) {
|
|
204
|
-
if (pixel !== TRANSPARENCY) {
|
|
205
|
-
imgData.data[i * 4 + 0] = ct[pixel][0];
|
|
206
|
-
imgData.data[i * 4 + 1] = ct[pixel][1];
|
|
207
|
-
imgData.data[i * 4 + 2] = ct[pixel][2];
|
|
208
|
-
imgData.data[i * 4 + 3] = 255; // Opaque.
|
|
209
|
-
}
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
TEMP_CANVAS_CTX.putImageData(imgData, img.leftPos, img.topPos);
|
|
213
|
-
|
|
214
|
-
if (FRAME_LIST[CURRENT_FRAME_INDEX]) {
|
|
215
|
-
FRAME_LIST[CURRENT_FRAME_INDEX].imgData = imgData;
|
|
216
|
-
FRAME_LIST[CURRENT_FRAME_INDEX].leftPos = img.leftPos;
|
|
217
|
-
FRAME_LIST[CURRENT_FRAME_INDEX].topPos = img.topPos;
|
|
218
|
-
// console.log(CURRENT_FRAME_INDEX, FRAME_LIST[CURRENT_FRAME_INDEX]);
|
|
219
|
-
}
|
|
220
|
-
};
|
|
221
|
-
|
|
222
|
-
function pushFrame(block) {
|
|
223
|
-
// console.log('putFrame', block);
|
|
224
|
-
// if (!TEMP_CANVAS_CTX) {
|
|
225
|
-
// return
|
|
226
|
-
// };
|
|
227
|
-
FRAME_LIST.push({ delayTime: block.delayTime });
|
|
228
|
-
};
|
|
229
|
-
|
|
230
|
-
// 解析
|
|
231
|
-
function parseExt(block) {
|
|
232
|
-
|
|
233
|
-
function parseGCExt(block) {
|
|
234
|
-
STREAM.readByte(); // Always 4 这个必须得这样执行一次
|
|
235
|
-
var bits = byteToBitArr(STREAM.readByte());
|
|
236
|
-
block.reserved = bits.splice(0, 3); // Reserved; should be 000.
|
|
237
|
-
|
|
238
|
-
block.disposalMethod = bitsToNum(bits.splice(0, 3));
|
|
239
|
-
LAST_DISPOSA_METHOD = block.disposalMethod
|
|
240
|
-
|
|
241
|
-
block.userInput = bits.shift();
|
|
242
|
-
block.transparencyGiven = bits.shift();
|
|
243
|
-
block.delayTime = STREAM.readUnsigned();
|
|
244
|
-
block.transparencyIndex = STREAM.readByte();
|
|
245
|
-
block.terminator = STREAM.readByte();
|
|
246
|
-
pushFrame(block);
|
|
247
|
-
TRANSPARENCY = block.transparencyGiven ? block.transparencyIndex : null;
|
|
248
|
-
};
|
|
249
|
-
|
|
250
|
-
function parseComExt(block) {
|
|
251
|
-
block.comment = readSubBlocks();
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
function parsePTExt(block) {
|
|
255
|
-
// No one *ever* uses this. If you use it, deal with parsing it yourself.
|
|
256
|
-
STREAM.readByte(); // Always 12 这个必须得这样执行一次
|
|
257
|
-
block.ptHeader = STREAM.readBytes(12);
|
|
258
|
-
block.ptData = readSubBlocks();
|
|
259
|
-
};
|
|
260
|
-
|
|
261
|
-
function parseAppExt(block) {
|
|
262
|
-
var parseNetscapeExt = function (block) {
|
|
263
|
-
STREAM.readByte(); // Always 3 这个必须得这样执行一次
|
|
264
|
-
block.unknown = STREAM.readByte(); // ??? Always 1? What is this?
|
|
265
|
-
block.iterations = STREAM.readUnsigned();
|
|
266
|
-
block.terminator = STREAM.readByte();
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
function parseUnknownAppExt(block) {
|
|
270
|
-
block.appData = readSubBlocks();
|
|
271
|
-
// FIXME: This won't work if a handler wants to match on any identifier.
|
|
272
|
-
// handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
|
|
273
|
-
};
|
|
274
|
-
|
|
275
|
-
STREAM.readByte(); // Always 11 这个必须得这样执行一次
|
|
276
|
-
block.identifier = STREAM.read(8);
|
|
277
|
-
block.authCode = STREAM.read(3);
|
|
278
|
-
switch (block.identifier) {
|
|
279
|
-
case 'NETSCAPE':
|
|
280
|
-
parseNetscapeExt(block);
|
|
281
|
-
break;
|
|
282
|
-
default:
|
|
283
|
-
parseUnknownAppExt(block);
|
|
284
|
-
break;
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
function parseUnknownExt(block) {
|
|
289
|
-
block.data = readSubBlocks();
|
|
290
|
-
};
|
|
291
|
-
|
|
292
|
-
block.label = STREAM.readByte();
|
|
293
|
-
switch (block.label) {
|
|
294
|
-
case 0xF9:
|
|
295
|
-
block.extType = 'gce';
|
|
296
|
-
parseGCExt(block);
|
|
297
|
-
break;
|
|
298
|
-
case 0xFE:
|
|
299
|
-
block.extType = 'com';
|
|
300
|
-
parseComExt(block);
|
|
301
|
-
break;
|
|
302
|
-
case 0x01:
|
|
303
|
-
block.extType = 'pte';
|
|
304
|
-
parsePTExt(block);
|
|
305
|
-
break;
|
|
306
|
-
case 0xFF:
|
|
307
|
-
block.extType = 'app';
|
|
308
|
-
parseAppExt(block);
|
|
309
|
-
break;
|
|
310
|
-
default:
|
|
311
|
-
block.extType = 'unknown';
|
|
312
|
-
parseUnknownExt(block);
|
|
313
|
-
break;
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
// console.log('parseExt:', block);
|
|
317
|
-
};
|
|
318
|
-
|
|
319
|
-
function parseImg(img) {
|
|
320
|
-
|
|
321
|
-
function deinterlace(pixels, width) {
|
|
322
|
-
// Of course this defeats the purpose of interlacing. And it's *probably*
|
|
323
|
-
// the least efficient way it's ever been implemented. But nevertheless...
|
|
324
|
-
let newPixels = new Array(pixels.length);
|
|
325
|
-
const rows = pixels.length / width;
|
|
326
|
-
|
|
327
|
-
function cpRow(toRow, fromRow) {
|
|
328
|
-
const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
|
|
329
|
-
newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
|
|
330
|
-
};
|
|
331
|
-
|
|
332
|
-
// See appendix E.
|
|
333
|
-
const offsets = [0, 4, 2, 1],
|
|
334
|
-
steps = [8, 8, 4, 2];
|
|
335
|
-
|
|
336
|
-
let fromRow = 0;
|
|
337
|
-
for (var pass = 0; pass < 4; pass++) {
|
|
338
|
-
for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
|
|
339
|
-
cpRow(toRow, fromRow)
|
|
340
|
-
fromRow++;
|
|
341
|
-
}
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
return newPixels;
|
|
345
|
-
};
|
|
346
|
-
|
|
347
|
-
img.leftPos = STREAM.readUnsigned();
|
|
348
|
-
img.topPos = STREAM.readUnsigned();
|
|
349
|
-
img.width = STREAM.readUnsigned();
|
|
350
|
-
img.height = STREAM.readUnsigned();
|
|
351
|
-
|
|
352
|
-
let bits = byteToBitArr(STREAM.readByte());
|
|
353
|
-
img.lctFlag = bits.shift();
|
|
354
|
-
img.interlaced = bits.shift();
|
|
355
|
-
img.sorted = bits.shift();
|
|
356
|
-
img.reserved = bits.splice(0, 2);
|
|
357
|
-
img.lctSize = bitsToNum(bits.splice(0, 3));
|
|
358
|
-
|
|
359
|
-
if (img.lctFlag) {
|
|
360
|
-
img.lct = parseCT(1 << (img.lctSize + 1));
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
img.lzwMinCodeSize = STREAM.readByte();
|
|
364
|
-
|
|
365
|
-
const lzwData = readSubBlocks();
|
|
366
|
-
|
|
367
|
-
img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
|
|
368
|
-
|
|
369
|
-
if (img.interlaced) { // Move
|
|
370
|
-
img.pixels = deinterlace(img.pixels, img.width);
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
doImg(img);
|
|
374
|
-
|
|
375
|
-
// console.log('parseImg', CURRENT_FRAME_INDEX, img);
|
|
376
|
-
};
|
|
377
|
-
|
|
378
|
-
// LZW (GIF-specific)
|
|
379
|
-
function parseCT(entries) { // Each entry is 3 bytes, for RGB.
|
|
380
|
-
let ct = [];
|
|
381
|
-
for (let i = 0; i < entries; i++) {
|
|
382
|
-
ct.push(STREAM.readBytes(3));
|
|
383
|
-
}
|
|
384
|
-
return ct;
|
|
385
|
-
};
|
|
386
|
-
|
|
387
|
-
function parseHeader() {
|
|
388
|
-
GIF_INFO.sig = STREAM.read(3);
|
|
389
|
-
GIF_INFO.ver = STREAM.read(3);
|
|
390
|
-
if (GIF_INFO.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely.
|
|
391
|
-
GIF_INFO.width = STREAM.readUnsigned();
|
|
392
|
-
GIF_INFO.height = STREAM.readUnsigned();
|
|
393
|
-
|
|
394
|
-
let bits = byteToBitArr(STREAM.readByte());
|
|
395
|
-
GIF_INFO.gctFlag = bits.shift();
|
|
396
|
-
GIF_INFO.colorRes = bitsToNum(bits.splice(0, 3));
|
|
397
|
-
GIF_INFO.sorted = bits.shift();
|
|
398
|
-
GIF_INFO.gctSize = bitsToNum(bits.splice(0, 3));
|
|
399
|
-
|
|
400
|
-
GIF_INFO.bgColor = STREAM.readByte();
|
|
401
|
-
GIF_INFO.pixelAspectRatio = STREAM.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
|
|
402
|
-
if (GIF_INFO.gctFlag) {
|
|
403
|
-
GIF_INFO.gct = parseCT(1 << (GIF_INFO.gctSize + 1));
|
|
404
|
-
}
|
|
405
|
-
// console.log('parseHeader:', GIF_INFO);
|
|
406
|
-
|
|
407
|
-
// 给 CANVAS、TEMP_CANVAS 设置大小
|
|
408
|
-
CANVAS.width = TEMP_CANVAS.width = GIF_INFO.width;
|
|
409
|
-
CANVAS.height = TEMP_CANVAS.height = GIF_INFO.height;
|
|
410
|
-
CANVAS.style.width = TEMP_CANVAS.style.width = GIF_INFO.width + 'px';
|
|
411
|
-
CANVAS.style.height = TEMP_CANVAS.style.height = GIF_INFO.height + 'px';
|
|
412
|
-
TEMP_CANVAS_CTX.setTransform(1, 0, 0, 1, 0, 0);
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
function parseBlock() {
|
|
416
|
-
let block = {};
|
|
417
|
-
block.sentinel = STREAM.readByte();
|
|
418
|
-
switch (String.fromCharCode(block.sentinel)) { // For ease of matching
|
|
419
|
-
case '!':
|
|
420
|
-
block.type = 'ext';
|
|
421
|
-
parseExt(block);
|
|
422
|
-
break;
|
|
423
|
-
case ',':
|
|
424
|
-
block.type = 'img';
|
|
425
|
-
parseImg(block);
|
|
426
|
-
break;
|
|
427
|
-
case ';':
|
|
428
|
-
block.type = 'eof';
|
|
429
|
-
// 结束
|
|
430
|
-
break;
|
|
431
|
-
default:
|
|
432
|
-
throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// 递归
|
|
436
|
-
if (block.type !== 'eof') {
|
|
437
|
-
parseBlock();
|
|
438
|
-
}
|
|
439
|
-
};
|
|
440
|
-
|
|
441
|
-
function drawAtlasImage() {
|
|
442
|
-
const length = FRAME_LIST.length;
|
|
443
|
-
if (length > 0) {
|
|
444
|
-
ATLAS_CANVAS.width = CANVAS.width * length;
|
|
445
|
-
ATLAS_CANVAS.height = CANVAS.height;
|
|
446
|
-
const context = ATLAS_CANVAS.getContext('2d');
|
|
447
|
-
let dx = 0;
|
|
448
|
-
for (let i = 0; i < length; i++) {
|
|
449
|
-
const frame = FRAME_LIST[i];
|
|
450
|
-
context.putImageData(frame.imgData, dx, 0);
|
|
451
|
-
dx += CANVAS.width;
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
function getNextFrameIndex() {
|
|
457
|
-
if (currentFrameIndex === -1)
|
|
458
|
-
return options.reverse ? FRAME_LIST.length - 1 : 0;
|
|
459
|
-
const delta = (options.reverse ? -1 : 1);
|
|
460
|
-
return (currentFrameIndex + delta + FRAME_LIST.length) % FRAME_LIST.length;
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
function drawFrame(frame) {
|
|
464
|
-
TEMP_CANVAS_CTX.putImageData(frame.imgData, 0, 0);
|
|
465
|
-
CANVAS_CTX.globalCompositeOperation = "copy";
|
|
466
|
-
CANVAS_CTX.drawImage(TEMP_CANVAS, 0, 0);
|
|
467
|
-
}
|
|
468
|
-
|
|
469
|
-
function drawCurrentFrame() {
|
|
470
|
-
const frame = FRAME_LIST[currentFrameIndex];
|
|
471
|
-
drawFrame(frame);
|
|
472
|
-
frameChanged.raiseEvent(currentFrameIndex, frame);
|
|
473
|
-
return frame;
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
function doFrame() {
|
|
477
|
-
const frame = drawCurrentFrame();
|
|
478
|
-
setTimeout(function () {
|
|
479
|
-
if (!isPlaying)
|
|
480
|
-
return;
|
|
481
|
-
currentFrameIndex = getNextFrameIndex();
|
|
482
|
-
doFrame();
|
|
483
|
-
}, frame.delayTime * options.timeScale * 10);
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// 播放gif
|
|
487
|
-
function playGif() {
|
|
488
|
-
if (isPlaying)
|
|
489
|
-
return;
|
|
490
|
-
|
|
491
|
-
isPlaying = true;
|
|
492
|
-
|
|
493
|
-
doFrame();
|
|
494
|
-
|
|
495
|
-
return player;
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
function pauseGif() {
|
|
499
|
-
isPlaying = false;
|
|
500
|
-
|
|
501
|
-
return player;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
function stopGif() {
|
|
505
|
-
isPlaying = false;
|
|
506
|
-
|
|
507
|
-
currentFrameIndex = 0;
|
|
508
|
-
drawCurrentFrame();
|
|
509
|
-
|
|
510
|
-
return player;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
function loadData(data, onLoad, onError) {
|
|
514
|
-
Check.defined('data', data);
|
|
515
|
-
|
|
516
|
-
return new Promise(function (resolve, reject) {
|
|
517
|
-
try {
|
|
518
|
-
if (data instanceof ArrayBuffer)
|
|
519
|
-
data = new Uint8Array(data);
|
|
520
|
-
reset();
|
|
521
|
-
STREAM = new Stream(data);
|
|
522
|
-
parseHeader();
|
|
523
|
-
parseBlock();
|
|
524
|
-
drawAtlasImage();
|
|
525
|
-
if (typeof onLoad === 'function')
|
|
526
|
-
onLoad(player);
|
|
527
|
-
resolve(player);
|
|
528
|
-
stopGif();
|
|
529
|
-
if (options.autoPlay)
|
|
530
|
-
playGif();
|
|
531
|
-
} catch (error) {
|
|
532
|
-
if (typeof onError === 'function')
|
|
533
|
-
onError(error);
|
|
534
|
-
reject(error);
|
|
535
|
-
}
|
|
536
|
-
});
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
// 用xhr请求本地文件
|
|
540
|
-
function loadUrl(url, requestOptions, onLoad, onError) {
|
|
541
|
-
Check.typeOf.string('url', url);
|
|
542
|
-
|
|
543
|
-
return new Promise(function (resolve, reject) {
|
|
544
|
-
|
|
545
|
-
function rejectError(error) {
|
|
546
|
-
if (typeof onError === 'function')
|
|
547
|
-
onError(error);
|
|
548
|
-
reject(error);
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
fetchArrayBuffer(url, requestOptions).then(function (arraybuffer) {
|
|
552
|
-
loadData(arraybuffer, onLoad, onError).then(resolve).catch(rejectError);
|
|
553
|
-
}).catch(rejectError);
|
|
554
|
-
});
|
|
555
|
-
}
|
|
556
|
-
|
|
557
|
-
function destroyGif() {
|
|
558
|
-
frameChanged.clear();
|
|
559
|
-
stopGif();
|
|
560
|
-
destroyHTMLElement(CANVAS);
|
|
561
|
-
destroyHTMLElement(TEMP_CANVAS);
|
|
562
|
-
destroyHTMLElement(ATLAS_CANVAS);
|
|
563
|
-
reset();
|
|
564
|
-
destroyObject(player);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
Object.defineProperties(player, {
|
|
568
|
-
loadData: {
|
|
569
|
-
value: loadData,
|
|
570
|
-
},
|
|
571
|
-
loadUrl: {
|
|
572
|
-
value: loadUrl,
|
|
573
|
-
},
|
|
574
|
-
play: {
|
|
575
|
-
value: playGif,
|
|
576
|
-
},
|
|
577
|
-
pause: {
|
|
578
|
-
value: pauseGif,
|
|
579
|
-
},
|
|
580
|
-
stop: {
|
|
581
|
-
value: stopGif,
|
|
582
|
-
},
|
|
583
|
-
canvas: {
|
|
584
|
-
get: function () {
|
|
585
|
-
return CANVAS;
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
atlasCanvas: {
|
|
589
|
-
get: function () {
|
|
590
|
-
return ATLAS_CANVAS;
|
|
591
|
-
},
|
|
592
|
-
},
|
|
593
|
-
frameWidth: {
|
|
594
|
-
get: function () {
|
|
595
|
-
return CANVAS.width;
|
|
596
|
-
}
|
|
597
|
-
},
|
|
598
|
-
frameHeight: {
|
|
599
|
-
get: function () {
|
|
600
|
-
return CANVAS.height;
|
|
601
|
-
}
|
|
602
|
-
},
|
|
603
|
-
framesNumber: {
|
|
604
|
-
get: function () {
|
|
605
|
-
return FRAME_LIST.length;
|
|
606
|
-
}
|
|
607
|
-
},
|
|
608
|
-
currentFrameIndex: {
|
|
609
|
-
set: function (value) {
|
|
610
|
-
if (!FRAME_LIST[value])
|
|
611
|
-
throw new Error('illegal index: ' + value);
|
|
612
|
-
currentFrameIndex = value;
|
|
613
|
-
},
|
|
614
|
-
get: function () {
|
|
615
|
-
return currentFrameIndex;
|
|
616
|
-
}
|
|
617
|
-
},
|
|
618
|
-
frameList: {
|
|
619
|
-
get: function () {
|
|
620
|
-
return FRAME_LIST;
|
|
621
|
-
}
|
|
622
|
-
},
|
|
623
|
-
reverse: {
|
|
624
|
-
set: function (value) {
|
|
625
|
-
Check.typeOf.bool('reverse', value);
|
|
626
|
-
options.reverse = value;
|
|
627
|
-
},
|
|
628
|
-
get: function () {
|
|
629
|
-
return options.reverse;
|
|
630
|
-
}
|
|
631
|
-
},
|
|
632
|
-
timeScale: {
|
|
633
|
-
set: function (value) {
|
|
634
|
-
Check.typeOf.number.greaterThan('timeScale', value, 0);
|
|
635
|
-
options.timeScale = value;
|
|
636
|
-
},
|
|
637
|
-
get: function () {
|
|
638
|
-
return options.timeScale;
|
|
639
|
-
}
|
|
640
|
-
},
|
|
641
|
-
frameChanged: {
|
|
642
|
-
get: function () {
|
|
643
|
-
return frameChanged;
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
defineDestroyProperties(player, destroyGif);
|
|
649
|
-
|
|
650
|
-
return player;
|
|
651
|
-
};
|
|
652
|
-
|
|
653
|
-
export default GifPlayer;
|
|
4
|
+
export default GIFPlayer;
|
|
5
|
+
export { GIFPlayer, GIFImageLoader };
|
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lijuhong1981/jsgif",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.6",
|
|
4
4
|
"description": "> TODO: description",
|
|
5
|
-
"author": "lijuhong1981",
|
|
5
|
+
"author": "lijuhong1981 <lijuhong@hotmail.com>",
|
|
6
6
|
"homepage": "https://github.com/lijuhong1981/jslibs#readme",
|
|
7
7
|
"license": "ISC",
|
|
8
8
|
"type": "module",
|
|
@@ -18,11 +18,11 @@
|
|
|
18
18
|
"bugs": {
|
|
19
19
|
"url": "https://github.com/lijuhong1981/jslibs/issues"
|
|
20
20
|
},
|
|
21
|
-
"gitHead": "
|
|
21
|
+
"gitHead": "28628d57c44769b5df9acbe19ce8a7f5b07f6b07",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@lijuhong1981/jscheck": "^1.0.
|
|
24
|
-
"@lijuhong1981/jsdestroy": "^1.1.
|
|
25
|
-
"@lijuhong1981/jsevents": "^1.1.
|
|
26
|
-
"@lijuhong1981/jsload": "^1.2.
|
|
23
|
+
"@lijuhong1981/jscheck": "^1.0.10",
|
|
24
|
+
"@lijuhong1981/jsdestroy": "^1.1.10",
|
|
25
|
+
"@lijuhong1981/jsevents": "^1.1.7",
|
|
26
|
+
"@lijuhong1981/jsload": "^1.2.5"
|
|
27
27
|
}
|
|
28
28
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import ImageLoader from "@lijuhong1981/jsload/src/ImageLoader.js";
|
|
2
|
+
import GIFPlayer from "./GIFPlayer.js";
|
|
3
|
+
|
|
4
|
+
class GIFImageLoader extends ImageLoader {
|
|
5
|
+
// constructor(options) {
|
|
6
|
+
// super(options);
|
|
7
|
+
// }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 重写图片加载函数,生成canvas以绘制gif动画
|
|
11
|
+
*/
|
|
12
|
+
onImageLoad(options, onLoad, onError) {
|
|
13
|
+
const player = new GIFPlayer();
|
|
14
|
+
player.loadUrl(options.url, options.requestOptions, function () {
|
|
15
|
+
onLoad(player.canvas);
|
|
16
|
+
}, function (error) {
|
|
17
|
+
onError(error);
|
|
18
|
+
});
|
|
19
|
+
player.canvas.gifPlayer = player;
|
|
20
|
+
return player.canvas;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export default GIFImageLoader;
|
package/src/GIFPlayer.js
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
import Check from "@lijuhong1981/jscheck/src/Check.js";
|
|
2
|
+
import defineDestroyProperties from "@lijuhong1981/jsdestroy/src/defineDestroyProperties.js";
|
|
3
|
+
import Destroyable from "@lijuhong1981/jsdestroy/src/Destroyable.js";
|
|
4
|
+
import destroyHTMLElement from "@lijuhong1981/jsdestroy/src/destroyHTMLElement.js";
|
|
5
|
+
import destroyObject from "@lijuhong1981/jsdestroy/src/destroyObject.js";
|
|
6
|
+
import Event from "@lijuhong1981/jsevents/src/EventSubscriber.js";
|
|
7
|
+
import fetchArrayBuffer from "@lijuhong1981/jsload/src/fetchArrayBuffer.js";
|
|
8
|
+
|
|
9
|
+
// 转流数组
|
|
10
|
+
function byteToBitArr(bite) {
|
|
11
|
+
let byteArr = [];
|
|
12
|
+
for (let i = 7; i >= 0; i--) {
|
|
13
|
+
byteArr.push(!!(bite & (1 << i)));
|
|
14
|
+
}
|
|
15
|
+
return byteArr;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Generic functions
|
|
19
|
+
function bitsToNum(ba) {
|
|
20
|
+
return ba.reduce(function (s, n) {
|
|
21
|
+
return s * 2 + n;
|
|
22
|
+
}, 0);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
class Stream extends Destroyable {
|
|
26
|
+
constructor(data) {
|
|
27
|
+
super();
|
|
28
|
+
this.data = data;
|
|
29
|
+
this.len = data.length;
|
|
30
|
+
this.pos = 0;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
readByte() {
|
|
34
|
+
if (this.pos >= this.data.length) {
|
|
35
|
+
throw new Error('Attempted to read past end of stream.');
|
|
36
|
+
}
|
|
37
|
+
if (this.data instanceof Uint8Array)
|
|
38
|
+
return this.data[this.pos++];
|
|
39
|
+
else
|
|
40
|
+
return this.data.charCodeAt(this.pos++) & 0xFF;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
readBytes(n) {
|
|
44
|
+
let bytes = [];
|
|
45
|
+
for (let i = 0; i < n; i++) {
|
|
46
|
+
bytes.push(this.readByte());
|
|
47
|
+
}
|
|
48
|
+
return bytes;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
read(n) {
|
|
52
|
+
let chars = '';
|
|
53
|
+
for (let i = 0; i < n; i++) {
|
|
54
|
+
chars += String.fromCharCode(this.readByte());
|
|
55
|
+
}
|
|
56
|
+
return chars;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
readUnsigned() { // Little-endian.
|
|
60
|
+
let unsigned = this.readBytes(2);
|
|
61
|
+
return (unsigned[1] << 8) + unsigned[0];
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
function lzwDecode(minCodeSize, data) {
|
|
66
|
+
// TODO: Now that the GIF parser is a bit different, maybe this should get an array of bytes instead of a String?
|
|
67
|
+
let pos = 0; // Maybe this streaming thing should be merged with the Stream?
|
|
68
|
+
|
|
69
|
+
function readCode(size) {
|
|
70
|
+
let code = 0;
|
|
71
|
+
for (let i = 0; i < size; i++) {
|
|
72
|
+
if (data.charCodeAt(pos >> 3) & (1 << (pos & 7))) {
|
|
73
|
+
code |= 1 << i;
|
|
74
|
+
}
|
|
75
|
+
pos++;
|
|
76
|
+
}
|
|
77
|
+
return code;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let output = [],
|
|
81
|
+
clearCode = 1 << minCodeSize,
|
|
82
|
+
eoiCode = clearCode + 1,
|
|
83
|
+
codeSize = minCodeSize + 1,
|
|
84
|
+
dict = [];
|
|
85
|
+
|
|
86
|
+
function clear() {
|
|
87
|
+
dict = [];
|
|
88
|
+
codeSize = minCodeSize + 1;
|
|
89
|
+
for (let i = 0; i < clearCode; i++) {
|
|
90
|
+
dict[i] = [i];
|
|
91
|
+
}
|
|
92
|
+
dict[clearCode] = [];
|
|
93
|
+
dict[eoiCode] = null;
|
|
94
|
+
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
let code = null, last = null;
|
|
98
|
+
while (true) {
|
|
99
|
+
last = code;
|
|
100
|
+
code = readCode(codeSize);
|
|
101
|
+
|
|
102
|
+
if (code === clearCode) {
|
|
103
|
+
clear();
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
if (code === eoiCode) {
|
|
107
|
+
break
|
|
108
|
+
};
|
|
109
|
+
if (code < dict.length) {
|
|
110
|
+
if (last !== clearCode) {
|
|
111
|
+
dict.push(dict[last].concat(dict[code][0]));
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
if (code !== dict.length) {
|
|
116
|
+
throw new Error('Invalid LZW code.');
|
|
117
|
+
}
|
|
118
|
+
dict.push(dict[last].concat(dict[last][0]));
|
|
119
|
+
}
|
|
120
|
+
output.push.apply(output, dict[code]);
|
|
121
|
+
|
|
122
|
+
if (dict.length === (1 << codeSize) && codeSize < 12) {
|
|
123
|
+
// If we're at the last code and codeSize is 12, the next code will be a clearCode, and it'll be 12 bits long.
|
|
124
|
+
codeSize++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
return output;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const defaultOptions = {
|
|
131
|
+
autoPlay: true,
|
|
132
|
+
reverse: false,
|
|
133
|
+
timeScale: 1.0,
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
function GIFPlayer(options = {}) {
|
|
137
|
+
options = Object.assign({}, defaultOptions, options);
|
|
138
|
+
console.log('options:', options);
|
|
139
|
+
const CANVAS = document.createElement("canvas");
|
|
140
|
+
const CANVAS_CTX = CANVAS.getContext('2d');
|
|
141
|
+
const TEMP_CANVAS = document.createElement("canvas");//用来拿 imageData 的工具人
|
|
142
|
+
const TEMP_CANVAS_CTX = TEMP_CANVAS.getContext('2d', { willReadFrequently: true, }); // 工具人的 getContext('2d')
|
|
143
|
+
const ATLAS_CANVAS = document.createElement("canvas");
|
|
144
|
+
const FRAME_LIST = []; // 存放每一帧数据以及对应的延时
|
|
145
|
+
let GIF_INFO = {}; // GIF 的一些信息
|
|
146
|
+
let STREAM = null;
|
|
147
|
+
let LAST_DISPOSA_METHOD = null;
|
|
148
|
+
let CURRENT_FRAME_INDEX = -1; //当前帧的下标
|
|
149
|
+
let TRANSPARENCY = null;
|
|
150
|
+
let isPlaying = false;
|
|
151
|
+
let currentFrameIndex = 0;
|
|
152
|
+
const frameChanged = new Event();
|
|
153
|
+
const player = {};
|
|
154
|
+
|
|
155
|
+
function reset() {
|
|
156
|
+
FRAME_LIST.length = 0;
|
|
157
|
+
GIF_INFO = {};
|
|
158
|
+
LAST_DISPOSA_METHOD = null;
|
|
159
|
+
CURRENT_FRAME_INDEX = -1;
|
|
160
|
+
TRANSPARENCY = null;
|
|
161
|
+
STREAM && STREAM.destroy();
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function readSubBlocks() {
|
|
165
|
+
let size = null, data = '';
|
|
166
|
+
do {
|
|
167
|
+
size = STREAM.readByte();
|
|
168
|
+
data += STREAM.read(size);
|
|
169
|
+
} while (size !== 0);
|
|
170
|
+
return data;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
function doImg(img) {
|
|
174
|
+
// if (!TEMP_CANVAS_CTX) {
|
|
175
|
+
// // 没有就创建
|
|
176
|
+
// TEMP_CANVAS_CTX = TEMP_CANVAS.getContext('2d');
|
|
177
|
+
// }
|
|
178
|
+
const currIdx = FRAME_LIST.length,
|
|
179
|
+
ct = img.lctFlag ? img.lct : GIF_INFO.gct;
|
|
180
|
+
if (currIdx > 0) {
|
|
181
|
+
// 这块不要动
|
|
182
|
+
if (LAST_DISPOSA_METHOD === 3) {
|
|
183
|
+
// Restore to previous
|
|
184
|
+
// If we disposed every TEMP_CANVAS_CTX including first TEMP_CANVAS_CTX up to this point, then we have
|
|
185
|
+
// no composited TEMP_CANVAS_CTX to restore to. In this case, restore to background instead.
|
|
186
|
+
if (CURRENT_FRAME_INDEX > -1) {
|
|
187
|
+
TEMP_CANVAS_CTX.putImageData(FRAME_LIST[CURRENT_FRAME_INDEX].data, 0, 0);
|
|
188
|
+
} else {
|
|
189
|
+
TEMP_CANVAS_CTX.clearRect(0, 0, TEMP_CANVAS.width, TEMP_CANVAS.height);
|
|
190
|
+
}
|
|
191
|
+
} else {
|
|
192
|
+
CURRENT_FRAME_INDEX = currIdx - 1;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (LAST_DISPOSA_METHOD === 2) {
|
|
196
|
+
// Restore to background color
|
|
197
|
+
// Browser implementations historically restore to transparent; we do the same.
|
|
198
|
+
// http://www.wizards-toolkit.org/discourse-server/viewtopic.php?f=1&t=21172#p86079
|
|
199
|
+
TEMP_CANVAS_CTX.clearRect(0, 0, TEMP_CANVAS.width, TEMP_CANVAS.height);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
let imgData = TEMP_CANVAS_CTX.getImageData(img.leftPos, img.topPos, img.width, img.height);
|
|
203
|
+
img.pixels.forEach(function (pixel, i) {
|
|
204
|
+
if (pixel !== TRANSPARENCY) {
|
|
205
|
+
imgData.data[i * 4 + 0] = ct[pixel][0];
|
|
206
|
+
imgData.data[i * 4 + 1] = ct[pixel][1];
|
|
207
|
+
imgData.data[i * 4 + 2] = ct[pixel][2];
|
|
208
|
+
imgData.data[i * 4 + 3] = 255; // Opaque.
|
|
209
|
+
}
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
TEMP_CANVAS_CTX.putImageData(imgData, img.leftPos, img.topPos);
|
|
213
|
+
|
|
214
|
+
if (FRAME_LIST[CURRENT_FRAME_INDEX]) {
|
|
215
|
+
FRAME_LIST[CURRENT_FRAME_INDEX].imgData = imgData;
|
|
216
|
+
FRAME_LIST[CURRENT_FRAME_INDEX].leftPos = img.leftPos;
|
|
217
|
+
FRAME_LIST[CURRENT_FRAME_INDEX].topPos = img.topPos;
|
|
218
|
+
// console.log(CURRENT_FRAME_INDEX, FRAME_LIST[CURRENT_FRAME_INDEX]);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
function pushFrame(block) {
|
|
223
|
+
// console.log('putFrame', block);
|
|
224
|
+
// if (!TEMP_CANVAS_CTX) {
|
|
225
|
+
// return
|
|
226
|
+
// };
|
|
227
|
+
FRAME_LIST.push({ delayTime: block.delayTime });
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
// 解析
|
|
231
|
+
function parseExt(block) {
|
|
232
|
+
|
|
233
|
+
function parseGCExt(block) {
|
|
234
|
+
STREAM.readByte(); // Always 4 这个必须得这样执行一次
|
|
235
|
+
var bits = byteToBitArr(STREAM.readByte());
|
|
236
|
+
block.reserved = bits.splice(0, 3); // Reserved; should be 000.
|
|
237
|
+
|
|
238
|
+
block.disposalMethod = bitsToNum(bits.splice(0, 3));
|
|
239
|
+
LAST_DISPOSA_METHOD = block.disposalMethod
|
|
240
|
+
|
|
241
|
+
block.userInput = bits.shift();
|
|
242
|
+
block.transparencyGiven = bits.shift();
|
|
243
|
+
block.delayTime = STREAM.readUnsigned();
|
|
244
|
+
block.transparencyIndex = STREAM.readByte();
|
|
245
|
+
block.terminator = STREAM.readByte();
|
|
246
|
+
pushFrame(block);
|
|
247
|
+
TRANSPARENCY = block.transparencyGiven ? block.transparencyIndex : null;
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
function parseComExt(block) {
|
|
251
|
+
block.comment = readSubBlocks();
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
function parsePTExt(block) {
|
|
255
|
+
// No one *ever* uses this. If you use it, deal with parsing it yourself.
|
|
256
|
+
STREAM.readByte(); // Always 12 这个必须得这样执行一次
|
|
257
|
+
block.ptHeader = STREAM.readBytes(12);
|
|
258
|
+
block.ptData = readSubBlocks();
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
function parseAppExt(block) {
|
|
262
|
+
var parseNetscapeExt = function (block) {
|
|
263
|
+
STREAM.readByte(); // Always 3 这个必须得这样执行一次
|
|
264
|
+
block.unknown = STREAM.readByte(); // ??? Always 1? What is this?
|
|
265
|
+
block.iterations = STREAM.readUnsigned();
|
|
266
|
+
block.terminator = STREAM.readByte();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
function parseUnknownAppExt(block) {
|
|
270
|
+
block.appData = readSubBlocks();
|
|
271
|
+
// FIXME: This won't work if a handler wants to match on any identifier.
|
|
272
|
+
// handler.app && handler.app[block.identifier] && handler.app[block.identifier](block);
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
STREAM.readByte(); // Always 11 这个必须得这样执行一次
|
|
276
|
+
block.identifier = STREAM.read(8);
|
|
277
|
+
block.authCode = STREAM.read(3);
|
|
278
|
+
switch (block.identifier) {
|
|
279
|
+
case 'NETSCAPE':
|
|
280
|
+
parseNetscapeExt(block);
|
|
281
|
+
break;
|
|
282
|
+
default:
|
|
283
|
+
parseUnknownAppExt(block);
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
function parseUnknownExt(block) {
|
|
289
|
+
block.data = readSubBlocks();
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
block.label = STREAM.readByte();
|
|
293
|
+
switch (block.label) {
|
|
294
|
+
case 0xF9:
|
|
295
|
+
block.extType = 'gce';
|
|
296
|
+
parseGCExt(block);
|
|
297
|
+
break;
|
|
298
|
+
case 0xFE:
|
|
299
|
+
block.extType = 'com';
|
|
300
|
+
parseComExt(block);
|
|
301
|
+
break;
|
|
302
|
+
case 0x01:
|
|
303
|
+
block.extType = 'pte';
|
|
304
|
+
parsePTExt(block);
|
|
305
|
+
break;
|
|
306
|
+
case 0xFF:
|
|
307
|
+
block.extType = 'app';
|
|
308
|
+
parseAppExt(block);
|
|
309
|
+
break;
|
|
310
|
+
default:
|
|
311
|
+
block.extType = 'unknown';
|
|
312
|
+
parseUnknownExt(block);
|
|
313
|
+
break;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// console.log('parseExt:', block);
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
function parseImg(img) {
|
|
320
|
+
|
|
321
|
+
function deinterlace(pixels, width) {
|
|
322
|
+
// Of course this defeats the purpose of interlacing. And it's *probably*
|
|
323
|
+
// the least efficient way it's ever been implemented. But nevertheless...
|
|
324
|
+
let newPixels = new Array(pixels.length);
|
|
325
|
+
const rows = pixels.length / width;
|
|
326
|
+
|
|
327
|
+
function cpRow(toRow, fromRow) {
|
|
328
|
+
const fromPixels = pixels.slice(fromRow * width, (fromRow + 1) * width);
|
|
329
|
+
newPixels.splice.apply(newPixels, [toRow * width, width].concat(fromPixels));
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// See appendix E.
|
|
333
|
+
const offsets = [0, 4, 2, 1],
|
|
334
|
+
steps = [8, 8, 4, 2];
|
|
335
|
+
|
|
336
|
+
let fromRow = 0;
|
|
337
|
+
for (var pass = 0; pass < 4; pass++) {
|
|
338
|
+
for (var toRow = offsets[pass]; toRow < rows; toRow += steps[pass]) {
|
|
339
|
+
cpRow(toRow, fromRow)
|
|
340
|
+
fromRow++;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return newPixels;
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
img.leftPos = STREAM.readUnsigned();
|
|
348
|
+
img.topPos = STREAM.readUnsigned();
|
|
349
|
+
img.width = STREAM.readUnsigned();
|
|
350
|
+
img.height = STREAM.readUnsigned();
|
|
351
|
+
|
|
352
|
+
let bits = byteToBitArr(STREAM.readByte());
|
|
353
|
+
img.lctFlag = bits.shift();
|
|
354
|
+
img.interlaced = bits.shift();
|
|
355
|
+
img.sorted = bits.shift();
|
|
356
|
+
img.reserved = bits.splice(0, 2);
|
|
357
|
+
img.lctSize = bitsToNum(bits.splice(0, 3));
|
|
358
|
+
|
|
359
|
+
if (img.lctFlag) {
|
|
360
|
+
img.lct = parseCT(1 << (img.lctSize + 1));
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
img.lzwMinCodeSize = STREAM.readByte();
|
|
364
|
+
|
|
365
|
+
const lzwData = readSubBlocks();
|
|
366
|
+
|
|
367
|
+
img.pixels = lzwDecode(img.lzwMinCodeSize, lzwData);
|
|
368
|
+
|
|
369
|
+
if (img.interlaced) { // Move
|
|
370
|
+
img.pixels = deinterlace(img.pixels, img.width);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
doImg(img);
|
|
374
|
+
|
|
375
|
+
// console.log('parseImg', CURRENT_FRAME_INDEX, img);
|
|
376
|
+
};
|
|
377
|
+
|
|
378
|
+
// LZW (GIF-specific)
|
|
379
|
+
function parseCT(entries) { // Each entry is 3 bytes, for RGB.
|
|
380
|
+
let ct = [];
|
|
381
|
+
for (let i = 0; i < entries; i++) {
|
|
382
|
+
ct.push(STREAM.readBytes(3));
|
|
383
|
+
}
|
|
384
|
+
return ct;
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
function parseHeader() {
|
|
388
|
+
GIF_INFO.sig = STREAM.read(3);
|
|
389
|
+
GIF_INFO.ver = STREAM.read(3);
|
|
390
|
+
if (GIF_INFO.sig !== 'GIF') throw new Error('Not a GIF file.'); // XXX: This should probably be handled more nicely.
|
|
391
|
+
GIF_INFO.width = STREAM.readUnsigned();
|
|
392
|
+
GIF_INFO.height = STREAM.readUnsigned();
|
|
393
|
+
|
|
394
|
+
let bits = byteToBitArr(STREAM.readByte());
|
|
395
|
+
GIF_INFO.gctFlag = bits.shift();
|
|
396
|
+
GIF_INFO.colorRes = bitsToNum(bits.splice(0, 3));
|
|
397
|
+
GIF_INFO.sorted = bits.shift();
|
|
398
|
+
GIF_INFO.gctSize = bitsToNum(bits.splice(0, 3));
|
|
399
|
+
|
|
400
|
+
GIF_INFO.bgColor = STREAM.readByte();
|
|
401
|
+
GIF_INFO.pixelAspectRatio = STREAM.readByte(); // if not 0, aspectRatio = (pixelAspectRatio + 15) / 64
|
|
402
|
+
if (GIF_INFO.gctFlag) {
|
|
403
|
+
GIF_INFO.gct = parseCT(1 << (GIF_INFO.gctSize + 1));
|
|
404
|
+
}
|
|
405
|
+
// console.log('parseHeader:', GIF_INFO);
|
|
406
|
+
|
|
407
|
+
// 给 CANVAS、TEMP_CANVAS 设置大小
|
|
408
|
+
CANVAS.width = TEMP_CANVAS.width = GIF_INFO.width;
|
|
409
|
+
CANVAS.height = TEMP_CANVAS.height = GIF_INFO.height;
|
|
410
|
+
CANVAS.style.width = TEMP_CANVAS.style.width = GIF_INFO.width + 'px';
|
|
411
|
+
CANVAS.style.height = TEMP_CANVAS.style.height = GIF_INFO.height + 'px';
|
|
412
|
+
TEMP_CANVAS_CTX.setTransform(1, 0, 0, 1, 0, 0);
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
function parseBlock() {
|
|
416
|
+
let block = {};
|
|
417
|
+
block.sentinel = STREAM.readByte();
|
|
418
|
+
switch (String.fromCharCode(block.sentinel)) { // For ease of matching
|
|
419
|
+
case '!':
|
|
420
|
+
block.type = 'ext';
|
|
421
|
+
parseExt(block);
|
|
422
|
+
break;
|
|
423
|
+
case ',':
|
|
424
|
+
block.type = 'img';
|
|
425
|
+
parseImg(block);
|
|
426
|
+
break;
|
|
427
|
+
case ';':
|
|
428
|
+
block.type = 'eof';
|
|
429
|
+
// 结束
|
|
430
|
+
break;
|
|
431
|
+
default:
|
|
432
|
+
throw new Error('Unknown block: 0x' + block.sentinel.toString(16)); // TODO: Pad this with a 0.
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// 递归
|
|
436
|
+
if (block.type !== 'eof') {
|
|
437
|
+
parseBlock();
|
|
438
|
+
}
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
function drawAtlasImage() {
|
|
442
|
+
const length = FRAME_LIST.length;
|
|
443
|
+
if (length > 0) {
|
|
444
|
+
ATLAS_CANVAS.width = CANVAS.width * length;
|
|
445
|
+
ATLAS_CANVAS.height = CANVAS.height;
|
|
446
|
+
const context = ATLAS_CANVAS.getContext('2d');
|
|
447
|
+
let dx = 0;
|
|
448
|
+
for (let i = 0; i < length; i++) {
|
|
449
|
+
const frame = FRAME_LIST[i];
|
|
450
|
+
context.putImageData(frame.imgData, dx, 0);
|
|
451
|
+
dx += CANVAS.width;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function getNextFrameIndex() {
|
|
457
|
+
if (currentFrameIndex === -1)
|
|
458
|
+
return options.reverse ? FRAME_LIST.length - 1 : 0;
|
|
459
|
+
const delta = (options.reverse ? -1 : 1);
|
|
460
|
+
return (currentFrameIndex + delta + FRAME_LIST.length) % FRAME_LIST.length;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function drawFrame(frame) {
|
|
464
|
+
TEMP_CANVAS_CTX.putImageData(frame.imgData, 0, 0);
|
|
465
|
+
CANVAS_CTX.globalCompositeOperation = "copy";
|
|
466
|
+
CANVAS_CTX.drawImage(TEMP_CANVAS, 0, 0);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function drawCurrentFrame() {
|
|
470
|
+
const frame = FRAME_LIST[currentFrameIndex];
|
|
471
|
+
drawFrame(frame);
|
|
472
|
+
frameChanged.raiseEvent(currentFrameIndex, frame);
|
|
473
|
+
return frame;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function doFrame() {
|
|
477
|
+
const frame = drawCurrentFrame();
|
|
478
|
+
setTimeout(function () {
|
|
479
|
+
if (!isPlaying)
|
|
480
|
+
return;
|
|
481
|
+
currentFrameIndex = getNextFrameIndex();
|
|
482
|
+
doFrame();
|
|
483
|
+
}, frame.delayTime * options.timeScale * 10);
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// 播放gif
|
|
487
|
+
function playGif() {
|
|
488
|
+
if (isPlaying)
|
|
489
|
+
return;
|
|
490
|
+
|
|
491
|
+
isPlaying = true;
|
|
492
|
+
|
|
493
|
+
doFrame();
|
|
494
|
+
|
|
495
|
+
return player;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function pauseGif() {
|
|
499
|
+
isPlaying = false;
|
|
500
|
+
|
|
501
|
+
return player;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function stopGif() {
|
|
505
|
+
isPlaying = false;
|
|
506
|
+
|
|
507
|
+
currentFrameIndex = 0;
|
|
508
|
+
drawCurrentFrame();
|
|
509
|
+
|
|
510
|
+
return player;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function loadData(data, onLoad, onError) {
|
|
514
|
+
Check.defined('data', data);
|
|
515
|
+
|
|
516
|
+
return new Promise(function (resolve, reject) {
|
|
517
|
+
try {
|
|
518
|
+
if (data instanceof ArrayBuffer)
|
|
519
|
+
data = new Uint8Array(data);
|
|
520
|
+
reset();
|
|
521
|
+
STREAM = new Stream(data);
|
|
522
|
+
parseHeader();
|
|
523
|
+
parseBlock();
|
|
524
|
+
drawAtlasImage();
|
|
525
|
+
if (typeof onLoad === 'function')
|
|
526
|
+
onLoad(player);
|
|
527
|
+
resolve(player);
|
|
528
|
+
stopGif();
|
|
529
|
+
if (options.autoPlay)
|
|
530
|
+
playGif();
|
|
531
|
+
} catch (error) {
|
|
532
|
+
if (typeof onError === 'function')
|
|
533
|
+
onError(error);
|
|
534
|
+
reject(error);
|
|
535
|
+
}
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// 用xhr请求本地文件
|
|
540
|
+
function loadUrl(url, requestOptions, onLoad, onError) {
|
|
541
|
+
Check.typeOf.string('url', url);
|
|
542
|
+
|
|
543
|
+
return new Promise(function (resolve, reject) {
|
|
544
|
+
|
|
545
|
+
function rejectError(error) {
|
|
546
|
+
if (typeof onError === 'function')
|
|
547
|
+
onError(error);
|
|
548
|
+
reject(error);
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
fetchArrayBuffer(url, requestOptions).then(function (arraybuffer) {
|
|
552
|
+
loadData(arraybuffer, onLoad, onError).then(resolve).catch(rejectError);
|
|
553
|
+
}).catch(rejectError);
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function destroyGif() {
|
|
558
|
+
frameChanged.clear();
|
|
559
|
+
stopGif();
|
|
560
|
+
destroyHTMLElement(CANVAS);
|
|
561
|
+
destroyHTMLElement(TEMP_CANVAS);
|
|
562
|
+
destroyHTMLElement(ATLAS_CANVAS);
|
|
563
|
+
reset();
|
|
564
|
+
destroyObject(player);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
Object.defineProperties(player, {
|
|
568
|
+
loadData: {
|
|
569
|
+
value: loadData,
|
|
570
|
+
},
|
|
571
|
+
loadUrl: {
|
|
572
|
+
value: loadUrl,
|
|
573
|
+
},
|
|
574
|
+
play: {
|
|
575
|
+
value: playGif,
|
|
576
|
+
},
|
|
577
|
+
pause: {
|
|
578
|
+
value: pauseGif,
|
|
579
|
+
},
|
|
580
|
+
stop: {
|
|
581
|
+
value: stopGif,
|
|
582
|
+
},
|
|
583
|
+
canvas: {
|
|
584
|
+
get: function () {
|
|
585
|
+
return CANVAS;
|
|
586
|
+
},
|
|
587
|
+
},
|
|
588
|
+
atlasCanvas: {
|
|
589
|
+
get: function () {
|
|
590
|
+
return ATLAS_CANVAS;
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
frameWidth: {
|
|
594
|
+
get: function () {
|
|
595
|
+
return CANVAS.width;
|
|
596
|
+
}
|
|
597
|
+
},
|
|
598
|
+
frameHeight: {
|
|
599
|
+
get: function () {
|
|
600
|
+
return CANVAS.height;
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
framesNumber: {
|
|
604
|
+
get: function () {
|
|
605
|
+
return FRAME_LIST.length;
|
|
606
|
+
}
|
|
607
|
+
},
|
|
608
|
+
currentFrameIndex: {
|
|
609
|
+
set: function (value) {
|
|
610
|
+
if (!FRAME_LIST[value])
|
|
611
|
+
throw new Error('illegal index: ' + value);
|
|
612
|
+
currentFrameIndex = value;
|
|
613
|
+
},
|
|
614
|
+
get: function () {
|
|
615
|
+
return currentFrameIndex;
|
|
616
|
+
}
|
|
617
|
+
},
|
|
618
|
+
frameList: {
|
|
619
|
+
get: function () {
|
|
620
|
+
return FRAME_LIST;
|
|
621
|
+
}
|
|
622
|
+
},
|
|
623
|
+
reverse: {
|
|
624
|
+
set: function (value) {
|
|
625
|
+
Check.typeOf.bool('reverse', value);
|
|
626
|
+
options.reverse = value;
|
|
627
|
+
},
|
|
628
|
+
get: function () {
|
|
629
|
+
return options.reverse;
|
|
630
|
+
}
|
|
631
|
+
},
|
|
632
|
+
timeScale: {
|
|
633
|
+
set: function (value) {
|
|
634
|
+
Check.typeOf.number.greaterThan('timeScale', value, 0);
|
|
635
|
+
options.timeScale = value;
|
|
636
|
+
},
|
|
637
|
+
get: function () {
|
|
638
|
+
return options.timeScale;
|
|
639
|
+
}
|
|
640
|
+
},
|
|
641
|
+
frameChanged: {
|
|
642
|
+
get: function () {
|
|
643
|
+
return frameChanged;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
defineDestroyProperties(player, destroyGif);
|
|
649
|
+
|
|
650
|
+
return player;
|
|
651
|
+
};
|
|
652
|
+
|
|
653
|
+
export default GIFPlayer;
|