Przejdź do treści

Temat 28 · 2 × 45 min

Czas, daty i animacja

Do tej pory twój kod wykonywał się „natychmiast”: kliknięcie – reakcja. Prawdziwe interfejsy żyją w czasie: komunikat znika po trzech sekundach, zegar tyka, slider sam przewija zdjęcia, pasek postępu płynnie rośnie. Do tego dochodzą daty – „ile dni do egzaminu?”, „który to dzień tygodnia?” – z obiektem Date, który ma kilka historycznych dziwactw. Ta lekcja uczy trzech narzędzi: timerów, dat i requestAnimationFrame, a przy okazji otwiera drzwi do asynchroniczności z lekcji 29.

setTimeout – zrób to później

setTimeout(funkcja, ms, ...argumenty) planuje jednorazowe wywołanie funkcji po co najmniej ms milisekundach i natychmiast zwraca identyfikator timera. Reszta skryptu nie czeka.

timeout.js
console.log('1. start');

const id = setTimeout(() => {
  console.log('3. minęło ok. 500 ms');
}, 500);

// dodatkowe argumenty trafiają do funkcji
setTimeout((kto, ile) => console.log(`4. ${kto} czekał ${ile} ms`), 800, 'Bartek', 800);

console.log('2. koniec skryptu – a timer dopiero się odliczy'); // wypisze się PRZED „3.”
// `id` to w przeglądarce liczba – przyda się do clearTimeout(id)
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Kolejność w konsoli: 1, 2, potem 3 i 4. setTimeout nie zatrzymuje programu – zleca przeglądarce „przypomnij mi za chwilę” i idzie dalej. Nawet setTimeout(f, 0) wykona się po całym bieżącym skrypcie, bo funkcja trafia do kolejki zadań i czeka, aż stos będzie pusty (szczegóły w lekcji 29).

Sprawdź się

Co wypisze ten kod?

console.log('A');
setTimeout(() => console.log('B'), 0);
console.log('C');

Zaplanowane wywołanie odwołujesz przez clearTimeout(id) – klasyczny przykład to komunikat, który znika po 3 s, chyba że użytkownik zamknie go wcześniej:

clearTimeout.js
let przypomnienie = setTimeout(() => console.log('To się NIE wypisze'), 300);
clearTimeout(przypomnienie); // odwołane, zanim zdążyło się uruchomić

// wzorzec „zresetuj odliczanie”: każde wywołanie kasuje poprzednie
let timerUkrycia = null;
function pokazKomunikat(tekst) {
  console.log('pokazuję:', tekst);
  clearTimeout(timerUkrycia); // clearTimeout(null) jest bezpieczne – nic nie robi
  timerUkrycia = setTimeout(() => console.log('ukrywam komunikat'), 400);
}
pokazKomunikat('Zapisano');
setTimeout(() => pokazKomunikat('Zapisano ponownie'), 200); // resetuje odliczanie – „ukrywam” pojawi się tylko raz, po ~600 ms
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

setInterval – powtarzaj co…

setInterval(funkcja, ms) wywołuje funkcję cyklicznie, co ms milisekund, aż do clearInterval(id). Bez zatrzymania działa, dopóki strona jest otwarta.

interval.js
let sekunda = 3;
const id = setInterval(() => {
  console.log(`Start za ${sekunda}…`);
  sekunda--;
  if (sekunda === 0) {
    clearInterval(id); // ZAWSZE musi być warunek stopu
    console.log('Start!');
  }
}, 300);
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

W przeglądarce zobaczysz trzy odliczenia i „Start!”. Interwał to najczęstsze źródło „wycieków” w aplikacjach: jeśli element znika z ekranu, a interwał dalej go aktualizuje, kod działa w pustkę i zużywa procesor. Zasady bezpieczeństwa:

  1. Jeden identyfikator, jedno miejsce startu. Przed setInterval zawsze clearInterval poprzedniego – inaczej dwa kliknięcia „Start” dadzą dwa interwały i zegar będzie „tykał” dwa razy na sekundę.
  2. Zatrzymuj przy końcu: osiągnięty cel, zamknięcie okna, usunięcie elementu.
  3. Przy długich obliczeniach rozważ rekurencyjny setTimeout – kolejny krok planujesz dopiero po zakończeniu bieżącego, więc odstępy się nie nakładają, a opóźnienie możesz zmieniać w locie:
rekurencyjny-timeout.js
let krok = 1;
function tik() {
  if (krok === 4) {
    console.log('tik 4 – koniec');
    return;
  }
  const odstep = (krok + 1) * 100; // odstęp rośnie: 200, 300, 400 ms
  console.log(`tik ${krok}, następny za ${odstep} ms`);
  krok++;
  setTimeout(tik, odstep); // kolejny krok planujemy dopiero teraz
}
tik();
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Zegar cyfrowy

Zegar – Start/Stop bez podwójnego interwału
const zegar = document.getElementById('zegar');
let idZegara = null; // null = zegar nie chodzi

function pokazCzas() {
  // toLocaleTimeString daje gotowy format HH:MM:SS zgodny z ustawieniami 'pl-PL'
  zegar.textContent = new Date().toLocaleTimeString('pl-PL');
}

document.getElementById('start').addEventListener('click', () => {
  if (idZegara !== null) return; // już chodzi – drugi klik nic nie robi
  pokazCzas(); // od razu, nie dopiero po sekundzie
  idZegara = setInterval(pokazCzas, 1000);
});

document.getElementById('stop').addEventListener('click', () => {
  clearInterval(idZegara);
  idZegara = null;
});

document.getElementById('start').click(); // uruchom na starcie
Podgląd

Zwróć uwagę na dwa drobiazgi, które odróżniają działający zegar od irytującego: pierwsze wyświetlenie natychmiast (nie po sekundzie czekania) i strażnik if (idZegara !== null) return.

Obiekt Date

Date reprezentuje jeden punkt w czasie: liczbę milisekund od 1 stycznia 1970 (UTC), zwaną znacznikiem czasu (timestamp). Z tego punktu umie wyliczyć rok, miesiąc, dzień tygodnia w lokalnej strefie czasowej użytkownika.

Tworzenie

date-tworzenie.js
const teraz = new Date(); // bieżąca chwila
console.log(typeof teraz, teraz instanceof Date); // object true

// rok, miesiąc (OD ZERA!), dzień, godz, min, sek – lokalnie
const egzamin = new Date(2027, 0, 12, 9, 0); // 12 stycznia 2027, 9:00
console.log(egzamin.getDate(), egzamin.getMonth(), egzamin.getFullYear()); // 12 0 2027

// napis ISO: samo "RRRR-MM-DD" jest traktowane jako UTC, z godziną – jako czas lokalny
const zIso = new Date('2026-10-20T08:30:00');
console.log(zIso.getHours()); // 8

// liczba ms od 1970
console.log(new Date(0).toISOString()); // 1970-01-01T00:00:00.000Z
console.log(typeof Date.now()); // number – bieżący znacznik czasu, bez tworzenia obiektu

// bez `new` Date() zwraca NAPIS z bieżącą datą – dialekt egzaminacyjny: document.write(Date())
console.log(typeof Date()); // string
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik
Sprawdź się

Co wypisze ten kod?

const d = new Date(2026, 2, 15);
console.log(d.getMonth() + '/' + d.getDate());

Odczyt i zmiana składników

OdczytZakresZmianaUwagi
getFullYear()4 cyfrysetFullYear(r)nie używaj getYear() (przestarzałe)
getMonth()0–11setMonth(m)0 = styczeń
getDate()1–31setDate(d)dzień miesiąca; setDate(0) = ostatni dzień poprzedniego miesiąca
getDay()0–6–0 = niedziela, 1 = poniedziałek… nie ma setDay
getHours(), getMinutes(), getSeconds(), getMilliseconds()0–23, 0–59, 0–59, 0–999setHours(…) itd.czas lokalny
getTime(), Date.now(), +datams od 1970setTime(ms)do porównań i różnic
getTimezoneOffset()minuty–Polska: −60 (zima) lub −120 (lato)
getUTC…()jak wyżej, ale w UTCsetUTC…()do komunikacji z serwerem

Settery „autokorygują” datę – to cecha, nie błąd, i daje wygodne sztuczki:

date-korekta.js
const d = new Date(2026, 0, 31); // 31 stycznia
d.setDate(d.getDate() + 1); // +1 dzień
console.log(d.getMonth(), d.getDate()); // 1 1 → 1 lutego – Date samo przeszło do następnego miesiąca

const ostatni = new Date(2026, 2, 0); // „dzień 0 marca” = ostatni dzień lutego
console.log(ostatni.getDate()); // 28 – ile dni ma luty 2026

const zaTydzien = new Date();
zaTydzien.setDate(zaTydzien.getDate() + 7); // +7 dni, przeskok miesiąca/roku załatwia Date
console.log(zaTydzien > new Date()); // true – porównania działają przez wartość liczbową
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Różnice między datami

Odejmowanie dwu dat daje różnicę w milisekundach (- zamienia obiekty na liczby). Dalej to arytmetyka:

date-roznica.js
const MS_DNIA = 24 * 60 * 60 * 1000;

function dniDo(dataDocelowa, od = new Date()) {
  // zerujemy godziny, żeby liczyć pełne dni kalendarzowe, nie 24-godzinne odcinki
  const a = new Date(od.getFullYear(), od.getMonth(), od.getDate());
  const b = new Date(dataDocelowa.getFullYear(), dataDocelowa.getMonth(), dataDocelowa.getDate());
  return Math.round((b - a) / MS_DNIA);
}

const dzis = new Date(2026, 9, 20); // 20 października 2026 (stała data, żeby wynik był przewidywalny)
console.log(dniDo(new Date(2027, 0, 12), dzis)); // 84 – do sesji styczniowej
console.log(dniDo(new Date(2026, 11, 24), dzis)); // 65 – do Wigilii

function wiek(urodziny, dzis = new Date()) {
  let lata = dzis.getFullYear() - urodziny.getFullYear();
  const przedUrodzinami =
    dzis.getMonth() < urodziny.getMonth() || (dzis.getMonth() === urodziny.getMonth() && dzis.getDate() < urodziny.getDate());
  if (przedUrodzinami) lata--; // w tym roku jeszcze nie było urodzin
  return lata;
}
console.log(wiek(new Date(2008, 10, 5), dzis)); // 17 – urodziny dopiero w listopadzie
console.log(wiek(new Date(2008, 5, 5), dzis)); // 18
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Dlaczego Math.round, a nie dzielenie całkowite? Przy zmianie czasu letni/zimowy jeden dzień ma 23 lub 25 godzin; (b - a) / MS_DNIA daje wtedy 83,96 albo 84,04 i zaokrąglenie ratuje wynik. Przy liczeniu wieku nie dziel w ogóle – porównuj składniki.

Formatowanie: toLocale… i Intl

Ręczne sklejanie getDate() + '.' + (getMonth() + 1) jest podatne na błędy (brak zera wiodącego, miesiąc od zera). Przeglądarka ma wbudowany formater zgodny z polskimi zwyczajami:

date-format.js
const d = new Date(2026, 9, 20, 8, 5); // 20 października 2026, 8:05

console.log(d.toLocaleDateString('pl-PL')); // 20.10.2026
console.log(d.toLocaleTimeString('pl-PL')); // 08:05:00
console.log(d.toLocaleString('pl-PL')); // 20.10.2026, 08:05:00
console.log(d.toLocaleDateString('pl-PL', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }));
// wtorek, 20 października 2026
console.log(d.toLocaleDateString('pl-PL', { month: 'short', day: '2-digit' })); // 20 paź
console.log(d.toLocaleTimeString('pl-PL', { hour: '2-digit', minute: '2-digit' })); // 08:05

// ten sam format wielokrotnie – utwórz formater raz
const format = new Intl.DateTimeFormat('pl-PL', { dateStyle: 'full', timeStyle: 'short' });
console.log(format.format(d)); // wtorek, 20 października 2026 08:05

// „3 dni temu”, „za 2 godziny”
const wzgledny = new Intl.RelativeTimeFormat('pl', { numeric: 'auto' });
console.log(wzgledny.format(-3, 'day')); // 3 dni temu
console.log(wzgledny.format(1, 'day')); // jutro
console.log(wzgledny.format(2, 'hour')); // za 2 godziny

// gdy mimo wszystko składasz ręcznie – padStart dla zer wiodących
const hh = String(d.getHours()).padStart(2, '0');
const mm = String(d.getMinutes()).padStart(2, '0');
console.log(`${hh}:${mm}`); // 08:05
Konsola
Kliknij „Uruchom”, aby zobaczyć wynik

Odliczanie do egzaminu

Odliczanie do daty
const pole = document.getElementById('cel');
const wyjscie = document.getElementById('odliczanie');

