طراحی تایمر پومودورو پیشرفته با صدا و انیمیشن حلقه‌ای

ساخت تایمر پومودوروی شخصی با HTML، CSS و جاوااسکریپت خالص؛ دارای حلقه پیشرفت SVG، سه حالت تمرکز و استراحت، و زنگ هشدار صوتی بدون نیاز به اینترنت.

⚡ خلاصه در یک نگاه:

  • کد کامل تک‌فایلی بدون نیاز به نصب هیچ کتابخانه‌ای
  • تولید صدای زنگ هشدار با Web Audio API داخلی مرورگر
  • دارای حالت‌های کار، استراحت کوتاه، استراحت بلند و شمارنده روزانه
🎧 خلاصه صوتی مقاله (پادکست ۲ دقیقه‌ای):

فرصت مطالعه ندارید؟ چکیده این مطلب را همین‌جا گوش دهید:

مدیریت زمان و تمرکز روی کارهای عمیق، یکی از بزرگ‌ترین چالش‌های دانشجویان، برنامه‌نویسان و افراد دورکار است. روش پومودورو یکی از اثبات‌شده‌ترین روش‌ها برای افزایش بهره‌وری است که در آن زمان به بازه‌های تمرکز ۲۵ دقیقه‌ای و استراحت‌های کوتاه ۵ دقیقه‌ای تقسیم می‌شود. اگر دوست دارید یک ابزار اختصاصی، زیبا و کاملاً سبک برای مدیریت زمان خود داشته باشید که بدون نیاز به اینترنت و ابزارهای سنگین روی هر مرورگری اجرا شود، ساخت این پروژه بهترین گزینه است.

در این مقاله قصد داریم کدهای کامل یک تایمر پومودوروی پیشرفته را بررسی کنیم که سه حالت مختلف (کار، استراحت کوتاه و استراحت بلند) دارد، یک حلقه پیشرفت گرافیکی با SVG ترسیم می‌کند، تعداد چرخه‌های تکمیل‌شده در طول روز را می‌شمارد و از همه مهم‌تر، با استفاده از قابلیت داخلی Web Audio API مرورگر، صدای هشدار پایان زمان را بدون نیاز به دانلود یا آپلود هیچ فایل صوتی خارجی تولید می‌کند. این ابزار شباهت زیادی به ابزارهای مدرن تولیدی دارد و همان‌طور که گاهی برای مدیریت اطلاعات خود به ابزارهایی مانند ساخت جعبه لایتنر دیجیتال در اکسل نیاز پیدا می‌کنید، داشتن یک تایمر اختصاصی هم تمرکز شما را متحول می‌کند.

مشخصات فنی و ویژگی‌های ابزار پومودورو

ویژگیمقدار / فناوری
تکنولوژی‌هاHTML5, CSS3, Vanilla JavaScript
ساختار فایلتک‌فایلی (Single File) بدون نیاز به فایل جانبی
تولید صداWeb Audio API داخلی مرورگر
گرافیک و انیمیشنحلقه پیشرفت داینامیک با SVG
وضعیت دسترسی ایرانکاملاً آزاد و بدون محدودیت (آفلاین)

چرا ساخت یک تایمر پومودوروی اختصاصی؟

بسیاری از اپلیکیشن‌های آنلاین پومودورو پر از تبلیغات هستند، سرعت بارگذاری پایینی دارند یا ممکن است حواس شما را با عناصر اضافی پرت کنند. با نوشتن یک کد ساده‌ی فرانت‌اند، شما صاحب ابزاری می‌شوید که روی سیستم شخصی شما، لپ‌تاپ یا حتی گوشی هوشمندتان به صورت آفلاین کار می‌کند. این پروژه نیازی به نصب هیچ پکیج اضافه‌ای (مثل Node.js یا کتابخانه‌های سنگین) ندارد و تمام کدهای ساختار، ظاهر و منطق آن در یک فایل واحد قرار گرفته‌اند.

از منظر فنی، این پروژه سه تکنولوژی کلیدی وب را به کار می‌گیرد: ساختار انعطاف‌پذیر با HTML5، طراحی واکنش‌گرا و انیمیشن‌های نرم با CSS3، و مدیریت زمان دقیق به همراه تولید فرکانس صوتی با جاوااسکریپت خالص. این ساختار سبک، اجرای روانی را حتی روی ضعیف‌ترین دستگاه‌ها تضمین می‌کند.

ساختار کامل کد تک‌فایلی پومودورو

برای استفاده از این ابزار، کافی است یک فایل متنی جدید ایجاد کرده، نام آن را مثلاً pomodoro.html بگذارید و کد زیر را به صورت کامل درون آن کپی کنید. سپس فایل را با هر مرورگر وب دلخواهی (مثل کروم، فایرفاکس یا اج) باز کنید.

<!DOCTYPE html>
<html lang="fa" dir="rtl">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>تایمر پومودورو پیشرفته</title>
    <style>
        :root {
            --bg-color: #0f172a;
            --card-bg: #1e293b;
            --text-color: #f8fafc;
            --accent-work: #ef4444;
            --accent-short: #10b981;
            --accent-long: #3b82f6;
            --current-accent: var(--accent-work);
        }

        * {
            box-sizing: border-box;
            margin: 0;
            padding: 0;
            font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
        }

        body {
            background-color: var(--bg-color);
            color: var(--text-color);
            min-height: 100vh;
            display: flex;
            justify-content: center;
            align-items: center;
            padding: 20px;
        }

        .container {
            background-color: var(--card-bg);
            padding: 30px;
            border-radius: 24px;
            box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.3), 0 10px 10px -5px rgba(0, 0, 0, 0.2);
            width: 100%;
            max-width: 420px;
            text-align: center;
        }

        h1 {
            font-size: 1.5rem;
            margin-bottom: 20px;
            font-weight: 700;
        }

        .modes {
            display: flex;
            justify-content: space-between;
            background-color: rgba(255, 255, 255, 0.05);
            border-radius: 12px;
            padding: 6px;
            margin-bottom: 30px;
        }

        .mode-btn {
            background: none;
            border: none;
            color: var(--text-color);
            padding: 8px 12px;
            border-radius: 8px;
            cursor: pointer;
            font-size: 0.9rem;
            font-weight: 600;
            transition: all 0.3s ease;
            flex: 1;
        }

        .mode-btn.active {
            background-color: var(--current-accent);
            color: #ffffff;
        }

        .timer-display {
            position: relative;
            width: 260px;
            height: 260px;
            margin: 0 auto 30px auto;
            display: flex;
            justify-content: center;
            align-items: center;
        }

        .progress-ring {
            transform: rotate(-90deg);
            width: 100%;
            height: 100%;
        }

        .progress-ring__circle-bg {
            fill: none;
            stroke: rgba(255, 255, 255, 0.05);
            stroke-width: 10;
        }

        .progress-ring__circle {
            fill: none;
            stroke: var(--current-accent);
            stroke-width: 10;
            stroke-linecap: round;
            stroke-dasharray: 754;
            stroke-dashoffset: 0;
            transition: stroke-dashoffset 1s linear, stroke 0.3s ease;
        }

        .time-text {
            position: absolute;
            font-size: 3rem;
            font-weight: 800;
            letter-spacing: -2px;
        }

        .controls {
            display: flex;
            gap: 15px;
            margin-bottom: 25px;
        }

        .btn {
            flex: 1;
            padding: 14px;
            border: none;
            border-radius: 12px;
            font-size: 1rem;
            font-weight: 700;
            cursor: pointer;
            transition: transform 0.1s ease, opacity 0.2s ease;
        }

        .btn:active {
            transform: scale(0.97);
        }

        .btn-main {
            background-color: var(--current-accent);
            color: #ffffff;
        }

        .btn-secondary {
            background-color: rgba(255, 255, 255, 0.1);
            color: var(--text-color);
        }

        .stats {
            font-size: 0.9rem;
            color: #94a3b8;
            border-top: 1px solid rgba(255, 255, 255, 0.05);
            padding-top: 15px;
        }
    </style>
