Skip to content

Instantly share code, notes, and snippets.

@gladchinda
Created January 23, 2020 12:28
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gladchinda/4cac386c65e32dd2c142952019386922 to your computer and use it in GitHub Desktop.
Save gladchinda/4cac386c65e32dd2c142952019386922 to your computer and use it in GitHub Desktop.
Ripple Effect
<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>
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);
.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