|
| 1 | +<!doctype html> |
| 2 | +<html lang="cs"> |
| 3 | +<head> |
| 4 | +<meta charset="utf-8"> |
| 5 | +<title>🕒 Hodiny – digitální / ručičkové / 7-segment</title> |
| 6 | +<meta name="viewport" content="width=device-width, initial-scale=1"> |
| 7 | +<link rel="stylesheet" href="../css/style.css"> |
| 8 | +<link rel="stylesheet" href="../css/hw-ui-kit.css"> |
| 9 | +<style> |
| 10 | + /* ===== Layout & UI ===== */ |
| 11 | + .app { max-width: 980px; margin: 24px auto; padding: 12px; } |
| 12 | + .bar { display:flex; gap:12px; align-items:center; flex-wrap:wrap; } |
| 13 | + .bar .spacer { flex:1 } |
| 14 | + .view { display:none } |
| 15 | + .view.active { display:block } |
| 16 | + .tabs button[aria-pressed="true"] { outline: 2px solid #3b82f6; } |
| 17 | + |
| 18 | + /* ===== Digitální (běžný text) ===== */ |
| 19 | + .big { font-size: clamp(32px, 8vw, 96px); letter-spacing: 0.02em; text-align:center; } |
| 20 | + .date { text-align:center; opacity:.75; margin-top: 6px } |
| 21 | + |
| 22 | + /* ===== Ručičkové (canvas kulaté) ===== */ |
| 23 | + .analog-wrap { display:grid; place-items:center; padding: 8px; } |
| 24 | + canvas#analog { width: min(90vw, 460px); height: min(90vw, 460px); max-width: 460px; max-height: 460px; } |
| 25 | + |
| 26 | + .seg-display { |
| 27 | + justify-content:center; |
| 28 | + } |
| 29 | + |
| 30 | + /* dvojtečka mezi HH:MM:SS */ |
| 31 | + .colon { width: 16px; position: relative; } |
| 32 | + .colon::before, .colon::after { |
| 33 | + content: ""; position: absolute; left: 3px; width: 10px; height: 10px; border-radius: 50%; |
| 34 | + background: var(--seg-off); |
| 35 | + } |
| 36 | + .colon.on::before { background: var(--seg-on); box-shadow: 0 0 10px var(--seg-glow); } |
| 37 | + .colon.on::after { background: var(--seg-on); box-shadow: 0 0 10px var(--seg-glow); } |
| 38 | + .colon::before { top: 22px; } .colon::after { bottom: 22px; } |
| 39 | + |
| 40 | + /* drobné utility */ |
| 41 | + .muted { opacity:.7 } |
| 42 | + .row { display:flex; gap:10px; align-items:center; flex-wrap:wrap } |
| 43 | + .right { margin-left:auto } |
| 44 | + label { display:flex; gap:6px; align-items:center } |
| 45 | + input[type="checkbox"]{ transform: scale(1.1) } |
| 46 | +</style> |
| 47 | +</head> |
| 48 | +<body> |
| 49 | + <div class="app"> |
| 50 | + <div class="card"> |
| 51 | + <script type="module" src="../js/site-header.js"></script> |
| 52 | + <site-header title="🕒 Aktuální čas"></site-header> |
| 53 | + </div> |
| 54 | + <div class="card"> |
| 55 | + <div class="bar"> |
| 56 | + <div class="spacer"></div> |
| 57 | + <div class="row"> |
| 58 | + <label><input id="toggleSeconds" type="checkbox" checked> Sekundy</label> |
| 59 | + <label><input id="toggle24h" type="checkbox" checked> 24h</label> |
| 60 | + <label><input id="toggleBlink" type="checkbox" checked> Blikající :</label> |
| 61 | + </div> |
| 62 | + </div> |
| 63 | + <div class="tabs" role="tablist" aria-label="Režim zobrazení"> |
| 64 | + <button class="tab" data-view="digital" aria-pressed="true">Digitální</button> |
| 65 | + <button class="tab" data-view="analog">Ručičkové</button> |
| 66 | + <button class="tab" data-view="seg7">7-segment</button> |
| 67 | + </div> |
| 68 | + </div> |
| 69 | + |
| 70 | + <!-- Digitální --> |
| 71 | + <section id="view-digital" class="card view active" role="tabpanel" aria-label="Digitální"> |
| 72 | + <div class="big" id="textClock">00:00:00</div> |
| 73 | + <div class="date" id="dateText"></div> |
| 74 | + </section> |
| 75 | + |
| 76 | + <!-- Analog --> |
| 77 | + <section id="view-analog" class="card view" role="tabpanel" aria-label="Ručičkové"> |
| 78 | + <div class="analog-wrap"> |
| 79 | + <canvas id="analog" width="460" height="460"></canvas> |
| 80 | + </div> |
| 81 | + </section> |
| 82 | + |
| 83 | + <!-- 7-segment --> |
| 84 | + <section id="view-seg7" class="card view" role="tabpanel" aria-label="7-segment"> |
| 85 | + <div class="seg-display" id="segClock"></div> |
| 86 | + <div class="date" id="dateTextSeg"></div> |
| 87 | + </section> |
| 88 | + </div> |
| 89 | + |
| 90 | +<script type="module"> |
| 91 | + import { createSevenSeg } from '../js/sevenseg.js'; |
| 92 | + // ===== Helpers ===== |
| 93 | + const $ = (s, r=document)=>r.querySelector(s); |
| 94 | + const pad2 = n => n.toString().padStart(2,'0'); |
| 95 | + |
| 96 | + // ===== State & options ===== |
| 97 | + const opts = { |
| 98 | + showSeconds: true, |
| 99 | + use24h: true, |
| 100 | + blinkColon: true |
| 101 | + }; |
| 102 | + |
| 103 | + // Restore from localStorage |
| 104 | + opts.showSeconds = localStorage.getItem('clock_showSeconds') !== '0'; |
| 105 | + opts.use24h = localStorage.getItem('clock_use24h') !== '0'; |
| 106 | + opts.blinkColon = localStorage.getItem('clock_blinkColon') !== '0'; |
| 107 | + $('#toggleSeconds').checked = opts.showSeconds; |
| 108 | + $('#toggle24h').checked = opts.use24h; |
| 109 | + $('#toggleBlink').checked = opts.blinkColon; |
| 110 | + |
| 111 | + // ===== Tabs (views) ===== |
| 112 | + const views = ['digital','analog','seg7']; |
| 113 | + const showView = (v) => { |
| 114 | + views.forEach(id=>{ |
| 115 | + $('#view-'+id).classList.toggle('active', id===v); |
| 116 | + $(`.tab[data-view="${id}"]`).setAttribute('aria-pressed', id===v ? 'true' : 'false'); |
| 117 | + }); |
| 118 | + localStorage.setItem('clock_view', v); |
| 119 | + // If switching to analog view, ensure the canvas is sized and drawn immediately. |
| 120 | + if (v === 'analog') { |
| 121 | + // Resize backing store (ResizeObserver will also do this when visible) |
| 122 | + try { resizeAnalog(); } catch(e) { /* ignore if resizeAnalog not ready */ } |
| 123 | + try { drawAnalog(new Date()); } catch(e) { console.warn('drawAnalog failed on view switch', e); } |
| 124 | + } |
| 125 | + }; |
| 126 | + const savedView = localStorage.getItem('clock_view'); |
| 127 | + if (views.includes(savedView)) showView(savedView); |
| 128 | + document.querySelectorAll('.tab').forEach(btn=>{ |
| 129 | + btn.addEventListener('click', ()=> showView(btn.dataset.view)); |
| 130 | + }); |
| 131 | + |
| 132 | + // ===== Date formatting (Czech locale) ===== |
| 133 | + function formatDate(d){ |
| 134 | + return d.toLocaleDateString('cs-CZ', { weekday:'long', year:'numeric', month:'long', day:'numeric' }); |
| 135 | + } |
| 136 | + |
| 137 | + // ===== Digital text clock ===== |
| 138 | + const textClock = $('#textClock'); |
| 139 | + const dateText = $('#dateText'); |
| 140 | + function formatTime(d){ |
| 141 | + let h = d.getHours(); |
| 142 | + const m = d.getMinutes(); |
| 143 | + const s = d.getSeconds(); |
| 144 | + let suffix = ''; |
| 145 | + if (!opts.use24h) { |
| 146 | + suffix = h>=12 ? ' PM' : ' AM'; |
| 147 | + h = (h%12)||12; |
| 148 | + } |
| 149 | + const base = `${pad2(h)}:${pad2(m)}${opts.showSeconds?':'+pad2(s):''}${suffix}`; |
| 150 | + if (!opts.blinkColon) return base; |
| 151 | + // blink : each second for both digital and 7seg visual |
| 152 | + const colon = (d.getMilliseconds()<500) ? ':' : ' '; |
| 153 | + return opts.showSeconds |
| 154 | + ? `${pad2(h)}${colon}${pad2(m)}${colon}${pad2(s)}${suffix}` |
| 155 | + : `${pad2(h)}${colon}${pad2(m)}${suffix}`; |
| 156 | + } |
| 157 | + |
| 158 | + // ===== Analog clock (canvas) ===== |
| 159 | + const analog = $('#analog'); |
| 160 | + const ag = analog.getContext('2d'); |
| 161 | + function drawAnalog(d){ |
| 162 | + const w = analog.width, h = analog.height; |
| 163 | + const cx = w/2, cy = h/2, R = Math.min(cx, cy) - 12; |
| 164 | + ag.clearRect(0,0,w,h); |
| 165 | + |
| 166 | + // face |
| 167 | + ag.fillStyle = getComputedStyle(document.body).getPropertyValue('--panel-bg') || '#0d1115'; |
| 168 | + ag.strokeStyle = '#223142'; |
| 169 | + ag.lineWidth = 8; |
| 170 | + ag.beginPath(); ag.arc(cx, cy, R, 0, Math.PI*2); ag.fill(); ag.stroke(); |
| 171 | + |
| 172 | + // ticks |
| 173 | + ag.save(); |
| 174 | + ag.translate(cx, cy); |
| 175 | + for (let i=0;i<60;i++){ |
| 176 | + const a = i * Math.PI/30; |
| 177 | + const r1 = R-12, r2 = i%5===0 ? R-32 : R-22; |
| 178 | + ag.lineWidth = i%5===0 ? 4 : 2; |
| 179 | + ag.strokeStyle = i%5===0 ? '#cfe1ff' : '#6b7f95'; |
| 180 | + ag.beginPath(); |
| 181 | + ag.moveTo(r2*Math.cos(a), r2*Math.sin(a)); |
| 182 | + ag.lineTo(r1*Math.cos(a), r1*Math.sin(a)); |
| 183 | + ag.stroke(); |
| 184 | + } |
| 185 | + ag.restore(); |
| 186 | + |
| 187 | + // hands |
| 188 | + const ms = d.getMilliseconds(); |
| 189 | + const sec = d.getSeconds() + ms/1000; |
| 190 | + const min = d.getMinutes() + sec/60; |
| 191 | + const hr = (d.getHours()%12) + min/60; |
| 192 | + |
| 193 | + // hour hand |
| 194 | + drawHand(hr * 30 * Math.PI/180, R*0.5, 7, '#e7eef6'); |
| 195 | + // minute hand |
| 196 | + drawHand(min * 6 * Math.PI/180, R*0.72, 5, '#9fb4cb'); |
| 197 | + // second hand (show only if enabled seconds) |
| 198 | + if (opts.showSeconds) drawHand(sec * 6 * Math.PI/180, R*0.78, 2, '#f43f5e'); |
| 199 | + |
| 200 | + // center cap |
| 201 | + ag.fillStyle = '#e7eef6'; |
| 202 | + ag.beginPath(); ag.arc(cx, cy, 6, 0, Math.PI*2); ag.fill(); |
| 203 | + |
| 204 | + function drawHand(angle, length, width, color){ |
| 205 | + ag.save(); |
| 206 | + ag.translate(cx, cy); |
| 207 | + ag.rotate(angle - Math.PI/2); |
| 208 | + ag.strokeStyle = color; |
| 209 | + ag.lineWidth = width; |
| 210 | + ag.lineCap = 'round'; |
| 211 | + ag.beginPath(); ag.moveTo(-length*0.12, 0); ag.lineTo(length, 0); ag.stroke(); |
| 212 | + ag.restore(); |
| 213 | + } |
| 214 | + } |
| 215 | + |
| 216 | + // ===== 7-segment clock ===== |
| 217 | + const segRoot = $('#segClock'); |
| 218 | + // build HH : MM : SS |
| 219 | + function makeDigit(){ |
| 220 | + const d = document.createElement('div'); |
| 221 | + d.className = 'digit'; |
| 222 | + ['a','b','c','d','e','f','g'].forEach(s=>{ |
| 223 | + const el = document.createElement('div'); el.className = `seg ${s}`; d.appendChild(el); |
| 224 | + }); |
| 225 | + const dp = document.createElement('div'); dp.className = 'dp'; d.appendChild(dp); |
| 226 | + return d; |
| 227 | + } |
| 228 | + function makeColon(){ const c = document.createElement('div'); c.className='colon'; return c; } |
| 229 | + const digits = []; |
| 230 | + function buildSegLayout(){ |
| 231 | + segRoot.innerHTML = ''; |
| 232 | + const order = opts.showSeconds ? ['H1','H2',':','M1','M2',':','S1','S2'] : ['H1','H2',':','M1','M2']; |
| 233 | + order.forEach(tok=>{ |
| 234 | + if (tok === ':') segRoot.appendChild(makeColon()); |
| 235 | + else { const d = makeDigit(); digits.push(d); segRoot.appendChild(d); } |
| 236 | + }); |
| 237 | + } |
| 238 | + // create the DOM layout and wire the reusable 7-seg helper |
| 239 | + let segDisplay = null; |
| 240 | + |
| 241 | + // initial layout |
| 242 | + buildSegLayout(); |
| 243 | + try { segDisplay = createSevenSeg(segRoot); } catch(e) { console.warn('sevenseg init failed', e); } |
| 244 | + |
| 245 | + // ===== Update loop ===== |
| 246 | + function tick(){ |
| 247 | + const now = new Date(); |
| 248 | + try { |
| 249 | + // digital text |
| 250 | + textClock.textContent = formatTime(now); |
| 251 | + dateText.textContent = formatDate(now); |
| 252 | + |
| 253 | + // analog |
| 254 | + if ($('#view-analog')?.classList.contains('active')) drawAnalog(now); |
| 255 | + |
| 256 | + // 7seg |
| 257 | + if ($('#view-seg7')?.classList.contains('active')){ |
| 258 | + const h = now.getHours(); |
| 259 | + const m = now.getMinutes(); |
| 260 | + const s = now.getSeconds(); |
| 261 | + const parts = opts.use24h ? [pad2(h), pad2(m), pad2(s)] : |
| 262 | + [pad2((h%12)||12), pad2(m), pad2(s)]; |
| 263 | + |
| 264 | + const str = opts.showSeconds ? parts.join('') : (parts[0] + parts[1]); |
| 265 | + // delegate rendering to the shared sevenseg module |
| 266 | + if (segDisplay && typeof segDisplay.setValue === 'function') { |
| 267 | + segDisplay.setValue(str); |
| 268 | + } else { |
| 269 | + // fallback: naive per-digit update (keep previous behavior) |
| 270 | + for (let i=0;i<digits.length;i++){ |
| 271 | + const ch = str[i] ?? '0'; |
| 272 | + // update segment classes directly (best-effort) |
| 273 | + digits[i].querySelectorAll('.seg').forEach(segEl=> segEl.classList.toggle('on', false)); |
| 274 | + // try to use DEFAULT_MAP if available |
| 275 | + try { |
| 276 | + // lazy import of DEFAULT_MAP not necessary here; skip fallback mapping |
| 277 | + } catch(_){} |
| 278 | + } |
| 279 | + } |
| 280 | + // dvojtečky blikání |
| 281 | + document.querySelectorAll('.colon').forEach(col=>{ |
| 282 | + col.classList.toggle('on', !opts.blinkColon || (now.getMilliseconds()<500)); |
| 283 | + }); |
| 284 | + $('#dateTextSeg').textContent = formatDate(now); |
| 285 | + } |
| 286 | + } catch (err) { |
| 287 | + // Ensure a single exception doesn't stop the animation loop entirely |
| 288 | + console.error('Clock tick error:', err); |
| 289 | + } finally { |
| 290 | + requestAnimationFrame(tick); |
| 291 | + } |
| 292 | + } |
| 293 | + |
| 294 | + // start the loop |
| 295 | + requestAnimationFrame(tick); |
| 296 | + |
| 297 | + // ===== Options UI ===== |
| 298 | + $('#toggleSeconds').addEventListener('change', (e)=>{ |
| 299 | + opts.showSeconds = e.target.checked; |
| 300 | + localStorage.setItem('clock_showSeconds', opts.showSeconds?'1':'0'); |
| 301 | + digits.length = 0; // reset store |
| 302 | + buildSegLayout(); |
| 303 | + try { segDisplay = createSevenSeg(segRoot); } catch(e) { console.warn('sevenseg init failed after rebuild', e); } |
| 304 | + }); |
| 305 | + $('#toggle24h').addEventListener('change', (e)=>{ |
| 306 | + opts.use24h = e.target.checked; |
| 307 | + localStorage.setItem('clock_use24h', opts.use24h?'1':'0'); |
| 308 | + }); |
| 309 | + $('#toggleBlink').addEventListener('change', (e)=>{ |
| 310 | + opts.blinkColon = e.target.checked; |
| 311 | + localStorage.setItem('clock_blinkColon', opts.blinkColon?'1':'0'); |
| 312 | + }); |
| 313 | + |
| 314 | + // Resize canvas backing store to device pixels |
| 315 | + const resizeAnalog = ()=>{ |
| 316 | + const dpr = Math.max(1, window.devicePixelRatio||1); |
| 317 | + const box = analog.getBoundingClientRect(); |
| 318 | + analog.width = Math.round(box.width * dpr); |
| 319 | + analog.height = Math.round(box.height * dpr); |
| 320 | + }; |
| 321 | + if (window.ResizeObserver) { |
| 322 | + new ResizeObserver(resizeAnalog).observe(analog); |
| 323 | + } else { |
| 324 | + // Fallback for older browsers |
| 325 | + window.addEventListener('resize', resizeAnalog); |
| 326 | + } |
| 327 | + // perform initial sizing |
| 328 | + resizeAnalog(); |
| 329 | +</script> |
| 330 | +</body> |
| 331 | +</html> |
0 commit comments