</head>
<body>

    <div class="container">
        <h1>تایمر تمرکز پومودورو</h1>
        
        <div class="modes">
            <button class="mode-btn active" onclick="switchMode('work')" id="btn-work">کار</button>
            <button class="mode-btn" onclick="switchMode('shortBreak')" id="btn-short">استراحت کوتاه</button>
            <button class="mode-btn" onclick="switchMode('longBreak')" id="btn-long">استراحت بلند</button>
        </div>

        <div class="timer-display">
            <svg class="progress-ring" viewBox="0 0 260 260">
                <circle class="progress-ring__circle-bg" cx="130" cy="130" r="120"></circle>
                <circle class="progress-ring__circle" id="progress-circle" cx="130" cy="130" r="120"></circle>
            </svg>
            <div class="time-text" id="time-display">25:00</div>
        </div>

        <div class="controls">
            <button class="btn btn-main" id="start-pause-btn" onclick="toggleTimer()">شروع</button>
            <button class="btn btn-secondary" onclick="resetTimer()">بازنشانی</button>
        </div>

        <div class="stats">
            چرخه‌های تکمیل‌شده امروز: <span id="completed-count">0</span>
        </div>
    </div>

    <script>
        // تعریف زمان‌های استاندارد بر حسب ثانیه
        const times = {
            work: 25 * 60,
            shortBreak: 5 * 60,
            longBreak: 15 * 60
        };

        // رنگ‌های تم متناسب با هر حالت
        const colors = {
            work: '#ef4444',
            shortBreak: '#10b981',
            longBreak: '#3b82f6'
        };

        let currentMode = 'work';
        let timeLeft = times[currentMode];
        let totalTime = times[currentMode];
        let timerId = null;
        let isRunning = false;
        let completedCycles = 0;

        const timeDisplay = document.getElementById('time-display');
        const startPauseBtn = document.getElementById('start-pause-btn');
        const progressCircle = document.getElementById('progress-circle');
        const completedCountSpan = document.getElementById('completed-count');
        
        const radius = progressCircle.r.baseVal.value;
        const circumference = 2 * Math.PI * radius;
        progressCircle.style.strokeDasharray = `${circumference} ${circumference}`;

        function setProgress(percent) {
            const offset = circumference - (percent / 100) * circumference;
            progressCircle.style.strokeDashoffset = offset;
        }

        function formatTime(seconds) {
            const mins = Math.floor(seconds / 60);
            const secs = seconds % 60;
            return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
        }

        function updateDisplay() {
            timeDisplay.textContent = formatTime(timeLeft);
            const percent = (timeLeft / totalTime) * 100;
            setProgress(percent);
            document.title = `${formatTime(timeLeft)} - پومودورو`;
        }

        function switchMode(mode) {
            clearInterval(timerId);
            isRunning = false;
            startPauseBtn.textContent = 'شروع';
            currentMode = mode;
            timeLeft = times[mode];
            totalTime = times[mode];

            document.querySelectorAll('.mode-btn').forEach(btn => btn.classList.remove('active'));
            document.getElementById(`btn-${mode === 'work' ? 'work' : mode === 'shortBreak' ? 'short' : 'long'}`).classList.add('active');

            document.documentElement.style.setProperty('--current-accent', colors[mode]);
            updateDisplay();
        }

        function toggleTimer() {
            if (isRunning) {
                clearInterval(timerId);
                isRunning = false;
                startPauseBtn.textContent = 'ادامه';
            } else {
                isRunning = true;
                startPauseBtn.textContent = 'توقف';
                timerId = setInterval(() => {
                    if (timeLeft > 0) {
                        timeLeft--;
                        updateDisplay();
                    } else {
                        clearInterval(timerId);
                        isRunning = false;
                        startPauseBtn.textContent = 'شروع';
                        playAlarm();
                        if (currentMode === 'work') {
                            completedCycles++;
                            completedCountSpan.textContent = completedCycles;
                        }
                    }
                }, 1000);
            }
        }

        function resetTimer() {
            clearInterval(timerId);
            isRunning = false;
            startPauseBtn.textContent = 'شروع';
            timeLeft = times[currentMode];
            updateDisplay();
        }

        function playAlarm() {
            try {
                const audioCtx = new (window.AudioContext || window.webkitAudioContext)();
                const oscillator = audioCtx.createOscillator();
                const gainNode = audioCtx.createGain();

                oscillator.type = 'sine';
                oscillator.frequency.setValueAtTime(587.33, audioCtx.currentTime); // فرکانس نت D5
                
                gainNode.gain.setValueAtTime(0.1, audioCtx.currentTime);
                gainNode.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + 1.5);

                oscillator.connect(gainNode);
                gainNode.connect(audioCtx.destination);

                oscillator.start();
                oscillator.stop(audioCtx.currentTime + 1.5);
            } catch (e) {
                console.log('امکان پخش صدا با Web Audio API وجود ندارد.');
            }
        }

        updateDisplay();
    </script>
</body>
</html>

پس از ذخیره و اجرای کد بالا در مرورگر، یک صفحه کاملاً مدرن با حالت‌های مختلف زمان‌سنج مشاهده خواهید کرد که به راحتی قابل شخصی‌سازی است.

بررسی بخش‌به‌بخش کدهای نوشته شده

این اسکریپت به سه بخش اصلی تقسیم شده است: بخش ظاهری (HTML)، بخش استایل و انیمیشن حلقه‌ای (CSS) و بخش منطق زمان‌سنجی و تولید صدا (JavaScript). در ادامه عملکرد هر بخش را با جزئیات بررسی می‌کنیم:

۱. مدیریت حالت‌ها و متغیرهای اصلی

