ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

jQuery+Vue圆形罗盘时钟:节气算法与动效实现解析

jQuery+Vue圆形罗盘时钟:节气算法与动效实现解析 简介一份结合 jQuery 与 Vue.js 实现的创意圆形罗盘时钟动画资源面向具备一定前端基础、希望学习组件化动画开发或需要参考传统文化主题 UI 效果的开发者。项目以实时节气为亮点将农历二十四节气融入时钟显示兼顾实用性与观赏性适合作为课程设计或作品集素材。压缩包共 8 个文件包含 3 个 CSS、3 个 JS、1 个 HTML 和 1 个工程配置整体约 173KB结构轻量便于直接运行与二次修改。已有 621 人学习浏览。资源内提供完整的 HTML/CSS/JS 实现Vue 负责数据驱动与组件逻辑jQuery 辅助 DOM 操作Canvas 或 SVG 绘制罗盘刻度与指针可直接看到时间更新、节气切换及平滑动画的完整流程同时保留了工程文件和依赖脚本方便提取时钟盘面绘制、动画缓动或节气计算等代码片段适合拆解练习。 这个圆形罗盘时钟项目是我从一个压缩包开始折腾的。文件名直接写明了它的卖点jQuery VUE 双框架带实时节气圆形罗盘造型整体动效走抖音短视频那种炫酷风格。我把它解压跑起来的第一反应是这类创意时钟放在直播间背景、个人主页挂件或者数据大屏的角落都能一眼抓住人。不过今天我不打算介绍怎么改包里的现成文件而是把整个项目从零实现的关键环节拆开节气日期怎么算、圆盘布局怎么摆、Vue 和 jQuery 怎么分工不打架、动效又是怎么一点点磨出来的。这几个点都趟通了你完全可以做一个更有个人风格的版本。1. 项目从0到1罗盘时钟解决什么问题技术组合为什么这么选1.1 打开压缩包之前先想清楚这个圆盘要呈现什么我见过不少做时钟的项目数字时钟、翻转时钟、表盘时钟一大把但把二十四节气、罗盘刻度、实时指针三个元素揉进一个圆盘里的确实不多。这个项目的核心不是计时而是展示节气氛围外圈是二十四节气的环形文字中圈是刻度和十二地支内圈是指针和当前时间读数背景则是不断流动的光效和粒子。想清楚这一点很重要因为它直接决定了后续的技术选型。如果只是显示时间原生 JS 一个setInterval就够了用不上框架。但这里要同时处理节气数据、当前时间、切换动画和罗盘旋转角度还希望以后能扩展成多个实例那就需要一套数据驱动的方式把状态管起来。同时动效部分希望直接用现成的 jQuery 粒子插件和 DOM 动画方法减少重复造轮子的工作。我在做项目拆分时的思路是内容层和数据层交给 Vue 响应式机制视觉层和交互动效交给 jQuery 的 DOM 操作能力。这样各自干各自擅长的事代码边界清楚后期维护也不至于变成一锅粥。1.2 为什么是jQueryVUE而不是一套框架走到底很多人看到 jQuery 和 Vue 混搭会皱眉觉得技术栈不纯。但真实项目里这种组合并不罕见特别是一些从老项目迭代过来的页面jQuery 插件已经写了一大堆强行升级成纯 Vue 成本很高。与其推倒重来不如划清边界让两者共存。我选择这种组合还有一个实际原因抖音风格的动效往往依赖现成的 jQuery 插件比如粒子上浮、光效轨迹、轮盘旋转这些社区里一搜一大把改成原生 Canvas 或者纯 Vue 实现反而要花大量时间调参。Vue 负责把节气和时间这类数据渲染成 DOMjQuery 再对这些 DOM 做动效增强两者通过 DOM 节点和事件衔接是一种务实且高效的方案。当然混搭的前提是明确谁说了算。我在这个项目里定了一条规则Vue 管数据源数据变化通过computed、watch传导到 DOMjQuery 只管动起来不直接修改业务数据。只要守住这条线双框架就不会打架。2. 节气计算不靠查表硬编码靠太阳黄经和偏移量2.1 节气到底是阴历概念还是阳历概念先用一个容易混淆的点开头二十四节气看着像农历其实它本质上是阳历概念。准确说它是由太阳在黄道上的位置决定的——地球绕太阳公转一圈每走过 15 度就对应一个节气所以一年 360 度正好 24 个节气。比如春分点定在黄经 0 度清明在 15 度谷雨在 30 度依此类推。因为节气是天文位置所以它每年公历日期相对固定比如春分基本在 3 月 20 日到 22 日之间冬至基本在 12 月 21 日到 23 日之间。做前端展示时不需要真的去算天体轨道只需要找到一套能把太阳黄经位置转换成具体日期的算法即可。2.2 用一组常数推算出任意年份的24个节气我采用的是社区里流传很广的常数表算法。它的原理是以 1900 年 1 月 6 日 02:05 作为基准点这个时刻正好接近当年的小寒然后利用一个回归年约等于 31556925974.7 毫秒这个常量把年份差换算成时间偏移每个节气再叠加一个以分钟为单位的偏移量就能得到当年第 n 个节气的精确时间。const TERM_NAMES [ 小寒, 大寒, 立春, 雨水, 惊蛰, 春分, 清明, 谷雨, 立夏, 小满, 芒种, 夏至, 小暑, 大暑, 立秋, 处暑, 白露, 秋分, 寒露, 霜降, 立冬, 小雪, 大雪, 冬至 ]; const TERM_OFFSETS [ 0, 21208, 42467, 63836, 85337, 107014, 128867, 150921, 173149, 195551, 218072, 240693, 263343, 285989, 308563, 331033, 353350, 375494, 397447, 419210, 440795, 462224, 483532, 504758 ]; function getTermDate(year, termIndex) { return new Date( 31556925974.7 * (year - 1900) TERM_OFFSETS[termIndex] * 60000 Date.UTC(1900, 0, 6, 2, 5) ); }这里的TERM_OFFSETS[termIndex] * 60000是把偏移量从分钟换算成毫秒。31556925974.7 * (year - 1900)则是从基准年走到目标年的毫秒数。这套算法不是纯物理级精确但近一百年内的误差通常不超过几分钟对前端展示来说绰绰有余。2.3 当前节气和进度条的实时判定拿到每年 24 个节气的日期后剩下的问题是今天到底落在哪个节气区间我的做法是循环对比当天时间和当年的每一个节气找到最后一个早于今天的节气作为当前节气再找到第一个晚于今天的节气作为下一个节气顺便还能算出距离下一个节气还有多少天。function getCurrentTerm(date) { const year date.getFullYear(); let current null; let next null; for (let i 0; i 24; i) { const termDate getTermDate(year, i); if (termDate.getTime() date.getTime()) { current { name: TERM_NAMES[i], date: termDate }; } else { next { name: TERM_NAMES[i], date: termDate }; break; } } if (!next) { next { name: TERM_NAMES[0], date: getTermDate(year 1, 0) }; } return { current, next }; }这里有个容易踩的坑getTermDate返回的是一个基于 UTC 的 Date 对象如果你直接和本地时间比较在东八区以外的浏览器上可能差出一天。稳妥的做法是明确把节气切换时刻固定到 UTC8 时区内部用getTime()统一比较不要在展示层混用getDate()和getUTCDate()。这样做以后不管访问者身处哪个时区看到的当前节气都是一致的。3. 圆形罗盘布局旋转定位和指针系统3.1 24个节气文字怎么均匀围成一圈布局阶段遇到的第一件事就是怎么让 24 个节气文字均匀分布在圆环上。最直接的做法是用 CSS 的旋转偏移把文字先定位到圆盘中心然后顺时针旋转指定角度再沿着旋转后的方向往外平移固定半径最后反向旋转回来让文字始终保持正立。div classterm-ring span v-for(term, i) in TERM_NAMES :keyterm classterm-item :styletermItemStyle(i) {{ term }}/span /div在 Vue 的方法里计算每个元素的角度和位移termItemStyle(i) { const angle i * 15; return { transform: rotate(${angle}deg) translateY(-260px) rotate(${-angle}deg) }; }i * 15是因为 24 个节气均匀分一圈每个占 15 度。translateY(-260px)的内外层半径要配合实际圆盘直径。如果你怕这种双重旋转容易把人绕晕还有一种替代方案用Math.cos和Math.sin直接算出每个文字点的left、top坐标这个方法的好处是逻辑直观但每次调圆环半径都要同步修改两处坐标公式。我实际测试下来双重旋转写法在维护时更省心。3.2 时、分、秒指针怎么做到平滑走动指针动画的要点有两个一是transform-origin必须设置到指针末端不然指针会绕自身的中心旋转二是更新频率和旋转角度的计算要统一。updateHands() { const now new Date(); const ms now.getMilliseconds(); const seconds now.getSeconds() ms / 1000; const minutes now.getMinutes() seconds / 60; const hours (now.getHours() % 12) minutes / 60; this.secondAngle seconds * 6; this.minuteAngle minutes * 6; this.hourAngle hours * 30; }秒针每秒转 6 度分针每分钟转 6 度时针每小时转 30 度这些都是时钟基本盘。为了让秒针不要一顿一顿地跳我把setInterval换成了requestAnimationFrame每帧都重新读取系统时间并更新角度。这样秒针在视觉上会有一种接近机械表的连续感和抖音风格的光效配合起来更顺滑。4. Vue管状态、jQuery管动效的分工与协作4.1 为什么在Vue里写computed和watch把数据关系理清楚Vue 的优势是数据驱动。在这个项目里当前时间、当前节气、下一个节气、距离下一节气的天数这些数据之间是有依赖关系的。与其在每个方法里手动维护不如用computed把派生数据算出来。computed: { currentTerm() { const { current } getCurrentTerm(new Date(this.now)); return current ? current.name : ; }, nextTerm() { const { next } getCurrentTerm(new Date(this.now)); return next ? next.name : ; } }这样只要this.now一变化节气和进度相关的视图会自动刷新。watch则用来监听某个大状态的变化后触发外部动效比如当前节气变了就通知 jQuery 插件做一次切换动画。分工上我始终坚持Vue 只输出数据和状态DOM 操作尽可能交给指令或者 jQuery。4.2 mounted $nextTick把jQuery插件接进来jQuery 插件要操作的是 Vue 渲染出来的真实 DOM所以在 Vue 的生命周期里接入时时机很重要。mounted阶段 DOM 已经挂载但为了确保子组件和v-for列表都渲染完毕我习惯再加一层this.$nextTick()避免拿到一个空节点。mounted() { this.$nextTick(() { $(#compass-container).particleground({ color: #88ccff, density: 12000 }); }); }如果你用的是 Vue 3写法略有不同需要在onMounted里调nextTick。这套思路本质上是把 jQuery 插件当作一个外部 DOM 动画库引入而不是让 jQuery 去接管 Vue 的渲染流程。这样做的好处是Vue 的响应式更新和 jQuery 的逐帧动画互不阻塞。4.3 双框架协作容易踩的两个坑第一个坑是在 jQuery 回调里直接改 Vue 数据。比如粒子插件有个点击回调如果你在回调里写this.currentTerm xxx作用域里的this未必是 Vue 实例。解决办法是提前用箭头函数保存 Vue 实例或者统一走一个事件中心。第二个坑是 Vue 的虚拟 DOM 更新会替换节点导致 jQuery 绑定的事件丢失。最常见的场景是你用v-for生成了 24 个节气文字jQuery 给这些文字绑定了 hover 事件某个时刻 Vue 因为数据变化重新渲染了这个列表原先的事件绑定就全没了。规避思路很直接要么把 jQuery 要操作的 DOM 放在 Vue 渲染区域之外要么用事件委托在容器层级做绑定让子节点变化不影响监听。5. 抖音风格的动效其实由粒子、流光和过渡拼出来5.1 粒子漂浮Canvas循环里的速度、半径和透明度手机屏幕上那种呼吸感很大程度来自漂浮粒子。我用一个透明的 Canvas 盖在罗盘背景上粒子不断向上漂到顶部后重新从底部出现。粒子本身只有三个参数速度、半径、透明度但组合起来效果很好。class Particle { constructor(width, height) { this.x Math.random() * width; this.y Math.random() * height; this.r Math.random() * 2 1; this.speedX (Math.random() - 0.5) * 0.4; this.speedY -Math.random() * 0.6 - 0.2; this.alpha Math.random() * 0.5 0.2; } update(width, height) { this.x this.speedX; this.y this.speedY; if (this.y -10 || this.x -10 || this.x width 10) { this.reset(width, height); } } draw(ctx) { ctx.beginPath(); ctx.arc(this.x, this.y, this.r, 0, Math.PI * 2); ctx.fillStyle rgba(140, 200, 255, ${this.alpha}); ctx.fill(); } }这里的rgba透明度我故意限制在 0.2 到 0.7 之间太亮会抢掉罗盘本身的风头太暗又看不出漂浮感。如果你想要抖音那种更贵的氛围可以把粒子画成细线几颗连成星座状但性能会明显下降移动端慎用。5.2 流光和呼吸光晕先用box-shadow再叠加一个渐变画面中最外圈的光环效果我用的是box-shadow叠加径向渐变。刚开始我直接上了一个很大的box-shadow值结果发现边缘生硬看起来像一圈死白的描边。后来把阴影拆成两层一层近距高亮一层远距离大范围微弱光晕再配合呼吸动画视觉上就柔和多了。.compass { background: radial-gradient(circle at 30% 30%, #1a2a4a, #050b18); box-shadow: 0 0 20px rgba(100, 200, 255, 0.5), 0 0 60px rgba(100, 200, 255, 0.2), inset 0 0 30px rgba(0, 0, 0, 0.6); animation: glowPulse 3s ease-in-out infinite; } keyframes glowPulse { 0%, 100% { box-shadow: 0 0 20px rgba(100, 200, 255, 0.5), 0 0 60px rgba(100, 200, 255, 0.2); } 50% { box-shadow: 0 0 30px rgba(100, 200, 255, 0.8), 0 0 90px rgba(100, 200, 255, 0.35); } }关键点是动画改变的是box-shadow而不是opacity这样阴影范围变化时会有一种光在膨胀收缩的感觉。如果浏览器不支持多层box-shadow动画退路是改用两个伪元素分别模拟近光和远光。5.3 节气切换的过渡动效Vue transition刚好够用节气变化不是每秒都发生但它一旦切换视觉上不能太突兀。我用 Vue 自带的transition组件包住当前节气显示切换时做一次轻微的缩放和透明度变化。transition nameterm-fade modeout-in div :keycurrentTerm classterm-display {{ currentTerm }} /div /transition.term-fade-enter-active, .term-fade-leave-active { transition: opacity 0.6s ease, transform 0.6s ease; } .term-fade-enter-from { opacity: 0; transform: scale(0.9); } .term-fade-leave-to { opacity: 0; transform: scale(1.1); }:keycurrentTerm是关键它告诉 Vue 当节气名称变化时把旧元素移除、新元素插入从而触发过渡动画。这里我不建议用 jQuery 去写 fadeIn/fadeOut因为 Vue 的数据驱动已经能做得很好再用 jQuery 反而会引入额外的竞态问题。6. 打包发布前要过的三关兼容性、性能和移动端6.1 兼容性清单不是所有浏览器都吃mix-blend-mode这个项目里有几个特性在老旧浏览器上容易出问题发布前最好过一遍特性用途兼容建议box-shadow多层阴影光晕IE11 以下只显示最后一层可接受mix-blend-mode颜色融合不支持时用普通透明度替代requestAnimationFrame指针和粒子动画老浏览器需要 polyfillDate.UTC与时间戳节气计算所有现代浏览器都支持如果项目负责人口径是必须兼容 IE11那requestAnimationFrame和Array.prototype.find这类 ES6 语法都要提前打 polyfillVue 本身也要选 2.x 版本。如果只是现代浏览器基本上不用操心太多。6.2 性能优化不要让一个时钟拖垮整页我在调试时遇到过一个问题粒子 Canvas 和box-shadow动画叠加页面帧率从 60 掉到 40风扇直接起飞。后来做了三件事才压回来。第一Canvas 的尺寸按devicePixelRatio缩小一倍绘制原样铺满但像素量只有原来的四分之一画质在手机上肉眼几乎无差别。第二给经常旋转的罗盘层加上will-change: transform让浏览器提前分配合成层避免每帧都重新布局。第三页面切后台时监听visibilitychange事件暂停粒子循环和指针动画回到前台再恢复。这个优化虽然小但在直播间挂件这种长时间运行的场景特别关键。document.addEventListener(visibilitychange, () { if (document.hidden) { cancelAnimationFrame(rafId); } else { loop(); } });6.3 移动端适配触摸、尺寸、处理器最后一步是移动端。罗盘尺寸我用的是vmin单位让圆盘直径始终占视口短边的 90% 左右竖屏横屏都能完整显示。粒子数量根据screen.width动态调整手机屏幕上我把粒子数从 80 降到 40视觉差别不大但帧率明显更稳。触摸上需要注意粒子 Canvas 默认会拦截触摸事件导致页面无法滚动我在 Canvas 上加了pointer-events: none让事件穿透到下层。如果希望用户点击罗盘跳转或触发某个动作就把事件绑定在外层容器上而不是 Canvas 本身。这个项目我跑了大概两周最大的体会是创意时钟这类东西功能不难难在细节统一。节气算法看似复杂其实用常数表一百行以内就能搞定粒子动画看似炫酷核心也只是速度、半径、透明度三参数的组合。最难的部分反而是 jQuery 和 Vue 的协作边界——只要守住了Vue 管数据jQuery 管动效这条线后续加再多的特效都不会乱。如果你也打算做一个类似的罗盘时钟我建议先把节气计算和圆形布局跑通再加动效。基础扎实了剩下的就是怎么让它更好看的问题。本文还有配套的精品资源点击获取
返回列表