练习动画最好的方式(一):锁链按钮

练习动画最好的方式(一):锁链按钮

你能相信这张图片是由纯CSS绘画的吗?

截屏2021-05-26 下午3.29.04.png

这是国外一位大师的作品,作者并把整个制作的过程都录制了下来,虽然不知道总共花了多少时间,但这绝对是一个惊人的作品

我知道很多人会觉得,『对呀很酷,但是怎么实现的呢?』。

我也不懂为什么,但知道能利用CSS 做出这种极限真的很令人感到兴奋。光是观察他的代码就可以学到不少技巧,

虽然试着理解高手如何做到是能吸收不少经验,但还是会想要自己动手做点什么,好在我又发现了另一个稍微平易近人的高手- Aaron Iker,他大多的作品都围绕在一个网页上不可缺乏,但鲜少被人拿来做文章的元件- "按钮"。

按钮,几乎所有网页都会用到它,但就是拿来触发一些动作,被触发的动作才是我们在意的,很少会在上头多作着墨,顶多加个Hover 变色或位移就很差不多了。

但看看下面这个实例:

2021-05-26 16.21.46.gif

一点小巧思,瞬间就让按钮活了起来。

而且因为范围限缩在了按钮的大小,就算动画稍微华丽一些也不会对整体页面造成太多干扰。

受到Aaron启发,趁着空闲时间我也试着做了一个按钮动画,今天这篇文章就分享一下过程中使用到的工具与眉角!

灵感来源

这次的按钮动画主要修改自Dribbble上YorKun的作品- Button Lock Animation,感谢作者还有附上源码档案,让我能更轻松的参照。

2021-05-26 16.18.12.gif

不过我并没有完全照着原作的动画制作,主要是想多试试一些不同的动画组合,接下来我会一一介绍。

动画实作

我一开始想达到的动画有四项:

  1. 滑动解锁
  2. 锁头开启与掉落
  3. 对应开锁状态的动画
  4. 锁头拖拉时的2D 物理效果

理论上应该是很快就能完成,但因为对GSAP不熟,走了很多冤枉路,导致最后只完成了前三项的效果,算是差强人意。

用到的工具主要是GSAP与GSAP的Draggable plugin

滑动解锁

GSAP的Draggable plugin真的有够简单好用,只要给定想要启动Draggable的DOM物件,并指定要拖拉的方向(type)与范围(bounds),就能瞬间完成这样的效果(demo由此去):

// 注册GSAP的draggable插件
gsap.registerPlugin(Draggable);

// 选择需要与queryselector交互的DOM
const button = document.querySelector(".unlock-btn");
const lockerArea = button.querySelector(".locker");
const dropArea = button.querySelector(".drop");

// 主要的可拖动的实例
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();
        }
      });
    }
  }
});
复制代码

中间可以看到,我们指定typex,表示移动方向为x轴,而boundsbuttonDOM物件,所以最多不会拖移超过butotn的范围。

另外,影片中有一个效果是当你拖拉到前后两端点的时候,会有一个吸力把拖移中的物件吸过去,这段其实是需要靠额外的两个动画效果来达成。

Draggable.create()可以传入的Option中,能指定onDragonReleas,在onRelease的时候我们可以透过this.hitTest(dropArea)这个内建的函式判断拖拉中的物件是否触碰到另一个指定的DOM物件,若还没碰到,我们就拉回到起点,也就是这段所做的事:

gsap.to(lockerArea, {
  x: 0,
  duration: 0.6,
  ease: "elastic.out(1, .75)"
});
复制代码

透过gsap.to可以让指定的DOM物件变换到我们传入的property状态,以此例子来说就是位移到原点,等同于apply transform:translateX(0)

而若触碰到指定物件,则可以调整x来将拖移物件直接拉到指定物件,这样就能制造出吸力的效果。

此外,在触碰到物件后的gsap.to函式中,我们也传入了OnUpdate,该OnUpdate会在动画完成后被触发,刚好让我们能接着下一阶段的动画-锁头开启与掉落。

锁头开启与掉落

当拖移物件触碰到指定物件时,onUpdate会被触发:

onUpdate(e) {
  tl.restart();
}
复制代码

onUpdate中我们放的是一个Timeline物件,它能让我们进行序列动画,一步步指定各个物件该如何依序执行动画。

由于我是将整个timeline动画定义在别处,所以当onUpdate被触发时是呼叫tl.restart(),你也可以直接定义在handler里面。

Timeline 使用方法一样简单:

let tl = gsap.timeline({ paused: true }); //创建时间表
复制代码

先创建一个timeline物件,这边传入{ paused: true }是因为我希望在之后才触发他(上述所说,在拖移物件移动到指定区域后才触发),所以先预设让他暂停,这样我们在onUpdate时再呼叫restart()即可。

题外话,一开始我并不是用Timeline而是在每个gsap.toonUpdate中去呼叫另一个gsap.to,这样虽然也是可行,但让程式码可读性降低很多,最终我才改成用Timeline来串接序列动画。

接着就是针对每个我们想要触发动画的DOM 物件设定欲变化的值:

先让整个锁头的身体部分往下位移,让上面铁环部分保持原地,造出开锁的效果。

2021-05-26 16.18.12.gif

tl.to(lockerBody, {
  y: "120%",
  duration: 0.2
})
复制代码

接着利用keyframes针对单一物件进行一连串较为细致的动画,这边主要是要将整个锁头(包含身体与铁环部分)进行位移与旋转,营造出锁头打开并从锁上拿掉的动画:

tl.to(lockerBody, { /*...略*/ })
  .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
      }
    ]
  })
复制代码

20dcd26375b64868b3c77b5fb6f25730.gif

接着也是差不多的步骤,一步步对其他的DOM物件加上最后的-对应开锁状态的动画,替换掉UNLOCK字样:

tl.to(lockerBody, { /*...略*/ })
  .to(locker, { /*...略*/ })
  .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
    }
  );
复制代码

注意到的是我们除了传入DOM object给gsap.to与gsap.fromTo外,也能直接指定class name,非常方便。

就这样几行代码,就做好了一个套用在按钮上的动画,应该还算是不错吧!

结论

今天简单练习了一下从Dribbble上找灵感然后用前端技术将动画实作出来的过程,或许没有什么新的东西,但希望能给大家带来点启发,

我自己平常没事在家做点有趣的动画或CSS,自娱一下!

完整代码:

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.5L10 16L24 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.6032V35H4V16.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 26.8431C27.1819 26.8431 27.6296 26.3954 27.6296 25.8431H25.6296ZM8.37036 35V16.6032H6.37036V35H8.37036ZM8.37036 16.6032C8.37036 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 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 6.3385 17.1689 6.3385V8.3385ZM25.6296 16.6032V25.8431H27.6296V16.6032H25.6296Z" fill="#7B8698" />
        </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-Animation</a></p>
</footer>
复制代码

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);
    }
  }
}
复制代码

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: 0.2
      },
      {
        y: 100,
        opacity: 0,
        duration: 0.2
      }
    ]
  })
  .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
    }
  );

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();
        }
      });
    }
  }
});
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端仙人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值