Skip to content

feat(core): fade any clip in and out with data-fade-in / data-fade-out - #3350

Open
miguel-heygen wants to merge 1 commit into
mainfrom
core/clip-fades
Open

feat(core): fade any clip in and out with data-fade-in / data-fade-out#3350
miguel-heygen wants to merge 1 commit into
mainfrom
core/clip-fades

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

What

Any clip can now declare its own fades, and the runtime plays them:

<video
  class="clip"
  data-start="0" data-duration="4"
  data-fade-in="0.5"
  data-fade-out="0.75"
  data-fade-curve="smooth"
></video>

Three curves — linear (default), smooth (eases both ends), sharp (holds,
then moves late). Either end works on its own.

Targets main directly: it is the framework half of Studio's fade handles, and
useful on its own to anyone writing a composition by hand.

Why

A fade had to be animated: an opacity tween positioned against the clip's own
start and duration, which then had to be kept in sync every time the clip moved
or was trimmed. Declaring it instead means the fade follows the clip — trim it,
slide it, ripple the track, and the fade is still a fade.

It also gives Studio's fade grips something to write for visual clips. Audio
already had somewhere to put a fade (data-automation); nothing else did.

How

  • filter: opacity(), not opacity. Opacity is the property animation
    engines drive, and a runtime that rewrites it every frame fights them for it —
    the repo has scar tissue from exactly that. A filter multiplies with whatever
    they set instead, so a fade composes with an author's opacity tween rather
    than racing it.
  • The authored filter is preserved. It is captured on first touch and the
    fade is composed onto it (blur(2px)blur(2px) opacity(0.5)), then
    restored exactly whenever the clip is not attenuating — including the frames
    where it is outside its own window. A clip that is not fading carries only
    what its author gave it.
  • One window, two readers. isTimedElementVisibleAt is split into a
    resolveTimedElementWindow that both visibility and the fade read, because a
    fade running on a different window than the clip is visible for is a fade that
    clips or hangs.
  • Fades longer than the clip share it in proportion rather than overlapping
    into a dip, so a clip trimmed shorter than its own fades still resolves to a
    clean in and out. A clip with no end can only fade in.
  • The per-frame path gates on two hasAttribute calls before parsing anything;
    it runs for every timed element on every frame and almost none declare a fade.

Test plan

  • Unit tests added/updated — 15 covering parsing (missing, partial, bad
    values, unknown curve), the three curve shapes, level at every interesting
    point of the window, the proportional split, the unbounded window, and
    filter composition.

  • Manual testing performed — seeked a real preview and read each element's
    computed inline filter back out of the page. clip-a is 0-8s with a 2s
    smooth fade at each end; clip-b starts at 8s with a 1s fade and an
    authored blur(2px):

    t clip-a clip-b
    0 opacity(0) blur(2px)
    0.5 opacity(0.1563) blur(2px)
    1 opacity(0.5) blur(2px)
    2 – 4 (no filter) blur(2px)
    7 opacity(0.5) blur(2px)
    8 (no filter) blur(2px) opacity(0)
    8.5 (no filter) blur(2px) opacity(0.5)
    9 (no filter) blur(2px)

    0.1563 is smoothstep(0.25), so the curve is the one it claims to be; the
    authored blur survives throughout and is what remains once each clip is past
    its fade.

  • Full packages/core suite green (2443 tests), typecheck, oxlint, oxfmt.

Not covered

  • Audio. A fade on a sound is volume automation, it already has
    data-automation to live in, and putting it there keeps it editable as
    breakpoints rather than as one number. A <video> with data-fade-in fades
    the picture; ducking its sound is the envelope's job. Folding the clip fade
    into media gain would mean matching it in the producer's PCM bake too, or
    preview and render would disagree — worth doing, worth doing separately.
  • Lint rule and docs. No hyperframes lint rule for a fade longer than its
    clip (the runtime resolves it sensibly), and the attribute is not in the docs
    site or the skills yet. Both follow once the shape is agreed.

A fade had to be animated: an opacity tween positioned against the clip's
own start and duration, which then had to be kept in sync every time the
clip moved or was trimmed. This makes it declarative — the author says how
long the fade lasts and the runtime attenuates the clip across that stretch
of its own window, so the fade follows the clip wherever it goes.

    <video class="clip" data-start="0" data-duration="4"
           data-fade-in="0.5" data-fade-out="0.75" data-fade-curve="smooth">

Applied as `filter: opacity()`, not `opacity`. Opacity is the property
animation engines drive, and a runtime that rewrites it every frame fights
them for it; a filter multiplies with whatever they set instead. The
element's authored filter is captured on first touch and composed onto, and
restored exactly whenever the clip is not attenuating — so a clip outside
its fades carries only what its author gave it.

Three curves: linear, smooth (eases both ends) and sharp. Fades too long for
the clip share it in proportion rather than overlapping into a dip, so a
trimmed clip still resolves to a clean in and out.

Visual only. A fade on a sound is volume automation, it already has
`data-automation` to live in, and putting it there keeps it editable as
breakpoints instead of as one number.
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