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>
323 lines
32 KiB
HTML
Vendored
323 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":"16:9","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":"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":70,"min":0,"max":140,"step":1},{"id":"gap","type":"number","role":"style","label":"Template options · Gap","description":"Spacing between cards.","default":2.5,"min":0,"max":5,"step":0.1,"unit":"px"},{"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":0,"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-sphereglobe</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-sphereglobe-bg {
|
|
position: absolute;
|
|
inset: 0;
|
|
}
|
|
#loop-sphereglobe-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-sphereglobe-globe,
|
|
#loop-sphereglobe-spin {
|
|
position: absolute;
|
|
left: 50%;
|
|
top: 50%;
|
|
width: 0;
|
|
height: 0;
|
|
transform-style: preserve-3d;
|
|
}
|
|
/* origin 0 0: transform is pos * rotate * translate(-50%,-50%); with the default centre origin rotated cards drift by up to w/2 (coordinator fix 2026-09-08, same bug as 3d.html round 2) */
|
|
.card {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
overflow: hidden;
|
|
transform-origin: 0 0;
|
|
}
|
|
.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-sphereglobe-root"
|
|
data-composition-id="loop-sphereglobe"
|
|
data-start="0"
|
|
data-width="1080"
|
|
data-height="1080"
|
|
data-duration="20"
|
|
>
|
|
<div
|
|
id="loop-sphereglobe-loop"
|
|
class="clip"
|
|
data-start="0"
|
|
data-duration="20"
|
|
data-track-index="1"
|
|
data-layout-allow-overflow
|
|
>
|
|
<div id="loop-sphereglobe-bg"></div>
|
|
<div id="loop-sphereglobe-stage">
|
|
<div id="loop-sphereglobe-globe"><div id="loop-sphereglobe-spin"></div></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 () => {
|
|
// SphereGlobe loop family — a brick globe: 16:9 cards tangent to a
|
|
// sphere on latitude rings, back side showing through the gaps, one full turn per loop.
|
|
// Measured on the 512 preview (2026-09-08):
|
|
// rings: 15 rows, 12deg pitch (row centres y=187/125/78/45/27 <-> lat 12/24/36/48/60 under
|
|
// the fitted camera); equator ring = 20 cards (geometry repeats every 30 frames =
|
|
// 1 s = one 18deg pitch; 360deg/20 s loop). Other rings hold round(20*cos(lat)) cards
|
|
// so bricks keep one width (visible rows are uniform, not meridian-shrunk).
|
|
// camera: D = 3R, f = 1.324*H (limb 237px with R = 0.35*H; near-pole card 100px predicted vs
|
|
// 102 measured; +-1 cards shrink 0.94/0.96 -> D/R 2.7..2.8). R = globeSize/200 * H
|
|
// (globeSize 70 -> 179px @512) is the only reading that makes the limb come out.
|
|
// cards: width = ring pitch - gap, height = width * 9/16 (central card 102x58); gap reads
|
|
// ~3.2 world px @512 for gap=2.5 -> gap * 0.0025*H (unit calibrated, not derived).
|
|
// spin: uniform; direction=left -> near side moves RIGHT (+3.5px/frame at centre, gap
|
|
// tracking f0->f5->f10) = positive rotateY. 'right' mirrors; 'alternate' unknown
|
|
// (treated as left).
|
|
// backFade: far cards' colour x (1 - backFade/100) (through-gap cards 57-64 = shades x0.45).
|
|
// Not implemented (no preview exercises them): motion=waypoints/waypoints-move, stops, zoom,
|
|
// focusStyle, scaleIntensity (no size gradient visible at 0.2), tilt (used: rotateZ like Orbit).
|
|
// Knobs: ROWS/COLS constants below, half-pitch stagger on odd rings (guess; ref offsets irregular).
|
|
// 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", "16:9");
|
|
const GSIZE = num("globeSize", 70);
|
|
const GAP = num("gap", 2.5) * 0.0025 * REF;
|
|
const BFADE = Math.min(100, Math.max(0, num("backFade", 55)));
|
|
const TILT = num("tilt", 0);
|
|
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 ROWS = 15,
|
|
COLS = 20; // ponytail: measured on the one preview; not derivable from settings
|
|
|
|
document.getElementById("loop-sphereglobe-loop").dataset.duration = DUR;
|
|
background(document.getElementById("loop-sphereglobe-bg"));
|
|
|
|
const R = (GSIZE / 200) * REF,
|
|
D = 3 * R,
|
|
f = 1.324 * REF;
|
|
const stage = document.getElementById("loop-sphereglobe-stage");
|
|
stage.style.perspective = f + "px";
|
|
document.getElementById("loop-sphereglobe-globe").style.transform =
|
|
`translate3d(0,0,${f - D}px) rotateZ(${TILT}deg)`;
|
|
const spin = document.getElementById("loop-sphereglobe-spin");
|
|
|
|
const cards = [];
|
|
let idx = 0;
|
|
for (let k = -(ROWS - 1) / 2; k <= (ROWS - 1) / 2; k++) {
|
|
const lat = (k * 180) / ROWS,
|
|
cl = Math.cos((lat * Math.PI) / 180);
|
|
const n = Math.max(1, Math.round(COLS * cl));
|
|
const pitch = (2 * Math.PI * R * cl) / n,
|
|
w = Math.max(1, pitch - GAP);
|
|
const phase = ((k % 2 ? 0.5 : 0) * 360) / n;
|
|
for (let j = 0; j < n; j++, idx++) {
|
|
const m = IMAGES[idx % N],
|
|
h = w / aspect(m, FIT, CROP_AR);
|
|
const el = document.createElement("div");
|
|
el.className = "card";
|
|
el.style.cssText = `width:${w}px;height:${h}px;border-radius:${RADIUS}px;`;
|
|
const img = document.createElement("img");
|
|
img.src = m.src;
|
|
const shade = document.createElement("div");
|
|
shade.className = "shade";
|
|
el.appendChild(img);
|
|
el.appendChild(shade);
|
|
const lon = (j * 360) / n + phase;
|
|
// rotateX(+lat) lifts the card (CSS y is down); tangent, upright
|
|
el.style.transform = `rotateY(${lon}deg) rotateX(${lat}deg) translate3d(0,0,${R}px) translate(-50%,-50%)`;
|
|
spin.appendChild(el);
|
|
cards.push({ shade, lon: (lon * Math.PI) / 180, cl });
|
|
}
|
|
}
|
|
const clamp01 = (x) => (x < 0 ? 0 : x > 1 ? 1 : x);
|
|
function renderAt(t) {
|
|
const ang = DIR * 360 * frac(t);
|
|
spin.style.transform = `rotateY(${ang}deg)`;
|
|
const a = (ang * Math.PI) / 180;
|
|
for (const c of cards) {
|
|
const zr = c.cl * Math.cos(c.lon + a); // +1 = nearest pole (tilt is about z, keeps it)
|
|
c.shade.style.opacity = (BFADE / 100) * clamp01((0.85 - zr) / 0.4);
|
|
}
|
|
}
|
|
// 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-sphereglobe"] = tl;
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|