Can you believe that this image is painted with pure CSS?

This is the work of a foreign master, the author and the whole process of production are recorded down, although I do not know how much time it took, but this is definitely an amazing work

I know a lot of people think, “Yeah, that’s cool, but how?” .

I don’t know why, but it’s really exciting to know that you can do this with CSS. You can learn a lot of tricks just by looking at his code,

While trying to understand how an expert does it can absorb a lot of experience, I still want to do it myself. Fortunately, I found another expert who is a little more approachable – Aaron Iker. Most of his work revolves around the “button”, an integral part of a web page that is rarely used.

Button, almost all web pages will use it, but it is used to trigger some action, triggered action is what we care about, few will make more ink on the top, top more Hover color or displacement is very much the same.

But consider the following example:

A little ingenuity, instantly let the button live up.

And because the scope is limited to the size of the button, a slightly flashy animation won’t cause too much disruption to the overall page.

Inspired by Aaron, I also tried to make a button animation in my spare time. In this article, I will share the tools and eyebrows used in the process.

inspiration

This time the Button Animation is mainly modified from YorKun’s work on Dribbble – Button Lock Animation. Thanks to the author and the attached source file, I can refer to it more easily.

However, I didn’t follow the original animation completely. I just wanted to try out some different combinations of animation, which I will introduce in the following sections.

Animation implementation

There were four animations I wanted to achieve at first:

  1. Slide to unlock
  2. Locks open and drop
  3. Animation corresponding to unlock state
  4. 2D physics of lock dragging

Theoretically, it should be completed soon, but because I am not familiar with GSAP, I did a lot of wrong things, so I only finished the first three items, which was not satisfactory.

The main tools used are GSAP and THE Draggable plugin of GSAP

Slide to unlock

The GSAP Draggable plugin is easy enough to use. Given the DOM object that you want to start the Draggable, and the direction (type) and bounds (bounds) to drag, this effect can be achieved instantly:

// Register the GSAP draggable plugin
gsap.registerPlugin(Draggable);

// Select the DOM to interact with querySelector
const button = document.querySelector(".unlock-btn");
const lockerArea = button.querySelector(".locker");
const dropArea = button.querySelector(".drop");

// Main draggable instance
Draggable.create(lockerArea, {
  type: "x".bounds: button,
  onDrag(e) {},
  onRelease(e) {
    if (!this.hitTest(dropArea)) {
      gsap.to(lockerArea, {
        x: 0.duration: 0.6.ease: "elastic.out(1, .75)"
      });
    } else {
      // this.disable();
      gsap.to(lockerArea, {
        x: dropArea.offsetLeft - 9.duration: 0.6.ease: "elastic.out(1, .8)".onUpdate(e){ tl.restart(); }}); }}});Copy the code

As you can see in the middle, we specify type x to indicate that the move is on the X-axis, and bounds is the buttonDOM object, so we can’t drag beyond the range of Butotn at most.

In addition, one of the effects in the movie is that when you drag to the front and back ends, a suction force pulls the object in motion, which is actually achieved by two additional animation effects.

Draggable.create() can be passed as an Option to specify onDrag and onReleas, On onRelease we can use the built-in this.hittest (dropArea) function to determine if the object in the drag touches another specified DOM object. If it doesn’t, we pull back to where we started, which is what this does:

Gsap. to(lockerArea, {x: 0, duration: 0.6, ease: "elastic. Out (1,.75)"});Copy the code

To transforms the specified DOM object to the property state we passed in, in this case to the origin, equivalent to apply Transform :translateX(0).

If the specified object is touched, x can be adjusted to pull the drag object directly to the specified object, thus creating a suction effect.

In addition, in the gsap.to function after the object is touched, we also pass in OnUpdate, which will be triggered after the animation completes, just in time for us to proceed to the next phase of the animation – lock opening and drop.

Locks open and drop

OnUpdate is emitted when the drag object touches the specified object:

onUpdate(e) {
  tl.restart();
}
Copy the code

In onUpdate we put a Timeline object, which allows us to sequence animation, step by step specifying how each object should be animated in order.

Since I’ve defined the entire timeline animation elsewhere, onUpdate is called tl.restart() when triggered, or you can define it directly in the handler.

Timeline is as simple to use as:

let tl = gsap.timeline({ paused: true }); // Create a scheduleCopy the code

I created a Timeline object and passed {paused: true} because I wanted to trigger it later, so I paused it by default so we could call restart() on onUpdate.

As an aside, INSTEAD of using Timeline, I initially called the other GSAP.to in the onUpdate of each Gsap.to, which worked, but made the code much less readable, and eventually I switched to using Timeline to concatenate sequential animations.

We then set the value we want to change for each DOM object we want to animate:

