diff --git a/CHANGELOG.md b/CHANGELOG.md index 3417c678..3b735b66 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ 54% (WebP) of the bytes in total; the less moves, the smaller a file gets, ones where everything moves stay about the same. Large animations encode faster, small ones up to about twice as slow. The files show the same animation as before, but don't decode to the same frames anymore: `{optimize: false}` stores the frames as they are. +- **`Animation.frames`**: the frames of an animation as an array, to use instead of the animation itself (see + Deprecated). Changing it (`push()`, `splice()`, …) changes the animation, and it can be replaced as a whole. - **Metadata in every format**: `encodeJPEG()`, `encodeWebP()`, `encodeTGA()` and, for animations, `encodeGIF()`, `encodeAPNG()` and `encodeWebP()` take the same metadata as `encode()` (now called `Metadata`), as the `metadata` option: `image.encodeJPEG({quality: 80, metadata: {author: 'me'}})`. Each format stores @@ -24,6 +26,10 @@ ### Deprecated +- **Animations as arrays**: `Animation` (and `GIF`) extend `Array`, so `animation[0]`, `animation.length` and + `animation.map()` work on the frames. In 4.0 they won't extend `Array` anymore, which makes array methods + unambiguous (`map()` can return anything, so it can't return an animation): use `animation.frames`, an array of + the frames that changes the animation when it's changed. - **`PNGMetadata`**: renamed to `Metadata`, the old name stays an alias until 4.0. ### Fixes diff --git a/src/animation.ts b/src/animation.ts index 6199bdac..a3c0b2c1 100644 --- a/src/animation.ts +++ b/src/animation.ts @@ -59,8 +59,11 @@ export interface APNGEncodeOptions extends AnimationEncodeOptions { } /** - * An animation: an array of frames that can be decoded from and encoded to GIF, APNG and WebP. - * Array methods like map and filter return plain arrays. + * An animation: frames ({@link Animation.frames}) that can be decoded from and encoded to GIF, APNG and WebP. + * + * The animation itself is an array of its frames too (`animation[0]`, `animation.map(...)`), which is deprecated: in 4.0 + * it won't extend `Array` anymore, use {@link Animation.frames} instead. Array methods like map and filter return + * plain arrays. */ export class Animation extends Array { /** How many times to repeat the animation (-1 = forever, 0 = play once) */ @@ -99,6 +102,24 @@ export class Animation extends Array { return Array; } + /** + * The animation's frames: an array that changes the animation when it's changed (e.g. `push()`, `splice()`, or + * setting an index), and can be replaced as a whole + */ + get frames(): Frame[] { + return this; + } + + /** Replaces the animation's frames */ + set frames(frames: Frame[]) { + frames = Array.from(frames); + frames.forEach((frame, i) => { + if (!(frame instanceof Frame)) throw new TypeError(`Frame ${i} is not an instance of Frame`); + }); + this.length = 0; + for (const frame of frames) this.push(frame); + } + /** The animation's width */ get width(): number { let max = 0; @@ -127,6 +148,8 @@ export class Animation extends Array { /** * Returns a new animation of the same class containing a range of this one's frames * (same semantics as Array.prototype.slice) + * @deprecated Use `new Animation(animation.frames.slice(start, end), animation.loopCount)`: in 4.0 animations + * aren't arrays anymore */ override slice(start?: number, end?: number): this { const Class = this.constructor as new (frames: Frame[], loopCount: number) => this; diff --git a/tests/animation.js b/tests/animation.js index f69b4d9b..7a8ae074 100644 --- a/tests/animation.js +++ b/tests/animation.js @@ -55,6 +55,20 @@ const square = (color, w, h) => new Image(w, h).fill((x, y) => (x + y) % 5 === 0 if (!(gif instanceof Animation) || gif.length !== 1) panic('Animation.decode should decode GIFs'); if (!(gif.slice(0, 1) instanceof Animation)) panic('slice should keep the class'); + // frames: the animation's frames, changing them changes the animation + { + const animation = new Animation([Frame.from(still)]); + if (animation.frames.length !== 1 || animation.frames[0] !== animation[0]) panic('frames should hold the frames'); + animation.frames.push(Frame.from(still, 50)); + if (animation.length !== 2 || animation.duration !== 150) panic('frames.push should add a frame'); + if (!Array.isArray(animation.frames.map(f => f.duration))) panic('frames.map should return an array'); + animation.frames = animation.frames.filter(f => f.duration === 50); + if (animation.length !== 1 || animation[0].duration !== 50) panic('setting frames should replace them'); + let threw = false; + try { animation.frames = [still]; } catch { threw = true; } + if (!threw || animation.length !== 1) panic('frames should only take frames'); + } + // loop counts: -1 forever, 0 once, n repeats for (const loopCount of [-1, 0, 5]) { // (different frames: identical ones are merged, and a single frame has no loop count)