Skip to content

feat(animation): chromatic_aberration, a red/cyan fringe on any node - #354

Merged
LeadcodeDev merged 2 commits into
mainfrom
feat/chromatic-aberration-effect
Sep 26, 2026
Merged

LeadcodeDev merged 2 commits into
mainfrom
feat/chromatic-aberration-effect

Conversation

@LeadcodeDev

Copy link
Copy Markdown
Owner

Refs #344 — its point 4.

chromatic_wipe splits the red and cyan channels, but only as a scene transition. A reference video shows an icon card arriving with channel separation — a per-element effect.

{ "style": { "animation": [{ "name": "chromatic_aberration", "amount": 6, "duration": 0.6 }] } }

amount (px, default 6) is the peak separation, reached at delay, decaying via easing (default ease_out) to exactly 0 by delay + duration — verified pixel-identical to no effect at all at that point, so no node is left permanently fringed.

How it is drawn

It builds a single Skia ImageFilter: the node's own layer, shifted and colour-matrixed to isolate red one way and cyan the other, summed with BlendMode::Plus. That filter is composed with any existing style.filter and fed into the same save_layer that already handles opacity and filters — so it inherits that layer's bleed-bounds logic (extended by the current shift) for free. No new layer, no manual pixel buffers.

active_chromatic_aberration mirrors active_shimmer's existing read-off-css.animation pattern.

A deliberate departure from chromatic_wipe

The isolated channels are paired as {R} vs {G+B}, not chromatic_wipe's {R} vs {B}. A naive red-vs-blue split gives red and yellow edges; pairing green with blue gives red and cyan, which is what the issue's own wording asks for. Left edge reads pure red, right edge pure cyan.

The curve differs too, and on purpose: chromatic_wipe is a symmetric tent, zero at both ends, because a transition must not leave a fringe bleeding into the next scene. This is an arrival — maximum at the start, zero at the end.

Two edits outside the brief, both forced

Flagging these rather than burying them:

  • schema/video.rs — AnimationEffect, the #[serde(tag = "name")] enum every style.animation entry parses through, lives here and not in schema/animation.rs. The JSON in the issue cannot deserialize without a variant here. One import, one variant, one shift_delay arm, same shape as the neighbouring Shimmer/Glow.
  • cli/commands/validate_schema.rs — the new variant made entrance_budget's exhaustive match an E0004. The fix is not merely a compile fix, it is the right semantic: a scene shorter than delay + duration now errors with "animation finishes at 0.60s ... but scene_duration is 0.30s — it will be truncated", which extends the "nothing left fringed" guarantee to validation time. Verified with a real rustmotion validate run.

Verification

9 tests: 3 serde (including deny_unknown_fields and the exact JSON from the issue), 3 on the shift curve, 3 pixel-level in paint_order_tests.

Proven to catch the absence — forcing the filter to None:

chromatic_aberration_shows_a_red_fringe_on_one_edge_and_a_cyan_fringe_on_the_other
panicked: expected a red-leaning fringe on the left edge mid-flight,
got (255, 255, 255)

cargo fmt --all --check, cargo clippy --workspace --all-targets -- -D warnings, cargo test --workspace (1534) all clean.

Written comment-free, per the codebase-wide rule from #345.

@LeadcodeDev LeadcodeDev added the enhancement New feature or request label Sep 26, 2026
@LeadcodeDev LeadcodeDev self-assigned this Sep 26, 2026
@LeadcodeDev
LeadcodeDev force-pushed the feat/chromatic-aberration-effect branch from 0520670 to f581931 Compare September 26, 2026 23:24
chromatic_wipe already splits red/cyan channels, but only as a scene
transition composited on two finished frame-buffers. Issue #344 point 4
asks for the same fringe on a single arriving element (an icon card
glitching into place), which a transition can't express since nothing
in it survives past the cut.

Add `chromatic_aberration` as a node-level animation effect: `amount`
sets the peak channel separation in px, reached the instant the effect
starts, decaying to exactly zero by the end of `duration` — mirroring
chromatic_wipe's own "zero at the edges" guarantee so no element is
left permanently fringed. Unlike the transition's symmetric tent curve
(zero at both ends, since it has to hand off cleanly to the next
scene), this is an arrival effect: max at the start, settled by the
end.

Implementation composes two `ImageFilter`s on the node's own paint
layer instead of hand-rolling pixel buffers like the transition does:
each is the node's content shifted a few px and colour-matrixed down
to just its red or cyan channels, then summed with `BlendMode::Plus`.
Where both copies overlap the sum reconstructs the original colour
exactly; at the edges, where a shifted sample lands outside the node's
own painted content, only one channel survives and the fringe shows.
This reuses the exact machinery `style.filter` (blur, drop-shadow...)
already has for composing an image filter onto a node's save_layer,
including its bleed-expansion of the layer bounds, rather than adding
a second parallel path.

Necessary edits outside the originally scoped file set: `AnimationEffect`
(schema/video.rs) is the single tag="name" enum every `style.animation`
entry parses through, so the new variant has to live there for the JSON
shape to deserialize at all — `ChromaticAberrationConfig` itself lives in
schema/animation.rs as scoped. That in turn made the exhaustive match in
validate_schema.rs's `entrance_budget` non-exhaustive; giving it its own
arm was the right call anyway, not just a compile fix — it's exactly the
kind of one-shot effect that check exists for, and it now catches a scene
too short to let the fringe fully decay before the cut.
@LeadcodeDev
LeadcodeDev force-pushed the feat/chromatic-aberration-effect branch from f581931 to ba30be7 Compare September 26, 2026 23:31
@LeadcodeDev
LeadcodeDev merged commit 4a41ef1 into main Sep 26, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant