// ==UserScript==
// @name 拟物时钟
// @namespace http://tampermonkey.net/
// @version 4.0
// @description 带毛玻璃底座的拟物时钟。支持位置/大小/显隐状态记忆。滚轮缩放,边缘拖拽,右键半透明。
// @author 舒凌
// @match *://*/*
// @grant none
// @run-at document-end
// ==/UserScript==
(function() {
'use strict';
// --- 1. 配置与默认值 ---
const STORAGE_KEY = 'glass_clock_settings_v4';
const DEFAULT_SETTINGS = {
top: 50,
right: 50,
size: 160, // 基础直径
isHidden: false // 是否处于“半透明”状态
};
// 读取本地存储
function loadSettings() {
try {
const saved = localStorage.getItem(STORAGE_KEY);
return saved ? JSON.parse(saved) : { ...DEFAULT_SETTINGS };
} catch (e) {
return { ...DEFAULT_SETTINGS };
}
}
// 保存本地存储
function saveSettings(settings) {
localStorage.setItem(STORAGE_KEY, JSON.stringify(settings));
}
let settings = loadSettings();
// --- 2. 注入 CSS 样式 ---
const style = document.createElement('style');
style.innerHTML = `
#glass-clock-wrapper {
position: fixed;
/* 初始位置由 JS 控制 */
z-index: 2147483647;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 20px;
border-radius: 24px;
/* 核心:毛玻璃底座效果 */
background: rgba(255, 255, 255, 0.15);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgba(255, 255, 255, 0.3);
box-shadow: 0 8px 32px 0 rgba(31, 38, 135, 0.15);
transition: opacity 0.3s ease, transform 0.1s;
user-select: none;
cursor: default;
}
/* 半透明模式(右键触发) */
#glass-clock-wrapper.dimmed {
opacity: 0.1 !important;
}
/* 鼠标悬停时恢复清晰 */
#glass-clock-wrapper.dimmed:hover {
opacity: 1 !important;
}
/* 拖拽时的样式 */
#glass-clock-wrapper.dragging {
cursor: grabbing;
transition: none; /* 拖拽时移除过渡动画以防延迟 */
}
/* 调整大小时的鼠标样式 */
.resize-handle {
position: absolute;
bottom: 0;
right: 0;
width: 20px;
height: 20px;
cursor: nwse-resize;
z-index: 10;
}
/* 表盘容器 */
#clock-face {
width: var(--clock-size);
height: var(--clock-size);
border-radius: 50%;
background: radial-gradient(circle at 30% 30%, rgba(255,255,255,0.9), rgba(240,240,240,0.6));
box-shadow:
inset 0 0 10px rgba(0,0,0,0.05),
0 5px 15px rgba(0,0,0,0.1);
position: relative;
margin-bottom: 10px;
}
/* 刻度 */
.tick {
position: absolute;
left: 50%;
top: 0;
width: 2px;
height: 100%;
pointer-events: none;
}
.tick::before {
content: '';
position: absolute;
top: 5px;
left: 0;
width: 100%;
height: 8px;
background: #555;
border-radius: 2px;
}
.tick.major::before {
height: 12px;
width: 3px;
left: -0.5px;
background: #222;
}
/* 指针 */
.hand {
position: absolute;
bottom: 50%;
left: 50%;
transform-origin: bottom center;
border-radius: 4px;
z-index: 2;
}
.hour-hand {
width: 4px;
height: 25%;
background: #333;
margin-left: -2px;
}
.minute-hand {
width: 3px;
height: 35%;
background: #666;
margin-left: -1.5px;
}
.second-hand {
width: 1px;
height: 40%;
background: #d32f2f;
margin-left: -0.5px;
z-index: 3;
}
.center-dot {
position: absolute;
top: 50%;
left: 50%;
width: 8px;
height: 8px;
background: #d32f2f;
border-radius: 50%;
transform: translate(-50%, -50%);
z-index: 4;
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
}
/* 数字时间显示 */
#digital-time {
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
font-size: calc(var(--clock-size) * 0.15);
color: #333;
font-weight: 600;
letter-spacing: 1px;
text-shadow: 0 1px 2px rgba(255,255,255,0.8);
margin-top: 5px;
}
`;
document.head.appendChild(style);
// --- 3. 构建 DOM 结构 ---
const wrapper = document.createElement('div');
wrapper.id = 'glass-clock-wrapper';
// 应用初始设置
wrapper.style.top = settings.top + 'px';
wrapper.style.right = settings.right + 'px';
wrapper.style.setProperty('--clock-size', settings.size + 'px');
if (settings.isHidden) wrapper.classList.add('dimmed');
// 内部 HTML
wrapper.innerHTML = `
<div id="clock-face">
<div class="hand hour-hand"></div>
<div class="hand minute-hand"></div>
<div class="hand second-hand"></div>
<div class="center-dot"></div>
</div>
<div id="digital-time">00:00:00</div>
<div class="resize-handle" title="拖动调整大小"></div>
`;
document.body.appendChild(wrapper);
// 生成刻度
const face = wrapper.querySelector('#clock-face');
for (let i = 0; i < 60; i++) {
const tick = document.createElement('div');
tick.className = i % 5 === 0 ? 'tick major' : 'tick';
tick.style.transform = `rotate(${i * 6}deg)`;
face.appendChild(tick);
}
// --- 4. 逻辑功能 ---
const hourHand = wrapper.querySelector('.hour-hand');
const minuteHand = wrapper.querySelector('.minute-hand');
const secondHand = wrapper.querySelector('.second-hand');
const digitalTime = wrapper.querySelector('#digital-time');
// 更新时间
function updateClock() {
const now = new Date();
const h = now.getHours();
const m = now.getMinutes();
const s = now.getSeconds();
const ms = now.getMilliseconds();
// 平滑秒针角度
const sDeg = (s + ms / 1000) * 6;
const mDeg = (m + s / 60) * 6;
const hDeg = ((h % 12) + m / 60) * 30;
secondHand.style.transform = `rotate(${sDeg}deg)`;
minuteHand.style.transform = `rotate(${mDeg}deg)`;
hourHand.style.transform = `rotate(${hDeg}deg)`;
// 数字时间
const pad = (n) => n.toString().padStart(2, '0');
digitalTime.textContent = `${pad(h)}:${pad(m)}:${pad(s)}`;
requestAnimationFrame(updateClock);
}
updateClock();
// --- 5. 交互事件 ---
// A. 拖拽移动 (Drag to Move)
let isDragging = false;
let startX, startY, initialRight, initialTop;
wrapper.addEventListener('mousedown', (e) => {
// 如果点击的是调整大小的手柄,不触发移动
if (e.target.classList.contains('resize-handle')) return;
isDragging = true;
wrapper.classList.add('dragging');
startX = e.clientX;
startY = e.clientY;
initialRight = parseInt(wrapper.style.right || 0);
initialTop = parseInt(wrapper.style.top || 0);
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
// 计算新位置
const newRight = initialRight - dx;
const newTop = initialTop + dy;
wrapper.style.right = newRight + 'px';
wrapper.style.top = newTop + 'px';
});
window.addEventListener('mouseup', () => {
if (isDragging) {
isDragging = false;
wrapper.classList.remove('dragging');
// 保存位置
settings.right = parseInt(wrapper.style.right);
settings.top = parseInt(wrapper.style.top);
saveSettings(settings);
}
});
// B. 滚轮缩放 (Scroll to Scale)
wrapper.addEventListener('wheel', (e) => {
e.preventDefault();
let newSize = settings.size;
// 向上滚动放大,向下缩小
newSize += e.deltaY > 0 ? -5 : 5;
// 限制最小和最大尺寸
newSize = Math.max(80, Math.min(newSize, 400));
settings.size = newSize;
wrapper.style.setProperty('--clock-size', newSize + 'px');
saveSettings(settings);
}, { passive: false });
// C. 边缘拖拽调整大小 (Resize Handle)
const resizeHandle = wrapper.querySelector('.resize-handle');
let isResizing = false;
let startSize, startMouseX, startMouseY;
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
startSize = settings.size;
startMouseX = e.clientX;
startMouseY = e.clientY;
e.stopPropagation(); // 防止触发父级拖拽
});
window.addEventListener('mousemove', (e) => {
if (!isResizing) return;
// 简单的对角线距离计算来调整大小
const deltaX = startMouseX - e.clientX; // 向左拖变大
const deltaY = startMouseY - e.clientY; // 向上拖变大
// 取变化量的平均值作为缩放依据
const delta = (deltaX + deltaY) / 2;
let newSize = startSize + delta;
newSize = Math.max(80, Math.min(newSize, 400));
settings.size = newSize;
wrapper.style.setProperty('--clock-size', newSize + 'px');
});
window.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
saveSettings(settings);
}
});
// D. 右键切换显隐 (Right Click Toggle)
wrapper.addEventListener('contextmenu', (e) => {
e.preventDefault(); // 阻止默认菜单
settings.isHidden = !settings.isHidden;
if (settings.isHidden) {
wrapper.classList.add('dimmed');
} else {
wrapper.classList.remove('dimmed');
}
saveSettings(settings);
});
})();