diff --git a/packages/mdc-ripple/_ripple-theme.scss b/packages/mdc-ripple/_ripple-theme.scss index 0a8bfb389f2..7d1a601ba6a 100644 --- a/packages/mdc-ripple/_ripple-theme.scss +++ b/packages/mdc-ripple/_ripple-theme.scss @@ -110,6 +110,17 @@ $light-theme: ( @include states-press-opacity(map.get($theme, pressed-state-layer-opacity)); } +@mixin ripple-mask($margin, $min-ratio) { + $radial-gradient: radial-gradient( + closest-side, + #fff max(calc(100% - #{$margin}), #{$min-ratio}), + #fff0 100% + ); + + -webkit-mask-image: $radial-gradient; + mask-image: $radial-gradient; +} + @mixin states-base-color( $color, $query: feature-targeting.all(), @@ -125,6 +136,12 @@ $light-theme: ( ); } + #{$ripple-target}::after { + @include feature-targeting.targets($feat-color) { + @include ripple-mask(70px, 65%); + } + } + #{$ripple-target}::before, #{$ripple-target}::after { @include feature-targeting.targets($feat-color) { diff --git a/packages/mdc-ripple/_ripple.scss b/packages/mdc-ripple/_ripple.scss index c43d27ab9d8..3160ab77b12 100644 --- a/packages/mdc-ripple/_ripple.scss +++ b/packages/mdc-ripple/_ripple.scss @@ -115,6 +115,7 @@ --mdc-ripple-left: 0; --mdc-ripple-top: 0; --mdc-ripple-fg-scale: 1; + --mdc-ripple-fg-edge-scale: 1; --mdc-ripple-fg-translate-end: 0; --mdc-ripple-fg-translate-start: 0; @@ -189,6 +190,18 @@ left: var(--mdc-ripple-left, 0); } } + + &.mdc-ripple-upgraded--foreground-activation { + #{$ripple-target}::after { + @include feature-targeting.targets($feat-animation) { + animation: mdc-ripple-fg-radius-in ripple-theme.$translate-duration + forwards, + mdc-ripple-fg-opacity-in ripple-theme.$fade-in-duration forwards, + mdc-unbounded-ripple-fg-mask-in ripple-theme.$translate-duration + forwards; + } + } + } } &.mdc-ripple-upgraded--foreground-activation { @@ -210,7 +223,7 @@ @include feature-targeting.targets($feat-structure) { // Retain transform from mdc-ripple-fg-radius-in activation transform: translate(var(--mdc-ripple-fg-translate-end, 0)) - scale(var(--mdc-ripple-fg-scale, 1)); + scale(var(--mdc-ripple-fg-edge-scale, 1)); } } } @@ -301,6 +314,17 @@ } @mixin keyframes_ { + $ripple-mask-keyframes: 0% 70px 65%, 70% 70px 65%, 75% 55px 75%, 80% 40px 85%, + 85% 25px 90%, 90% 10px 95%, 100% 0px 100%; + + @keyframes mdc-unbounded-ripple-fg-mask-in { + @each $frame, $margin, $min-ratio in $ripple-mask-keyframes { + #{$frame} { + @include ripple-theme.ripple-mask($margin, $min-ratio); + } + } + } + @keyframes mdc-ripple-fg-radius-in { from { animation-timing-function: variables2.$standard-curve-timing-function; @@ -313,7 +337,7 @@ to { transform: translate(var(--mdc-ripple-fg-translate-end, 0)) - scale(var(--mdc-ripple-fg-scale, 1)); + scale(var(--mdc-ripple-fg-edge-scale, 1)); } } diff --git a/packages/mdc-ripple/constants.ts b/packages/mdc-ripple/constants.ts index bf58a8f7409..88732128311 100644 --- a/packages/mdc-ripple/constants.ts +++ b/packages/mdc-ripple/constants.ts @@ -34,6 +34,7 @@ export const cssClasses = { export const strings = { VAR_FG_SCALE: '--mdc-ripple-fg-scale', + VAR_FG_EDGE_SCALE: '--mdc-ripple-fg-edge-scale', VAR_FG_SIZE: '--mdc-ripple-fg-size', VAR_FG_TRANSLATE_END: '--mdc-ripple-fg-translate-end', VAR_FG_TRANSLATE_START: '--mdc-ripple-fg-translate-start', @@ -42,9 +43,15 @@ export const strings = { }; export const numbers = { - DEACTIVATION_TIMEOUT_MS: 225, // Corresponds to $mdc-ripple-translate-duration (i.e. activation animation duration) - FG_DEACTIVATION_MS: 150, // Corresponds to $mdc-ripple-fade-out-duration (i.e. deactivation animation duration) + DEACTIVATION_TIMEOUT_MS: + 225, // Corresponds to $mdc-ripple-translate-duration (i.e. activation + // animation duration) + FG_DEACTIVATION_MS: 150, // Corresponds to $mdc-ripple-fade-out-duration + // (i.e. deactivation animation duration) INITIAL_ORIGIN_SCALE: 0.6, PADDING: 10, - TAP_DELAY_MS: 300, // Delay between touch and simulated mouse events on touch devices + TAP_DELAY_MS: + 300, // Delay between touch and simulated mouse events on touch devices + SOFT_EDGE_MINIMUM_SIZE: 70, + SOFT_EDGE_CONTAINER_RATIO: 0.35 }; diff --git a/packages/mdc-ripple/foundation.ts b/packages/mdc-ripple/foundation.ts index 01166df7f79..d8589256b61 100644 --- a/packages/mdc-ripple/foundation.ts +++ b/packages/mdc-ripple/foundation.ts @@ -105,6 +105,7 @@ export class MDCRippleFoundation extends MDCFoundation { private activationTimer = 0; private fgDeactivationRemovalTimer = 0; private fgScale = '0'; + private fgEdgeScale = '0'; private frame = {width: 0, height: 0}; private initialSize = 0; private layoutFrame = 0; @@ -516,6 +517,11 @@ export class MDCRippleFoundation extends MDCFoundation { private layoutInternal() { this.frame = this.adapter.computeBoundingRect(); const maxDim = Math.max(this.frame.height, this.frame.width); + const isUnbounded = this.adapter.isUnbounded(); + + const softEdgeSize = Math.max( + numbers.SOFT_EDGE_CONTAINER_RATIO * maxDim, + numbers.SOFT_EDGE_MINIMUM_SIZE); // Surface diameter is treated differently for unbounded vs. bounded ripples. // Unbounded ripple diameter is calculated smaller since the surface is expected to already be padded appropriately @@ -526,31 +532,39 @@ export class MDCRippleFoundation extends MDCFoundation { const getBoundedRadius = () => { const hypotenuse = Math.sqrt( Math.pow(this.frame.width, 2) + Math.pow(this.frame.height, 2)); - return hypotenuse + MDCRippleFoundation.numbers.PADDING; + return hypotenuse + MDCRippleFoundation.numbers.PADDING + softEdgeSize; }; - this.maxRadius = this.adapter.isUnbounded() ? maxDim : getBoundedRadius(); + this.maxRadius = isUnbounded ? maxDim : getBoundedRadius(); // Ripple is sized as a fraction of the largest dimension of the surface, then scales up using a CSS scale transform const initialSize = Math.floor(maxDim * MDCRippleFoundation.numbers.INITIAL_ORIGIN_SCALE); // Unbounded ripple size should always be even number to equally center align. - if (this.adapter.isUnbounded() && initialSize % 2 !== 0) { + if (isUnbounded && initialSize % 2 !== 0) { this.initialSize = initialSize - 1; } else { this.initialSize = initialSize; } this.fgScale = `${this.maxRadius / this.initialSize}`; + this.fgEdgeScale = isUnbounded ? + this.fgScale : + `${(this.maxRadius + softEdgeSize) / this.initialSize}`; this.updateLayoutCssVars(); } private updateLayoutCssVars() { const { - VAR_FG_SIZE, VAR_LEFT, VAR_TOP, VAR_FG_SCALE, + VAR_FG_SIZE, + VAR_LEFT, + VAR_TOP, + VAR_FG_SCALE, + VAR_FG_EDGE_SCALE, } = MDCRippleFoundation.strings; this.adapter.updateCssVariable(VAR_FG_SIZE, `${this.initialSize}px`); this.adapter.updateCssVariable(VAR_FG_SCALE, this.fgScale); + this.adapter.updateCssVariable(VAR_FG_EDGE_SCALE, this.fgEdgeScale); if (this.adapter.isUnbounded()) { this.unboundedCoords = { diff --git a/packages/mdc-ripple/test/foundation.test.ts b/packages/mdc-ripple/test/foundation.test.ts index bdad0503d1a..ad69d7a349a 100644 --- a/packages/mdc-ripple/test/foundation.test.ts +++ b/packages/mdc-ripple/test/foundation.test.ts @@ -359,10 +359,14 @@ describe('MDCRippleFoundation', () => { jasmine.clock().tick(1); const maxSize = Math.max(width, height); + const softEdgeSize = Math.max( + numbers.SOFT_EDGE_CONTAINER_RATIO * maxSize, + numbers.SOFT_EDGE_MINIMUM_SIZE); + const initialSize = maxSize * numbers.INITIAL_ORIGIN_SCALE; const surfaceDiameter = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2)); - const maxRadius = surfaceDiameter + numbers.PADDING; + const maxRadius = surfaceDiameter + numbers.PADDING + softEdgeSize; const fgScale = `${maxRadius / initialSize}`; expect(adapter.updateCssVariable)