mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 13:58:20 +00:00
The generated catalog pages rendered the "Source" accordion title as a
JSX expression, `<Accordion title={`name.html`}>`. Mintlify's search
indexer stringifies expression attribute nodes rather than evaluating
them, so every one of the 399 catalog pages surfaced its Source section
in search results as "[object Object]" (visible on the Scroll Feed page,
among others).
The template literal carried no escaping benefit: every title is a
single-segment `name.html` path, and a double-quoted JSX string handles
it directly. Introduce `mdxStringAttribute`, which emits a plain
`name="value"` attribute and escapes `& " < { }` as HTML character
references (MDX decodes references inside quoted attribute values, and
unlike JSON.stringify it does not rely on backslash escapes, which JSX
strings do not honour). Use it for both the Source accordion title and
the preview iframe title, and regenerate the catalog pages.
Regenerated with `npx tsx scripts/generate-catalog-pages.ts`; the only
change across the 399 pages is the one Accordion title line each.
Co-authored-by: Miguel Ángel <miguel.sierra@heygen.com>
1076 lines
43 KiB
Plaintext
1076 lines
43 KiB
Plaintext
---
|
|
title: "Cut the Curve"
|
|
description: "A velocity-matched directional hard cut: the outgoing subject accelerates with power4.in, swaps at peak velocity, and the incoming subject continues in the same direction with power4.out."
|
|
---
|
|
|
|
import { InstallCommand } from "/snippets/install-command.jsx";
|
|
import { VariablesExplorer } from "/snippets/variables-explorer.jsx";
|
|
|
|
## Install
|
|
|
|
<InstallCommand command="npx hyperframes add cut-the-curve" item="cut-the-curve" />
|
|
|
|
That writes one file: `compositions/components/cut-the-curve.html`.
|
|
|
|
<VariablesExplorer
|
|
previewSrc="/public/catalog/components/cut-the-curve.json"
|
|
compositionId="cut-the-curve"
|
|
compositionSrc="compositions/components/cut-the-curve.html"
|
|
variables={[{"id":"subject","type":"enum","role":"content","label":"Subject","description":"Visual payload used on both sides of the cut.","default":"cursor","options":[{"value":"cursor","label":"Cursor"},{"value":"card","label":"Card"},{"value":"scene","label":"Scene"}]},{"id":"direction","type":"enum","role":"motion","label":"Direction","description":"Shared screen direction for outgoing and incoming travel.","default":"left","options":[{"value":"left","label":"Left"},{"value":"right","label":"Right"},{"value":"up","label":"Up"},{"value":"down","label":"Down"}]},{"id":"cutFraction","type":"number","role":"timing","label":"Cut fraction","description":"Normalized point where the hard cut swaps subjects.","default":0.33,"min":0,"max":1,"step":0.01},{"id":"blurPx","type":"number","role":"style","label":"Cut blur","description":"Blur in pixels shared by both subjects at the cut seam.","default":12,"min":0,"max":40,"step":1,"unit":"px"},{"id":"exit","type":"enum","role":"timing","label":"Exit","description":"Optional departure of the incoming subject. Default none: it rests until the frame cuts.","default":"none","options":[{"value":"none","label":"None"},{"value":"fade","label":"Fade"},{"value":"up","label":"Up"}]}]}
|
|
>
|
|
|
|
```html cut-the-curve.html
|
|
<!doctype html>
|
|
<!--
|
|
cut-the-curve -- HyperFrames video primitive (transitions / bridge)
|
|
|
|
Concept: a velocity-matched directional hard cut. The outgoing subject
|
|
accelerates along one straight screen vector with power4.in. At peak
|
|
velocity, the timeline swaps identities. The incoming subject starts on
|
|
the far side of the same vector and decelerates to rest with power4.out.
|
|
Matched blur and opacity at the cut conceal the seam.
|
|
|
|
Compiled from the launch write-up for this piece.
|
|
The source identifies the production signature as a single power4 whip,
|
|
with an approximately 1:2 outgoing-to-incoming distance and duration split.
|
|
|
|
Use when: bridging attention and momentum between shots, UI states, cards,
|
|
cursor beats, or scene blocks without requiring object identity to persist.
|
|
|
|
Variables (declared in data-composition-variables below):
|
|
- subject ("cursor" | "card" | "scene", default "cursor") selects the
|
|
simple visual payload used for both sides of the cut.
|
|
- direction ("left" | "right" | "up" | "down", default "left") owns
|
|
both the motion axis and the shared screen direction.
|
|
- cutFraction (number, 0-1, default 0.33) sets the hard-cut point. The
|
|
incoming distance is derived from this fraction so seam velocity stays
|
|
matched. At 0 or 1, the corresponding zero-duration half collapses.
|
|
- blurPx (number, default 12) sets blur at the cut seam.
|
|
- exit ("none" | "fade" | "up", default "none") optionally departs the
|
|
incoming subject at the end. Enabling it reserves a short tail window
|
|
(min(0.35s, 25% of D)) after the incoming subject lands.
|
|
|
|
Envelope (transition profile, no elastic HOLD; T = D minus the exit tail,
|
|
which is 0 when exit is none):
|
|
OUT = T * cutFraction, power4.in
|
|
IN = T * (1 - cutFraction), power4.out
|
|
The default 0.33 split is approximately 1:2 in time. Incoming distance is
|
|
derived from OUT distance and the duration ratio, preserving the same
|
|
approximately 1:2 spatial split and matching velocity at the cut.
|
|
|
|
Sync point: CUT_AT = T * cutFraction, the fixed boundary between OUT and
|
|
IN. It never lands in a HOLD because this profile has no HOLD phase.
|
|
|
|
Sound cue: a short whip cut fires at CUT_AT. The primitive never plays
|
|
audio. It dispatches an `hf:sfx` CustomEvent with
|
|
{ id: "whip-cut", t: CUT_AT } for the scene mix stage to route.
|
|
|
|
EDIT ZONE: change only the subject geometry custom properties in the style
|
|
block when adapting the payload drawings. The vector ratios and the paired
|
|
power4 eases are the mechanic and must remain together.
|
|
|
|
INVARIANTS: outgoing and incoming always share one vector; the identity swap
|
|
happens only at CUT_AT; their blur and opacity values match at that seam;
|
|
exactly one paused timeline registers under the literal id "cut-the-curve".
|
|
|
|
RETIME RANGE: 0.3s to 3s. Retiming changes D only. Never timeScale the GSAP
|
|
timeline and never split the two halves into independently tuned velocities.
|
|
|
|
Mount contract: this is a template-wrapped sub-composition. Everything the
|
|
mount needs, including styles, markup, GSAP, and timeline registration,
|
|
lives inside <template>. #root fills the host box with position:absolute,
|
|
inset:0, and container-type:size. It has no data-width or data-height.
|
|
-->
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[
|
|
{ "id": "subject", "type": "enum", "role": "content", "label": "Subject", "description": "Visual payload used on both sides of the cut.", "default": "cursor", "options": [{ "value": "cursor", "label": "Cursor" }, { "value": "card", "label": "Card" }, { "value": "scene", "label": "Scene" }] },
|
|
{ "id": "direction", "type": "enum", "role": "motion", "label": "Direction", "description": "Shared screen direction for outgoing and incoming travel.", "default": "left", "options": [{ "value": "left", "label": "Left" }, { "value": "right", "label": "Right" }, { "value": "up", "label": "Up" }, { "value": "down", "label": "Down" }] },
|
|
{ "id": "cutFraction", "type": "number", "role": "timing", "label": "Cut fraction", "description": "Normalized point where the hard cut swaps subjects.", "default": 0.33, "min": 0, "max": 1, "step": 0.01 },
|
|
{ "id": "blurPx", "type": "number", "role": "style", "label": "Cut blur", "description": "Blur in pixels shared by both subjects at the cut seam.", "default": 12, "min": 0, "max": 40, "step": 1, "unit": "px" },
|
|
{ "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure of the incoming subject. Default none: it rests until the frame cuts.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Cut the Curve</title>
|
|
</head>
|
|
<body>
|
|
<template>
|
|
<div id="root" data-composition-id="cut-the-curve" data-duration="1" data-fps="30">
|
|
<style>
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
#root {
|
|
position: absolute;
|
|
inset: 0;
|
|
container-type: size;
|
|
isolation: isolate;
|
|
overflow: hidden;
|
|
color: var(--fg, #f8fafc);
|
|
font-family: var(--font-body, Inter, system-ui, sans-serif);
|
|
}
|
|
|
|
.ctc-clip,
|
|
.ctc-backdrop,
|
|
.ctc-subject {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
.ctc-clip {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ctc-backdrop {
|
|
background:
|
|
radial-gradient(
|
|
circle at var(--ctc-glow-x, 24%) var(--ctc-glow-y, 28%),
|
|
color-mix(
|
|
in srgb,
|
|
var(--fg, #f8fafc) var(--ctc-glow-strength, 18%),
|
|
var(--transparent, transparent)
|
|
),
|
|
var(--transparent, transparent) var(--ctc-glow-radius, 42%)
|
|
),
|
|
var(--bg, #0b1120);
|
|
}
|
|
|
|
.ctc-subject {
|
|
display: grid;
|
|
place-items: center;
|
|
opacity: 0;
|
|
will-change: transform, filter, opacity;
|
|
}
|
|
|
|
.ctc-subject.is-outgoing {
|
|
opacity: 1;
|
|
--ctc-primary: color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent);
|
|
--ctc-secondary: color-mix(in srgb, var(--fg, #f8fafc) 18%, transparent);
|
|
}
|
|
|
|
.ctc-subject.is-incoming {
|
|
--ctc-primary: color-mix(in srgb, var(--fg, #f8fafc) 72%, transparent);
|
|
--ctc-secondary: color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent);
|
|
}
|
|
|
|
.ctc-art {
|
|
display: none;
|
|
}
|
|
|
|
#root[data-subject="cursor"] .ctc-cursor,
|
|
#root[data-subject="card"] .ctc-card,
|
|
#root[data-subject="scene"] .ctc-scene {
|
|
display: block;
|
|
}
|
|
|
|
.ctc-cursor {
|
|
position: relative;
|
|
width: var(--ctc-cursor-width, 15cqw);
|
|
height: var(--ctc-cursor-height, 24cqh);
|
|
}
|
|
|
|
.ctc-cursor svg {
|
|
width: var(--ctc-cursor-svg-width, 100%);
|
|
height: var(--ctc-cursor-svg-height, 100%);
|
|
overflow: visible;
|
|
filter: drop-shadow(
|
|
var(--ctc-shadow-x, 0cqw) var(--ctc-shadow-y, 1cqh) var(--ctc-shadow-blur, 1.6cqw)
|
|
color-mix(
|
|
in srgb,
|
|
var(--bg, #0b1120) var(--ctc-shadow-strength, 65%),
|
|
var(--transparent, transparent)
|
|
)
|
|
);
|
|
}
|
|
|
|
.ctc-cursor-shape {
|
|
fill: var(--ctc-primary, var(--fg, #f8fafc));
|
|
stroke: var(--fg, #f8fafc);
|
|
stroke-width: var(--ctc-cursor-stroke, 1.8);
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.ctc-cursor-pulse {
|
|
position: absolute;
|
|
left: var(--ctc-pulse-left, -5cqw);
|
|
top: var(--ctc-pulse-top, -5cqh);
|
|
width: var(--ctc-pulse-size, 11cqw);
|
|
aspect-ratio: var(--ctc-pulse-ratio, 1);
|
|
border: var(--ctc-pulse-border, 0.25cqw) solid
|
|
color-mix(
|
|
in srgb,
|
|
var(--ctc-secondary, var(--fg, #f8fafc)) var(--ctc-pulse-strength, 58%),
|
|
var(--transparent, transparent)
|
|
);
|
|
border-radius: var(--ctc-round, 50%);
|
|
}
|
|
|
|
.ctc-card {
|
|
width: var(--ctc-card-width, 42cqw);
|
|
min-height: var(--ctc-card-height, 34cqh);
|
|
padding: var(--ctc-card-padding-y, var(--space-3, 4.5cqh))
|
|
var(--ctc-card-padding-x, var(--space-3, 4cqw));
|
|
border: var(--ctc-border-width, 0.22cqw) solid var(--border, #334155);
|
|
border-radius: var(--radius, 2cqw);
|
|
background: var(--surface, #1e293b);
|
|
box-shadow: var(--ctc-shadow-x, 0cqw) var(--ctc-card-shadow-y, 2.4cqh)
|
|
var(--ctc-card-shadow-blur, 5cqw)
|
|
color-mix(
|
|
in srgb,
|
|
var(--bg, #0b1120) var(--ctc-card-shadow-strength, 55%),
|
|
var(--transparent, transparent)
|
|
);
|
|
}
|
|
|
|
.ctc-card-chip {
|
|
width: var(--ctc-chip-width, 9cqw);
|
|
height: var(--ctc-chip-height, 2.2cqh);
|
|
border-radius: var(--ctc-pill-radius, 99cqw);
|
|
background: var(--ctc-primary, var(--fg, #f8fafc));
|
|
margin-bottom: var(--ctc-card-gap, var(--space-3, 3.2cqh));
|
|
}
|
|
|
|
.ctc-card-line,
|
|
.ctc-scene-line {
|
|
height: var(--ctc-line-height, 1.8cqh);
|
|
border-radius: var(--ctc-pill-radius, 99cqw);
|
|
background: var(--fg, #f8fafc);
|
|
}
|
|
|
|
.ctc-card-line + .ctc-card-line {
|
|
margin-top: var(--ctc-line-gap, var(--space-2, 2.2cqh));
|
|
}
|
|
|
|
.ctc-card-line:first-of-type {
|
|
width: var(--ctc-line-long, 88%);
|
|
}
|
|
|
|
.ctc-card-line:last-of-type {
|
|
width: var(--ctc-line-short, 58%);
|
|
opacity: var(--ctc-muted-opacity, 0.42);
|
|
}
|
|
|
|
.ctc-scene {
|
|
width: var(--ctc-scene-width, 68cqw);
|
|
height: var(--ctc-scene-height, 58cqh);
|
|
overflow: hidden;
|
|
border: var(--ctc-border-width, 0.22cqw) solid var(--border, #334155);
|
|
border-radius: var(--radius, 2cqw);
|
|
background: var(--surface, #1e293b);
|
|
box-shadow: var(--ctc-shadow-x, 0cqw) var(--ctc-card-shadow-y, 2.4cqh)
|
|
var(--ctc-card-shadow-blur, 5cqw)
|
|
color-mix(
|
|
in srgb,
|
|
var(--bg, #0b1120) var(--ctc-card-shadow-strength, 55%),
|
|
var(--transparent, transparent)
|
|
);
|
|
}
|
|
|
|
.ctc-scene-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--ctc-dot-gap, var(--space-1, 1cqw));
|
|
height: var(--ctc-scene-bar-height, 8cqh);
|
|
padding-inline: var(--ctc-scene-pad, var(--space-2, 2cqw));
|
|
border-bottom: var(--ctc-border-width, 0.22cqw) solid var(--border, #334155);
|
|
}
|
|
|
|
.ctc-scene-dot {
|
|
width: var(--ctc-dot-size, 1.4cqw);
|
|
aspect-ratio: var(--ctc-pulse-ratio, 1);
|
|
border-radius: var(--ctc-round, 50%);
|
|
background: var(--ctc-primary, var(--fg, #f8fafc));
|
|
}
|
|
|
|
.ctc-scene-body {
|
|
display: grid;
|
|
grid-template-columns: var(--ctc-scene-columns, 1fr 2fr);
|
|
gap: var(--ctc-scene-gap, var(--space-3, 3cqw));
|
|
padding: var(--ctc-scene-body-pad, var(--space-3, 5cqh))
|
|
var(--ctc-scene-pad, var(--space-3, 3cqw));
|
|
}
|
|
|
|
.ctc-scene-panel {
|
|
min-height: var(--ctc-panel-height, 34cqh);
|
|
border-radius: var(--ctc-panel-radius, var(--radius, 1.2cqw));
|
|
background: color-mix(
|
|
in srgb,
|
|
var(--ctc-primary, var(--fg, #f8fafc)) var(--ctc-panel-strength, 24%),
|
|
var(--surface, #1e293b)
|
|
);
|
|
}
|
|
|
|
.ctc-scene-copy {
|
|
padding-top: var(--ctc-copy-top, var(--space-2, 2cqh));
|
|
}
|
|
|
|
.ctc-scene-line + .ctc-scene-line {
|
|
margin-top: var(--ctc-scene-line-gap, var(--space-3, 3cqh));
|
|
}
|
|
|
|
.ctc-scene-line:nth-child(1) {
|
|
width: var(--ctc-scene-line-one, 92%);
|
|
}
|
|
|
|
.ctc-scene-line:nth-child(2) {
|
|
width: var(--ctc-scene-line-two, 68%);
|
|
opacity: var(--ctc-muted-opacity, 0.42);
|
|
}
|
|
|
|
.ctc-scene-line:nth-child(3) {
|
|
width: var(--ctc-scene-line-three, 78%);
|
|
background: var(--ctc-secondary, var(--fg, #f8fafc));
|
|
}
|
|
</style>
|
|
|
|
<div
|
|
id="cut-the-curve-clip"
|
|
class="ctc-clip clip"
|
|
data-start="0"
|
|
data-duration="1"
|
|
data-track-index="0"
|
|
>
|
|
<div class="ctc-backdrop"></div>
|
|
|
|
<div class="ctc-subject is-outgoing" aria-hidden="true">
|
|
<div class="ctc-art ctc-cursor">
|
|
<span class="ctc-cursor-pulse"></span>
|
|
<svg viewBox="0 0 64 88" aria-hidden="true">
|
|
<path
|
|
class="ctc-cursor-shape"
|
|
d="M8 5 L54 43 L34 47 L45 75 L30 82 L19 53 L8 68 Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ctc-art ctc-card">
|
|
<div class="ctc-card-chip"></div>
|
|
<div class="ctc-card-line"></div>
|
|
<div class="ctc-card-line"></div>
|
|
</div>
|
|
<div class="ctc-art ctc-scene">
|
|
<div class="ctc-scene-bar">
|
|
<span class="ctc-scene-dot"></span><span class="ctc-scene-dot"></span
|
|
><span class="ctc-scene-dot"></span>
|
|
</div>
|
|
<div class="ctc-scene-body">
|
|
<div class="ctc-scene-panel"></div>
|
|
<div class="ctc-scene-copy">
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ctc-subject is-incoming" aria-hidden="true">
|
|
<div class="ctc-art ctc-cursor">
|
|
<span class="ctc-cursor-pulse"></span>
|
|
<svg viewBox="0 0 64 88" aria-hidden="true">
|
|
<path
|
|
class="ctc-cursor-shape"
|
|
d="M8 5 L54 43 L34 47 L45 75 L30 82 L19 53 L8 68 Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ctc-art ctc-card">
|
|
<div class="ctc-card-chip"></div>
|
|
<div class="ctc-card-line"></div>
|
|
<div class="ctc-card-line"></div>
|
|
</div>
|
|
<div class="ctc-art ctc-scene">
|
|
<div class="ctc-scene-bar">
|
|
<span class="ctc-scene-dot"></span><span class="ctc-scene-dot"></span
|
|
><span class="ctc-scene-dot"></span>
|
|
</div>
|
|
<div class="ctc-scene-body">
|
|
<div class="ctc-scene-panel"></div>
|
|
<div class="ctc-scene-copy">
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var root = document.getElementById("root");
|
|
var outgoing = root.querySelector(".is-outgoing");
|
|
var incoming = root.querySelector(".is-incoming");
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
var subject =
|
|
vars.subject === "card" || vars.subject === "scene" ? vars.subject : "cursor";
|
|
var direction =
|
|
vars.direction === "right" || vars.direction === "up" || vars.direction === "down"
|
|
? vars.direction
|
|
: "left";
|
|
var rawCut = Number(vars.cutFraction);
|
|
var cutFraction = Number.isFinite(rawCut) ? Math.max(0, Math.min(1, rawCut)) : 0.33;
|
|
var rawBlur = Number(vars.blurPx);
|
|
var blurPx = Number.isFinite(rawBlur) ? Math.max(0, rawBlur) : 12;
|
|
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
|
|
|
|
root.dataset.subject = subject;
|
|
|
|
// Direction is the single owner of axis, sign, unit, and base travel.
|
|
var vectors = {
|
|
left: { axis: "x", sign: -1, unit: "cqw", distance: 34 },
|
|
right: { axis: "x", sign: 1, unit: "cqw", distance: 34 },
|
|
up: { axis: "y", sign: -1, unit: "cqh", distance: 26 },
|
|
down: { axis: "y", sign: 1, unit: "cqh", distance: 26 },
|
|
};
|
|
var vector = vectors[direction];
|
|
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "1"));
|
|
// Exit none keeps the whole duration for the cut profile. An
|
|
// enabled exit reserves a short tail after the incoming lands.
|
|
var EXIT = exit === "none" ? 0 : Math.min(0.35, duration * 0.25);
|
|
var TRAVEL = Math.max(0.001, duration - EXIT);
|
|
var CUT_AT = TRAVEL * cutFraction;
|
|
var ENTRY_DURATION = TRAVEL - CUT_AT;
|
|
var entryDistance =
|
|
CUT_AT > 0 ? vector.distance * (ENTRY_DURATION / CUT_AT) : vector.distance * 2;
|
|
var seamBlur = "blur(" + blurPx + "px)";
|
|
var clearBlur = "blur(0px)";
|
|
var seamOpacity = 0.72;
|
|
|
|
var outgoingEnd = { opacity: seamOpacity, filter: seamBlur };
|
|
outgoingEnd[vector.axis] = vector.sign * vector.distance + vector.unit;
|
|
|
|
var incomingStart = { x: 0, y: 0, opacity: 0, filter: seamBlur };
|
|
incomingStart[vector.axis] = -vector.sign * entryDistance + vector.unit;
|
|
|
|
var incomingEnd = { opacity: 1, filter: clearBlur };
|
|
incomingEnd[vector.axis] = 0;
|
|
|
|
gsap.set(outgoing, { x: 0, y: 0, opacity: 1, filter: clearBlur });
|
|
gsap.set(incoming, incomingStart);
|
|
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
if (CUT_AT > 0) {
|
|
tl.to(
|
|
outgoing,
|
|
Object.assign({}, outgoingEnd, { duration: CUT_AT, ease: "power4.in" }),
|
|
0,
|
|
);
|
|
}
|
|
|
|
// CUT: one visibility owner, matched blur and opacity on both sides.
|
|
tl.set(outgoing, { opacity: 0 }, CUT_AT);
|
|
tl.set(incoming, { opacity: seamOpacity }, CUT_AT);
|
|
tl.call(
|
|
function () {
|
|
root.dispatchEvent(
|
|
new CustomEvent("hf:sfx", {
|
|
detail: { id: "whip-cut", t: CUT_AT },
|
|
bubbles: true,
|
|
}),
|
|
);
|
|
},
|
|
[],
|
|
CUT_AT,
|
|
);
|
|
|
|
if (ENTRY_DURATION > 0) {
|
|
tl.to(
|
|
incoming,
|
|
Object.assign({}, incomingEnd, {
|
|
duration: ENTRY_DURATION,
|
|
ease: "power4.out",
|
|
}),
|
|
CUT_AT,
|
|
);
|
|
} else {
|
|
tl.set(incoming, incomingEnd, CUT_AT);
|
|
}
|
|
|
|
// Optional departure; exit none rests until the frame cuts.
|
|
if (exit === "fade") {
|
|
tl.to(incoming, { opacity: 0, duration: EXIT, ease: "power2.in" }, TRAVEL);
|
|
} else if (exit === "up") {
|
|
tl.to(
|
|
incoming,
|
|
{ opacity: 0, y: "-6cqh", duration: EXIT, ease: "power2.in" },
|
|
TRAVEL,
|
|
);
|
|
}
|
|
|
|
tl.seek(0);
|
|
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["cut-the-curve"] = tl;
|
|
})();
|
|
</script>
|
|
</div>
|
|
</template>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
</VariablesExplorer>
|
|
|
|
## Paste it into your composition
|
|
|
|
Open `compositions/components/cut-the-curve.html` and copy what is inside into your own composition.
|
|
|
|
A component has no size or duration of its own. It takes both from the composition
|
|
you paste it into.
|
|
|
|
## Variables
|
|
|
|
Every one of these has a default, so the piece works untouched. Set the ones you
|
|
want to change on the element:
|
|
|
|
| Variable | Default | Accepts | What it does |
|
|
| --- | --- | --- | --- |
|
|
| `subject` | `cursor` | `cursor`, `card`, `scene` | Visual payload used on both sides of the cut. |
|
|
| `direction` | `left` | `left`, `right`, `up`, `down` | Shared screen direction for outgoing and incoming travel. |
|
|
| `cutFraction` | `0.33` | 0 to 1, step 0.01 | Normalized point where the hard cut swaps subjects. |
|
|
| `blurPx` | `12` | 0px to 40px, step 1px | Blur in pixels shared by both subjects at the cut seam. |
|
|
| `exit` | `none` | `none`, `fade`, `up` | Optional departure of the incoming subject. Default none: it rests until the frame cuts. |
|
|
|
|
Set them with `data-variable-values` on the element that mounts it. These are the
|
|
defaults, so this behaves exactly like the preview above until you change one:
|
|
|
|
```html wrap
|
|
<div
|
|
data-composition-id="cut-the-curve"
|
|
data-composition-src="compositions/components/cut-the-curve.html"
|
|
data-variable-values='{"subject":"cursor","direction":"left","cutFraction":0.33,"blurPx":12,"exit":"none"}'
|
|
></div>
|
|
```
|
|
|
|
## Source
|
|
|
|
<Accordion title="cut-the-curve.html">
|
|
|
|
```html
|
|
<!doctype html>
|
|
<!--
|
|
cut-the-curve -- HyperFrames video primitive (transitions / bridge)
|
|
|
|
Concept: a velocity-matched directional hard cut. The outgoing subject
|
|
accelerates along one straight screen vector with power4.in. At peak
|
|
velocity, the timeline swaps identities. The incoming subject starts on
|
|
the far side of the same vector and decelerates to rest with power4.out.
|
|
Matched blur and opacity at the cut conceal the seam.
|
|
|
|
Compiled from the launch write-up for this piece.
|
|
The source identifies the production signature as a single power4 whip,
|
|
with an approximately 1:2 outgoing-to-incoming distance and duration split.
|
|
|
|
Use when: bridging attention and momentum between shots, UI states, cards,
|
|
cursor beats, or scene blocks without requiring object identity to persist.
|
|
|
|
Variables (declared in data-composition-variables below):
|
|
- subject ("cursor" | "card" | "scene", default "cursor") selects the
|
|
simple visual payload used for both sides of the cut.
|
|
- direction ("left" | "right" | "up" | "down", default "left") owns
|
|
both the motion axis and the shared screen direction.
|
|
- cutFraction (number, 0-1, default 0.33) sets the hard-cut point. The
|
|
incoming distance is derived from this fraction so seam velocity stays
|
|
matched. At 0 or 1, the corresponding zero-duration half collapses.
|
|
- blurPx (number, default 12) sets blur at the cut seam.
|
|
- exit ("none" | "fade" | "up", default "none") optionally departs the
|
|
incoming subject at the end. Enabling it reserves a short tail window
|
|
(min(0.35s, 25% of D)) after the incoming subject lands.
|
|
|
|
Envelope (transition profile, no elastic HOLD; T = D minus the exit tail,
|
|
which is 0 when exit is none):
|
|
OUT = T * cutFraction, power4.in
|
|
IN = T * (1 - cutFraction), power4.out
|
|
The default 0.33 split is approximately 1:2 in time. Incoming distance is
|
|
derived from OUT distance and the duration ratio, preserving the same
|
|
approximately 1:2 spatial split and matching velocity at the cut.
|
|
|
|
Sync point: CUT_AT = T * cutFraction, the fixed boundary between OUT and
|
|
IN. It never lands in a HOLD because this profile has no HOLD phase.
|
|
|
|
Sound cue: a short whip cut fires at CUT_AT. The primitive never plays
|
|
audio. It dispatches an `hf:sfx` CustomEvent with
|
|
{ id: "whip-cut", t: CUT_AT } for the scene mix stage to route.
|
|
|
|
EDIT ZONE: change only the subject geometry custom properties in the style
|
|
block when adapting the payload drawings. The vector ratios and the paired
|
|
power4 eases are the mechanic and must remain together.
|
|
|
|
INVARIANTS: outgoing and incoming always share one vector; the identity swap
|
|
happens only at CUT_AT; their blur and opacity values match at that seam;
|
|
exactly one paused timeline registers under the literal id "cut-the-curve".
|
|
|
|
RETIME RANGE: 0.3s to 3s. Retiming changes D only. Never timeScale the GSAP
|
|
timeline and never split the two halves into independently tuned velocities.
|
|
|
|
Mount contract: this is a template-wrapped sub-composition. Everything the
|
|
mount needs, including styles, markup, GSAP, and timeline registration,
|
|
lives inside <template>. #root fills the host box with position:absolute,
|
|
inset:0, and container-type:size. It has no data-width or data-height.
|
|
-->
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[
|
|
{ "id": "subject", "type": "enum", "role": "content", "label": "Subject", "description": "Visual payload used on both sides of the cut.", "default": "cursor", "options": [{ "value": "cursor", "label": "Cursor" }, { "value": "card", "label": "Card" }, { "value": "scene", "label": "Scene" }] },
|
|
{ "id": "direction", "type": "enum", "role": "motion", "label": "Direction", "description": "Shared screen direction for outgoing and incoming travel.", "default": "left", "options": [{ "value": "left", "label": "Left" }, { "value": "right", "label": "Right" }, { "value": "up", "label": "Up" }, { "value": "down", "label": "Down" }] },
|
|
{ "id": "cutFraction", "type": "number", "role": "timing", "label": "Cut fraction", "description": "Normalized point where the hard cut swaps subjects.", "default": 0.33, "min": 0, "max": 1, "step": 0.01 },
|
|
{ "id": "blurPx", "type": "number", "role": "style", "label": "Cut blur", "description": "Blur in pixels shared by both subjects at the cut seam.", "default": 12, "min": 0, "max": 40, "step": 1, "unit": "px" },
|
|
{ "id": "exit", "type": "enum", "role": "timing", "label": "Exit", "description": "Optional departure of the incoming subject. Default none: it rests until the frame cuts.", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "fade", "label": "Fade" }, { "value": "up", "label": "Up" }] }
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<title>Cut the Curve</title>
|
|
</head>
|
|
<body>
|
|
<template>
|
|
<div id="root" data-composition-id="cut-the-curve" data-duration="1" data-fps="30">
|
|
<style>
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
#root {
|
|
position: absolute;
|
|
inset: 0;
|
|
container-type: size;
|
|
isolation: isolate;
|
|
overflow: hidden;
|
|
color: var(--fg, #f8fafc);
|
|
font-family: var(--font-body, Inter, system-ui, sans-serif);
|
|
}
|
|
|
|
.ctc-clip,
|
|
.ctc-backdrop,
|
|
.ctc-subject {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
|
|
.ctc-clip {
|
|
overflow: hidden;
|
|
}
|
|
|
|
.ctc-backdrop {
|
|
background:
|
|
radial-gradient(
|
|
circle at var(--ctc-glow-x, 24%) var(--ctc-glow-y, 28%),
|
|
color-mix(
|
|
in srgb,
|
|
var(--fg, #f8fafc) var(--ctc-glow-strength, 18%),
|
|
var(--transparent, transparent)
|
|
),
|
|
var(--transparent, transparent) var(--ctc-glow-radius, 42%)
|
|
),
|
|
var(--bg, #0b1120);
|
|
}
|
|
|
|
.ctc-subject {
|
|
display: grid;
|
|
place-items: center;
|
|
opacity: 0;
|
|
will-change: transform, filter, opacity;
|
|
}
|
|
|
|
.ctc-subject.is-outgoing {
|
|
opacity: 1;
|
|
--ctc-primary: color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent);
|
|
--ctc-secondary: color-mix(in srgb, var(--fg, #f8fafc) 18%, transparent);
|
|
}
|
|
|
|
.ctc-subject.is-incoming {
|
|
--ctc-primary: color-mix(in srgb, var(--fg, #f8fafc) 72%, transparent);
|
|
--ctc-secondary: color-mix(in srgb, var(--fg, #f8fafc) 45%, transparent);
|
|
}
|
|
|
|
.ctc-art {
|
|
display: none;
|
|
}
|
|
|
|
#root[data-subject="cursor"] .ctc-cursor,
|
|
#root[data-subject="card"] .ctc-card,
|
|
#root[data-subject="scene"] .ctc-scene {
|
|
display: block;
|
|
}
|
|
|
|
.ctc-cursor {
|
|
position: relative;
|
|
width: var(--ctc-cursor-width, 15cqw);
|
|
height: var(--ctc-cursor-height, 24cqh);
|
|
}
|
|
|
|
.ctc-cursor svg {
|
|
width: var(--ctc-cursor-svg-width, 100%);
|
|
height: var(--ctc-cursor-svg-height, 100%);
|
|
overflow: visible;
|
|
filter: drop-shadow(
|
|
var(--ctc-shadow-x, 0cqw) var(--ctc-shadow-y, 1cqh) var(--ctc-shadow-blur, 1.6cqw)
|
|
color-mix(
|
|
in srgb,
|
|
var(--bg, #0b1120) var(--ctc-shadow-strength, 65%),
|
|
var(--transparent, transparent)
|
|
)
|
|
);
|
|
}
|
|
|
|
.ctc-cursor-shape {
|
|
fill: var(--ctc-primary, var(--fg, #f8fafc));
|
|
stroke: var(--fg, #f8fafc);
|
|
stroke-width: var(--ctc-cursor-stroke, 1.8);
|
|
stroke-linejoin: round;
|
|
}
|
|
|
|
.ctc-cursor-pulse {
|
|
position: absolute;
|
|
left: var(--ctc-pulse-left, -5cqw);
|
|
top: var(--ctc-pulse-top, -5cqh);
|
|
width: var(--ctc-pulse-size, 11cqw);
|
|
aspect-ratio: var(--ctc-pulse-ratio, 1);
|
|
border: var(--ctc-pulse-border, 0.25cqw) solid
|
|
color-mix(
|
|
in srgb,
|
|
var(--ctc-secondary, var(--fg, #f8fafc)) var(--ctc-pulse-strength, 58%),
|
|
var(--transparent, transparent)
|
|
);
|
|
border-radius: var(--ctc-round, 50%);
|
|
}
|
|
|
|
.ctc-card {
|
|
width: var(--ctc-card-width, 42cqw);
|
|
min-height: var(--ctc-card-height, 34cqh);
|
|
padding: var(--ctc-card-padding-y, var(--space-3, 4.5cqh))
|
|
var(--ctc-card-padding-x, var(--space-3, 4cqw));
|
|
border: var(--ctc-border-width, 0.22cqw) solid var(--border, #334155);
|
|
border-radius: var(--radius, 2cqw);
|
|
background: var(--surface, #1e293b);
|
|
box-shadow: var(--ctc-shadow-x, 0cqw) var(--ctc-card-shadow-y, 2.4cqh)
|
|
var(--ctc-card-shadow-blur, 5cqw)
|
|
color-mix(
|
|
in srgb,
|
|
var(--bg, #0b1120) var(--ctc-card-shadow-strength, 55%),
|
|
var(--transparent, transparent)
|
|
);
|
|
}
|
|
|
|
.ctc-card-chip {
|
|
width: var(--ctc-chip-width, 9cqw);
|
|
height: var(--ctc-chip-height, 2.2cqh);
|
|
border-radius: var(--ctc-pill-radius, 99cqw);
|
|
background: var(--ctc-primary, var(--fg, #f8fafc));
|
|
margin-bottom: var(--ctc-card-gap, var(--space-3, 3.2cqh));
|
|
}
|
|
|
|
.ctc-card-line,
|
|
.ctc-scene-line {
|
|
height: var(--ctc-line-height, 1.8cqh);
|
|
border-radius: var(--ctc-pill-radius, 99cqw);
|
|
background: var(--fg, #f8fafc);
|
|
}
|
|
|
|
.ctc-card-line + .ctc-card-line {
|
|
margin-top: var(--ctc-line-gap, var(--space-2, 2.2cqh));
|
|
}
|
|
|
|
.ctc-card-line:first-of-type {
|
|
width: var(--ctc-line-long, 88%);
|
|
}
|
|
|
|
.ctc-card-line:last-of-type {
|
|
width: var(--ctc-line-short, 58%);
|
|
opacity: var(--ctc-muted-opacity, 0.42);
|
|
}
|
|
|
|
.ctc-scene {
|
|
width: var(--ctc-scene-width, 68cqw);
|
|
height: var(--ctc-scene-height, 58cqh);
|
|
overflow: hidden;
|
|
border: var(--ctc-border-width, 0.22cqw) solid var(--border, #334155);
|
|
border-radius: var(--radius, 2cqw);
|
|
background: var(--surface, #1e293b);
|
|
box-shadow: var(--ctc-shadow-x, 0cqw) var(--ctc-card-shadow-y, 2.4cqh)
|
|
var(--ctc-card-shadow-blur, 5cqw)
|
|
color-mix(
|
|
in srgb,
|
|
var(--bg, #0b1120) var(--ctc-card-shadow-strength, 55%),
|
|
var(--transparent, transparent)
|
|
);
|
|
}
|
|
|
|
.ctc-scene-bar {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--ctc-dot-gap, var(--space-1, 1cqw));
|
|
height: var(--ctc-scene-bar-height, 8cqh);
|
|
padding-inline: var(--ctc-scene-pad, var(--space-2, 2cqw));
|
|
border-bottom: var(--ctc-border-width, 0.22cqw) solid var(--border, #334155);
|
|
}
|
|
|
|
.ctc-scene-dot {
|
|
width: var(--ctc-dot-size, 1.4cqw);
|
|
aspect-ratio: var(--ctc-pulse-ratio, 1);
|
|
border-radius: var(--ctc-round, 50%);
|
|
background: var(--ctc-primary, var(--fg, #f8fafc));
|
|
}
|
|
|
|
.ctc-scene-body {
|
|
display: grid;
|
|
grid-template-columns: var(--ctc-scene-columns, 1fr 2fr);
|
|
gap: var(--ctc-scene-gap, var(--space-3, 3cqw));
|
|
padding: var(--ctc-scene-body-pad, var(--space-3, 5cqh))
|
|
var(--ctc-scene-pad, var(--space-3, 3cqw));
|
|
}
|
|
|
|
.ctc-scene-panel {
|
|
min-height: var(--ctc-panel-height, 34cqh);
|
|
border-radius: var(--ctc-panel-radius, var(--radius, 1.2cqw));
|
|
background: color-mix(
|
|
in srgb,
|
|
var(--ctc-primary, var(--fg, #f8fafc)) var(--ctc-panel-strength, 24%),
|
|
var(--surface, #1e293b)
|
|
);
|
|
}
|
|
|
|
.ctc-scene-copy {
|
|
padding-top: var(--ctc-copy-top, var(--space-2, 2cqh));
|
|
}
|
|
|
|
.ctc-scene-line + .ctc-scene-line {
|
|
margin-top: var(--ctc-scene-line-gap, var(--space-3, 3cqh));
|
|
}
|
|
|
|
.ctc-scene-line:nth-child(1) {
|
|
width: var(--ctc-scene-line-one, 92%);
|
|
}
|
|
|
|
.ctc-scene-line:nth-child(2) {
|
|
width: var(--ctc-scene-line-two, 68%);
|
|
opacity: var(--ctc-muted-opacity, 0.42);
|
|
}
|
|
|
|
.ctc-scene-line:nth-child(3) {
|
|
width: var(--ctc-scene-line-three, 78%);
|
|
background: var(--ctc-secondary, var(--fg, #f8fafc));
|
|
}
|
|
</style>
|
|
|
|
<div
|
|
id="cut-the-curve-clip"
|
|
class="ctc-clip clip"
|
|
data-start="0"
|
|
data-duration="1"
|
|
data-track-index="0"
|
|
>
|
|
<div class="ctc-backdrop"></div>
|
|
|
|
<div class="ctc-subject is-outgoing" aria-hidden="true">
|
|
<div class="ctc-art ctc-cursor">
|
|
<span class="ctc-cursor-pulse"></span>
|
|
<svg viewBox="0 0 64 88" aria-hidden="true">
|
|
<path
|
|
class="ctc-cursor-shape"
|
|
d="M8 5 L54 43 L34 47 L45 75 L30 82 L19 53 L8 68 Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ctc-art ctc-card">
|
|
<div class="ctc-card-chip"></div>
|
|
<div class="ctc-card-line"></div>
|
|
<div class="ctc-card-line"></div>
|
|
</div>
|
|
<div class="ctc-art ctc-scene">
|
|
<div class="ctc-scene-bar">
|
|
<span class="ctc-scene-dot"></span><span class="ctc-scene-dot"></span
|
|
><span class="ctc-scene-dot"></span>
|
|
</div>
|
|
<div class="ctc-scene-body">
|
|
<div class="ctc-scene-panel"></div>
|
|
<div class="ctc-scene-copy">
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="ctc-subject is-incoming" aria-hidden="true">
|
|
<div class="ctc-art ctc-cursor">
|
|
<span class="ctc-cursor-pulse"></span>
|
|
<svg viewBox="0 0 64 88" aria-hidden="true">
|
|
<path
|
|
class="ctc-cursor-shape"
|
|
d="M8 5 L54 43 L34 47 L45 75 L30 82 L19 53 L8 68 Z"
|
|
/>
|
|
</svg>
|
|
</div>
|
|
<div class="ctc-art ctc-card">
|
|
<div class="ctc-card-chip"></div>
|
|
<div class="ctc-card-line"></div>
|
|
<div class="ctc-card-line"></div>
|
|
</div>
|
|
<div class="ctc-art ctc-scene">
|
|
<div class="ctc-scene-bar">
|
|
<span class="ctc-scene-dot"></span><span class="ctc-scene-dot"></span
|
|
><span class="ctc-scene-dot"></span>
|
|
</div>
|
|
<div class="ctc-scene-body">
|
|
<div class="ctc-scene-panel"></div>
|
|
<div class="ctc-scene-copy">
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
<div class="ctc-scene-line"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<script>
|
|
(function () {
|
|
"use strict";
|
|
|
|
var root = document.getElementById("root");
|
|
var outgoing = root.querySelector(".is-outgoing");
|
|
var incoming = root.querySelector(".is-incoming");
|
|
var vars =
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {};
|
|
|
|
var subject =
|
|
vars.subject === "card" || vars.subject === "scene" ? vars.subject : "cursor";
|
|
var direction =
|
|
vars.direction === "right" || vars.direction === "up" || vars.direction === "down"
|
|
? vars.direction
|
|
: "left";
|
|
var rawCut = Number(vars.cutFraction);
|
|
var cutFraction = Number.isFinite(rawCut) ? Math.max(0, Math.min(1, rawCut)) : 0.33;
|
|
var rawBlur = Number(vars.blurPx);
|
|
var blurPx = Number.isFinite(rawBlur) ? Math.max(0, rawBlur) : 12;
|
|
var exit = vars.exit === "fade" || vars.exit === "up" ? vars.exit : "none";
|
|
|
|
root.dataset.subject = subject;
|
|
|
|
// Direction is the single owner of axis, sign, unit, and base travel.
|
|
var vectors = {
|
|
left: { axis: "x", sign: -1, unit: "cqw", distance: 34 },
|
|
right: { axis: "x", sign: 1, unit: "cqw", distance: 34 },
|
|
up: { axis: "y", sign: -1, unit: "cqh", distance: 26 },
|
|
down: { axis: "y", sign: 1, unit: "cqh", distance: 26 },
|
|
};
|
|
var vector = vectors[direction];
|
|
var duration = Math.max(0.001, parseFloat(root.dataset.duration || "1"));
|
|
// Exit none keeps the whole duration for the cut profile. An
|
|
// enabled exit reserves a short tail after the incoming lands.
|
|
var EXIT = exit === "none" ? 0 : Math.min(0.35, duration * 0.25);
|
|
var TRAVEL = Math.max(0.001, duration - EXIT);
|
|
var CUT_AT = TRAVEL * cutFraction;
|
|
var ENTRY_DURATION = TRAVEL - CUT_AT;
|
|
var entryDistance =
|
|
CUT_AT > 0 ? vector.distance * (ENTRY_DURATION / CUT_AT) : vector.distance * 2;
|
|
var seamBlur = "blur(" + blurPx + "px)";
|
|
var clearBlur = "blur(0px)";
|
|
var seamOpacity = 0.72;
|
|
|
|
var outgoingEnd = { opacity: seamOpacity, filter: seamBlur };
|
|
outgoingEnd[vector.axis] = vector.sign * vector.distance + vector.unit;
|
|
|
|
var incomingStart = { x: 0, y: 0, opacity: 0, filter: seamBlur };
|
|
incomingStart[vector.axis] = -vector.sign * entryDistance + vector.unit;
|
|
|
|
var incomingEnd = { opacity: 1, filter: clearBlur };
|
|
incomingEnd[vector.axis] = 0;
|
|
|
|
gsap.set(outgoing, { x: 0, y: 0, opacity: 1, filter: clearBlur });
|
|
gsap.set(incoming, incomingStart);
|
|
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
if (CUT_AT > 0) {
|
|
tl.to(
|
|
outgoing,
|
|
Object.assign({}, outgoingEnd, { duration: CUT_AT, ease: "power4.in" }),
|
|
0,
|
|
);
|
|
}
|
|
|
|
// CUT: one visibility owner, matched blur and opacity on both sides.
|
|
tl.set(outgoing, { opacity: 0 }, CUT_AT);
|
|
tl.set(incoming, { opacity: seamOpacity }, CUT_AT);
|
|
tl.call(
|
|
function () {
|
|
root.dispatchEvent(
|
|
new CustomEvent("hf:sfx", {
|
|
detail: { id: "whip-cut", t: CUT_AT },
|
|
bubbles: true,
|
|
}),
|
|
);
|
|
},
|
|
[],
|
|
CUT_AT,
|
|
);
|
|
|
|
if (ENTRY_DURATION > 0) {
|
|
tl.to(
|
|
incoming,
|
|
Object.assign({}, incomingEnd, {
|
|
duration: ENTRY_DURATION,
|
|
ease: "power4.out",
|
|
}),
|
|
CUT_AT,
|
|
);
|
|
} else {
|
|
tl.set(incoming, incomingEnd, CUT_AT);
|
|
}
|
|
|
|
// Optional departure; exit none rests until the frame cuts.
|
|
if (exit === "fade") {
|
|
tl.to(incoming, { opacity: 0, duration: EXIT, ease: "power2.in" }, TRAVEL);
|
|
} else if (exit === "up") {
|
|
tl.to(
|
|
incoming,
|
|
{ opacity: 0, y: "-6cqh", duration: EXIT, ease: "power2.in" },
|
|
TRAVEL,
|
|
);
|
|
}
|
|
|
|
tl.seek(0);
|
|
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["cut-the-curve"] = tl;
|
|
})();
|
|
</script>
|
|
</div>
|
|
</template>
|
|
</body>
|
|
</html>
|
|
```
|
|
|
|
</Accordion>
|
|
|
|
{/* hf:generated-footer */}
|
|
|
|
Tagged `transition` `directional-cut` `velocity-match` `whip` `bridge` `demonstrate`.
|
|
|
|
## Related topics
|
|
|
|
- [Browse the complete Catalog](/catalog)
|
|
- [Add assets and Catalog items in Studio](/studio/assets-and-blocks)
|
|
- [Build a richer composition](/go-further)
|