Skip to content

Commit 48665d0

Browse files
committed
document the frame and animation size changes and the dither defaults
1 parent 2180106 commit 48665d0

2 files changed

Lines changed: 52 additions & 12 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
### Breaking changes
66

7-
Upgrading from 3.x? Upgrade to the latest 3.x release first, fix the deprecation hints, then switch: the [migration guide](https://imagescript.dev/documents/Migrating_to_4.0.html) lists every change and the seven behavior changes that don't show up as deprecations or compile errors.
7+
Upgrading from 3.x? Upgrade to the latest 3.x release first, fix the deprecation hints, then switch: the [migration guide](https://imagescript.dev/documents/Migrating_to_4.0.html) lists every change and the nine behavior changes that don't show up as deprecations or compile errors.
88

99
- **Methods that take options refuse anything else than an object**: a stray number, string, boolean or `null` as the options of `cropCircle()`, `Image.renderText()`, the encoders (`encodeWebP()`, `encodeAVIF()`, `encodeJXL()`, ...), `quantize()`, `transform()`, `dropShadow()`, `outline()`, the gradients, `Image.decode()`, the animation encoders, and `new GainMap()` throws a `TypeError` that names the working form (e.g. `cropCircle({ size: 'cover' })`), so the 3.x calls that are gone like `cropCircle(true)` or `Image.renderText(font, 32, 'Hi')` fail instead of being ignored. Only the type of the argument is checked: a key that isn't an option is still ignored, so a typo like `{ maxWith: 300 }` isn't caught.
1010
- **The mode constants are removed, modes are plain strings**: `Image.RESIZE_*`, `Image.SVG_MODE_*`, `Frame.DISPOSAL_*` and the `ResizeModes`, `SVGScaleModes` and `FrameDisposalModes` classes are gone. Resize modes are `'nearest-neighbor'`, `'bilinear'`, `'bicubic'` and `'area'` (the `ResizeMode` type), disposal modes `'any'`, `'keep'`, `'previous'` and `'background'` (`FrameDisposalModeName`), and the SVG modes became options of `renderSVG()` (below). `Image.RESIZE_AUTO`/`ResizeModes.AUTO` is `image.resize({ width: 256 })`. The old values are no longer accepted either: `'RESIZE_BILINEAR'`, the numbers 1-3 for SVG scale modes and the numeric disposal ids 0-3.
@@ -67,10 +67,12 @@ Upgrading from 3.x? Upgrade to the latest 3.x release first, fix the deprecation
6767

6868
- **`encodeJPEG()` composites transparent pixels onto a background**, black by default (`background` sets the color), instead of dropping the alpha and showing the stored color. Opaque images encode as before.
6969

70-
- **`Frame` holds an image instead of extending `Image`**: `frame.image` is the image, `new Frame(image, options)` takes it (without a copy), and `Frame.from()` is removed. `resize()`, `scale()`, `crop()` and `trim()` stay on the frame and move its offsets; everything else is `frame.image.blur(2)` and the like, and leaves the offsets alone (`transform()`, `dropShadow()` and `outline()` no longer move them).
70+
- **`Frame` holds an image instead of extending `Image`**: `frame.image` is the image, `new Frame(image, options)` takes it (without a copy), and `Frame.from()` is removed. The methods that change the size (`resize()`, `scale()`, `contain()`, `cover()`, `crop()`, `trim()`, `rotate()`, `transform()`, `dropShadow()` and `outline()`) stay on the frame and move its offsets so it stays in place; everything else is `frame.image.blur(2)` and the like. `frame.crop()` and `frame.rotate()` now move the offsets too (3.x kept them), `frame.cover()` moves them by the cropped amount, and the offsets can become negative where 3.x stopped the growth at 0.
7171

7272
- **GIF palettes take a color count**: the `quality` of `Animation.encodeGIF()` and `Animation.gifEncoder()` is `colors` (2-256, default 256, which `quality` 90 and above gave), with the new `dither` and `ditherStrength`. Their options type is `GIFAnimationEncodeOptions`; `GIFEncodeOptions` is now the options of `Image.encodeGIF()`.
7373

74+
- **`quantize()` dithers by default**: Floyd-Steinberg at a strength of `0.85`, like the GIF encoders (3.x: no dithering, and a strength of `1` when dithering). `dither: 'none'` gives the old default.
75+
7476
- **Removed**: `getRGBAAt()` (use `Colors.toRGBA(image.getPixelAt(x, y))`).
7577

7678
- **Renamed**: `greyscale()` is `grayscale()` (American spelling throughout), `region()` is `extract()`, the `resize` option of `rotate()` is `expand` (like `transform()`, `outline()` and `dropShadow()`), `GainMap`'s `gainMapMin` and `gainMapMax` (properties and options) are `min` and `max`, and the parameters of `setPixelAt()`, `lightness()` and `saturation()` are `color` and `factor`.
@@ -89,6 +91,8 @@ Upgrading from 3.x? Upgrade to the latest 3.x release first, fix the deprecation
8991
- `resize({ width, height, fit: 'inside' })` on images, frames and animations: as large as fits inside the box, keeping the aspect ratio.
9092
- `encodeJPEG({ subsampling: '4:4:4' | '4:2:0' })`; the default stays 4:2:0 up to quality 90 and 4:4:4 above.
9193
- `Colors.gradient()` takes an array of evenly spaced colors, like the `stops` of the gradient functions.
94+
- `Animation.scale()`, `Animation.trim()`, `Animation.rotate()`, `Animation.transform()`, `Animation.dropShadow()` and `Animation.outline()`. `scale()` and `trim()` keep the frames; the others work on the composited animation (full canvas frames) and resize the canvas.
95+
- `Frame.contain()`, `Frame.cover()`, `Frame.rotate()`, `Frame.transform()`, `Frame.dropShadow()` and `Frame.outline()`, which move the offsets like `Frame.resize()` and `Frame.crop()`.
9296
- `Image.encodeGIF()` for single images, with `colors`, `dither` and `ditherStrength` (also on the animation encoders).
9397
- `renderSVG({ fonts })` takes a single font without an array.
9498

‎guides/Migrating to 4.0.md‎

Lines changed: 46 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Migrating to 4.0
22

3-
ImageScript 4.0 removes everything 3.x deprecated, reshapes parts of the API, and changes seven behaviors. Most of the
4-
removals are mechanical: the compiler points at them. The seven behavior changes are not deprecations, nothing warns
3+
ImageScript 4.0 removes everything 3.x deprecated, reshapes parts of the API, and changes nine behaviors. Most of the
4+
removals are mechanical: the compiler points at them. The nine behavior changes are not deprecations, nothing warns
55
about them, and code keeps compiling while it does something else. Read the first section even if your build is clean.
66

77
Node.js 22.13 or newer is required, as it was for 3.x (`engines` in `package.json`).
@@ -13,7 +13,7 @@ The recommended path:
1313
problems list, or a lint rule for deprecated APIs (typescript-eslint has `no-deprecated`), to get the full list.
1414
2. Fix every deprecation hint, using the tables below.
1515
3. Switch to 4.0, fix what the compiler reports with the [renamed and reshaped APIs](#renamed-and-reshaped-apis), and go
16-
through the seven behavior changes. The [checklist](#checklist) at the end has grep patterns
16+
through the nine behavior changes. The [checklist](#checklist) at the end has grep patterns
1717
for finding the code they affect.
1818

1919
## Behavior changes
@@ -153,6 +153,40 @@ for another color: `image.encodeJPEG({ background: 0xffffffff })`. Opaque images
153153

154154
Search your code for: `encodeJPEG(` on images that can have transparency.
155155

156+
### `quantize()` dithers by default
157+
158+
`image.quantize()` now dithers with Floyd-Steinberg at a strength of `0.85` by default, like the GIF encoders. In 3.x
159+
it didn't dither unless you passed `dither`. For the old result, pass `dither: 'none'`:
160+
161+
| 3.x | 4.0 |
162+
| --- | --- |
163+
| `image.quantize({ colors: 16 })` | `image.quantize({ colors: 16, dither: 'none' })` |
164+
| `image.quantize({ colors: 16, dither: 'floyd-steinberg' })` (strength 1) | `image.quantize({ colors: 16, dither: 'floyd-steinberg', ditherStrength: 1 })` |
165+
166+
The second row matters for every dither method: the default strength is `0.85` now, so pass `ditherStrength: 1` to
167+
keep 3.x's result.
168+
169+
Search your code for: `.quantize(`.
170+
171+
### Frames move their offsets when their size changes
172+
173+
A frame's `crop()`, `cover()` and `rotate()` now move its offsets, so the frame's pixels stay where they are in the
174+
animation. In 3.x, `crop()` and `rotate()` kept the offsets (the top left corner stayed in place), and `cover()` only
175+
scaled them. Now:
176+
177+
- `frame.crop(x, y, w, h)` adds `x` and `y` to the offsets, like `trim()` already did.
178+
- `frame.rotate(angle)` keeps the frame's middle in place.
179+
- `frame.cover(w, h)` scales the offsets and then moves them by the cropped amount.
180+
181+
`transform()`, `dropShadow()` and `outline()` moved the offsets in 3.x too, but only as far as they allowed (a frame at
182+
`xOffset: 0` grew to the right instead). Now the offsets can become negative: the frame grows evenly, and the part
183+
outside the animation's canvas isn't encoded.
184+
185+
To keep a frame's offsets as they are, call the method on its image: `frame.image.crop(x, y, w, h)`. See
186+
[frames hold an image](#frames-hold-an-image) for the other frame changes.
187+
188+
Search your code for: `.crop(`, `.cover(` and `.rotate(` on frames.
189+
156190
## Removed APIs
157191

158192
Each table maps a 3.x API to its 4.0 replacement. The deprecation hint in 3.x names a replacement too, but some of those
@@ -363,11 +397,13 @@ A `Frame` is no longer an `Image`: it holds one, `frame.image`, with the frame's
363397
| `image.composite(frame)`, `frame.encodePNG()` | `image.composite(frame.image)`, `frame.image.encodePNG()` |
364398
| `frame instanceof Image` | `false` now |
365399

366-
`resize()`, `scale()`, `crop()` and `trim()` stay on the frame and move its offsets so its pixels stay where they are in
367-
the animation, and `width`, `height`, `clone()` and `toString()` work as before. Everything else goes through
368-
`frame.image` and leaves the offsets alone. In 3.x, a frame that grew up or left from `transform()`, `dropShadow()` or
369-
`outline()` moved its offsets with the growth (as far as they allowed); now the image grows and the offsets stay, so
370-
move them yourself if the frame's pixels have to stay in place.
400+
The methods that change the size stay on the frame: `resize()`, `scale()`, `contain()`, `cover()`, `crop()`, `trim()`,
401+
`rotate()`, `transform()`, `dropShadow()` and `outline()`. They move the offsets so the frame stays in place in the
402+
animation (resizing scales the offsets, as if the whole animation was resized); see
403+
[frames move their offsets](#frames-move-their-offsets-when-their-size-changes) for how that differs from 3.x.
404+
`width`, `height`, `clone()` and `toString()` work as before. Everything else goes through `frame.image`. The image's
405+
own methods don't know about the frame and leave the offsets alone, so `frame.image.rotate(90)` turns the frame around
406+
its top left corner.
371407

372408
### Checked values and the canvas
373409

@@ -448,6 +484,6 @@ grep -rnE 'gainMap\.encode\(|\.encode\(\s*[0-9]' src
448484
Also search for the variables that hold animations, and for `[0]`, `.map(`, `.filter(` and `.forEach(` on them: the
449485
search can't tell an animation from an array, so check the type of each receiver.
450486

451-
Then compile with `tsc`, and run the tests that cover your decode and encode paths. The seven behavior changes need
487+
Then compile with `tsc`, and run the tests that cover your decode and encode paths. The nine behavior changes need
452488
runtime checks: a file with several frames, a one-frame GIF, any `loopCount: -1`, every `contain()`, every HSL hue,
453-
every `rotate()` and every JPEG made from an image with transparency.
489+
every `rotate()`, every JPEG made from an image with transparency, every `quantize()`, and the size changes of frames.

0 commit comments

Comments
 (0)