js效果使用教程:新手完成第一张作品的方法

js效果使用教程:新手完成第一张作品的方法
想做一个网页视觉效果,不一定要先学框架,也不必从复杂动画开始。只用 HTML、CSS 和 JavaScript,就能做出可以点击、清空和下载的互动作品。本文会带你完成一张“光斑画布”:点击画布添加彩色光斑,也可以随机添加、清空或导出 PNG。
这里所说的“JS 效果”,是指由 JavaScript 控制网页行为或视觉变化的效果,不是某个特定软件或插件的名称。示例使用浏览器原生能力,不需要安装依赖。完成后,你可以在它的基础上继续制作粒子背景、绘图工具、交互卡片或简单的视觉展示页。
一、适合做什么,不适合做什么
JavaScript 视觉效果适合用于网页中的交互展示,例如点击后改变颜色、根据鼠标位置绘图、生成简单图案、控制元素显示与隐藏,以及让用户把画布内容保存为图片。它的优势是部署简单:代码放进网页,就能在现代浏览器中运行。
但“能运行”不代表适用于所有场景。Canvas 很适合绘制大量图形,但绘制结果通常不像普通网页文字那样可以直接搜索、选择或由屏幕阅读器逐项理解;高频动画和超大画布也会占用处理器与内存。如果作品需要复杂图层、专业绘画能力、稳定的跨设备文件处理,或大量可访问的文本内容,就要考虑更合适的工具或方案。
二、先认识作品的组成
- HTML:放置画布和操作按钮,为页面提供结构。
- CSS:设置画布尺寸、颜色、间距和页面布局。
- JavaScript:接收点击事件、记录光斑数据、绘制图形并导出图片。
- Canvas 2D:浏览器提供的二维绘图接口。它负责把图形画到画布上,不需要额外安装库。
这份作品会把每个光斑的位置、半径和色相保存在数组里。用户添加或清空光斑后,程序根据数组重新绘制画布。这样做的好处是页面尺寸变化时可以重画已有内容,而不是只依赖画布上已经绘制、无法方便编辑的像素。
三、完整可运行示例
将下面代码保存为 index.html,再用现代浏览器打开即可运行。代码没有外部依赖,也不会请求网络资源。点击画布添加光斑,“随机添加”会在画布内生成一个光斑,“清空”移除全部内容,“下载 PNG”导出当前画面。
<!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 {
margin: 0;
padding: 32px 16px;
color: #f4f2ff;
background: #12111a;
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
}
main {
width: min(760px, 100%);
margin: 0 auto;
}
h1 {
margin-top: 0;
font-size: clamp(1.5rem, 5vw, 2.2rem);
}
p {
line-height: 1.7;
color: #c9c6d8;
}
.toolbar {
display: flex;
flex-wrap: wrap;
gap: 10px;
margin: 20px 0 12px;
}
button {
min-height: 42px;
padding: 9px 14px;
border: 1px solid #625d79;
border-radius: 8px;
color: #fff;
background: #292638;
font: inherit;
cursor: pointer;
}
button:hover {
background: #39344b;
}
button:focus-visible {
outline: 3px solid #b39cff;
outline-offset: 3px;
}
canvas {
display: block;
width: 100%;
height: 420px;
border: 1px solid #49445d;
border-radius: 12px;
background: #1c1a28;
cursor: crosshair;
touch-action: manipulation;
}
#status {
min-height: 1.5em;
font-size: 0.95rem;
}
</style>
</head>
<body>
<main>
<h1>我的第一张光斑作品</h1>
<p>点击画布添加光斑。你也可以随机添加、清空,或下载当前画面。</p>
<div class="toolbar">
<button id="addButton" type="button">随机添加</button>
<button id="clearButton" type="button">清空</button>
<button id="downloadButton" type="button">下载 PNG</button>
</div>
<canvas
id="art"
aria-label="光斑作品画布。使用下方按钮可以随机添加、清空或下载作品。"
>你的浏览器不支持 Canvas。</canvas>
<p id="status" aria-live="polite">当前光斑:0 个</p>
</main>
<script>
const canvas = document.querySelector("#art");
const ctx = canvas.getContext("2d");
const status = document.querySelector("#status");
const dots = [];
const maxDots = 120;
const cssHeight = 420;
if (!ctx) {
status.textContent = "当前浏览器无法创建 Canvas 2D 绘图环境。";
throw new Error("Canvas 2D is not available");
}
function getCssWidth() {
return canvas.getBoundingClientRect().width;
}
function resizeCanvas() {
const width = getCssWidth();
if (width <= 0) return;
// 限制像素比,避免高像素密度屏幕上画布过大。
const dpr = Math.min(window.devicePixelRatio || 1, 2);
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(cssHeight * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
draw();
}
function draw() {
const width = getCssWidth();
ctx.clearRect(0, 0, width, cssHeight);
for (const dot of dots) {
const gradient = ctx.createRadialGradient(
dot.x, dot.y, 0,
dot.x, dot.y, dot.radius
);
gradient.addColorStop(0, `hsla(${dot.hue}, 95%, 72%, 0.95)`);
gradient.addColorStop(0.45, `hsla(${dot.hue}, 90%, 62%, 0.38)`);
gradient.addColorStop(1, `hsla(${dot.hue}, 90%, 55%, 0)`);
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(dot.x, dot.y, dot.radius, 0, Math.PI * 2);
ctx.fill();
}
status.textContent = `当前光斑:${dots.length} 个`;
}
function addDot(x, y) {
if (dots.length >= maxDots) {
status.textContent = `最多保留 ${maxDots} 个光斑,请先清空再继续。`;
return;
}
dots.push({
x,
y,
radius: 28 + Math.random() * 48,
hue: Math.floor(Math.random() * 360)
});
draw();
}
canvas.addEventListener("pointerdown", (event) => {
const rect = canvas.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
addDot(x, y);
});
document.querySelector("#addButton").addEventListener("click", () => {
const width = getCssWidth();
addDot(Math.random() * width, Math.random() * cssHeight);
});
document.querySelector("#clearButton").addEventListener("click", () => {
dots.length = 0;
draw();
});
document.querySelector("#downloadButton").addEventListener("click", () => {
canvas.toBlob((blob) => {
if (!blob) {
status.textContent = "图片生成失败,请换用较新的浏览器再试。";
return;
}
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "my-light-art.png";
link.click();
URL.revokeObjectURL(url);
}, "image/png");
});
const resizeObserver = new ResizeObserver(resizeCanvas);
resizeObserver.observe(canvas);
resizeCanvas();
</script>
</body>
</html>
四、关键代码怎么理解
1. 点击位置为什么要减去画布偏移量
event.clientX 和 event.clientY 表示指针在浏览器视口中的位置,不是画布内部坐标。getBoundingClientRect() 能取得画布在视口中的位置,因此用指针坐标减去画布左上角坐标,才能得到画布内部的绘图位置。
示例将 Canvas 的绘图坐标设为 CSS 像素,并通过 ctx.setTransform() 处理设备像素比。因此,点击位置可以直接用于绘图,不需要再乘以设备像素比。如果改成用画布实际像素尺寸绘图,就必须相应换算坐标,否则光斑的位置会偏移。
2. 为什么要设置设备像素比
Canvas 有两套尺寸:页面显示的 CSS 尺寸,以及内部像素尺寸。设备像素比(DPR)较高时,如果内部尺寸仍与 CSS 尺寸相同,图形可能显得模糊。示例将内部画布尺寸乘以 DPR,并把 DPR 限制在 2 以内,在清晰度与资源消耗之间做取舍。
这不是所有作品都适用的固定最佳值。画布越大、DPR 越高,像素内存占用通常越多。需要更高精度时可以调整上限,但应该在目标设备上测试,尤其是手机和低性能设备。
3. 为什么保留光斑数据
dots 数组保存每个光斑的坐标、半径和色相。每次数据变化,draw() 会清空画面并重新绘制。这种做法适合本例这种数量较少、结构简单的图形。如果每帧都做大量复杂计算,或要维护数万个对象,就需要进一步优化绘制方式和数据结构。
4. 为什么用 Pointer Events
pointerdown 可统一处理鼠标、触控笔和触摸输入,避免为了不同输入设备分别维护多套点击逻辑。示例还设置了 touch-action: manipulation,减少画布点击与浏览器触摸手势之间的冲突。若要在画布上支持拖动绘图,还需要处理指针移动、按下状态以及触控设备的手势行为。
五、参数边界与可以调整的地方
maxDots = 120:限制光斑数量,避免用户无限添加。这个数值是示例的产品选择,不是 Canvas 的平台限制。数量上限应根据光斑复杂度和目标设备调整。cssHeight = 420:画布逻辑高度,单位为 CSS 像素。修改它时,要同时调整 CSS 中的height,并检查随机添加、清空重绘等逻辑使用的高度是否一致。28 + Math.random() * 48:光斑半径会落在 28 到 76 之间。随机数用于生成视觉变化,不适合用作安全凭证、验证码或加密用途。Math.random() * 360:随机生成色相角度。色相范围按 HSL 色彩模型使用,随机生成的颜色可能不符合品牌色或无障碍对比度要求。Math.min(window.devicePixelRatio || 1, 2):将 DPR 上限设为 2。此值是为了控制本示例的画布开销,不是浏览器规定的最大 DPR。
窗口尺寸变化时,Canvas 的内部尺寸会重设,而重设画布尺寸会清除已有像素。示例因此在重新设置尺寸后调用 draw(),从数据数组恢复画面。若改为直接把绘图内容留在画布上、没有保存可重绘的数据,调整窗口后内容就可能消失。
六、下载功能的安全边界
示例使用 canvas.toBlob() 将画布内容转换为 PNG,再通过对象 URL 触发下载。画布只包含本地绘制的渐变,因此正常情况下可以导出。导出接口可能失败并返回空值,代码对此做了检查。
如果将外部图片绘入 Canvas,情况会不同:当图片来自其他来源且没有满足跨域读取条件时,画布可能被标记为“污染”,浏览器会禁止读取或导出像素数据。仅在图片 URL 上设置 crossOrigin 并不能自动获得权限;图片服务器也必须正确提供相应的跨域响应头。应使用有权使用且服务器配置允许跨域访问的资源,不要尝试绕过浏览器的安全限制。
这里的“下载”是浏览器发起的普通文件下载,不是把文件上传到服务器。示例没有网络请求,也没有收集用户输入。若要增加上传、分享或保存到服务器的功能,需要另行设计服务端接口,并明确告知用户数据会发送到哪里、如何处理。
七、新手常见错误
只设置 CSS 宽高,不设置画布内部尺寸
CSS 只改变显示大小,不会自动把 Canvas 的绘图分辨率提升到合适的像素密度。反过来,只设置 canvas.width 而没有控制 CSS 尺寸,也可能让页面上的画布显示得过大。示例分别处理了显示尺寸和内部尺寸。
画布重设后忘记重画
给 canvas.width 或 canvas.height 赋值会重置绘图状态并清除画面。尺寸变化后应恢复需要的绘图状态,再根据保存的数据绘制内容。
把页面坐标直接当画布坐标
页面布局一旦有边距、滚动或画布不在视口左上角,直接使用 clientX 和 clientY 就会画错位置。先通过 getBoundingClientRect() 转换坐标;如果绘图坐标体系与 CSS 像素不同,还要做对应缩放。
给每个按钮写行内事件
初学时容易在 HTML 上写 onclick。小例子虽然能运行,但把结构和行为分开更容易维护,也更方便使用严格的内容安全策略。示例通过 addEventListener() 绑定事件。
没有给数量和尺寸设边界
没有上限地创建图形、不断叠加事件监听器,或者把画布分辨率设得过大,都可能造成卡顿和资源消耗。交互作品应该明确对象数量、绘制频率和画布尺寸的合理范围,并在目标设备上测试。
把 Canvas 当作可访问的普通内容
Canvas 中绘制的光斑并不会自动成为可读的文字或可操作的控件。示例给画布添加了标签,并提供普通按钮作为操作入口,但这不等于所有画面细节都已适配辅助技术。若作品表达了重要信息,应另提供文字说明或其他可访问的呈现方式。
八、从第一张作品继续改进
建议一次只增加一个功能,并在每次修改后重新测试:可以先把光斑改成用户选择颜色,再添加“撤销上一步”,随后尝试保存作品数据。保存数据时,优先保存数组中的位置、半径和色相等结构化信息;加载数据前要检查格式、字段类型和数量上限,不要把不可信输入直接拼成 HTML 或交给 eval() 执行。
本例的作品目前只存在当前页面运行期间。要增加本地保存,可以研究浏览器的存储能力;要上传到服务器,则应验证请求内容、限制文件大小,并根据实际需求设置访问控制。客户端的参数检查有助于用户体验,但不能代替服务端校验。
完成第一张作品的关键,不是一次塞进很多特效,而是先让一个清晰的交互完整闭环:用户能看懂如何操作,操作后画面有明确变化,出错时有提示,作品也能按预期保存。掌握这套结构后,再把光斑替换为其他图形或数据,你就有了继续制作 JavaScript 视觉作品的基础。









