深度工作计时器:从零实现一个环形倒计时组件
深度工作计时器:从零实现一个环形倒计时组件
1小时前 2 阅读
  • 首页
  • /
  • 分享
  • /
  • 正文
  • 环形进度条、精确秒级倒计时、Web Audio 提示音——这是一个完整可用的计时器组件,核心逻辑不到 100 行。

    这个计时器基于番茄工作法的理念设计,有四个预设时长可选,环形进度条随时间实时推进,完成后触发提示音。所有代码没有依赖任何外部库,用原生 HTML/CSS/JavaScript 实现。


    效果演示

    完整代码如下,可直接保存为 .html 文件在浏览器打开:


    核心原理解析

    1. 环形进度条:SVG + stroke-dasharray

    这是计时器最关键的部分。SVG 的 有几个控制描边的属性:

    <svg viewBox="0 0 200 200">
      <circle cx="100" cy="100" r="90" class="timer-bg"/>
      <circle cx="100" cy="100" r="90" class="timer-progress" id="progressCircle"/>
    </svg>

    stroke-dasharray 把一条连续的描边切成等长的虚线段,stroke-dashoffset 控制虚线的起点偏移量:

    const CIRCUMFERENCE = 2 * Math.PI * 90; // 周长 ≈ 565.49
    progressCircle.style.strokeDasharray = CIRCUMFERENCE; // 虚线段长度 = 整条圆周
    progressCircle.style.strokeDashoffset = 0;            // 起点偏移 = 0,全部可见

    当倒计时进行时:

    // 剩余 50% 时间 → offset 为周长的一半 → 可见长度 = 周长 * 50%
    const offset = CIRCUMFERENCE * (1 - remainingSeconds / totalSeconds);
    progressCircle.style.strokeDashoffset = offset;

    transition: stroke-dashoffset 1s linear 让每次偏移变化平滑过渡——不需要 JavaScript 控制动画帧,CSS 帮你自动补间。

    2. 倒计时逻辑:setInterval + 状态管理

    let totalSeconds = 25 * 60;     // 总秒数
    let remainingSeconds = totalSeconds; // 剩余秒数
    let timerInterval = null;        // setInterval 返回的 ID
    let isRunning = false;           // 运行状态锁
    
    timerInterval = setInterval(() => {
      remainingSeconds--;             // 每秒减 1
      updateDisplay();               // 更新数字显示
      updateProgress();              // 更新环形进度
    
      if (remainingSeconds <= 0) {
        clearInterval(timerInterval); // 时间到,停止计时器
        timerInterval = null;
        playSound();                 // 触发提示音
      }
    }, 1000);

    关键点:状态锁 isRunning 防止计时器重复启动。如果用户快速连点"开始"按钮,第二个 startTimer() 调用会因为 isRunning === true 直接 return,而不会创建第二个 setInterval。

    3. 预设按钮:选中状态 + 锁定运行中状态

    function setPreset(minutes, btn) {
      if (isRunning) return;  // 计时中不允许切换时长
    
      totalSeconds = minutes * 60;
      remainingSeconds = totalSeconds;
      updateDisplay();
    
      // 清除所有按钮的 active 状态,给当前按钮加 active
      document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
      btn.classList.add('active');
    
      progressCircle.style.strokeDashoffset = 0; // 重置环形进度
    }

    "计时中不允许切换时长"是一个有意为之的设计决策:强制用户为每一个计时周期做出明确的选择,减少随意性带来的仪式感。

    4. 提示音:Web Audio API

    const ctx = new (window.AudioContext || window.webkitAudioContext)();
    const osc = ctx.createOscillator();  // 声音振子
    const gain = ctx.createGain();       // 音量控制器
    
    osc.connect(gain);       // 振子 → 音量控制 → 扬声器
    gain.connect(ctx.destination);
    
    osc.frequency.value = 880;           // 频率 880Hz(A5 音符)
    gain.gain.setValueAtTime(0.3, ctx.currentTime);           // 起始音量
    gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.8); // 0.8秒内指数衰减到静音
    
    osc.start(ctx.currentTime);   // 立即开始
    osc.stop(ctx.currentTime + 0.8); // 0.8秒后停止

    频率 880Hz 听起来是一个干净的高音 A,exponentialRampToValueAtTime 实现自然的声音衰减,不需要音频文件,一个 API 调用就能合成出来。


    数据结构

    整个组件只有两个核心状态变量:

    let totalSeconds = 25 * 60;      // 当前设定的总时长(秒)
    let remainingSeconds = totalSeconds; // 倒计时剩余秒数

    其他所有变量都是这两者的衍生:

    变量类型说明
    CIRCUMFERENCE常量圆的周长(px),计算进度偏移量用
    timerIntervalnumber\nullsetInterval 的 ID,null 表示未运行
    isRunningboolean运行状态锁,防止重复启动
    phrases[]string[]口号文案数组,随机选一条显示

    可调节参数

    参数默认值作用
    CIRCUMFERENCE2π × 90根据 SVG 圆圈半径计算,调整半径时同步修改
    25 * 601500 秒默认预设时长(25 分钟)
    phrases[]5 条口号计时开始时随机显示的文案
    osc.frequency.value880提示音频率(Hz),越高越尖锐

    实际用途

    这个计时器可以嵌入多种场景:

    博客侧边栏:作为南枝已谢或荣小站的挂件工具,读者在阅读文章时可以顺手开启一个专注计时

    个人首页:代替传统的"联系我"区块,用一个实用的工具留住访客

    团队看板:在内部工具中作为番茄工作法提醒,减少无效会议

    0

    评语 (0)

    取消