1// Android 30 RippleBackground: a full-radius circle below the moving waves. 2// State opacity animates for 80ms; paint alpha is selected separately and rounded 3// to a byte on every frame. These are not the Material selector's alpha values. 4export const BACKGROUND_DURATION=80; 5export function rippleBackgroundState(previous,{focused=false,hovered=false,pressed=false}) { 6 focused=!!focused&&(!!previous.focused||!pressed); 7 hovered=!!hovered&&(!!previous.hovered||!pressed); 8 return {focused,hovered,opacity:focused?.6:hovered?.2:0}; 9} 10export function rippleStateColor(config,{pressed=false,focused=false,hovered=false}) { 11 return pressed?config.color:focused?(config.focusColor||config.color):hovered?(config.hoverColor||config.color):(config.releaseColor||config.color); 12} 13export function rippleBackgroundAlpha(color,opacity) { 14 return Math.round(Math.min(128,parseInt(color.slice(7,9)||'ff',16))*opacity); 15} 16 17export function attachRippleBackgrounds(root,resolve) { 18 const states=new Map(),abort=new AbortController(); 19 let suspended=false; 20 const options={capture:true,passive:true,signal:abort.signal}; 21 const valid=target=>target.element.isConnected&&root.contains(target.element)&& 22 !target.control.matches(':disabled,[aria-disabled="true"]')&&!target.control.closest('[inert],.leaving'); 23 const focused=target=>!suspended&&!document.hidden&&document.activeElement===target.control&&target.control.matches(':focus-visible'); 24 function ensure(target) { 25 let state=states.get(target.element); 26 if(!state){state={target,raw:{hovered:false,pressed:false},active:{focused:false,hovered:false,opacity:0},opacity:0,from:0,to:0,started:0,frame:0,layer:null,circle:null};states.set(target.element,state);} 27 return state; 28 } 29 function flags(target) { 30 return {...(states.get(target.element)?.raw||{hovered:false,pressed:false}),focused:focused(target)}; 31 } 32 function geometry(state) { 33 if(!state.circle)return; 34 const {surface,config}=state.target,r=surface.getBoundingClientRect(),inset=config.insetY||0,maskInset=config.maskInsetY||0; 35 const diameter=Math.hypot(r.width,r.height+inset*2); 36 Object.assign(state.circle.style,{width:diameter+'px',height:diameter+'px',left:(r.width-diameter)/2+'px',top:(r.height-diameter)/2-maskInset+'px'}); 37 } 38 const resize=new ResizeObserver(entries=>{ 39 for(const entry of entries)for(const state of states.values())if(state.target.surface===entry.target)geometry(state); 40 }); 41 function createLayer(state) { 42 if(state.layer)return; 43 const {surface,config}=state.target; 44 const layer=document.createElement('nb-ripple-state'),circle=document.createElement('nb-ripple-state-circle'); 45 layer.className='native-ripple-state'+(config.background?' native-ripple-background':''); 46 layer.setAttribute('aria-hidden','true');layer.style.borderRadius=config.radius+'px'; 47 if(config.maskInsetY)layer.style.top=layer.style.bottom=config.maskInsetY+'px'; 48 circle.className='native-ripple-state-circle';layer.append(circle); 49 // Android draws the state circle before all exiting and active wave circles. 50 surface.insertBefore(layer,surface.querySelector(':scope > .native-ripple')); 51 state.layer=layer;state.circle=circle;geometry(state);resize.observe(surface); 52 } 53 function sample(state,now=performance.now()) { 54 const progress=Math.min(1,Math.max(0,(now-state.started)/BACKGROUND_DURATION)); 55 state.opacity=state.from+(state.to-state.from)*progress; 56 return progress; 57 } 58 function paint(state) { 59 if(!state.layer&&state.to===0)return; 60 createLayer(state); 61 const color=rippleStateColor(state.target.config,flags(state.target)); 62 state.circle.style.background=color.slice(0,7); 63 state.circle.style.opacity=rippleBackgroundAlpha(color,state.opacity)/255; 64 } 65 function remove(state) { 66 cancelAnimationFrame(state.frame);resize.unobserve(state.target.surface); 67 state.layer?.remove();states.delete(state.target.element); 68 } 69 function frame(state,now) { 70 state.frame=0; 71 if(!valid(state.target)){remove(state);return;} 72 const progress=sample(state,now);paint(state); 73 if(progress<1)state.frame=requestAnimationFrame(now=>frame(state,now)); 74 else if(state.to===0&&!state.raw.pressed&&!state.raw.hovered&&!focused(state.target))remove(state); 75 } 76 function update(state) { 77 if(!valid(state.target)){remove(state);return;}
78 const next=rippleBackgroundState(state.active,flags(state.target)); 79 if(next.focused!==state.active.focused||next.hovered!==state.active.hovered){ 80 sample(state);state.from=state.opacity;state.to=next.opacity;state.started=performance.now();state.active=next; 81 cancelAnimationFrame(state.frame);state.frame=requestAnimationFrame(now=>frame(state,now)); 82 } 83 paint(state); 84 if(!state.frame&&state.to===0&&!state.raw.pressed&&!state.raw.hovered&&!focused(state.target))remove(state); 85 } 86 function syncFocus() { 87 if(abort.signal.aborted||suspended)return; 88 const target=resolve(document.activeElement); 89 if(target&&focused(target))ensure(target); 90 for(const state of [...states.values()])update(state); 91 } 92 function setPressed(target,pressed) { 93 const state=ensure(target);state.raw.pressed=pressed;update(state); 94 } 95 root.addEventListener('pointerover',event=>{ 96 // Touch pointerover is a contact event, not Android state_hovered. 97 if(suspended||event.pointerType!=='mouse'&&(event.pointerType!=='pen'||event.buttons!==0))return; 98 const target=resolve(event.target);if(!target)return; 99 const state=ensure(target);state.raw.hovered=true;update(state); 100 },options); 101 root.addEventListener('pointerout',event=>{ 102 const target=resolve(event.target),next=resolve(event.relatedTarget); 103 if(!target||next?.element===target.element)return; 104 const state=states.get(target.element);if(state){state.raw.hovered=false;update(state);} 105 },options); 106 root.addEventListener('pointercancel',event=>{ 107 const target=resolve(event.target),state=target&&states.get(target.element); 108 if(state){state.raw.hovered=false;update(state);} 109 },options); 110 for(const type of ['focusin','focusout','keydown','pointerdown'])root.addEventListener(type,()=>queueMicrotask(syncFocus),options); 111 function clear(){for(const state of [...states.values()])remove(state);} 112 // View.onWindowFocusChanged clears pressed, but does not clear state_focused 113 // or state_hovered. The wave owner handles press cancellation on window blur. 114 window.addEventListener('focus',syncFocus,options); 115 document.addEventListener('visibilitychange',()=>{suspended=document.hidden;if(suspended)clear();else syncFocus();},options); 116 const observer=new MutationObserver(()=>{ 117 for(const state of [...states.values()])if(!valid(state.target))remove(state); 118 }); 119 observer.observe(root,{childList:true,subtree:true,attributes:true,attributeFilter:['disabled','aria-disabled','inert','class']}); 120 syncFocus(); 121 return {setPressed,flags,dispose(){abort.abort();observer.disconnect();resize.disconnect();clear();}}; 122}
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.