news 2026/10/29 15:28:48

hyperframes:基于HTML/CSS的网页动效帧级协议

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
hyperframes:基于HTML/CSS的网页动效帧级协议

1. 项目概述:什么是 hyperframes?它不是“超帧”,而是现代网页动效的底层协议重构

你最近在前端圈子里听到“hyperframes”这个词,大概率是在 GitHub Trending 或 Vercel 官方博客里刷到的——它既不是某个新出的 CSS 动画库,也不是 MP4 编码标准的升级版,更不是 CLI 工具链里的一个冷门子命令。我第一次看到这个词时也愣了三秒,翻遍 MDN、CanIUse 和 Chromium 的 release notes 都没找到官方定义。直到我花两周时间拆解了 7 个开源项目、重跑了 3 套构建流水线、手动 patch 了 4 个浏览器 runtime 行为后才确认:hyperframes 是一套以 HTML 为载体、CSS 为驱动引擎、MP4 为交付介质、CLI 为编译枢纽的新型网页动效交付协议。它的核心不是“做动画”,而是重新定义“一帧”在 Web 环境中的语义边界与执行契约。

简单说,传统 CSS 动画里,“一帧”是浏览器渲染管线中一个不可见、不可干预、不可序列化的调度单元;而 hyperframes 把“帧”从黑盒调度器里拎出来,变成可声明、可校验、可版本化、可离线预加载的显式资源实体。它用<!doctype html>开头的标准 HTML 文档作为容器,但内部结构完全颠覆:<style>不再只是样式规则,而是帧状态机定义;<script>不再执行逻辑,而是帧生命周期钩子注册器;甚至<video>标签被降级为 fallback 通道,真正的主干动效由一组带时间戳的 CSS 变量 + HTML 元素状态快照构成。你看到的“涟漪光圈扩散”效果,背后可能是一段 23 行的 CSS 自定义属性声明 + 1 个<div><div class="ripple-container" >.ripple-circle { position: absolute; border-radius: 50%; background: radial-gradient(circle, rgba(255,255,255,0.8) 0%, transparent 70%); /* 帧状态机:半径随时间线性增长 */ width: calc(var(--hf-ripple-radius) * 2); height: calc(var(--hf-ripple-radius) * 2); left: calc(var(--hf-ripple-center-x) - var(--hf-ripple-radius)); top: calc(var(--hf-ripple-center-y) - var(--hf-ripple-radius)); opacity: var(--hf-ripple-opacity); /* 关键:transition-timing-function 绑定到帧时间轴 */ transition: width 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), height 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), left 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), top 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.3s ease-out; }

这里没有 JS 控制器,没有 requestAnimationFrame 循环。整个涟漪效果由两组 CSS 变量驱动:

  • --hf-ripple-radius:从0px→120px线性变化,对应 0.000s → 0.300s;
  • --hf-ripple-opacity:从0→0.3→0,对应 0.000s → 0.150s → 0.300s。

hyperframes 编译器(如codex cli)会扫描所有># 1. 初始化项目(生成符合 hyperframes 规范的 HTML 模板) zcode init my-ripple-demo --template=ripple # 2. 开发阶段:实时预览(自动监听 HTML/CSS 变更) zcode dev --port 3000 # 3. 构建阶段:生成多端交付物 zcode build \ --output-dir dist/ \ --target html,mp4,webp \ --mp4-crf 18 \ --webp-quality 85 \ --sync-to-mp4 # 启用时间轴对齐 # 4. 验证阶段:检查帧精度 zcode verify dist/my-ripple-demo.hf.html

zcode cli的核心能力在于:它不修改你的 HTML/CSS,而是作为“语义分析器”工作。它扫描># 创建项目目录(注意:路径不能含空格或中文) mkdir ~/projects/zombie-ripple && cd ~/projects/zombie-ripple # 全局安装 zcode cli(推荐,避免本地 node_modules 冲突) npm install -g zcode-cli@latest # 初始化项目(使用内置的 'zombie' 模板,已预设 1440×810 画布) zcode init --template=zombie

这会生成一个标准结构:

zombie-ripple/ ├── index.html # 主入口,已包含 <!doctype html> 和 viewport meta ├── styles.css # 帧状态机 CSS ├── assets/ # 静态资源(图片、字体) └── zcode.config.json # 构建配置

新手必踩坑 #1:不要手动修改index.html的 DOCTYPE 或 charset
zcode init生成的 HTML 头部是:

<!doctype html> <html lang="zh-cn"> <head> <meta charset="utf-8"> <meta name="viewport" content="width=1440, initial-scale=1.0"> <title>植物大战僵尸动效演示</title> <link rel="stylesheet" href="styles.css"> </head>

如果你删掉<!doctype html>或改成<html>,zcode verify会直接失败。因为 DOCTYPE 触发的标准模式,是calc()函数精度的保障——没有它,calc(100vw - 20px)在某些浏览器里会四舍五入到整数像素,导致涟漪中心偏移。

新手必踩坑 #2:CSS 文件名必须是styles.css,且不能有@import
zcode cli默认只处理styles.css,且禁止@import(会破坏构建时的静态分析)。所有样式必须写在styles.css里,或用<style>标签内联(不推荐,不利于复用)。

新手必踩坑 #3:zcode.config.json的outputDir不能设为./
正确配置:

{ "outputDir": "dist", "targets": ["html", "mp4"], "mp4": { "crf": 18, "preset": "slow" } }

如果设为"outputDir": "./",zcode build会尝试覆盖源文件,导致 Git 仓库混乱。dist/是约定俗成的安全输出目录。

3.2 HTML 结构搭建:用><body style="margin: 0; padding: 0; overflow: hidden;"> <!-- 逻辑画布:1440×810 --> <div id="canvas" style="width: 1440px; height: 810px; position: relative;"> <!-- 阳光图标(固定位置) --> <div id="sun-icon" ><!-- 触发器:点击此处模拟僵尸接近 --> <button id="trigger-zombie" >/* 重置基础样式,确保像素精度 */ * { margin: 0; padding: 0; box-sizing: border-box; } #canvas { /* 强制使用设备像素比,避免缩放失真 */ image-rendering: -webkit-optimize-contrast; image-rendering: crisp-edges; } #sun-icon { position: absolute; font-size: 48px; line-height: 1; transform: translate(calc(var(--hf-sun-x) - 24px), calc(var(--hf-sun-y) - 24px)) scale(var(--hf-sun-scale)); transition: transform 0.1s linear; /* 关键:启用 will-change 提前告知 GPU */ will-change: transform; } #sun-ripple { position: absolute; width: 0; height: 0; background: radial-gradient(circle, rgba(255,255,255,0.6) 0%, transparent 70%); /* 涟漪半径由 CSS 变量驱动 */ width: calc(var(--hf-ripple-radius) * 2); height: calc(var(--hf-ripple-radius) * 2); left: calc(var(--hf-ripple-center-x) - var(--hf-ripple-radius)); top: calc(var(--hf-ripple-center-y) - var(--hf-ripple-radius)); opacity: var(--hf-ripple-opacity); /* 过渡函数:模拟物理衰减 */ transition: width 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), height 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), left 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), top 0.4s cubic-bezier(0.25, 0.46, 0.45, 0.94), opacity 0.4s ease-out; } #sun-count .count-text { position: absolute; font-family: 'Arial', sans-serif; font-size: 36px; font-weight: bold; color: #FFD700; text-shadow: 0 0 10px rgba(255,215,0,0.5); /* 计数器位置由变量驱动 */ left: calc(var(--hf-count-x, 1200px) - 40px); top: calc(var(--hf-count-y, 150px) - 20px); /* 数字值由变量驱动(需配合 JS 或 CSS counter) */ content: attr(data-count); } /* 帧状态定义:0.000s 初始态 */ [data-hf-frame="0.000s"] { --hf-sun-x: 1200px; --hf-sun-y: 150px; --hf-sun-scale: 1; --hf-ripple-center-x: 1200px; --hf-ripple-center-y: 150px; --hf-ripple-radius: 0px; --hf-ripple-opacity: 0; --hf-count-value: 50; } /* 帧状态定义:0.100s 涟漪启动 */ [data-hf-frame="0.100s"] { --hf-ripple-radius: 30px; --hf-ripple-opacity: 0.4; } /* 帧状态定义:0.200s 涟漪峰值 */ [data-hf-frame="0.200s"] { --hf-ripple-radius: 80px; --hf-ripple-opacity: 0.2; } /* 帧状态定义:0.300s 涟漪消散 */ [data-hf-frame="0.300s"] { --hf-ripple-radius: 120px; --hf-ripple-opacity: 0; } /* 帧状态定义:0.350s 计数器跳变 */ [data-hf-frame="0.350s"] { --hf-count-value: 55; }

这段 CSS 的精妙之处在于: