
js网页特效使用教程:新手完成第一张作品的方法
网页特效不一定要从复杂的动画库或成套框架开始。一个按钮、一块画布,再加上一段能响应点击的 JavaScript,就足以做出一张可交互的作品。本文会带你完成一个“点击按钮,画布中央出现扩散波纹”的小效果,并说明如何运行、修改和检查它。
示例只使用浏览器内置的 HTML、CSS 和 JavaScript,不需要安装依赖或联网加载资源。你可以把代码保存成一个文件,在浏览器中打开。跟着完成后,你也能把同样的思路用于卡片反馈、按钮动效、提示状态和简单的视觉实验。
一、什么场景适合用 JavaScript 特效
JavaScript 的价值在于根据用户行为或页面状态改变内容。适合使用它的场景包括:
- 操作反馈:点击后显示提示、展开面板、播放轻量的确认动画。
- 数据可视化:根据数据绘制图表、标记或简单的动态示意。
- 互动展示:让用户通过点击、拖动或键盘操作体验页面内容。
- 页面状态变化:根据选择项切换内容,或者在加载、成功、失败等状态间更新界面。
如果只需颜色变化、悬停反馈或简单的淡入,优先考虑 CSS。CSS 转场和动画通常更直接,也不必用 JavaScript 持续更新样式。需要响应用户输入、计算坐标、组合多个状态或绘制图形时,再引入 JavaScript 更合适。特效不应掩盖主要内容,也不应该让关键操作只能通过动画表达。
二、先把第一张作品拆成几件事
本文的作品由三个部分组成:页面里的画布负责绘图;按钮提供明确的操作入口;JavaScript 监听点击并绘制波纹。点击时记录动画开始时间,之后用浏览器的 requestAnimationFrame 按帧更新波纹半径,动画结束后停止更新。
这份示例不需要让动画一直运行。只有点击后才申请动画帧,效果结束就停下来。这样比在页面加载时永久循环更节制,也更适合初学者理解“事件触发动画、动画结束清理”的基本结构。
三、可直接运行的完整代码
复制下面的代码,保存为 first-effect.html,再用现代浏览器打开。它是完整的单文件示例,没有外部库或外部图片。按钮可以用鼠标点击,也可以通过键盘聚焦后按回车或空格触发。
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>我的第一张网页特效</title>
<style>
* {
box-sizing: border-box;
}
body {
min-height: 100vh;
margin: 0;
padding: 24px;
display: grid;
place-items: center;
color: #f3f6ff;
background: #101522;
font-family: system-ui, sans-serif;
}
.work {
width: min(100%, 680px);
}
h1 {
margin: 0 0 8px;
font-size: clamp(1.5rem, 4vw, 2.3rem);
}
p {
color: #bac4d8;
line-height: 1.7;
}
.stage {
position: relative;
overflow: hidden;
min-height: 300px;
margin-top: 24px;
border: 1px solid #3a4560;
border-radius: 20px;
background:
radial-gradient(circle at 50% 45%, #293c64 0, #19243d 45%, #141b2b 100%);
}
canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.stage-content {
position: relative;
z-index: 1;
min-height: 300px;
display: grid;
place-content: center;
justify-items: center;
padding: 24px;
text-align: center;
}
button {
padding: 12px 20px;
border: 0;
border-radius: 999px;
color: #101522;
background: #a7e8ff;
font: inherit;
font-weight: 700;
cursor: pointer;
}
button:hover {
background: #d0f4ff;
}
button:focus-visible {
outline: 3px solid #fff;
outline-offset: 4px;
}
.status {
min-height: 1.5em;
margin: 14px 0 0;
font-size: 0.95rem;
}
</style>
</head>
<body>
<main class="work">
<h1>涟漪实验室</h1>
<p>点击按钮,让画布中央出现一圈扩散的光波。</p>
<section class="stage" aria-label="波纹特效展示区域">
<canvas id="effect" aria-hidden="true"></canvas>
<div class="stage-content">
<button id="play" type="button">生成一圈波纹</button>
<p class="status" id="status" aria-live="polite">等待操作</p>
</div>
</section>
</main>
<script>
const canvas = document.querySelector("#effect");
const button = document.querySelector("#play");
const status = document.querySelector("#status");
const context = canvas.getContext("2d");
const duration = 900;
const maxRadius = 150;
const motionPreference = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
let width = 0;
let height = 0;
let scale = 1;
let animationId = 0;
let startTime = 0;
function resizeCanvas() {
const rect = canvas.getBoundingClientRect();
width = rect.width;
height = rect.height;
// 限制画布分辨率,避免高像素比设备创建过大的绘图缓冲区。
scale = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(width * scale);
canvas.height = Math.round(height * scale);
// 后续绘图继续使用 CSS 像素作为坐标单位。
context.setTransform(scale, 0, 0, scale, 0, 0);
}
function drawRipple(progress) {
context.clearRect(0, 0, width, height);
const radius = maxRadius * progress;
const opacity = 1 - progress;
context.beginPath();
context.arc(width / 2, height / 2, radius, 0, Math.PI * 2);
context.strokeStyle = `rgba(167, 232, 255, ${opacity})`;
context.lineWidth = 3;
context.stroke();
context.beginPath();
context.arc(width / 2, height / 2, radius * 0.62, 0, Math.PI * 2);
context.strokeStyle = `rgba(255, 255, 255, ${opacity * 0.45})`;
context.lineWidth = 1;
context.stroke();
}
function animate(now) {
const progress = Math.min((now - startTime) / duration, 1);
drawRipple(progress);
if (progress < 1) {
animationId = requestAnimationFrame(animate);
} else {
animationId = 0;
context.clearRect(0, 0, width, height);
}
}
function playEffect() {
if (animationId) {
cancelAnimationFrame(animationId);
animationId = 0;
}
if (motionPreference.matches) {
// 用户偏好减少动态效果时,呈现静态结果,不播放扩散动画。
drawRipple(0.55);
status.textContent = "已生成静态波纹";
return;
}
startTime = performance.now();
status.textContent = "波纹正在扩散";
animationId = requestAnimationFrame(animate);
}
button.addEventListener("click", playEffect);
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
</script>
</body>
</html>
四、代码是怎样工作的
1. 画布尺寸和坐标
画布的 CSS 尺寸由容器控制,JavaScript 则读取它在页面上的实际尺寸,并设置画布内部绘图缓冲区。两者不是同一个概念:只用 CSS 放大画布,可能让图形显得模糊;直接把绘图缓冲区设得很大,又会增加内存和绘制成本。
示例将 devicePixelRatio 限制在不高于 2,再用 setTransform 把绘图坐标映射回 CSS 像素。这个上限是示例中的性能取舍,不是浏览器规定的唯一正确值,也不能保证所有设备都同样清晰或流畅。若作品要适配更多设备,应在目标机型上实际测试,权衡画质与资源占用。
2. 用进度计算动画
duration 表示动画时长,单位是毫秒;maxRadius 是波纹扩散到的最大半径,单位是画布坐标里的 CSS 像素。动画每一帧以当前时间减去起始时间,除以时长得到进度,再用 Math.min 将进度限制在 1 以内。
进度从 0 变到 1,半径随之增加,透明度随之减小。这里使用直线变化,便于看懂公式;如果希望波纹开始时慢、随后加速,可以对进度做缓动计算,但应保证用于计算半径和透明度的结果仍处于预期范围。CSS 动画中的缓动曲线不能直接自动套用到 Canvas 绘图,JavaScript 需要自己计算。
3. 为什么要清理动画
按钮可能在动画未结束时再次被点击。代码会先取消尚未完成的动画帧,再开始新一轮效果,避免多个循环同时绘制。动画完成后,它将画布清空,不再继续申请帧。若把这段作品扩展成持续播放的动画,还应提供暂停入口,并在页面隐藏时考虑暂停不必要的计算。
4. 减少动态效果偏好
prefers-reduced-motion 是浏览器提供的媒体偏好,用于读取用户系统是否设置了减少动态效果。示例在此偏好启用时显示静态波纹,不播放扩散过程。它不是对所有动画行为的自动禁令,也不会替开发者完成无障碍设计;应由页面根据效果特点决定如何降动效,并确保信息和功能仍然可用。
五、可以怎样修改这张作品
- 调整颜色:修改
strokeStyle中的 RGBA 数值。前三个数字表示红、绿、蓝,最后一个数字表示透明度,通常取 0 到 1 之间的值。 - 调整速度:把
duration从 900 改成更大的数,动画会持续更久;减小数值则会更快。浏览器每秒能绘制多少帧并不由这个参数保证。 - 调整扩散范围:修改
maxRadius。半径以 CSS 像素为单位,实际视觉效果还取决于容器尺寸。 - 改变画布外观:调整
.stage的背景、边框和最小高度。绘图区域随容器尺寸变化,因此不同屏幕上可见效果可能不同。 - 增加触发方式:可以监听其他按钮或特定页面事件,但要避免无意中让动画连续触发。新增控件时保留清楚的焦点样式与键盘操作。
建议每次只改一个参数,保存并刷新后观察差异。若同时改动动画时长、半径、画布尺寸和样式,出现问题时就不容易判断原因。
六、常见错误与排查方法
打开页面后按钮没有反应
先检查代码是否完整复制,尤其是脚本是否放在对应 HTML 元素之后,以及选择器中的 id 是否和页面一致。然后打开浏览器开发者工具的控制台查看报错。复制代码时如果漏掉括号、引号或结束标签,脚本可能会停止执行。
画布是空白或尺寸不对
检查画布是否有可见尺寸。画布的 CSS 宽高和 canvas.width、canvas.height 分工不同:前者控制页面布局,后两者控制绘图缓冲区。只设置其中一组,可能出现拉伸、模糊或坐标不匹配。容器尺寸变化后也要重新计算画布尺寸。
画面发虚或耗费资源
高像素比屏幕会让绘图缓冲区面积快速增大,而绘制工作量大致会随宽高乘积增长。示例将像素比上限设为 2,目的是控制这个成本;如果容器特别大、特效数量很多或目标设备性能较弱,还需要进一步降低分辨率、减少绘图对象或缩短持续时间。
快速连点后出现多重动画
确认每次开始动画前都处理上一轮的动画帧。若还有定时器、事件监听器或其他动画循环,也要分别管理和清理。不要仅通过不断创建新的循环来重置效果。
页面缩放或调整窗口后坐标错位
绘制坐标应以当前画布尺寸为准。示例在窗口尺寸变化时重新设置缓冲区。更复杂的页面可以用 ResizeObserver 观察具体容器,而不是只监听窗口;不过要避免在尺寸回调中无条件触发大量同步布局计算。
七、关键边界:参数、性能与浏览器行为
示例参数是为了展示思路,不是适用于所有项目的固定限制。duration、maxRadius 和像素比上限都可以调整,但值越大不代表效果必然更好。尺寸过大的画布会增加内存和绘制成本;过长或频繁的动画会影响耗电、流畅度和页面可操作性。
requestAnimationFrame 用于请求浏览器在合适的绘制时机运行回调,通常比用固定间隔的定时器模拟逐帧绘图更适合此类效果。但它不承诺固定帧率,也不保证动画在所有设备上以相同速度呈现。后台标签页、低性能设备和系统负载都可能影响实际绘制节奏,所以要避免用“每帧增加固定像素”的方式假定帧率恒定。示例依据时间计算进度,就是为了让时长更少依赖帧数。
Canvas 绘制的图形本身不是普通 DOM 文本,通常不能像 HTML 元素那样直接被辅助技术读取或选择。此示例把画布设为装饰性内容,并用真实按钮触发、用状态文字反馈结果。若画布承载重要数据,应提供相应的文本说明、表格或可访问的 DOM 表达,不能把关键含义只画在画布上。
八、安全用途与需要避免的做法
这份作品在画布上绘制固定的圆形,不解析用户输入,也不调用网络服务,因此没有必要引入 HTML 字符串拼接。不要为了让页面“更动态”,把不可信输入直接交给 innerHTML:它会把字符串按 HTML 解析,可能造成跨站脚本风险。显示普通文本时,优先使用 textContent;如果业务确实需要展示用户提供的富文本,应采用经过审查的消毒方案,并按具体上下文处理。
不要把动画参数校验误当成安全防护。限制半径或时长有助于控制体验与资源消耗,却不构成权限控制、身份验证或数据保护。客户端代码可以被用户查看和修改,不能把秘密密钥、服务端权限判断或可信业务规则只放在浏览器里。涉及敏感数据和访问权限时,应在服务端执行必要验证。
同样,不要把 JavaScript 特效当作安全工具来使用。Canvas 波纹、遮罩或视觉提示不会保护页面内容,也不能阻止复制、截屏或绕过前端逻辑。安全边界应由正确的服务端校验、权限控制和合适的安全策略建立,而不是由动画效果建立。
九、从练习走向实际项目
完成后,可以尝试把波纹改成状态提示、给画布增加不同的颜色主题,或在点击位置绘制图形。扩展时保持几个原则:操作有明确入口;效果有结束条件;信息不只靠动画传达;画布尺寸和绘图坐标保持一致;不需要的循环及时停止。
若项目已经使用组件框架,应把事件监听、动画帧和画布资源的创建与清理放在组件生命周期内,避免组件卸载后回调仍在运行。若页面里有多个相同特效,应避免为每个区域创建不受管理的永久循环。需要更复杂的图形、物理模拟或大量对象时,再评估 Canvas、SVG、CSS 动画或专业图形库各自的取舍,而不是一开始就引入多余工具。
结语
第一张 JavaScript 网页特效不必复杂:先用 HTML 提供结构,用 CSS 建立视觉区域,再让 JavaScript 响应一次真实操作。本文的点击波纹展示了事件监听、画布尺寸、时间驱动动画、重绘和资源清理这些基础方法。先让单文件示例稳定运行,再逐项修改参数、检查键盘操作和设备表现,你就拥有了一条可复用的创作起点。




