A Pen by Glad Chinda on CodePen.
Created
January 23, 2020 12:28
-
-
Save gladchinda/4cac386c65e32dd2c142952019386922 to your computer and use it in GitHub Desktop.
Ripple Effect
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<button type="button" class="ripple btn"> | |
<i class="btn__icon"> | |
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"> | |
<path d="M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zm6.93 6h-2.95c-.32-1.25-.78-2.45-1.38-3.56 1.84.63 3.37 1.91 4.33 3.56zM12 4.04c.83 1.2 1.48 2.53 1.91 3.96h-3.82c.43-1.43 1.08-2.76 1.91-3.96zM4.26 14C4.1 13.36 4 12.69 4 12s.1-1.36.26-2h3.38c-.08.66-.14 1.32-.14 2 0 .68.06 1.34.14 2H4.26zm.82 2h2.95c.32 1.25.78 2.45 1.38 3.56-1.84-.63-3.37-1.9-4.33-3.56zm2.95-8H5.08c.96-1.66 2.49-2.93 4.33-3.56C8.81 5.55 8.35 6.75 8.03 8zM12 19.96c-.83-1.2-1.48-2.53-1.91-3.96h3.82c-.43 1.43-1.08 2.76-1.91 3.96zM14.34 14H9.66c-.09-.66-.16-1.32-.16-2 0-.68.07-1.35.16-2h4.68c.09.65.16 1.32.16 2 0 .68-.07 1.34-.16 2zm.25 5.56c.6-1.11 1.06-2.31 1.38-3.56h2.95c-.96 1.65-2.49 2.93-4.33 3.56zM16.36 14c.08-.66.14-1.32.14-2 0-.68-.06-1.34-.14-2h3.38c.16.64.26 1.31.26 2s-.1 1.36-.26 2h-3.38z" fill="currentColor"/> | |
</svg> | |
</i> | |
<span class="btn__label">Change Language</span> | |
<div class="ripple__inner"></div> | |
</button> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function easeOut (x) { | |
return 1 - (1 - x) * (1 - x); | |
} | |
function createAnimation ({ duration = 300, update, done }) { | |
let start = 0; | |
let elapsed = 0; | |
let progress = 0; | |
let aborted = false; | |
let animationFrameId = 0; | |
// Ensure the `update` and `done` callbacks are callable functions | |
done = (typeof done === 'function') ? done : function () {}; | |
update = (typeof update === 'function') ? update : function () {}; | |
// Function to effectively cancel the current animation frame | |
function stopAnimation () { | |
cancelAnimationFrame(animationFrameId); | |
animationFrameId = 0; | |
} | |
// Start a new animation by requesting for an animation frame | |
animationFrameId = requestAnimationFrame( | |
function _animation (timestamp) { | |
// Set the animation start timestamp if not set | |
if (!start) start = timestamp; | |
// Compute the time elapsed and the progress (0 - 1) | |
elapsed = timestamp - start; | |
progress = Math.min(elapsed / duration, 1); | |
// Call the `update()` callback with the current progress | |
update(progress); | |
// Stop the animation if `.abort()` has been called | |
if (aborted === true) return stopAnimation(); | |
// Request another animation frame until duration elapses | |
if (timestamp < start + duration) { | |
animationFrameId = requestAnimationFrame(_animation); | |
return; | |
} | |
// If duration has elapsed, cancel the current animation frame | |
// and call the `done()` callback | |
stopAnimation(); | |
done(); | |
} | |
); | |
// Return an object with an `.abort()` method to stop the animation | |
// Returns: Object({ abort: fn() }) | |
return Object.defineProperty(Object.create(null), 'abort', { | |
value: function _abortAnimation () { aborted = true } | |
}); | |
} | |
function getRippleElementProps (elem) { | |
// Initialize the ripple elements registry (first call only) | |
const rippleElems = new WeakMap(); | |
getRippleElementProps = function (elem) { | |
if (elem instanceof HTMLElement) { | |
if (!rippleElems.has(elem)) { | |
// Get the dimensions and position of the element on the page | |
const { width, height, y: top, x: left } = elem.getBoundingClientRect(); | |
const diameter = Math.min(width, height); | |
const radius = Math.ceil(diameter / 2); | |
// Configure functions to set and remove style properties | |
const style = elem.style; | |
const setProperty = style.setProperty.bind(style); | |
const removeProperty = style.removeProperty.bind(style); | |
// Function to remove multiple style properties at once | |
function removeProperties (...properties) { | |
properties.forEach(removeProperty); | |
} | |
// Set the diameter of the ripple in a custom CSS property | |
setProperty('--ripple-diameter', `${diameter}px`); | |
// Add the element and its geometric properties | |
// to the ripple elements registry (WeakMap) | |
rippleElems.set(elem, { | |
animations: [], | |
width, height, radius, top, left, setProperty, removeProperties | |
}); | |
} | |
// Return the geometric properties of the element | |
return rippleElems.get(elem); | |
} | |
} | |
return getRippleElementProps(elem); | |
} | |
function runRippleAnimation (elem, scaleFactor) { | |
const { animations, setProperty, removeProperties } = getRippleElementProps(elem); | |
// Abort all animations in the current sequence | |
while (animations.length) { | |
animations.pop().abort(); | |
} | |
// Start the "scale up" animation and add it to the animation sequence | |
animations.push(createAnimation({ | |
duration: 300, | |
update: progress => { | |
setProperty('--ripple-scale', easeOut(progress) * scaleFactor); | |
} | |
})); | |
// Start the "opacity up" animation and add it to the animation sequence | |
animations.push(createAnimation({ | |
duration: 200, | |
update: progress => { | |
setProperty('--ripple-opacity', Math.min(1, easeOut(progress) + 0.5)); | |
}, | |
done: () => { | |
// Wait for at least 50ms | |
// Start the "opacity down" animation and add it to the animation sequence | |
setTimeout(() => { | |
animations.push(createAnimation({ | |
duration: 200, | |
update: progress => { | |
setProperty('--ripple-opacity', easeOut(1 - progress)); | |
}, | |
done: () => { | |
// Remove all the properties at the end of the sequence | |
removeProperties( | |
'--ripple-center-x', | |
'--ripple-center-y', | |
'--ripple-opacity', | |
'--ripple-scale' | |
); | |
} | |
})); | |
}, 50); | |
} | |
})); | |
} | |
document.addEventListener('click', function _rippleClickHandler (evt) { | |
// Capture clicks happening inside a ripple element | |
const target = evt.target.closest('.ripple'); | |
if (target) { | |
// Get ripple element geometric properties from registry | |
const { | |
width, height, radius, top, left, setProperty | |
} = getRippleElementProps(target); | |
// Get the half width and height of the ripple element | |
const width_2 = width / 2; | |
const height_2 = height / 2; | |
// Get the x and y offsets of the click within the ripple element | |
const x = evt.clientX - left; | |
const y = evt.clientY - top; | |
// Compute the scale factor using Pythagoras' theorem | |
// and dividing by the ripple radius | |
const scaleFactor = Math.ceil( | |
Math.sqrt( | |
Math.pow(width_2 + Math.abs(x - width_2), 2) + | |
Math.pow(height_2 + Math.abs(y - height_2), 2) | |
) / radius | |
); | |
// Set the ripple center coordinates on the custom CSS properties | |
// Notice the ripple radius being used for offsets | |
setProperty('--ripple-center-x', `${x - radius}px`); | |
setProperty('--ripple-center-y', `${y - radius}px`); | |
// Run the ripple spreading animation | |
runRippleAnimation(target, scaleFactor); | |
} | |
}, false); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
.btn { | |
margin: 100px auto; | |
color: #6a7a7a; | |
cursor: pointer; | |
background: #f8f8f8; | |
// background: transparent; | |
padding: 0.5rem 0.75rem; | |
border: 1px solid #d4dbdb; | |
border-radius: 5px; | |
outline: none; | |
-webkit-tap-highlight-color: rgba(0, 0, 0, 0); | |
max-width: 200px; | |
display: block; | |
overflow: hidden; | |
white-space: nowrap; | |
text-overflow: ellipsis; | |
svg { | |
display: inline; | |
vertical-align: middle; | |
} | |
&__label { | |
font-size: 0.875rem; | |
font-weight: 600; | |
line-height: 24px; | |
margin-left: 0.25rem; | |
vertical-align: middle; | |
} | |
&__icon, &__label { | |
position: relative; | |
right: 0.25rem; | |
} | |
} | |
.ripple { | |
z-index: 0; | |
position: relative; | |
& > &__inner:empty { | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
z-index: -9999; | |
overflow: hidden; | |
position: absolute; | |
&::after { | |
content: ''; | |
position: absolute; | |
border-radius: 50%; | |
background: lighten(#000, 92.5%); | |
top: var(--ripple-center-y, 0); | |
left: var(--ripple-center-x, 0); | |
width: var(--ripple-diameter, 0); | |
height: var(--ripple-diameter, 0); | |
opacity: var(--ripple-opacity, 0); | |
transform: scale(var(--ripple-scale, 0)); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment