PageSourceSearch

https://numberbook.ir/web/src/ripple-background.mjs

js numberbook.ir collected 2026-10-02 01:58:51 UTC 7,013 bytes, 122 lines download raw bytes

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.