1// A flexible sheet hinged at the album spine. Slice positions join edge to edge 2// throughout the turn; front/reverse snapshots are the actual displayed pages. 3export const ALBUM_TURN_MS=1200; 4const STRIPS=10; 5export function albumLeafPose(progress,direction,width,gap=0,count=STRIPS){ 6 const p=Math.max(0,Math.min(1,progress)),bend=.34*Math.sin(Math.PI*p); 7 const step=width/count,poses=[]; 8 let x=direction*gap/2*Math.cos(Math.PI*p),z=gap/2*Math.sin(Math.PI*p); 9 for(let index=0;index<count;index++){ 10 const angle=-direction*Math.PI*p+direction*bend*(.5-(index+.5)/count); 11 const dx=direction*step*Math.cos(angle),dz=-direction*step*Math.sin(angle); 12 poses.push({x:x+dx/2,z:z+dz/2,angle:angle*180/Math.PI,source:direction>0?index:count-1-index}); 13 x+=dx;z+=dz; 14 } 15 return poses; 16} 17 18export function startAlbumTurn({book,overlay,left,right,nextLeft,nextRight,direction,onFinish}){ 19 const rect=book.getBoundingClientRect(),leftRect=left.getBoundingClientRect(),rightRect=right.getBoundingClientRect(); 20 const width=rightRect.width,gap=rightRect.left-leftRect.right,strips=[]; 21 // Measure the incoming pages in this book's responsive layout before freezing 22 // its height. Long titles cannot pop through, or resize, the turning sheet. 23 const measure=document.createElement('div');measure.className='album-turn-measure';measure.style.width=width+'px'; 24 measure.append(nextLeft,nextRight);book.append(measure); 25 const height=Math.max(leftRect.height,rightRect.height,nextLeft.getBoundingClientRect().height,nextRight.getBoundingClientRect().height); 26 measure.remove(); 27 const outgoing=(direction>0?right:left).cloneNode(true),incoming=(direction>0?nextLeft:nextRight).cloneNode(true); 28 for(const page of [outgoing,incoming]){ 29 page.removeAttribute('id');page.style.width=width+'px';page.style.height=height+'px'; 30 for(const button of page.querySelectorAll('button'))button.tabIndex=-1; 31 } 32 left.style.height=right.style.height=height+'px';left.inert=right.inert=true; 33 book.setAttribute('aria-busy','true'); 34 // The departing sheet uncovers the incoming page, not another copy of itself. 35 const exposed=direction>0?right:left,destination=direction>0?nextRight:nextLeft; 36 exposed.replaceChildren(...destination.childNodes); 37 overlay.replaceChildren();overlay.classList.add('turn');overlay.classList.toggle('reverse',direction<0); 38 overlay.style.left=(leftRect.right+gap/2-rect.left)+'px';overlay.style.top=(leftRect.top-rect.top)+'px';overlay.style.height=height+'px'; 39 const shadow=document.createElement('div');shadow.className='album-leaf-shadow';shadow.style.width=width+'px';shadow.style.height=height+'px'; 40 shadow.style.left=direction>0?gap/2+'px':(-width-gap/2)+'px';overlay.append(shadow); 41 const initial=albumLeafPose(0,direction,width,gap); 42 for(let i=0;i<STRIPS;i++){ 43 const source=initial[i].source,strip=document.createElement('div');strip.className='album-leaf-strip'; 44 strip.style.width=(width/STRIPS+.35)+'px';strip.style.height=height+'px'; 45 for(const [back,page,index] of [[false,outgoing,source],[true,incoming,STRIPS-1-source]]){ 46 const face=document.createElement('div');face.className='album-leaf-face'+(back?' album-leaf-back':''); 47 const content=page.cloneNode(true);content.style.left=(-index*width/STRIPS)+'px'; 48 const sheen=document.createElement('div');sheen.className='album-leaf-sheen';sheen.style.width=width+'px';sheen.style.left=content.style.left; 49 face.append(content,sheen);strip.append(face); 50 } 51 overlay.append(strip); 52 strips.push(strip); 53 } 54 // Drive the visible sheet directly. It must not silently disappear when the 55 // Web Animations API or the system's decorative animations are disabled. 56 let canceled=false,frame=null,wake=null,started=null; 57 const paint=progress=>{ 58 const poses=albumLeafPose(progress,direction,width,gap); 59 strips.forEach((strip,i)=>{const pose=poses[i];strip.style.transform=`translate3d(${pose.x-width/STRIPS/2}px,0,${pose.z}px) rotateY(${pose.angle}deg)`;}); 60 shadow.style.opacity=String(.24*Math.sin(Math.PI*progress)); 61 }; 62 const tick=(now=performance.now())=>{ 63 if(canceled)return; 64 cancelAnimationFrame(frame);clearTimeout(wake); 65 if(started===null)started=now; 66 const elapsed=Math.min(1,(now-started)/ALBUM_TURN_MS); 67 paint(elapsed*elapsed*(3-2*elapsed)); 68 if(elapsed<1)schedule();else onFinish(); 69 }; 70 // Some browser/compositor combinations stall frame callbacks in dialogs. 71 // The timer is a watchdog for that frame, canceled whenever rAF arrives. 72 const schedule=()=>{frame=requestAnimationFrame(tick);wake=setTimeout(tick,40);}; 73 paint(0);schedule(); 74 return {cancel(){ 75 canceled=true;cancelAnimationFrame(frame);clearTimeout(wake); 76 overlay.classList.remove('turn','reverse');overlay.replaceChildren(); 77 overlay.removeAttribute('style');left.style.height=right.style.height='';left.inert=right.inert=false;book.removeAttribute('aria-busy'); 78 }}; 79}
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.