// domyślnie: najbliższy 12 stycznia, 9:00 (sesja zimowa)
const teraz = new Date();
let cel = new Date(teraz.getFullYear(), 0, 12, 9);
if (cel < teraz) cel.setFullYear(cel.getFullYear() + 1);
// input datetime-local chce "RRRR-MM-DDTHH:MM" w czasie lokalnym
const pad = (n) => String(n).padStart(2, '0');
pole.value = `${cel.getFullYear()}-${pad(cel.getMonth() + 1)}-${pad(cel.getDate())}T${pad(cel.getHours())}:${pad(cel.getMinutes())}`;

let id = null;

pole.addEventListener('change', () => {
  if (!pole.value) return; // wyczyszczone pole – zostaw poprzedni cel
  cel = new Date(pole.value); // napis z godziną → czas lokalny
  clearInterval(id); // interwał mógł już stanąć na „Już!” – startujemy od nowa
  odlicz();
  id = setInterval(odlicz, 1000);
});

function odlicz() {
  const roznica = cel - Date.now();
  if (roznica <= 0) {
    wyjscie.textContent = 'Już!';
    clearInterval(id);
    return;
  }
  const s = Math.floor(roznica / 1000);
  const dni = Math.floor(s / 86400);
  const godz = Math.floor((s % 86400) / 3600);
  const min = Math.floor((s % 3600) / 60);
  const sek = s % 60;
  wyjscie.textContent = `${dni} dni ${pad(godz)}:${pad(min)}:${pad(sek)}`;
}

odlicz();
id = setInterval(odlicz, 1000);
Podgląd

Slider z autoodtwarzaniem i pauzą

Łączymy wszystko: stan (indeks z zawijaniem, lekcja 17), interwał ze strażnikiem i zdarzenia mouseenter/mouseleave (lekcja 27).

Slider: autoodtwarzanie, pauza na najechanie, kropki
const slajdy = [
  { tekst: 'Zakopane', kolor: '#2563eb' },
  { tekst: 'Gdańsk', kolor: '#0891b2' },
  { tekst: 'Kraków', kolor: '#dc2626' },
  { tekst: 'Wrocław', kolor: '#16a34a' },
];
const CO_ILE = 2000;

const slider = document.getElementById('slider');
const slajd = document.getElementById('slajd');
const kropki = document.getElementById('kropki');
const stan = document.getElementById('stan');
let indeks = 0;
let idAuto = null;

// kropki generowane z danych – jedna na slajd
slajdy.forEach((_, i) => {
  const b = document.createElement('button');
  b.type = 'button';
  b.dataset.indeks = i;
  b.setAttribute('aria-label', `slajd ${i + 1}`);
  kropki.append(b);
});

function pokaz(nowy) {
  indeks = (nowy + slajdy.length) % slajdy.length;
  slajd.textContent = slajdy[indeks].tekst;
  slajd.style.background = slajdy[indeks].kolor;
  kropki.querySelectorAll('button').forEach((b, i) => b.classList.toggle('aktywna', i === indeks));
}

function start() {
  if (idAuto !== null) return; // strażnik: nigdy dwa interwały
  idAuto = setInterval(() => pokaz(indeks + 1), CO_ILE);
  stan.textContent = 'odtwarzanie';
}

function stop() {
  clearInterval(idAuto);
  idAuto = null;
  stan.textContent = 'pauza (kursor nad sliderem)';
}

// strzałki – delegacja; po ręcznej zmianie restartujemy odliczanie, żeby slajd nie przeskoczył od razu
slider.addEventListener('click', (e) => {
  const btn = e.target.closest('button[data-krok]');
  if (!btn) return;
  pokaz(indeks + Number(btn.dataset.krok));
  if (idAuto !== null) {
    stop();
    start();
  }
});

kropki.addEventListener('click', (e) => {
  const b = e.target.closest('button[data-indeks]');
  if (b) pokaz(Number(b.dataset.indeks));
});

// mouseenter/mouseleave nie bąbelkują i nie „migają” przy przejściu na przyciski w środku
slider.addEventListener('mouseenter', stop);
slider.addEventListener('mouseleave', start);

// gdy karta przeglądarki jest w tle, nie ma sensu przewijać
document.addEventListener('visibilitychange', () => (document.hidden ? stop() : start()));

pokaz(0);
start();
Podgląd

Dwie decyzje projektowe warte zapamiętania: restart odliczania po ręcznym kliknięciu (inaczej slajd zmienia się pół sekundy po tym, jak użytkownik wybrał inny) i pauza, gdy karta jest niewidoczna (visibilitychange). Przeglądarki i tak spowalniają timery w kartach w tle, ale jawne zatrzymanie jest czystsze.

requestAnimationFrame – płynna animacja

setInterval(rysuj, 16) jako animacja to zły pomysł: ekran odświeża się 60 (lub 120) razy na sekundę, a interwał nie jest z tym zsynchronizowany – klatki się gubią lub dublują, animacja „szarpie”. requestAnimationFrame(callback) prosi przeglądarkę: wywołaj moją funkcję tuż przed następnym odmalowaniem ekranu. Callback dostaje znacznik czasu w ms (performance.now()), a żeby animować dalej, w jego środku prosisz o kolejną klatkę.

raf-szkielet.js
let idKlatki;

function klatka(czas) {
  // czas – ms od załadowania strony, z dokładnością do ułamków
  rysuj(czas);
  idKlatki = requestAnimationFrame(klatka); // kolejna klatka
}
idKlatki = requestAnimationFrame(klatka);

// zatrzymanie
cancelAnimationFrame(idKlatki);

Najważniejsza zasada: animuj na podstawie upływu czasu, nie liczby klatek. Jeśli w każdej klatce przesuwasz o 2 px, na ekranie 120 Hz animacja jest dwa razy szybsza niż na 60 Hz. Zamiast tego oblicz, jaka część animacji powinna już minąć: postep = (teraz - start) / czasTrwania i ustaw położenie z tego ułamka.

Pasek postępu: setInterval vs requestAnimationFrame
const kulkaA = document.getElementById('kulkaA');
const kulkaB = document.getElementById('kulkaB');
const info = document.getElementById('info');
const CZAS = 2000; // ms
let idInterval = null;
let idRaf = null;

// wygładzenie (easing): start powoli, koniec powoli
const easeInOut = (t) => (t < 0.5 ? 2 * t * t : 1 - (-2 * t + 2) ** 2 / 2);

function start() {
  clearInterval(idInterval);
  cancelAnimationFrame(idRaf);
  const dystans = kulkaA.parentElement.clientWidth - kulkaA.offsetWidth;

  // A: naiwnie – stały krok na klatkę; prędkość zależy od tego, jak często odpala się interwał
  let x = 0;
  kulkaA.style.left = '0px';
  idInterval = setInterval(() => {
    x += dystans / 125; // „125 klatek po 16 ms = 2 s” – ale interwał nie trzyma 16 ms dokładnie
    if (x >= dystans) {
      x = dystans;
      clearInterval(idInterval);
    }
    kulkaA.style.left = `${x}px`;
  }, 16);

  // B: poprawnie – położenie wynika z czasu, więc trwa 2 s na każdym ekranie
  const startCzas = performance.now();
  let klatki = 0;
  function klatka(teraz) {
    klatki++;
    const postep = Math.min((teraz - startCzas) / CZAS, 1); // 0 → 1, nigdy powyżej
    kulkaB.style.left = `${easeInOut(postep) * dystans}px`;
    if (postep < 1) {
      idRaf = requestAnimationFrame(klatka);
    } else {
      info.textContent = `rAF: ${klatki} klatek w ${Math.round(teraz - startCzas)} ms`;
    }
  }
  idRaf = requestAnimationFrame(klatka);
}

document.getElementById('start').addEventListener('click', start);
start();
Podgląd

Na większości komputerów obie kulki dojadą w podobnym czasie – ale czerwona szarpie i spóźnia się, gdy przeglądarka ma dużo pracy, a niebieska zawsze kończy dokładnie po 2 s. requestAnimationFrame dodatkowo zatrzymuje się w kartach w tle, więc nie marnuje baterii.

Ściąga

SkładniaCo robiPrzykład
const id = setTimeout(fn, ms, ...args)wywołaj fn raz po ms; zwraca idsetTimeout(() => ukryj(), 3000)
clearTimeout(id)odwołaj zaplanowane wywołaniewzorzec „resetuj odliczanie”
const id = setInterval(fn, ms)wywołuj co ms aż do clearIntervalzegar, odliczanie, slider
clearInterval(id)zatrzymaj; przed setInterval wyczyść poprzedniif (id !== null) return
rekurencyjny setTimeoutkolejny krok po zakończeniu bieżącego; zmienny odstępfunction tik() { …; setTimeout(tik, ms); }
new Date() / Date.now()teraz jako obiekt / jako liczba msconst start = Date.now()
new Date(r, m, d, h, min)data lokalna, miesiąc 0–11new Date(2027, 0, 12) = 12 I 2027
new Date('RRRR-MM-DDTHH:MM')z napisu ISO (z godziną – lokalnie)new Date('2026-10-20T08:30')
getFullYear/Month/Date/Day/Hours…składniki; getDay() 0 = niedzieladni[d.getDay()]
setDate(d.getDate() + n)przesuń o n dni z autokorektąsetDate(0) → ostatni dzień poprz. miesiąca
b - aróżnica w msMath.round((b - a) / 86400000) dni
toLocaleDateString('pl-PL', opcje)20.10.2026; opcje weekday, month: 'long'…toLocaleTimeString('pl-PL') → 08:05:00
new Intl.DateTimeFormat(…), Intl.RelativeTimeFormatformater wielokrotnego użytku; „3 dni temu”format(d), format(-3, 'day')
toISOString()2026-10-19T22:00:00.000Z – UTC!do serwera; nie do „daty kalendarzowej”
String(n).padStart(2, '0')zero wiodące08, 05
requestAnimationFrame(fn) / cancelAnimationFrame(id)klatka przed odmalowaniem; fn(czas)postep = (teraz - start) / czas
performance.now()precyzyjny czas od załadowania strony (ms)pomiar, animacja
document.hidden, visibilitychangekarta w tlezatrzymaj slider/zegar

Zadania

Zadanie 1
łatwe

Powiadomienie, które samo znika

Przycisk „Zapisz” pokazuje <div id="toast">Zapisano!</div> (klasa widoczny), który znika po 3 sekundach. Wymagania: wielokrotne kliknięcie resetuje odliczanie (powiadomienie nie znika po 3 s od pierwszego kliknięcia, lecz od ostatniego); przycisk „×” w powiadomieniu zamyka je od razu i odwołuje timer. Pokaż w powiadomieniu, które to z kolei zapisanie.

Pokaż rozwiązanie
Toast z resetem odliczania
const toast = document.getElementById('toast');
const tekst = document.getElementById('toast-tekst');
let timer = null;
let licznik = 0;

function pokazToast(komunikat) {
  tekst.textContent = komunikat;
  toast.classList.add('widoczny');
  clearTimeout(timer); // reset – liczymy 3 s od OSTATNIEGO pokazania
  timer = setTimeout(ukryjToast, 3000);
}

function ukryjToast() {
  toast.classList.remove('widoczny');
  clearTimeout(timer);
  timer = null;
}

document.getElementById('zapisz').addEventListener('click', () => {
  licznik++;
  pokazToast(`Zapisano! (${licznik}. raz)`);
});
document.getElementById('zamknij').addEventListener('click', ukryjToast);
Podgląd
Zadanie 2
średnie

Stoper z międzyczasami

Stoper w formacie mm:ss.d (dziesiąte sekundy) z przyciskami Start/Stop (jeden przycisk zmieniający etykietę), „Międzyczas” (dopisuje aktualny czas do listy <ol>) i „Reset”. Czas licz z różnicy Date.now() od chwili startu (plus czas zgromadzony przed pauzą), nie przez dodawanie 100 ms w każdym ticku – interwały nie są dokładne. Odświeżaj co 50 ms. Reset ma być możliwy tylko po zatrzymaniu.

Pokaż rozwiązanie
Stoper
const czasEl = document.getElementById('czas');
const btnStartStop = document.getElementById('startstop');
const btnMiedzy = document.getElementById('miedzyczas');
const btnReset = document.getElementById('reset');
const lista = document.getElementById('lista');

let zgromadzone = 0; // ms zliczone przed ostatnią pauzą
let startOd = null; // Date.now() ostatniego startu, null = stoi
let id = null;

const pad = (n, len = 2) => String(n).padStart(len, '0');

function aktualny() {
  return zgromadzone + (startOd === null ? 0 : Date.now() - startOd);
}

function formatuj(ms) {
  const min = Math.floor(ms / 60000);
  const sek = Math.floor((ms % 60000) / 1000);
  const dziesiate = Math.floor((ms % 1000) / 100);
  return `${pad(min)}:${pad(sek)}.${dziesiate}`;
}

function odswiez() {
  czasEl.textContent = formatuj(aktualny());
}

btnStartStop.addEventListener('click', () => {
  if (startOd === null) {
    startOd = Date.now();
    id = setInterval(odswiez, 50);
    btnStartStop.textContent = 'Stop';
    btnMiedzy.disabled = false;
    btnReset.disabled = true;
  } else {
    zgromadzone += Date.now() - startOd; // dolicz odcinek, który właśnie się skończył
    startOd = null;
    clearInterval(id);
    odswiez();
    btnStartStop.textContent = 'Start';
    btnMiedzy.disabled = true;
    btnReset.disabled = false;
  }
});

btnMiedzy.addEventListener('click', () => {
  const li = document.createElement('li');
  li.textContent = formatuj(aktualny());
  lista.append(li);
});

btnReset.addEventListener('click', () => {
  zgromadzone = 0;
  lista.innerHTML = '';
  odswiez();
  btnReset.disabled = true;
});
Podgląd
Zadanie 3
średnie

Kalkulator dat

Formularz z polem <input type="date" id="data"> i trzema wynikami w akapitach: a) dzień tygodnia po polsku i czy to weekend, b) liczba dni od dziś do wybranej daty (ujemna, gdy w przeszłości; tekst: Za 84 dni / 12 dni temu / To dziś), c) data za 30 dni od wybranej, sformatowana wtorek, 20 października 2026. Reaguj na change i policz od razu dla dzisiejszej daty. Pamiętaj, że pole type="date" zwraca napis RRRR-MM-DD – zbuduj z niego datę lokalną (nie new Date('2026-10-20'), które jest w UTC).

Pokaż rozwiązanie
Kalkulator dat
const pole = document.getElementById('data');
const DNI = ['niedziela', 'poniedziałek', 'wtorek', 'środa', 'czwartek', 'piątek', 'sobota'];
const MS_DNIA = 86400000;
const pad = (n) => String(n).padStart(2, '0');

// napis "RRRR-MM-DD" → data LOKALNA o północy (split zamiast new Date(napis), które dałoby UTC)
function zPola(napis) {
  const [r, m, d] = napis.split('-').map(Number);
  return new Date(r, m - 1, d);
}
function naPole(data) {
  return `${data.getFullYear()}-${pad(data.getMonth() + 1)}-${pad(data.getDate())}`;
}
function polnoc(data) {
  return new Date(data.getFullYear(), data.getMonth(), data.getDate());
}

