commit fd28ef5edcaba12497e19b3c66ff22ec1b23fc72
parent 3dfdb6b792f9128437224c4694e9a57d51404f30
Author: I Mean I'm Just Saying <imeanimjustsaying@kiwifarms.st>
Date: Wed, 30 Sep 2026 21:39:25 -0400
Merge deck/s2-composition (deck slice S2) — chrome-deck.mjs (the deck composition: lifted panel, pip track with per-clip fill, title wipe + blade, trailing subtitle, flipping QR, card slides), compose-chrome port (region dispatch, --still, windows, workers, vendored GSAP, pinned renderer, render cache, qrPng); deckLayout tuned; reviewed by stills
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Diffstat:
5 files changed, 1215 insertions(+), 46 deletions(-)
diff --git a/umtool/report-to-video/assets/gsap.min.js b/umtool/report-to-video/assets/gsap.min.js
@@ -0,0 +1,11 @@
+/*!
+ * GSAP 3.14.2
+ * https://gsap.com
+ *
+ * @license Copyright 2025, GreenSock. All rights reserved.
+ * Subject to the terms at https://gsap.com/standard-license.
+ * @author: Jack Doyle, jack@greensock.com
+ */
+
+!function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t=t||self).window=t.window||{})}(this,function(e){"use strict";function _inheritsLoose(t,e){t.prototype=Object.create(e.prototype),(t.prototype.constructor=t).__proto__=e}function _assertThisInitialized(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function r(t){return"string"==typeof t}function s(t){return"function"==typeof t}function t(t){return"number"==typeof t}function u(t){return void 0===t}function v(t){return"object"==typeof t}function w(t){return!1!==t}function x(){return"undefined"!=typeof window}function y(t){return s(t)||r(t)}function R(t){return(i=bt(t,ht))&&Fe}function S(t,e){return console.warn("Invalid property",t,"set to",e,"Missing plugin? gsap.registerPlugin()")}function T(t,e){return!e&&console.warn(t)}function U(t,e){return t&&(ht[t]=e)&&i&&(i[t]=e)||ht}function V(){return 0}function ga(t){var e,r,i=t[0];if(v(i)||s(i)||(t=[t]),!(e=(i._gsap||{}).harness)){for(r=yt.length;r--&&!yt[r].targetTest(i););e=yt[r]}for(r=t.length;r--;)t[r]&&(t[r]._gsap||(t[r]._gsap=new Xt(t[r],e)))||t.splice(r,1);return t}function ha(t){return t._gsap||ga(Pt(t))[0]._gsap}function ia(t,e,r){return(r=t[e])&&s(r)?t[e]():u(r)&&t.getAttribute&&t.getAttribute(e)||r}function ja(t,e){return(t=t.split(",")).forEach(e)||t}function ka(t){return Math.round(1e5*t)/1e5||0}function la(t){return Math.round(1e7*t)/1e7||0}function ma(t,e){var r=e.charAt(0),i=parseFloat(e.substr(2));return t=parseFloat(t),"+"===r?t+i:"-"===r?t-i:"*"===r?t*i:t/i}function na(t,e){for(var r=e.length,i=0;t.indexOf(e[i])<0&&++i<r;);return i<r}function oa(){var t,e,r=pt.length,i=pt.slice(0);for(_t={},t=pt.length=0;t<r;t++)(e=i[t])&&e._lazy&&(e.render(e._lazy[0],e._lazy[1],!0)._lazy=0)}function pa(t){return!!(t._initted||t._startAt||t.add)}function qa(t,e,r,i){pt.length&&!I&&oa(),t.render(e,r,i||!!(I&&e<0&&pa(t))),pt.length&&!I&&oa()}function ra(t){var e=parseFloat(t);return(e||0===e)&&(t+"").match(ot).length<2?e:r(t)?t.trim():t}function sa(t){return t}function ta(t,e){for(var r in e)r in t||(t[r]=e[r]);return t}function wa(t,e){for(var r in e)"__proto__"!==r&&"constructor"!==r&&"prototype"!==r&&(t[r]=v(e[r])?wa(t[r]||(t[r]={}),e[r]):e[r]);return t}function xa(t,e){var r,i={};for(r in t)r in e||(i[r]=t[r]);return i}function ya(t){var e=t.parent||L,r=t.keyframes?function _setKeyframeDefaults(i){return function(t,e){for(var r in e)r in t||"duration"===r&&i||"ease"===r||(t[r]=e[r])}}($(t.keyframes)):ta;if(w(t.inherit))for(;e;)r(t,e.vars.defaults),e=e.parent||e._dp;return t}function Aa(t,e,r,i,n){void 0===r&&(r="_first"),void 0===i&&(i="_last");var a,s=t[i];if(n)for(a=e[n];s&&s[n]>a;)s=s._prev;return s?(e._next=s._next,s._next=e):(e._next=t[r],t[r]=e),e._next?e._next._prev=e:t[i]=e,e._prev=s,e.parent=e._dp=t,e}function Ba(t,e,r,i){void 0===r&&(r="_first"),void 0===i&&(i="_last");var n=e._prev,a=e._next;n?n._next=a:t[r]===e&&(t[r]=a),a?a._prev=n:t[i]===e&&(t[i]=n),e._next=e._prev=e.parent=null}function Ca(t,e){t.parent&&(!e||t.parent.autoRemoveChildren)&&t.parent.remove&&t.parent.remove(t),t._act=0}function Da(t,e){if(t&&(!e||e._end>t._dur||e._start<0))for(var r=t;r;)r._dirty=1,r=r.parent;return t}function Fa(t,e,r,i){return t._startAt&&(I?t._startAt.revert(ft):t.vars.immediateRender&&!t.vars.autoRevert||t._startAt.render(e,!0,i))}function Ha(t){return t._repeat?wt(t._tTime,t=t.duration()+t._rDelay)*t:0}function Ja(t,e){return(t-e._start)*e._ts+(0<=e._ts?0:e._dirty?e.totalDuration():e._tDur)}function Ka(t){return t._end=la(t._start+(t._tDur/Math.abs(t._ts||t._rts||q)||0))}function La(t,e){var r=t._dp;return r&&r.smoothChildTiming&&t._ts&&(t._start=la(r._time-(0<t._ts?e/t._ts:((t._dirty?t.totalDuration():t._tDur)-e)/-t._ts)),Ka(t),r._dirty||Da(r,t)),t}function Ma(t,e){var r;if((e._time||!e._dur&&e._initted||e._start<t._time&&(e._dur||!e.add))&&(r=Ja(t.rawTime(),e),(!e._dur||Mt(0,e.totalDuration(),r)-e._tTime>q)&&e.render(r,!0)),Da(t,e)._dp&&t._initted&&t._time>=t._dur&&t._ts){if(t._dur<t.duration())for(r=t;r._dp;)0<=r.rawTime()&&r.totalTime(r._tTime),r=r._dp;t._zTime=-q}}function Na(e,r,i,n){return r.parent&&Ca(r),r._start=la((t(i)?i:i||e!==L?Ot(e,i,r):e._time)+r._delay),r._end=la(r._start+(r.totalDuration()/Math.abs(r.timeScale())||0)),Aa(e,r,"_first","_last",e._sort?"_start":0),xt(r)||(e._recent=r),n||Ma(e,r),e._ts<0&&La(e,e._tTime),e}function Oa(t,e){return(ht.ScrollTrigger||S("scrollTrigger",e))&&ht.ScrollTrigger.create(e,t)}function Pa(t,e,r,i,n){return Qt(t,e,n),t._initted?!r&&t._pt&&!I&&(t._dur&&!1!==t.vars.lazy||!t._dur&&t.vars.lazy)&&f!==It.frame?(pt.push(t),t._lazy=[n,i],1):void 0:1}function Ua(t,e,r,i){var n=t._repeat,a=la(e)||0,s=t._tTime/t._tDur;return s&&!i&&(t._time*=a/t._dur),t._dur=a,t._tDur=n?n<0?1e10:la(a*(n+1)+t._rDelay*n):a,0<s&&!i&&La(t,t._tTime=t._tDur*s),t.parent&&Ka(t),r||Da(t.parent,t),t}function Va(t){return t instanceof Zt?Da(t):Ua(t,t._dur)}function Ya(e,r,i){var n,a,s=t(r[1]),o=(s?2:1)+(e<2?0:1),u=r[o];if(s&&(u.duration=r[1]),u.parent=i,e){for(n=u,a=i;a&&!("immediateRender"in n);)n=a.vars.defaults||{},a=w(a.vars.inherit)&&a.parent;u.immediateRender=w(n.immediateRender),e<2?u.runBackwards=1:u.startAt=r[o-1]}return new te(r[0],u,r[1+o])}function Za(t,e){return t||0===t?e(t):e}function _a(t,e){return r(t)&&(e=ut.exec(t))?e[1]:""}function cb(t,e){return t&&v(t)&&"length"in t&&(!e&&!t.length||t.length-1 in t&&v(t[0]))&&!t.nodeType&&t!==h}function fb(r){return r=Pt(r)[0]||T("Invalid scope")||{},function(t){var e=r.current||r.nativeElement||r;return Pt(t,e.querySelectorAll?e:e===r?T("Invalid scope")||a.createElement("div"):r)}}function gb(t){return t.sort(function(){return.5-Math.random()})}function hb(t){if(s(t))return t;var p=v(t)?t:{each:t},_=Vt(p.ease),m=p.from||0,g=parseFloat(p.base)||0,y={},e=0<m&&m<1,T=isNaN(m)||e,b=p.axis,w=m,x=m;return r(m)?w=x={center:.5,edges:.5,end:1}[m]||0:!e&&T&&(w=m[0],x=m[1]),function(t,e,r){var i,n,a,s,o,u,h,l,f,d=(r||p).length,c=y[d];if(!c){if(!(f="auto"===p.grid?0:(p.grid||[1,X])[1])){for(h=-X;h<(h=r[f++].getBoundingClientRect().left)&&f<d;);f<d&&f--}for(c=y[d]=[],i=T?Math.min(f,d)*w-.5:m%f,n=f===X?0:T?d*x/f-.5:m/f|0,l=X,u=h=0;u<d;u++)a=u%f-i,s=n-(u/f|0),c[u]=o=b?Math.abs("y"===b?s:a):J(a*a+s*s),h<o&&(h=o),o<l&&(l=o);"random"===m&&gb(c),c.max=h-l,c.min=l,c.v=d=(parseFloat(p.amount)||parseFloat(p.each)*(d<f?d-1:b?"y"===b?d/f:f:Math.max(f,d/f))||0)*("edges"===m?-1:1),c.b=d<0?g-d:g,c.u=_a(p.amount||p.each)||0,_=_&&d<0?jt(_):_}return d=(c[t]-c.min)/c.max||0,la(c.b+(_?_(d):d)*c.v)+c.u}}function ib(i){var n=Math.pow(10,((i+"").split(".")[1]||"").length);return function(e){var r=la(Math.round(parseFloat(e)/i)*i*n);return(r-r%1)/n+(t(e)?0:_a(e))}}function jb(h,e){var l,f,r=$(h);return!r&&v(h)&&(l=r=h.radius||X,h.values?(h=Pt(h.values),(f=!t(h[0]))&&(l*=l)):h=ib(h.increment)),Za(e,r?s(h)?function(t){return f=h(t),Math.abs(f-t)<=l?f:t}:function(e){for(var r,i,n=parseFloat(f?e.x:e),a=parseFloat(f?e.y:0),s=X,o=0,u=h.length;u--;)(r=f?(r=h[u].x-n)*r+(i=h[u].y-a)*i:Math.abs(h[u]-n))<s&&(s=r,o=u);return o=!l||s<=l?h[o]:e,f||o===e||t(e)?o:o+_a(e)}:ib(h))}function kb(t,e,r,i){return Za($(t)?!e:!0===r?!!(r=0):!i,function(){return $(t)?t[~~(Math.random()*t.length)]:(r=r||1e-5)&&(i=r<1?Math.pow(10,(r+"").length-2):1)&&Math.floor(Math.round((t-r/2+Math.random()*(e-t+.99*r))/r)*r*i)/i})}function ob(e,r,t){return Za(t,function(t){return e[~~r(t)]})}function rb(t){return t.replace(tt,function(t){var e=t.indexOf("[")+1,r=t.substring(e||7,e?t.indexOf("]"):t.length-1).split(et);return kb(e?r:+r[0],e?0:+r[1],+r[2]||1e-5)})}function ub(t,e,r){var i,n,a,s=t.labels,o=X;for(i in s)(n=s[i]-e)<0==!!r&&n&&o>(n=Math.abs(n))&&(a=i,o=n);return a}function wb(t){return Ca(t),t.scrollTrigger&&t.scrollTrigger.kill(!!I),t.progress()<1&&Dt(t,"onInterrupt"),t}function zb(t){if(t)if(t=!t.name&&t.default||t,x()||t.headless){var e=t.name,r=s(t),i=e&&!r&&t.init?function(){this._props=[]}:t,n={init:V,render:ve,add:Jt,kill:Te,modifier:ye,rawVars:0},a={targetTest:0,get:0,getSetter:le,aliases:{},register:0};if(Lt(),t!==i){if(mt[e])return;ta(i,ta(xa(t,n),a)),bt(i.prototype,bt(n,xa(t,a))),mt[i.prop=e]=i,t.targetTest&&(yt.push(i),ct[e]=1),e=("css"===e?"CSS":e.charAt(0).toUpperCase()+e.substr(1))+"Plugin"}U(e,i),t.register&&t.register(Fe,i,we)}else St.push(t)}function Cb(t,e,r){return(6*(t+=t<0?1:1<t?-1:0)<1?e+(r-e)*t*6:t<.5?r:3*t<2?e+(r-e)*(2/3-t)*6:e)*zt+.5|0}function Db(e,r,i){var n,a,s,o,u,h,l,f,d,c,p=e?t(e)?[e>>16,e>>8&zt,e&zt]:0:Et.black;if(!p){if(","===e.substr(-1)&&(e=e.substr(0,e.length-1)),Et[e])p=Et[e];else if("#"===e.charAt(0)){if(e.length<6&&(e="#"+(n=e.charAt(1))+n+(a=e.charAt(2))+a+(s=e.charAt(3))+s+(5===e.length?e.charAt(4)+e.charAt(4):"")),9===e.length)return[(p=parseInt(e.substr(1,6),16))>>16,p>>8&zt,p&zt,parseInt(e.substr(7),16)/255];p=[(e=parseInt(e.substr(1),16))>>16,e>>8&zt,e&zt]}else if("hsl"===e.substr(0,3))if(p=c=e.match(rt),r){if(~e.indexOf("="))return p=e.match(it),i&&p.length<4&&(p[3]=1),p}else o=+p[0]%360/360,u=p[1]/100,n=2*(h=p[2]/100)-(a=h<=.5?h*(u+1):h+u-h*u),3<p.length&&(p[3]*=1),p[0]=Cb(o+1/3,n,a),p[1]=Cb(o,n,a),p[2]=Cb(o-1/3,n,a);else p=e.match(rt)||Et.transparent;p=p.map(Number)}return r&&!c&&(n=p[0]/zt,a=p[1]/zt,s=p[2]/zt,h=((l=Math.max(n,a,s))+(f=Math.min(n,a,s)))/2,l===f?o=u=0:(d=l-f,u=.5<h?d/(2-l-f):d/(l+f),o=l===n?(a-s)/d+(a<s?6:0):l===a?(s-n)/d+2:(n-a)/d+4,o*=60),p[0]=~~(o+.5),p[1]=~~(100*u+.5),p[2]=~~(100*h+.5)),i&&p.length<4&&(p[3]=1),p}function Eb(t){var r=[],i=[],n=-1;return t.split(Rt).forEach(function(t){var e=t.match(nt)||[];r.push.apply(r,e),i.push(n+=e.length+1)}),r.c=i,r}function Fb(t,e,r){var i,n,a,s,o="",u=(t+o).match(Rt),h=e?"hsla(":"rgba(",l=0;if(!u)return t;if(u=u.map(function(t){return(t=Db(t,e,1))&&h+(e?t[0]+","+t[1]+"%,"+t[2]+"%,"+t[3]:t.join(","))+")"}),r&&(a=Eb(t),(i=r.c).join(o)!==a.c.join(o)))for(s=(n=t.replace(Rt,"1").split(nt)).length-1;l<s;l++)o+=n[l]+(~i.indexOf(l)?u.shift()||h+"0,0,0,0)":(a.length?a:u.length?u:r).shift());if(!n)for(s=(n=t.split(Rt)).length-1;l<s;l++)o+=n[l]+u[l];return o+n[s]}function Ib(t){var e,r=t.join(" ");if(Rt.lastIndex=0,Rt.test(r))return e=Ft.test(r),t[1]=Fb(t[1],e),t[0]=Fb(t[0],e,Eb(t[1])),!0}function Rb(t){var e=(t+"").split("("),r=Bt[e[0]];return r&&1<e.length&&r.config?r.config.apply(null,~t.indexOf("{")?[function _parseObjectInString(t){for(var e,r,i,n={},a=t.substr(1,t.length-3).split(":"),s=a[0],o=1,u=a.length;o<u;o++)r=a[o],e=o!==u-1?r.lastIndexOf(","):r.length,i=r.substr(0,e),n[s]=isNaN(i)?i.replace(Nt,"").trim():+i,s=r.substr(e+1).trim();return n}(e[1])]:function _valueInParentheses(t){var e=t.indexOf("(")+1,r=t.indexOf(")"),i=t.indexOf("(",e);return t.substring(e,~i&&i<r?t.indexOf(")",r+1):r)}(t).split(",").map(ra)):Bt._CE&&Yt.test(t)?Bt._CE("",t):r}function Tb(t,e){for(var r,i=t._first;i;)i instanceof Zt?Tb(i,e):!i.vars.yoyoEase||i._yoyo&&i._repeat||i._yoyo===e||(i.timeline?Tb(i.timeline,e):(r=i._ease,i._ease=i._yEase,i._yEase=r,i._yoyo=e)),i=i._next}function Vb(t,e,r,i){void 0===r&&(r=function easeOut(t){return 1-e(1-t)}),void 0===i&&(i=function easeInOut(t){return t<.5?e(2*t)/2:1-e(2*(1-t))/2});var n,a={easeIn:e,easeOut:r,easeInOut:i};return ja(t,function(t){for(var e in Bt[t]=ht[t]=a,Bt[n=t.toLowerCase()]=r,a)Bt[n+("easeIn"===e?".in":"easeOut"===e?".out":".inOut")]=Bt[t+"."+e]=a[e]}),a}function Wb(e){return function(t){return t<.5?(1-e(1-2*t))/2:.5+e(2*(t-.5))/2}}function Xb(r,t,e){function Lm(t){return 1===t?1:i*Math.pow(2,-10*t)*G((t-a)*n)+1}var i=1<=t?t:1,n=(e||(r?.3:.45))/(t<1?t:1),a=n/Z*(Math.asin(1/i)||0),s="out"===r?Lm:"in"===r?function(t){return 1-Lm(1-t)}:Wb(Lm);return n=Z/n,s.config=function(t,e){return Xb(r,t,e)},s}function Yb(e,r){function Tm(t){return t?--t*t*((r+1)*t+r)+1:0}void 0===r&&(r=1.70158);var t="out"===e?Tm:"in"===e?function(t){return 1-Tm(1-t)}:Wb(Tm);return t.config=function(t){return Yb(e,t)},t}var F,I,l,L,h,n,a,i,o,f,d,c,p,_,m,g,b,k,O,M,C,P,A,D,z,E,B,Y,N={autoSleep:120,force3D:"auto",nullTargetWarn:1,units:{lineHeight:""}},j={duration:.5,overwrite:!1,delay:0},X=1e8,q=1/X,Z=2*Math.PI,W=Z/4,H=0,J=Math.sqrt,Q=Math.cos,G=Math.sin,K="function"==typeof ArrayBuffer&&ArrayBuffer.isView||function(){},$=Array.isArray,tt=/random\([^)]+\)/g,et=/,\s*/g,rt=/(?:-?\.?\d|\.)+/gi,it=/[-+=.]*\d+[.e\-+]*\d*[e\-+]*\d*/g,nt=/[-+=.]*\d+[.e-]*\d*[a-z%]*/g,at=/[-+=.]*\d+\.?\d*(?:e-|e\+)?\d*/gi,st=/[+-]=-?[.\d]+/,ot=/[^,'"\[\]\s]+/gi,ut=/^[+\-=e\s\d]*\d+[.\d]*([a-z]*|%)\s*$/i,ht={},lt={suppressEvents:!0,isStart:!0,kill:!1},ft={suppressEvents:!0,kill:!1},dt={suppressEvents:!0},ct={},pt=[],_t={},mt={},gt={},vt=30,yt=[],Tt="",bt=function _merge(t,e){for(var r in e)t[r]=e[r];return t},wt=function _animationCycle(t,e){var r=Math.floor(t=la(t/e));return t&&r===t?r-1:r},xt=function _isFromOrFromStart(t){var e=t.data;return"isFromStart"===e||"isStart"===e},kt={_start:0,endTime:V,totalDuration:V},Ot=function _parsePosition(t,e,i){var n,a,s,o=t.labels,u=t._recent||kt,h=t.duration()>=X?u.endTime(!1):t._dur;return r(e)&&(isNaN(e)||e in o)?(a=e.charAt(0),s="%"===e.substr(-1),n=e.indexOf("="),"<"===a||">"===a?(0<=n&&(e=e.replace(/=/,"")),("<"===a?u._start:u.endTime(0<=u._repeat))+(parseFloat(e.substr(1))||0)*(s?(n<0?u:i).totalDuration()/100:1)):n<0?(e in o||(o[e]=h),o[e]):(a=parseFloat(e.charAt(n-1)+e.substr(n+1)),s&&i&&(a=a/100*($(i)?i[0]:i).totalDuration()),1<n?_parsePosition(t,e.substr(0,n-1),i)+a:h+a)):null==e?h:+e},Mt=function _clamp(t,e,r){return r<t?t:e<r?e:r},Ct=[].slice,Pt=function toArray(t,e,i){return l&&!e&&l.selector?l.selector(t):!r(t)||i||!n&&Lt()?$(t)?function _flatten(t,e,i){return void 0===i&&(i=[]),t.forEach(function(t){return r(t)&&!e||cb(t,1)?i.push.apply(i,Pt(t)):i.push(t)})||i}(t,i):cb(t)?Ct.call(t,0):t?[t]:[]:Ct.call((e||a).querySelectorAll(t),0)},At=function mapRange(e,t,r,i,n){var a=t-e,s=i-r;return Za(n,function(t){return r+((t-e)/a*s||0)})},Dt=function _callback(t,e,r){var i,n,a,s=t.vars,o=s[e],u=l,h=t._ctx;if(o)return i=s[e+"Params"],n=s.callbackScope||t,r&&pt.length&&oa(),h&&(l=h),a=i?o.apply(n,i):o.call(n),l=u,a},St=[],zt=255,Et={aqua:[0,zt,zt],lime:[0,zt,0],silver:[192,192,192],black:[0,0,0],maroon:[128,0,0],teal:[0,128,128],blue:[0,0,zt],navy:[0,0,128],white:[zt,zt,zt],olive:[128,128,0],yellow:[zt,zt,0],orange:[zt,165,0],gray:[128,128,128],purple:[128,0,128],green:[0,128,0],red:[zt,0,0],pink:[zt,192,203],cyan:[0,zt,zt],transparent:[zt,zt,zt,0]},Rt=function(){var t,e="(?:\\b(?:(?:rgb|rgba|hsl|hsla)\\(.+?\\))|\\B#(?:[0-9a-f]{3,4}){1,2}\\b";for(t in Et)e+="|"+t+"\\b";return new RegExp(e+")","gi")}(),Ft=/hsl[a]?\(/,It=(O=Date.now,M=500,C=33,P=O(),A=P,z=D=1e3/240,g={time:0,frame:0,tick:function tick(){Al(!0)},deltaRatio:function deltaRatio(t){return b/(1e3/(t||60))},wake:function wake(){o&&(!n&&x()&&(h=n=window,a=h.document||{},ht.gsap=Fe,(h.gsapVersions||(h.gsapVersions=[])).push(Fe.version),R(i||h.GreenSockGlobals||!h.gsap&&h||{}),St.forEach(zb)),m="undefined"!=typeof requestAnimationFrame&&requestAnimationFrame,p&&g.sleep(),_=m||function(t){return setTimeout(t,z-1e3*g.time+1|0)},c=1,Al(2))},sleep:function sleep(){(m?cancelAnimationFrame:clearTimeout)(p),c=0,_=V},lagSmoothing:function lagSmoothing(t,e){M=t||1/0,C=Math.min(e||33,M)},fps:function fps(t){D=1e3/(t||240),z=1e3*g.time+D},add:function add(n,t,e){var a=t?function(t,e,r,i){n(t,e,r,i),g.remove(a)}:n;return g.remove(n),E[e?"unshift":"push"](a),Lt(),a},remove:function remove(t,e){~(e=E.indexOf(t))&&E.splice(e,1)&&e<=k&&k--},_listeners:E=[]}),Lt=function _wake(){return!c&&It.wake()},Bt={},Yt=/^[\d.\-M][\d.\-,\s]/,Nt=/["']/g,jt=function _invertEase(e){return function(t){return 1-e(1-t)}},Vt=function _parseEase(t,e){return t&&(s(t)?t:Bt[t]||Rb(t))||e};function Al(t){var e,r,i,n,a=O()-A,s=!0===t;if((M<a||a<0)&&(P+=a-C),(0<(e=(i=(A+=a)-P)-z)||s)&&(n=++g.frame,b=i-1e3*g.time,g.time=i/=1e3,z+=e+(D<=e?4:D-e),r=1),s||(p=_(Al)),r)for(k=0;k<E.length;k++)E[k](i,b,n,t)}function jn(t){return t<Y?B*t*t:t<.7272727272727273?B*Math.pow(t-1.5/2.75,2)+.75:t<.9090909090909092?B*(t-=2.25/2.75)*t+.9375:B*Math.pow(t-2.625/2.75,2)+.984375}ja("Linear,Quad,Cubic,Quart,Quint,Strong",function(t,e){var r=e<5?e+1:e;Vb(t+",Power"+(r-1),e?function(t){return Math.pow(t,r)}:function(t){return t},function(t){return 1-Math.pow(1-t,r)},function(t){return t<.5?Math.pow(2*t,r)/2:1-Math.pow(2*(1-t),r)/2})}),Bt.Linear.easeNone=Bt.none=Bt.Linear.easeIn,Vb("Elastic",Xb("in"),Xb("out"),Xb()),B=7.5625,Y=1/2.75,Vb("Bounce",function(t){return 1-jn(1-t)},jn),Vb("Expo",function(t){return Math.pow(2,10*(t-1))*t+t*t*t*t*t*t*(1-t)}),Vb("Circ",function(t){return-(J(1-t*t)-1)}),Vb("Sine",function(t){return 1===t?1:1-Q(t*W)}),Vb("Back",Yb("in"),Yb("out"),Yb()),Bt.SteppedEase=Bt.steps=ht.SteppedEase={config:function config(t,e){void 0===t&&(t=1);var r=1/t,i=t+(e?0:1),n=e?1:0;return function(t){return((i*Mt(0,.99999999,t)|0)+n)*r}}},j.ease=Bt["quad.out"],ja("onComplete,onUpdate,onStart,onRepeat,onReverseComplete,onInterrupt",function(t){return Tt+=t+","+t+"Params,"});var Ut,Xt=function GSCache(t,e){this.id=H++,(t._gsap=this).target=t,this.harness=e,this.get=e?e.get:ia,this.set=e?e.getSetter:le},qt=((Ut=Animation.prototype).delay=function delay(t){return t||0===t?(this.parent&&this.parent.smoothChildTiming&&this.startTime(this._start+t-this._delay),this._delay=t,this):this._delay},Ut.duration=function duration(t){return arguments.length?this.totalDuration(0<this._repeat?t+(t+this._rDelay)*this._repeat:t):this.totalDuration()&&this._dur},Ut.totalDuration=function totalDuration(t){return arguments.length?(this._dirty=0,Ua(this,this._repeat<0?t:(t-this._repeat*this._rDelay)/(this._repeat+1))):this._tDur},Ut.totalTime=function totalTime(t,e){if(Lt(),!arguments.length)return this._tTime;var r=this._dp;if(r&&r.smoothChildTiming&&this._ts){for(La(this,t),!r._dp||r.parent||Ma(r,this);r&&r.parent;)r.parent._time!==r._start+(0<=r._ts?r._tTime/r._ts:(r.totalDuration()-r._tTime)/-r._ts)&&r.totalTime(r._tTime,!0),r=r.parent;!this.parent&&this._dp.autoRemoveChildren&&(0<this._ts&&t<this._tDur||this._ts<0&&0<t||!this._tDur&&!t)&&Na(this._dp,this,this._start-this._delay)}return(this._tTime!==t||!this._dur&&!e||this._initted&&Math.abs(this._zTime)===q||!this._initted&&this._dur&&t||!t&&!this._initted&&(this.add||this._ptLookup))&&(this._ts||(this._pTime=t),qa(this,t,e)),this},Ut.time=function time(t,e){return arguments.length?this.totalTime(Math.min(this.totalDuration(),t+Ha(this))%(this._dur+this._rDelay)||(t?this._dur:0),e):this._time},Ut.totalProgress=function totalProgress(t,e){return arguments.length?this.totalTime(this.totalDuration()*t,e):this.totalDuration()?Math.min(1,this._tTime/this._tDur):0<=this.rawTime()&&this._initted?1:0},Ut.progress=function progress(t,e){return arguments.length?this.totalTime(this.duration()*(!this._yoyo||1&this.iteration()?t:1-t)+Ha(this),e):this.duration()?Math.min(1,this._time/this._dur):0<this.rawTime()?1:0},Ut.iteration=function iteration(t,e){var r=this.duration()+this._rDelay;return arguments.length?this.totalTime(this._time+(t-1)*r,e):this._repeat?wt(this._tTime,r)+1:1},Ut.timeScale=function timeScale(t,e){if(!arguments.length)return this._rts===-q?0:this._rts;if(this._rts===t)return this;var r=this.parent&&this._ts?Ja(this.parent._time,this):this._tTime;return this._rts=+t||0,this._ts=this._ps||t===-q?0:this._rts,this.totalTime(Mt(-Math.abs(this._delay),this.totalDuration(),r),!1!==e),Ka(this),function _recacheAncestors(t){for(var e=t.parent;e&&e.parent;)e._dirty=1,e.totalDuration(),e=e.parent;return t}(this)},Ut.paused=function paused(t){return arguments.length?(this._ps!==t&&((this._ps=t)?(this._pTime=this._tTime||Math.max(-this._delay,this.rawTime()),this._ts=this._act=0):(Lt(),this._ts=this._rts,this.totalTime(this.parent&&!this.parent.smoothChildTiming?this.rawTime():this._tTime||this._pTime,1===this.progress()&&Math.abs(this._zTime)!==q&&(this._tTime-=q)))),this):this._ps},Ut.startTime=function startTime(t){if(arguments.length){this._start=la(t);var e=this.parent||this._dp;return!e||!e._sort&&this.parent||Na(e,this,this._start-this._delay),this}return this._start},Ut.endTime=function endTime(t){return this._start+(w(t)?this.totalDuration():this.duration())/Math.abs(this._ts||1)},Ut.rawTime=function rawTime(t){var e=this.parent||this._dp;return e?t&&(!this._ts||this._repeat&&this._time&&this.totalProgress()<1)?this._tTime%(this._dur+this._rDelay):this._ts?Ja(e.rawTime(t),this):this._tTime:this._tTime},Ut.revert=function revert(t){void 0===t&&(t=dt);var e=I;return I=t,pa(this)&&(this.timeline&&this.timeline.revert(t),this.totalTime(-.01,t.suppressEvents)),"nested"!==this.data&&!1!==t.kill&&this.kill(),I=e,this},Ut.globalTime=function globalTime(t){for(var e=this,r=arguments.length?t:e.rawTime();e;)r=e._start+r/(Math.abs(e._ts)||1),e=e._dp;return!this.parent&&this._sat?this._sat.globalTime(t):r},Ut.repeat=function repeat(t){return arguments.length?(this._repeat=t===1/0?-2:t,Va(this)):-2===this._repeat?1/0:this._repeat},Ut.repeatDelay=function repeatDelay(t){if(arguments.length){var e=this._time;return this._rDelay=t,Va(this),e?this.time(e):this}return this._rDelay},Ut.yoyo=function yoyo(t){return arguments.length?(this._yoyo=t,this):this._yoyo},Ut.seek=function seek(t,e){return this.totalTime(Ot(this,t),w(e))},Ut.restart=function restart(t,e){return this.play().totalTime(t?-this._delay:0,w(e)),this._dur||(this._zTime=-q),this},Ut.play=function play(t,e){return null!=t&&this.seek(t,e),this.reversed(!1).paused(!1)},Ut.reverse=function reverse(t,e){return null!=t&&this.seek(t||this.totalDuration(),e),this.reversed(!0).paused(!1)},Ut.pause=function pause(t,e){return null!=t&&this.seek(t,e),this.paused(!0)},Ut.resume=function resume(){return this.paused(!1)},Ut.reversed=function reversed(t){return arguments.length?(!!t!==this.reversed()&&this.timeScale(-this._rts||(t?-q:0)),this):this._rts<0},Ut.invalidate=function invalidate(){return this._initted=this._act=0,this._zTime=-q,this},Ut.isActive=function isActive(){var t,e=this.parent||this._dp,r=this._start;return!(e&&!(this._ts&&this._initted&&e.isActive()&&(t=e.rawTime(!0))>=r&&t<this.endTime(!0)-q))},Ut.eventCallback=function eventCallback(t,e,r){var i=this.vars;return 1<arguments.length?(e?(i[t]=e,r&&(i[t+"Params"]=r),"onUpdate"===t&&(this._onUpdate=e)):delete i[t],this):i[t]},Ut.then=function then(t){var i=this,n=i._prom;return new Promise(function(e){function Fo(){var t=i.then;i.then=null,n&&n(),s(r)&&(r=r(i))&&(r.then||r===i)&&(i.then=t),e(r),i.then=t}var r=s(t)?t:sa;i._initted&&1===i.totalProgress()&&0<=i._ts||!i._tTime&&i._ts<0?Fo():i._prom=Fo})},Ut.kill=function kill(){wb(this)},Animation);function Animation(t){this.vars=t,this._delay=+t.delay||0,(this._repeat=t.repeat===1/0?-2:t.repeat||0)&&(this._rDelay=t.repeatDelay||0,this._yoyo=!!t.yoyo||!!t.yoyoEase),this._ts=1,Ua(this,+t.duration,1,1),this.data=t.data,l&&(this._ctx=l).data.push(this),c||It.wake()}ta(qt.prototype,{_time:0,_start:0,_end:0,_tTime:0,_tDur:0,_dirty:0,_repeat:0,_yoyo:!1,parent:null,_initted:!1,_rDelay:0,_ts:1,_dp:0,ratio:0,_zTime:-q,_prom:0,_ps:!1,_rts:1});var Zt=function(i){function Timeline(t,e){var r;return void 0===t&&(t={}),(r=i.call(this,t)||this).labels={},r.smoothChildTiming=!!t.smoothChildTiming,r.autoRemoveChildren=!!t.autoRemoveChildren,r._sort=w(t.sortChildren),L&&Na(t.parent||L,_assertThisInitialized(r),e),t.reversed&&r.reverse(),t.paused&&r.paused(!0),t.scrollTrigger&&Oa(_assertThisInitialized(r),t.scrollTrigger),r}_inheritsLoose(Timeline,i);var e=Timeline.prototype;return e.to=function to(t,e,r){return Ya(0,arguments,this),this},e.from=function from(t,e,r){return Ya(1,arguments,this),this},e.fromTo=function fromTo(t,e,r,i){return Ya(2,arguments,this),this},e.set=function set(t,e,r){return e.duration=0,e.parent=this,ya(e).repeatDelay||(e.repeat=0),e.immediateRender=!!e.immediateRender,new te(t,e,Ot(this,r),1),this},e.call=function call(t,e,r){return Na(this,te.delayedCall(0,t,e),r)},e.staggerTo=function staggerTo(t,e,r,i,n,a,s){return r.duration=e,r.stagger=r.stagger||i,r.onComplete=a,r.onCompleteParams=s,r.parent=this,new te(t,r,Ot(this,n)),this},e.staggerFrom=function staggerFrom(t,e,r,i,n,a,s){return r.runBackwards=1,ya(r).immediateRender=w(r.immediateRender),this.staggerTo(t,e,r,i,n,a,s)},e.staggerFromTo=function staggerFromTo(t,e,r,i,n,a,s,o){return i.startAt=r,ya(i).immediateRender=w(i.immediateRender),this.staggerTo(t,e,i,n,a,s,o)},e.render=function render(t,e,r){var i,n,a,s,o,u,h,l,f,d,c,p,_=this._time,m=this._dirty?this.totalDuration():this._tDur,g=this._dur,v=t<=0?0:la(t),y=this._zTime<0!=t<0&&(this._initted||!g);if(this!==L&&m<v&&0<=t&&(v=m),v!==this._tTime||r||y){if(_!==this._time&&g&&(v+=this._time-_,t+=this._time-_),i=v,f=this._start,u=!(l=this._ts),y&&(g||(_=this._zTime),!t&&e||(this._zTime=t)),this._repeat){if(c=this._yoyo,o=g+this._rDelay,this._repeat<-1&&t<0)return this.totalTime(100*o+t,e,r);if(i=la(v%o),v===m?(s=this._repeat,i=g):((s=~~(d=la(v/o)))&&s===d&&(i=g,s--),g<i&&(i=g)),d=wt(this._tTime,o),!_&&this._tTime&&d!==s&&this._tTime-d*o-this._dur<=0&&(d=s),c&&1&s&&(i=g-i,p=1),s!==d&&!this._lock){var T=c&&1&d,b=T===(c&&1&s);if(s<d&&(T=!T),_=T?0:v%g?g:v,this._lock=1,this.render(_||(p?0:la(s*o)),e,!g)._lock=0,this._tTime=v,!e&&this.parent&&Dt(this,"onRepeat"),this.vars.repeatRefresh&&!p&&(this.invalidate()._lock=1,d=s),_&&_!==this._time||u!=!this._ts||this.vars.onRepeat&&!this.parent&&!this._act)return this;if(g=this._dur,m=this._tDur,b&&(this._lock=2,_=T?g:-1e-4,this.render(_,!0),this.vars.repeatRefresh&&!p&&this.invalidate()),this._lock=0,!this._ts&&!u)return this;Tb(this,p)}}if(this._hasPause&&!this._forcing&&this._lock<2&&(h=function _findNextPauseTween(t,e,r){var i;if(e<r)for(i=t._first;i&&i._start<=r;){if("isPause"===i.data&&i._start>e)return i;i=i._next}else for(i=t._last;i&&i._start>=r;){if("isPause"===i.data&&i._start<e)return i;i=i._prev}}(this,la(_),la(i)))&&(v-=i-(i=h._start)),this._tTime=v,this._time=i,this._act=!l,this._initted||(this._onUpdate=this.vars.onUpdate,this._initted=1,this._zTime=t,_=0),!_&&v&&g&&!e&&!d&&(Dt(this,"onStart"),this._tTime!==v))return this;if(_<=i&&0<=t)for(n=this._first;n;){if(a=n._next,(n._act||i>=n._start)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0<n._ts?(i-n._start)*n._ts:(n._dirty?n.totalDuration():n._tDur)+(i-n._start)*n._ts,e,r),i!==this._time||!this._ts&&!u){h=0,a&&(v+=this._zTime=-q);break}}n=a}else{n=this._last;for(var w=t<0?t:i;n;){if(a=n._prev,(n._act||w<=n._end)&&n._ts&&h!==n){if(n.parent!==this)return this.render(t,e,r);if(n.render(0<n._ts?(w-n._start)*n._ts:(n._dirty?n.totalDuration():n._tDur)+(w-n._start)*n._ts,e,r||I&&pa(n)),i!==this._time||!this._ts&&!u){h=0,a&&(v+=this._zTime=w?-q:q);break}}n=a}}if(h&&!e&&(this.pause(),h.render(_<=i?0:-q)._zTime=_<=i?1:-1,this._ts))return this._start=f,Ka(this),this.render(t,e,r);this._onUpdate&&!e&&Dt(this,"onUpdate",!0),(v===m&&this._tTime>=this.totalDuration()||!v&&_)&&(f!==this._start&&Math.abs(l)===Math.abs(this._ts)||this._lock||(!t&&g||!(v===m&&0<this._ts||!v&&this._ts<0)||Ca(this,1),e||t<0&&!_||!v&&!_&&m||(Dt(this,v===m&&0<=t?"onComplete":"onReverseComplete",!0),!this._prom||v<m&&0<this.timeScale()||this._prom())))}return this},e.add=function add(e,i){var n=this;if(t(i)||(i=Ot(this,i,e)),!(e instanceof qt)){if($(e))return e.forEach(function(t){return n.add(t,i)}),this;if(r(e))return this.addLabel(e,i);if(!s(e))return this;e=te.delayedCall(0,e)}return this!==e?Na(this,e,i):this},e.getChildren=function getChildren(t,e,r,i){void 0===t&&(t=!0),void 0===e&&(e=!0),void 0===r&&(r=!0),void 0===i&&(i=-X);for(var n=[],a=this._first;a;)a._start>=i&&(a instanceof te?e&&n.push(a):(r&&n.push(a),t&&n.push.apply(n,a.getChildren(!0,e,r)))),a=a._next;return n},e.getById=function getById(t){for(var e=this.getChildren(1,1,1),r=e.length;r--;)if(e[r].vars.id===t)return e[r]},e.remove=function remove(t){return r(t)?this.removeLabel(t):s(t)?this.killTweensOf(t):(t.parent===this&&Ba(this,t),t===this._recent&&(this._recent=this._last),Da(this))},e.totalTime=function totalTime(t,e){return arguments.length?(this._forcing=1,!this._dp&&this._ts&&(this._start=la(It.time-(0<this._ts?t/this._ts:(this.totalDuration()-t)/-this._ts))),i.prototype.totalTime.call(this,t,e),this._forcing=0,this):this._tTime},e.addLabel=function addLabel(t,e){return this.labels[t]=Ot(this,e),this},e.removeLabel=function removeLabel(t){return delete this.labels[t],this},e.addPause=function addPause(t,e,r){var i=te.delayedCall(0,e||V,r);return i.data="isPause",this._hasPause=1,Na(this,i,Ot(this,t))},e.removePause=function removePause(t){var e=this._first;for(t=Ot(this,t);e;)e._start===t&&"isPause"===e.data&&Ca(e),e=e._next},e.killTweensOf=function killTweensOf(t,e,r){for(var i=this.getTweensOf(t,r),n=i.length;n--;)Wt!==i[n]&&i[n].kill(t,e);return this},e.getTweensOf=function getTweensOf(e,r){for(var i,n=[],a=Pt(e),s=this._first,o=t(r);s;)s instanceof te?na(s._targets,a)&&(o?(!Wt||s._initted&&s._ts)&&s.globalTime(0)<=r&&s.globalTime(s.totalDuration())>r:!r||s.isActive())&&n.push(s):(i=s.getTweensOf(a,r)).length&&n.push.apply(n,i),s=s._next;return n},e.tweenTo=function tweenTo(t,e){e=e||{};var r,i=this,n=Ot(i,t),a=e.startAt,s=e.onStart,o=e.onStartParams,u=e.immediateRender,h=te.to(i,ta({ease:e.ease||"none",lazy:!1,immediateRender:!1,time:n,overwrite:"auto",duration:e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale())||q,onStart:function onStart(){if(i.pause(),!r){var t=e.duration||Math.abs((n-(a&&"time"in a?a.time:i._time))/i.timeScale());h._dur!==t&&Ua(h,t,0,1).render(h._time,!0,!0),r=1}s&&s.apply(h,o||[])}},e));return u?h.render(0):h},e.tweenFromTo=function tweenFromTo(t,e,r){return this.tweenTo(e,ta({startAt:{time:Ot(this,t)}},r))},e.recent=function recent(){return this._recent},e.nextLabel=function nextLabel(t){return void 0===t&&(t=this._time),ub(this,Ot(this,t))},e.previousLabel=function previousLabel(t){return void 0===t&&(t=this._time),ub(this,Ot(this,t),1)},e.currentLabel=function currentLabel(t){return arguments.length?this.seek(t,!0):this.previousLabel(this._time+q)},e.shiftChildren=function shiftChildren(t,e,r){void 0===r&&(r=0);var i,n=this._first,a=this.labels;for(t=la(t);n;)n._start>=r&&(n._start+=t,n._end+=t),n=n._next;if(e)for(i in a)a[i]>=r&&(a[i]+=t);return Da(this)},e.invalidate=function invalidate(t){var e=this._first;for(this._lock=0;e;)e.invalidate(t),e=e._next;return i.prototype.invalidate.call(this,t)},e.clear=function clear(t){void 0===t&&(t=!0);for(var e,r=this._first;r;)e=r._next,this.remove(r),r=e;return this._dp&&(this._time=this._tTime=this._pTime=0),t&&(this.labels={}),Da(this)},e.totalDuration=function totalDuration(t){var e,r,i,n=0,a=this,s=a._last,o=X;if(arguments.length)return a.timeScale((a._repeat<0?a.duration():a.totalDuration())/(a.reversed()?-t:t));if(a._dirty){for(i=a.parent;s;)e=s._prev,s._dirty&&s.totalDuration(),o<(r=s._start)&&a._sort&&s._ts&&!a._lock?(a._lock=1,Na(a,s,r-s._delay,1)._lock=0):o=r,r<0&&s._ts&&(n-=r,(!i&&!a._dp||i&&i.smoothChildTiming)&&(a._start+=la(r/a._ts),a._time-=r,a._tTime-=r),a.shiftChildren(-r,!1,-Infinity),o=0),s._end>n&&s._ts&&(n=s._end),s=e;Ua(a,a===L&&a._time>n?a._time:n,1,1),a._dirty=0}return a._tDur},Timeline.updateRoot=function updateRoot(t){if(L._ts&&(qa(L,Ja(t,L)),f=It.frame),It.frame>=vt){vt+=N.autoSleep||120;var e=L._first;if((!e||!e._ts)&&N.autoSleep&&It._listeners.length<2){for(;e&&!e._ts;)e=e._next;e||It.sleep()}}},Timeline}(qt);ta(Zt.prototype,{_lock:0,_hasPause:0,_forcing:0});function dc(t,e,i,n,a,o){var u,h,l,f;if(mt[t]&&!1!==(u=new mt[t]).init(a,u.rawVars?e[t]:function _processVars(t,e,i,n,a){if(s(t)&&(t=Gt(t,a,e,i,n)),!v(t)||t.style&&t.nodeType||$(t)||K(t))return r(t)?Gt(t,a,e,i,n):t;var o,u={};for(o in t)u[o]=Gt(t[o],a,e,i,n);return u}(e[t],n,a,o,i),i,n,o)&&(i._pt=h=new we(i._pt,a,t,0,1,u.render,u,0,u.priority),i!==d))for(l=i._ptLookup[i._targets.indexOf(a)],f=u._props.length;f--;)l[u._props[f]]=h;return u}function jc(t,r,e,i){var n,a,s=r.ease||i||"power1.inOut";if($(r))a=e[t]||(e[t]=[]),r.forEach(function(t,e){return a.push({t:e/(r.length-1)*100,v:t,e:s})});else for(n in r)a=e[n]||(e[n]=[]),"ease"===n||a.push({t:parseFloat(t),v:r[n],e:s})}var Wt,Ht,Jt=function _addPropTween(t,e,i,n,a,o,u,h,l,f){s(n)&&(n=n(a||0,t,o));var d,c=t[e],p="get"!==i?i:s(c)?l?t[e.indexOf("set")||!s(t["get"+e.substr(3)])?e:"get"+e.substr(3)](l):t[e]():c,_=s(c)?l?ue:re:ee;if(r(n)&&(~n.indexOf("random(")&&(n=rb(n)),"="===n.charAt(1)&&(!(d=ma(p,n)+(_a(p)||0))&&0!==d||(n=d))),!f||p!==n||Ht)return isNaN(p*n)||""===n?(c||e in t||S(e,n),function _addComplexStringPropTween(t,e,r,i,n,a,s){var o,u,h,l,f,d,c,p,_=new we(this._pt,t,e,0,1,ge,null,n),m=0,g=0;for(_.b=r,_.e=i,r+="",(c=~(i+="").indexOf("random("))&&(i=rb(i)),a&&(a(p=[r,i],t,e),r=p[0],i=p[1]),u=r.match(at)||[];o=at.exec(i);)l=o[0],f=i.substring(m,o.index),h?h=(h+1)%5:"rgba("===f.substr(-5)&&(h=1),l!==u[g++]&&(d=parseFloat(u[g-1])||0,_._pt={_next:_._pt,p:f||1===g?f:",",s:d,c:"="===l.charAt(1)?ma(d,l)-d:parseFloat(l)-d,m:h&&h<4?Math.round:0},m=at.lastIndex);return _.c=m<i.length?i.substring(m,i.length):"",_.fp=s,(st.test(i)||c)&&(_.e=0),this._pt=_}.call(this,t,e,p,n,_,h||N.stringFilter,l)):(d=new we(this._pt,t,e,+p||0,n-(p||0),"boolean"==typeof c?_e:ce,0,_),l&&(d.fp=l),u&&d.modifier(u,this,t),this._pt=d)},Qt=function _initTween(t,e,r){var i,n,a,s,o,u,h,l,f,d,c,p,_,m=t.vars,g=m.ease,v=m.startAt,y=m.immediateRender,T=m.lazy,b=m.onUpdate,x=m.runBackwards,k=m.yoyoEase,O=m.keyframes,M=m.autoRevert,C=t._dur,P=t._startAt,A=t._targets,D=t.parent,S=D&&"nested"===D.data?D.vars.targets:A,z="auto"===t._overwrite&&!F,E=t.timeline;if(!E||O&&g||(g="none"),t._ease=Vt(g,j.ease),t._yEase=k?jt(Vt(!0===k?g:k,j.ease)):0,k&&t._yoyo&&!t._repeat&&(k=t._yEase,t._yEase=t._ease,t._ease=k),t._from=!E&&!!m.runBackwards,!E||O&&!m.stagger){if(p=(l=A[0]?ha(A[0]).harness:0)&&m[l.prop],i=xa(m,ct),P&&(P._zTime<0&&P.progress(1),e<0&&x&&y&&!M?P.render(-1,!0):P.revert(x&&C?ft:lt),P._lazy=0),v){if(Ca(t._startAt=te.set(A,ta({data:"isStart",overwrite:!1,parent:D,immediateRender:!0,lazy:!P&&w(T),startAt:null,delay:0,onUpdate:b&&function(){return Dt(t,"onUpdate")},stagger:0},v))),t._startAt._dp=0,t._startAt._sat=t,e<0&&(I||!y&&!M)&&t._startAt.revert(ft),y&&C&&e<=0&&r<=0)return void(e&&(t._zTime=e))}else if(x&&C&&!P)if(e&&(y=!1),a=ta({overwrite:!1,data:"isFromStart",lazy:y&&!P&&w(T),immediateRender:y,stagger:0,parent:D},i),p&&(a[l.prop]=p),Ca(t._startAt=te.set(A,a)),t._startAt._dp=0,t._startAt._sat=t,e<0&&(I?t._startAt.revert(ft):t._startAt.render(-1,!0)),t._zTime=e,y){if(!e)return}else _initTween(t._startAt,q,q);for(t._pt=t._ptCache=0,T=C&&w(T)||T&&!C,n=0;n<A.length;n++){if(h=(o=A[n])._gsap||ga(A)[n]._gsap,t._ptLookup[n]=d={},_t[h.id]&&pt.length&&oa(),c=S===A?n:S.indexOf(o),l&&!1!==(f=new l).init(o,p||i,t,c,S)&&(t._pt=s=new we(t._pt,o,f.name,0,1,f.render,f,0,f.priority),f._props.forEach(function(t){d[t]=s}),f.priority&&(u=1)),!l||p)for(a in i)mt[a]&&(f=dc(a,i,t,c,o,S))?f.priority&&(u=1):d[a]=s=Jt.call(t,o,a,"get",i[a],c,S,0,m.stringFilter);t._op&&t._op[n]&&t.kill(o,t._op[n]),z&&t._pt&&(Wt=t,L.killTweensOf(o,d,t.globalTime(e)),_=!t.parent,Wt=0),t._pt&&T&&(_t[h.id]=1)}u&&be(t),t._onInit&&t._onInit(t)}t._onUpdate=b,t._initted=(!t._op||t._pt)&&!_,O&&e<=0&&E.render(X,!0,!0)},Gt=function _parseFuncOrString(t,e,i,n,a){return s(t)?t.call(e,i,n,a):r(t)&&~t.indexOf("random(")?rb(t):t},Kt=Tt+"repeat,repeatDelay,yoyo,repeatRefresh,yoyoEase,autoRevert",$t={};ja(Kt+",id,stagger,delay,duration,paused,scrollTrigger",function(t){return $t[t]=1});var te=function(R){function Tween(e,r,i,n){var a;"number"==typeof r&&(i.duration=r,r=i,i=null);var s,o,u,h,l,f,d,c,p=(a=R.call(this,n?r:ya(r))||this).vars,_=p.duration,m=p.delay,g=p.immediateRender,b=p.stagger,x=p.overwrite,k=p.keyframes,O=p.defaults,M=p.scrollTrigger,C=p.yoyoEase,P=r.parent||L,A=($(e)||K(e)?t(e[0]):"length"in r)?[e]:Pt(e);if(a._targets=A.length?ga(A):T("GSAP target "+e+" not found. https://gsap.com",!N.nullTargetWarn)||[],a._ptLookup=[],a._overwrite=x,k||b||y(_)||y(m)){if(r=a.vars,(s=a.timeline=new Zt({data:"nested",defaults:O||{},targets:P&&"nested"===P.data?P.vars.targets:A})).kill(),s.parent=s._dp=_assertThisInitialized(a),s._start=0,b||y(_)||y(m)){if(h=A.length,d=b&&hb(b),v(b))for(l in b)~Kt.indexOf(l)&&((c=c||{})[l]=b[l]);for(o=0;o<h;o++)(u=xa(r,$t)).stagger=0,C&&(u.yoyoEase=C),c&&bt(u,c),f=A[o],u.duration=+Gt(_,_assertThisInitialized(a),o,f,A),u.delay=(+Gt(m,_assertThisInitialized(a),o,f,A)||0)-a._delay,!b&&1===h&&u.delay&&(a._delay=m=u.delay,a._start+=m,u.delay=0),s.to(f,u,d?d(o,f,A):0),s._ease=Bt.none;s.duration()?_=m=0:a.timeline=0}else if(k){ya(ta(s.vars.defaults,{ease:"none"})),s._ease=Vt(k.ease||r.ease||"none");var D,S,z,E=0;if($(k))k.forEach(function(t){return s.to(A,t,">")}),s.duration();else{for(l in u={},k)"ease"===l||"easeEach"===l||jc(l,k[l],u,k.easeEach);for(l in u)for(D=u[l].sort(function(t,e){return t.t-e.t}),o=E=0;o<D.length;o++)(z={ease:(S=D[o]).e,duration:(S.t-(o?D[o-1].t:0))/100*_})[l]=S.v,s.to(A,z,E),E+=z.duration;s.duration()<_&&s.to({},{duration:_-s.duration()})}}_||a.duration(_=s.duration())}else a.timeline=0;return!0!==x||F||(Wt=_assertThisInitialized(a),L.killTweensOf(A),Wt=0),Na(P,_assertThisInitialized(a),i),r.reversed&&a.reverse(),r.paused&&a.paused(!0),(g||!_&&!k&&a._start===la(P._time)&&w(g)&&function _hasNoPausedAncestors(t){return!t||t._ts&&_hasNoPausedAncestors(t.parent)}(_assertThisInitialized(a))&&"nested"!==P.data)&&(a._tTime=-q,a.render(Math.max(0,-m)||0)),M&&Oa(_assertThisInitialized(a),M),a}_inheritsLoose(Tween,R);var e=Tween.prototype;return e.render=function render(t,e,r){var i,n,a,s,o,u,h,l,f,d=this._time,c=this._tDur,p=this._dur,_=t<0,m=c-q<t&&!_?c:t<q?0:t;if(p){if(m!==this._tTime||!t||r||!this._initted&&this._tTime||this._startAt&&this._zTime<0!=_||this._lazy){if(i=m,l=this.timeline,this._repeat){if(s=p+this._rDelay,this._repeat<-1&&_)return this.totalTime(100*s+t,e,r);if(i=la(m%s),m===c?(a=this._repeat,i=p):(a=~~(o=la(m/s)))&&a===o?(i=p,a--):p<i&&(i=p),(u=this._yoyo&&1&a)&&(f=this._yEase,i=p-i),o=wt(this._tTime,s),i===d&&!r&&this._initted&&a===o)return this._tTime=m,this;a!==o&&(l&&this._yEase&&Tb(l,u),this.vars.repeatRefresh&&!u&&!this._lock&&i!==s&&this._initted&&(this._lock=r=1,this.render(la(s*a),!0).invalidate()._lock=0))}if(!this._initted){if(Pa(this,_?t:i,r,e,m))return this._tTime=0,this;if(!(d===this._time||r&&this.vars.repeatRefresh&&a!==o))return this;if(p!==this._dur)return this.render(t,e,r)}if(this._tTime=m,this._time=i,!this._act&&this._ts&&(this._act=1,this._lazy=0),this.ratio=h=(f||this._ease)(i/p),this._from&&(this.ratio=h=1-h),!d&&m&&!e&&!o&&(Dt(this,"onStart"),this._tTime!==m))return this;for(n=this._pt;n;)n.r(h,n.d),n=n._next;l&&l.render(t<0?t:l._dur*l._ease(i/this._dur),e,r)||this._startAt&&(this._zTime=t),this._onUpdate&&!e&&(_&&Fa(this,t,0,r),Dt(this,"onUpdate")),this._repeat&&a!==o&&this.vars.onRepeat&&!e&&this.parent&&Dt(this,"onRepeat"),m!==this._tDur&&m||this._tTime!==m||(_&&!this._onUpdate&&Fa(this,t,0,!0),!t&&p||!(m===this._tDur&&0<this._ts||!m&&this._ts<0)||Ca(this,1),e||_&&!d||!(m||d||u)||(Dt(this,m===c?"onComplete":"onReverseComplete",!0),!this._prom||m<c&&0<this.timeScale()||this._prom()))}}else!function _renderZeroDurationTween(t,e,r,i){var n,a,s,o=t.ratio,u=e<0||!e&&(!t._start&&function _parentPlayheadIsBeforeStart(t){var e=t.parent;return e&&e._ts&&e._initted&&!e._lock&&(e.rawTime()<0||_parentPlayheadIsBeforeStart(e))}(t)&&(t._initted||!xt(t))||(t._ts<0||t._dp._ts<0)&&!xt(t))?0:1,h=t._rDelay,l=0;if(h&&t._repeat&&(l=Mt(0,t._tDur,e),a=wt(l,h),t._yoyo&&1&a&&(u=1-u),a!==wt(t._tTime,h)&&(o=1-u,t.vars.repeatRefresh&&t._initted&&t.invalidate())),u!==o||I||i||t._zTime===q||!e&&t._zTime){if(!t._initted&&Pa(t,e,i,r,l))return;for(s=t._zTime,t._zTime=e||(r?q:0),r=r||e&&!s,t.ratio=u,t._from&&(u=1-u),t._time=0,t._tTime=l,n=t._pt;n;)n.r(u,n.d),n=n._next;e<0&&Fa(t,e,0,!0),t._onUpdate&&!r&&Dt(t,"onUpdate"),l&&t._repeat&&!r&&t.parent&&Dt(t,"onRepeat"),(e>=t._tDur||e<0)&&t.ratio===u&&(u&&Ca(t,1),r||I||(Dt(t,u?"onComplete":"onReverseComplete",!0),t._prom&&t._prom()))}else t._zTime||(t._zTime=e)}(this,t,e,r);return this},e.targets=function targets(){return this._targets},e.invalidate=function invalidate(t){return t&&this.vars.runBackwards||(this._startAt=0),this._pt=this._op=this._onUpdate=this._lazy=this.ratio=0,this._ptLookup=[],this.timeline&&this.timeline.invalidate(t),R.prototype.invalidate.call(this,t)},e.resetTo=function resetTo(t,e,r,i,n){c||It.wake(),this._ts||this.play();var a,s=Math.min(this._dur,(this._dp._time-this._start)*this._ts);return this._initted||Qt(this,s),a=this._ease(s/this._dur),function _updatePropTweens(t,e,r,i,n,a,s,o){var u,h,l,f,d=(t._pt&&t._ptCache||(t._ptCache={}))[e];if(!d)for(d=t._ptCache[e]=[],l=t._ptLookup,f=t._targets.length;f--;){if((u=l[f][e])&&u.d&&u.d._pt)for(u=u.d._pt;u&&u.p!==e&&u.fp!==e;)u=u._next;if(!u)return Ht=1,t.vars[e]="+=0",Qt(t,s),Ht=0,o?T(e+" not eligible for reset"):1;d.push(u)}for(f=d.length;f--;)(u=(h=d[f])._pt||h).s=!i&&0!==i||n?u.s+(i||0)+a*u.c:i,u.c=r-u.s,h.e&&(h.e=ka(r)+_a(h.e)),h.b&&(h.b=u.s+_a(h.b))}(this,t,e,r,i,a,s,n)?this.resetTo(t,e,r,i,1):(La(this,0),this.parent||Aa(this._dp,this,"_first","_last",this._dp._sort?"_start":0),this.render(0))},e.kill=function kill(t,e){if(void 0===e&&(e="all"),!(t||e&&"all"!==e))return this._lazy=this._pt=0,this.parent?wb(this):this.scrollTrigger&&this.scrollTrigger.kill(!!I),this;if(this.timeline){var i=this.timeline.totalDuration();return this.timeline.killTweensOf(t,e,Wt&&!0!==Wt.vars.overwrite)._first||wb(this),this.parent&&i!==this.timeline.totalDuration()&&Ua(this,this._dur*this.timeline._tDur/i,0,1),this}var n,a,s,o,u,h,l,f=this._targets,d=t?Pt(t):f,c=this._ptLookup,p=this._pt;if((!e||"all"===e)&&function _arraysMatch(t,e){for(var r=t.length,i=r===e.length;i&&r--&&t[r]===e[r];);return r<0}(f,d))return"all"===e&&(this._pt=0),wb(this);for(n=this._op=this._op||[],"all"!==e&&(r(e)&&(u={},ja(e,function(t){return u[t]=1}),e=u),e=function _addAliasesToVars(t,e){var r,i,n,a,s=t[0]?ha(t[0]).harness:0,o=s&&s.aliases;if(!o)return e;for(i in r=bt({},e),o)if(i in r)for(n=(a=o[i].split(",")).length;n--;)r[a[n]]=r[i];return r}(f,e)),l=f.length;l--;)if(~d.indexOf(f[l]))for(u in a=c[l],"all"===e?(n[l]=e,o=a,s={}):(s=n[l]=n[l]||{},o=e),o)(h=a&&a[u])&&("kill"in h.d&&!0!==h.d.kill(u)||Ba(this,h,"_pt"),delete a[u]),"all"!==s&&(s[u]=1);return this._initted&&!this._pt&&p&&wb(this),this},Tween.to=function to(t,e,r){return new Tween(t,e,r)},Tween.from=function from(t,e){return Ya(1,arguments)},Tween.delayedCall=function delayedCall(t,e,r,i){return new Tween(e,0,{immediateRender:!1,lazy:!1,overwrite:!1,delay:t,onComplete:e,onReverseComplete:e,onCompleteParams:r,onReverseCompleteParams:r,callbackScope:i})},Tween.fromTo=function fromTo(t,e,r){return Ya(2,arguments)},Tween.set=function set(t,e){return e.duration=0,e.repeatDelay||(e.repeat=0),new Tween(t,e)},Tween.killTweensOf=function killTweensOf(t,e,r){return L.killTweensOf(t,e,r)},Tween}(qt);ta(te.prototype,{_targets:[],_lazy:0,_startAt:0,_op:0,_onInit:0}),ja("staggerTo,staggerFrom,staggerFromTo",function(r){te[r]=function(){var t=new Zt,e=Ct.call(arguments,0);return e.splice("staggerFromTo"===r?5:4,0,0),t[r].apply(t,e)}});function rc(t,e,r){return t.setAttribute(e,r)}function zc(t,e,r,i){i.mSet(t,e,i.m.call(i.tween,r,i.mt),i)}var ee=function _setterPlain(t,e,r){return t[e]=r},re=function _setterFunc(t,e,r){return t[e](r)},ue=function _setterFuncWithParam(t,e,r,i){return t[e](i.fp,r)},le=function _getSetter(t,e){return s(t[e])?re:u(t[e])&&t.setAttribute?rc:ee},ce=function _renderPlain(t,e){return e.set(e.t,e.p,Math.round(1e6*(e.s+e.c*t))/1e6,e)},_e=function _renderBoolean(t,e){return e.set(e.t,e.p,!!(e.s+e.c*t),e)},ge=function _renderComplexString(t,e){var r=e._pt,i="";if(!t&&e.b)i=e.b;else if(1===t&&e.e)i=e.e;else{for(;r;)i=r.p+(r.m?r.m(r.s+r.c*t):Math.round(1e4*(r.s+r.c*t))/1e4)+i,r=r._next;i+=e.c}e.set(e.t,e.p,i,e)},ve=function _renderPropTweens(t,e){for(var r=e._pt;r;)r.r(t,r.d),r=r._next},ye=function _addPluginModifier(t,e,r,i){for(var n,a=this._pt;a;)n=a._next,a.p===i&&a.modifier(t,e,r),a=n},Te=function _killPropTweensOf(t){for(var e,r,i=this._pt;i;)r=i._next,i.p===t&&!i.op||i.op===t?Ba(this,i,"_pt"):i.dep||(e=1),i=r;return!e},be=function _sortPropTweensByPriority(t){for(var e,r,i,n,a=t._pt;a;){for(e=a._next,r=i;r&&r.pr>a.pr;)r=r._next;(a._prev=r?r._prev:n)?a._prev._next=a:i=a,(a._next=r)?r._prev=a:n=a,a=e}t._pt=i},we=(PropTween.prototype.modifier=function modifier(t,e,r){this.mSet=this.mSet||this.set,this.set=zc,this.m=t,this.mt=r,this.tween=e},PropTween);function PropTween(t,e,r,i,n,a,s,o,u){this.t=e,this.s=i,this.c=n,this.p=r,this.r=a||ce,this.d=s||this,this.set=o||ee,this.pr=u||0,(this._next=t)&&(t._prev=this)}ja(Tt+"parent,duration,ease,delay,overwrite,runBackwards,startAt,yoyo,immediateRender,repeat,repeatDelay,data,paused,reversed,lazy,callbackScope,stringFilter,id,yoyoEase,stagger,inherit,repeatRefresh,keyframes,autoRevert,scrollTrigger",function(t){return ct[t]=1}),ht.TweenMax=ht.TweenLite=te,ht.TimelineLite=ht.TimelineMax=Zt,L=new Zt({sortChildren:!1,defaults:j,autoRemoveChildren:!0,id:"root",smoothChildTiming:!0}),N.stringFilter=Ib;function Hc(t){return(Oe[t]||Me).map(function(t){return t()})}function Ic(){var t=Date.now(),o=[];2<t-Ce&&(Hc("matchMediaInit"),ke.forEach(function(t){var e,r,i,n,a=t.queries,s=t.conditions;for(r in a)(e=h.matchMedia(a[r]).matches)&&(i=1),e!==s[r]&&(s[r]=e,n=1);n&&(t.revert(),i&&o.push(t))}),Hc("matchMediaRevert"),o.forEach(function(e){return e.onMatch(e,function(t){return e.add(null,t)})}),Ce=t,Hc("matchMedia"))}var xe,ke=[],Oe={},Me=[],Ce=0,Pe=0,De=((xe=Context.prototype).add=function add(t,i,n){function Jw(){var t,e=l,r=a.selector;return e&&e!==a&&e.data.push(a),n&&(a.selector=fb(n)),l=a,t=i.apply(a,arguments),s(t)&&a._r.push(t),l=e,a.selector=r,a.isReverted=!1,t}s(t)&&(n=i,i=t,t=s);var a=this;return a.last=Jw,t===s?Jw(a,function(t){return a.add(null,t)}):t?a[t]=Jw:Jw},xe.ignore=function ignore(t){var e=l;l=null,t(this),l=e},xe.getTweens=function getTweens(){var e=[];return this.data.forEach(function(t){return t instanceof Context?e.push.apply(e,t.getTweens()):t instanceof te&&!(t.parent&&"nested"===t.parent.data)&&e.push(t)}),e},xe.clear=function clear(){this._r.length=this.data.length=0},xe.kill=function kill(i,t){var n=this;if(i?function(){for(var t,e=n.getTweens(),r=n.data.length;r--;)"isFlip"===(t=n.data[r]).data&&(t.revert(),t.getChildren(!0,!0,!1).forEach(function(t){return e.splice(e.indexOf(t),1)}));for(e.map(function(t){return{g:t._dur||t._delay||t._sat&&!t._sat.vars.immediateRender?t.globalTime(0):-1/0,t:t}}).sort(function(t,e){return e.g-t.g||-1/0}).forEach(function(t){return t.t.revert(i)}),r=n.data.length;r--;)(t=n.data[r])instanceof Zt?"nested"!==t.data&&(t.scrollTrigger&&t.scrollTrigger.revert(),t.kill()):t instanceof te||!t.revert||t.revert(i);n._r.forEach(function(t){return t(i,n)}),n.isReverted=!0}():this.data.forEach(function(t){return t.kill&&t.kill()}),this.clear(),t)for(var e=ke.length;e--;)ke[e].id===this.id&&ke.splice(e,1)},xe.revert=function revert(t){this.kill(t||{})},Context);function Context(t,e){this.selector=e&&fb(e),this.data=[],this._r=[],this.isReverted=!1,this.id=Pe++,t&&this.add(t)}var Se,Ee=((Se=MatchMedia.prototype).add=function add(t,e,r){v(t)||(t={matches:t});var i,n,a,s=new De(0,r||this.scope),o=s.conditions={};for(n in l&&!s.selector&&(s.selector=l.selector),this.contexts.push(s),e=s.add("onMatch",e),s.queries=t)"all"===n?a=1:(i=h.matchMedia(t[n]))&&(ke.indexOf(s)<0&&ke.push(s),(o[n]=i.matches)&&(a=1),i.addListener?i.addListener(Ic):i.addEventListener("change",Ic));return a&&e(s,function(t){return s.add(null,t)}),this},Se.revert=function revert(t){this.kill(t||{})},Se.kill=function kill(e){this.contexts.forEach(function(t){return t.kill(e,!0)})},MatchMedia);function MatchMedia(t){this.contexts=[],this.scope=t,l&&l.data.push(this)}var Re={registerPlugin:function registerPlugin(){for(var t=arguments.length,e=new Array(t),r=0;r<t;r++)e[r]=arguments[r];e.forEach(function(t){return zb(t)})},timeline:function timeline(t){return new Zt(t)},getTweensOf:function getTweensOf(t,e){return L.getTweensOf(t,e)},getProperty:function getProperty(i,t,e,n){r(i)&&(i=Pt(i)[0]);var a=ha(i||{}).get,s=e?sa:ra;return"native"===e&&(e=""),i?t?s((mt[t]&&mt[t].get||a)(i,t,e,n)):function(t,e,r){return s((mt[t]&&mt[t].get||a)(i,t,e,r))}:i},quickSetter:function quickSetter(r,e,i){if(1<(r=Pt(r)).length){var n=r.map(function(t){return Fe.quickSetter(t,e,i)}),a=n.length;return function(t){for(var e=a;e--;)n[e](t)}}r=r[0]||{};var s=mt[e],o=ha(r),u=o.harness&&(o.harness.aliases||{})[e]||e,h=s?function(t){var e=new s;d._pt=0,e.init(r,i?t+i:t,d,0,[r]),e.render(1,e),d._pt&&ve(1,d)}:o.set(r,u);return s?h:function(t){return h(r,u,i?t+i:t,o,1)}},quickTo:function quickTo(t,i,e){function by(t,e,r){return n.resetTo(i,t,e,r)}var r,n=Fe.to(t,ta(((r={})[i]="+=0.1",r.paused=!0,r.stagger=0,r),e||{}));return by.tween=n,by},isTweening:function isTweening(t){return 0<L.getTweensOf(t,!0).length},defaults:function defaults(t){return t&&t.ease&&(t.ease=Vt(t.ease,j.ease)),wa(j,t||{})},config:function config(t){return wa(N,t||{})},registerEffect:function registerEffect(t){var i=t.name,n=t.effect,e=t.plugins,a=t.defaults,r=t.extendTimeline;(e||"").split(",").forEach(function(t){return t&&!mt[t]&&!ht[t]&&T(i+" effect requires "+t+" plugin.")}),gt[i]=function(t,e,r){return n(Pt(t),ta(e||{},a),r)},r&&(Zt.prototype[i]=function(t,e,r){return this.add(gt[i](t,v(e)?e:(r=e)&&{},this),r)})},registerEase:function registerEase(t,e){Bt[t]=Vt(e)},parseEase:function parseEase(t,e){return arguments.length?Vt(t,e):Bt},getById:function getById(t){return L.getById(t)},exportRoot:function exportRoot(t,e){void 0===t&&(t={});var r,i,n=new Zt(t);for(n.smoothChildTiming=w(t.smoothChildTiming),L.remove(n),n._dp=0,n._time=n._tTime=L._time,r=L._first;r;)i=r._next,!e&&!r._dur&&r instanceof te&&r.vars.onComplete===r._targets[0]||Na(n,r,r._start-r._delay),r=i;return Na(L,n,0),n},context:function context(t,e){return t?new De(t,e):l},matchMedia:function matchMedia(t){return new Ee(t)},matchMediaRefresh:function matchMediaRefresh(){return ke.forEach(function(t){var e,r,i=t.conditions;for(r in i)i[r]&&(i[r]=!1,e=1);e&&t.revert()})||Ic()},addEventListener:function addEventListener(t,e){var r=Oe[t]||(Oe[t]=[]);~r.indexOf(e)||r.push(e)},removeEventListener:function removeEventListener(t,e){var r=Oe[t],i=r&&r.indexOf(e);0<=i&&r.splice(i,1)},utils:{wrap:function wrap(e,t,r){var i=t-e;return $(e)?ob(e,wrap(0,e.length),t):Za(r,function(t){return(i+(t-e)%i)%i+e})},wrapYoyo:function wrapYoyo(e,t,r){var i=t-e,n=2*i;return $(e)?ob(e,wrapYoyo(0,e.length-1),t):Za(r,function(t){return e+(i<(t=(n+(t-e)%n)%n||0)?n-t:t)})},distribute:hb,random:kb,snap:jb,normalize:function normalize(t,e,r){return At(t,e,0,1,r)},getUnit:_a,clamp:function clamp(e,r,t){return Za(t,function(t){return Mt(e,r,t)})},splitColor:Db,toArray:Pt,selector:fb,mapRange:At,pipe:function pipe(){for(var t=arguments.length,e=new Array(t),r=0;r<t;r++)e[r]=arguments[r];return function(t){return e.reduce(function(t,e){return e(t)},t)}},unitize:function unitize(e,r){return function(t){return e(parseFloat(t))+(r||_a(t))}},interpolate:function interpolate(e,i,t,n){var a=isNaN(e+i)?0:function(t){return(1-t)*e+t*i};if(!a){var s,o,u,h,l,f=r(e),d={};if(!0===t&&(n=1)&&(t=null),f)e={p:e},i={p:i};else if($(e)&&!$(i)){for(u=[],h=e.length,l=h-2,o=1;o<h;o++)u.push(interpolate(e[o-1],e[o]));h--,a=function func(t){t*=h;var e=Math.min(l,~~t);return u[e](t-e)},t=i}else n||(e=bt($(e)?[]:{},e));if(!u){for(s in i)Jt.call(d,e,s,"get",i[s]);a=function func(t){return ve(t,d)||(f?e.p:e)}}}return Za(t,a)},shuffle:gb},install:R,effects:gt,ticker:It,updateRoot:Zt.updateRoot,plugins:mt,globalTimeline:L,core:{PropTween:we,globals:U,Tween:te,Timeline:Zt,Animation:qt,getCache:ha,_removeLinkedListItem:Ba,reverting:function reverting(){return I},context:function context(t){return t&&l&&(l.data.push(t),t._ctx=l),l},suppressOverwrites:function suppressOverwrites(t){return F=t}}};ja("to,from,fromTo,delayedCall,set,killTweensOf",function(t){return Re[t]=te[t]}),It.add(Zt.updateRoot),d=Re.to({},{duration:0});function Mc(t,e){for(var r=t._pt;r&&r.p!==e&&r.op!==e&&r.fp!==e;)r=r._next;return r}function Oc(t,a){return{name:t,headless:1,rawVars:1,init:function init(t,n,e){e._onInit=function(t){var e,i;if(r(n)&&(e={},ja(n,function(t){return e[t]=1}),n=e),a){for(i in e={},n)e[i]=a(n[i]);n=e}!function _addModifiers(t,e){var r,i,n,a=t._targets;for(r in e)for(i=a.length;i--;)(n=(n=t._ptLookup[i][r])&&n.d)&&(n._pt&&(n=Mc(n,r)),n&&n.modifier&&n.modifier(e[r],t,a[i],r))}(t,n)}}}}var Fe=Re.registerPlugin({name:"attr",init:function init(t,e,r,i,n){var a,s,o;for(a in this.tween=r,e)o=t.getAttribute(a)||"",(s=this.add(t,"setAttribute",(o||0)+"",e[a],i,n,0,0,a)).op=a,s.b=o,this._props.push(a)},render:function render(t,e){for(var r=e._pt;r;)I?r.set(r.t,r.p,r.b,r):r.r(t,r.d),r=r._next}},{name:"endArray",headless:1,init:function init(t,e){for(var r=e.length;r--;)this.add(t,r,t[r]||0,e[r],0,0,0,0,0,1)}},Oc("roundProps",ib),Oc("modifiers"),Oc("snap",jb))||Re;te.version=Zt.version=Fe.version="3.14.2",o=1,x()&&Lt();function yd(t,e){return e.set(e.t,e.p,Math.round(1e4*(e.s+e.c*t))/1e4+e.u,e)}function zd(t,e){return e.set(e.t,e.p,1===t?e.e:Math.round(1e4*(e.s+e.c*t))/1e4+e.u,e)}function Ad(t,e){return e.set(e.t,e.p,t?Math.round(1e4*(e.s+e.c*t))/1e4+e.u:e.b,e)}function Bd(t,e){return e.set(e.t,e.p,1===t?e.e:t?Math.round(1e4*(e.s+e.c*t))/1e4+e.u:e.b,e)}function Cd(t,e){var r=e.s+e.c*t;e.set(e.t,e.p,~~(r+(r<0?-.5:.5))+e.u,e)}function Dd(t,e){return e.set(e.t,e.p,t?e.e:e.b,e)}function Ed(t,e){return e.set(e.t,e.p,1!==t?e.b:e.e,e)}function Fd(t,e,r){return t.style[e]=r}function Gd(t,e,r){return t.style.setProperty(e,r)}function Hd(t,e,r){return t._gsap[e]=r}function Id(t,e,r){return t._gsap.scaleX=t._gsap.scaleY=r}function Jd(t,e,r,i,n){var a=t._gsap;a.scaleX=a.scaleY=r,a.renderTransform(n,a)}function Kd(t,e,r,i,n){var a=t._gsap;a[e]=r,a.renderTransform(n,a)}function Nd(t,e){var r=this,i=this.target,n=i.style,a=i._gsap;if(t in hr&&n){if(this.tfm=this.tfm||{},"transform"===t)return mr.transform.split(",").forEach(function(t){return Nd.call(r,t,e)});if(~(t=mr[t]||t).indexOf(",")?t.split(",").forEach(function(t){return r.tfm[t]=xr(i,t)}):this.tfm[t]=a.x?a[t]:xr(i,t),t===vr&&(this.tfm.zOrigin=a.zOrigin),0<=this.props.indexOf(gr))return;a.svg&&(this.svgo=i.getAttribute("data-svg-origin"),this.props.push(vr,e,"")),t=gr}(n||e)&&this.props.push(t,e,n[t])}function Od(t){t.translate&&(t.removeProperty("translate"),t.removeProperty("scale"),t.removeProperty("rotate"))}function Pd(){var t,e,r=this.props,i=this.target,n=i.style,a=i._gsap;for(t=0;t<r.length;t+=3)r[t+1]?2===r[t+1]?i[r[t]](r[t+2]):i[r[t]]=r[t+2]:r[t+2]?n[r[t]]=r[t+2]:n.removeProperty("--"===r[t].substr(0,2)?r[t]:r[t].replace(cr,"-$1").toLowerCase());if(this.tfm){for(e in this.tfm)a[e]=this.tfm[e];a.svg&&(a.renderTransform(),i.setAttribute("data-svg-origin",this.svgo||"")),(t=Ue())&&t.isStart||n[gr]||(Od(n),a.zOrigin&&n[vr]&&(n[vr]+=" "+a.zOrigin+"px",a.zOrigin=0,a.renderTransform()),a.uncache=1)}}function Qd(t,e){var r={target:t,props:[],revert:Pd,save:Nd};return t._gsap||Fe.core.getCache(t),e&&t.style&&t.nodeType&&e.split(",").forEach(function(t){return r.save(t)}),r}function Sd(t,e){var r=Le.createElementNS?Le.createElementNS((e||"http://www.w3.org/1999/xhtml").replace(/^https/,"http"),t):Le.createElement(t);return r&&r.style?r:Le.createElement(t)}function Td(t,e,r){var i=getComputedStyle(t);return i[e]||i.getPropertyValue(e.replace(cr,"-$1").toLowerCase())||i.getPropertyValue(e)||!r&&Td(t,Tr(e)||e,1)||""}function Wd(){(function _windowExists(){return"undefined"!=typeof window})()&&window.document&&(Ie=window,Le=Ie.document,Ye=Le.documentElement,je=Sd("div")||{style:{}},Sd("div"),gr=Tr(gr),vr=gr+"Origin",je.style.cssText="border-width:0;line-height:0;position:absolute;padding:0",Xe=!!Tr("perspective"),Ue=Fe.core.reverting,Ne=1)}function Xd(t){var e,r=t.ownerSVGElement,i=Sd("svg",r&&r.getAttribute("xmlns")||"http://www.w3.org/2000/svg"),n=t.cloneNode(!0);n.style.display="block",i.appendChild(n),Ye.appendChild(i);try{e=n.getBBox()}catch(t){}return i.removeChild(n),Ye.removeChild(i),e}function Yd(t,e){for(var r=e.length;r--;)if(t.hasAttribute(e[r]))return t.getAttribute(e[r])}function Zd(e){var r,i;try{r=e.getBBox()}catch(t){r=Xd(e),i=1}return r&&(r.width||r.height)||i||(r=Xd(e)),!r||r.width||r.x||r.y?r:{x:+Yd(e,["x","cx","x1"])||0,y:+Yd(e,["y","cy","y1"])||0,width:0,height:0}}function $d(t){return!(!t.getCTM||t.parentNode&&!t.ownerSVGElement||!Zd(t))}function _d(t,e){if(e){var r,i=t.style;e in hr&&e!==vr&&(e=gr),i.removeProperty?("ms"!==(r=e.substr(0,2))&&"webkit"!==e.substr(0,6)||(e="-"+e),i.removeProperty("--"===r?e:e.replace(cr,"-$1").toLowerCase())):i.removeAttribute(e)}}function ae(t,e,r,i,n,a){var s=new we(t._pt,e,r,0,1,a?Ed:Dd);return(t._pt=s).b=i,s.e=n,t._props.push(r),s}function de(t,e,r,i){var n,a,s,o,u=parseFloat(r)||0,h=(r+"").trim().substr((u+"").length)||"px",l=je.style,f=pr.test(e),d="svg"===t.tagName.toLowerCase(),c=(d?"client":"offset")+(f?"Width":"Height"),p="px"===i,_="%"===i;if(i===h||!u||br[i]||br[h])return u;if("px"===h||p||(u=de(t,e,r,"px")),o=t.getCTM&&$d(t),(_||"%"===h)&&(hr[e]||~e.indexOf("adius")))return n=o?t.getBBox()[f?"width":"height"]:t[c],ka(_?u/n*100:u/100*n);if(l[f?"width":"height"]=100+(p?h:i),a="rem"!==i&&~e.indexOf("adius")||"em"===i&&t.appendChild&&!d?t:t.parentNode,o&&(a=(t.ownerSVGElement||{}).parentNode),a&&a!==Le&&a.appendChild||(a=Le.body),(s=a._gsap)&&_&&s.width&&f&&s.time===It.time&&!s.uncache)return ka(u/s.width*100);if(!_||"height"!==e&&"width"!==e)!_&&"%"!==h||wr[Td(a,"display")]||(l.position=Td(t,"position")),a===t&&(l.position="static"),a.appendChild(je),n=je[c],a.removeChild(je),l.position="absolute";else{var m=t.style[e];t.style[e]=100+i,n=t[c],m?t.style[e]=m:_d(t,e)}return f&&_&&((s=ha(a)).time=It.time,s.width=a[c]),ka(p?n*u/100:n&&u?100/n*u:0)}function fe(t,e,r,i){if(!r||"none"===r){var n=Tr(e,t,1),a=n&&Td(t,n,1);a&&a!==r?(e=n,r=a):"borderColor"===e&&(r=Td(t,"borderTopColor"))}var s,o,u,h,l,f,d,c,p,_,m,g=new we(this._pt,t.style,e,0,1,ge),v=0,y=0;if(g.b=r,g.e=i,r+="","var(--"===(i+="").substring(0,6)&&(i=Td(t,i.substring(4,i.indexOf(")")))),"auto"===i&&(f=t.style[e],t.style[e]=i,i=Td(t,e)||i,f?t.style[e]=f:_d(t,e)),Ib(s=[r,i]),i=s[1],u=(r=s[0]).match(nt)||[],(i.match(nt)||[]).length){for(;o=nt.exec(i);)d=o[0],p=i.substring(v,o.index),l?l=(l+1)%5:"rgba("!==p.substr(-5)&&"hsla("!==p.substr(-5)||(l=1),d!==(f=u[y++]||"")&&(h=parseFloat(f)||0,m=f.substr((h+"").length),"="===d.charAt(1)&&(d=ma(h,d)+m),c=parseFloat(d),_=d.substr((c+"").length),v=nt.lastIndex-_.length,_||(_=_||N.units[e]||m,v===i.length&&(i+=_,g.e+=_)),m!==_&&(h=de(t,e,f,_)||0),g._pt={_next:g._pt,p:p||1===y?p:",",s:h,c:c-h,m:l&&l<4||"zIndex"===e?Math.round:0});g.c=v<i.length?i.substring(v,i.length):""}else g.r="display"===e&&"none"===i?Ed:Dd;return st.test(i)&&(g.e=0),this._pt=g}function he(t){var e=t.split(" "),r=e[0],i=e[1]||"50%";return"top"!==r&&"bottom"!==r&&"left"!==i&&"right"!==i||(t=r,r=i,i=t),e[0]=kr[r]||r,e[1]=kr[i]||i,e.join(" ")}function ie(t,e){if(e.tween&&e.tween._time===e.tween._dur){var r,i,n,a=e.t,s=a.style,o=e.u,u=a._gsap;if("all"===o||!0===o)s.cssText="",i=1;else for(n=(o=o.split(",")).length;-1<--n;)r=o[n],hr[r]&&(i=1,r="transformOrigin"===r?vr:gr),_d(a,r);i&&(_d(a,gr),u&&(u.svg&&a.removeAttribute("transform"),s.scale=s.rotate=s.translate="none",Pr(a,1),u.uncache=1,Od(s)))}}function me(t){return"matrix(1, 0, 0, 1, 0, 0)"===t||"none"===t||!t}function ne(t){var e=Td(t,gr);return me(e)?Mr:e.substr(7).match(it).map(ka)}function oe(t,e){var r,i,n,a,s=t._gsap||ha(t),o=t.style,u=ne(t);return s.svg&&t.getAttribute("transform")?"1,0,0,1,0,0"===(u=[(n=t.transform.baseVal.consolidate().matrix).a,n.b,n.c,n.d,n.e,n.f]).join(",")?Mr:u:(u!==Mr||t.offsetParent||t===Ye||s.svg||(n=o.display,o.display="block",(r=t.parentNode)&&(t.offsetParent||t.getBoundingClientRect().width)||(a=1,i=t.nextElementSibling,Ye.appendChild(t)),u=ne(t),n?o.display=n:_d(t,"display"),a&&(i?r.insertBefore(t,i):r?r.appendChild(t):Ye.removeChild(t))),e&&6<u.length?[u[0],u[1],u[4],u[5],u[12],u[13]]:u)}function pe(t,e,r,i,n,a){var s,o,u,h=t._gsap,l=n||oe(t,!0),f=h.xOrigin||0,d=h.yOrigin||0,c=h.xOffset||0,p=h.yOffset||0,_=l[0],m=l[1],g=l[2],v=l[3],y=l[4],T=l[5],b=e.split(" "),w=parseFloat(b[0])||0,x=parseFloat(b[1])||0;r?l!==Mr&&(o=_*v-m*g)&&(u=w*(-m/o)+x*(_/o)-(_*T-m*y)/o,w=w*(v/o)+x*(-g/o)+(g*T-v*y)/o,x=u):(w=(s=Zd(t)).x+(~b[0].indexOf("%")?w/100*s.width:w),x=s.y+(~(b[1]||b[0]).indexOf("%")?x/100*s.height:x)),i||!1!==i&&h.smooth?(y=w-f,T=x-d,h.xOffset=c+(y*_+T*g)-y,h.yOffset=p+(y*m+T*v)-T):h.xOffset=h.yOffset=0,h.xOrigin=w,h.yOrigin=x,h.smooth=!!i,h.origin=e,h.originIsAbsolute=!!r,t.style[vr]="0px 0px",a&&(ae(a,h,"xOrigin",f,w),ae(a,h,"yOrigin",d,x),ae(a,h,"xOffset",c,h.xOffset),ae(a,h,"yOffset",p,h.yOffset)),t.setAttribute("data-svg-origin",w+" "+x)}function se(t,e,r){var i=_a(e);return ka(parseFloat(e)+parseFloat(de(t,"x",r+"px",i)))+i}function ze(t,e,i,n,a){var s,o,u=360,h=r(a),l=parseFloat(a)*(h&&~a.indexOf("rad")?lr:1)-n,f=n+l+"deg";return h&&("short"===(s=a.split("_")[1])&&(l%=u)!==l%180&&(l+=l<0?u:-u),"cw"===s&&l<0?l=(l+36e9)%u-~~(l/u)*u:"ccw"===s&&0<l&&(l=(l-36e9)%u-~~(l/u)*u)),t._pt=o=new we(t._pt,e,i,n,l,zd),o.e=f,o.u="deg",t._props.push(i),o}function Ae(t,e){for(var r in e)t[r]=e[r];return t}function Be(t,e,r){var i,n,a,s,o,u,h,l=Ae({},r._gsap),f=r.style;for(n in l.svg?(a=r.getAttribute("transform"),r.setAttribute("transform",""),f[gr]=e,i=Pr(r,1),_d(r,gr),r.setAttribute("transform",a)):(a=getComputedStyle(r)[gr],f[gr]=e,i=Pr(r,1),f[gr]=a),hr)(a=l[n])!==(s=i[n])&&"perspective,force3D,transformOrigin,svgOrigin".indexOf(n)<0&&(o=_a(a)!==(h=_a(s))?de(r,n,a,h):parseFloat(a),u=parseFloat(s),t._pt=new we(t._pt,i,n,o,u-o,yd),t._pt.u=h||0,t._props.push(n));Ae(i,l)}var Ie,Le,Ye,Ne,je,Ve,Ue,Xe,qe=Bt.Power0,Ze=Bt.Power1,We=Bt.Power2,He=Bt.Power3,Je=Bt.Power4,Qe=Bt.Linear,Ge=Bt.Quad,Ke=Bt.Cubic,$e=Bt.Quart,tr=Bt.Quint,er=Bt.Strong,rr=Bt.Elastic,ir=Bt.Back,nr=Bt.SteppedEase,ar=Bt.Bounce,sr=Bt.Sine,or=Bt.Expo,ur=Bt.Circ,hr={},lr=180/Math.PI,fr=Math.PI/180,dr=Math.atan2,cr=/([A-Z])/g,pr=/(left|right|width|margin|padding|x)/i,_r=/[\s,\(]\S/,mr={autoAlpha:"opacity,visibility",scale:"scaleX,scaleY",alpha:"opacity"},gr="transform",vr=gr+"Origin",yr="O,Moz,ms,Ms,Webkit".split(","),Tr=function _checkPropPrefix(t,e,r){var i=(e||je).style,n=5;if(t in i&&!r)return t;for(t=t.charAt(0).toUpperCase()+t.substr(1);n--&&!(yr[n]+t in i););return n<0?null:(3===n?"ms":0<=n?yr[n]:"")+t},br={deg:1,rad:1,turn:1},wr={grid:1,flex:1},xr=function _get(t,e,r,i){var n;return Ne||Wd(),e in mr&&"transform"!==e&&~(e=mr[e]).indexOf(",")&&(e=e.split(",")[0]),hr[e]&&"transform"!==e?(n=Pr(t,i),n="transformOrigin"!==e?n[e]:n.svg?n.origin:Ar(Td(t,vr))+" "+n.zOrigin+"px"):(n=t.style[e])&&"auto"!==n&&!i&&!~(n+"").indexOf("calc(")||(n=Or[e]&&Or[e](t,e,r)||Td(t,e)||ia(t,e)||("opacity"===e?1:0)),r&&!~(n+"").trim().indexOf(" ")?de(t,e,n,r)+r:n},kr={top:"0%",bottom:"100%",left:"0%",right:"100%",center:"50%"},Or={clearProps:function clearProps(t,e,r,i,n){if("isFromStart"!==n.data){var a=t._pt=new we(t._pt,e,r,0,0,ie);return a.u=i,a.pr=-10,a.tween=n,t._props.push(r),1}}},Mr=[1,0,0,1,0,0],Cr={},Pr=function _parseTransform(t,e){var r=t._gsap||new Xt(t);if("x"in r&&!e&&!r.uncache)return r;var i,n,a,s,o,u,h,l,f,d,c,p,_,m,g,v,y,T,b,w,x,k,O,M,C,P,A,D,S,z,E,R,F=t.style,I=r.scaleX<0,L="deg",B=getComputedStyle(t),Y=Td(t,vr)||"0";return i=n=a=u=h=l=f=d=c=0,s=o=1,r.svg=!(!t.getCTM||!$d(t)),B.translate&&("none"===B.translate&&"none"===B.scale&&"none"===B.rotate||(F[gr]=("none"!==B.translate?"translate3d("+(B.translate+" 0 0").split(" ").slice(0,3).join(", ")+") ":"")+("none"!==B.rotate?"rotate("+B.rotate+") ":"")+("none"!==B.scale?"scale("+B.scale.split(" ").join(",")+") ":"")+("none"!==B[gr]?B[gr]:"")),F.scale=F.rotate=F.translate="none"),m=oe(t,r.svg),r.svg&&(M=r.uncache?(C=t.getBBox(),Y=r.xOrigin-C.x+"px "+(r.yOrigin-C.y)+"px",""):!e&&t.getAttribute("data-svg-origin"),pe(t,M||Y,!!M||r.originIsAbsolute,!1!==r.smooth,m)),p=r.xOrigin||0,_=r.yOrigin||0,m!==Mr&&(T=m[0],b=m[1],w=m[2],x=m[3],i=k=m[4],n=O=m[5],6===m.length?(s=Math.sqrt(T*T+b*b),o=Math.sqrt(x*x+w*w),u=T||b?dr(b,T)*lr:0,(f=w||x?dr(w,x)*lr+u:0)&&(o*=Math.abs(Math.cos(f*fr))),r.svg&&(i-=p-(p*T+_*w),n-=_-(p*b+_*x))):(R=m[6],z=m[7],A=m[8],D=m[9],S=m[10],E=m[11],i=m[12],n=m[13],a=m[14],h=(g=dr(R,S))*lr,g&&(M=k*(v=Math.cos(-g))+A*(y=Math.sin(-g)),C=O*v+D*y,P=R*v+S*y,A=k*-y+A*v,D=O*-y+D*v,S=R*-y+S*v,E=z*-y+E*v,k=M,O=C,R=P),l=(g=dr(-w,S))*lr,g&&(v=Math.cos(-g),E=x*(y=Math.sin(-g))+E*v,T=M=T*v-A*y,b=C=b*v-D*y,w=P=w*v-S*y),u=(g=dr(b,T))*lr,g&&(M=T*(v=Math.cos(g))+b*(y=Math.sin(g)),C=k*v+O*y,b=b*v-T*y,O=O*v-k*y,T=M,k=C),h&&359.9<Math.abs(h)+Math.abs(u)&&(h=u=0,l=180-l),s=ka(Math.sqrt(T*T+b*b+w*w)),o=ka(Math.sqrt(O*O+R*R)),g=dr(k,O),f=2e-4<Math.abs(g)?g*lr:0,c=E?1/(E<0?-E:E):0),r.svg&&(M=t.getAttribute("transform"),r.forceCSS=t.setAttribute("transform","")||!me(Td(t,gr)),M&&t.setAttribute("transform",M))),90<Math.abs(f)&&Math.abs(f)<270&&(I?(s*=-1,f+=u<=0?180:-180,u+=u<=0?180:-180):(o*=-1,f+=f<=0?180:-180)),e=e||r.uncache,r.x=i-((r.xPercent=i&&(!e&&r.xPercent||(Math.round(t.offsetWidth/2)===Math.round(-i)?-50:0)))?t.offsetWidth*r.xPercent/100:0)+"px",r.y=n-((r.yPercent=n&&(!e&&r.yPercent||(Math.round(t.offsetHeight/2)===Math.round(-n)?-50:0)))?t.offsetHeight*r.yPercent/100:0)+"px",r.z=a+"px",r.scaleX=ka(s),r.scaleY=ka(o),r.rotation=ka(u)+L,r.rotationX=ka(h)+L,r.rotationY=ka(l)+L,r.skewX=f+L,r.skewY=d+L,r.transformPerspective=c+"px",(r.zOrigin=parseFloat(Y.split(" ")[2])||!e&&r.zOrigin||0)&&(F[vr]=Ar(Y)),r.xOffset=r.yOffset=0,r.force3D=N.force3D,r.renderTransform=r.svg?Fr:Xe?Rr:Dr,r.uncache=0,r},Ar=function _firstTwoOnly(t){return(t=t.split(" "))[0]+" "+t[1]},Dr=function _renderNon3DTransforms(t,e){e.z="0px",e.rotationY=e.rotationX="0deg",e.force3D=0,Rr(t,e)},Sr="0deg",zr="0px",Er=") ",Rr=function _renderCSSTransforms(t,e){var r=e||this,i=r.xPercent,n=r.yPercent,a=r.x,s=r.y,o=r.z,u=r.rotation,h=r.rotationY,l=r.rotationX,f=r.skewX,d=r.skewY,c=r.scaleX,p=r.scaleY,_=r.transformPerspective,m=r.force3D,g=r.target,v=r.zOrigin,y="",T="auto"===m&&t&&1!==t||!0===m;if(v&&(l!==Sr||h!==Sr)){var b,w=parseFloat(h)*fr,x=Math.sin(w),k=Math.cos(w);w=parseFloat(l)*fr,b=Math.cos(w),a=se(g,a,x*b*-v),s=se(g,s,-Math.sin(w)*-v),o=se(g,o,k*b*-v+v)}_!==zr&&(y+="perspective("+_+Er),(i||n)&&(y+="translate("+i+"%, "+n+"%) "),!T&&a===zr&&s===zr&&o===zr||(y+=o!==zr||T?"translate3d("+a+", "+s+", "+o+") ":"translate("+a+", "+s+Er),u!==Sr&&(y+="rotate("+u+Er),h!==Sr&&(y+="rotateY("+h+Er),l!==Sr&&(y+="rotateX("+l+Er),f===Sr&&d===Sr||(y+="skew("+f+", "+d+Er),1===c&&1===p||(y+="scale("+c+", "+p+Er),g.style[gr]=y||"translate(0, 0)"},Fr=function _renderSVGTransforms(t,e){var r,i,n,a,s,o=e||this,u=o.xPercent,h=o.yPercent,l=o.x,f=o.y,d=o.rotation,c=o.skewX,p=o.skewY,_=o.scaleX,m=o.scaleY,g=o.target,v=o.xOrigin,y=o.yOrigin,T=o.xOffset,b=o.yOffset,w=o.forceCSS,x=parseFloat(l),k=parseFloat(f);d=parseFloat(d),c=parseFloat(c),(p=parseFloat(p))&&(c+=p=parseFloat(p),d+=p),d||c?(d*=fr,c*=fr,r=Math.cos(d)*_,i=Math.sin(d)*_,n=Math.sin(d-c)*-m,a=Math.cos(d-c)*m,c&&(p*=fr,s=Math.tan(c-p),n*=s=Math.sqrt(1+s*s),a*=s,p&&(s=Math.tan(p),r*=s=Math.sqrt(1+s*s),i*=s)),r=ka(r),i=ka(i),n=ka(n),a=ka(a)):(r=_,a=m,i=n=0),(x&&!~(l+"").indexOf("px")||k&&!~(f+"").indexOf("px"))&&(x=de(g,"x",l,"px"),k=de(g,"y",f,"px")),(v||y||T||b)&&(x=ka(x+v-(v*r+y*n)+T),k=ka(k+y-(v*i+y*a)+b)),(u||h)&&(s=g.getBBox(),x=ka(x+u/100*s.width),k=ka(k+h/100*s.height)),s="matrix("+r+","+i+","+n+","+a+","+x+","+k+")",g.setAttribute("transform",s),w&&(g.style[gr]=s)};ja("padding,margin,Width,Radius",function(e,r){var t="Right",i="Bottom",n="Left",o=(r<3?["Top",t,i,n]:["Top"+n,"Top"+t,i+t,i+n]).map(function(t){return r<2?e+t:"border"+t+e});Or[1<r?"border"+e:e]=function(e,t,r,i,n){var a,s;if(arguments.length<4)return a=o.map(function(t){return xr(e,t,r)}),5===(s=a.join(" ")).split(a[0]).length?a[0]:s;a=(i+"").split(" "),s={},o.forEach(function(t,e){return s[t]=a[e]=a[e]||a[(e-1)/2|0]}),e.init(t,s,n)}});var Ir,Lr,Br,Yr={name:"css",register:Wd,targetTest:function targetTest(t){return t.style&&t.nodeType},init:function init(t,e,i,n,a){var s,o,u,h,l,f,d,c,p,_,m,g,v,y,T,b,w,x=this._props,k=t.style,O=i.vars.startAt;for(d in Ne||Wd(),this.styles=this.styles||Qd(t),b=this.styles.props,this.tween=i,e)if("autoRound"!==d&&(o=e[d],!mt[d]||!dc(d,e,i,n,t,a)))if(l=typeof o,f=Or[d],"function"===l&&(l=typeof(o=o.call(i,n,t,a))),"string"===l&&~o.indexOf("random(")&&(o=rb(o)),f)f(this,t,d,o,i)&&(T=1);else if("--"===d.substr(0,2))s=(getComputedStyle(t).getPropertyValue(d)+"").trim(),o+="",Rt.lastIndex=0,Rt.test(s)||(c=_a(s),(p=_a(o))?c!==p&&(s=de(t,d,s,p)+p):c&&(o+=c)),this.add(k,"setProperty",s,o,n,a,0,0,d),x.push(d),b.push(d,0,k[d]);else if("undefined"!==l){if(O&&d in O?(s="function"==typeof O[d]?O[d].call(i,n,t,a):O[d],r(s)&&~s.indexOf("random(")&&(s=rb(s)),_a(s+"")||"auto"===s||(s+=N.units[d]||_a(xr(t,d))||""),"="===(s+"").charAt(1)&&(s=xr(t,d))):s=xr(t,d),h=parseFloat(s),(_="string"===l&&"="===o.charAt(1)&&o.substr(0,2))&&(o=o.substr(2)),u=parseFloat(o),d in mr&&("autoAlpha"===d&&(1===h&&"hidden"===xr(t,"visibility")&&u&&(h=0),b.push("visibility",0,k.visibility),ae(this,k,"visibility",h?"inherit":"hidden",u?"inherit":"hidden",!u)),"scale"!==d&&"transform"!==d&&~(d=mr[d]).indexOf(",")&&(d=d.split(",")[0])),m=d in hr){if(this.styles.save(d),w=o,"string"===l&&"var(--"===o.substring(0,6)){if("calc("===(o=Td(t,o.substring(4,o.indexOf(")")))).substring(0,5)){var M=t.style.perspective;t.style.perspective=o,o=Td(t,"perspective"),M?t.style.perspective=M:_d(t,"perspective")}u=parseFloat(o)}if(g||((v=t._gsap).renderTransform&&!e.parseTransform||Pr(t,e.parseTransform),y=!1!==e.smoothOrigin&&v.smooth,(g=this._pt=new we(this._pt,k,gr,0,1,v.renderTransform,v,0,-1)).dep=1),"scale"===d)this._pt=new we(this._pt,v,"scaleY",v.scaleY,(_?ma(v.scaleY,_+u):u)-v.scaleY||0,yd),this._pt.u=0,x.push("scaleY",d),d+="X";else{if("transformOrigin"===d){b.push(vr,0,k[vr]),o=he(o),v.svg?pe(t,o,0,y,0,this):((p=parseFloat(o.split(" ")[2])||0)!==v.zOrigin&&ae(this,v,"zOrigin",v.zOrigin,p),ae(this,k,d,Ar(s),Ar(o)));continue}if("svgOrigin"===d){pe(t,o,1,y,0,this);continue}if(d in Cr){ze(this,v,d,h,_?ma(h,_+o):o);continue}if("smoothOrigin"===d){ae(this,v,"smooth",v.smooth,o);continue}if("force3D"===d){v[d]=o;continue}if("transform"===d){Be(this,o,t);continue}}}else d in k||(d=Tr(d)||d);if(m||(u||0===u)&&(h||0===h)&&!_r.test(o)&&d in k)u=u||0,(c=(s+"").substr((h+"").length))!==(p=_a(o)||(d in N.units?N.units[d]:c))&&(h=de(t,d,s,p)),this._pt=new we(this._pt,m?v:k,d,h,(_?ma(h,_+u):u)-h,m||"px"!==p&&"zIndex"!==d||!1===e.autoRound?yd:Cd),this._pt.u=p||0,m&&w!==o?(this._pt.b=s,this._pt.e=w,this._pt.r=Bd):c!==p&&"%"!==p&&(this._pt.b=s,this._pt.r=Ad);else if(d in k)fe.call(this,t,d,s,_?_+o:o);else if(d in t)this.add(t,d,s||t[d],_?_+o:o,n,a);else if("parseTransform"!==d){S(d,o);continue}m||(d in k?b.push(d,0,k[d]):"function"==typeof t[d]?b.push(d,2,t[d]()):b.push(d,1,s||t[d])),x.push(d)}T&&be(this)},render:function render(t,e){if(e.tween._time||!Ue())for(var r=e._pt;r;)r.r(t,r.d),r=r._next;else e.styles.revert()},get:xr,aliases:mr,getSetter:function getSetter(t,e,r){var i=mr[e];return i&&i.indexOf(",")<0&&(e=i),e in hr&&e!==vr&&(t._gsap.x||xr(t,"x"))?r&&Ve===r?"scale"===e?Id:Hd:(Ve=r||{})&&("scale"===e?Jd:Kd):t.style&&!u(t.style[e])?Fd:~e.indexOf("-")?Gd:le(t,e)},core:{_removeProperty:_d,_getMatrix:oe}};Fe.utils.checkPrefix=Tr,Fe.core.getStyleSaver=Qd,Br=ja((Ir="x,y,z,scale,scaleX,scaleY,xPercent,yPercent")+","+(Lr="rotation,rotationX,rotationY,skewX,skewY")+",transform,transformOrigin,svgOrigin,force3D,smoothOrigin,transformPerspective",function(t){hr[t]=1}),ja(Lr,function(t){N.units[t]="deg",Cr[t]=1}),mr[Br[13]]=Ir+","+Lr,ja("0:translateX,1:translateY,2:translateZ,8:rotate,8:rotationZ,8:rotateZ,9:rotateX,10:rotateY",function(t){var e=t.split(":");mr[e[1]]=Br[e[0]]}),ja("x,y,z,top,right,bottom,left,width,height,fontSize,padding,margin,perspective",function(t){N.units[t]="px"}),Fe.registerPlugin(Yr);var Nr=Fe.registerPlugin(Yr)||Fe,jr=Nr.core.Tween;e.Back=ir,e.Bounce=ar,e.CSSPlugin=Yr,e.Circ=ur,e.Cubic=Ke,e.Elastic=rr,e.Expo=or,e.Linear=Qe,e.Power0=qe,e.Power1=Ze,e.Power2=We,e.Power3=He,e.Power4=Je,e.Quad=Ge,e.Quart=$e,e.Quint=tr,e.Sine=sr,e.SteppedEase=nr,e.Strong=er,e.TimelineLite=Zt,e.TimelineMax=Zt,e.TweenLite=te,e.TweenMax=jr,e.default=Nr,e.gsap=Nr;if (typeof(window)==="undefined"||window!==e){Object.defineProperty(e,"__esModule",{value:!0})} else {delete e.default}});
+
diff --git a/umtool/report-to-video/chrome-deck.mjs b/umtool/report-to-video/chrome-deck.mjs
@@ -0,0 +1,574 @@
+// The bottom deck's composition: one HyperFrames page for a whole report cut.
+//
+// PURE, like deck.mjs: a schedule and a render block in, an HTML string out.
+// compose-chrome.mjs copies the assets in beside it, writes it and renders it;
+// nothing here touches a file, so a test can read the page it would draw.
+//
+// ---------------------------------------------------------------------------
+// Why the timeline is a list of cues computed here, not tweens written there
+// ---------------------------------------------------------------------------
+// A HyperFrames render is a SEEK per frame, from parallel workers, in any
+// order. A GSAP `set` or `to` remembers the value it found the first time it
+// rendered -- which, on a seek, is whatever the last seek left behind. Every
+// cue below is therefore a fromTo whose FROM is stated, and the from of each
+// is the to of the cue before it on the same element: `deckCues` walks the
+// cues in time order and carries every property's value forward. The page's
+// script is a dumb interpreter of that list. So the browser holds no logic a
+// test cannot see, and the times in it are deckChoreography's, untouched --
+// there is no second copy of the timing to drift.
+//
+// ---------------------------------------------------------------------------
+// Why the wipe is two translates and not a clip-path
+// ---------------------------------------------------------------------------
+// A wipe is a window sliding across text that stays put. The clip box (an
+// inline-block, so it is exactly as wide as its fitted title) moves by
+// xPercent and its child moves by the opposite xPercent: the two cancel for
+// the glyphs and not for the window. xPercent is relative to the element's OWN
+// width, so it follows the load-time text fit with nothing measured at tween
+// time, and it is a transform -- on the render's animatable allowlist, which
+// clip-path is not.
+//
+// ---------------------------------------------------------------------------
+// The progress fill is a fuse
+// ---------------------------------------------------------------------------
+// While clip k plays the accent fill burns from pip k toward pip k+1, reaching
+// it as the marker sets off. The marker then travels over track that is
+// already lit, and the fill states "how far through this clip" without a
+// second clock or a label.
+import { fileURLToPath } from "node:url";
+
+import { deckChoreography, deckLayout, pipSegments, pipXs, resolveDeck } from "./deck.mjs";
+
+/**
+ * GSAP, vendored, for every chrome region.
+ *
+ * It used to come off a CDN at render time. A deck render is ten thousand
+ * frames; one DNS failure in the middle of it is blank frames and no error
+ * anybody would recognise. The file is in the repo at the version the chart
+ * band was written against, and compose-chrome copies it in beside each page.
+ *
+ * Declared here, not in compose-chrome, and as `new URL(…, import.meta.url)`:
+ * a bundler resolves that form to the one file, and a name derived from
+ * import.meta in the module that does the fs work would mark every path that
+ * module joins (scripts/next-build-trace.test.mjs).
+ */
+export const GSAP_FILE = fileURLToPath(new URL("./assets/gsap.min.js", import.meta.url));
+
+/** Instant cues still take a millisecond: a zero-duration tween has its own seek rules. */
+const INSTANT = 0.001;
+
+const esc = (s) =>
+ String(s ?? "")
+ .replace(/&/g, "&")
+ .replace(/</g, "<")
+ .replace(/>/g, ">")
+ .replace(/"/g, """);
+
+const r4 = (v) => Math.round(v * 10000) / 10000;
+
+/** The host a QR resolves to -- the one thing on the tile a viewer cannot read off the code. */
+export function hostOf(url) {
+ try {
+ return new URL(url).host.replace(/^www\./, "");
+ } catch {
+ return "";
+ }
+}
+
+// ---------------------------------------------------------------------------
+// Colour. The palette is five hexes; the panel's lift is arithmetic on them.
+// ---------------------------------------------------------------------------
+
+function rgbOf(hex) {
+ const h = String(hex).replace("#", "");
+ const full = h.length === 3 ? [...h].map((c) => c + c).join("") : h.slice(0, 6);
+ const n = Number.parseInt(full, 16);
+ if (!Number.isFinite(n)) throw new Error(`deck: palette colour ${hex} is not a hex colour`);
+ return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
+}
+
+/** `a` moved toward `b` by `t` (0..1), as a hex. */
+export function mix(a, b, t) {
+ const x = rgbOf(a), y = rgbOf(b);
+ return `#${x.map((v, i) => Math.round(v + (y[i] - v) * t).toString(16).padStart(2, "0")).join("")}`;
+}
+
+/** A palette colour at an alpha, as rgba(). */
+export function rgba(hex, a) {
+ const [r, g, b] = rgbOf(hex);
+ return `rgba(${r}, ${g}, ${b}, ${a})`;
+}
+
+// ---------------------------------------------------------------------------
+// The cue list.
+// ---------------------------------------------------------------------------
+
+/**
+ * Segment i's two text states. "before" is wiped to the LEFT of its window and
+ * not yet visible; "shown" is at rest; "after" is wiped off to the right.
+ * The QR is edge-on at ±90° on either side, so the flip turns one way.
+ */
+const SEG_STATE = {
+ before: (lift) => ({
+ seg: { autoAlpha: 0 },
+ tc: { xPercent: -100 }, ti: { xPercent: 100, y: lift }, blade: { opacity: 0 },
+ sc: { xPercent: -100 }, si: { xPercent: 100 },
+ rule: { scaleX: 0, transformOrigin: "0% 50%" },
+ qr: { rotationY: -90 },
+ }),
+ shown: () => ({
+ seg: { autoAlpha: 1 },
+ tc: { xPercent: 0 }, ti: { xPercent: 0, y: 0 }, blade: { opacity: 0 },
+ sc: { xPercent: 0 }, si: { xPercent: 0 },
+ rule: { scaleX: 1, transformOrigin: "0% 50%" },
+ qr: { rotationY: 0 },
+ }),
+};
+
+const PARTS = ["tc", "ti", "blade", "sc", "si", "rule", "qr"];
+const keyOf = (i, part) => (part === "seg" ? `s${i}` : `s${i}.${part}`);
+
+/**
+ * Everything the deck's timeline does, as data.
+ *
+ * @returns {{ init: Record<string, object>, cues: Array<{ k: string, at: number,
+ * dur: number, from: object, to: object, ease: string, why: string }>,
+ * pips: Array<{ id: string, x: number }>, choreography: object }}
+ *
+ * `init` is each element's state at t = 0 (applied once at load); `cues` are in
+ * time order and each states its own from. `why` names the choreography entry
+ * a cue came from -- the tests match on it, and a reader of the page can too.
+ */
+export function deckCues(schedule, render) {
+ const deck = resolveDeck(render);
+ const lay = deckLayout(render);
+ const segs = schedule.segments;
+ const ch = deckChoreography(schedule, render);
+ const pipped = pipSegments(schedule);
+ const xs = pipXs(
+ pipped.length, lay.pipTrack.x0, lay.pipTrack.x1, deck.pip.spacing,
+ deck.pip.spacing === "time" ? { total: schedule.total, segments: pipped } : null,
+ );
+ const pips = pipped.map((s, k) => ({ id: s.id, x: r4(xs[k]) }));
+ const pipOf = new Map(pipped.map((s, k) => [s.id, k]));
+ const fuseX0 = xs[0] ?? 0;
+ const fuseW = xs.length > 1 ? xs[xs.length - 1] - xs[0] : 0;
+ const frac = (k) => (fuseW > 0 ? r4((xs[k] - fuseX0) / fuseW) : 0);
+ const lift = Math.round(lay.text.titleSize * 0.16);
+ const slideY = lay.height + 12;
+
+ // ---- t = 0 -------------------------------------------------------------
+ const init = {};
+ const put = (k, v) => { init[k] = { ...(init[k] ?? {}), ...v }; };
+ const startsHidden = !!segs[0]?.hideDeck;
+ put("panel", { y: startsHidden ? slideY : 0 });
+ put("marker", { x: r4(xs[0] ?? lay.pipTrack.x0) });
+ if (fuseW > 0) put("fuse", { scaleX: 0, transformOrigin: "0% 50%" });
+ pips.forEach((_, k) => put(`pip${k}`, { opacity: 0 }));
+ segs.forEach((s, i) => {
+ const st = i === 0 && !s.hideDeck ? SEG_STATE.shown() : SEG_STATE.before(lift);
+ put(keyOf(i, "seg"), st.seg);
+ for (const p of PARTS) put(keyOf(i, p), st[p]);
+ });
+
+ // ---- events, unordered; froms are filled in below ----------------------
+ const ev = [];
+ const add = (k, at, dur, to, ease, why) => ev.push({ k, at: r4(at), dur: r4(Math.max(INSTANT, dur)), to, ease, why });
+ const setSeg = (i, state, at, why) => {
+ add(keyOf(i, "seg"), at, INSTANT, state.seg, "none", why);
+ for (const p of PARTS) add(keyOf(i, p), at, INSTANT, state[p], "none", why);
+ };
+ const idx = new Map(segs.map((s, i) => [s.id, i]));
+ const filled = new Set();
+ const fillPipsBefore = (k, at, dur, why) => {
+ for (let j = 0; j < k; j += 1) {
+ if (filled.has(j)) continue;
+ filled.add(j);
+ add(`pip${j}`, at, dur, { opacity: 1 }, "power1.out", why);
+ }
+ };
+ // When the marker ARRIVES at each pip, and when it LEAVES: the fuse burns between.
+ const arrive = new Map();
+ const depart = new Map();
+ if (pips.length && !startsHidden) arrive.set(0, 0);
+
+ for (const h of ch.handovers) {
+ const a = idx.get(h.from), b = idx.get(h.to);
+ const why = `handover ${h.from}->${h.to}`;
+ // Out. A segment shorter than in + out would start leaving before it had
+ // arrived; the clamp below (per element) keeps the two from overlapping.
+ add(keyOf(a, "tc"), h.out[0], h.out[1] - h.out[0], { xPercent: 100 }, "power2.in", `${why} out`);
+ add(keyOf(a, "ti"), h.out[0], h.out[1] - h.out[0], { xPercent: -100 }, "power2.in", `${why} out`);
+ add(keyOf(a, "sc"), h.out[0], h.out[1] - h.out[0], { xPercent: 100 }, "power2.in", `${why} out`);
+ add(keyOf(a, "si"), h.out[0], h.out[1] - h.out[0], { xPercent: -100 }, "power2.in", `${why} out`);
+ add(keyOf(a, "rule"), h.out[0], h.out[1] - h.out[0], { scaleX: 0, transformOrigin: "100% 50%" }, "power2.in", `${why} out`);
+ add(keyOf(a, "qr"), h.qr[0], h.m - h.qr[0], { rotationY: 90 }, "power1.in", `${why} qr`);
+ add(keyOf(a, "seg"), h.m, INSTANT, { autoAlpha: 0 }, "none", `${why} out`);
+ // In.
+ add(keyOf(b, "seg"), h.m, INSTANT, { autoAlpha: 1 }, "none", `${why} in`);
+ add(keyOf(b, "tc"), h.in[0], h.in[1] - h.in[0], { xPercent: 0 }, "power3.out", `${why} in`);
+ add(keyOf(b, "ti"), h.in[0], h.in[1] - h.in[0], { xPercent: 0, y: 0 }, "power3.out", `${why} in`);
+ // The blade rides the reveal's leading edge and is gone as the title settles.
+ add(keyOf(b, "blade"), h.in[0], INSTANT, { opacity: 1 }, "none", `${why} in`);
+ add(keyOf(b, "blade"), h.in[0] + (h.in[1] - h.in[0]) * 0.35, (h.in[1] - h.in[0]) * 0.65, { opacity: 0 }, "power1.in", `${why} in`);
+ add(keyOf(b, "sc"), h.sub[0], h.sub[1] - h.sub[0], { xPercent: 0 }, "power3.out", `${why} sub`);
+ add(keyOf(b, "si"), h.sub[0], h.sub[1] - h.sub[0], { xPercent: 0 }, "power3.out", `${why} sub`);
+ add(keyOf(b, "rule"), h.in[0], h.in[1] - h.in[0], { scaleX: 1, transformOrigin: "0% 50%" }, "power3.out", `${why} in`);
+ add(keyOf(b, "qr"), h.m, h.qr[1] - h.m, { rotationY: 0 }, "power1.out", `${why} qr`);
+ // The pip.
+ const pa = pipOf.get(h.from), pb = pipOf.get(h.to);
+ add("marker", h.pip[0], h.pip[1] - h.pip[0], { x: pips[pb].x }, "power3.inOut", `${why} pip`);
+ fillPipsBefore(pb, h.pip[0], Math.min(0.25, h.pip[1] - h.pip[0]), `${why} pip`);
+ depart.set(pa, h.pip[0]);
+ arrive.set(pb, h.pip[1]);
+ }
+
+ for (const v of ch.visibility) {
+ if (v.at[1] - v.at[0] <= 0 && v.i === 0) continue; // hidden from the first frame: that is init
+ const why = `${v.hide ? "hide" : "show"} @${segs[v.i].id}`;
+ add("panel", v.at[0], v.at[1] - v.at[0], { y: v.hide ? slideY : 0 }, v.hide ? "power2.in" : "power3.out", why);
+ if (v.hide) {
+ // The text on the way down stays until it is off screen, then is gone
+ // for good -- nothing is handed over behind a card.
+ const prev = segs.slice(0, v.i).map((s, j) => j).filter((j) => !segs[j].hideDeck).pop();
+ if (prev !== undefined) {
+ add(keyOf(prev, "seg"), v.at[1], INSTANT, { autoAlpha: 0 }, "none", why);
+ const pk = pipOf.get(segs[prev].id);
+ if (!depart.has(pk)) depart.set(pk, v.at[0]);
+ }
+ } else {
+ // Whatever came before is already set when the deck rises: the new text
+ // is in place, the marker is on its pip and every pip before it is lit.
+ setSeg(v.i, SEG_STATE.shown(), v.at[0], why);
+ const k = pipOf.get(segs[v.i].id);
+ add("marker", v.at[0], INSTANT, { x: pips[k].x }, "none", why);
+ fillPipsBefore(k, v.at[0], INSTANT, why);
+ arrive.set(k, v.at[0]);
+ }
+ }
+
+ // The fuse: from pip k to pip k+1 over the time the marker rests on k.
+ if (fuseW > 0) {
+ for (let k = 0; k < pips.length - 1; k += 1) {
+ const a = arrive.get(k), b = depart.get(k);
+ if (a === undefined || b === undefined) continue;
+ add("fuse", a, b - a, { scaleX: frac(k + 1), transformOrigin: "0% 50%" }, "none", `fuse ${pips[k].id}`);
+ }
+ }
+
+ // ---- order, clamp, and state the froms ---------------------------------
+ ev.forEach((e, n) => { e.n = n; });
+ ev.sort((x, y) => x.at - y.at || x.n - y.n);
+ const state = Object.fromEntries(Object.entries(init).map(([k, v]) => [k, { ...v }]));
+ const freeAt = new Map();
+ const cues = [];
+ for (const e of ev) {
+ const free = freeAt.get(e.k) ?? 0;
+ let { at, dur } = e;
+ if (at < free) {
+ const end = at + dur;
+ at = r4(free);
+ dur = r4(Math.max(INSTANT, end - at));
+ }
+ const cur = state[e.k] ?? (state[e.k] = {});
+ const from = {};
+ for (const p of Object.keys(e.to)) from[p] = cur[p];
+ Object.assign(cur, e.to);
+ freeAt.set(e.k, r4(at + dur));
+ cues.push({ k: e.k, at, dur, from, to: e.to, ease: e.ease, why: e.why });
+ }
+ return { init, cues, pips, slideY, choreography: ch };
+}
+
+// ---------------------------------------------------------------------------
+// The page.
+// ---------------------------------------------------------------------------
+
+/** The subtitle's markup: its " · " separators become accent dots. */
+export function subtitleMarkup(text) {
+ return String(text ?? "")
+ .split(" · ")
+ .map((p) => `<span class="part">${esc(p)}</span>`)
+ .join('<span class="sep">·</span>');
+}
+
+/**
+ * The deck composition's HTML.
+ *
+ * `fonts` = `{ regular, bold }` asset-relative paths (DeckSans / DeckSansBold),
+ * `qrSrcs` = `{ [segmentId]: "assets/qrNN.png" }`, `gsap` = the vendored
+ * script's asset path. `from`/`duration` render a window of the cut: the root
+ * declares `duration` seconds and the timeline plays the cut's [from,
+ * from + duration] -- the page is otherwise identical.
+ */
+export function deckHtml(schedule, render, opts = {}) {
+ const deck = resolveDeck(render);
+ const lay = deckLayout(render);
+ const pal = render.palette;
+ const W = lay.width, H = lay.height;
+ const fonts = opts.fonts ?? {};
+ const qrSrcs = opts.qrSrcs ?? {};
+ const gsapSrc = opts.gsap ?? "assets/gsap.min.js";
+ const from = Number(opts.from ?? 0);
+ const total = schedule.total;
+ const dur = opts.duration != null ? Number(opts.duration) : r4(total - from);
+ if (!(dur > 0)) throw new Error(`deck: nothing to render from ${from}s of a ${total}s cut`);
+ const windowed = from > 0 || Math.abs(dur - total) > 1e-6;
+
+ const { init, cues, pips } = deckCues(schedule, render);
+ const t = lay.text;
+ const tr = lay.pipTrack;
+ const qr = lay.qr;
+ const pip = deck.pip;
+ const panel = deck.background === "panel";
+
+ // The lift: a few percent toward the foreground, so the deck reads as a
+ // surface standing on the frame's ground rather than a hole cut in it.
+ const top = panel ? mix(pal.bg, pal.fg, 0.095) : pal.bg;
+ const bottom = panel ? mix(pal.bg, pal.fg, 0.04) : pal.bg;
+ // The untitled arrangement, centred on the same block.
+ const uSubSize = Math.round(t.subtitleSize * 1.35);
+ const uSubBox = Math.round(uSubSize * 1.3);
+ const uSubY = Math.round(t.titleY + (t.subtitleY + t.subtitleBox - t.titleY - uSubBox - 14) / 2);
+ const uRuleY = uSubY + uSubBox + 6;
+ const plateX = qr ? qr.x - 46 : W;
+ const ink = panel ? mix(pal.bg, pal.fg, 0.085) : pal.bg; // what the marker's ring cuts back to
+
+ const segHtml = schedule.segments
+ .map((s, i) => {
+ const src = qrSrcs[s.id];
+ return (
+ `<div class="seg${s.title ? "" : " untitled"}" data-seg="${esc(s.id)}" data-k="s${i}">` +
+ `<div class="tc" data-k="s${i}.tc"><div class="ti" data-k="s${i}.ti">` +
+ `<div class="deck-title">${esc(s.title)}</div></div>` +
+ `<div class="blade" data-k="s${i}.blade"></div></div>` +
+ `<div class="rule" data-k="s${i}.rule"></div>` +
+ `<div class="sc" data-k="s${i}.sc"><div class="si" data-k="s${i}.si">` +
+ `<div class="deck-sub">${subtitleMarkup(s.subtitle)}</div></div></div>` +
+ (qr
+ ? `<div class="deck-qr${src ? "" : " empty"}" data-k="s${i}.qr">` +
+ (src ? `<img src="${esc(src)}" width="${qr.size}" height="${qr.size}" alt="">` : "") +
+ `</div>` +
+ (src && hostOf(s.qrUrl) ? `<div class="deck-qr-host">${esc(hostOf(s.qrUrl))}</div>` : "")
+ : `<div class="deck-qr empty" data-k="s${i}.qr"></div>`) +
+ `</div>`
+ );
+ })
+ .join("\n ");
+
+ const pipHtml = pips
+ .map(
+ (p, k) =>
+ `<div class="pip" style="left:${r4(p.x - pip.size / 2)}px"></div>` +
+ `<div class="pip lit" data-k="pip${k}" style="left:${r4(p.x - pip.size / 2)}px"></div>`,
+ )
+ .join("");
+ const fuseLeft = pips[0]?.x ?? tr.x0;
+ const fuseWidth = pips.length > 1 ? pips[pips.length - 1].x - fuseLeft : 0;
+
+ const data = {
+ total,
+ window: windowed ? { from, dur } : null,
+ titleSize: t.titleSize,
+ floor: Math.ceil(t.titleSize * 0.6),
+ ids: schedule.segments.map((s) => s.id),
+ init,
+ cues: cues.map(({ why, ...c }) => c),
+ };
+ // `</script>` inside a JSON string would close the tag; a title may say anything.
+ const json = JSON.stringify(data).replace(/</g, "\\u003c");
+
+ return `<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=${W}, height=${H}" />
+ <script src="${esc(gsapSrc)}"></script>
+ <style>
+ /* Private family names, real files beside the page. A bare local() falls
+ back silently in the render browser, and naming a real family anywhere
+ in the stack makes the compiler fetch it from the network -- see
+ docs/quirks.md. sans-serif is the only fallback. */
+ @font-face { font-family: 'DeckSans'; font-weight: 400; font-style: normal;
+ src: url('${esc(fonts.regular ?? "")}'); }
+ @font-face { font-family: 'DeckSansBold'; font-weight: 400; font-style: normal;
+ src: url('${esc(fonts.bold ?? "")}'); }
+ * { margin: 0; padding: 0; box-sizing: border-box; }
+ html, body { width: ${W}px; height: ${H}px; overflow: hidden; background: transparent; }
+ body { font-family: 'DeckSans', sans-serif; font-synthesis: none; color: ${pal.fg};
+ -webkit-font-smoothing: antialiased; text-rendering: geometricPrecision; }
+ #root { position: relative; width: ${W}px; height: ${H}px; overflow: hidden; }
+ #deck-clip { position: absolute; left: 0; top: 0; width: ${W}px; height: ${H}px; }
+ .panel { position: absolute; left: 0; top: 0; width: ${W}px; height: ${H}px; }
+ .ground { position: absolute; inset: 0;
+ background: linear-gradient(180deg, ${top} 0%, ${bottom} 100%); }
+ ${panel ? `
+ /* The top edge: a hairline that carries the accent in from the left and
+ settles to a quiet rule -- the one line that says "panel", not "band". */
+ .edge { position: absolute; left: 0; top: 0; width: ${W}px; height: 1px;
+ background: linear-gradient(90deg, ${rgba(pal.accent, 0.9)} 0px, ${rgba(pal.accent, 0.35)} ${Math.round(W * 0.18)}px,
+ ${rgba(pal.fg, 0.14)} ${Math.round(W * 0.42)}px, ${rgba(pal.fg, 0.14)} ${W}px); }
+ .sheen { position: absolute; left: 0; top: 1px; width: ${W}px; height: 28px;
+ background: linear-gradient(180deg, ${rgba(pal.fg, 0.035)} 0%, ${rgba(pal.fg, 0)} 100%); }` : ""}
+ /* The source cell: the QR and its host stand in a cell of their own,
+ a shade down from the panel, so the deck reads as two fields -- what
+ this is, and where to check it -- rather than a caption beside a code. */
+ .plate { position: absolute; left: ${plateX}px; top: 0; width: ${W - plateX}px; height: ${H}px;
+ background: linear-gradient(180deg, ${mix(top, pal.bg, 0.55)} 0%, ${mix(bottom, pal.bg, 0.6)} 100%);
+ border-left: 1px solid ${rgba(pal.fg, 0.07)}; }
+ .track { position: absolute; left: 0; top: ${tr.y}px; width: ${W}px; height: 0; }
+ .rail { position: absolute; left: ${tr.x0}px; top: -1px; width: ${tr.x1 - tr.x0}px; height: 2px;
+ border-radius: 1px; background: ${rgba(pal.fg, 0.1)}; }
+ .fuse { position: absolute; left: ${r4(fuseLeft)}px; top: -1px; width: ${r4(fuseWidth)}px; height: 2px;
+ border-radius: 1px; background: ${pal.accent}; }
+ .pip { position: absolute; top: ${-pip.size / 2}px; width: ${pip.size}px; height: ${pip.size}px;
+ border-radius: 50%; background: ${mix(bottom, pal.fg, 0.32)}; }
+ .pip.lit { background: ${pal.accent}; }
+ .marker { position: absolute; left: ${-pip.activeSize / 2}px; top: ${-pip.activeSize / 2}px;
+ width: ${pip.activeSize}px; height: ${pip.activeSize}px; border-radius: 50%;
+ background: ${pal.fg};
+ box-shadow: 0 0 0 3px ${ink}, 0 0 0 5px ${rgba(pal.accent, 0.95)}, 0 0 18px 4px ${rgba(pal.accent, 0.45)}; }
+ .seg { position: absolute; left: 0; top: 0; width: ${W}px; height: ${H}px; }
+ .tc, .sc { position: absolute; left: ${t.x}px; display: inline-block; overflow: hidden;
+ max-width: ${t.width}px; vertical-align: top; }
+ .tc { top: ${t.titleY}px; height: ${t.titleBox}px; }
+ .sc { top: ${t.subtitleY}px; height: ${t.subtitleBox}px; }
+ .ti, .si { display: block; }
+ .deck-title { display: block; max-width: ${t.width}px; white-space: nowrap; overflow: hidden;
+ text-overflow: ellipsis; font-family: 'DeckSansBold', sans-serif;
+ font-size: ${t.titleSize}px; line-height: ${t.titleBox}px; letter-spacing: -0.012em;
+ color: ${pal.fg}; }
+ .deck-sub { display: block; max-width: ${t.width}px; white-space: nowrap; overflow: hidden;
+ text-overflow: ellipsis; font-size: ${t.subtitleSize}px; line-height: ${t.subtitleBox}px;
+ letter-spacing: 0.005em; color: ${pal.muted}; }
+ .deck-sub .sep { color: ${pal.accent}; padding: 0 0.42em; font-family: 'DeckSansBold', sans-serif; }
+ .blade { position: absolute; right: 0; top: ${Math.round(t.titleBox * 0.14)}px; width: 4px;
+ height: ${Math.round(t.titleBox * 0.72)}px; border-radius: 2px; background: ${pal.accent};
+ box-shadow: 0 0 14px 2px ${rgba(pal.accent, 0.55)}; }
+ /* Untitled: the source line is promoted into the title's band rather than
+ sitting under a blank one. */
+ .seg.untitled .tc { visibility: hidden; }
+ .seg.untitled .sc { top: ${uSubY}px; height: ${uSubBox}px; }
+ .seg.untitled .deck-sub { font-size: ${uSubSize}px; line-height: ${uSubBox}px; color: ${mix(pal.muted, pal.fg, 0.55)}; }
+ .seg.untitled .rule { top: ${uRuleY}px; }
+ .rule { position: absolute; left: ${t.x}px; top: ${t.ruleY}px; width: ${Math.round(t.titleSize * 1.5)}px;
+ height: 4px; border-radius: 2px; background: ${pal.accent}; }
+ .deck-qr { position: absolute; left: ${qr?.x ?? 0}px; top: ${qr?.y ?? 0}px;
+ width: ${qr?.size ?? 0}px; height: ${qr?.size ?? 0}px; transform-style: preserve-3d; }
+ .deck-qr-host { position: absolute; left: ${(qr?.x ?? 0) - 30}px; top: ${qr?.y ?? 0}px; width: 18px;
+ height: ${qr?.size ?? 0}px; writing-mode: vertical-rl; transform: rotate(180deg);
+ text-align: left; white-space: nowrap; font-size: 11px; line-height: 18px;
+ letter-spacing: 0.1em; text-transform: uppercase; color: ${rgba(pal.muted, 0.85)}; }
+ .deck-qr img { display: block; width: ${qr?.size ?? 0}px; height: ${qr?.size ?? 0}px; border-radius: 6px;
+ image-rendering: pixelated; backface-visibility: hidden;
+ box-shadow: 0 0 0 1px ${rgba(pal.fg, 0.25)}, 0 6px 18px rgba(0, 0, 0, 0.35); }
+ </style>
+ </head>
+ <body>
+ <div id="root" data-composition-id="deck" data-start="0" data-duration="${r4(dur)}"
+ data-width="${W}" data-height="${H}">
+ <div id="deck-clip" class="clip" data-start="0" data-duration="${r4(dur)}" data-track-index="1">
+ <div class="panel" data-k="panel">
+ <div class="ground"></div>
+ ${panel && qr ? `<div class="plate"></div>` : ""}
+ ${panel ? `<div class="edge"></div><div class="sheen"></div>` : ""}
+ <div class="track">
+ <div class="rail"></div>
+ ${fuseWidth > 0 ? `<div class="fuse" data-k="fuse"></div>` : ""}
+ ${pipHtml}
+ <div class="marker" data-k="marker"></div>
+ </div>
+ ${segHtml}
+ </div>
+ </div>
+ </div>
+
+ <script id="deck-data" type="application/json">${json}</script>
+ <script>
+ const D = JSON.parse(document.getElementById("deck-data").textContent);
+ const byK = {};
+ for (const el of document.querySelectorAll("[data-k]")) byK[el.dataset.k] = el;
+ const QR_PERSPECTIVE = 700;
+
+ // t = 0, then the cues. Every cue states its from, so a seek from
+ // anywhere to anywhere lands on the same pixels.
+ for (const k of Object.keys(D.init)) {
+ if (byK[k]) gsap.set(byK[k], k.endsWith(".qr") ? { transformPerspective: QR_PERSPECTIVE, ...D.init[k] } : D.init[k]);
+ }
+ const inner = gsap.timeline({ paused: true });
+ for (const c of D.cues) {
+ const el = byK[c.k];
+ if (!el) continue;
+ inner.fromTo(el, c.from, { ...c.to, duration: c.dur, ease: c.ease, immediateRender: false }, c.at);
+ }
+ // A window of the cut plays the whole timeline's [from, from + dur]
+ // through one tween; seeking the window seeks the cut.
+ let tl = inner;
+ if (D.window) {
+ tl = gsap.timeline({ paused: true });
+ tl.add(inner.tweenFromTo(D.window.from, D.window.from + D.window.dur,
+ { duration: D.window.dur, ease: "none" }), 0);
+ }
+ window.__timelines = window.__timelines || {};
+ window.__timelines["deck"] = tl;
+
+ // The text fit. A title wider than its column shrinks a pixel at a time
+ // to 60 % of its size, then ellipsizes (the CSS already does). It runs
+ // once the faces are in: measuring the fallback face would fit the wrong
+ // glyphs. The renderer waits on document.fonts.ready, and these loads are
+ // requested before it looks.
+ function fitTitle(node) {
+ let s = D.titleSize;
+ node.style.fontSize = s + "px";
+ while (s > D.floor && node.scrollWidth > node.clientWidth + 0.5) {
+ s -= 1;
+ node.style.fontSize = s + "px";
+ }
+ }
+ const fitAll = () => document.querySelectorAll(".deck-title").forEach(fitTitle);
+ const ready = Promise.all([
+ document.fonts.load(D.titleSize + "px DeckSansBold"),
+ document.fonts.load("26px DeckSans"),
+ ]).catch(() => {}).then(() => { fitAll(); document.documentElement.dataset.fit = "1"; });
+
+ const params = new URLSearchParams(location.search);
+ // The review still: a seek and nothing else -- the same seek the renderer
+ // makes for every frame.
+ const still = params.get("still");
+ if (still !== null) {
+ tl.seek(Number(still), false);
+ ready.then(() => tl.seek(Number(still), false));
+ }
+
+ // umtool's live preview. Never on in a render: nothing here moves a
+ // pixel unless a parent frame asks.
+ if (params.get("preview") === "1") {
+ const esc = (s) => String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
+ const subMarkup = (s) => String(s ?? "").split(" · ").map((p) => '<span class="part">' + esc(p) + "</span>")
+ .join('<span class="sep">·</span>');
+ window.addEventListener("message", (e) => {
+ const m = e.data || {};
+ if (m.type === "deck:seek") tl.seek(Math.max(0, Number(m.t) || 0), false);
+ if (m.type === "deck:text") {
+ const seg = [...document.querySelectorAll("[data-seg]")].find((n) => n.dataset.seg === m.id);
+ if (!seg) return;
+ if (typeof m.title === "string") {
+ const node = seg.querySelector(".deck-title");
+ node.textContent = m.title;
+ seg.classList.toggle("untitled", !m.title);
+ fitTitle(node);
+ }
+ if (typeof m.subtitle === "string") seg.querySelector(".deck-sub").innerHTML = subMarkup(m.subtitle);
+ }
+ });
+ ready.then(() => {
+ if (window.parent !== window) {
+ window.parent.postMessage({ type: "deck:ready", total: D.total, ids: D.ids }, "*");
+ }
+ });
+ }
+ </script>
+ </body>
+</html>
+`;
+}
diff --git a/umtool/report-to-video/chrome-deck.test.mjs b/umtool/report-to-video/chrome-deck.test.mjs
@@ -0,0 +1,309 @@
+// Tests for the deck composition (chrome-deck.mjs) and compose-chrome's deck
+// path: the page has every segment's nodes, the timeline's times are
+// deckChoreography's, nothing leaves the machine, and the render cache skips
+// what it should.
+//
+// Run with: pnpm test:scripts
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { chmodSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import path from "node:path";
+import test from "node:test";
+import { fileURLToPath } from "node:url";
+
+import { deckCues, deckHtml, GSAP_FILE, hostOf, mix, subtitleMarkup } from "./chrome-deck.mjs";
+import { deckChoreography, deckLayout } from "./deck.mjs";
+
+const HERE = path.dirname(fileURLToPath(import.meta.url));
+
+const RENDER = {
+ width: 1920,
+ height: 1080,
+ fps: 30,
+ transition: 0.5,
+ palette: { bg: "#12101a", fg: "#f4f1ea", muted: "#9a93ad", accent: "#a97bff", amber: "#ffc860" },
+ chrome: { engine: "hyperframes", layout: "deck", deck: {} },
+};
+
+/** A cut that opens and closes on a card, with an untitled clip and a hostile title. */
+function schedule() {
+ const seg = (id, type, start, duration, extra = {}) => ({
+ id, type, start, duration, end: start + duration,
+ title: "", subtitle: "", qrUrl: null, hideDeck: false, ...extra,
+ });
+ return {
+ version: 1, kind: "deck", estimated: true, fps: 30, transition: 0.5, total: 40, multiChannel: false,
+ segments: [
+ seg("t00", "card", 0, 5, { title: "Opening", subtitle: "a card", hideDeck: true }),
+ seg("c01", "clip", 4.5, 10, { title: 'He said "largest" </script><b>', subtitle: "Ferret Rescue · Sep 5, 2024", qrUrl: "https://jasolyzer.pages.dev/?v=a&t=51" }),
+ seg("c02", "clip", 14, 8, { title: "A sanctuary", subtitle: "Ferret Rescue · Sep 5, 2024", qrUrl: "https://jasolyzer.pages.dev/?v=a&t=62" }),
+ seg("c03", "clip", 21.5, 13, { subtitle: "Untitled source · Sep 29, 2024", qrUrl: "https://www.youtube.com/watch?v=b" }),
+ seg("end", "card", 34, 6, { title: "Close", subtitle: "", hideDeck: true }),
+ ],
+ };
+}
+
+const QRS = { c01: "assets/qr00.png", c02: "assets/qr01.png", c03: "assets/qr02.png" };
+const FONTS = { regular: "assets/DeckSans.ttf", bold: "assets/DeckSansBold.ttf" };
+
+const dataOf = (html) => JSON.parse(/<script id="deck-data" type="application\/json">(.*?)<\/script>/s.exec(html)[1]);
+const near = (a, b, msg) => assert.ok(Math.abs(a - b) < 1e-3, `${msg}: ${a} != ${b}`);
+
+test("every segment has its own nodes: title, subtitle, QR", () => {
+ const s = schedule();
+ const html = deckHtml(s, RENDER, { fonts: FONTS, qrSrcs: QRS });
+ for (const seg of s.segments) {
+ const open = html.indexOf(`data-seg="${seg.id}"`);
+ assert.ok(open > 0, `no node for ${seg.id}`);
+ const body = html.slice(open, html.indexOf('<div class="seg', open + 1) > 0 ? html.indexOf('<div class="seg', open + 1) : undefined);
+ assert.match(body, /class="deck-title"/, `${seg.id} title`);
+ assert.match(body, /class="deck-sub"/, `${seg.id} subtitle`);
+ assert.match(body, /class="deck-qr/, `${seg.id} qr`);
+ if (QRS[seg.id]) assert.ok(body.includes(`<img src="${QRS[seg.id]}"`), `${seg.id} qr img`);
+ }
+ // The title is text, not markup, and cannot close the page's script.
+ assert.ok(html.includes("He said "largest" </script><b>"));
+ assert.equal((html.match(/<\/script>/g) ?? []).length, 3, "gsap, data, runtime -- and no fourth");
+ // An untitled clip is marked, so its source line takes the title's band.
+ assert.match(html, /class="seg untitled" data-seg="c03"/);
+ // The composition contract.
+ assert.match(html, /data-composition-id="deck" data-start="0" data-duration="40"/);
+ assert.match(html, /data-width="1920" data-height="190"/);
+ assert.match(html, /window\.__timelines\["deck"\] = tl/);
+ assert.match(html, /font-family: 'DeckSans'/);
+ assert.match(html, /font-family: 'DeckSansBold'/);
+});
+
+test("nothing on the page leaves the machine", () => {
+ const html = deckHtml(schedule(), RENDER, { fonts: FONTS, qrSrcs: QRS });
+ assert.doesNotMatch(html, /https?:\/\//, "a URL in the page");
+ assert.doesNotMatch(html, /(?:src|href)="\/\//, "a protocol-relative URL");
+ assert.doesNotMatch(html, /@import|fonts\.googleapis|cdn\./);
+ assert.match(html, /<script src="assets\/gsap\.min\.js"><\/script>/);
+ // Every font stack ends in sans-serif and names no real family.
+ for (const m of html.matchAll(/font-family: ([^;]+);/g)) {
+ assert.match(m[1], /^'DeckSans(?:Bold)?'(?:, sans-serif)?$/, m[1]);
+ }
+ // The QR's host is printed as a host, not a link.
+ assert.ok(html.includes(">jasolyzer.pages.dev<"));
+ assert.ok(html.includes(">youtube.com<"));
+});
+
+test("the chart band's GSAP is vendored too", () => {
+ const src = readFileSync(path.join(HERE, "compose-chrome.mjs"), "utf8");
+ assert.doesNotMatch(src, /cdn\.jsdelivr|unpkg|cdnjs/);
+ assert.match(src, /<script src="assets\/gsap\.min\.js"><\/script>/);
+ assert.match(readFileSync(GSAP_FILE, "utf8").slice(0, 200), /GSAP 3\.14\.2/);
+});
+
+test("the timeline's times are deckChoreography's", () => {
+ const s = schedule();
+ const ch = deckChoreography(s, RENDER);
+ const { cues } = deckCues(s, RENDER);
+ const ids = s.segments.map((x) => x.id);
+ const at = (k, why) => cues.filter((c) => c.k === k && c.why.startsWith(why));
+ assert.equal(ch.handovers.length, 2);
+ for (const h of ch.handovers) {
+ const a = ids.indexOf(h.from), b = ids.indexOf(h.to);
+ const why = `handover ${h.from}->${h.to}`;
+ const [out] = at(`s${a}.tc`, `${why} out`);
+ near(out.at, h.out[0], "title out starts");
+ near(out.at + out.dur, h.out[1], "title out ends");
+ const [inn] = at(`s${b}.tc`, `${why} in`);
+ near(inn.at, h.in[0], "title in starts");
+ near(inn.at + inn.dur, h.in[1], "title in ends");
+ assert.equal(inn.ease, "power3.out");
+ const [sub] = at(`s${b}.sc`, `${why} sub`);
+ near(sub.at, h.sub[0], "subtitle trails");
+ near(sub.at + sub.dur, h.sub[1], "subtitle ends");
+ const [rule] = at(`s${b}.rule`, `${why} in`);
+ near(rule.at, h.in[0], "the rule grows with the title");
+ const [qrOut] = at(`s${a}.qr`, `${why} qr`);
+ const [qrIn] = at(`s${b}.qr`, `${why} qr`);
+ near(qrOut.at, h.qr[0], "qr flips out");
+ near(qrOut.at + qrOut.dur, h.m, "qr edge-on at m");
+ near(qrIn.at, h.m, "qr flips in from m");
+ near(qrIn.at + qrIn.dur, h.qr[1], "qr square again");
+ const [pip] = at("marker", why);
+ near(pip.at, h.pip[0], "marker leaves");
+ near(pip.at + pip.dur, h.pip[1], "marker arrives");
+ }
+ // The slides: down off the first card from the first frame, up into c01, down into the end card.
+ const { init } = deckCues(s, RENDER);
+ assert.equal(init.panel.y, 202);
+ const slides = cues.filter((c) => c.k === "panel");
+ const vis = ch.visibility.filter((v) => v.at[1] > v.at[0]);
+ assert.equal(slides.length, vis.length);
+ slides.forEach((c, n) => {
+ near(c.at, vis[n].at[0], "slide starts");
+ near(c.at + c.dur, vis[n].at[1], "slide ends");
+ assert.equal(c.to.y, vis[n].hide ? 202 : 0);
+ });
+});
+
+test("the page carries exactly deckCues' list", () => {
+ const s = schedule();
+ const { init, cues } = deckCues(s, RENDER);
+ const d = dataOf(deckHtml(s, RENDER, { fonts: FONTS, qrSrcs: QRS }));
+ assert.deepEqual(d.init, init);
+ assert.deepEqual(d.cues, cues.map(({ why, ...c }) => c));
+ assert.equal(d.window, null);
+ assert.deepEqual(d.ids, s.segments.map((x) => x.id));
+ assert.equal(d.floor, Math.ceil(54 * 0.6));
+});
+
+test("every cue states the from the cue before it left, and none overlaps another on one element", () => {
+ const { init, cues } = deckCues(schedule(), RENDER);
+ const state = JSON.parse(JSON.stringify(init));
+ const busy = new Map();
+ for (const c of cues) {
+ for (const [p, v] of Object.entries(c.from)) assert.deepEqual(v, state[c.k]?.[p], `${c.k}.${p} at ${c.at}`);
+ Object.assign((state[c.k] ??= {}), c.to);
+ assert.ok(c.at >= (busy.get(c.k) ?? 0) - 1e-9, `${c.k} overlaps at ${c.at}`);
+ busy.set(c.k, c.at + c.dur);
+ }
+ for (let i = 1; i < cues.length; i += 1) assert.ok(cues[i].at >= cues[i - 1].at, "in time order");
+});
+
+test("pips: one per segment shown with the deck, none for cards, all lit by the end", () => {
+ const s = schedule();
+ const { pips, cues, init } = deckCues(s, RENDER);
+ assert.deepEqual(pips.map((p) => p.id), ["c01", "c02", "c03"]);
+ const lay = deckLayout(RENDER);
+ assert.equal(pips[0].x, lay.pipTrack.x0);
+ assert.equal(pips[2].x, lay.pipTrack.x1);
+ for (let k = 0; k < 2; k += 1) {
+ assert.equal(init[`pip${k}`].opacity, 0);
+ assert.ok(cues.some((c) => c.k === `pip${k}` && c.to.opacity === 1), `pip ${k} is lit once passed`);
+ }
+ // The fuse reaches each next pip as the marker sets off for it.
+ const fuse = cues.filter((c) => c.k === "fuse");
+ const marker = cues.filter((c) => c.k === "marker" && c.dur > 0.01);
+ assert.equal(fuse.length, 2);
+ near(fuse[0].at + fuse[0].dur, marker[0].at, "fuse meets the marker");
+ assert.equal(fuse[1].to.scaleX, 1);
+});
+
+test("a window plays the cut's own clock from `from`", () => {
+ const s = schedule();
+ const html = deckHtml(s, RENDER, { fonts: FONTS, qrSrcs: QRS, from: 12, duration: 6 });
+ assert.match(html, /data-composition-id="deck" data-start="0" data-duration="6"/);
+ assert.deepEqual(dataOf(html).window, { from: 12, dur: 6 });
+ assert.throws(() => deckHtml(s, RENDER, { from: 40 }), /nothing to render/);
+});
+
+test("flush has no lift; panel does", () => {
+ const flush = { ...RENDER, chrome: { ...RENDER.chrome, deck: { background: "flush" } } };
+ const f = deckHtml(schedule(), flush, { fonts: FONTS, qrSrcs: QRS });
+ assert.match(f, new RegExp(`linear-gradient\\(180deg, ${RENDER.palette.bg} 0%, ${RENDER.palette.bg} 100%\\)`));
+ assert.doesNotMatch(f, /class="edge"/);
+ const p = deckHtml(schedule(), RENDER, { fonts: FONTS, qrSrcs: QRS });
+ assert.match(p, /class="edge"/);
+ assert.ok(p.includes(mix(RENDER.palette.bg, RENDER.palette.fg, 0.095)));
+});
+
+test("qr.show false: no codes anywhere", () => {
+ const off = { ...RENDER, chrome: { ...RENDER.chrome, deck: { qr: { show: false } } } };
+ const html = deckHtml(schedule(), off, { fonts: FONTS, qrSrcs: {} });
+ assert.doesNotMatch(html, /<img/);
+ assert.doesNotMatch(html, /class="plate"/);
+});
+
+test("subtitleMarkup and hostOf", () => {
+ assert.equal(
+ subtitleMarkup("A <b> · Sep 5, 2024"),
+ '<span class="part">A <b></span><span class="sep">·</span><span class="part">Sep 5, 2024</span>',
+ );
+ assert.equal(subtitleMarkup(""), '<span class="part"></span>');
+ assert.equal(hostOf("https://www.youtube.com/watch?v=x"), "youtube.com");
+ assert.equal(hostOf("not a url"), "");
+});
+
+// ---------------------------------------------------------------------------
+// compose-chrome's deck path, end to end with a stub renderer.
+// ---------------------------------------------------------------------------
+
+const haveTools = ["qrencode", "magick"].every(
+ (b) => spawnSync(b, [b === "magick" ? "-version" : "-V"], { stdio: "ignore" }).status === 0,
+);
+
+test("composeChrome(deck): project, frames, cache hit, and a changed title re-renders", { skip: !haveTools }, async () => {
+ const { composeChrome } = await import("./compose-chrome.mjs");
+ const dir = mkdtempSync(path.join(tmpdir(), "deck-s2-"));
+ const prevBin = process.env.HYPERFRAMES_BIN;
+ try {
+ // A renderer stub: writes as many frames as the root declares and logs each run.
+ const stub = path.join(dir, "hf-stub.mjs");
+ writeFileSync(stub, `#!/usr/bin/env node
+import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import path from "node:path";
+const a = process.argv.slice(2);
+const out = a[a.indexOf("--output") + 1], fps = Number(a[a.indexOf("--fps") + 1]), proj = a[a.length - 1];
+const dur = Number(/data-duration="([\\d.]+)"/.exec(readFileSync(path.join(proj, "index.html"), "utf8"))[1]);
+mkdirSync(out, { recursive: true });
+for (let i = 1; i <= Math.round(dur * fps); i += 1) writeFileSync(path.join(out, "frame_" + String(i).padStart(6, "0") + ".png"), "");
+appendFileSync(${JSON.stringify(path.join(dir, "runs.log"))}, a.join(" ") + "\\n");
+`);
+ chmodSync(stub, 0o755);
+ process.env.HYPERFRAMES_BIN = stub;
+
+ const manifest = {
+ slug: "t", title: "t", provenance: {},
+ render: {
+ ...RENDER,
+ fontRegular: path.join(HERE, "fonts", "IBMPlexMono-Regular.ttf"),
+ fontBold: path.join(HERE, "fonts", "IBMPlexMono-Bold.ttf"),
+ },
+ timeline: [],
+ };
+ const manifestPath = path.join(dir, "video.manifest.json");
+ writeFileSync(manifestPath, JSON.stringify(manifest));
+ const sched = { ...schedule(), total: 2, segments: schedule().segments.slice(1, 3).map((x, i) => ({ ...x, start: i * 1, end: i + 1, duration: 1 })) };
+ const runs = () => { try { return readFileSync(path.join(dir, "runs.log"), "utf8").trim().split("\n").length; } catch { return 0; } };
+
+ const first = await composeChrome({ manifestPath, region: "deck", schedule: sched, doRender: true, workers: 2 });
+ assert.equal(first.cached, false);
+ assert.equal(first.frameCount, 60);
+ assert.equal(first.projDir, path.join(dir, "out", "sourced", "chrome", "deck"));
+ assert.equal(first.frames, path.join(dir, "out", "sourced", "chrome", "deck-frames"));
+ assert.equal(readFileSync(path.join(first.frames, ".key"), "utf8").trim(), first.key);
+ assert.equal(runs(), 1);
+ const html = readFileSync(path.join(first.projDir, "index.html"), "utf8");
+ assert.match(html, /src="assets\/qr00\.png"/);
+ assert.match(html, /url\('assets\/DeckSansBold\.ttf'\)/);
+ assert.match(readFileSync(path.join(dir, "runs.log"), "utf8"), /render --format png-sequence --quality high --fps 30 -w 2 --no-browser-gpu --output/);
+
+ const again = await composeChrome({ manifestPath, region: "deck", schedule: sched, doRender: true, workers: 2 });
+ assert.equal(again.cached, true);
+ assert.equal(again.key, first.key, "same inputs, same key (the QR PNGs are byte-stable)");
+ assert.equal(runs(), 1);
+
+ const edited = { ...sched, segments: sched.segments.map((x, i) => (i ? x : { ...x, title: "Edited" })) };
+ const third = await composeChrome({ manifestPath, region: "deck", schedule: edited, doRender: true, workers: 2 });
+ assert.equal(third.cached, false);
+ assert.notEqual(third.key, first.key);
+ assert.equal(runs(), 2);
+
+ // A preview composes elsewhere and never renders.
+ const prev = await composeChrome({ manifestPath, region: "deck", schedule: sched, preview: true, doRender: true });
+ assert.equal(prev.projDir, path.join(dir, "out", "sourced", "chrome", "deck-preview"));
+ assert.equal(prev.frames, null);
+ assert.equal(runs(), 2);
+
+ // A window is its own project and frames.
+ const win = await composeChrome({ manifestPath, region: "deck", schedule: sched, doRender: true, from: 0.5, duration: 1 });
+ assert.equal(win.frames, path.join(dir, "out", "sourced", "chrome", "deck-from0.5-frames"));
+ assert.equal(win.frameCount, 30);
+
+ // Without a schedule passed in, it reads out/<variant>/schedule.json -- and says so when there is none.
+ await assert.rejects(
+ composeChrome({ manifestPath, region: "deck", outDir: path.join(dir, "nowhere") }),
+ /the deck needs .*schedule\.json/,
+ );
+ } finally {
+ if (prevBin === undefined) delete process.env.HYPERFRAMES_BIN;
+ else process.env.HYPERFRAMES_BIN = prevBin;
+ rmSync(dir, { recursive: true, force: true });
+ }
+});
diff --git a/umtool/report-to-video/compose-chrome.mjs b/umtool/report-to-video/compose-chrome.mjs
@@ -31,16 +31,23 @@
// and the crossfade -- so the build writes the schedule and this reads it.
// Recomputing it here would be a second implementation of segmentOffsets() and
// would drift the first time the transition changed.
-import { copyFile, mkdir, readFile, writeFile } from "node:fs/promises";
-import { execFile } from "node:child_process";
+import { copyFile, mkdir, readdir, readFile, rm, writeFile } from "node:fs/promises";
+import { execFile, spawn } from "node:child_process";
import { promisify } from "node:util";
import path from "node:path";
import { ledgerTotals, dateKey } from "./ledger-totals.mjs";
import { selectVariant } from "./build-video.mjs";
+import { chromeCacheKey, deckLayout, frameCount, hyperframesCommand, sha256 } from "./deck.mjs";
+import { deckHtml, GSAP_FILE } from "./chrome-deck.mjs";
const run = promisify(execFile);
+/** Chromium, for the review still. `CHROME` overrides it. */
+const CHROME = process.env.CHROME ?? "/usr/bin/chromium";
+const QRENCODE = process.env.QRENCODE_BIN ?? "qrencode";
+const MAGICK = process.env.MAGICK_BIN ?? "magick";
+
const esc = (s) =>
String(s ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
@@ -345,7 +352,7 @@ export function chartBandHtml(manifest, totals, schedule, opts = {}) {
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=${W}, height=${H}" />
- <script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
+ <script src="assets/gsap.min.js"></script>
<style>
/* The REAL files, copied in beside the composition. A bare local()
resolves in a desktop browser and FAILS in the render browser, which
@@ -523,7 +530,44 @@ export function chartBandHtml(manifest, totals, schedule, opts = {}) {
}
// ---------------------------------------------------------------------------
-// CLI
+// QR codes, for the deck.
+// ---------------------------------------------------------------------------
+
+/**
+ * One QR as a PNG of exactly `size` px.
+ *
+ * `-filter point`: any resampling filter blurs the module edges, and a blurred
+ * QR stops scanning. `-strip`: ImageMagick stamps a creation date into every
+ * PNG, and an asset whose bytes change each run is a render cache that never
+ * hits. Fully opaque, with its quiet zone -- the white border is part of the
+ * symbol, not decoration.
+ */
+export async function qrPng(url, render, outPath, size) {
+ const q = render.qr ?? {};
+ const raw = `${outPath}.raw.png`;
+ await run(QRENCODE, ["-o", raw, "-s", String(q.scale ?? 4), "-m", String(q.quiet ?? 3), "-l", q.ecc ?? "M", url]);
+ await run(MAGICK, [raw, "-filter", "point", "-resize", `${size}x${size}!`, "-strip", outPath]);
+ await rm(raw, { force: true });
+ return outPath;
+}
+
+/**
+ * One PNG per DISTINCT url into `assetsDir`, named in first-seen order.
+ * Returns `Map(url -> "assets/qrNN.png")`.
+ */
+export async function qrPngsFor(urls, render, size, assetsDir) {
+ const out = new Map();
+ for (const url of urls) {
+ if (!url || out.has(url)) continue;
+ const name = `qr${String(out.size).padStart(2, "0")}.png`;
+ await qrPng(url, render, path.join(assetsDir, name), size);
+ out.set(url, `assets/${name}`);
+ }
+ return out;
+}
+
+// ---------------------------------------------------------------------------
+// Regions.
// ---------------------------------------------------------------------------
const HF_JSON = JSON.stringify(
@@ -532,68 +576,273 @@ const HF_JSON = JSON.stringify(
2,
);
-export async function composeChrome({ manifestPath, outDir, region = "chart", duration = null, doRender = false, fps = null, variant = "sourced" }) {
- // The variant's view, and its own out directory. The band plots the ledger
- // the cut carries; handed the whole manifest it would draw marks for claims
- // this cut never makes and put the playhead schedule out by that many.
- const manifest = selectVariant(JSON.parse(await readFile(manifestPath, "utf8")), variant);
- const base = outDir ?? path.join(path.dirname(path.resolve(manifestPath)), "out", variant);
- const schedule = JSON.parse(await readFile(path.join(base, "schedule.json"), "utf8"));
- const totals = ledgerTotals(manifest.ledger);
-
- const projDir = path.join(base, "chrome", region);
- await mkdir(path.join(projDir, "assets"), { recursive: true });
- if (region !== "chart") throw new Error(`unknown chrome region: ${region}`);
-
- // The SAME faces the ffmpeg cards use, copied in beside the composition.
- // Chrome will not resolve a bare local() in the render browser, and the
- // failure is silent: it falls back and every metric in the band shifts.
+/**
+ * The faces a region draws in, copied in beside it under fixed names.
+ *
+ * Chrome will not resolve a bare local() in the render browser, and the failure
+ * is silent: it falls back and every metric shifts. The deck refuses a missing
+ * face outright -- the band, shipped before this rule, keeps its old leniency.
+ */
+async function copyFonts(render, assetsDir, names, { strict }) {
const fonts = {};
- for (const [slot, src] of [["regular", manifest.render.fontRegular], ["bold", manifest.render.fontBold]]) {
- if (!src) continue;
- const name = `${slot}${path.extname(src) || ".ttf"}`;
- await copyFile(src, path.join(projDir, "assets", name)).catch(() => {});
+ for (const [slot, src, base] of [
+ ["regular", render.fontRegular, names.regular],
+ ["bold", render.fontBold, names.bold],
+ ]) {
+ if (!src) {
+ if (strict) throw new Error(`the deck needs render.${slot === "regular" ? "fontRegular" : "fontBold"}`);
+ continue;
+ }
+ const name = `${base}${path.extname(src) || ".ttf"}`;
+ try {
+ await copyFile(src, path.join(assetsDir, name));
+ } catch (e) {
+ if (strict) throw new Error(`the deck's ${slot} face ${src} cannot be copied: ${e.message}`);
+ // The band as it shipped: the url stays, and the browser falls back.
+ }
fonts[slot] = `assets/${name}`;
}
+ return fonts;
+}
- const html = chartBandHtml(manifest, totals, schedule, {
- fonts,
- ...(duration ? { duration } : {}),
+/**
+ * Which HTML each chrome region is, with the assets it needs written into
+ * `projDir/assets`. The chart's branch is the band as it shipped; only where
+ * its GSAP comes from has changed.
+ */
+async function regionHtml(region, { manifest, base, projDir, assetsDir, schedule, duration, from }) {
+ if (region === "chart") {
+ const sched = schedule ?? JSON.parse(await readFile(path.join(base, "schedule.json"), "utf8"));
+ const fonts = await copyFonts(manifest.render, assetsDir, { regular: "regular", bold: "bold" }, { strict: false });
+ const totals = ledgerTotals(manifest.ledger);
+ return chartBandHtml(manifest, totals, sched, { fonts, ...(duration ? { duration } : {}) });
+ }
+ if (region === "deck") {
+ const render = manifest.render;
+ const fonts = await copyFonts(render, assetsDir, { regular: "DeckSans", bold: "DeckSansBold" }, { strict: true });
+ const lay = deckLayout(render);
+ const qrSrcs = {};
+ if (lay.qr) {
+ const byUrl = await qrPngsFor(schedule.segments.map((s) => s.qrUrl), render, lay.qr.size, assetsDir);
+ for (const s of schedule.segments) if (s.qrUrl) qrSrcs[s.id] = byUrl.get(s.qrUrl);
+ }
+ return deckHtml(schedule, render, { fonts, qrSrcs, from, duration });
+ }
+ throw new Error(`unknown chrome region: ${region}`);
+}
+
+/** The root's declared size and length, read back off the HTML rather than re-derived. */
+function compositionSize(html) {
+ const w = /data-width="(\d+)"/.exec(html);
+ const h = /data-height="(\d+)"/.exec(html);
+ const d = /data-composition-id="[^"]*"[^>]*data-duration="([\d.]+)"/.exec(html);
+ return { width: Number(w?.[1] ?? 1920), height: Number(h?.[1] ?? 1080), duration: Number(d?.[1] ?? 0) };
+}
+
+const fmtSeconds = (v) => String(Math.round(v * 1000) / 1000);
+
+async function framesOnDisk(dir) {
+ try {
+ return (await readdir(dir)).filter((f) => /^frame_\d+\.png$/.test(f)).length;
+ } catch {
+ return 0;
+ }
+}
+
+/** Run the renderer, its chatter to stderr (stdout may be a build's NDJSON). */
+function runRenderer(cmd, args) {
+ return new Promise((resolve, reject) => {
+ const child = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
+ let tail = "";
+ const keep = (b) => {
+ process.stderr.write(b);
+ tail = (tail + b.toString()).slice(-4000);
+ };
+ child.stdout.on("data", keep);
+ child.stderr.on("data", keep);
+ child.on("error", reject);
+ child.on("close", (code) =>
+ code === 0 ? resolve() : reject(new Error(`${cmd} ${args.join(" ")} exited ${code}\n${tail}`)),
+ );
+ });
+}
+
+/**
+ * Compose a chrome region and, optionally, render it or take a still of it.
+ *
+ * Deck (`region: "deck"`):
+ * - project `out/<variant>/chrome/deck/` (`deck-preview/` when `preview`, which
+ * never renders and so never disturbs a build's project or cache);
+ * - frames `chrome/deck-frames/frame_%06d.png` and `deck-frames/.key`;
+ * - a window (`from` > 0, or a `duration` shorter than the cut) is its own
+ * project and frames, `deck-from<s>[-frames]`, so a preview slice never
+ * overwrites the whole cut's sequence;
+ * - the render is SKIPPED when `.key` equals this compose's `chromeCacheKey`
+ * and the frame count on disk is `frameCount(duration, fps)`.
+ *
+ * `schedule` (an object) overrides reading `out/<variant>/schedule.json`.
+ *
+ * @returns {Promise<{ projDir: string, frames: string|null, still: string|null,
+ * cached: boolean, key: string|null, frameCount: number|null }>}
+ */
+export async function composeChrome({
+ manifestPath, outDir = null, variant = "sourced", region = "chart",
+ schedule = null, preview = false, doRender = false,
+ fps = null, workers = null, quality = "high", format = "png-sequence",
+ still = null, png = null, from = 0, duration = null,
+}) {
+ // The variant's view, and its own out directory. Handed the whole manifest
+ // the band would draw claims this cut never makes, and the deck would name
+ // clips it does not play.
+ const manifest = selectVariant(JSON.parse(await readFile(manifestPath, "utf8")), variant);
+ // Absolute: the still is a file:// URL, and a relative one is no page at all.
+ const base = path.resolve(outDir ?? path.join(path.dirname(path.resolve(manifestPath)), "out", variant));
+ from = Number(from ?? 0);
+
+ let sched = schedule;
+ if (region === "deck" && !sched) {
+ const p = path.join(base, "schedule.json");
+ try {
+ sched = JSON.parse(await readFile(p, "utf8"));
+ } catch (e) {
+ throw new Error(`the deck needs ${p} (the build writes it) or a schedule passed in: ${e.message}`);
+ }
+ if (sched.kind !== "deck") throw new Error(`${p} is not a deck schedule (kind ${sched.kind ?? "missing"})`);
+ }
+ const total = region === "deck" ? sched.total : null;
+ const windowed =
+ region === "deck" && (from > 0 || (duration != null && Math.abs(Number(duration) - total) > 1e-6));
+ const suffix = windowed ? `-from${fmtSeconds(from)}` : "";
+
+ const projName = region === "deck" && preview ? "deck-preview" : `${region}${suffix}`;
+ const projDir = path.join(base, "chrome", projName);
+ const assetsDir = path.join(projDir, "assets");
+ // The deck's assets are rebuilt every time: a QR from a clip that has since
+ // left the cut must not sit in the directory the cache key hashes.
+ if (region === "deck") await rm(assetsDir, { recursive: true, force: true });
+ await mkdir(assetsDir, { recursive: true });
+ await copyFile(GSAP_FILE, path.join(assetsDir, "gsap.min.js"));
+
+ const html = await regionHtml(region, {
+ manifest, base, projDir, assetsDir, schedule: sched,
+ duration: duration != null ? Number(duration) : null, from,
});
await writeFile(path.join(projDir, "index.html"), html, "utf8");
await writeFile(path.join(projDir, "hyperframes.json"), HF_JSON + "\n", "utf8");
- if (!doRender) return { projDir, frames: null };
+ const size = compositionSize(html);
+ const rate = Number(fps ?? sched?.fps ?? manifest.render.fps ?? 30);
+ const hf = hyperframesCommand(process.env);
+ let key = null;
+ let frames = null;
+ if (region === "deck") {
+ frames = frameCount(size.duration, rate);
+ const assets = [];
+ for (const name of (await readdir(assetsDir)).sort()) {
+ assets.push([name, sha256(await readFile(path.join(assetsDir, name)))]);
+ }
+ key = chromeCacheKey({ html, assets, fps: rate, frames, version: hf.version });
+ }
+ const result = { projDir, frames: null, still: null, cached: false, key, frameCount: frames };
+
+ // The review still: seek and screenshot, no HyperFrames, ~2 s. It is the SAME
+ // seek the renderer performs for every frame, which is why a still that is
+ // right is evidence about the frames and not just about the markup. The
+ // background is transparent, as the frames' is.
+ if (still != null) {
+ const out = path.resolve(png ?? path.join(projDir, `still-${fmtSeconds(Number(still))}.png`));
+ await mkdir(path.dirname(out), { recursive: true });
+ await run(CHROME, [
+ "--headless", "--disable-gpu", "--no-sandbox", "--hide-scrollbars",
+ "--default-background-color=00000000",
+ `--window-size=${size.width},${size.height}`,
+ "--virtual-time-budget=6000",
+ `--screenshot=${out}`,
+ `file://${path.join(projDir, "index.html")}?still=${Number(still)}`,
+ ], { maxBuffer: 1 << 26 });
+ return { ...result, still: out };
+ }
+
+ if (!doRender || (region === "deck" && preview)) return result;
- const frames = path.join(base, "chrome", `${region}-frames`);
- await run("npx", [
- "--yes", "hyperframes@latest", "render",
- "--format", "png-sequence", "--quality", "high",
- "--fps", String(fps ?? manifest.render.fps),
- "--output", frames, projDir,
- ], { maxBuffer: 1 << 26 });
- return { projDir, frames };
+ // A sequence is a directory; every other format is a file.
+ const sequence = format === "png-sequence";
+ const target = sequence
+ ? path.join(base, "chrome", `${region}${suffix}-frames`)
+ : path.join(base, "chrome", `${region}${suffix}.${format}`);
+ const keyFile = path.join(target, ".key");
+
+ if (region === "deck" && sequence) {
+ const onDisk = await readFile(keyFile, "utf8").then((s) => s.trim(), () => null);
+ if (onDisk === key && (await framesOnDisk(target)) === frames) {
+ return { ...result, frames: target, cached: true };
+ }
+ // Stale frames past the new count would be overlaid as the tail of the cut.
+ await rm(target, { recursive: true, force: true });
+ }
+
+ const args = [
+ ...hf.args, "render",
+ "--format", format, "--quality", quality,
+ "--fps", String(rate),
+ ...(workers != null ? ["-w", String(workers)] : []),
+ // The deck is flat colour and text: software GL is deterministic and the
+ // GPU probe is a second per worker for nothing.
+ ...(region === "deck" ? ["--no-browser-gpu"] : []),
+ "--output", target, projDir,
+ ];
+ await runRenderer(hf.cmd, args);
+
+ if (region === "deck" && sequence) {
+ const got = await framesOnDisk(target);
+ if (got !== frames) {
+ throw new Error(`the deck render wrote ${got} frames to ${target}, expected ${frames} (${size.duration}s at ${rate} fps)`);
+ }
+ await writeFile(keyFile, key + "\n", "utf8");
+ }
+ return { ...result, frames: target };
}
if (import.meta.url === `file://${process.argv[1]}`) {
const argv = process.argv.slice(2);
const flag = (n) => { const i = argv.indexOf(n); return i < 0 ? null : argv[i + 1]; };
- const VALUED = new Set(["--out", "--region", "--duration", "--variant"]);
+ const VALUED = new Set([
+ "--out", "--region", "--duration", "--variant", "--from",
+ "--still", "--png", "--workers", "--quality", "--format", "--fps",
+ ]);
const manifestPath = argv.find((a, i) => !a.startsWith("--") && !VALUED.has(argv[i - 1]));
if (!manifestPath) {
console.error(
- "usage: compose-chrome.mjs <manifest.json> [--region chart] [--variant sourced|full]\n" +
- " [--duration <s>] [--out <dir>] [--render]",
+ "usage: compose-chrome.mjs <manifest.json> [--region chart|deck] [--variant sourced|full]\n" +
+ " [--from <s>] [--duration <s>] [--out <dir>] [--preview]\n" +
+ " [--still <s> --png <path>]\n" +
+ " [--render] [--workers 4] [--quality high] [--format png-sequence] [--fps 30]",
);
process.exit(2);
}
+ const num = (n) => (flag(n) == null ? null : Number(flag(n)));
+ const region = flag("--region") ?? "chart";
+ const started = Date.now();
const r = await composeChrome({
manifestPath,
outDir: flag("--out"),
- region: flag("--region") ?? "chart",
+ region,
variant: flag("--variant") ?? "sourced",
- duration: flag("--duration") ? Number(flag("--duration")) : null,
+ preview: argv.includes("--preview"),
+ duration: num("--duration"),
+ from: num("--from") ?? 0,
+ still: num("--still"),
+ png: flag("--png"),
+ // The deck renders four-wide by default; the band keeps the renderer's own default.
+ workers: num("--workers") ?? (region === "deck" ? 4 : null),
+ quality: flag("--quality") ?? "high",
+ format: flag("--format") ?? "png-sequence",
+ fps: num("--fps"),
doRender: argv.includes("--render"),
});
- console.log(r.frames ? `frames -> ${r.frames}` : `project -> ${r.projDir}`);
+ const secs = ((Date.now() - started) / 1000).toFixed(1);
+ if (r.still) console.log(`still -> ${r.still} (${secs}s)`);
+ else if (r.frames) console.log(`frames -> ${r.frames}${r.cached ? " (cached)" : ""} (${r.frameCount ?? "?"} frames, ${secs}s)`);
+ else console.log(`project -> ${r.projDir}`);
+ if (r.key) console.log(`key ${r.key}`);
}
diff --git a/umtool/report-to-video/deck.mjs b/umtool/report-to-video/deck.mjs
@@ -260,16 +260,42 @@ export function deckLayout(render) {
const { W, deck: rect } = deckGeometry(render);
const d = resolveDeck(render);
const padX = 48;
+ const trackY = 16;
+ // The body is everything under the pip track: the text block and the QR are
+ // both centred in it, so the title's optical centre and the code's agree
+ // whatever the deck's height. 14 px clears the active pip's halo.
+ const bodyTop = trackY + 14;
+ const bodyH = rect.height - bodyTop - 8;
const qr = d.qr.show
- ? { x: W - padX - d.qr.size, y: Math.round((rect.height - d.qr.size) / 2) + 6, size: d.qr.size }
+ ? { x: W - padX - d.qr.size, y: Math.round(bodyTop + (bodyH - d.qr.size) / 2), size: d.qr.size }
: null;
const textX = padX;
- const textRight = qr ? qr.x - 40 : W - padX;
+ const textRight = qr ? qr.x - 48 : W - padX;
+ // Line boxes, not font sizes: the title's box holds its descenders inside the
+ // wipe's overflow clip, and the rule sits in the gap between the two lines.
+ // The subtitle scales with the title (26 px at the default 54).
+ const titleSize = d.title.size;
+ const subtitleSize = Math.round(Math.max(18, Math.min(34, titleSize * 0.48)));
+ const titleBox = Math.round(titleSize * 1.2);
+ const subtitleBox = Math.round(subtitleSize * 1.3);
+ const gap = Math.round(titleSize * 0.3);
+ const block = titleBox + gap + subtitleBox;
+ const titleY = Math.round(bodyTop + Math.max(0, (bodyH - block) / 2));
return {
width: W,
height: rect.height,
- pipTrack: { x0: padX, x1: W - padX, y: 16 },
- text: { x: textX, width: textRight - textX, titleSize: d.title.size, subtitleSize: 26 },
+ pipTrack: { x0: padX, x1: W - padX, y: trackY },
+ text: {
+ x: textX,
+ width: textRight - textX,
+ titleSize,
+ subtitleSize,
+ titleY,
+ titleBox,
+ ruleY: titleY + titleBox + Math.round((gap - 4) / 2),
+ subtitleY: titleY + titleBox + gap,
+ subtitleBox,
+ },
qr,
};
}