@sandlada/mdc
Version:
@sandlada/mdc is an open source component library that follows the Material Design 3 design specifications.
24 lines • 7.12 kB
JavaScript
import{customElement as e}from"../../node_modules/@lit/reactive-element/decorators/custom-element.js";import{property as t}from"../../node_modules/@lit/reactive-element/decorators/property.js";import"../../node_modules/lit/decorators.js";import{html as n,nothing as r}from"../../node_modules/lit-html/lit-html.js";import{LitElement as i}from"../../node_modules/lit-element/lit-element.js";import"../../node_modules/lit/index.js";import{classMap as a}from"../../node_modules/lit-html/directives/class-map.js";import"../../node_modules/lit/directives/class-map.js";import{composeMixin as o}from"../../utils/compose-mixin/compose-mixin.js";import s from"../../_virtual/_@oxc-project_runtime@0.144.0/helpers/esm/decorate.js";import{mixinDelegatesAria as c}from"../../utils/aria/delegate.js";import{LOADING_INDICATOR_COMPLETE_EVENT as l}from"./loading-indicator.interface.js";import{LoadingIndicatorStyles as u}from"./loading-indicator.style.js";
/**
* @license
* Copyright 2026 Kai-Orion & Sandlada
* SPDX-License-Identifier: MIT
*
* @fileoverview
* `mdc-loading-indicator` — the MD3 Expressive morphing-shape loading indicator.
*
* The indeterminate form morphs through the MD3E shape sequence
* (SoftBurst → Cookie9Sided → Pentagon → Pill → Sunny → Cookie4Sided → Oval)
* with a spring-driven morph between consecutive shapes, a short hold on each
* shape, an extra 90° spin per completed morph, and a continuous global
* rotation — mirroring the Jetpack Compose `LoadingIndicator` and the Android
* Views `LoadingIndicatorDrawable` animation parameters. The determinate form
* drives a circle → SoftBurst morph from `progress`.
*
* Geometry: every shape is a radial function sampled at uniform angles, so all
* shapes share the same point count and morphing reduces to a per-point lerp.
* The result is rendered as a single closed SVG path (a dense polyline reads
* as a smooth curve at the 38px active size).
*/
const d=Math.PI*2,f=1.2*Math.sqrt(200),p=(e,t,n)=>Math.min(n,Math.max(t,e));function m(e){let t=[],n=0;for(let r=0;r<72;r++){let i=r/72*d,a=e(i);t.push([Math.cos(i)*a,Math.sin(i)*a]),n=Math.max(n,a)}return n?t.map(([e,t])=>[e/n,t/n]):t}const h=(e,t=0)=>n=>{let r=(n%(d/e)+d/e)%(d/e);return Math.cos(Math.PI/e)/Math.cos(r-Math.PI/e)*(1+t*Math.cos(e*n))},g=(e,t)=>n=>e*t/Math.hypot(t*Math.cos(n),e*Math.sin(n));function _(e,t){let n=[];for(let r=0;r<16;r++){let i=-Math.PI/2+r/16*Math.PI;n.push([e+t*Math.cos(i),t*Math.sin(i)])}for(let r=0;r<16;r++)n.push([e-r/16*e*2,t]);for(let r=0;r<16;r++){let i=Math.PI/2+r/16*Math.PI;n.push([-e+t*Math.cos(i),t*Math.sin(i)])}for(let r=0;r<16;r++)n.push([-e+r/16*e*2,-t]);return v(n)}function v(e){let t=e.map(([e,t])=>({t:(Math.atan2(t,e)+d)%d,r:Math.hypot(e,t)})).sort((e,t)=>e.t-t.t),n=t.length;return m(e=>{if(e<=t[0].t)return t[0].r;if(e>=t[n-1].t)return t[n-1].r;let r=0,i=n-1;for(;i-r>1;){let n=r+i>>1;t[n].t<=e?r=n:i=n}let a=t[r],o=t[i],s=o.t-a.t||1;return a.r+(o.r-a.r)*((e-a.t)/s)})}const y=[m(e=>1+.38*Math.cos(4*e)),m(h(9,.08)),m(h(5)),_(.5,.5),m(e=>1+.5*Math.max(0,Math.cos(6*e))**.6),m(h(4,.1)),m(g(1,.82))],b=[m(()=>1),y[0]];function x(e,t,n){return e.map(([e,r],i)=>[e+(t[i][0]-e)*n,r+(t[i][1]-r)*n])}function S(e){if(!e.length)return``;let t=`M ${e[0][0].toFixed(4)} ${e[0][1].toFixed(4)}`;for(let n=1;n<e.length;n++)t+=` L ${e[n][0].toFixed(4)} ${e[n][1].toFixed(4)}`;return`${t} Z`}let C=class extends o(c)(i){constructor(...e){super(...e),this.indeterminate=!1,this.progress=0,this.variant=`primary`,this.contained=!1,this.speed=1,this.rafId=null,this.lastFrameTime=0,this.morphIndex=0,this.springPos=0,this.springVel=0,this.holdElapsed=0,this.springRunning=!1,this.rotationTarget=0,this.globalRotation=0,this.reducedMotion=!1,this.reducedMotionQuery=null,this.completeFired=!1,this.frame=e=>{if(this.rafId===null)return;let t=Math.min((e-this.lastFrameTime)/1e3,.05);this.lastFrameTime=e,this.stepIndeterminate(t),this.requestUpdate(),this.rafId=requestAnimationFrame(this.frame)},this.handleReducedMotionChange=e=>{this.reducedMotion=e.matches,this.reducedMotion?(this.stopAnimation(),this.requestUpdate()):this.indeterminate&&this.startAnimation()}}static{this.styles=u}connectedCallback(){super.connectedCallback();let e=matchMedia(`(prefers-reduced-motion: reduce)`);this.reducedMotionQuery=e,this.reducedMotion=e.matches,e.addEventListener(`change`,this.handleReducedMotionChange),this.indeterminate&&this.startAnimation()}disconnectedCallback(){super.disconnectedCallback(),this.reducedMotionQuery?.removeEventListener(`change`,this.handleReducedMotionChange),this.reducedMotionQuery=null,this.stopAnimation()}updated(e){e.has(`indeterminate`)&&(this.indeterminate?(this.completeFired=!0,this.startAnimation()):(this.stopAnimation(),this.completeFired=!1)),e.has(`progress`)&&this.progress<1&&(this.completeFired=!1),!this.indeterminate&&!this.completeFired&&this.progress>=1&&(this.completeFired=!0,this.dispatchEvent(new CustomEvent(l,{detail:{value:1},bubbles:!0,composed:!0})))}render(){let{ariaLabel:e}=this,t=this.computeRender(),i=a({container:!0,contained:this.contained,indeterminate:this.indeterminate,[`variant-${this.variant}`]:!0});return n`<div class="${i}" role="progressbar" aria-label="${e||r}" aria-valuemin="0" aria-valuemax="1" aria-valuenow="${this.indeterminate?r:this.progress}"><span class="background" aria-hidden="true"></span> <svg class="indicator" viewBox="-1.1 -1.1 2.2 2.2" aria-hidden="true"><path d="${t.path}" transform="rotate(${t.rotation.toFixed(2)})"/></svg></div>`}computeRender(){let e=this.indeterminate?y:b,t,n,r;if(this.indeterminate)t=this.morphIndex%e.length,n=this.springPos,r=this.globalRotation+this.rotationTarget+n*90;else{let i=p(this.progress,0,1)*e.length;t=Math.min(Math.floor(i),e.length-1),n=t===e.length-1?0:i-t,r=-p(this.progress,0,1)*180}let i=e[(t+1)%e.length];return{path:S(x(e[t],i,p(n,0,1.1))),rotation:r}}startAnimation(){if(this.rafId===null){if(this.reducedMotion){this.requestUpdate();return}this.lastFrameTime=performance.now(),this.rafId=requestAnimationFrame(this.frame)}}stopAnimation(){this.rafId!==null&&(cancelAnimationFrame(this.rafId),this.rafId=null)}stepIndeterminate(e){let t=Math.max(this.speed,0)*e;if(this.springRunning){let e=-200*(this.springPos-1)-f*this.springVel;this.springVel+=e*t,this.springPos+=this.springVel*t,Math.abs(this.springPos-1)<.001&&Math.abs(this.springVel)<.001&&(this.springPos=1,this.springVel=0,this.springRunning=!1,this.holdElapsed=0)}else this.holdElapsed+=t,this.holdElapsed>=.65&&(this.morphIndex=(this.morphIndex+1)%y.length,this.springPos=0,this.springVel=0,this.springRunning=!0,this.rotationTarget=(this.rotationTarget+90)%360);this.globalRotation=(this.globalRotation+360/4.666*t)%360}};s([t({type:Boolean,reflect:!0})],C.prototype,`indeterminate`,void 0),s([t({type:Number})],C.prototype,`progress`,void 0),s([t({type:String,reflect:!0})],C.prototype,`variant`,void 0),s([t({type:Boolean,reflect:!0})],C.prototype,`contained`,void 0),s([t({type:Number,reflect:!0})],C.prototype,`speed`,void 0),C=s([e(`mdc-loading-indicator`)],C);export{C as MDCLoadingIndicator};
//# sourceMappingURL=loading-indicator.js.map