mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-03 23:03:09 +00:00
feat(registry): bring back the video-primitive moves (#3169)
Restores the 208 catalog items reverted after their previews 404'd in production, this time on the payload mechanism rather than the .html files that caused the outage. The generator no longer writes a preview document to docs/public. That writer, and the machinery under it, existed only to produce files the docs host discards, so it is gone rather than bypassed. Items now embed the composition itself via a payload, which is what the previous change already does for the items that were already in the catalog. The variables explorer is parked, not restored: it drove its preview through the same unpublished .html path, so it would have shown an empty frame. Items that declare variables get the live player plus the static variables table, and reconnecting the explorer to payloads is a follow-up.
This commit is contained in:
+118
@@ -0,0 +1,118 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-composition-id="device-frame-stage-demo" data-composition-duration="5">
|
||||
<!--
|
||||
Device Frame Stage - Demo. Mounts the primitive via a clip with
|
||||
data-composition-src instead of inlining its markup, proving the
|
||||
sub-composition mount contract (template + #root + host-driven sizing;
|
||||
see skills/hyperframes-core/references/sub-compositions.md).
|
||||
|
||||
The mount clip is deliberately sized to a 960x540 slot inside this
|
||||
1920x1080 stage (not full-bleed): the primitive must size the device off
|
||||
that HOST box, not off an assumed 1920x1080 stage, or it renders 2x too
|
||||
large and bleeds past the slot's border.
|
||||
|
||||
Non-default variables (tablet / island / silver) exercise all three
|
||||
variables away from the primitive's defaults (phone / none / graphite);
|
||||
see device-frame-stage.html for the full header (concept, envelope, sync
|
||||
points, sound cues).
|
||||
-->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Device Frame Stage - Demo</title>
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#root {
|
||||
/* 17-token dark theme contract, matching the other test-scene hosts. */
|
||||
--bg: #06080b;
|
||||
--fg: #f4f6f8;
|
||||
--muted: #8b95a4;
|
||||
--surface: #12161d;
|
||||
--border: rgba(244, 246, 248, 0.16);
|
||||
--brand: #ff7a33;
|
||||
--accent: #22d3ee;
|
||||
--font-display:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Display", "Segoe UI", Roboto, sans-serif;
|
||||
--font-body:
|
||||
-apple-system, BlinkMacSystemFont, "SF Pro Text", "Segoe UI", Roboto, sans-serif;
|
||||
--font-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
|
||||
--radius: 16px;
|
||||
--space-1: 8px;
|
||||
--space-2: 16px;
|
||||
--space-3: 32px;
|
||||
|
||||
position: relative;
|
||||
width: 1920px;
|
||||
height: 1080px;
|
||||
overflow: hidden;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
font-family: var(--font-body);
|
||||
}
|
||||
|
||||
.demo-backdrop {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(120% 120% at 50% 18%, #222b40 0%, #05070d 65%);
|
||||
}
|
||||
|
||||
/* The mount slot: 960x540, centered. Border makes any overflow past
|
||||
this box immediately visible in a snapshot. */
|
||||
.demo-slot {
|
||||
position: absolute;
|
||||
left: 480px;
|
||||
top: 270px;
|
||||
width: 960px;
|
||||
height: 540px;
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: var(--radius);
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="device-frame-stage-demo"
|
||||
data-start="0"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
>
|
||||
<div class="demo-backdrop"></div>
|
||||
|
||||
<div
|
||||
id="device-frame-stage-mount"
|
||||
class="clip demo-slot"
|
||||
data-composition-id="device-frame-stage"
|
||||
data-composition-src="./device-frame-stage.html"
|
||||
data-variable-values='{"device":"tablet","cutout":"island","body":"silver"}'
|
||||
data-start="0"
|
||||
data-duration="5"
|
||||
data-track-index="0"
|
||||
data-width="960"
|
||||
data-height="540"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
window.__timelines = window.__timelines || {};
|
||||
const tl = gsap.timeline({ paused: true });
|
||||
window.__timelines["device-frame-stage-demo"] = tl;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,466 @@
|
||||
<!doctype html>
|
||||
<html
|
||||
lang="en"
|
||||
data-composition-id="device-frame-stage"
|
||||
data-composition-duration="5"
|
||||
data-composition-variables='[
|
||||
{ "id": "device", "type": "enum", "role": "layout", "label": "Device kind", "default": "phone", "options": [{ "value": "phone", "label": "Phone" }, { "value": "tablet", "label": "Tablet" }] },
|
||||
{ "id": "cutout", "type": "enum", "role": "layout", "label": "Camera cutout", "default": "none", "options": [{ "value": "none", "label": "None" }, { "value": "island", "label": "Island" }, { "value": "notch", "label": "Notch" }] },
|
||||
{ "id": "body", "type": "enum", "role": "style", "label": "Body material", "default": "graphite", "options": [{ "value": "graphite", "label": "Graphite" }, { "value": "silver", "label": "Silver" }] }
|
||||
]'
|
||||
>
|
||||
<!--
|
||||
Device Frame Stage, a HyperFrames video primitive.
|
||||
|
||||
Concept: a phone or tablet mockup treated as a physical scene prop, not a
|
||||
screenshot border. It stages arbitrary reconstructed UI in a screen slot,
|
||||
rises onto the stage as one rigid prop, settles, idles, and holds. The
|
||||
frame serves the screen (device-frame-spec's governing law): no spinning
|
||||
hardware reveal, glare sweep, or camera-lens tour, ever.
|
||||
|
||||
This file is a mountable HyperFrames sub-composition (see
|
||||
skills/hyperframes-core/references/sub-compositions.md): a host
|
||||
composition embeds it via a clip carrying data-composition-src, a
|
||||
matching data-composition-id, and its own data-start / data-duration /
|
||||
data-track-index / data-width / data-height. Everything the render needs
|
||||
lives inside <template> below; the #root div's own data-width /
|
||||
data-height are declarative reference metadata only (the linter's
|
||||
root-dimensions contract) -- the CSS never reads them, and the device
|
||||
sizes relative to whatever host box it actually lands in (cqh/cqw), so
|
||||
it never needs to be opened or rendered standalone. See demo.html for
|
||||
the reference mount.
|
||||
|
||||
Compiled from: fixture + mobile mechanics inventory #1 + device-frame-spec.md
|
||||
(~/dev/hyperframes-corpus-data/mobile/device-frame-spec.md), the
|
||||
authoritative geometry / material / entry-law source for this primitive.
|
||||
|
||||
Use when: a scene needs to stage mobile or tablet UI as a physical
|
||||
product (hero product demo, gesture-led interaction, screen swap) rather
|
||||
than a bare floating screenshot.
|
||||
|
||||
Variables (data-composition-variables above):
|
||||
device enum phone|tablet default "phone" canonical outer ratio + logical viewport
|
||||
cutout enum none|island|notch default "none" camera cutout; must match the installed screen content's safe area
|
||||
body enum graphite|silver default "graphite" device material tone
|
||||
|
||||
Envelope (IN / HOLD / OUT, elasticity lives in HOLD only, never gsap.timeScale):
|
||||
IN 1.08s fixed rise-and-settle: 0.18s pre-roll, 0.72s power3.out rise past rest, 0.18s power2.inOut settle
|
||||
HOLD elastic idle float loop (period 3.2s, +/-0.6cqh), stretches or compresses to fill whatever duration remains
|
||||
OUT 0.56s fixed sink, power2.in
|
||||
|
||||
Sync points (fixed offsets into IN/OUT, never inside the elastic HOLD):
|
||||
device-settled t = IN (end of the settle segment) — screen content may start animating here
|
||||
|
||||
Sound cues: one SFX hook, "device-settle-soft", fired as an `hf:sfx`
|
||||
CustomEvent at the device-settled sync point (t = IN). The primitive
|
||||
never plays audio itself; the host scene mixes the cue.
|
||||
-->
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=1920, height=1080" />
|
||||
<title>Device Frame Stage</title>
|
||||
<!-- head is metadata for the source file only; the runtime only clones
|
||||
<template> contents on mount (see sub-compositions.md), so the gsap
|
||||
loader here exists purely for opening this file directly during
|
||||
authoring -- a host composition already provides window.gsap. -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<template>
|
||||
<style>
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* INVARIANT: root is styled by #root, never a class -- the compiler
|
||||
scopes CSS to [data-composition-id="device-frame-stage"] as a
|
||||
descendant selector, so a rule keyed on the root's own class would
|
||||
never match the root itself (see sub-compositions.md, Pitfall 3).
|
||||
This primitive is elastic and fills whatever box a host clip
|
||||
gives it, from a 960x540 slot up to a full 1920x1080 frame.
|
||||
|
||||
The width/height !important below is load-bearing, not decoration:
|
||||
when this root gets inlined into a host, the compiler's
|
||||
flatten step (prepareFlattenedInnerRoot) reads this element's own
|
||||
data-width/data-height (required by the root_missing_dimensions
|
||||
lint rule) and force-writes them back as an inline
|
||||
style="width:1920px;height:1080px". Inline style beats a plain
|
||||
stylesheet rule, so without !important the root would render at a
|
||||
fixed 1920x1080 instead of filling the host box -- exactly the
|
||||
2x-oversized-and-bleeding bug this primitive was fixed for. */
|
||||
#root {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
container-type: size;
|
||||
isolation: isolate;
|
||||
}
|
||||
|
||||
.device-stage {
|
||||
--stage-perspective: 1600px;
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
perspective: var(--stage-perspective);
|
||||
perspective-origin: 50% 50%;
|
||||
}
|
||||
|
||||
.device-frame {
|
||||
/* INVARIANT: geometry below is physical-material law from
|
||||
device-frame-spec.md (corner/bezel/aspect percentages, gradient,
|
||||
shadow), not brand paint. It may carry literal fallbacks per the
|
||||
primitive's material exception; do not swap in --radius/--space-*. */
|
||||
--device-color: #1b1d22;
|
||||
--device-color-hi: #343740;
|
||||
--device-color-dark: #111318;
|
||||
--device-radius: 46px;
|
||||
--device-bezel: 9px;
|
||||
--screen-radius: calc(var(--device-radius) - var(--device-bezel));
|
||||
--device-aspect: 9 / 19.5;
|
||||
--device-y: 18cqh;
|
||||
--device-scale: 0.94;
|
||||
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
height: 90cqh;
|
||||
aspect-ratio: var(--device-aspect);
|
||||
border-radius: var(--device-radius);
|
||||
padding: var(--device-bezel);
|
||||
background: linear-gradient(
|
||||
145deg,
|
||||
var(--device-color-hi),
|
||||
var(--device-color) 42%,
|
||||
var(--device-color-dark)
|
||||
);
|
||||
border: 1px solid rgba(255, 255, 255, 0.22);
|
||||
box-shadow:
|
||||
0 52px 110px rgba(7, 10, 20, 0.3),
|
||||
0 18px 38px rgba(7, 10, 20, 0.22),
|
||||
0 3px 9px rgba(7, 10, 20, 0.16),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.24),
|
||||
inset 0 0 0 1px rgba(0, 0, 0, 0.48);
|
||||
transform: translate(-50%, calc(-50% + var(--device-y))) scale(var(--device-scale));
|
||||
transform-style: preserve-3d;
|
||||
transform-origin: 50% 88%;
|
||||
will-change: transform;
|
||||
}
|
||||
|
||||
/* EDIT ZONE: kind/cutout/body are driven by data-composition-variables
|
||||
(see JS below); an installing agent may also override
|
||||
--device-color / --device-color-hi / --device-color-dark inline for
|
||||
a bespoke finish without touching the geometry laws. */
|
||||
.device-frame[data-kind="tablet"] {
|
||||
--device-radius: 48px;
|
||||
--device-bezel: 16px;
|
||||
--screen-radius: 34px;
|
||||
--device-aspect: 3 / 4;
|
||||
height: 82cqh;
|
||||
}
|
||||
|
||||
.device-frame[data-body="silver"] {
|
||||
--device-color: #c9cdd3;
|
||||
--device-color-hi: #eef1f4;
|
||||
--device-color-dark: #9a9ea6;
|
||||
}
|
||||
|
||||
.device-screen {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
border-radius: var(--screen-radius);
|
||||
background: var(--surface, #f5f6f8);
|
||||
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.42);
|
||||
isolation: isolate;
|
||||
container-type: size;
|
||||
}
|
||||
|
||||
.screen-viewport {
|
||||
/* Logical-pixel authoring space (390x844 phone / 768x1024 tablet),
|
||||
uniformly scaled to the actual screen box by JS-measured
|
||||
--screen-scale (device-frame-spec, "Screen content contract"). The
|
||||
scale is scene setup, not animation. */
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: calc(var(--logical-width, 390) * 1px);
|
||||
height: calc(var(--logical-height, 844) * 1px);
|
||||
transform-origin: 0 0;
|
||||
transform: scale(var(--screen-scale, 1));
|
||||
}
|
||||
|
||||
.screen-safe-area {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
padding-top: calc(var(--logical-height, 844) * 1px * 0.03);
|
||||
}
|
||||
|
||||
.device-frame[data-cutout="island"] .screen-safe-area {
|
||||
padding-top: calc(var(--logical-height, 844) * 1px * 0.075);
|
||||
}
|
||||
|
||||
.device-frame[data-cutout="notch"] .screen-safe-area {
|
||||
padding-top: calc(var(--logical-height, 844) * 1px * 0.06);
|
||||
}
|
||||
|
||||
.device-screen-slot {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Visible only while the slot is empty (comments do not count toward
|
||||
:empty); vanishes the moment an installing agent drops real content
|
||||
in, no cleanup required. */
|
||||
.device-screen-slot:empty {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: 1px dashed var(--border, rgba(15, 23, 42, 0.16));
|
||||
border-radius: calc(var(--screen-radius) * 0.4);
|
||||
}
|
||||
|
||||
.device-screen-slot:empty::before {
|
||||
content: "screen slot: replace with reconstructed UI";
|
||||
padding: var(--space-2, 16px);
|
||||
color: var(--muted, #94a3b8);
|
||||
font-family: var(--font-body, Inter, system-ui, sans-serif);
|
||||
font-size: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.device-cutout {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
width: 0;
|
||||
height: 0;
|
||||
background: #050607;
|
||||
pointer-events: none;
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.device-frame[data-cutout="island"] .device-cutout {
|
||||
top: 1.4%;
|
||||
width: 28%;
|
||||
height: 3.2%;
|
||||
border-radius: 999px;
|
||||
}
|
||||
|
||||
.device-frame[data-cutout="notch"] .device-cutout {
|
||||
top: 0;
|
||||
width: 52%;
|
||||
height: 3.8%;
|
||||
border-radius: 0 0 18px 18px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- data-width / data-height here satisfy the linter's root-dimensions
|
||||
contract (root_missing_dimensions) and match every other
|
||||
sub-composition's #root -- see sub-compositions.md's own
|
||||
file-shape example. The compiler's flatten step also reads these
|
||||
two attributes and force-writes them back as an inline
|
||||
width/height style on mount, so #root's width/height !important
|
||||
above is required to keep the rendered box elastic (fills the
|
||||
host clip's actual box, from a 960x540 slot up to a full
|
||||
1920x1080 frame) instead of a fixed 1920x1080. -->
|
||||
<div
|
||||
id="root"
|
||||
data-composition-id="device-frame-stage"
|
||||
data-width="1920"
|
||||
data-height="1080"
|
||||
data-duration="5"
|
||||
>
|
||||
<div class="device-stage">
|
||||
<div class="device-frame" data-kind="phone" data-cutout="none" data-body="graphite">
|
||||
<div class="device-screen">
|
||||
<div class="screen-viewport">
|
||||
<div class="screen-safe-area">
|
||||
<div class="device-screen-slot" data-screen-slot>
|
||||
<!-- EDIT ZONE: replace with reconstructed product UI, or
|
||||
nest a <div data-composition-src="..." data-duration="...">
|
||||
sub-composition host. Author content at the device's
|
||||
logical viewport (390x844 phone, 768x1024 tablet);
|
||||
this slot is pre-scaled for you, so keep coordinates
|
||||
in logical screen pixels, never canvas pixels. -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="device-cutout" aria-hidden="true"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var html = document.documentElement;
|
||||
var root = document.getElementById("root");
|
||||
var frame = root.querySelector(".device-frame");
|
||||
var screen = root.querySelector(".device-screen");
|
||||
var viewport = root.querySelector(".screen-viewport");
|
||||
|
||||
var declared = {};
|
||||
try {
|
||||
JSON.parse(html.getAttribute("data-composition-variables") || "[]").forEach(
|
||||
function (variable) {
|
||||
declared[variable.id] = variable.default;
|
||||
},
|
||||
);
|
||||
} catch (error) {}
|
||||
var vars = Object.assign({}, declared, window.__hfVariables || {});
|
||||
|
||||
var device = vars.device === "tablet" ? "tablet" : "phone";
|
||||
var cutout = vars.cutout === "island" || vars.cutout === "notch" ? vars.cutout : "none";
|
||||
var body = vars.body === "silver" ? "silver" : "graphite";
|
||||
|
||||
frame.setAttribute("data-kind", device);
|
||||
frame.setAttribute("data-cutout", cutout);
|
||||
frame.setAttribute("data-body", body);
|
||||
|
||||
var LOGICAL = { phone: [390, 844], tablet: [768, 1024] };
|
||||
var logicalWidth = LOGICAL[device][0];
|
||||
var logicalHeight = LOGICAL[device][1];
|
||||
viewport.style.setProperty("--logical-width", String(logicalWidth));
|
||||
viewport.style.setProperty("--logical-height", String(logicalHeight));
|
||||
|
||||
function updateScreenScale() {
|
||||
var rect = screen.getBoundingClientRect();
|
||||
if (rect.width > 0)
|
||||
viewport.style.setProperty("--screen-scale", String(rect.width / logicalWidth));
|
||||
}
|
||||
updateScreenScale();
|
||||
if (typeof ResizeObserver !== "undefined") {
|
||||
new ResizeObserver(updateScreenScale).observe(screen);
|
||||
}
|
||||
|
||||
// NOTE: the timeline registration key below is a literal string,
|
||||
// not read from root.getAttribute("data-composition-id") -- when
|
||||
// this file is mounted via data-composition-src, the compiler
|
||||
// strips data-composition-id from the inlined root (the HOST
|
||||
// clip keeps its own copy instead), so a live read here would
|
||||
// return null and the host would never find this timeline (see
|
||||
// sub-compositions.md, Pitfall 2). registry/test-scenes/slots/empty.html
|
||||
// models the same literal-key pattern.
|
||||
var compositionId = "device-frame-stage";
|
||||
var duration = Math.max(
|
||||
0.001,
|
||||
parseFloat(
|
||||
root.dataset.duration || html.getAttribute("data-composition-duration") || "5",
|
||||
),
|
||||
);
|
||||
|
||||
// INVARIANT: IN/OUT durations are fixed by device-frame-spec's entry
|
||||
// and exit law; only HOLD stretches or compresses to fit whatever
|
||||
// duration the host gives this instance. Never gsap.timeScale().
|
||||
var IN_DELAY = 0.18;
|
||||
var IN_RISE = 0.72;
|
||||
var IN_SETTLE = 0.18;
|
||||
var IN_BASE = IN_DELAY + IN_RISE + IN_SETTLE; // 1.08s
|
||||
var OUT_BASE = 0.56;
|
||||
var totalBase = IN_BASE + OUT_BASE;
|
||||
var IN = IN_BASE;
|
||||
var OUT = OUT_BASE;
|
||||
if (duration < totalBase) {
|
||||
var shrink = duration / totalBase;
|
||||
IN = IN_BASE * shrink;
|
||||
OUT = OUT_BASE * shrink;
|
||||
}
|
||||
var HOLD = Math.max(0, duration - (IN + OUT));
|
||||
var OUT_AT = IN + HOLD;
|
||||
// SYNC POINT: device-settled fires at t = IN (fixed offset into IN,
|
||||
// never inside the elastic HOLD).
|
||||
|
||||
function easeOutCubic(x) {
|
||||
return 1 - Math.pow(1 - x, 3);
|
||||
}
|
||||
function easeInOutQuad(x) {
|
||||
return x < 0.5 ? 2 * x * x : 1 - Math.pow(-2 * x + 2, 2) / 2;
|
||||
}
|
||||
function easeInQuad(x) {
|
||||
return x * x;
|
||||
}
|
||||
|
||||
function setPose(yCqh, scaleValue) {
|
||||
frame.style.setProperty("--device-y", yCqh.toFixed(3) + "cqh");
|
||||
frame.style.setProperty("--device-scale", scaleValue.toFixed(4));
|
||||
}
|
||||
|
||||
function renderIn(t) {
|
||||
var delayEnd = IN_DELAY * (IN / IN_BASE);
|
||||
var riseEnd = delayEnd + IN_RISE * (IN / IN_BASE);
|
||||
if (t <= delayEnd) {
|
||||
setPose(18, 0.94);
|
||||
return;
|
||||
}
|
||||
if (t <= riseEnd) {
|
||||
var p = easeOutCubic(
|
||||
Math.min(1, (t - delayEnd) / Math.max(0.0001, riseEnd - delayEnd)),
|
||||
);
|
||||
setPose(18 + (-1.2 - 18) * p, 0.94 + (1.01 - 0.94) * p);
|
||||
return;
|
||||
}
|
||||
var p2 = easeInOutQuad(Math.min(1, (t - riseEnd) / Math.max(0.0001, IN - riseEnd)));
|
||||
setPose(-1.2 + (0 - -1.2) * p2, 1.01 + (1 - 1.01) * p2);
|
||||
}
|
||||
|
||||
// RETIME RANGE: the idle float is the only elastic segment. It
|
||||
// repeats for as long as HOLD lasts, so stretching the host duration
|
||||
// never re-times the fixed IN or OUT phases above.
|
||||
function renderHold(holdT) {
|
||||
var period = 3.2;
|
||||
var amp = 0.6;
|
||||
var phase = ((holdT % period) / period) * Math.PI * 2;
|
||||
setPose(Math.sin(phase) * amp, 1);
|
||||
}
|
||||
|
||||
function renderOut(p) {
|
||||
var e = easeInQuad(Math.min(1, p));
|
||||
setPose(0 + 18 * e, 1 + (0.96 - 1) * e);
|
||||
}
|
||||
|
||||
function render(t) {
|
||||
if (t <= IN) renderIn(t);
|
||||
else if (t < OUT_AT) renderHold(t - IN);
|
||||
else renderOut((t - OUT_AT) / Math.max(0.0001, OUT));
|
||||
}
|
||||
|
||||
function fireSfx(id, atTime) {
|
||||
root.dispatchEvent(
|
||||
new CustomEvent("hf:sfx", { detail: { id: id, t: atTime }, bubbles: true }),
|
||||
);
|
||||
}
|
||||
|
||||
var proxy = { t: 0 };
|
||||
var tl = gsap.timeline({ paused: true });
|
||||
tl.to(proxy, {
|
||||
t: duration,
|
||||
duration: duration,
|
||||
ease: "none",
|
||||
onUpdate: function () {
|
||||
render(proxy.t);
|
||||
},
|
||||
});
|
||||
tl.call(
|
||||
function () {
|
||||
fireSfx("device-settle-soft", IN);
|
||||
},
|
||||
[],
|
||||
IN,
|
||||
);
|
||||
tl.to({}, { duration: Math.max(0, duration - tl.duration()) });
|
||||
render(0);
|
||||
tl.seek(0);
|
||||
window.__timelines = window.__timelines || {};
|
||||
window.__timelines[compositionId] = tl;
|
||||
})();
|
||||
</script>
|
||||
</template>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"$schema": "https://hyperframes.heygen.com/schema/registry-item.json",
|
||||
"name": "device-frame-stage",
|
||||
"type": "hyperframes:component",
|
||||
"title": "Device Frame Stage",
|
||||
"description": "A phone or tablet mockup staged as a physical scene prop with a screen slot for arbitrary reconstructed UI, rising onto the stage, settling, and holding an idle float",
|
||||
"tags": ["prop", "device", "mobile", "tablet", "ui-props", "holdable"],
|
||||
"jobs": ["demonstrate"],
|
||||
"family": "ui-props",
|
||||
"profile": "holdable",
|
||||
"evidence": "fixture + mobile inventory #1 + device-frame-spec.md",
|
||||
"files": [
|
||||
{
|
||||
"path": "device-frame-stage.html",
|
||||
"target": "compositions/components/device-frame-stage.html",
|
||||
"type": "hyperframes:snippet"
|
||||
}
|
||||
],
|
||||
"variables": [
|
||||
{
|
||||
"id": "device",
|
||||
"type": "enum",
|
||||
"role": "layout",
|
||||
"label": "Device kind",
|
||||
"default": "phone",
|
||||
"options": [
|
||||
{ "value": "phone", "label": "Phone" },
|
||||
{ "value": "tablet", "label": "Tablet" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "cutout",
|
||||
"type": "enum",
|
||||
"role": "layout",
|
||||
"label": "Camera cutout",
|
||||
"default": "none",
|
||||
"options": [
|
||||
{ "value": "none", "label": "None" },
|
||||
{ "value": "island", "label": "Island" },
|
||||
{ "value": "notch", "label": "Notch" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "body",
|
||||
"type": "enum",
|
||||
"role": "style",
|
||||
"label": "Body material",
|
||||
"default": "graphite",
|
||||
"options": [
|
||||
{ "value": "graphite", "label": "Graphite" },
|
||||
{ "value": "silver", "label": "Silver" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user