@slightlyoff/wpt-embed
Version:
Scripts and Web Components for capturing and displaying WPT traces in a blog
498 lines (430 loc) • 23.5 kB
JavaScript
let P=`
wpt-embed.js, 0.2.18
Copyright 2024-2025
Alex Russell -- infrequently.org
Licensed under the MIT license.
`,j=function(n,t){if(n?.length>1||t?.length>1)throw"`css` tag called with values";return n[0]};CSS.registerProperty({name:"--wpt-scroll-pct",syntax:"<percentage>",inherits:!0,initialValue:"0%"});let b=(n,t)=>{let e=typeof n;if(e==="boolean")return n;if(e==="string"){let i=n.toLowerCase();if(!n.length||i=="true"||i==t)return!0}return!1},A=(n,t,e)=>{let i=n.toLowerCase();return i===t||i==="true"?e:n.split(/\s+/)},d=(n,t)=>n.querySelector(t),M=n=>t=>d(n,t),R=new Map,B=(n,t)=>{let e=R.get(t);if(!e){try{e={type:"CSS",value:new CSSStyleSheet},e.value.replaceSync(t)}catch{e={type:"sheet",value:t}}R.set(t,e)}switch(e.type){case"sheet":let i=n.createElement("style");i.textContent=e.value,n.appendChild(i);break;case"CSS":n.adoptedStyleSheets=[...n.adoptedStyleSheets,e.value];break}},q=(()=>{let n=new Map;return t=>{let e=n.get(t);return e||(e=t.replace(/(-)+([a-z]?)/g,(i,a,l,s)=>{let r=i[i.length-1];return s?r==="-"?"":r.toUpperCase():r}),n.set(t,e),e)}})(),y=n=>{let t=document.createElement("template");return t.innerHTML=n,t.content},D=(n=0)=>{if(!n||n<1)return Math.round(n)+" kB";let t=Math.round(n)+"",e=[],i=t.length;for(;i>3;)e.unshift(t.slice(-3)),t=t.slice(0,i-3),i=t.length;return e.unshift(t),e.join(",")+" kB"};class m extends HTMLElement{static observedAttributes=["aspect-ratio","size","interval","filmstrip","waterfall","connections","breakdown","crux","video","gif","end","order"];static styles=j`
/* A wee reset */
h1, h2, h3, h4, p, figure, blockquote, dl, dd {
margin-block-end: 0;
margin-block-start: 0;
}
h1, h2, h3, h4 {
text-wrap: balance;
}
* {
box-sizing: border-box;
}
:host {
timeline-scope: --wpt-embed-scroller;
--wpt-image-width: var(--image-width, 100px);
--wpt-progress-line-color: transparent;
--wpt-progress-line-width: 0px;
((animation-timeline: scroll()) and (animation-range: 0% 100%)) {
--wpt-progress-line-color: red;
--wpt-progress-line-width: 2px;
}
--wpt-section-padding: 1rem 0;
/* TODO:
--wpt-no-change-border-color: transparent;
--wpt-visual-change-border-color: yellow;
--wpt-lcp-border-color: red;
*/
}
:host([debug]) {
* {
outline: 1px solid blue;
}
outline: 2px dotted red;
}
/**************
*
* All sections
*
**/
:host {
display: flex;
flex-direction: column;
}
:host > div {
width: 100%;
margin: var(--wpt-section-padding);
/* center */
display: flex;
justify-content: center;
gap: 1rem;
}
caption,
figcaption {
text-align: center;
margin: 0.25em 0;
}
table {
border-collapse: collapse;
}
figure {
margin: 0;
padding: 0;
}
/**************
*
* Filmstrip section
*
***/
#filmstrip {
overflow-x: auto;
display: block;
position: relative;
scrollbar-gutter: stable;
scroll-timeline-axis: x;
scroll-timeline-name: --wpt-embed-scroller;
}
/* TODO: elide when there's no filmstrip */
:host([waterfall]),
:host([connections]) {
#filmstrip {
border-left: var(--wpt-progress-line-width) solid var(--wpt-progress-line-color);
}
#filmstrip.hidden { display: none; }
}
#main-table {
width: 100%;
top: 0px;
left: 0px;
/* TODO: not working in FF */
margin-right: calc(100%);
}
.filmstrip-row {
width: 100%;
& img {
margin-inline: 2px;
outline: 1px solid black;
content-visibility: auto;
}
& .pct {
text-align: center;
}
& .visualChange > img {
outline: var(
--wpt-visual-change-outline,
2px solid #ffc233
);
}
& .lcp > img {
outline: var(--wpt-lcp-outline, 2px solid #ff0000);
}
& .layoutShift.visualChange > img {
outline: var(
--wpt-layout-shift-visual-change-outline,
2px dotted #ffc233
);
}
& .layoutShift.lcp > img {
outline: var(
--wpt-layout-shift-lcp-outline,
2px dotted #ff0000
);
}
}
.meta {
text-align: left;
}
.labels {
position: sticky;
display: inline-block;
top: 0px;
left: 0px;
padding: 0.5rem;
}
#timing td {
text-align: center;
}
:host([size="small"]) {
--wpt-image-width: 50px;
}
:host([size="medium"]) {
--wpt-image-width: 100px;
}
:host([size="large"]) {
--wpt-image-width: 200px;
}
.filmstrip-row img {
width: var(--wpt-image-width, 100px);
contain-intrinsic-width: var(--wpt-image-width, 100px);
aspect-ratio: var(--wpt-aspect-ratio);
}
.filmstrip-meta {
padding: 1em;
}
:host > div.hidden {
display: none;
margin: 0;
padding: 0;
}
scrollTransform {
from {
--wpt-scroll-pct: 0%;
}
to {
--wpt-scroll-pct: 100%;
}
}
/**************
*
* Breakdown table and charts section
*
***/
#breakdown {
& > table {
min-width: 20rem;
width: 100%;
max-width: 35rem;
border-collapse: collapse;
border: 1px solid #dddddd;
margin: 0;
& > caption {
caption-side: bottom;
}
& td, th {
padding: 0.5em 0.35em;
}
& > thead {
background-color: gainsboro;
text-align: center;
color: var(--wpt-breakdown-even-color, inherit);
}
& > tbody {
& > tr {
border-bottom: 1px solid #dddddd;
}
& > tr:nth-of-type(even) {
background-color: #f3f3f3;
color: var(--wpt-breakdown-even-color, inherit);
}
& th {
text-align: left;
}
& td {
text-align: right;
}
}
}
}
/**************
*
* CrUX data
*
***/
#crux {
flex-direction: column;
font-size: 0.8rem;
& > .crux {
width: 100%;
& > .metric {
width: 100%;
margin: 2em 0;
& .title {
opacity: 0.7;
}
& .value{
font-weight: 900;
font-size: 2em;
line-height: 1;
margin: 0.2em 0;
}
& .pct{
margin: 0.2em 0;
}
--good: var(--wpt-crux-good, rgb(12, 206, 107));
--fair: var(--wpt-crux-fair, rgb(255, 164, 0));
--poor: var(--wpt-crux-poor, rgb(255, 78, 66));
/* TODO: themes & contrast */
& .good {
background-color: var(--good);
color: white;
}
& .fair {
background-color: var(--fair);
}
& .poor {
background-color: var(--poor);
color: white;
}
& .value {
background-color: inherit;
&.good { color: var(--good); }
&.fair { color: var(--fair); }
&.poor { color: var(--poor); }
}
& > ul {
list-style: none;
padding: 0;
display: flex;
width: 100%;
& > li {
line-height: 2.2;
text-indent: 0.8em;
}
}
& > .thresholds {
display: flex;
& > div {
padding: 0.2em 0.8em;
& > .key {
display: inline-block;
width: 1.5em;
}
}
}
}
}
}
/**************
*
* Waterfall and Connections sections
*
***/
#waterfall,
#connections {
align-items: inherit;
overflow-x: auto;
width: 100%;
--wpt-start-stop: 0.24;
& picture {
width: 100%;
max-width: 1012px;
display: inline-block;
position: relative;
contain: content;
margin: 0;
padding: 0;
border: 0;
--es-tl: var(--wpt-test-length);
--es-lt: var(--wpt-longest-test, 1);
--wpt-end-stop: calc(var(--es-tl) / var(--es-lt) * 100%);
& > img {
width: 100%;
max-width: 1012px;
}
}
& picture::after {
content: "";
display: block;
z-index: 1;
position: absolute;
display: block;
width: var(--wpt-progress-line-width);
left: var(--wpt-scroll-pct);
top: var(--wpt-line-pct-top, 37px);
bottom: var(--wpt-line-pct-bottom, 170px);
background-color: var(--wpt-progress-line-color);
opacity: 0.8;
will-change: left;
animation: scrollTransform linear(0, var(--wpt-start-stop) 0%, 1 var(--wpt-end-stop) 90%);
animation-timeline: --wpt-embed-scroller;
}
}
/**************
*
* Misc
*
***/
#gif,
#video,
#breakdown {
flex-wrap: wrap;
}
`;static template=y(`
<div id="filmstrip" part="filmstrip">
<table id="main-table">
<tbody>
<tr id="timing"></tr>
</tbody>
</table>
</div>
<div id="waterfall" part="waterfall" class="hidden"></div>
<div id="connections" part="connections" class="hidden"></div>
<div id="breakdown" part="breakdown" class="hidden"></div>
<div id="crux" part="crux" class="hidden"></div>
<div id="gif" part="gif" class="hidden"></div>
<div id="video" part="video" class="hidden"></div>
`);static tagName="wpt-embed";get tagName(){return this.constructor.tagName}constructor(){super();let t=this.attachShadow({mode:"open"})}attributeChangedCallback(t,e,i){if(m.observedAttributes.includes(t)&&e!==i){let a=q(t);this[a]=i}}#t=100;#o="100";#e=1;set interval(t){let e=this.#t;typeof t=="number"&&(t=t.toString());let i;switch(t){case"16":case"16ms":case"60fps":this.#t=16,this.#e=3;break;case"1000":case"1000ms":case"1s":this.#t=1e3,this.#e=0;break;case"5000":case"5000ms":case"5s":this.#t=5e3,this.#e=0;break;case"500":case"500ms":case"0.5s":this.#t=500,this.#e=1;break;case"100":case"100ms":case"0.1s":default:this.#t=100,this.#e=1;break}this.#t!==e&&this.updateTests()}get interval(){return this.#o}getTimingFor(t=0){let e=document.createElement("td"),i=document.createElement("span"),a=Math.trunc(t/1e3),l=Math.abs(a?1e3*a-t:t),s=a+"";return this.#e&&(s+="."+(l+"").padEnd("0",this.#e).substring(0,this.#e)),i.innerText=s,e.appendChild(i),e}#a=!0;set filmstrip(t){this.#a=b(t,"filmstrip")}get filmstrip(){return this.#a}#f=!1;set waterfall(t){this.#f=b(t,"waterfall")}get waterfall(){return this.#f}#h=!1;set connections(t){this.#h=b(t,"connections")}get connections(){return this.#h}#c=!1;set breakdown(t){this.#c=b(t,"breakdown")}get breakdown(){return this.#c}#l=[];set crux(t){this.#l=A(t,"crux",["inp","lcp","cls"])}get crux(){return this.#l}#u=!1;set video(t){this.#u=b(t,"video")}get video(){return this.#u}#d=!1;set gif(t){this.#d=b(t,"gif")}get gif(){return this.#d}#p=[];set order(t){this.#p=A(t,"order",[])}get order(){return this.#p}#n="full";#m={full:"fullyLoaded",visual:"visualComplete",onload:"loadEventEnd",lcp:"LargestContentfulPaint",fcp:"FirstContentfulPaint"};set end(t){this.#m[t]&&(this.#n=t==="end"?"full":t)}get end(){return this.#n}get longEnd(){return this.#m[this.#n]}connectedCallback(){this.wireElements()}get#i(){return Array.from(this.children).filter(t=>t.tagName==="wpt-test")}#s(t,e){typeof e=="string"&&(e=this.byId(e));let i=!0;return typeof t=="string"||Array.isArray(t)?i=!t.length:i=!t,e.classList[i?"add":"remove"]("hidden"),e}updateTests(){if(!this.#r)return;let t=this.#i;for(let s of t)if(!s.data)return;let e=t.map(s=>s.duration),i=Math.max(...e)+this.#t,a=[];for(let s=0;s<=i;s+=this.#t)a.push(this.getTimingFor(s));this.filmstrip&&this.byId("timing").replaceChildren(...a),this.#p.length&&this.shadowRoot.prepend(...this.#p.map(s=>this.byId(s)));var l=0;for(let s of this.#i){let r=s.duration||0;r>l&&(l=r)}this.style.setProperty("--wpt-longest-test",l),this.#i.forEach(s=>{this.filmstrip?s.renderFilmstripInto(this.#t,a.length,this.byId("main-table").tBodies[0]):(this.byId("main-table").classList.add("hidden"),this.byId("filmstrip").classList.add("hidden"));let r=this.byId("waterfall");this.#s(this.waterfall,r),this.waterfall&&s.renderWaterfallInto(r),r=this.byId("connections"),this.#s(this.connections,r),this.connections&&s.renderConnectionsInto(r),r=this.byId("breakdown"),this.#s(this.breakdown,r),this.breakdown&&s.renderBreakdownInto(r),r=this.byId("crux"),this.#s(this.crux,r),this.crux.length&&s.renderCruxInto(r,this.crux),r=this.byId("video"),this.#s(this.video,r),this.video&&s.renderVideoInto(r),r=this.byId("gif"),this.#s(this.gif,r),this.gif&&s.renderGifInto(r)})}#r=!1;byId(t){return this.shadowRoot.getElementById(t)}wireElements(){if(this.#r)return;this.#r=!0;let t=this.shadowRoot,e=(i,a,l)=>{let s=typeof l=="string"?this[l].bind(this):l;this.byId(i).addEventListener(a,s)};B(t,m.styles),t.appendChild(m.template.cloneNode(!0)),this.addEventListener("test-modified",this.updateTests)}}customElements.define(m.tagName,m),customElements.define("wpt-filmstrip",class extends m{});class f extends HTMLElement{static observedAttributes=["label","test","run","view","timeline","timeline-video","aspect-ratio","avif"];static tagName="wpt-test";get tagName(){return this.constructor.tagName}constructor(){super()}#t=!1;#o(){this.#t=!0,this.#e&&(this.dispatchEvent(new CustomEvent("test-modified",{bubbles:!0})),this.#t=!1)}#e=!1;connectedCallback(){this.parentNode&&this.parentNode?.tagName===m.tagName&&(this.#e=!0,this.#o(),this.#d())}data=null;#a="";set timeline(t){this.updateTimeline(t)}get timeline(){return this.#a}#f="";set label(t){this.#f=t,this.#o()}get label(){return this.#f}#h="";set test(t){t&&!t.endsWith("/")&&(t+="/"),this.#h=t,t&&this.#d()}#c="1";set run(t){this.#c=parseInt(t)+"",t&&this.#d()}#l="first";set view(t){t&&["first","repeat"].includes(t)&&(this.#l=t,this.#d())}get duration(){return this?.data?.[this?.parentNode?.longEnd]||0}#u=!1;set avif(t){this.#u=b(t,"avif")}get avif(){return this.#u}#d(){if(!this.#e)return;if(this.#h&&this.#c&&this.#l){let e=`${this.#h}runs/${this.#c}/${this.#l}View/timeline.json`;this.updateTimeline(e);return}let t=this.querySelector(':scope > script[type="text/json"]')||this.querySelector(':scope > script[type="application/json"]');if(t&&t.hasAttribute("dir")){let e=JSON.parse(t.textContent),a=`${t.getAttribute("dir")}${e.testName||e.id}/runs/${e.run}/${e.view}/timeline.json`;this.data=e,this.avif=this.data.optimizedImages,this.#a=a,this.#o()}}async updateTimeline(t){if(!(!t||t===this.#a)){this.#a=t;try{let e=await fetch(t);this.data=await e.json(),this.avif=this.data.optimizedImages,this.#o()}catch(e){console.error(e),this.data=null}}}#p=!1;attributeChangedCallback(t,e,i){if(this.#p=!0,f.observedAttributes.includes(t)&&e!==i){let a=q(t);this[a]=i,this.#o(a,i)}}static rowTemplate=y(`
<!-- start -->
<tr class="meta-row">
<td class="meta">
<div class="labels">
<a class="test-link" target="_new" part="test-link">
<span class="label" part="label"></span>
</a>
</div>
</td>
</tr>
<tr class="filmstrip-row">
</tr>
<!-- end -->
`);#n=null;#m=null;#i=null;extract(){if(this.#n){if(this.#i)return this.#i;let t=new Range;return t.setStartBefore(this.#n),t.setEndAfter(this.#m),this.#i=t.extractContents(),t.detach(),[this.#g,this.#w,this.#b,this.#C,this.#k,this.#$].forEach(e=>{e&&e.remove()}),this.#i}}disconnectedCallback(){this.extract()}renderFilmstripInto(t=100,e,i){if(!this.data)return;let a;if(this.#n&&(a=this.extract(),!this.#t)){i.append(a),this.#i=null;return}a=f.rowTemplate.cloneNode(!0);let l=Array.from(a.childNodes).filter(h=>h.nodeType===8);this.#n=l.shift(),this.#m=l.shift();let s=M(a);s(".test-link").setAttribute("href",this.data.summary),s(".meta").setAttribute("colspan",e),s(".label").innerText=this.label||this.data.url;let r=this.getFrames(t,e),o=s(".filmstrip-row");o.replaceChildren(...r),o.style.setProperty("--wpt-aspect-ratio",this.data.filmstripImageAspectRatio),i.append(a),this.#i=null}#s=null;#r(t){return this.#s||(this.#s=new URL(this.#a,window.location)),this.avif&&(t.endsWith(".png")||t.endsWith(".jpg"))&&(t=t.slice(0,-4)+".avif"),new URL(t,this.#s)}static figureTemplate=y(`
<figure>
<a target="_blank">
<picture>
<img loading="lazy" decoding="async">
</picture>
</a>
<figcaption></figcaption>
</figure>
`);#y(t,e="",i="",a="",l="",s="",r=!1){t.appendChild(f.figureTemplate.cloneNode(!0));let o=t.lastElementChild,h=M(o);i&&o.setAttribute("part",i);let c=h("img");return e&&(c.src=e),c.alt=a,l&&(h("a").href=l),s&&(h("figcaption").textContent=s),r&&(c.addEventListener("load",I=>{let u=c.naturalWidth,p=c.naturalHeight;o.style.setProperty("--wpt-line-pct-top",`${(37/p).toFixed(5)*100}%`),o.style.setProperty("--wpt-start-stop",`${(250/u).toFixed(5)}`),o.style.setProperty("--wpt-line-pct-bottom",`${(170/p).toFixed(5)*100}%`)}),o.style.setProperty("--wpt-test-length",this?.data?.fullyLoaded)),o}#v="";get location(){if(!this.#v&&this.data){let t=new URL(this.data.testUrl);this.#v=`${t.host}${t.pathname=="/"?"":t.pathname}`}return this.#v}#x="";get summary(){if(this.#x&&this.data){let t=this.data.from.replaceAll("<b>","").replaceAll("</b>","").replace(" - ",` in ${this.data.mobile?"mobile":"desktop"} `).replace(" - "," on an ").replace(" - "," using an emulated ")+" connection";this.#x=`${this.location} tested from ${t}; ${this.#l} view`}return this.#x}#g=null;renderWaterfallInto(t){if(this.data){if(this?.parentNode?.end!="full"){console.error("'end' must be 'full' to display waterfall");return}this.#g?t.appendChild(this.#g):this.#g=this.#y(t,this.#r(this?.data?.waterfall),"waterfall-figure",`Resource waterfall chart for ${this.location}.`,this.data.summary,this.summary,!0)}}#w=null;renderConnectionsInto(t){if(this?.parentNode?.end!="full"){console.error("'end' must be 'full' to display connections");return}if(this.#w){t.appendChild(this.#w);return}this.#w=this.#y(t,this.#r(this.data.connectionView),"container-figure",`Network connections chart for ${this.location}.`,this.data.summary,`Connections and utilization. ${this.data.view=="firstView"?"First":"Repeat"} view, ${(this.data.bwDown/1e3).toFixed(1)}/${(this.data.bwUp/1e3).toFixed(1)}Mbps, ${this.data.latency}ms RTT.`,!0)}static breakdownTemplate=y(`
<table part="breakdown-table">
<caption></caption>
<thead>
<tr>
<th>Type</th>
<th>Wire Size</th>
<th>Decoded</th>
<th>Requests</th>
</tr>
</thead>
<tbody>
<tr>
<th></th>
<td></td>
<td></td>
<td></td>
</tr>
</tbody>
</table>
`);#b=null;renderBreakdownInto(t){if(this.#b){t.appendChild(this.#b);return}if(!this?.data?.breakdown)return;delete this.data.breakdown.flash,t.appendChild(f.breakdownTemplate.cloneNode(!0));let e=this.#b=t.lastElementChild,i=d(e,"caption");i.textContent=`${this.location}, ${this.#l} view`;let a=d(e,"tbody > tr");a.remove();let l={bytes:0,bytesUncompressed:0,requests:0};for(let[s,r]of Object.entries(this.data.breakdown))r.bytes!==0&&(l.bytes+=r.bytes,l.bytesUncompressed+=r.bytesUncompressed,l.requests+=r.requests);this.data.breakdown.Total=l;for(let[s,r]of Object.entries(this.data.breakdown)){let o=a.cloneNode(!0);o.firstElementChild.textContent=s,o.children[1].textContent=D(r.bytes/1e3),o.children[2].textContent=D(r.bytesUncompressed/1e3),o.children[3].textContent=r.requests,e.tBodies[0].appendChild(o)}}static cruxTemplate=y(`
<div class="crux">
<h3 class="details"></h3>
<div class="metric">
<h4 class="title"></h3>
<p class="value"></p>
<p class="pct">At 75th percentile of visits</p>
<ul></ul>
<div class="thresholds">
<div>
<span class="key good"> </span>
Good
(< <span class="goodValue"></span>)
</div>
<div>
<span class="key fair"> </span>
Fair
</div>
<div>
<span class="key poor"> </span>
Poor
(≥ <span class="poorValue"></span>)
</div>
</div>
</div>
</div>
`);#I={fcp:{name:"first_contentful_paint",title:"First Contentful Paint"},lcp:{name:"largest_contentful_paint",title:"Largest Contentful Paint"},inp:{name:"interaction_to_next_paint",title:"Interaction to Next Paint"},cls:{name:"cumulative_layout_shift",title:"Cumulative Layout Shift",unitless:!0},ttfb:{name:"experimental_time_to_first_byte",title:"Time to First byte"}};#L=["good","fair","poor"];#C=null;renderCruxInto(t,e=["inp","lcp","cls"]){if(this.#C||!this?.data?.crux)return;t.appendChild(f.cruxTemplate.cloneNode(!0));let i=this.#C=t.lastElementChild,a=d(i,".metric");a.remove();let l=this.data.crux;for(let N of e){let C=this.#I[N];if(!C)continue;let v=l.metrics[C.name],w=a.cloneNode(!0);d(w,".title").textContent=`${C.title} (${N.toUpperCase()})`;let k=v.percentiles.p75,T=k,L=v.histogram[0].end,E=v.histogram[2].start;C.unitless||(T=`${k/1e3}s`,L=`${L/1e3}s`,E=`${E/1e3}s`);let $="good";k>v.histogram[1].end?$="poor":k>v.histogram[0].end&&($="fair"),d(w,".value").textContent=`${T} (${$})`,d(w,".value").classList.add($);let z=d(w,"ul");this.#L.forEach((U,O)=>{let F=parseInt(v.histogram[O].density*100)+"%",S=document.createElement("li");S.textContent=F,S.style.flexBasis=F,S.classList.add(U),z.appendChild(S)}),d(w,".goodValue").textContent=L,d(w,".poorValue").textContent=E,i.appendChild(w)}let s=l.collectionPeriod.firstDate,r=l.collectionPeriod.lastDate,o={year:"numeric",month:"long",day:"numeric"},h=(s.month+"").padStart(2,"0"),c=(s.day+"").padStart(2,"0"),I=new Date(`${s.year}-${h}-${c}`).toLocaleDateString("en",o),u=(r.month+"").padStart(2,"0"),p=(r.day+"").padStart(2,"0"),g=new Date(`${r.year}-${u}-${p}`).toLocaleDateString("en",o),x=l.key.formFactor=="PHONE",_=new URL(this.data.crux.key.url).host;d(i,".details").textContent=`Web Vitals data for ${_}. Collected from Chrome ${x?"mobile":"desktop"} users, ${I} \u2013 ${g}.`}#S(t){if(!this.data)return;let e=this.data.gifImageData,i=t.querySelector("img,video");[t,i].forEach(a=>{a.style.width="100%",a.style.maxWidth=`${e.width}px`,a.aspectRatio=this.data.gifImageAspectRatio})}static videoTemplate=y(`
<figure part="video-figure">
<video controls preload="none">
</video>
<figcaption></figcaption>
</figure>
`);#k=null;renderVideoInto(t){if(this.#k||!this.data)return;t.appendChild(f.videoTemplate.cloneNode(!0));let e=this.#k=t.lastElementChild,i=e.querySelector("video");i.poster=this.#r("poster.png"),i.src=this.#r("timeline.mp4"),this.#S(e)}#$=null;renderGifInto(t){if(this.#$||!this.data)return;let e=this.#$=this.#y(t,this.#r("timeline.gif"),"gif-figure",`Loading ${this.location} took ${this.duration/1e3} seconds.`,this.data.summary);this.#S(e)}static imgTemplate=y(`
<td>
<img loading="lazy" decoding="async">
<div class="pct"></div>
</td>`);getFrames(t,e){let i=Array.from(this.data.filmstripFrames),a=[],l=0,s=Array.from(this.data?.lcps||[]),r=Array.from(this.data?.layoutShifts||[]),o=(p=0)=>{if(i[0].time<p)for(;i[0].time<p&&i[1]&&i[1].time<=p;)i.shift();return i[0]},h=s.shift(),c=r.shift(),I=null,u=null;for(;l<=this.duration+t;){let p=u;u=o(l);let g=this.getFilmstripImage(u),x=d(g,"img");a.length<5&&x.removeAttribute("loading"),p&&p!==u&&g.classList.add("visualChange"),h&&l>=h&&(h=s.shift(),g.classList.add("lcp")),c&&l>=c&&(c=r.shift(),g.classList.add("layoutShift")),x.alt=`${this.location} at ${l/1e3}s, ${u.VisuallyComplete}% loaded.`,a.push(g),l+=t}return a}getFilmstripImage(t){let i=f.imgTemplate.cloneNode(!0).children[0];return i.children[0].src=this.#r(t.image),i.children[1].textContent=`${t.VisuallyComplete}%`,i}}customElements.define(f.tagName,f);var H=m;export{H as default};