preface

Recently I saw a button component and found that when clicked, it would appear a ripple effect (see the example of the GIF). It was very nice, so I studied its implementation and now I share with you. I use Vue for this sharing, if you want to port to another framework is also very easy, the core logic is the same, just the template part of the syntax changed slightly on the line.

Examples of button effects:

The realization of a single ripple effect

Let’s first implement a single ripple effect so we can understand how it works, and finally implement a multi-ripple final version.

In effect, clicking anywhere inside the button will cause a circular ripple to spread out over the mouse click, and a gradient of color from dark to light to finally disappearing.

Since the ripples are round, the problem can be translated into a circle that grows from small to large and gradually changes color from dark to light at the mouse click.

Since the ripple effect needs to be used in a container, this article explains it in a button.

To build the framework

First, create a file named: ripple-ink.vue with the following contents:

<template>
  <div class="ripple-ink">
  </div>
</template>

<script>
export default {
  name: "RippleInk".data() {
    return{}; },computed: {},methods: {}};</script>

<style lang="scss" scoped>
.ripple-ink {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: inherit;
}
</style>
Copy the code

Set the size of the ripple container and border rounded corners to the size of the parent. Using absolute, the parent must add position: relative to the ripple component, otherwise the ripple will spread.

In a demo file such as app. vue, introduce the ripple component and write a simple button shape. Place the ripple-ink inside the button.

<template>
  <div class="button">
    <span>submit</span>
    <ripple-ink></ripple-ink>
  </div>
</template>
<script>
import RippleInk from './components/ripple-ink.vue';
export default {
  name: "App".components: {
    RippleInk,
  },
}
</script>
<style >
body {
  margin: 0;
  padding: 0;
}
.button {
  color: #326de6;
  line-height: 40px;
  margin-left: 100px;
  margin-top: 100px;
  position: relative;
  width: 120px;
  height: 40px;
  border: 1px solid #ccc;
  border-radius: 4px;
  background: #e0e0e0;
  cursor: pointer;
}
</style>
Copy the code

Combined with ripples

Because the ripple happens when you click, add a mouse event to the component that needs to be split into mouse down and up actions, plus a sub-div that performs the ripple effect, and use V-show to show and hide it because the ripple eventually disappears.

Since the ripples are circular, have an initial size of 0, and are animated, add the pattern of the ripples.

The ripples are circular. How? Use borders. Just make them round.

Next comes the background color of the ripples. We can’t fix a single color, so we can consider using currentColor inherited from our parent.

The initial scale(0) is required, but it will be set to 1 when the ripples are shown, and then the transition effect will be added. The code is as follows:

<template>
  <div class="ripple-ink" @mousedown="handleMouseDown" @mouseup="handleMouseUp">
    <div v-show="isShow" class="ink"></div>
  </div>
</template>

<script>
export default {
  name: "RippleInk".data() {
    return {
      isShow: false}; },computed: {},methods: {
    handleMouseDown(e) {
      this.isShow = true;
    },
    handleMouseUp() {
      this.isShow = false; ,}}};</script>

<style lang="scss" scoped>
.ripple-ink {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: inherit;
  .ink {
    width: 0;
    height: 0;
    position: absolute; // Set the border to a circle so that the ripples are roundborder-radius: 50%;
    background-color: currentColor;
    transform: scale(0);
    transition: transform 0.6 s ease-out, opacity 0.6 sease-out; }}</style>
Copy the code

Now that the preparation is complete, the hard part comes: when you click the button, you add the width of the ripple and set the location of the ripple.

How do you determine the size of the ripples? The easiest way to do this is to create a circle larger than the size of the button, so that no matter where the button is, the ripples will not cover it. Since the base shape of all divs on a web page is a rectangle, we can use the diagonal formula to calculate a reference value as long as the ripple size exceeds this reference value:

let max;
if (rect.width === rect.height) {
  // If it is a square, simply multiply it by the square root of 2 instead of using the following formula to save money.
  max = rect.width * Math.SQRT2;
} else {
  max = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
}
// Set the ripple size to double the reference value
const size = max * 2 + "px";
Copy the code

Then calculate the coordinates of the mouse, and then put the position of the ripple according to this coordinate to the position of the mouse click:

const rect = this.$el.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
Copy the code

Finally, add the calculated data to the ripples. In order to achieve the effect of magnification and disappearance, you need to add two state styles, and you are done. Note that when the mouse is up, the setTimeout value needs to be larger than the duration of the transition effect you set.

Finally, remember in the.ripple-inkIn theoverflow: hiddenPlus, you can remove it when you’re debugging

Single ripple complete code

<template>
  <div class="ripple-ink" @mousedown="handleMouseDown" @mouseup="handleMouseUp">
    <div v-show="isShow" :style="inkStyle" :class="inkClass"></div>
  </div>
</template>

<script>
export default {
  name: "RippleInk".data() {
    return {
      isShow: false.isHolding: false.isDone: false.inkWidth: 0.inkHeight: 0.inkMarginLeft: 0.inkMarginTop: 0}; },computed: {
    inkStyle() {
      return {
        width: this.inkWidth,
        height: this.inkHeight,
        marginLeft: this.inkMarginLeft,
        marginTop: this.inkMarginTop,
      };
    },
    inkClass() {
      return [
        "ink".this.isHolding ? "is-holding" : "".this.isDone ? "is-done" : "",]; }},methods: {
    handleMouseDown(e) {
      this.isShow = true;
      this.$nextTick(() = > {
        const rect = this.$el.getBoundingClientRect();
        const x = e.clientX - rect.left;
        const y = e.clientY - rect.top;
        let max;
        if (rect.width === rect.height) {
          max = rect.width * Math.SQRT2;
        } else {
          max = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
        }
        const size = max * 2 + "px";
        this.inkWidth = size;
        this.inkHeight = size;
        // Adjust the position of the ripples
        this.inkMarginLeft = x - max + "px";
        this.inkMarginTop = y - max + "px";
        this.isHolding = true;
      });
    },
    handleMouseUp() {
      this.isDone = true;
      setTimeout(() = > {
        this.isShow = false;
        this.isDone = false;
        this.isHolding = false;
      }, 650); ,}}};</script>

<style lang="scss" scoped>
.ripple-ink {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: inherit;
  overflow: hidden;
  .ink {
    width: 0;
    height: 0;
    position: absolute;
    border-radius: 50%;
    background-color: currentColor;
    transform: scale(0);
    transition: transform 0.6 s ease-out, opacity 0.6 s ease-out;
    &.is-holding {
      opacity: 0.4;
      transform: scale(1);
      &.is-done {
        opacity: 0; }}}}</style>
Copy the code

The current single ripple is a bit of a problem, the ripple is not normal when clicking in quick succession. So there’s an optimized version.

Two, the optimization of the corrugated version

So, how do you achieve polyripple?

The single corrugated version above, ripples are fixed level of a child, to achieve more corrugated, will need to have a lot of children can be, and train of thought is: use a queue to store the ripple, when the mouse click, the ripples data into the queue, when the ripple effect after the execution, head removed from the queue it is ok, the same principle as a single corrugated core.

<template>
  <div
    ref="ripple-ink"
    class="ripple-ink"
    @mousedown="handleMouseDown"
    @mouseup="handleMouseUp"
  >
    <div
      v-for="ink in inkQueue"
      :key="ink.key"
      :style="{ width: ink.width, height: ink.height, marginLeft: ink.marginLeft, marginTop: ink.marginTop, }"
      :class="[ 'ink', ink.isHolding ? 'is-holding' : '', ink.isDone ? 'is-done' : '', ]"
    ></div>
  </div>
</template>

<script>
export default {
  name: "RippleInk".data() {
    return {
      inkQueue: [],}; },methods: {
    handleMouseDown(e) {
      const rect = this.$el.getBoundingClientRect();
      const x = e.clientX - rect.left;
      const y = e.clientY - rect.top;
      let max;
      if (rect.width === rect.height) {
        max = rect.width * Math.SQRT2;
      } else {
        max = Math.sqrt(rect.width * rect.width + rect.height * rect.height);
      }
      const size = max * 2 + "px";
      this.inkQueue.push({
        key: Math.random(),
        width: size,
        height: size,
        marginLeft: x - max + "px".marginTop: y - max + "px".isHolding: false.isDone: false});// After the queue data is added, the animation begins in the next event cycle
      setTimeout(() = > {
        this.inkQueue[this.inkQueue.length - 1].isHolding = true;
      });
    },
    handleMouseUp() {
      this.inkQueue[this.inkQueue.length - 1].isDone = true;
      setTimeout(() = > {
        this.inkQueue.shift();
      }, 650); ,}}};</script>

<style lang="scss" scoped>
.ripple-ink {
  position: absolute;
  top: 0;
  bottom: 0;
  left: 0;
  right: 0;
  border-radius: inherit;
  overflow: hidden;
  .ink {
    width: 0;
    height: 0;
    position: absolute;
    border-radius: 50%;
    background-color: currentColor;
    transform: scale(0);
    transition: transform 0.6 s ease-out, opacity 0.6 s ease-out;
    &.is-holding {
      opacity: 0.4;
      transform: scale(1);
      &.is-done {
        opacity: 0; }}}}</style>
Copy the code