实现 3D 倒计时器

2023-09-18 12:57:00

构建单个倒计时器卡片

实现思路

从上述的总体效果图来看,单个倒计时器的卡片主要是分为头部尾部两个部分,所以我们可以采用flex布局来实现整体的布局,并且利用flex布局实现文字内容的布局。具体实现步骤如下:

编写 HTML 结构

<div class="flip_card flip">
  <div class="top">4</div>
  <div class="bottom">4</div>
</div>

编写样式

.flip_card {
  position: relative;
  display: inline-flex;
  flex-direction: column;
  box-shadow: 0px 3px 8px #b7b7b7;
  border-radius: var(--border-radius);
}
.top,
.bottom {
  height: 0.75em;
  line-height: 1;
  padding: 0.25em;
  overflow: hidden;
}

.flip_card .top {
  border-top-left-radius: var(--border-radius);
  border-top-right-radius: var(--border-radius);
  background-color: var(--flip-card-top);
  border-bottom: 1px solid rgba(0, 0, 0, 0.1);
}

.flip_card .bottom {
  display: flex;
  align-items: flex-end;
  border-bottom-left-radius: var(--border-radius);
  border-bottom-right-radius: var(--border-radius);
  background-color: var(--flip-card-bottom);
}

完成上述代码后,单个卡片的效果如下:

在这里插入图片描述

编写单个倒计时器卡片动画

实现思路

单个倒计时器卡片的动画,我们可以 JS 动态添加新的一个倒计时器卡片,并且新的一个倒计时器卡片悬浮在卡片上方,这样在执行动画效果的时候,切换效果会比较好。而动画的执行动作选用rotateX来执行。具体的实现过程如下。

编写样式(只展示核心动画样式,布局样式跟上述步骤一致)

.flip_card .top_flip {
  position: absolute;
  width: 100%;
  animation: flip-top 250ms ease-in;
  transform-origin: bottom;
}

@keyframes flip-top {
  100% {
    transform: rotateX(90deg);
  }
}

.flip_card .bottom_flip {
  position: absolute;
  bottom: 0;
  width: 100%;
  animation: flip-bottom 250ms ease-out 250ms;
  transform-origin: top;
  transform: rotateX(90deg);
}

@keyframes flip-bottom {
  100% {
    transform: rotateX(0deg);
  }
}

结合 JS 完成倒计时器的数据切换动画测试

实现了上述的样式布局和动画后,我们的页面是看不到动画执行过程的,因为我们添加的倒计时器的动画元素是需要 JS 动态添加的,所以我们需要添加如下代码进行测试。

function test() {
  const flipCard = document.querySelector(".flip_card");
  const topHalf = flipCard.querySelector(".top");
  const bottomHalf = flipCard.querySelector(".bottom");
  const startNumber = parseInt(topHalf.textContent);

  const topFlip = document.createElement("div");
  topFlip.classList.add("top_flip");
  const bottomFlip = document.createElement("div");
  bottomFlip.classList.add("bottom_flip");

  top.textContent = startNumber;
  bottomHalf.textContent = startNumber;
  topFlip.textContent = startNumber;
  bottomFlip.textContent = 4;

  topFlip.addEventListener("animationstart", (e) => {
    topHalf.textContent = 4;
  });

  topFlip.addEventListener("animationend", (e) => {
    topFlip.remove();
  });

  bottomFlip.addEventListener("animationend", (e) => {
    bottomHalf.textContent = 4;
    bottomFlip.remove();
  });

  flipCard.append(topFlip, bottomFlip);
}

执行上述 JS 代码后,可以看到动画已经可以正常运行,我们接下来就可以实现整体布局。

在单个倒计时器卡片的基础上实现整体的布局

我们现在已经把核心的功能完成了 80%左右,现在我们就是需要把整体的倒计时器整体布局完成。具体的核心代码如下:

<div class="container">
  <!-- 小时/分/秒 元素一致,只是初始值不一样 -->
  <div class="container_segment">
    <div class="segment-title">小时</div>
    <div class="segment">
      <div class="flip_card">
        <div class="top">2</div>
        <div class="bottom">2</div>
      </div>
      <div class="flip_card" data-hours-ones>
        <div class="top">4</div>
        <div class="bottom">4</div>
      </div>
    </div>
  </div>
</div>
.container {
  display: flex;
  gap: 0.5em;
  justify-content: center;
}

.container_segment {
  display: flex;
  flex-direction: column;
  gap: 0.1em;
  align-items: center;
}

.container_segment .segment_title {
  font-size: 0.5em;
  text-align: center;
}

.segment {
  display: flex;
  gap: 0.1em;
}

