Skip to content
Open
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
1 change: 1 addition & 0 deletions docs/docs.json
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@
"studio/canvas",
"studio/timeline",
"studio/trim-tools",
"studio/fades",
"studio/animation",
"studio/captions"
]
Expand Down
127 changes: 127 additions & 0 deletions docs/studio/fades.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
---
title: "Fade a clip in and out"
description: "Draw fades on any clip in HyperFrames Studio, bend each ramp by dragging it, and author the same fade by hand."
---

Select a clip and two small square grips appear on its top corners. Drag one
inward and it draws a fade: the clip comes up from nothing at the start, or goes
away to nothing at the end.

<Frame caption="A fade in and a fade out on the same clip. Each grip rides the top of the wedge it drew, and a dot sits on each curve.">
<img
src="https://static.heygen.ai/hyperframes-oss/docs/images/studio/fade-wedges-v1.png"
alt="A Studio timeline clip with a fade in and a fade out drawn on it"
/>
</Frame>

Every clip can fade, not only audio. What "fade" means depends on the clip:
picture fades to nothing, sound fades to silence.

## Draw a fade

Grab the small square on a top corner and drag it towards the middle of the
clip. The wedge follows as you drag, and the fade is written when you let go.
New fades ease out by default, so they move quickly and settle softly into
their end state.

<Frame caption="Dragging the fade-in grip, then the fade-out grip.">
<video
className="aspect-video w-full object-cover"
src="https://static.heygen.ai/hyperframes-oss/docs/images/studio/fade-draw-v1.mp4#t=0.1"
poster="https://static.heygen.ai/hyperframes-oss/docs/images/studio/fade-draw-v1.jpg"
controls
muted
playsInline
loop
/>
</Frame>

Drag a grip back to its corner to remove the fade. The whole gesture is one
history entry, so a single undo takes the fade away.

## Bend each ramp

A straight fade is not always the one you want. Drag the dot sitting on a fade
line to bend it, and the curve follows your pointer.

<Frame caption="The head bent down and the tail bent up. The dot stays under the pointer because the curve is drawn through it.">
<video
className="aspect-video w-full object-cover"
src="https://static.heygen.ai/hyperframes-oss/docs/images/studio/fade-bend-v1.mp4#t=0.1"
poster="https://static.heygen.ai/hyperframes-oss/docs/images/studio/fade-bend-v1.jpg"
controls
muted
playsInline
loop
/>
</Frame>

- **Drag down** and the fade starts slowly and finishes fast. It holds near
nothing, then arrives late. Useful under a voice, where you want the music out
of the way early.
- **Drag up** and the fade starts fast and finishes slowly. It gets most of the
way there immediately, then eases in. The least noticeable fade.
- **Drag back through the middle** and it is exactly straight again.

Each fade has its own dot and its own shape. Bending the one at the start leaves
the one at the end exactly as it was. The dot only appears once there is a fade
to bend; on a clip with none there is no line to pull, so start with the grips.

## Where a fade is stored

A fade is a value that moves across a clip's life, so it lives where every other
moving value on a clip lives: the clip's own automation envelope. There is no
fade attribute to learn. A picture's fade is a lane targeting `opacity`, a
sound's is the `volume` lane it already had.

```html
<div id="title" class="clip" data-start="0" data-duration="8"
data-automation='{"version":1,"lanes":[{"target":"opacity","points":[
{"t":0,"v":0},{"t":1.5,"v":1},{"t":6.8,"v":1},{"t":8,"v":0}]}]}'>
Hello
</div>
```

Four points: dark at 0s, full at 1.5s, holding until 6.8s, dark again at 8s.
Times are the clip's own, not the timeline's, which is what lets a fade survive
being moved or trimmed.

| Field | Meaning |
| --- | --- |
| `target` | `opacity` for picture, `volume` for sound |
| `t` | Seconds from the clip's start |
| `v` | The level at that moment, 0 to 1 |
| `curve` | Optional bend on the segment leaving this point, -1 to 1 |

<Frame caption="The same composition seeked across a hand-written 1.5 second fade in.">
<img
src="https://static.heygen.ai/hyperframes-oss/docs/images/studio/fade-runtime-v1.png"
alt="Four frames showing a clip rising from black to full brightness"
/>
</Frame>

<Note>
The fade is applied as a CSS `filter`, not as `opacity`. Opacity is the
property animation engines drive, so a runtime writing it every frame would
fight your tweens for it. A filter multiplies with whatever they set, and it
composes onto whatever filter you wrote yourself. At full level the clip
carries no fade filter at all.
</Note>

## A fade is just an envelope

Storing it this way means a fade is not a special case, and two useful things
follow.

**You can refine it by hand.** Expand the clip's automation lane and the fade is
there as draggable breakpoints, on a video exactly as on music. The grips and
the lane are two views of one thing.

**You can stop it being a fade.** Drag a third point into the middle and it
becomes an envelope: a dip under a voice, a flicker, a hold. Nothing has to be
migrated, because it was always the same data.

## Related topics

- [Edit timing on the timeline](/studio/timeline)
- [Trim with ripple, roll, slip, and slide](/studio/trim-tools)
1 change: 1 addition & 0 deletions docs/studio/timeline.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ Move, trim, split, and many other timeline actions participate in Studio history
## Related topics

