// // Copyright 2023 Google LLC // SPDX-License-Identifier: Apache-2.0 // // go/keep-sorted start @use 'sass:list'; // go/keep-sorted end // go/keep-sorted start @use '../../tokens'; // go/keep-sorted end @mixin theme($tokens) { $supported-tokens: tokens.$md-comp-circular-progress-supported-tokens; @each $token, $value in $tokens { @if list.index($supported-tokens, $token) == null { @error 'Token `#{$token}` is not a supported token.'; } @if $value { --md-circular-progress-#{$token}: #{$value}; } } } @mixin styles() { $tokens: tokens.md-comp-circular-progress-values(); // If changing this value, make sure to change $size-without-padding in the // circular-progress tokens. $container-padding: 4px; // note, these value come from the m2 version but match current gm3 values. // Constants: // ARCSIZE = 270 degrees (amount of circle the arc takes up) // ARCTIME = 1333ms (time it takes to expand and contract arc) // ARCSTARTROT = 216 degrees (how much the start location of the arc // should rotate each time, 216 gives us a // 5 pointed star shape (it's 360/5 * 3). // For a 7 pointed star, we might do // 360/7 * 3 = 154.286) // ARCTIME $arc-duration: 1333ms; // 4 * ARCTIME $cycle-duration: calc(4 * $arc-duration); // ARCTIME * 360 / (ARCSTARTROT + (360-ARCSIZE)) $linear-rotate-duration: calc($arc-duration * 360 / 306); $indeterminate-easing: cubic-bezier(0.4, 0, 0.2, 1); :host { @each $token, $value in $tokens { --_#{$token}: #{$value}; } display: inline-flex; vertical-align: middle; width: var(--_size); height: var(--_size); position: relative; align-items: center; justify-content: center; // `contain` and `content-visibility` are performance optimizations // important here because progress indicators are often used when a cpu // intensive task is underway so it's especially important to minimize // their cpu consumption. contain: strict; content-visibility: auto; } .progress { flex: 1; align-self: stretch; margin: $container-padding; } .progress, .spinner, .left, .right, .circle, svg, .track, .active-track { position: absolute; inset: 0; } svg { transform: rotate(-90deg); } circle { cx: 50%; cy: 50%; r: calc(50% * (1 - var(--_active-indicator-width) / 100)); // match size to indeterminate border width stroke-width: calc(var(--_active-indicator-width) * 1%); // note, pathLength is set so this can be normalized stroke-dasharray: 100; fill: transparent; } .active-track { // note, these value come from the m2 version but match current gm3 values. transition: stroke-dashoffset 500ms cubic-bezier(0, 0, 0.2, 1); stroke: var(--_active-indicator-color); } .track { stroke: transparent; } .progress.indeterminate { animation: linear infinite linear-rotate; animation-duration: $linear-rotate-duration; } .spinner { animation: infinite both rotate-arc; animation-duration: $cycle-duration; animation-timing-function: $indeterminate-easing; } .left { overflow: hidden; inset: 0 50% 0 0; } .right { overflow: hidden; inset: 0 0 0 50%; } .circle { box-sizing: border-box; border-radius: 50%; // match size to svg stroke width, which is a fraction of the overall // padding box width. $_padding-box-width: calc(var(--_size) - 2 * $container-padding); $_active-indicator-fraction: calc(var(--_active-indicator-width) / 100); border: solid calc($_active-indicator-fraction * $_padding-box-width); border-color: var(--_active-indicator-color) var(--_active-indicator-color) transparent transparent; animation: expand-arc; animation-iteration-count: infinite; animation-fill-mode: both; animation-duration: $arc-duration, $cycle-duration; animation-timing-function: $indeterminate-easing; } .four-color .circle { animation-name: expand-arc, four-color; } .left .circle { rotate: 135deg; inset: 0 -100% 0 0; } .right .circle { rotate: 100deg; inset: 0 0 0 -100%; animation-delay: calc(-0.5 * $arc-duration), 0ms; } @media (forced-colors: active) { .active-track { stroke: CanvasText; } .circle { border-color: CanvasText CanvasText Canvas Canvas; } } // Indeterminate mode is 3 animations composed together: // 1. expand-arc: an arc is expanded/contracted between 10deg and 270deg. // 2. rotate-arc: at the same time, the arc is rotated in increments // of 270deg. // 3. linear-rotate: that rotating arc is then linearly rotated to produce // a spinning expanding/contracting arc. // // See original implementation: // https://github.com/PolymerElements/paper-spinner/blob/master/paper-spinner-styles.js. // 1. expand-arc // This is used on 2 divs which each represent half the desired // 270deg arc with one offset by 50%. This creates an arc which expands from // 10deg to 270deg. @keyframes expand-arc { 0% { transform: rotate(265deg); } 50% { transform: rotate(130deg); } 100% { transform: rotate(265deg); } } // 2. rotate-arc // The arc seamlessly travels around the circle indefinitely so it needs to // end at a full rotation of the circle. This rotates the 270 deg // (270/360 = 3/4) arc 4x (4 * 3/4 = 3) so it ends at // (3 * 360 = 1080). // This is sub-divided into increments of 135deg since the arc is rendered // with 2 divs acting together. @keyframes rotate-arc { 12.5% { transform: rotate(135deg); } 25% { transform: rotate(270deg); } 37.5% { transform: rotate(405deg); } 50% { transform: rotate(540deg); } 62.5% { transform: rotate(675deg); } 75% { transform: rotate(810deg); } 87.5% { transform: rotate(945deg); } 100% { transform: rotate(1080deg); } } // 3. linear-rotate // The traveling expanding arc is linearly rotated to produce the spinner // effect. @keyframes linear-rotate { to { transform: rotate(360deg); } } // This animates between 4 colors which are each shown for 25% of the time. // Each color is shown solid for 3/5 of that time (3/5 * 25% = 15%) and // transitions to the next color for 2/5 of that time (2/5 * 25% = 10%). @keyframes four-color { 0% { border-top-color: var(--_four-color-active-indicator-one-color); border-right-color: var(--_four-color-active-indicator-one-color); } 15% { border-top-color: var(--_four-color-active-indicator-one-color); border-right-color: var(--_four-color-active-indicator-one-color); } 25% { border-top-color: var(--_four-color-active-indicator-two-color); border-right-color: var(--_four-color-active-indicator-two-color); } 40% { border-top-color: var(--_four-color-active-indicator-two-color); border-right-color: var(--_four-color-active-indicator-two-color); } 50% { border-top-color: var(--_four-color-active-indicator-three-color); border-right-color: var(--_four-color-active-indicator-three-color); } 65% { border-top-color: var(--_four-color-active-indicator-three-color); border-right-color: var(--_four-color-active-indicator-three-color); } 75% { border-top-color: var(--_four-color-active-indicator-four-color); border-right-color: var(--_four-color-active-indicator-four-color); } 90% { border-top-color: var(--_four-color-active-indicator-four-color); border-right-color: var(--_four-color-active-indicator-four-color); } 100% { border-top-color: var(--_four-color-active-indicator-one-color); border-right-color: var(--_four-color-active-indicator-one-color); } } }