در بخش جاوااسکریپت، شیء times مدت‌زمان‌های استاندارد پومودورو را ذخیره می‌کند (۲۵ دقیقه برای کار، ۵ دقیقه استراحت کوتاه و ۱۵ دقیقه استراحت بلند). متغیرهای وضعیت مانند currentMode حالت فعلی را رصد کرده و متغیر timerId برای مدیریت تابع setInterval استفاده می‌شود تا شمارش معکوس به صورت ثانیه‌به‌ثانیه اجرا شود.

۲. انیمیشن حلقه‌ای پیشرفت با SVG

برای ایجاد نوار پیشرفت دورانی، از یک المان svg با تگ circle استفاده شده است. ویژگی stroke-dasharray محیط دایره را به واحدهای خطی تقسیم می‌کند و با کم کردن مقدار stroke-dashoffset در هر ثانیه بر اساس درصد باقی‌مانده از زمان، خط دور دایره به نرمی کاهش می‌یابد تا کاربر به صورت بصری متوجه گذر زمان بشود.

۳. تولید هشدار صوتی بدون فایل خارجی (Web Audio API)

یکی از ویژگی‌های بارز این پروژه عدم نیاز به آپلود فایل‌های صوتی MP3 یا WAV است. تابع playAlarm یک نمونه‌ی امن از AudioContext ایجاد می‌کند، یک اسیلاتور موج سینوسی با فرکانس مشخص می‌سازد و با تنظیم یک گین‌نود (GainNode)، صدایی ملحق به زنگ هشدار تولید می‌کند که پس از ۱.۵ ثانیه به طور خودکار محو می‌شود. این ویژگی باعث می‌شود پروژه کاملاً مستقل و تک‌فایلی باقی بماند.

نکات پیشرفته، محدودیت‌ها و اشتباهات رایج

هرچند این ابزار بسیار سبک و کارآمد است، اما باید به چند نکته‌ی فنی درباره‌ی رفتار مرورگرها توجه داشته باشید:

  • محدودیت اجرای صدا: در مرورگرهای مدرن، سیاست‌های امنیتی ایجاب می‌کند که کاربر حتماً یک‌بار روی صفحه کلیک کرده باشد تا مرورگر اجازه دهد Web Audio API صدا تولید کند. اولین کلیک روی دکمه‌ی شروع این دسترسی را فراهم می‌کند.
  • بستن تب مرورگر: از آنجا که این برنامه در حافظه‌ی مرورگر (RAM) اجرا می‌شود، اگر تب مرورگر را ببندید، شمارشگر متوقف شده و مقدار چرخه‌ها به صفر بازنشمی‌گردد. برای ذخیره‌سازی ماندگار، می‌توانید در نسخه‌های پیشرفته‌تر از localStorage استفاده کنید.
  • توقف در پس‌زمینه موبایل: برخی سیستم‌عامل‌های موبایل ممکن است تب‌های در پس‌زمینه را فریز کنند و دقت تایمر را کاهش دهند. بهتر است هنگام کار با این ابزار، تب مربوطه در حالت فعال بماند.

پرسش‌های متداول

آیا برای اجرای این کد نیاز به نصب نرم‌افزار خاصی دارم؟

خیر. شما فقط به یک مرورگر وب (مثل گوگل کروم، فایرفاکس یا مایکروسافت اج) نیاز دارید. فایل HTML را ذخیره کرده و با دابل‌کلیک آن را در مرورگر باز کنید.

چگونه می‌توانم زمان‌های کار یا استراحت را تغییر دهم؟

در بخش اسکریپت جاوااسکریپت، شیء times مقادیر زمان را بر حسب ثانیه ذخیره کرده است. به عنوان مثال عدد 25 * 60 نشان‌دهنده‌ی ۲۵ دقیقه است و می‌توانید آن را به دلخواه ویرایش کنید.

چرا صدای هشدار در ابتدای کار پخش نمی‌شود؟

مرورگرها اجازه پخش خودکار صدا را بدون تعامل کاربر نمی‌دهند. به محض اینکه اولین بار دکمه‌ی «شروع» را بزنید، این محدودیت برطرف شده و صدا در انتهای تایمر پخش خواهد شد.

جمع‌بندی

ساخت ابزارهای شخصی ساده نه تنها مهارت برنامه‌نویسی فرانت‌اند شما را تقویت می‌کند، بلکه ابزاری کاملاً شخصی‌سازی‌شده و بدون حواس‌پرتی در اختیارتان می‌گذارد که هیچ وابستگی به اینترنت یا سرورهای خارجی ندارد. با کپی کردن این کد تک‌فایلی، همین الان می‌توانید یک پومودوروی مینیمال، سریع و زیبا روی سیستم خود داشته باشید و بهره‌وری روزانه‌ی خود را به شکل چشمگیری افزایش دهید.

پاسخی بگذارید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *