数字时钟翻页效果

javascript 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="UTF-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
  <title>Flip Clock</title>
  <style>
    body {
      background: #111;
      display: flex;
      justify-content: center;
      align-items: center;
      height: 100vh;
      margin: 0;
      font-family: 'Courier New', monospace;
      color: white;
    }

    .clock {
      display: flex;
      gap: 20px;
    }

    .card-container {
      width: 80px;
      height: 120px;
      position: relative;
      perspective: 500px;
      background: #2c292c;
      border-radius: 8px;
      box-shadow: 0 4px 12px rgba(0,0,0,0.5);
    }

    /* 中间分割线 */
    .card-container::before {
      content: "";
      position: absolute;
      left: 0;
      top: 50%;
      width: 100%;
      height: 4px;
      background: #120f12;
      z-index: 10;
    }

    .card-item {
      position: absolute;
      width: 100%;
      height: 50%;
      left: 0;
      overflow: hidden;
      background: #2c292c;
      color: white;
      text-align: center;
      font-size: 64px;
      font-weight: bold;
      backface-visibility: hidden;
      transition: transform 0.4s ease-in-out;
    }

    /* 下层数字:初始对折(背面朝上) */
    .card1 { /* 下层上半 */
      top: 0;
      line-height: 120px; /* 整体高度对齐 */
    }
    .card2 { /* 下层下半 */
      top: 50%;
      line-height: 0;
      transform-origin: center top;
      transform: rotateX(180deg);
      z-index: 2;
    }

    /* 上层数字:当前显示 */
    .card3 { /* 上层上半 */
      top: 0;
      line-height: 120px;
      transform-origin: center bottom;
      z-index: 3;
    }
    .card4 { /* 上层下半 */
      top: 50%;
      line-height: 0;
      z-index: 1;
    }

    /* 翻页动画触发 */
    .flip .card2 {
      transform: rotateX(0deg);
    }
    .flip .card3 {
      transform: rotateX(-180deg);
    }

    /* 冒号分隔符 */
    .colon {
      font-size: 64px;
      display: flex;
      align-items: center;
      color: #aaa;
    }
  </style>
</head>
<body>
  <div class="clock">
    <div class="card-container flip" id="hour" data-number="00">
      <div class="card1 card-item">00</div>
      <div class="card2 card-item">00</div>
      <div class="card3 card-item">00</div>
      <div class="card4 card-item">00</div>
    </div>

    <div class="colon">:</div>

    <div class="card-container flip" id="minute" data-number="00">
      <div class="card1 card-item">00</div>
      <div class="card2 card-item">00</div>
      <div class="card3 card-item">00</div>
      <div class="card4 card-item">00</div>
    </div>

    <div class="colon">:</div>

    <div class="card-container flip" id="second" data-number="00">
      <div class="card1 card-item">00</div>
      <div class="card2 card-item">00</div>
      <div class="card3 card-item">00</div>
      <div class="card4 card-item">00</div>
    </div>
  </div>

  <script>
    function setHTML(dom, nextValue) {
      const curValue = dom.dataset.number;
      if (nextValue === curValue) return;

      // 更新 DOM 结构:下层为新值,上层为旧值
      dom.innerHTML = `
        <div class="card1 card-item">${nextValue}</div>
        <div class="card2 card-item">${nextValue}</div>
        <div class="card3 card-item">${curValue}</div>
        <div class="card4 card-item">${curValue}</div>
      `;

      // 触发重绘以重启动画
      dom.classList.remove('flip');
      void dom.offsetWidth; // 强制重排
      dom.classList.add('flip');

      dom.dataset.number = nextValue;
    }

    function updateClock() {
      const now = new Date();
      const h = String(now.getHours()).padStart(2, '0');
      const m = String(now.getMinutes()).padStart(2, '0');
      const s = String(now.getSeconds()).padStart(2, '0');

      setHTML(document.getElementById('hour'), h);
      setHTML(document.getElementById('minute'), m);
      setHTML(document.getElementById('second'), s);
    }

    // 初始化
    updateClock();
	// setTimeout(updateClock,1000)
	
    setInterval(updateClock, 1000);
  </script>
</body>
</html>

这个翻页时钟(Flip Clock)通过 CSS 3D 变换 + 动画类切换 + DOM 内容动态更新 的方式,模拟了类似机械翻页牌的效果。下面从结构、样式和逻辑三方面详细分析其实现原理:


🔧 一、HTML 结构设计

每个时间单位(小时、分钟、秒)由一个 .card-container 容器表示,内部包含 4 个 .card-item 元素

html 复制代码
<div class="card-container" id="second">
  <div class="card1">00</div> <!-- 下层上半 -->
  <div class="card2">00</div> <!-- 下层下半(初始翻转180°)-->
  <div class="card3">00</div> <!-- 上层上半(当前显示)-->
  <div class="card4">00</div> <!-- 上层下半 -->
</div>

四个卡片的作用:

  • .card3.card4:组成当前显示的数字(上半+下半),正常显示。
  • .card1.card2 :组成即将翻出的新数字,但初始时 .card2rotateX(180deg) 翻转到背面(不可见)。
  • 中间有一条 ::before 伪元素作为"折痕",增强翻页视觉效果。

🎨 二、CSS 样式与 3D 翻转原理

关键 CSS 技术点:

1. 3D 空间设置
css 复制代码
.card-container {
  perspective: 500px; /* 创建 3D 视角 */
}
  • perspective 让子元素的 3D 变换有景深感。
2. 上下两半的定位与旋转轴
css 复制代码
.card2 {
  transform-origin: center top;
  transform: rotateX(180deg); /* 初始翻到背面 */
}
.card3 {
  transform-origin: center bottom;
}
  • .card2顶部边缘旋转 180°,藏在下方背面。
  • .card3底部边缘旋转,用于向上翻折。
3. 翻页动画(通过 .flip 类触发)
css 复制代码
.flip .card2 {
  transform: rotateX(0deg); /* 展开新数字下半部分 */
}
.flip .card3 {
  transform: rotateX(-180deg); /* 当前数字上半部分向上翻折隐藏 */
}
  • 动画持续 0.4s,使用 ease-in-out 缓动。
  • .card1.card4 始终保持静态,作为背景支撑。

视觉效果

  • 上半部分(.card3)向上翻走(像书页翻开)
  • 下半部分(.card2)从背面转正,露出新数字
  • 中间的"折痕"让翻页更真实

⚙️ 三、JavaScript 动态更新逻辑

核心函数:setHTML(dom, nextValue)

步骤分解:
  1. 对比新旧值:如果相同,不更新(避免无谓动画)。

  2. 重写整个容器的 HTML

    • 下层(新值).card1.card2 显示 nextValue
    • 上层(旧值).card3.card4 显示 curValue
  3. 触发动画

    js 复制代码
    dom.classList.remove('flip');
    void dom.offsetWidth; // 强制浏览器重排(关键!)
    dom.classList.add('flip');
    • 先移除 .flip,再强制重排(flush styles),再加回 .flip,确保动画重新触发。
  4. 更新 data-number 保存当前值。

时间更新:

  • 每秒调用 updateClock(),获取当前时分秒(两位数格式)。
  • 分别调用 setHTML 更新三个容器。

🌟 四、为什么能实现"翻页"错觉?

元素 初始状态 翻页后状态 视觉作用
.card3 显示旧数字上半 向上翻转 180° 隐藏 模拟"翻走"的上半页
.card2 旧数字下半(翻转180°藏起) 转正显示新数字下半 模拟"翻出"的下半页
.card1 / .card4 静态背景 不变 提供视觉连续性

💡 关键技巧

  • 利用 两个完整数字(新+旧)叠加,通过控制上下半部分的旋转,制造"翻页"而非"淡入淡出"。
  • 强制重排(offsetWidth 是确保 CSS 动画每次都能重新触发的经典 hack。

✅ 总结

这个 Flip Clock 的精妙之处在于:

  1. 结构设计:4 个卡片分工明确,上下层分离。
  2. CSS 3D :利用 rotateX + transform-origin 实现真实翻页。
  3. JS 控制:动态替换内容 + 巧妙触发动画。
  4. 性能优化:仅在值变化时更新,避免无效渲染。

这是一种典型的 "用 2D DOM 模拟 3D 物理效果" 的前端动画范例,既高效又视觉惊艳。

相关推荐
Van_Moonlight18 小时前
RN for OpenHarmony 实战 TodoList 项目:今日任务数量统计
javascript·开源·harmonyos
xkxnq19 小时前
第一阶段:Vue 基础入门(第 13天)
前端·javascript·vue.js
赵民勇19 小时前
ES5中prototype和prototype.constructor详解
javascript
Van_captain19 小时前
rn_for_openharmony常用组件_Tabs选项卡
javascript·开源·harmonyos
赵民勇19 小时前
ES6中的const用法详解
javascript·es6
霍理迪19 小时前
CSS布局方式——弹性盒子(flex)
前端·css
Van_captain19 小时前
React Native for OpenHarmony Toast 轻提示组件:自动消失的操作反馈
javascript·开源·harmonyos
Van_captain19 小时前
React Native for OpenHarmony Modal 模态框组件:阻断式交互的设计与实现
javascript·开源·harmonyos
xkxnq19 小时前
第一阶段:Vue 基础入门(第 14天)
前端·javascript·vue.js