Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
27 changes: 25 additions & 2 deletions src/animation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Frame> {
/** How many times to repeat the animation (-1 = forever, 0 = play once) */
Expand Down Expand Up @@ -99,6 +102,24 @@ export class Animation extends Array<Frame> {
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;
Expand Down Expand Up @@ -127,6 +148,8 @@ export class Animation extends Array<Frame> {
/**
* 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;
Expand Down
14 changes: 14 additions & 0 deletions tests/animation.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading