'use strict'; const api = window.clockApi; const WEEKDAYS = ['周日', '周一', '周二', '周三', '周四', '周五', '周六']; const stage = document.getElementById('stage'); const digitalView = document.getElementById('digitalView'); const digitalTime = document.getElementById('digitalTime'); const digitalDate = document.getElementById('digitalDate'); const analogView = document.getElementById('analogView'); const badge = document.getElementById('badge'); const closeBtn = document.getElementById('closeBtn'); const hands = { hour: document.getElementById('hourHand'), minute: document.getElementById('minuteHand'), second: document.getElementById('secondHand') }; // 主进程下发的走时参数:目标时间 = baseMs + (真实时间 - baseRealMs) × rate let snapshot = { baseMs: Date.now(), baseRealMs: Date.now(), rate: 1, style: 'digital' }; // 呈现时间层:跳变时由它把「目标时间」缓动过来(数字滚、指针扫都是它的副产品) const presented = presentedTime.create(); function pad2(value) { return String(value).padStart(2, '0'); } function buildDial() { const ticks = document.getElementById('ticks'); const numerals = document.getElementById('numerals'); const ns = 'http://www.w3.org/2000/svg'; for (let i = 0; i < 60; i += 1) { const major = i % 5 === 0; const angle = (i * 6 * Math.PI) / 180; const outer = 90; const inner = major ? 78 : 85; const line = document.createElementNS(ns, 'line'); line.setAttribute('x1', String(Math.sin(angle) * inner)); line.setAttribute('y1', String(-Math.cos(angle) * inner)); line.setAttribute('x2', String(Math.sin(angle) * outer)); line.setAttribute('y2', String(-Math.cos(angle) * outer)); line.setAttribute('class', major ? 'tick tick-major' : 'tick'); ticks.appendChild(line); } for (let h = 1; h <= 12; h += 1) { const angle = (h * 30 * Math.PI) / 180; const text = document.createElementNS(ns, 'text'); text.setAttribute('x', String(Math.sin(angle) * 62)); text.setAttribute('y', String(-Math.cos(angle) * 62)); text.setAttribute('class', 'numeral'); text.textContent = String(h); numerals.appendChild(text); } } // 数字时钟用等宽 tabular 字体,字符宽度与字号成正比,所以量一次比例就能按窗口尺寸精确铺满 let fontRatio = null; function measureFontRatio() { const previous = digitalTime.style.fontSize; digitalTime.style.fontSize = '100px'; const rect = digitalTime.getBoundingClientRect(); fontRatio = { width: rect.width / 100, height: rect.height / 100 }; digitalTime.style.fontSize = previous; } function layoutDigital() { if (snapshot.style !== 'digital' || !fontRatio || !fontRatio.width || !fontRatio.height) return; const box = digitalView.getBoundingClientRect(); const size = Math.min((box.width * 0.9) / fontRatio.width, (box.height * 0.6) / fontRatio.height); digitalTime.style.fontSize = `${Math.max(12, Math.floor(size))}px`; } function renderDigital(ms) { const date = new Date(ms); digitalTime.textContent = `${pad2(date.getHours())}:${pad2(date.getMinutes())}:${pad2(date.getSeconds())}`; digitalDate.textContent = `${date.getFullYear()}-${pad2(date.getMonth() + 1)}-${pad2(date.getDate())} ${WEEKDAYS[date.getDay()]}`; } function renderAnalog(ms) { const date = new Date(ms); // 秒针按毫秒连续走,这样在高倍率/负倍率下看起来是平滑扫过而不是一格一格跳 const seconds = date.getSeconds() + date.getMilliseconds() / 1000; const minutes = date.getMinutes() + seconds / 60; const hours = (date.getHours() % 12) + minutes / 60; hands.hour.setAttribute('transform', `rotate(${hours * 30} 0 0)`); hands.minute.setAttribute('transform', `rotate(${minutes * 6} 0 0)`); hands.second.setAttribute('transform', `rotate(${seconds * 6} 0 0)`); } function updateBadge() { const rate = snapshot.rate; if (rate === 1) { badge.hidden = true; return; } badge.hidden = false; if (rate === 0) badge.textContent = '暂停'; else if (rate < 0) badge.textContent = `倒转 ×${rate}`; else badge.textContent = `×${rate}`; } function applyStyle() { const analog = snapshot.style === 'analog'; digitalView.hidden = analog; analogView.hidden = !analog; if (!analog) layoutDigital(); } function frame() { // 数字与表盘都只从「呈现时间」推导:平时它等于目标时间,跳变后的 0.35 秒里它是缓动中的中间值 const ms = presentedTime.valueAt(presented, Date.now()); if (snapshot.style === 'analog') renderAnalog(ms); else renderDigital(ms); // 自检脚本靠这个读数核对「主进程模型 → IPC → 画面」是否一致 document.body.dataset.displayMs = String(Math.round(ms)); document.body.dataset.style = snapshot.style; requestAnimationFrame(frame); } // —— 自定义拖边缘改大小 —— // 无边框窗口在 Windows 上边缘命中不稳定,所以完全自己做:8 条热区接 pointer 事件, // 用按下瞬间的窗口 bounds 加鼠标位移算出新 bounds,交给主进程 setBounds。 let resize = null; function applyResize(screenX, screenY) { const next = computeBounds( resize.bounds, resize.dir, screenX - resize.startX, screenY - resize.startY ); api.setBounds(next); } for (const handle of document.querySelectorAll('.rz')) { handle.addEventListener('pointerdown', (event) => { if (event.button !== 0) return; const bounds = api.getBoundsSync(); if (!bounds) return; resize = { dir: handle.dataset.dir, startX: event.screenX, startY: event.screenY, bounds }; handle.setPointerCapture(event.pointerId); event.preventDefault(); }); handle.addEventListener('pointermove', (event) => { if (!resize) return; applyResize(event.screenX, event.screenY); }); const stop = (event) => { if (!resize) return; resize = null; try { handle.releasePointerCapture(event.pointerId); } catch { /* 指针已经失效时 release 会抛,忽略即可 */ } }; handle.addEventListener('pointerup', stop); handle.addEventListener('pointercancel', stop); } closeBtn.addEventListener('click', () => api.closeClockWindow()); // 万一鼠标松开的事件丢了(拖拽中窗口失焦等),不能让下一次移动继续改大小 window.addEventListener('blur', () => { resize = null; }); // 主进程每次下发参数都走这里:先让呈现时间层判断这次要不要缓动,再更新画面 function applyState(next) { presentedTime.accept(presented, Date.now(), next); snapshot = next; applyStyle(); updateBadge(); } api.onState(applyState); new ResizeObserver(() => layoutDigital()).observe(stage); buildDial(); measureFontRatio(); api.getState().then((initial) => { applyState(initial); frame(); });