function policz() {
  const data = zPola(pole.value);
  const dzis = polnoc(new Date());

  const dzien = data.getDay();
  const weekend = dzien === 0 || dzien === 6;
  document.getElementById('a').textContent = `To ${DNI[dzien]}${weekend ? ' – weekend!' : ''}.`;

  const roznica = Math.round((data - dzis) / MS_DNIA);
  const b = roznica === 0 ? 'To dziś.' : roznica > 0 ? `Za ${roznica} dni.` : `${-roznica} dni temu.`;
  document.getElementById('b').textContent = b;

  const za30 = new Date(data);
  za30.setDate(za30.getDate() + 30);
  document.getElementById('c').textContent =
    '30 dni później: ' + za30.toLocaleDateString('pl-PL', { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' });
}

pole.value = naPole(new Date());
pole.addEventListener('change', policz);
policz();
Podgląd
Zadanie 4
trudne

Kalendarz miesiąca

Wyświetl siatkę kalendarza dla dowolnego miesiąca: nagłówek październik 2026 z przyciskami „‹” / „›”, wiersz skrótów dni od poniedziałku (Pn Wt Śr Cz Pt So Nd), potem dni miesiąca w siatce 7 kolumn (CSS Grid). Pierwszy dzień musi trafić do właściwej kolumny (uwaga: getDay() daje 0 dla niedzieli, a ty zaczynasz od poniedziałku). Dzisiejszy dzień ma klasę dzis, weekendy – klasę weekend. Liczbę dni w miesiącu wylicz sztuczką new Date(rok, miesiac + 1, 0).getDate().

Pokaż rozwiązanie
Kalendarz
const tytul = document.getElementById('tytul');
const siatka = document.getElementById('siatka');
const SKROTY = ['Pn', 'Wt', 'Śr', 'Cz', 'Pt', 'So', 'Nd'];

const dzis = new Date();
let rok = dzis.getFullYear();
let miesiac = dzis.getMonth();

function rysuj() {
  const pierwszy = new Date(rok, miesiac, 1);
  tytul.textContent = pierwszy.toLocaleDateString('pl-PL', { month: 'long', year: 'numeric' });

  const dniWMiesiacu = new Date(rok, miesiac + 1, 0).getDate(); // „dzień 0” następnego miesiąca
  // getDay(): 0 = niedziela … 6 = sobota → przesuwamy, by 0 = poniedziałek … 6 = niedziela
  const przesuniecie = (pierwszy.getDay() + 6) % 7;

  siatka.innerHTML = '';
  for (const s of SKROTY) {
    const el = document.createElement('div');
    el.className = 'dzien-tyg';
    el.textContent = s;
    siatka.append(el);
  }
  for (let i = 0; i < przesuniecie; i++) siatka.append(document.createElement('div')); // puste pola przed 1.

  for (let d = 1; d <= dniWMiesiacu; d++) {
    const el = document.createElement('div');
    el.className = 'dzien';
    el.textContent = d;
    const kolumna = (przesuniecie + d - 1) % 7; // 5 = sobota, 6 = niedziela
    if (kolumna >= 5) el.classList.add('weekend');
    if (d === dzis.getDate() && miesiac === dzis.getMonth() && rok === dzis.getFullYear()) el.classList.add('dzis');
    siatka.append(el);
  }
}

function przesun(oIle) {
  // Date sam skoryguje miesiąc 12 → styczeń następnego roku i -1 → grudzień poprzedniego
  const nowa = new Date(rok, miesiac + oIle, 1);
  rok = nowa.getFullYear();
  miesiac = nowa.getMonth();
  rysuj();
}

document.getElementById('poprz').addEventListener('click', () => przesun(-1));
document.getElementById('nast').addEventListener('click', () => przesun(1));
rysuj();
Podgląd
Zadanie 5
trudne

Licznik rosnący do wartości (rAF)

Na stronach „o nas” liczby (1250 uczniów, 98 % zdawalności) animują się od 0 do wartości docelowej. Zbuduj to: trzy elementy <span class="licznik" data-cel="1250">0</span> itd. Po kliknięciu „Start” każdy licznik rośnie do swojego data-cel w ciągu 1,5 s z wygładzeniem (easeOut: 1 - (1 - t) ** 3), używając requestAnimationFrame i upływu czasu. Liczby formatuj przez toLocaleString('pl-PL') (separator tysięcy). Kliknięcie „Start” w trakcie animacji ma ją zrestartować, nie uruchomić drugiej.

Pokaż rozwiązanie
Liczniki rAF
const liczniki = [...document.querySelectorAll('.licznik')];
const CZAS = 1500;
const easeOut = (t) => 1 - (1 - t) ** 3;
let idKlatki = null;

function start() {
  cancelAnimationFrame(idKlatki); // restart zamiast drugiej animacji
  const startCzas = performance.now();

  function klatka(teraz) {
    const t = Math.min((teraz - startCzas) / CZAS, 1);
    const postep = easeOut(t);
    for (const el of liczniki) {
      const cel = Number(el.dataset.cel);
      el.textContent = Math.round(cel * postep).toLocaleString('pl-PL');
    }
    if (t < 1) idKlatki = requestAnimationFrame(klatka);
  }
  idKlatki = requestAnimationFrame(klatka);
}

document.getElementById('start').addEventListener('click', start);
start();
Podgląd

Test powtórkowy

Test powtórkowy

Timery, Date, formatowanie i requestAnimationFrame. Jedna poprawna odpowiedź.

0/0poprawnych
Sprawdź się

Co wypisze ten kod?

setTimeout(() => console.log('A'), 100);
setTimeout(() => console.log('B'), 0);
console.log('C');
Sprawdź się

Dlaczego komunikat nigdy się nie pokaże?

function pokaz() { console.log('Czas minął'); }
setTimeout(pokaz(), 2000);
Sprawdź się

Użytkownik klika „Start” trzy razy. Co się stanie?

btn.addEventListener('click', () => {
setInterval(tik, 1000);
});
Sprawdź się

Jaką datę reprezentuje new Date(2026, 11, 24)?

Sprawdź się

Co zwróci new Date(2026, 9, 25).getDay(), jeśli 25 października 2026 to niedziela?

Sprawdź się

Który zapis zwraca liczbę dni między dwiema datami a i b (obie o północy)?

Sprawdź się

Co wypisze console.log(typeof Date(), typeof new Date())?

Sprawdź się

Który zapis wypisze datę w formacie 20.10.2026 dla polskiego użytkownika bez ręcznego składania napisu?

Sprawdź się

Dlaczego do płynnej animacji zaleca się requestAnimationFrame zamiast setInterval(f, 16)?

Sprawdź się

Animacja z requestAnimationFrame przesuwa element o 3 px w każdej klatce. Co się stanie na monitorze 120 Hz w porównaniu z 60 Hz?