Skip to content

Fast rotation: 8-bit SWAR bilinear sampling - #74

Merged
matmen merged 1 commit into
masterfrom
faster-rotate
Oct 16, 2026
Merged

matmen merged 1 commit into
masterfrom
faster-rotate

Conversation

@matmen

@matmen matmen commented Oct 16, 2026

Copy link
Copy Markdown
Collaborator

Adds RotateOptions.fast, on by default like ResizeOptions.fast. Rotations by angles that aren't multiples of 90° now sample with 8-bit weights on 8-bit premultiplied pixels, so red+blue and green+alpha each share one multiplication. fast: false keeps the floating-point sampling.

Speed (1920×1080 photo rotated by 30°)

fast: false fast (default)
opaque 119 ms 51 ms
with alpha 114 ms 61 ms

How it works (src/util/rotate.ts)

  • Transparent border: The source is copied with a transparent border one pixel wide.
    • Bilinear sampling reads the 4 neighbours of each source position. Neighbours outside the image now land on transparent pixels, so no bounds checks are needed.
    • Edges fade out exactly as before, when out-of-image taps were skipped.
    • The border exists only in the copy; the output size is computed from the original dimensions, as before.
  • Weights: The 4 weights come from 8-bit fractions and are adjusted to sum to exactly 256, so a lane never overflows.
  • Row spans: Each output row only visits the span that maps into the source (span()). At 30°, about 40% of the output is empty corners.
  • Shared helpers: It uses the helpers from src/util/alpha.ts. unpremultiply8 now writes fully transparent pixels directly instead of doing three table lookups; the result is identical.

Precision

Measured over the corpus (1298 images, 4 angles), fast against fast: false:

  • Opaque pixels are within 1. Only 0.5–2% of all values differ.
  • Translucent pixels, including the faded edges of opaque images, differ by at most about 2.5 steps where they show (channel × alpha).
  • Alpha differs by at most 2.
  • tests/rotate.js checks these limits on a photo crop, with and without an alpha gradient.
  • The rotate-45 targets are regenerated. They differ from the old ones by at most 1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ

`RotateOptions.fast` (default true): angles that aren't multiples of 90°
sample 8-bit premultiplied pixels with 8-bit weights, red+blue and
green+alpha sharing a multiplication (src/util/rotate.ts). The source
gets a transparent border of one pixel, so taps outside it need no
checks and edges fade as before; each row only visits the span that hits
the source. unpremultiply8 writes fully transparent pixels directly.

1920x1080 photo by 30°: 119 -> 51 ms (alpha: 114 -> 61 ms). Over the
corpus (4 angles), opaque pixels stay within 1 of `fast: false`, what
shows of translucent ones (also the faded edges) within about 2.5 steps,
alpha within 2. rotate-45 targets regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YJgRK5qpwJEbHPuFv6kgAQ
@matmen
matmen merged commit 0923dda into master Oct 16, 2026
13 checks passed
@matmen
matmen deleted the faster-rotate branch October 16, 2026 19:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant