做个交通信号灯特效

概述

实现现实生活中的交通信号灯的切换,可能我们第一想法是使用定时器去切换,但是会存在一个问题,凡是使用到计时器的,都是不精准的,单次计时还好,但是会随着时间的推移,误差会被逐渐放大,会变得越来越不精确,因此下面我们使用问询模式来做这个功能。

计时器问题

前端计时器不精准的原因有如下:

  • JavaScript 的单线程特性 :浏览器中 JavaScript 运行在单线程环境中,定时器(如 setTimeout/setInterval)的回调需要等待主线程空闲时才会执行。
  • 事件循环的优先级:用户交互、页面渲染等任务的优先级高于定时器回调。如果主线程被阻塞(如执行长任务),定时器会被延迟,即使时间到了也无法立即执行。

具体可阅读下 John Resig(jQuery 作者)的这篇文章How JavaScript Timers Work

requestAnimationFrame

由于 setTimeout 和 setInterval 的不精准问题,促使了 requestAnimationFrame 的诞生。 requestAnimationFrame 是专门为实现高性能的帧动画而设计的一个API,目前已在多个浏览器得到了支持,你可以把它用在 DOM 上的效果切换或者 Canvas 画布动画中。 requestAnimationFrame 并不是定时器,但和 setTimeout 很相似,在没有 requestAnimationFrame 的浏览器一般都是用setTimeout模拟。 requestAnimationFrame 跟屏幕刷新同步(大多数是 60Hz )。确保动画帧率与屏幕刷新率一致。

实现

效果

目录结构

js 复制代码
|------------------index.html
|------------------ index.js

程序

index.html

js 复制代码
class TrafficLight {
  constructor(lights) {
    this.lights = lights;
    this.currentIndex = 0; //当前信号灯的下标
    this.switchTime = Date.now(); //切换到当前信号灯时间点
  }
  get current() {
    return this.lights[this.currentIndex];
  }
  get disTime() {
    return Date.now() - this.switchTime;
  }
  render(fn) {
    requestAnimationFrame(this.render.bind(this, fn));
    const current = this.getCurrentLight();
    fn(current);
  }

  //   更新信号灯状态
  update() {
    while (true) {
      if (this.disTime < this.current.latest) {
        break; //当前信号灯的剩余时间>0,则不切换
      } else {
        this.currentIndex =
          this.currentIndex + 1 > this.lights.length - 1
            ? 0
            : this.currentIndex + 1;
        this.switchTime = Date.now(); //更新切换时间
      }
    }
  }

  getCurrentLight() {
    this.update();
    return {
      color: this.current.color,
      remain: this.current.latest - this.disTime,
    };
  }
}

index.html

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Document</title>
    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            background-color: #f0f0f0;
            font-family: Arial, sans-serif;
        }

        #traffic-light {
            width: 120px;
            background-color: #333;
            border-radius: 10px;
            padding: 20px;
            box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
            position: relative;
        }

        .light {
            width: 100px;
            height: 100px;
            border-radius: 50%;
            margin: 10px auto;
            opacity: 0.3;
            transition: opacity 0.3s;
            position: relative;
        }

        #red {
            background-color: red;
        }

        #yellow {
            background-color: yellow;
        }

        #green {
            background-color: green;
        }

        .active {
            opacity: 1;
            box-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
        }

        .countdown {
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 30px;
            font-weight: bold;
            color: white;
            text-shadow: 0 0 5px rgba(0, 0, 0, 0.8);
        }

        .controls {
            margin-top: 30px;
            text-align: center;
        }

        button {
            padding: 8px 15px;
            margin: 0 5px;
            cursor: pointer;
            background-color: #4CAF50;
            color: white;
            border: none;
            border-radius: 4px;
        }

        button:hover {
            background-color: #45a049;
        }

        .timer-display {
            text-align: center;
            margin-top: 20px;
            font-size: 24px;
            font-weight: bold;
        }

        .red #red,
        .yellow #yellow,
        .green #green {
            opacity: 1;
            box-shadow: 0 0 20px rgba(255, 255, 255, 0.8);
        }

        .time {
            color: #fff;
            font-size: 25px;
        }
    </style>
</head>

<body>
    <div id="traffic-light">
        <div class="light red" id="red">
        </div>
        <div class="light yellow" id="yellow">
        </div>
        <div class="light green" id="green">
        </div>

        <div class="time"></div>
    </div>
    <script src="./index.js"></script>
    <script>

        const trafficLight = document.querySelector('#traffic-light');
        const timeWrapper = document.querySelector('.time');
        const light = new TrafficLight([
            {
                color: 'red',
                latest: 7000
            },
            {
                color: 'yellow',
                latest: 3000
            },
            {
                color: 'green',
                latest: 10000
            },
        ])

        light.render((current) => {
            console.log('current-', current)
            trafficLight.className = `${current.color}`;
            timeWrapper.textContent = (current.remain / 1000).toFixed(0) + "s"
        })
    </script>
</body>

</html>
相关推荐
_阿南_7 小时前
项目中新增给AI制定的代码规范
前端·程序员
名字还没想好☜7 小时前
React 实现暗黑模式切换:localStorage 持久化、SSR 首屏闪烁与跟随系统主题
前端·javascript·react.js·ecmascript·react·next.js
自动化监测Learner8 小时前
主流 Web 端地图引擎对比:选型指南与优劣分析
javascript
console.log('npc')9 小时前
Git 冲突与 AI 协助指南
前端·人工智能·git·大模型
爱学堂IT分享10 小时前
Cesium可视化系统实战课程-Cesium教程学习
前端
糖墨夕10 小时前
理解大语言模型:Agent 的“大脑”
前端·agent
Rain的Java大神之路10 小时前
JavaWeb开发如何解决跨域问题
java·前端·后端·nginx·web安全·面试·运维开发
cpolar技术支持11 小时前
浏览器也能跑本地 AI:用 Transformers.js + WebGPU 做一个最小推理 Demo,cpolar 给同事远程体验
前端·ai·cpolar·webgpu·transformers.js
计算机魔术师12 小时前
OpenAI 发布 GPT-6 Astra:多项基准刷新纪录, cybersecurity 能力达 Critical 阈值
前端
xy345312 小时前
Axure9.0中继器遮罩实现方法
前端·ui·html·axure·原型·产品设计