.segment-title {
  font-size: 1rem;
}

实现上述代码后的效果如下:

在这里插入图片描述

实现倒计时功能

封装单个倒计时器翻牌动画函数

在上述的步骤中,我们已经编写了一个用于测试单个倒计时器卡片翻牌的 JS 函数,我们可以在这个基础上重新梳理逻辑来封装对应的动画函数。

通过分析上述测试代码,我们可以得知单个倒计时器卡片翻牌的执行最关键的有如下两个部分:

  • 单个倒计时器卡片容器

    因为此容器主要是可以获取当前倒计时的数值,并且需要动态添加对应的卡片动画 DIV 元素和初始化对应数值,元素添加完成后自动执行动画。最为重要的就是此容器中的元素需要监听动画的开始和结束时间,从而为下一次动画的数值进行赋值。

  • 下一次翻牌的新数值

    下一次翻牌的新数值不应该在此函数中来进行计算,要不然代码的耦合度会比较高,所以这里的新数值是作为参数传入的。

具体代码如下:

function flip(flipCard, newNumber) {
  const topHalf = flipCard.querySelector(".top");
  const bottomHalf = flipCard.querySelector(".bottom");
  const startNumber = parseInt(topHalf.textContent);

  if (newNumber === startNumber) return;

  const topFlip = document.createElement("div");
  topFlip.classList.add("top_flip");
  const bottomFlip = document.createElement("div");
  bottomFlip.classList.add("bottom_flip");

  top.textContent = startNumber;
  bottomHalf.textContent = startNumber;
  topFlip.textContent = startNumber;
  bottomFlip.textContent = newNumber;

  topFlip.addEventListener("animationstart", (e) => {
    topHalf.textContent = newNumber;
  });

  topFlip.addEventListener("animationend", (e) => {
    topFlip.remove();
  });

  bottomFlip.addEventListener("animationend", (e) => {
    bottomHalf.textContent = newNumber;
    bottomFlip.remove();
  });

  flipCard.append(topFlip, bottomFlip);
}

封装所有倒计时器翻牌动画

上述步骤我们已经封装了单个倒计时器翻牌,但是整个倒计时器是由很多单个翻牌卡片组成的,为了能够方便维护代码,所以我们可以把所有倒计时翻牌动画封装到一个函数中进行控制,然后调用上一个步骤我们编写的flip函数。

为了方便能够获取到时分秒的卡片容器和对齐进行操作,我们可以在卡片容器中添加data-[hours | minutes | seconds]-tensdata-[hours | minutes | seconds]-ones属性来方便获取元素,具体的实例代码如下:

<div class="segment">
  <div class="flip_card" data-hours-tens>
    <div class="top">2</div>
    <div class="bottom">2</div>
  </div>
  <div class="flip_card" data-hours-ones>
    <div class="top">4</div>
    <div class="bottom">4</div>
  </div>
</div>
/**
 * 对所有单个倒计时器卡片进行数值设置
 * @param {Number} time 当前倒计时的时间(毫秒值)
 */
function flipAllCards(time) {
  const seconds = time % 60;
  const minutes = Math.floor(time / 60) % 60;
  const hours = Math.floor(time / 3600);

  flip(document.querySelector("[data-hours-tens]"), Math.floor(hours / 10));
  flip(document.querySelector("[data-hours-ones]"), hours % 10);
  flip(document.querySelector("[data-minutes-tens]"), Math.floor(minutes / 10));
  flip(document.querySelector("[data-minutes-ones]"), minutes % 10);
  flip(document.querySelector("[data-seconds-tens]"), Math.floor(seconds / 10));
  flip(document.querySelector("[data-seconds-ones]"), seconds % 10);
}

编写定时函数定时执行动画

通过上述两个步骤的铺垫,我们现在就只差编写一个定时器来调用flipAllCards函数。具体实例代码如下:

const countToDate = new Date().setHours(new Date().getHours() + 24); // 程序执行时的时间点

setInterval(() => {
  const currentDate = new Date();
  const timeBetweenDates = Math.ceil((countToDate - currentDate) / 1000); // 计算时间差
  flipAllCards(timeBetweenDates);
}, 250);

实例代码下载

实例代码下载

更多推荐

分享一个springboot+uniapp基于微信小程序的校医务室健康服务系统源码 lw 调试

💕💕作者:计算机源码社💕💕个人简介:本人七年开发经验,擅长Java、Python、PHP、.NET、微信小程序、爬虫、大数据等,大家有这一块的问题可以一起交流!💕💕学习资料、程序开发、技术解答、文档报告💕💕如需要源码,可以扫取文章下方二维码联系咨询💕💕JavaWeb项目💕💕微信小程序项目💕💕