Let the whole body part of the lock head move down first, let the upper part of the iron ring stay in place, to create the effect of opening the lock.

Tl. to(lockerBody, {y: "120%", duration: 0.2})Copy the code

Then, keyframes were used to perform a series of detailed animations for a single object, mainly to shift and rotate the entire lock (including the body and the iron ring) to create an animation of opening the lock and removing it from the lock:

tl.to(lockerBody, { / *... Just * / })
  .to(locker, {
    keyframes: [{rotation: -45.x: -8.transformOrigin: "center".duration: 0.2
      },
      {
        x: -15.y: -1.duration: 0.2
      },
      {
        x: -30.y: 10.duration: 0.2
      },
      {
        y: 100.opacity: 0.duration: 0.2}]})Copy the code

The next step is to replace UNLOCK with a final – UNLOCK state animation for other DOM objects:

tl.to(lockerBody, { / *... Just * / })
  .to(locker, { / *... Just * / })
  .to(lockerArea, {
    rotation: -90.duration: 0.3
  })
  .to(".message,.drop,.locker-area", {
    y: 30.opacity: 0.duration: 0.1
  })
  .fromTo(
    ".read-ok, .unlock-msg",
    {
      y: -30.opacity: 0
    },
    {
      opacity: 1.y: 0.duration: 0.2});Copy the code

Note that in addition to passing the DOM object to gsap.to and gsap.fromTo, we can also specify the class name directly, which is very convenient.

With a few lines of code like this, a set of buttons on the animation, should still be good!

conclusion

So today we did a little bit of a little bit of a practice of taking inspiration from Dribbble and using the front end technology to actually animate it, maybe nothing new, but hopefully it gave you a little bit of inspiration,

I usually have no matter at home to do some interesting animation or CSS, amuse yourself!

Complete code:

HTML

<button class="unlock-btn"> <div class="locker-placeHolder"></div> <div class="read-ok"> <svg width="27" height="20" ViewBox = "0 0 27 20" fill = "none" XMLNS = "http://www.w3.org/2000/svg" > < path d = "M3 9.5 L10 16 l24 3" stroke = "white" stroke-width="5" stroke-linecap="round" /> </svg> </div> <div class="locker-area"> <div class="locker"> <svg class="locker-head" width="34" height="41" viewBox="0 0 34 41" fill="none" xmlns="http://www.w3.org/2000/svg"> <path Fill-rule ="evenodd" clip-rule="evenodd" d="M12.0968 12.6839C11.1539 13.8322 10.7407 15.3606 10.7407 16.6032 v35h4v16.6032C4 14.1299 4.76939 11.026 6.86742 8.47087C9.06581 5.79355 12.4996 4 17.1689 4C21.6987 4 24.9976 6.06894 27.0798 8.6843C29.0655 11.1783 30 14.2215 30 16.6032V25.8431C30 27.6869 28.491 29.1815 26.6296 29.1815C24.7682 29.1815 23.2593 27.6869 23.2593 25.8431V16.6032C23.2593 15.7336 22.8423 14.1444 21.787 12.8188C20.8282 11.6147 19.3969 10.6769 17.1689 10.6769C14.4049 10.6769 12.9394 11.6578 12.0968 12.6839Z" fill="#282F38" /> <g filter="url(#filter0_f)"> <path d="M25.6296 25.8431C25.6296 26.3954 26.0773 26.8431 26.6296 C27.1819 26.8431 27.6296 26.3954 27.6296 35 v16 25.8431 H25.6296 ZM8.37036. 6032 h6. 37036 v35h8. 37036 zm8. 37036 C8.37036 16.6032 14.9319 8.90752 12.853 10.255 11.212c11.573 9.60682 13.7314 8.3385 17.1689 8.3385V6.3385C13.1731 6.3385 10.4321 7.84463 8.70928 9.94283C7.01579 12.0053 6.37036 14.5587 6.37036 16.6032H8.37036ZM17.1689 8.3385c20.2101 8.3385 22.2987 9.67592 23.6511 11.3745c25.0323 12.0053 6.37036 14.5587 6.37036 16.6032H8.37036ZM17.1689 8.3385c20.2101 8.3385 22.2987 9.67592 23.6511 11.3745c25.0323 13.1094 25.6296 15.2065 25.6296 16.6032H27.6296C27.6296 14.7486 26.8754 12.2134 25.2157 10.1288C23.5271 8.00777 20.8854 16.6032 6.3385 17.1689 6.3385 V8.3385 ZM25.6296 V25.8431 H27.6296 V16.6032 H25.6296 Z "fill =" # 7 b8698 "/ > < / g > < defs > < filter Id ="filter0_f" x="0.370361" y="0.338501" width="33.2593" height="40.6615" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB"> <feFlood flood-opacity="0" result="BackgroundImageFix" /> <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape" /> <feGaussianBlur stdDeviation="3" result="effect1_foregroundBlur" /> </filter> </defs> </svg> <div class="locker-body"></div> </div> </div> <span class="message">UNLOCK</span> <span class="unlock-msg">Read</span> <div class="drop"></div> </button> <footer> <p>original design credits to: <a href="https://dribbble.com/shots/15522609-Button-Lock-Animation">https://dribbble.com/shots/15522609-Button-Lock-Animati on</a></p> </footer>Copy the code

