mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-11 14:50:02 +00:00
* fix(skills): clear Snyk findings and harden supply-chain surface
Address the security-audit findings on the published skills with no change to
any skill's behaviour.
- media-use: resolve.test.mjs runs resolve.mjs via execFileSync with an argv
array instead of execSync(`node … "${tmp}" …`), removing the command-injection
(CWE-78) sink that drove the Snyk Fail.
- music-to-video: replace dynamic `element.innerHTML = <var>` with a setSvg()
helper (DOMParser image/svg+xml + importNode, text fallback) in the
intro-kinetic-cascade and logo-split-lockup-pulse frame templates, clearing the
DOM-XSS (CWE-79) Snyk Fail. Renders identical SVG.
- pr-to-video: fetch-people-avatars.mjs refuses any avatar URL that is not https
on a GitHub avatar host (SSRF guard) and only writes under the project dir
(path-traversal guard); best-effort, always-exit-0 behaviour is unchanged.
- embedded-captions: pin `uvx --from whisperx==3.8.6` (overridable via
$WHISPERX_VERSION) so transcription no longer resolves "latest" at runtime.
- gsap: add Subresource Integrity (integrity + crossorigin) to the 8 render-time
CDN GSAP <script> tags across embedded-captions, music-to-video,
faceless-explainer, pr-to-video and product-launch-video.
- hyperframes-animation / hyperframes-creative: document package-loader's
defense-in-depth and note that the installLine strings are display-only.
Verified: media-use resolve (12/12), probe injection (1/1) and manifest (19/19)
tests pass; avatar host-allowlist checks pass; all changed JS passes node --check
and oxfmt.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(skills): clarify product-launch-video vs website-to-video routing
Sharpen the router's product-vs-site decision in hyperframes/SKILL.md: the
split is now "is the site selling a product?" — yes (SaaS / app / product /
company site) → /product-launch-video (a promo; the default for any commercial
URL, even if the site is only named); no, or the user just wants the site shown
as-is (portfolio / blog / docs / personal / event) → /website-to-video (a tour).
Updates the workflow table, the disambiguation bullet, and both workflows'
Input/Output blurbs to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* style(skills): satisfy oxfmt in the two music-to-video templates
The CI Format job runs `oxfmt --check .`, which also formats embedded <script> in .html. Reflow the setSvg() blocks added for the DOM-XSS fix to oxfmt's wrapping — no logic change. Regenerate the music-to-video manifest hash to match.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(skills): sanitize SVG in music-to-video templates (real CWE-79 fix)
Addresses @Magi's review: the previous setSvg() only swapped the sink
(innerHTML → DOMParser + importNode) but did NOT sanitize, so active SVG
content still executed on insertion into the live document. Verified in
headless Chrome that the old shape fired both an svg `onload` handler and an
inline `<script>`.
setSvg() now runs a default-deny cleanSvg() over the parsed tree before it ever
enters the document: only an allow-list of inert drawing elements
(svg/g/path/line/rect/circle/… ) and presentation attributes
(d/fill/stroke/viewBox/…) survives. Every other element (`<script>`, `<image>`,
`<use>`, `<foreignObject>`, `<a>`, `<animate>`, …), every `on*` handler, and
href/xlink:href/style are stripped — on the root node too. Non-SVG or malformed
input still falls back to textContent.
Trusted content (the bundled icon library + the default spark/cloud marks)
renders byte-identically; only hostile markup in vars.icon / leftMark / rightMark
is neutralized.
Browser-verified (headless Chrome, both templates' helper):
old setSvg → fired ["script","onload"]
new setSvg → fired [] · trusted icon still renders · 0 danger nodes · 0 on* attrs
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
597 lines
26 KiB
HTML
597 lines
26 KiB
HTML
<!doctype html>
|
|
<html
|
|
lang="en"
|
|
data-composition-variables='[
|
|
{"id":"bgColor", "type":"color", "label":"Background", "default":"#249b5f"},
|
|
{"id":"markColor", "type":"color", "label":"Logo mark color", "default":"#e7e3dd"},
|
|
{"id":"textColor", "type":"color", "label":"Word color", "default":"#e7e3dd"},
|
|
{"id":"leftMark", "type":"string", "label":"Left mark SVG (empty = spark)", "default":""},
|
|
{"id":"rightMark", "type":"string", "label":"Right mark SVG (empty = cloud)", "default":""},
|
|
{"id":"word1", "type":"string", "label":"Word 1 (opens the gap)", "default":"lille"},
|
|
{"id":"word2", "type":"string", "label":"Word 2 (lands on the surge)", "default":"2020"},
|
|
{"id":"word3", "type":"string", "label":"Word 3 (optional)", "default":""},
|
|
{"id":"word4", "type":"string", "label":"Word 4 (optional)", "default":""}
|
|
]'
|
|
>
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=1920, height=1080" />
|
|
<title>Logo Split / Lockup Pulse — group_7 reference impl</title>
|
|
<script src="../../motion-primitives/assets/gsap.min.js"></script>
|
|
<!-- Velocity-driven directional motion blur (registry: motion-blur). Samples each
|
|
target's GSAP x/y per seek and applies a one-sided ghost trail + top blur
|
|
proportional to speed; clears at rest. This is what makes the fast mark moves
|
|
(split / snap) read SILKY instead of stiff. Self-contained (SVG filters). -->
|
|
<script>
|
|
(function () {
|
|
if (!window._hfMbUid) window._hfMbUid = 0;
|
|
window.attachMotionBlur = function (selector, tl, opts) {
|
|
opts = opts || {};
|
|
var blurScale = opts.blurScale !== undefined ? opts.blurScale : 0.008;
|
|
var blurMax = opts.blurMax !== undefined ? opts.blurMax : 20;
|
|
var stretchScale = opts.stretchScale !== undefined ? opts.stretchScale : 0.0002;
|
|
var stretchMax = opts.stretchMax !== undefined ? opts.stretchMax : 0;
|
|
var axis = opts.axis || "both";
|
|
var items = Array.isArray(selector) ? selector : [selector];
|
|
var targets = items.reduce(function (acc, s) {
|
|
if (typeof s === "string") {
|
|
document.querySelectorAll(s).forEach(function (el) {
|
|
acc.push(el);
|
|
});
|
|
} else if (s instanceof Element) {
|
|
acc.push(s);
|
|
}
|
|
return acc;
|
|
}, []);
|
|
var ns = "http://www.w3.org/2000/svg";
|
|
var GHOST_CFG = [
|
|
{ frac: 0.25, slope: 0.55 },
|
|
{ frac: 0.55, slope: 0.28 },
|
|
{ frac: 1.0, slope: 0.1 },
|
|
];
|
|
var state = targets.map(function (el) {
|
|
var uid = "hf-mb-" + window._hfMbUid++;
|
|
var svg = document.createElementNS(ns, "svg");
|
|
svg.setAttribute("style", "position:absolute;width:0;height:0;overflow:hidden;");
|
|
var filter = document.createElementNS(ns, "filter");
|
|
filter.id = uid;
|
|
filter.setAttribute("x", "-110%");
|
|
filter.setAttribute("y", "-25%");
|
|
filter.setAttribute("width", "260%");
|
|
filter.setAttribute("height", "150%");
|
|
var ghosts = GHOST_CFG.map(function (cfg, gi) {
|
|
var feOff = document.createElementNS(ns, "feOffset");
|
|
feOff.setAttribute("in", "SourceGraphic");
|
|
feOff.setAttribute("dx", "0");
|
|
feOff.setAttribute("dy", "0");
|
|
feOff.setAttribute("result", "go" + gi);
|
|
var feGB = document.createElementNS(ns, "feGaussianBlur");
|
|
feGB.setAttribute("in", "go" + gi);
|
|
feGB.setAttribute("stdDeviation", "0 0");
|
|
feGB.setAttribute("result", "gb" + gi);
|
|
var feCT = document.createElementNS(ns, "feComponentTransfer");
|
|
feCT.setAttribute("in", "gb" + gi);
|
|
feCT.setAttribute("result", "gf" + gi);
|
|
var feFuncA = document.createElementNS(ns, "feFuncA");
|
|
feFuncA.setAttribute("type", "linear");
|
|
feFuncA.setAttribute("slope", String(cfg.slope));
|
|
feCT.appendChild(feFuncA);
|
|
filter.appendChild(feOff);
|
|
filter.appendChild(feGB);
|
|
filter.appendChild(feCT);
|
|
return { feOff: feOff, feGB: feGB, frac: cfg.frac };
|
|
});
|
|
var feTopBlur = document.createElementNS(ns, "feGaussianBlur");
|
|
feTopBlur.setAttribute("in", "SourceGraphic");
|
|
feTopBlur.setAttribute("stdDeviation", "0 0");
|
|
feTopBlur.setAttribute("result", "top");
|
|
filter.appendChild(feTopBlur);
|
|
var feMerge = document.createElementNS(ns, "feMerge");
|
|
for (var mi = GHOST_CFG.length - 1; mi >= 0; mi--) {
|
|
var mn = document.createElementNS(ns, "feMergeNode");
|
|
mn.setAttribute("in", "gf" + mi);
|
|
feMerge.appendChild(mn);
|
|
}
|
|
var mnTop = document.createElementNS(ns, "feMergeNode");
|
|
mnTop.setAttribute("in", "top");
|
|
feMerge.appendChild(mnTop);
|
|
filter.appendChild(feMerge);
|
|
svg.appendChild(filter);
|
|
document.body.appendChild(svg);
|
|
return {
|
|
el: el,
|
|
ghosts: ghosts,
|
|
feTopBlur: feTopBlur,
|
|
filterId: uid,
|
|
prevX: parseFloat(gsap.getProperty(el, "x")) || 0,
|
|
prevY: parseFloat(gsap.getProperty(el, "y")) || 0,
|
|
prevTime: tl.time(),
|
|
};
|
|
});
|
|
var _proxy = { t: 0 };
|
|
tl.to(
|
|
_proxy,
|
|
{
|
|
t: 1,
|
|
duration: Math.max(tl.duration(), 0.1),
|
|
ease: "none",
|
|
onUpdate: function () {
|
|
var time = tl.time();
|
|
state.forEach(function (s) {
|
|
var x = parseFloat(gsap.getProperty(s.el, "x")) || 0;
|
|
var y = parseFloat(gsap.getProperty(s.el, "y")) || 0;
|
|
var dt = time - s.prevTime;
|
|
if (dt > 0.0005) {
|
|
var vx = axis !== "y" ? (x - s.prevX) / dt : 0;
|
|
var vy = axis !== "x" ? (y - s.prevY) / dt : 0;
|
|
var bx = Math.min(Math.abs(vx) * blurScale, blurMax);
|
|
var by = Math.min(Math.abs(vy) * blurScale, blurMax);
|
|
var bxFinal = axis !== "y" ? bx : Math.max(by * 0.08, 0.4);
|
|
var byFinal = axis !== "x" ? by : Math.max(bx * 0.08, 0.4);
|
|
var active = bx > 0.3 || by > 0.3;
|
|
if (active) {
|
|
s.el.style.filter = "url(#" + s.filterId + ")";
|
|
s.ghosts.forEach(function (g) {
|
|
var dx =
|
|
axis !== "y" ? (vx >= 0 ? -bxFinal * g.frac : bxFinal * g.frac) : 0;
|
|
var dy =
|
|
axis !== "x" ? (vy >= 0 ? -byFinal * g.frac : byFinal * g.frac) : 0;
|
|
g.feOff.setAttribute("dx", dx.toFixed(2));
|
|
g.feOff.setAttribute("dy", dy.toFixed(2));
|
|
var gbx =
|
|
axis !== "y"
|
|
? (bxFinal * g.frac * 0.5).toFixed(2)
|
|
: Math.max(byFinal * g.frac * 0.04, 0.4).toFixed(2);
|
|
var gby =
|
|
axis !== "x"
|
|
? (byFinal * g.frac * 0.5).toFixed(2)
|
|
: Math.max(bxFinal * g.frac * 0.04, 0.4).toFixed(2);
|
|
g.feGB.setAttribute("stdDeviation", gbx + " " + gby);
|
|
});
|
|
s.feTopBlur.setAttribute(
|
|
"stdDeviation",
|
|
(bxFinal * 0.15).toFixed(2) + " " + (byFinal * 0.15).toFixed(2),
|
|
);
|
|
} else {
|
|
s.el.style.filter = "";
|
|
s.ghosts.forEach(function (g) {
|
|
g.feOff.setAttribute("dx", "0");
|
|
g.feOff.setAttribute("dy", "0");
|
|
g.feGB.setAttribute("stdDeviation", "0 0");
|
|
});
|
|
s.feTopBlur.setAttribute("stdDeviation", "0 0");
|
|
}
|
|
s.prevX = x;
|
|
s.prevY = y;
|
|
s.prevTime = time;
|
|
} else if (dt < -0.0005) {
|
|
s.el.style.filter = "";
|
|
s.ghosts.forEach(function (g) {
|
|
g.feOff.setAttribute("dx", "0");
|
|
g.feOff.setAttribute("dy", "0");
|
|
g.feGB.setAttribute("stdDeviation", "0 0");
|
|
});
|
|
s.feTopBlur.setAttribute("stdDeviation", "0 0");
|
|
s.prevX = x;
|
|
s.prevY = y;
|
|
s.prevTime = time;
|
|
}
|
|
});
|
|
},
|
|
},
|
|
0,
|
|
);
|
|
};
|
|
})();
|
|
</script>
|
|
<style>
|
|
@font-face {
|
|
font-family: "Lockup";
|
|
src: url("./assets/fredoka-700.woff2") format("woff2");
|
|
font-weight: 700;
|
|
font-style: normal;
|
|
font-display: block;
|
|
}
|
|
*,
|
|
*::before,
|
|
*::after {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
html,
|
|
body {
|
|
background: var(--bg, #249b5f);
|
|
overflow: hidden;
|
|
}
|
|
#root {
|
|
position: relative;
|
|
width: 1920px;
|
|
height: 1080px;
|
|
overflow: hidden;
|
|
background: var(--bg, #249b5f);
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
/* a 0-size anchor at screen center; marks + words are positioned absolutely
|
|
around it, and the whole group scales/translates for the pulse. */
|
|
#lockup {
|
|
position: relative;
|
|
width: 0;
|
|
height: 0;
|
|
}
|
|
.words {
|
|
position: absolute;
|
|
left: 0;
|
|
top: 0;
|
|
transform: translate(-50%, -50%);
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 34px;
|
|
white-space: nowrap;
|
|
color: var(--text, #e7e3dd);
|
|
font-family: "Lockup", system-ui, sans-serif;
|
|
font-size: 200px;
|
|
font-weight: 700;
|
|
letter-spacing: -0.045em;
|
|
line-height: 0.8;
|
|
}
|
|
.word {
|
|
display: none; /* revealed one per onset on the timeline */
|
|
}
|
|
.mark {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 300px;
|
|
height: 340px;
|
|
color: var(--mark, #e7e3dd);
|
|
}
|
|
.mark svg {
|
|
width: 100%;
|
|
height: 100%;
|
|
display: block;
|
|
overflow: visible;
|
|
}
|
|
/* edge/vertical centering is set in JS via GSAP xPercent/yPercent so it
|
|
composes with the animated x (a CSS % transform would be clobbered). */
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<!-- duration = 2.875s = the green logo lifecycle (seed -> split -> lockup ->
|
|
snap -> pulse); the source then HARD-CUTS to the next group at f70 (2.917s). -->
|
|
<div
|
|
data-hf-id="hf-g7rt"
|
|
id="root"
|
|
data-composition-id="group-seven-logo"
|
|
data-width="1920"
|
|
data-height="1080"
|
|
data-start="0"
|
|
data-duration="2.875"
|
|
data-root="true"
|
|
>
|
|
<div data-hf-id="hf-g7lk" id="lockup">
|
|
<div data-hf-id="hf-g7ml" class="mark left" id="markLeft"></div>
|
|
<div data-hf-id="hf-g7wd" class="words" id="words"></div>
|
|
<div data-hf-id="hf-g7mr" class="mark right" id="markRight"></div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function () {
|
|
// ════════════════════════════════════════════════════════════════
|
|
// LOGO SPLIT / LOCKUP PULSE · structural_type: per_onset_sequential
|
|
// A two-part mark sits joined at center, SPLITS left<->right to open a
|
|
// gap, GROWS a center word-lockup one word per onset (key word on the
|
|
// surge), SNAP-CLOSES on a hit, then PULSES with the roll/hihat bed.
|
|
// Reversed from group_7 (Lille — World Design Capital 2020 sting).
|
|
// ════════════════════════════════════════════════════════════════
|
|
|
|
// ── 1. Content variables (what a user changes) ─────────────────────
|
|
var defaults = {
|
|
bgColor: "#249b5f",
|
|
markColor: "#e7e3dd",
|
|
textColor: "#e7e3dd",
|
|
leftMark: "",
|
|
rightMark: "",
|
|
word1: "lille",
|
|
word2: "2020",
|
|
word3: "",
|
|
word4: "",
|
|
};
|
|
var vars = Object.assign(
|
|
{},
|
|
defaults,
|
|
window.__hyperframes && window.__hyperframes.getVariables
|
|
? window.__hyperframes.getVariables()
|
|
: {},
|
|
);
|
|
|
|
// ── 2. DEFAULT LOGO HALVES (line-art; swap via leftMark / rightMark) ─
|
|
// Either two related SVGs (a left + a right graphic) OR the two halves of
|
|
// one logo. currentColor inherits --mark. Thick rounded line-art so they
|
|
// read as a bold pair bracketing the words (matched to the Lille mark:
|
|
// a left radial SPARK with a hollow hub, a right CLOUD/SWIRL).
|
|
var DEFAULT_LEFT = [
|
|
// radial SPARK: 5 rounded strokes, a tight fan up-and-left from a hollow
|
|
// hub near the text (NOT converging to a needle — reads as a spark/shine).
|
|
// hub at the INNER (right) edge so the spark hugs the text; 5 long strokes
|
|
// fanning ~140 up-and-left — a big radial spark that helps wrap the lockup.
|
|
'<svg viewBox="0 0 150 150" fill="none" stroke="currentColor"',
|
|
' stroke-width="16" stroke-linecap="round">',
|
|
'<line x1="134" y1="66" x2="112" y2="6"/>',
|
|
'<line x1="130" y1="72" x2="66" y2="16"/>',
|
|
'<line x1="126" y1="80" x2="24" y2="42"/>',
|
|
'<line x1="124" y1="88" x2="10" y2="84"/>',
|
|
'<line x1="126" y1="96" x2="36" y2="136"/>',
|
|
"</svg>",
|
|
].join("");
|
|
var DEFAULT_RIGHT = [
|
|
// CLOUD / backwards-3 SWIRL: two bold stacked lobes bulging right with a
|
|
// left-pointing middle notch, opening to the left (the Lille cloud).
|
|
// bold backwards-3 swirl: opening tips at the INNER (left) edge so it hugs the
|
|
// text, bulging right (mass OUTER). Fills the box height to help wrap the lockup.
|
|
'<svg viewBox="0 0 150 150" fill="none" stroke="currentColor"',
|
|
' stroke-width="16" stroke-linecap="round" stroke-linejoin="round">',
|
|
'<path d="M14 40',
|
|
" C50 6 128 14 122 58",
|
|
" C120 86 80 92 54 78",
|
|
" C86 86 128 96 122 128",
|
|
' C116 154 40 158 14 120"/>',
|
|
"</svg>",
|
|
].join("");
|
|
|
|
// ── 3. BEAT MAP (filled from <group>.audiomap.json — see README) ────
|
|
// group_7.audiomap.json: BPM 92.3. ABSOLUTE audio seconds.
|
|
// The marks move on only these DISCRETE anchors — NOT on the sustained hi-hat
|
|
// fill (f17-f86), which is rhythmic texture, not a motion driver. Left + right
|
|
// always move together (mirrored). After the snap the weld HOLDS dead still.
|
|
var ENTRY = 0.083; // f2 — joined bracket pair appears at center (perc onset)
|
|
var SPLIT = 0.25; // f6 — brackets fly apart to full width (perc onset)
|
|
var WORD_ONSETS = [0.25, 1.0]; // one per word; 1.0 = SURGE@1s (f24) carries the key word
|
|
var CLOSE = 2.083; // f50 — snap shut: words vanish + brackets weld back together
|
|
var DUR = 2.875; // f69 — hold, then the source hard-cuts to the next group (f70)
|
|
|
|
var MARK_PAD = 14; // gap between a mark's inner edge and the nearest word
|
|
var WORD_GAP = 34; // must match .words { gap }
|
|
|
|
// ── 4. Build content ───────────────────────────────────────────────
|
|
document.documentElement.style.setProperty("--bg", vars.bgColor);
|
|
document.documentElement.style.setProperty("--mark", vars.markColor);
|
|
document.documentElement.style.setProperty("--text", vars.textColor);
|
|
|
|
// Insert SVG markup as SANITIZED parsed nodes — never innerHTML, and never
|
|
// a raw appendChild. DOMParser builds the tree, then cleanSvg() hard-strips
|
|
// everything that isn't inert drawing: only allow-listed shape elements +
|
|
// presentation attributes survive, so <script>, <image>/<use>/<foreignObject>,
|
|
// javascript: hrefs and every on* handler are removed before the nodes ever
|
|
// enter the live document. A custom mark SVG therefore can't smuggle active
|
|
// content (CWE-79 DOM-XSS). Non-SVG / malformed input falls back to text.
|
|
var SVG_OK_TAGS = {
|
|
svg: 1,
|
|
g: 1,
|
|
path: 1,
|
|
line: 1,
|
|
polyline: 1,
|
|
polygon: 1,
|
|
rect: 1,
|
|
circle: 1,
|
|
ellipse: 1,
|
|
defs: 1,
|
|
lineargradient: 1,
|
|
radialgradient: 1,
|
|
stop: 1,
|
|
clippath: 1,
|
|
title: 1,
|
|
desc: 1,
|
|
text: 1,
|
|
tspan: 1,
|
|
};
|
|
var SVG_OK_ATTRS = {
|
|
viewbox: 1,
|
|
xmlns: 1,
|
|
width: 1,
|
|
height: 1,
|
|
fill: 1,
|
|
"fill-rule": 1,
|
|
"fill-opacity": 1,
|
|
stroke: 1,
|
|
"stroke-width": 1,
|
|
"stroke-linecap": 1,
|
|
"stroke-linejoin": 1,
|
|
"stroke-dasharray": 1,
|
|
"stroke-dashoffset": 1,
|
|
"stroke-opacity": 1,
|
|
opacity: 1,
|
|
d: 1,
|
|
x: 1,
|
|
y: 1,
|
|
x1: 1,
|
|
y1: 1,
|
|
x2: 1,
|
|
y2: 1,
|
|
cx: 1,
|
|
cy: 1,
|
|
r: 1,
|
|
rx: 1,
|
|
ry: 1,
|
|
points: 1,
|
|
transform: 1,
|
|
offset: 1,
|
|
"stop-color": 1,
|
|
"stop-opacity": 1,
|
|
gradientunits: 1,
|
|
gradienttransform: 1,
|
|
"clip-path": 1,
|
|
"clip-rule": 1,
|
|
class: 1,
|
|
id: 1,
|
|
};
|
|
function cleanSvg(node) {
|
|
var attrs = Array.prototype.slice.call(node.attributes || []);
|
|
for (var a = 0; a < attrs.length; a++) {
|
|
if (!SVG_OK_ATTRS[attrs[a].name.toLowerCase()]) node.removeAttribute(attrs[a].name);
|
|
}
|
|
var kids = Array.prototype.slice.call(node.childNodes);
|
|
for (var k = 0; k < kids.length; k++) {
|
|
var c = kids[k];
|
|
if (c.nodeType === 1) {
|
|
if (SVG_OK_TAGS[(c.localName || c.nodeName).toLowerCase()]) cleanSvg(c);
|
|
else node.removeChild(c);
|
|
} else if (c.nodeType !== 3) {
|
|
node.removeChild(c);
|
|
}
|
|
}
|
|
}
|
|
function setSvg(el, markup) {
|
|
el.textContent = "";
|
|
var doc = new DOMParser().parseFromString(String(markup), "image/svg+xml");
|
|
var root = doc.documentElement;
|
|
if (
|
|
!root ||
|
|
root.nodeName.toLowerCase() !== "svg" ||
|
|
doc.getElementsByTagName("parsererror").length
|
|
) {
|
|
el.textContent = String(markup);
|
|
return;
|
|
}
|
|
cleanSvg(root);
|
|
el.appendChild(document.importNode(root, true));
|
|
}
|
|
|
|
var markLeft = document.getElementById("markLeft");
|
|
var markRight = document.getElementById("markRight");
|
|
setSvg(markLeft, (vars.leftMark && vars.leftMark.trim()) || DEFAULT_LEFT);
|
|
setSvg(markRight, (vars.rightMark && vars.rightMark.trim()) || DEFAULT_RIGHT);
|
|
|
|
var wordsEl = document.getElementById("words");
|
|
var wordList = [vars.word1, vars.word2, vars.word3, vars.word4].filter(function (w) {
|
|
return w && String(w).trim() !== "";
|
|
});
|
|
var wordEls = wordList.map(function (w) {
|
|
var s = document.createElement("span");
|
|
s.className = "word";
|
|
s.textContent = w;
|
|
wordsEl.appendChild(s);
|
|
return s;
|
|
});
|
|
|
|
// ── 5. Build the timeline once the lockup font is ready ─────────────
|
|
// The word widths drive how far the marks open, so they MUST be measured
|
|
// in the real (rounded) face, not a fallback. Gate the whole build on
|
|
// document.fonts so the measurement is correct; the render engine also
|
|
// waits for fonts before capturing.
|
|
function build() {
|
|
// measure each word in the loaded face, then hide them all so a hidden
|
|
// word occupies NO layout width (the lockup truly grows as words enter).
|
|
var widths = wordEls.map(function (el) {
|
|
el.style.display = "inline-block";
|
|
var w = el.offsetWidth;
|
|
el.style.display = "none";
|
|
return w;
|
|
});
|
|
function markOffsetFor(k) {
|
|
if (k <= 0) return 0;
|
|
var blockW = 0;
|
|
for (var i = 0; i < k; i++) blockW += widths[i];
|
|
blockW += WORD_GAP * (k - 1);
|
|
return blockW / 2 + MARK_PAD;
|
|
}
|
|
|
|
var OFF_FULL = markOffsetFor(wordEls.length); // open width (brackets the full lockup)
|
|
var OVL = 14; // joined position: welded brackets sit close (small center gap)
|
|
|
|
window.__timelines = window.__timelines || {};
|
|
var tl = gsap.timeline({ paused: true });
|
|
|
|
// start state: the two brackets JOINED at center (full-outside / empty-middle),
|
|
// invisible; words hidden. xPercent/yPercent center each mark on the anchor and
|
|
// COMPOSE with the animated x (a CSS % transform would be clobbered by GSAP x).
|
|
gsap.set(markLeft, { autoAlpha: 0, xPercent: -100, yPercent: -50, x: OVL, scale: 0.72 });
|
|
gsap.set(markRight, { autoAlpha: 0, xPercent: 0, yPercent: -50, x: -OVL, scale: 0.72 });
|
|
gsap.set("#lockup", { scale: 1, transformOrigin: "50% 50%" });
|
|
|
|
// ── MOTION = the few DISCRETE anchors only (the hi-hat bed is texture, not
|
|
// a motion driver). Left + right always move TOGETHER (mirrored). ──
|
|
|
|
// f2 ENTRY — the joined bracket pair BLOOMS in at center (smooth scale-in,
|
|
// not a 1-frame pop — the soft grow is what makes the start feel silky).
|
|
tl.to(
|
|
[markLeft, markRight],
|
|
{ autoAlpha: 1, scale: 1, duration: 0.16, ease: "power2.out" },
|
|
ENTRY,
|
|
);
|
|
|
|
// f6 SPLIT — both brackets GLIDE apart together to the FULL lockup width in ONE
|
|
// move: a fast launch + long settle (expo.out), and the velocity motion blur
|
|
// (attached below) smooths the travel so it reads silky, not stiff. Then HOLD
|
|
// — words fill the reserved center space; the marks don't track each word.
|
|
tl.to(markLeft, { x: -OFF_FULL, duration: 0.3, ease: "expo.out" }, SPLIT);
|
|
tl.to(markRight, { x: OFF_FULL, duration: 0.3, ease: "expo.out" }, SPLIT);
|
|
|
|
// word reveals — each word eases in with a soft stretch on its onset (the key
|
|
// word on the SURGE). Smooth deceleration (power3.out), NO poppy overshoot.
|
|
wordEls.forEach(function (el, i) {
|
|
var t =
|
|
i < WORD_ONSETS.length
|
|
? WORD_ONSETS[i]
|
|
: WORD_ONSETS[WORD_ONSETS.length - 1] + 0.3 * (i - WORD_ONSETS.length + 1);
|
|
tl.set(el, { display: "inline-block" }, t);
|
|
tl.fromTo(
|
|
el,
|
|
{ autoAlpha: 0, scaleX: 0.58, scaleY: 1.09, transformOrigin: "50% 50%" },
|
|
{ autoAlpha: 1, scaleX: 1, scaleY: 1, duration: 0.24, ease: "power3.out" },
|
|
t,
|
|
);
|
|
});
|
|
|
|
// f50 SNAP-CLOSE — 1 frame: words vanish + both brackets teleport back together
|
|
// (the teleport reads as an impact streak through the motion blur).
|
|
tl.set(wordEls, { autoAlpha: 0, display: "none" }, CLOSE);
|
|
tl.set(markLeft, { x: OVL }, CLOSE);
|
|
tl.set(markRight, { x: -OVL }, CLOSE);
|
|
tl.fromTo(
|
|
"#lockup",
|
|
{ scale: 0.9 },
|
|
{ scale: 1, duration: 0.22, ease: "power2.out" },
|
|
CLOSE,
|
|
);
|
|
|
|
// SEE-SAW BOUNCE — the welded unit does a damped vertical spring + a building
|
|
// rigid tilt (rotation: +deg = left-spark UP / right-cloud DOWN), beat-synced to
|
|
// the hi-hat hits after the snap: f51 slam-down (lowest), f55 up-rebound (highest),
|
|
// f58 smaller dip, f63 final flick — amplitude decays, ending tilted to the f70 cut.
|
|
var B1 = 2.125,
|
|
B2 = 2.292,
|
|
B3 = 2.417,
|
|
B4 = 2.625; // f51 f55 f58 f63
|
|
tl.set("#lockup", { y: 0, rotation: 0 }, CLOSE);
|
|
tl.to("#lockup", { y: 34, rotation: -3, duration: B1 - CLOSE, ease: "power2.in" }, CLOSE);
|
|
tl.to("#lockup", { y: -64, rotation: 10, duration: B2 - B1, ease: "power3.out" }, B1);
|
|
tl.to("#lockup", { y: -38, rotation: 6, duration: B3 - B2, ease: "power2.inOut" }, B2);
|
|
tl.to("#lockup", { y: -60, rotation: 10.5, duration: B4 - B3, ease: "power2.inOut" }, B3);
|
|
tl.to("#lockup", { y: -56, rotation: 10.5, duration: DUR - B4, ease: "sine.out" }, B4);
|
|
|
|
// pin duration, then attach velocity motion blur: the brackets (x: split + snap)
|
|
// and the welded group (y: the ending bounce). MUST be after all tweens.
|
|
tl.set(document.body, {}, DUR);
|
|
attachMotionBlur([markLeft, markRight], tl, { axis: "x", blurMax: 26, blurScale: 0.011 });
|
|
attachMotionBlur("#lockup", tl, { axis: "y", blurMax: 20, blurScale: 0.012 });
|
|
|
|
tl.seek(0);
|
|
window.__timelines["group-seven-logo"] = tl;
|
|
}
|
|
|
|
if (document.fonts && document.fonts.ready) {
|
|
document.fonts.load('700 200px "Lockup"').then(function () {
|
|
document.fonts.ready.then(build);
|
|
});
|
|
} else {
|
|
build();
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|