java学习--day18(TreeSet底层&内部类)

文章目录1.二叉树的了解2.使用比较器将数据存储到TreeSet中3.匿名内部类3.1基于抽象类的匿名内部类3.2基于接口的匿名内部类4.内部类4.1成员内部类昨天总结:ArrayList:就是单纯的addLinkedList:也是单纯的addHashSet:不单纯得重写equals和hashCode方法TreeSet

本地搭建CFimagehost私人图床——“cpolar内网穿透”

文章目录1.前言2.CFImagehost网站搭建2.1CFImagehost下载和安装2.2CFImagehost网页测试2.3cpolar的安装和注册3.本地网页发布3.1Cpolar临时数据隧道3.2Cpolar稳定隧道(云端设置)3.3.Cpolar稳定隧道(本地设置)4.公网访问测试5.结语1.前言图片服务器

Docker ~ 从入门到入坑。

Docker~从入门到入坑。文章目录Docker~从入门到入坑。Docker实用篇。0.学习目标。1.初识Docker。1.1.什么是Docker。1.1.1.应用部署的环境问题。1.1.2.Docker解决依赖兼容问题。1.1.3.Docker解决操作系统环境差异。1.1.4.小结。1.2.Docker和虚拟机的区别

2023/9/20总结

mavenmaven本质是一个项目管理工具将项目开发和管理过程抽象成一个项目对象模型(POM)POM(ProjectObjectModel)项目对象模型作用项目构建提供标准的自动化项目构建方式依赖管理方便快捷的管理项目依赖的资源(jar包),避免资源间的版本冲突问题统一开发结构提供标准的、统一的项目结构安装Maven–

二、链表(linked-list)

文章目录一、定义二、经典例题(一)[21.合并两个有序链表](https://leetcode.cn/problems/merge-two-sorted-lists/description/)1.思路2.复杂度分析3.注意4.代码(二)[86.分割链表](https://leetcode.cn/problems/par

【错误记录】Android Studio 中最新的 Gradle 配置中设置插件依赖 ( 2023 年 8 月 24 日 | 最新 Gradle 中配置插件依赖的变化 | 增加 Maven 仓库源 )

文章目录一、最新Gradle中配置插件依赖的变化二、报错信息三、增加Maven仓库源五、使用老版本方式导入插件一、最新Gradle中配置插件依赖的变化当前最新的AndroidStudio开发环境,生成的Gradle配置脚本使用了最新API,用起来不太习惯;根目录下的build.gradle构建脚本变成了下面的样式,单纯

【错误记录】Android Studio 创建 Module 模块报错 ( Cannot resolve external dependency org.jetbrains.kotlin:kotl )

文章目录一、报错信息二、解决方案目前使用的是最新的Gradle配置,创建Module生成的源码与Gradle配置出现了冲突,导致的问题;解决此类问题,要仔细检查Gradle构建脚本,排查每个依赖库的来源;本次错误就是AS系统自动成的Module修改了Gradle构建脚本,导致依赖下载失败;一、报错信息在AndroidS

亚马逊云科技打造SAP核心业务系统上云最佳实践,加快业务转型和价值实现

数字化转型步入深水区,企业竞争日益激烈,乘云而上、快速进行现代化转型和创新,才能不断紧跟趋势变化,实现「高质量发展」。作为亚马逊云科技全球战略合作伙伴,SAP和亚马逊云科技的联合创新已超过15年,双方共同为SAP客户的关键业务型工作负载提供支持,持续助力企业数字化转型。如今,全球有数以千计的企业将SAP核心业务系统运行

前端工程师路上的宝藏:不可错过的进阶必读文章!

JavaScript《javascript高级程序设计》核心知识总结必要性:⭐️⭐️⭐️⭐️难度:⭐️⭐️⭐️⭐️谏言:建议初学者先读一两遍红宝石书(即JavaScript高级程序设计),犀牛书可以暂时不看(读起来有点累)ES6入门教程必要性:⭐️⭐️⭐️⭐️⭐️难度:⭐️⭐️⭐️⭐️⭐️谏言:阮大佬的开源精品,强推!

如何在Ubuntu中挂载新硬盘

参考:如何在Ubuntu中挂载新硬盘_笔记大全_设计学院ubuntu将大于2T硬盘挂载到/home目录并使其永久生效的详细操作步骤_ubuntu挂载硬盘到home目录_菲玛的博客-CSDN博客win10+ubuntu18.04home目录扩容方法_ubuntu增加home空间_cfreeze的博客-CSDN博客1、检测

热文推荐