CSS

body { background: #27282d; color: #fff; } footer { position: fixed; bottom: 10px; } .unlock-btn { /* variables */ --button-width: 250px; --button-background: linear-gradient(180deg, #3385ff 0%, #2075f4 100%); --locker-size: 50px; Locker - BACKGROUND: RGBA (104, 164, 255, 0.5); /* style */ position: absolute; display: flex; justify-content: space-around; align-items: center; width: var(--button-width); height: 80px; left: calc(50% - var(--button-width) / 2); top: 100px; background: var(--button-background); Box-shadow: inset 0px 4px 10px Rgba (255, 255, 255, 0.2); border-radius: 1000px; border: 0; cursor: pointer; &:hover { background: Linear-gradient (0DEg, Rgba (0, 0, 0, 0.15), Rgba (0, 0, 0, 0.15)), Linear-gradient (180DEg, # 3385FF 0%, # 2075F4 100%); } .message, .unlock-msg { font-size: 28px; color: #fff; } .unlock-msg { position: absolute; transform: translate3d(0px, 0px, 0); opacity: 0; } .read-ok, .locker-placeHolder { position: relative; width: var(--locker-size); height: var(--locker-size); box-sizing: border-box; border: 4px solid #68a4ff; border-radius: 50%; backdrop-filter: blur(calc(2 * 1px)); position: absolute; left: 10px; } .read-ok { border: 4px solid #fff; display: flex; justify-content: center; align-items: center; opacity: 0; } .drop { position: relative; width: var(--locker-size); height: var(--locker-size); box-sizing: border-box; border: 4px solid #68a4ff; border-radius: 50%; backdrop-filter: blur(calc(2 * 1px)); } .locker-area { position: relative; width: var(--locker-size); height: var(--locker-size); background: var(--locker-background); border-radius: 50%; box-sizing: border-box; border: 4px solid #fff; backdrop-filter: blur(calc(2 * 1px)); &::after { width: 10px; position: absolute; height: calc(var(--button-width) / 10); content: " "; background: linear-gradient(180deg, #3183ff 0%, #2379f9 100%); left: 50%; transform: translate3d(-50%, -50%, 0); top: 50%; } .locker-head { position: absolute; transform: translate3d(-50%, 30%, 0); } .locker-body { height: 40px; width: 45px; border-radius: 12px; background: linear-gradient(180deg, #f4f7fc 0%, #e3e6ea 100%); box-shadow: inset 0px 4px 6px rgb(255 255 255 / 50%), inset 0px -5px 17px rgb(0 0 0 / 40%); transform: translate3d(-1px, 100%, 0); }}}Copy the code

JS

gsap.registerPlugin(Draggable); const button = document.querySelector(".unlock-btn"); const lockerArea = button.querySelector(".locker-area"); const locker = button.querySelector(".locker"); const lockerHead = button.querySelector(".locker-head"); const lockerBody = button.querySelector(".locker-body"); const dropArea = button.querySelector(".drop"); let tl = gsap.timeline({ paused: true }); // Create the timeline tl.to(lockerBody, {y: "120%", duration: 0.2}). To (locker, {keyframes: [{rotation: -45, x: -8, transformOrigin: "Center ", duration: 0.2}, {x: -15, y: -1, duration: 0.2}, {x: -30, y: 10, duration: Opacity: 0, duration: 0}]}). To (opacity: 0, rotation: -90, duration: 0). Opacity: 0, duration: 0.1}). FromTo (".read-ok,.unlock- MSG ", {y: 0, opacity: 0, duration: 0.1}). Opacity: 0}, {opacity: 1, y: 0, duration: 0.2}); Draggable.create(lockerArea, { type: "x", bounds: button, onDrag(e) {}, onRelease(e) { if (! This.hittest (dropArea)) {gsap. To (lockerArea, {x: 0, duration: 0.6, ease: "elastic. Out (1,.75)"}); } else { this.disable(); Gsap. to(lockerArea, {x: droparea.offsetleft-9, duration: 0.6, ease: "elastic.out(1, .8)", onUpdate(e) { tl.restart(); }}); }}});Copy the code