- [Trim with ripple, roll, slip, and slide](/studio/trim-tools)
- [Fade a clip in and out](/studio/fades)
- [Edit the frame](/studio/canvas)
- [Edit animation and keyframes](/studio/animation)
- [Use Studio keyboard shortcuts](/studio/shortcuts)
1 change: 1 addition & 0 deletions docs/studio/trim-tools.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,5 @@ A tool tells you why instead of running a gesture with no effect:
## Related topics

- [Edit timing on the timeline](/studio/timeline)
- [Fade a clip in and out](/studio/fades)
- [Use Studio keyboard shortcuts](/studio/shortcuts)
6 changes: 6 additions & 0 deletions packages/core/package-subpaths.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,12 @@
"types": "./dist/audioAutomation.d.ts",
"environments": ["browser", "bun", "node"]
},
"./clip-fade": {
"source": "./src/clipFade.ts",
"runtime": "./dist/clipFade.js",
"types": "./dist/clipFade.d.ts",
"environments": ["browser", "bun", "node"]
},
"./audio-gain": {
"source": "./src/audioGain.ts",
"runtime": "./dist/audioGain.js",
Expand Down
10 changes: 10 additions & 0 deletions packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,12 @@
"import": "./src/audioAutomation.ts",
"types": "./src/audioAutomation.ts"
},
"./clip-fade": {
"bun": "./src/clipFade.ts",
"node": "./dist/clipFade.js",
"import": "./src/clipFade.ts",
"types": "./src/clipFade.ts"
},
"./audio-gain": {
"bun": "./src/audioGain.ts",
"node": "./dist/audioGain.js",
Expand Down Expand Up @@ -484,6 +490,10 @@
"import": "./dist/audioAutomation.js",
"types": "./dist/audioAutomation.d.ts"
},
"./clip-fade": {
"import": "./dist/clipFade.js",
"types": "./dist/clipFade.d.ts"
},
"./audio-gain": {
"import": "./dist/audioGain.js",
"types": "./dist/audioGain.d.ts"
Expand Down
42 changes: 41 additions & 1 deletion packages/core/src/audioAutomation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,30 @@ export class AudioAutomationError extends Error {

export const VOLUME_TARGET = "volume";

/**
* The picture's level, on the same footing as the sound's.
*
* A fade is a value that moves across a clip's life, which is what this whole
* module is for, so a fade on a picture belongs here rather than in attributes
* of its own. It also means one editor: a fade drawn on a video clip is the
* same two breakpoints, in the same lane UI, as a fade drawn on music.
*
* NOTE: this module is still called audioAutomation and still throws
* AudioAutomationError. The name is now too narrow. Renaming it touches 58
* files, so it is left for a change that does only that.
*/
export const OPACITY_TARGET = "opacity";

export type HfAutomationTarget =
| { kind: "volume" }
| { kind: "opacity" }
| { kind: "fx"; nodeId: string; param: string }
| { kind: "preset"; presetId: string };

/** Split a target string. Returns null for anything unrecognised. */
export function parseAutomationTarget(target: string): HfAutomationTarget | null {
if (target === VOLUME_TARGET) return { kind: "volume" };
if (target === OPACITY_TARGET) return { kind: "opacity" };
const parts = target.split(".");
// `fx.preset.<id>` before the 3-part fx form, because it IS a 3-part fx form
// with a reserved node id — an effect can never be called "preset", since ids
Expand Down Expand Up @@ -159,6 +175,21 @@ export interface AutomationRange {
* automating a boosted clip silently discard the boost — and the panel
* disables the fader while a lane owns it, so there was no way back.
*/
/**
* Opacity runs 0 to 1 and stops there. Unlike volume, which is allowed above
* unity because a quiet source sometimes needs lifting, there is nothing past
* fully opaque.
*/
export const OPACITY_RANGE: AutomationRange = {
min: 0,
max: 1,
default: 1,
step: 0.01,
unit: "",
label: "Opacity",
scale: "linear",
};

export const VOLUME_RANGE: AutomationRange = {
min: 0,
max: MAX_AUDIO_GAIN,
Expand All @@ -181,6 +212,8 @@ export function resolveAutomationRange(
const parsed = parseAutomationTarget(target);
if (!parsed) return null;
if (parsed.kind === "volume") return VOLUME_RANGE;
// Not chain-dependent: the picture has a level whether or not there is audio.
if (parsed.kind === "opacity") return OPACITY_RANGE;
if (parsed.kind === "preset") {
// Only for a preset the chain actually carries, so a lane left behind by a
// removed preset resolves to nothing and is dropped at read time — the same
Expand Down Expand Up @@ -331,11 +364,18 @@ function normalizePoints(
* points of every lane and drop lanes that carry none. Range clamping and
* orphan removal need the chain and happen in `resolveAutomation`.
*/
/** The range a target has on its own, before any chain is consulted. */
function laneRangeWithoutChain(target: string): AutomationRange | null {
if (target === VOLUME_TARGET) return VOLUME_RANGE;
if (target === OPACITY_TARGET) return OPACITY_RANGE;
return null;
}

export function normalizeAutomation(automation: HfAutomation): HfAutomation {
const lanes: HfAutomationLane[] = [];
for (const lane of automation.lanes) {
if (!parseAutomationTarget(lane.target)) continue;
const range = lane.target === VOLUME_TARGET ? VOLUME_RANGE : null;
const range = laneRangeWithoutChain(lane.target);
const points = normalizePoints(lane.points ?? [], range);
if (points.length > 0) lanes.push({ target: lane.target, points });
}
Expand Down
Loading
Loading