mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-11-15 22:08:03 +00:00
One block per loop variant (loop-<family>[-<n>], 1080x1080), one hand-written engine per family, driven by a single paused GSAP timeline that recomputes every card transform from t. Every engine parameter is exposed through data-composition-variables, grouped like a template editor's panel (Media / Background / Appearance / Animation / Template options), plus image1..N content slots that default to monochrome SVG placeholders. No third-party media ships. Validation: scripts/lint-registry-items.mjs 249/249, oxfmt clean, hyperframes check on one variant per family (95/95), registry.json + docs/catalog pages regenerated by the repo scripts. Previews are rendered locally (720^2) and need the docs-images upload. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
325 lines
32 KiB
HTML
Vendored
325 lines
32 KiB
HTML
Vendored
<!doctype html>
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[{"id":"mediaFit","type":"enum","role":"style","label":"Media · Fit","description":"Natural keeps each image's own ratio; Crop fills the card box at the media aspect below.","default":"crop","options":[{"value":"natural","label":"Natural (original ratio)"},{"value":"crop","label":"Crop"}]},{"id":"cropAspectRatio","type":"enum","role":"style","label":"Media · Aspect","description":"Card aspect used when Fit is Crop.","default":"1:1","options":[{"value":"16:9","label":"16:9"},{"value":"9:16","label":"9:16"},{"value":"1:1","label":"1:1"},{"value":"4:5","label":"4:5"},{"value":"3:2","label":"3:2"},{"value":"3:4","label":"3:4"},{"value":"2:3","label":"2:3"}]},{"id":"backgroundType","type":"enum","role":"style","label":"Background · Type","description":"Gradient (start → end colours), flat Colour, or your own Image.","default":"gradient","options":[{"value":"gradient","label":"Gradient"},{"value":"color","label":"Color"},{"value":"image","label":"Image"}]},{"id":"backgroundGradientStart","type":"color","role":"style","label":"Background · Gradient start","description":"Top colour of the gradient.","default":"#111111"},{"id":"backgroundGradientEnd","type":"color","role":"style","label":"Background · Gradient end","description":"Bottom colour of the gradient.","default":"#444444"},{"id":"backgroundColor","type":"color","role":"style","label":"Background · Color","description":"Flat colour when Type is Color; also the ground under an Image.","default":"#0b0b0f"},{"id":"backgroundImage","type":"image","role":"style","label":"Background · Image","description":"Cover-fitted background when Type is Image.","portrays":["background_image"],"default":""},{"id":"blurAmount","type":"number","role":"style","label":"Background · Blur","description":"Blur on the background image, px.","default":0,"min":0,"max":60,"step":1,"unit":"px"},{"id":"backgroundGradientNoise","type":"number","role":"style","label":"Background · Grain","description":"Film-grain overlay strength.","default":0,"min":0,"max":100,"step":1,"unit":"%"},{"id":"backgroundOpacity","type":"number","role":"style","label":"Background · Opacity","description":"Opacity of the whole background layer (0 = transparent ground).","default":1,"min":0,"max":1,"step":0.05},{"id":"scaleIntensity","type":"number","role":"style","label":"Appearance · Media scale","description":"Strength of the size change.","default":0.28,"min":0,"max":1,"step":0.05},{"id":"borderRadius","type":"number","role":"style","label":"Appearance · Border radius","description":"Card corner radius in px at 1080.","default":11,"min":0,"max":200,"step":1,"unit":"px"},{"id":"direction","type":"enum","role":"style","label":"Animation · Direction","description":"Direction of travel.","default":"left","options":[{"value":"left","label":"Left"},{"value":"right","label":"Right"},{"value":"alternate","label":"Alternate"}]},{"id":"globeSize","type":"number","role":"style","label":"Template options · Globe Size","description":"Globe Size (engine parameter; see the block header comment).","default":50,"min":0,"max":100,"step":1},{"id":"backFade","type":"number","role":"style","label":"Template options · Back Fade","description":"Back Fade (engine parameter; see the block header comment).","default":55,"min":0,"max":110,"step":1},{"id":"tilt","type":"number","role":"style","label":"Template options · Tilt","description":"Tilt (engine parameter; see the block header comment).","default":27,"min":-180,"max":180,"step":1,"unit":"°"},{"id":"image1","type":"image","role":"content","label":"Media · Image 1","description":"Image on card 1 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image2","type":"image","role":"content","label":"Media · Image 2","description":"Image on card 2 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image3","type":"image","role":"content","label":"Media · Image 3","description":"Image on card 3 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image4","type":"image","role":"content","label":"Media · Image 4","description":"Image on card 4 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image5","type":"image","role":"content","label":"Media · Image 5","description":"Image on card 5 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image6","type":"image","role":"content","label":"Media · Image 6","description":"Image on card 6 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image7","type":"image","role":"content","label":"Media · Image 7","description":"Image on card 7 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"},{"id":"image8","type":"image","role":"content","label":"Media · Image 8","description":"Image on card 8 of 8. Any aspect; fitted per mediaFit.","portrays":["subject_image"],"default":"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E"}]'
|
|
>
|
|
<head>
|
|
<title>loop-orbitglobe</title>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=1080, height=1080" />
|
|
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
|
|
<style>
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
html,
|
|
body {
|
|
width: 1080px;
|
|
height: 1080px;
|
|
overflow: hidden;
|
|
background: #0b0b0f;
|
|
}
|
|
#loop-orbitglobe-bg {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
#loop-orbitglobe-stage {
|
|
position: absolute;
|
|
inset: 0;
|
|
perspective-origin: 50% 50%;
|
|
}
|
|
/* never put opacity/filter in will-change on preserve-3d elements: it flattens them silently */
|
|
#loop-orbitglobe-world {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 0;
|
|
height: 0;
|
|
transform-style: preserve-3d;
|
|
}
|
|
.card {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
overflow: hidden;
|
|
}
|
|
.card img {
|
|
display: block;
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
}
|
|
.card .shade {
|
|
position: absolute;
|
|
inset: 0;
|
|
background: #000;
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div
|
|
id="loop-orbitglobe-root"
|
|
data-composition-id="loop-orbitglobe"
|
|
data-start="0"
|
|
data-width="1080"
|
|
data-height="1080"
|
|
data-duration="20"
|
|
>
|
|
<div
|
|
id="loop-orbitglobe-loop"
|
|
class="clip"
|
|
data-start="0"
|
|
data-duration="20"
|
|
data-track-index="1"
|
|
data-layout-allow-overflow
|
|
>
|
|
<div id="loop-orbitglobe-bg"></div>
|
|
<div id="loop-orbitglobe-stage"><div id="loop-orbitglobe-world"></div></div>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
// Shared runtime for the the reference tool loop ports. Loaded by every family engine.
|
|
// Everything here is a pure function of the HyperFrames variables + t.
|
|
//
|
|
// Reference constants (measured on the 512x512 preview library, 2026-09-08):
|
|
// Marquee planeSize -> 0.3% of frame height per unit (7 variants, all 1.54px/unit @512)
|
|
// Marquee gap -> 0.2% of frame height per unit
|
|
// 3D perspective -> CSS focal length f = 1000px * (100/perspective) at H=1080
|
|
// (3D01/02/08/11: card widths & far-card spacing match to <3%)
|
|
// Both are calibrated on a SQUARE canvas; for 16:9 we scale by H. ponytail: if the
|
|
// dashboard 16:9 reference shows width-relative scaling, flip REF below to W.
|
|
window.RF = (() => {
|
|
// Variables are injected by the inline script via RF.use(): HyperFrames wraps INLINE scripts in a
|
|
// per-composition scope where getVariables() resolves; this external file only sees the host window.
|
|
const V = {},
|
|
MEDIA = [];
|
|
const use = (vars, media) => {
|
|
Object.assign(V, vars || {});
|
|
MEDIA.splice(0, MEDIA.length, ...(media || []));
|
|
};
|
|
const root = document.querySelector("[data-composition-id]");
|
|
const W = +root.dataset.width,
|
|
H = +root.dataset.height;
|
|
const REF = H;
|
|
const num = (k, d) => (V[k] !== undefined && V[k] !== "" && !isNaN(+V[k]) ? +V[k] : d);
|
|
const str = (k, d) => (V[k] !== undefined && V[k] !== "" ? String(V[k]) : d);
|
|
const on = (k, d) => /^(true|on|1)$/i.test(str(k, d ? "on" : "off")); // spec booleans arrive as "true"/"false", enums as on/off
|
|
|
|
function pick(n) {
|
|
n = Math.max(1, Math.min(MEDIA.length, n | 0));
|
|
return MEDIA.slice(0, n);
|
|
}
|
|
// aspect (w/h) a card takes for a media item under mediaFit + cropAspectRatio
|
|
function aspect(m, fit, cropAR) {
|
|
if (fit === "natural") return m.w / m.h;
|
|
const [a, b] = String(cropAR || "1:1")
|
|
.split(":")
|
|
.map(Number);
|
|
return a && b ? a / b : 1;
|
|
}
|
|
function card(m, w, h, radius) {
|
|
const el = document.createElement("div");
|
|
el.className = "card";
|
|
el.style.cssText = `position:absolute;width:${w}px;height:${h}px;overflow:hidden;border-radius:${radius}px;`;
|
|
const img = document.createElement("img");
|
|
img.src = m.src;
|
|
img.width = Math.round(w);
|
|
img.height = Math.round(h);
|
|
img.style.cssText = `display:block;width:${w}px;height:${h}px;object-fit:cover;`;
|
|
el.appendChild(img);
|
|
return el;
|
|
}
|
|
function background(el) {
|
|
// the editor's Background panel: type gradient | color | image, gradient start/end, grain (noise %), opacity, image blur
|
|
const type = str("backgroundType", "gradient");
|
|
const c = str("backgroundColor", "#0b0b0f");
|
|
el.style.background = c;
|
|
if (type === "gradient") {
|
|
const g1 = str("backgroundGradientStart", "#111111"),
|
|
g2 = str("backgroundGradientEnd", "#444444");
|
|
el.style.background = `linear-gradient(180deg, ${g1}, ${g2})`;
|
|
} else if (type === "image") {
|
|
const im = V.backgroundImage,
|
|
src = im && typeof im === "object" ? im.url || im.src || "" : im || "";
|
|
if (src) {
|
|
el.style.background = `${c} url("${src}") center / cover no-repeat`;
|
|
const b = num("blurAmount", 0);
|
|
if (b > 0) el.style.filter = `blur(${b}px)`;
|
|
}
|
|
}
|
|
el.style.opacity = String(Math.max(0, Math.min(1, num("backgroundOpacity", 1))));
|
|
const noise = num("backgroundGradientNoise", 0);
|
|
if (noise > 0) {
|
|
const g = document.createElement("div");
|
|
g.style.cssText = `position:absolute;inset:0;pointer-events:none;mix-blend-mode:overlay;opacity:${((noise / 100) * 0.7).toFixed(3)};background:url("data:image/svg+xml,%3Csvg%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%20width%3D'256'%20height%3D'256'%3E%3Cfilter%20id%3D'n'%3E%3CfeTurbulence%20type%3D'fractalNoise'%20baseFrequency%3D'0.9'%20numOctaves%3D'3'%20stitchTiles%3D'stitch'%20seed%3D'7'%2F%3E%3CfeColorMatrix%20values%3D'0%200%200%200%200.5%200%200%200%200%200.5%200%200%200%200%200.5%200%200%200%201%200'%2F%3E%3C%2Ffilter%3E%3Crect%20width%3D'256'%20height%3D'256'%20filter%3D'url(%23n)'%2F%3E%3C%2Fsvg%3E") 0 0 / 256px 256px repeat`;
|
|
el.appendChild(g);
|
|
}
|
|
}
|
|
const expoInOut = (x) =>
|
|
x <= 0
|
|
? 0
|
|
: x >= 1
|
|
? 1
|
|
: x < 0.5
|
|
? Math.pow(2, 20 * x - 10) / 2
|
|
: (2 - Math.pow(2, -20 * x + 10)) / 2;
|
|
const frac = (x) => x - Math.floor(x);
|
|
return {
|
|
V,
|
|
use,
|
|
W,
|
|
H,
|
|
REF,
|
|
num,
|
|
str,
|
|
on,
|
|
MEDIA,
|
|
pick,
|
|
aspect,
|
|
card,
|
|
background,
|
|
expoInOut,
|
|
frac,
|
|
};
|
|
})();
|
|
</script>
|
|
<script>
|
|
(async () => {
|
|
// OrbitGlobe loop family — upright billboards on latitude rings of a
|
|
// sphere whose axis is tilted in the screen plane, one full turn per loop, back side fading.
|
|
// Measured on the 512 preview (2026-09-08):
|
|
// rings: frame-0 similarity has minima every 75 frames (2.5 s) -> 8 cards per ring (45deg,
|
|
// 360deg/20 s). Near cards fall on three lines tilted 27deg: rings at 0 and +-45deg
|
|
// (line offsets +-130px ~ R*sin45 under the fitted camera) -> 24 cards.
|
|
// tilt: a tracked card moves right AND down with slope 0.505 = tan 27deg -> tilt is a
|
|
// rotateZ of the globe axis (CSS +z rotation = clockwise = down-right).
|
|
// size: SCREEN-space: base 112px = 21.9% of H = 0.875 R, times (1 + scaleIntensity*zr),
|
|
// zr = z/R toward the camera (pole card 143px = 112*1.28; same card at zr 0.70 reads
|
|
// 134 = 112*1.196; far pole 81 matches the 75-85px run-length mass).
|
|
// camera: R = globeSize/200 * H (128px), D = 3.4R, f = 0.985*H: limb 154px (= bbox 210 -
|
|
// base/2) and centre speed 2.2px/frame at 18deg/s both fit.
|
|
// spin: uniform, direction=left -> near side moves RIGHT (+6.5px/3 frames) = positive
|
|
// rotateY; 'right' mirrors; 'alternate' unknown (treated as left).
|
|
// backFade: colour x (1 - backFade/100) on the far side; ramp as in globe.html (unverified here).
|
|
// Knobs: RINGS/COLS constants (not derivable from settings), half-pitch stagger on the +-45 rings
|
|
// (guess), motion=waypoints not implemented (only the continuous preview exists), gap unused
|
|
// (billboards overlap in the reference; cards are sized from base, not from a pitch).
|
|
// media list [{src,w,h}] is inlined by build.mjs; HyperFrames scopes inline scripts, so hand both to the shared lib explicitly
|
|
const __V = Object.assign(
|
|
{},
|
|
(window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: null) || {},
|
|
{ loopDuration: 20, loopRepeat: 2, imageCount: 8 },
|
|
);
|
|
const __PH = [
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1050' viewBox%3D'0 0 1400 1050'%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1050' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1046' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 778.05H621.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M286.65 778.05L379.05 662.55L471.45 778.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'552.3' cy%3D'674.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'788' viewBox%3D'0 0 1400 788'%3E%3Crect width%3D'1400' height%3D'788' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'788' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'784' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 583.908H529.376' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'12.382857142857144' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M278.004 583.908L347.348 497.228L416.692 583.908Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'477.36800000000005' cy%3D'505.896' r%3D'26.004' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1050' height%3D'1400' viewBox%3D'0 0 1050 1400'%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1050' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1046' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M189 995.05H558.6' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'16.5' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M223.65 995.05L316.05 879.55L408.45 995.05Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'489.3' cy%3D'891.1' r%3D'34.65' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
"data:image/svg+xml,%3Csvg xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg' width%3D'1400' height%3D'1400' viewBox%3D'0 0 1400 1400'%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'%230b0b0f'%2F%3E%3Crect width%3D'1400' height%3D'1400' fill%3D'rgba(255%2C255%2C255%2C0.18)'%2F%3E%3Crect x%3D'2' y%3D'2' width%3D'1396' height%3D'1396' fill%3D'none' stroke%3D'rgba(255%2C255%2C255%2C0.14)' stroke-width%3D'4'%2F%3E%3Cpath d%3D'M252 1037.4H744.8' stroke%3D'rgba(255%2C255%2C255%2C0.45)' stroke-width%3D'22' stroke-linecap%3D'round'%2F%3E%3Cpath d%3D'M298.2 1037.4L421.4 883.4L544.5999999999999 1037.4Z' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3Ccircle cx%3D'652.4000000000001' cy%3D'898.8' r%3D'46.199999999999996' fill%3D'rgba(255%2C255%2C255%2C0.45)'%2F%3E%3C%2Fsvg%3E",
|
|
];
|
|
const __MEDIA = await Promise.all(
|
|
["image1", "image2", "image3", "image4", "image5", "image6", "image7", "image8"].map(
|
|
async (id, i) => {
|
|
const v = __V[id];
|
|
const src = (v && typeof v === "object" ? v.url || v.src : v) || __PH[i];
|
|
const img = new Image();
|
|
img.src = src;
|
|
try {
|
|
await img.decode();
|
|
} catch (e) {}
|
|
return { src, w: img.naturalWidth || 1400, h: img.naturalHeight || 1050 };
|
|
},
|
|
),
|
|
);
|
|
RF.use(__V, __MEDIA);
|
|
const { W, H, REF, num, str, pick, aspect, background, frac } = RF;
|
|
const DUR = num("loopDuration", 20);
|
|
const FIT = str("mediaFit", "crop");
|
|
const CROP_AR = str("cropAspectRatio", "1:1");
|
|
const GSIZE = num("globeSize", 50);
|
|
const SI = num("scaleIntensity", 0.28);
|
|
const BFADE = Math.min(100, Math.max(0, num("backFade", 55)));
|
|
const TILT = (num("tilt", 27) * Math.PI) / 180;
|
|
const DIR = str("direction", "left") === "right" ? -1 : 1;
|
|
const RADIUS = (num("borderRadius", 11) * REF) / 1080;
|
|
const IMAGES = pick(num("imageCount", 16));
|
|
const N = IMAGES.length;
|
|
const RINGS = [0, 45, -45],
|
|
COLS = 8; // ponytail: measured on the one preview
|
|
|
|
document.getElementById("loop-orbitglobe-loop").dataset.duration = DUR;
|
|
background(document.getElementById("loop-orbitglobe-bg"));
|
|
|
|
const R = (GSIZE / 200) * REF,
|
|
D = 3.4 * R,
|
|
f = 0.985 * REF,
|
|
BASE = 0.875 * R;
|
|
const stage = document.getElementById("loop-orbitglobe-stage");
|
|
stage.style.perspective = f + "px";
|
|
const world = document.getElementById("loop-orbitglobe-world");
|
|
const CZ = f - D; // camera at stage z = f, centre D in front
|
|
|
|
const cards = [];
|
|
RINGS.forEach((latDeg, ri) => {
|
|
const lat = (latDeg * Math.PI) / 180,
|
|
phase = ((ri % 2) * Math.PI) / COLS;
|
|
for (let j = 0; j < COLS; j++) {
|
|
const m = IMAGES[cards.length % N],
|
|
a = aspect(m, FIT, CROP_AR);
|
|
const el = document.createElement("div");
|
|
el.className = "card";
|
|
const img = document.createElement("img");
|
|
img.src = m.src;
|
|
const shade = document.createElement("div");
|
|
shade.className = "shade";
|
|
el.appendChild(img);
|
|
el.appendChild(shade);
|
|
world.appendChild(el);
|
|
cards.push({ el, shade, a, lat, lon: (j * 2 * Math.PI) / COLS + phase });
|
|
}
|
|
});
|
|
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
function renderAt(t) {
|
|
const ang = DIR * 2 * Math.PI * frac(t),
|
|
ct = Math.cos(TILT),
|
|
st = Math.sin(TILT);
|
|
for (const c of cards) {
|
|
// ring point, spun about the globe's y axis (CSS-handed), then the axis tilted by rotateZ
|
|
const cl = Math.cos(c.lat),
|
|
lo = c.lon + ang;
|
|
const x0 = cl * Math.sin(lo),
|
|
y0 = -Math.sin(c.lat),
|
|
zr = cl * Math.cos(lo);
|
|
const dx = x0 * ct - y0 * st,
|
|
dy = x0 * st + y0 * ct;
|
|
const depth = D - zr * R;
|
|
const screen = BASE * (1 + SI * zr),
|
|
k = depth / f; // undo the perspective division -> screen-space size
|
|
const h = screen * k,
|
|
w = h * c.a;
|
|
c.el.style.width = w + "px";
|
|
c.el.style.height = h + "px";
|
|
c.el.style.borderRadius = RADIUS * k + "px";
|
|
c.shade.style.opacity = (BFADE / 100) * clamp01((0.85 - zr) / 0.4);
|
|
c.el.style.transform = `translate3d(${dx * R}px,${dy * R}px,${CZ + zr * R}px) translate(-50%,-50%)`;
|
|
}
|
|
}
|
|
// one paused linear tween drives t; every frame is recomputed from t alone (seek-safe)
|
|
const state = { t: 0 };
|
|
const tl = gsap.timeline({ paused: true });
|
|
tl.to(state, { t: 1, duration: DUR, ease: "none", onUpdate: () => renderAt(state.t) }, 0);
|
|
renderAt(0);
|
|
window.__timelines = window.__timelines || {};
|
|
window.__timelines["loop-orbitglobe"] = tl;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|