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

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 视觉作品的基础。

© 版权声明

相关文章