Install
That writes one file:compositions/components/zoom-through-transition.html.
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 |
|---|---|---|---|
from-label | Before | string | Caption on the outgoing scene, the one on screen when the transition starts. |
to-label | Zoom Through Transition | string | Caption on the incoming scene, the one the timeline wipes into frame. |
depth | standard | close, standard, far | Multiplies how far both scenes travel and how hard the outgoing one blurs, so the push covers a shorter or longer run in the same time. |
accent | blue | blue, green, violet | Theme token for the ring marking the incoming scene. Blue rides —accent, green —brand, violet —accent-2; each falls back to a neutral hairline. |
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:
<div
data-composition-id="zoom-through-transition"
data-composition-src="compositions/components/zoom-through-transition.html"
data-variable-values='{"from-label":"Before","to-label":"Zoom Through Transition","depth":"standard","accent":"blue"}'
></div>
Source
zoom-through-transition.html
zoom-through-transition.html
<!doctype html>
<!--
Zoom Through Transition - a scene push that wipes the incoming scene through
the outgoing one while the outgoing one drifts back and blurs.
Mount contract: MOUNTABLE SUB-COMPOSITION. The runtime clones only <template>
contents; #root fills the host box (inset: 0, container-type: size), and the
file registers one paused timeline under the literal "zoom-through-transition"
key. Variables come from window.__hyperframes.getVariables().
This file owns its motion. It previously shipped as a fragment whose only
executable timeline lived in demo.html, so an installed copy rendered the
finished pose and never moved.
Variables (declared in data-composition-variables below):
- from-label (string, default "Before"): caption on the outgoing scene.
- to-label (string, default "Zoom Through Transition"): caption on the
incoming scene, clipped out of frame until the wipe brings it in.
- depth (close | standard | far, default standard): multiplies how far both
scenes travel and how hard the outgoing one blurs. Every input it
multiplies is zero at rest, so it collapses to the shipped pose there. It
deliberately leaves the two scale tracks alone: the timeline drives those
from their own start values, which a multiplier would distort.
- accent (blue | green | violet, default blue): ink of the ring that marks
the incoming scene. Both scenes are monochrome placeholder material; the
ring is the one accent-coloured element. blue rides --accent, green rides
--brand, violet rides --accent-2, and each falls back to a neutral
hairline so an unthemed composition renders in black and white.
Envelope: the push starts at 0.35s (clamped to fit shorter durations), runs
0.9s, then holds the incoming scene for whatever is left.
-->
<html
lang="en"
data-composition-id="zoom-through-transition"
data-composition-duration="3"
data-composition-variables='[
{ "id": "from-label", "type": "string", "role": "content", "label": "Outgoing label", "description": "Caption on the outgoing scene, the one on screen when the transition starts.", "default": "Before" },
{ "id": "to-label", "type": "string", "role": "content", "label": "Incoming label", "description": "Caption on the incoming scene, the one the timeline wipes into frame.", "default": "Zoom Through Transition" },
{ "id": "depth", "type": "enum", "role": "motion", "label": "Depth", "description": "Multiplies how far both scenes travel and how hard the outgoing one blurs, so the push covers a shorter or longer run in the same time.", "default": "standard", "options": [{ "value": "close", "label": "Close" }, { "value": "standard", "label": "Standard" }, { "value": "far", "label": "Far" }] },
{ "id": "accent", "type": "enum", "role": "style", "label": "Accent", "description": "Theme token for the ring marking the incoming scene. Blue rides --accent, green --brand, violet --accent-2; each falls back to a neutral hairline.", "default": "blue", "options": [{ "value": "blue", "label": "Blue" }, { "value": "green", "label": "Green" }, { "value": "violet", "label": "Violet" }] }
]'
>
<head>
<meta charset="UTF-8" />
<title>Zoom Through Transition</title>
</head>
<body>
<template>
<div id="root" data-composition-id="zoom-through-transition" data-duration="3" data-fps="30">
<style>
*,
*::before,
*::after {
box-sizing: border-box;
}
/* Root fills the host-owned box; the scenes measure against it. */
#root {
position: absolute;
inset: 0;
container-type: size;
isolation: isolate;
overflow: hidden;
background: #111827;
}
.hf-remocn-zoom-through-transition {
--hf-accent: var(--accent, rgba(255, 255, 255, 0.28));
--hf-l1: rgba(255, 255, 255, 0.72);
--hf-l3: rgba(255, 255, 255, 0.18);
--hf-l4: rgba(255, 255, 255, 0.08);
--hf-hair: rgba(255, 255, 255, 0.14);
--hf-ink: #111827;
--hf-muted: #6b7280;
--hf-surface: #ffffff;
color: var(--hf-ink);
font-family: Inter, system-ui, sans-serif;
}
.hf-remocn-zoom-through-transition {
position: absolute;
inset: 0;
overflow: hidden;
background: #111827;
}
.hf-remocn-zoom-through-transition .scene {
position: absolute;
inset: 0;
display: grid;
place-items: center;
font-weight: 900;
font-size: 13.5cqmin;
letter-spacing: -0.05em;
color: #fff;
}
.hf-remocn-zoom-through-transition .from {
background:
repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.04) 0 1px,
transparent 1px 4px
),
var(--hf-l4);
transform: translate(
calc(var(--hf-from-x, 0px) * var(--hf-depth, 1)),
calc(var(--hf-from-y, 0px) * var(--hf-depth, 1))
)
scale(var(--hf-from-scale, 1));
filter: blur(calc(var(--hf-from-blur, 0px) * var(--hf-depth, 1)));
}
.hf-remocn-zoom-through-transition .to {
background:
repeating-linear-gradient(
90deg,
rgba(255, 255, 255, 0.05) 0 1px,
transparent 1px 4px
),
var(--hf-l3);
box-shadow: inset 0 0 0 2px var(--hf-accent);
transform: translate(
calc(var(--hf-to-x, 0px) * var(--hf-depth, 1)),
calc(var(--hf-to-y, 0px) * var(--hf-depth, 1))
)
scale(var(--hf-to-scale, 1));
clip-path: inset(
var(--hf-clip-top, 0%) var(--hf-clip-right, 100%) var(--hf-clip-bottom, 0%)
var(--hf-clip-left, 0%)
);
}
.hf-remocn-zoom-through-transition .fx {
position: absolute;
inset: 0;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.75), transparent);
mix-blend-mode: screen;
transform: translateX(var(--hf-fx-x, -110%));
}
.hf-remocn-zoom-through-transition .rgb {
position: absolute;
inset: 0;
pointer-events: none;
opacity: var(--hf-rgb-opacity, 0);
background: linear-gradient(
90deg,
rgba(239, 68, 68, 0.35),
transparent,
rgba(34, 211, 238, 0.35)
);
transform: translateX(var(--hf-rgb-x, 0px));
}
</style>
<div class="hf-remocn-zoom-through-transition">
>
<div class="scene from">Before</div>
<div class="scene to">Zoom Through Transition</div>
<div class="fx"></div>
<div class="rgb"></div>
</div>
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<script>
(function () {
"use strict";
var vars =
window.__hyperframes && window.__hyperframes.getVariables
? window.__hyperframes.getVariables()
: {};
// Unrecognised overrides return to their declared defaults.
var depths = { close: 0.55, standard: 1, far: 1.7 };
// Each choice routes to a different contract token so the variable stays
// meaningful under a theme rather than collapsing into one themed accent.
var HAIRLINE = "rgba(255, 255, 255, 0.28)";
var accents = {
blue: "var(--accent, " + HAIRLINE + ")",
green: "var(--brand, " + HAIRLINE + ")",
violet: "var(--accent-2, " + HAIRLINE + ")",
};
function pick(table, value, fallback) {
return Object.prototype.hasOwnProperty.call(table, value) ? value : fallback;
}
function label(value) {
return typeof value === "string" && value !== "" ? value : null;
}
var depth = depths[pick(depths, vars.depth, "standard")];
var accent = accents[pick(accents, vars.accent, "blue")];
var fromLabel = label(vars["from-label"]);
var toLabel = label(vars["to-label"]);
var roots = document.querySelectorAll(".hf-remocn-zoom-through-transition");
var mountRoot = document.getElementById("root");
for (var i = 0; i < roots.length; i += 1) {
var root = roots[i];
root.style.setProperty("--hf-depth", String(depth));
root.style.setProperty("--hf-accent", accent);
var fromScene = root.querySelector(".from");
var toScene = root.querySelector(".to");
if (fromLabel !== null && fromScene) {
fromScene.textContent = fromLabel;
}
if (toLabel !== null && toScene) {
toScene.textContent = toLabel;
}
}
// The motion used to live only in this file's demo page, so an installed
// copy rendered the finished pose and never moved. The timeline belongs
// with the markup it drives: build it here, paused, and register it under
// this composition's own id so the runtime seeks it frame by frame.
if (!mountRoot || !window.gsap) return;
var SELF = ".hf-remocn-zoom-through-transition";
var duration = Math.max(0.001, parseFloat(mountRoot.dataset.duration || "3"));
// The push reads as a transition only with a beat of the outgoing scene
// before it. Clamped so the wipe always lands inside the duration.
var PUSH = 0.9;
var START = Math.max(0, Math.min(0.35, duration - PUSH));
var tl = window.gsap.timeline({ paused: true });
tl.fromTo(
SELF + " .to",
{ "--hf-clip-right": "100%", "--hf-to-x": "60px", "--hf-to-scale": 1.04 },
{
"--hf-clip-right": "0%",
"--hf-to-x": "0px",
"--hf-to-scale": 1,
duration: PUSH,
ease: "power3.inOut",
},
START,
);
tl.fromTo(
SELF + " .from",
{ "--hf-from-x": "0px", "--hf-from-scale": 1, "--hf-from-blur": "0px" },
{
"--hf-from-x": "-70px",
"--hf-from-scale": 0.96,
"--hf-from-blur": "5px",
duration: PUSH,
ease: "power3.inOut",
},
START,
);
tl.fromTo(
SELF + " .fx",
{ "--hf-fx-x": "-110%" },
{ "--hf-fx-x": "110%", duration: 0.58, ease: "power2.inOut" },
START + 0.18,
);
// repeat/yoyo resolves from absolute time on every seek, so the strobe is
// reproducible at any frame rather than accumulated across ticks.
tl.fromTo(
SELF + " .rgb",
{ "--hf-rgb-opacity": 0, "--hf-rgb-x": "-16px" },
{
"--hf-rgb-opacity": 1,
"--hf-rgb-x": "16px",
repeat: 3,
yoyo: true,
duration: 0.07,
ease: "steps(1)",
},
START + 0.32,
);
// HOLD: the incoming scene rests for whatever is left of the duration.
tl.set({}, {}, duration);
tl.seek(0);
window.__timelines = window.__timelines || {};
window.__timelines["zoom-through-transition"] = tl;
})();
</script>
</div>
</template>
</body>
</html>
Usage
Opencompositions/components/zoom-through-transition.html and paste its contents into your composition. See the comment header in the file for timeline integration guidance.
Tagged transition-primitive remocn-port.