Yeartimer

Yeartimer
 avatar
unknown
javascript
a month ago
13 kB
4
Indexable
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Таймер года — сколько прошло / осталось</title>
<style>
  :root{
    --passed:#0078ff;
    --remaining:#ff7f29;
    --bg:#0f1011;
    --muted:#9aa0a6;
    --card:#101214;
    --accent:#de7fff;
  }
  html,body{height:100%}
  body{
    margin:28px;
    font-family: system-ui, "Segoe UI", Roboto, Arial, sans-serif;
    background:var(--bg);
    color:#e9eef2;
    -webkit-font-smoothing:antialiased;
    -moz-osx-font-smoothing:grayscale;
  }

  .wrap{
    max-width:720px;
    margin:0 auto;
    padding:18px;
    background:linear-gradient(180deg,var(--card),#0b0c0d);
    border-radius:12px;
    border:1px solid rgba(255,255,255,0.03);
    box-shadow:0 10px 30px rgba(0,0,0,.6);
  }

  .title{
    text-align:center;
    font-weight:700;
    font-size:18px;
    margin-bottom:6px;
  }
  .subtitle{
    text-align:center;
    color:var(--muted);
    font-size:13px;
    margin-bottom:14px;
  }

  /* layout: weeks left | diagram | days right */
  .main-row{
    display:flex;
    gap:18px;
    align-items:center;
    justify-content:center;
    flex-wrap:wrap;
    margin-bottom:14px;
  }

  /* small diagrams (div.side) 45% opacity as requested */
  .side {
    min-width:140px;
    text-align:center;
    opacity:0.45; /* 45% прозрачности */
  }
  .side .small{ color:var(--muted); font-size:13px } /* requested style */
  .side .big{ font-weight:700; font-size:14px; margin-top:6px }

  .svg-wrap{ display:flex; align-items:center; justify-content:center; }
  svg.timer{ width:240px; height:240px; display:block }

  .mini-chart{ width:64px; height:64px; display:block; margin:6px auto 0; }

  .info{
    display:flex;
    gap:12px;
    justify-content:center;
    flex-wrap:wrap;
    align-items:center;
  }
  .info > div{min-width:180px;text-align:center}
  .big{font-weight:700;font-size:16px}
  .small{color:var(--muted);font-size:13px}

  .legend{display:flex;gap:12px;justify-content:center;margin-top:12px}
  .legend .item{display:flex;gap:8px;align-items:center;color:var(--muted);font-size:13px}
  .sw{width:14px;height:14px;border-radius:3px;display:inline-block}
  .controls{display:flex;gap:8px;justify-content:center;margin-top:14px}
  button{
    padding:8px 12px;border-radius:8px;border:1px solid rgba(255,255,255,0.04);
    background:#0c0d0e;color:#eaeff3;cursor:pointer;font-weight:600;
  }
  button[disabled]{opacity:.45;cursor:default}
  .muted{color:var(--muted)}

  /* responsive */
  @media (max-width:640px){
    .main-row{ flex-direction:column; gap:12px }
    .side{ min-width:unset }
  }
</style>
</head>
<body>
  <div class="wrap" role="main" aria-label="Таймер года">
    <div class="title">Таймер года</div>
    <div class="subtitle">Показывает, сколько прошло и сколько осталось в текущем году</div>

    <div class="main-row" aria-hidden="false">
      <!-- Left: weeks passed / remaining with mini chart -->
      <div class="side" aria-label="Недели">
        <div class="small">Недели:</div>
        <svg class="mini-chart" viewBox="0 0 40 40" aria-hidden="true">
          <g transform="translate(20,20)">
            <circle r="16" fill="none" stroke="var(--remaining)" stroke-width="6"></circle>
            <circle id="weeksPassedChart" r="16" fill="none" stroke="var(--passed)" stroke-width="6"
                    stroke-linecap="round" transform="rotate(-90)" stroke-dasharray="100" stroke-dashoffset="100"></circle>
          </g>
        </svg>
        <div id="weeksPassed" class="big">0 прошло</div>
        <div id="weeksRemaining" class="small muted">0 осталось</div>
      </div>

      <!-- Center: diagram -->
      <div class="svg-wrap" aria-hidden="false">
        <svg class="timer" viewBox="0 0 120 120" role="img" aria-label="Круговая диаграмма прогресса года">
          <g transform="translate(60,60)">
            <!-- фон круга (полный: оставшееся цвет) -->
            <circle r="52" fill="none" stroke="var(--remaining)" stroke-width="12"></circle>

            <!-- прогресс (пройденное) — будет скрывать часть фона через stroke-dashoffset -->
            <circle id="passed" r="52" fill="none" stroke="var(--passed)" stroke-width="12"
                    stroke-linecap="round" transform="rotate(-90)" stroke-dasharray="326.726016" stroke-dashoffset="326.726016"></circle>

            <!-- маленький центр для контраста -->
            <circle r="34" fill="#0b0c0d" stroke="rgba(255,255,255,0.03)" stroke-width="0.5"></circle>

            <!-- централь текст (процент пройдено / осталось) -->
            <text id="centerPct" x="0" y="-4" text-anchor="middle" font-size="12" fill="#fff" font-weight="700"></text>
            <text id="centerLbl" x="0" y="12" text-anchor="middle" font-size="9" fill="var(--muted)"></text>
          </g>
        </svg>
      </div>

      <!-- Right: days passed / remaining with mini chart -->
      <div class="side" aria-label="Дни">
        <div class="small">Дни:</div>
        <svg class="mini-chart" viewBox="0 0 40 40" aria-hidden="true">
          <g transform="translate(20,20)">
            <circle r="16" fill="none" stroke="var(--remaining)" stroke-width="6"></circle>
            <circle id="daysPassedChart" r="16" fill="none" stroke="var(--passed)" stroke-width="6"
                    stroke-linecap="round" transform="rotate(-90)" stroke-dasharray="100" stroke-dashoffset="100"></circle>
          </g>
        </svg>
        <div id="daysPassed" class="big">0 прошло</div>
        <div id="daysRemaining" class="small muted">0 осталось</div>
      </div>
    </div>

    <div class="info" aria-hidden="false">
      <div>
        <div class="small">Прошло:</div>
        <div id="passedPct" class="big">0.00%</div>
        <div id="passedSec" class="small muted">0 сек</div>
      </div>

      <!-- Добавлен блок с текущей датой между "Прошло" и "Осталось" -->
      <div>
        <div class="small">Текущая дата:</div>
        <div id="currentDate" class="big" style="line-height:1.05"></div>
      </div>

      <div>
        <div class="small">Осталось:</div>
        <div id="remPct" class="big">0.00%</div>
        <div id="remSec" class="small muted">0 сек</div>
      </div>
    </div>

    <div class="legend" aria-hidden="true">
      <div class="item"><span class="sw" style="background:var(--passed)"></span>Прошло</div>
      <div class="item"><span class="sw" style="background:var(--remaining)"></span>Осталось</div>
    </div>

  </div>

<script>
/*
  Таймер года — обновляется в реальном времени (каждую секунду).
  Мини-диаграммы (div.side) имеют opacity:0.45.
  Между "Прошло" и "Осталось" выводится текущая дата в формате:
    день недели (с заглавной буквы), <br>
    число, <br>
    месяц — короткий вариант с точкой, строчными, <br>
    год + " г."
*/

const R = 52;
const CIRC = 2*Math.PI*R;
const passedCircle = document.getElementById('passed');
passedCircle.style.strokeDasharray = String(CIRC);

const centerPct = document.getElementById('centerPct');
const centerLbl = document.getElementById('centerLbl');
const passedPctEl = document.getElementById('passedPct');
const remPctEl = document.getElementById('remPct');
const passedSecEl = document.getElementById('passedSec');
const remSecEl = document.getElementById('remSec');

const weeksPassedEl = document.getElementById('weeksPassed');
const weeksRemainingEl = document.getElementById('weeksRemaining');
const daysPassedEl = document.getElementById('daysPassed');
const daysRemainingEl = document.getElementById('daysRemaining');

const weeksPassedChart = document.getElementById('weeksPassedChart');
const daysPassedChart = document.getElementById('daysPassedChart');

const currentDateEl = document.getElementById('currentDate');

const pauseBtn = document.getElementById('pauseBtn');
const resumeBtn = document.getElementById('resumeBtn');
const resetBtn = document.getElementById('resetBtn');

let tickId = null;

const fmtNum = n => new Intl.NumberFormat('ru-RU').format(n);

function boundariesFor(date){
  const y = date.getFullYear();
  const start = new Date(y,0,1,0,0,0,0);
  const end = new Date(y+1,0,1,0,0,0,0);
  return {start, end, year: y};
}
function isLeapYear(y){ return (y%4===0 && y%100!==0) || (y%400===0); }
function secondsInYear(y){ return (isLeapYear(y)?366:365)*86400; }

function formatCurrentDate(now){
  // weekday (capitalize first letter), day (numeric), month (short with dot), year + " г."
  const weekdayRaw = new Intl.DateTimeFormat('ru-RU', {weekday:'long'}).format(now);
  const weekday = weekdayRaw.charAt(0).toUpperCase() + weekdayRaw.slice(1); // "Среда"

  const day = now.getDate();

  let monthShort = new Intl.DateTimeFormat('ru-RU', {month:'short'}).format(now).toLowerCase();
  if (!monthShort.endsWith('.')) monthShort = monthShort + '.'; // ensure trailing dot, e.g. "июн."

  const year = now.getFullYear();

  return `${weekday}<br>${day}<br>${monthShort}<br>${year} г.`;
}

function tick(){
  const now = new Date();
  let {start, end, year} = boundariesFor(now);
  if (now >= end){
    const nextNow = new Date();
    ({start, end, year} = boundariesFor(nextNow));
  }

  const elapsedMs = now - start;
  const remMs = end - now;

  const elapsedSec = Math.max(0, Math.floor(elapsedMs/1000));
  const remSec = Math.max(0, Math.ceil(remMs/1000));
  const totalSec = secondsInYear(year);

  const passedFrac = Math.min(1, Math.max(0, elapsedSec / totalSec));
  const remFrac = 1 - passedFrac;

  const passedPct = passedFrac * 100;
  const remPct = remFrac * 100;

  // big circle
  const offset = CIRC * (1 - passedFrac);
  passedCircle.style.strokeDashoffset = String(offset);

  centerPct.textContent = passedPct.toFixed(2).replace('.',',') + '%';
  centerLbl.textContent = 'прошло';
  passedPctEl.textContent = passedPct.toFixed(2).replace('.',',') + '%';
  remPctEl.textContent = remPct.toFixed(2).replace('.',',') + '%';
  passedSecEl.textContent = fmtNum(elapsedSec) + ' сек';
  remSecEl.textContent = fmtNum(remSec) + ' сек';

  // days
  const daysPassed = Math.floor(elapsedSec / 86400);
  const daysRemaining = Math.max(0, Math.ceil((totalSec - elapsedSec) / 86400));
  daysPassedEl.textContent = fmtNum(daysPassed) + ' прошло';
  daysRemainingEl.textContent = fmtNum(daysRemaining) + ' осталось';

  // weeks
  const secPerWeek = 7 * 86400;
  const weeksPassed = Math.floor(elapsedSec / secPerWeek);
  const weeksRemaining = Math.max(0, Math.ceil((totalSec - elapsedSec) / secPerWeek));
  weeksPassedEl.textContent = fmtNum(weeksPassed) + ' прошло';
  weeksRemainingEl.textContent = fmtNum(weeksRemaining) + ' осталось';

  // mini charts (dasharray 100)
  const weeksTotalWeeks = totalSec / secPerWeek;
  const weeksFrac = Math.min(1, elapsedSec / (weeksTotalWeeks * secPerWeek));
  weeksPassedChart.style.strokeDasharray = '100';
  weeksPassedChart.style.strokeDashoffset = String(100 * (1 - weeksFrac));

  const totalDays = isLeapYear(year)?366:365;
  const daysFrac = Math.min(1, elapsedSec / (totalDays * 86400));
  daysPassedChart.style.strokeDasharray = '100';
  daysPassedChart.style.strokeDashoffset = String(100 * (1 - daysFrac));

  // current date block
  if (currentDateEl) currentDateEl.innerHTML = formatCurrentDate(now);
}

function startTicker(){
  if (tickId) clearInterval(tickId);
  tick();
  tickId = setInterval(tick, 1000);
  if (pauseBtn) pauseBtn.disabled = false;
  if (resumeBtn) resumeBtn.disabled = true;
}
function stopTicker(){
  if (tickId) { clearInterval(tickId); tickId = null; }
  if (pauseBtn) pauseBtn.disabled = true;
  if (resumeBtn) resumeBtn.disabled = false;
}

window.addEventListener('DOMContentLoaded', () => {
  // ensure initial state for mini charts
  weeksPassedChart.style.strokeDasharray = '100';
  weeksPassedChart.style.strokeDashoffset = '100';
  daysPassedChart.style.strokeDasharray = '100';
  daysPassedChart.style.strokeDashoffset = '100';
  startTicker();
});

if (pauseBtn) pauseBtn.addEventListener('click', () => stopTicker());
if (resumeBtn) resumeBtn.addEventListener('click', () => startTicker());
if (resetBtn) resetBtn.addEventListener('click', () => { startTicker(); tick(); });
</script>
</body>
</html>
Editor is loading...
Leave a Comment