You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SvelteKit项目中集成Codepen颜色变化Canvas效果

如何将Codepen Canvas交互效果集成到SvelteKit项目

你遇到的问题是因为SvelteKit的组件化与服务端渲染特性,直接把原生JS放到全局文件会存在DOM获取时机错误、依赖缺失等问题,正确的集成方式如下:


步骤1:安装依赖

原代码用到了anime.js动画库,先在项目中安装:

npm install animejs
# 或使用pnpm/yarn
pnpm add animejs

步骤2:创建专用Svelte组件

在src/lib/components目录下新建CanvasRipple.svelte文件(无components目录则自行创建),将Canvas交互逻辑封装到组件中:

<script>
  import { onMount } from 'svelte';
  import anime from 'animejs';

  let canvas;
  let ctx;
  let cH;
  let cW;
  let bgColor = "#FF6138";
  let animations = [];

  // 颜色切换工具函数
  const colorPicker = (() => {
    const colors = ["#FF6138", "#FFBE53", "#2980B9", "#282741"];
    let index = 0;
    return {
      next() {
        index = index < colors.length - 1 ? index + 1 : 0;
        return colors[index];
      },
      current() {
        return colors[index];
      }
    };
  })();

  // 移除动画实例
  function removeAnimation(animation) {
    const index = animations.indexOf(animation);
    if (index > -1) animations.splice(index, 1);
  }

  // 计算填充整个画布的半径
  function calcPageFillRadius(x, y) {
    const l = Math.max(x - 0, cW - x);
    const h = Math.max(y - 0, cH - y);
    return Math.sqrt(Math.pow(l, 2) + Math.pow(h, 2));
  }

  // 处理点击/触摸事件
  function handleEvent(e) {
    if (e.touches) { 
      e.preventDefault();
      e = e.touches[0];
    }
    const currentColor = colorPicker.current();
    const nextColor = colorPicker.next();
    const targetR = calcPageFillRadius(e.pageX, e.pageY);
    const rippleSize = Math.min(200, cW * 0.4);
    const minCoverDuration = 750;

    // 圆形元素类
    class Circle {
      constructor(opts) {
        Object.assign(this, opts);
      }
      draw() {
        ctx.globalAlpha = this.opacity || 1;
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.r, 0, 2 * Math.PI, false);
        if (this.stroke) {
          ctx.strokeStyle = this.stroke.color;
          ctx.lineWidth = this.stroke.width;
          ctx.stroke();
        }
        if (this.fill) {
          ctx.fillStyle = this.fill;
          ctx.fill();
        }
        ctx.closePath();
        ctx.globalAlpha = 1;
      }
    }

    // 页面填充动画
    const pageFill = new Circle({
      x: e.pageX,
      y: e.pageY,
      r: 0,
      fill: nextColor
    });
    const fillAnimation = anime({
      targets: pageFill,
      r: targetR,
      duration: Math.max(targetR / 2, minCoverDuration),
      easing: "easeOutQuart",
      complete() {
        bgColor = pageFill.fill;
        removeAnimation(fillAnimation);
      }
    });

    // 波纹扩散动画
    const ripple = new Circle({
      x: e.pageX,
      y: e.pageY,
      r: 0,
      fill: currentColor,
      stroke: {
        width: 3,
        color: currentColor
      },
      opacity: 1
    });
    const rippleAnimation = anime({
      targets: ripple,
      r: rippleSize,
      opacity: 0,
      easing: "easeOutExpo",
      duration: 900,
      complete: removeAnimation
    });

    // 粒子扩散动画
    const particles = [];
    for (let i = 0; i < 32; i++) {
      const particle = new Circle({
        x: e.pageX,
        y: e.pageY,
        fill: currentColor,
        r: anime.random(24, 48)
      });
      particles.push(particle);
    }
    const particlesAnimation = anime({
      targets: particles,
      x: (particle) => particle.x + anime.random(rippleSize, -rippleSize),
      y: (particle) => particle.y + anime.random(rippleSize * 1.15, -rippleSize * 1.15),
      r: 0,
      easing: "easeOutExpo",
      duration: anime.random(1000, 1300),
      complete: removeAnimation
    });
    animations.push(fillAnimation, rippleAnimation, particlesAnimation);
  }

  // 画布尺寸适配
  function resizeCanvas() {
    cW = window.innerWidth;
    cH = window.innerHeight;
    canvas.width = cW * devicePixelRatio;
    canvas.height = cH * devicePixelRatio;
    ctx.scale(devicePixelRatio, devicePixelRatio);
  }

  // 闲置用户自动触发动画
  function handleInactiveUser() {
    function fauxClick(x, y) {
      const fauxEvent = new Event("mousedown");
      fauxEvent.pageX = x;
      fauxEvent.pageY = y;
      document.dispatchEvent(fauxEvent);
    }

    let inactive = setTimeout(() => {
      fauxClick(cW / 2, cH / 2);
    }, 2000);

    function clearInactiveTimeout() {
      clearTimeout(inactive);
      document.removeEventListener("mousedown", clearInactiveTimeout);
      document.removeEventListener("touchstart", clearInactiveTimeout);
    }

    document.addEventListener("mousedown", clearInactiveTimeout);
    document.addEventListener("touchstart", clearInactiveTimeout);
  }

  // 组件挂载后初始化
  onMount(() => {
    ctx = canvas.getContext("2d");
    resizeCanvas();

    // 启动全局动画循环
    anime({
      duration: Infinity,
      update() {
        ctx.fillStyle = bgColor;
        ctx.fillRect(0, 0, cW, cH);
        animations.forEach((anim) => {
          anim.animatables.forEach((animatable) => {
            animatable.target.draw();
          });
        });
      }
    });

    // 绑定事件监听
    document.addEventListener("touchstart", handleEvent);
    document.addEventListener("mousedown", handleEvent);
    window.addEventListener("resize", resizeCanvas);

    handleInactiveUser();

    // 组件销毁时清理资源
    return () => {
      document.removeEventListener("touchstart", handleEvent);
      document.removeEventListener("mousedown", handleEvent);
      window.removeEventListener("resize", resizeCanvas);
    };
  });
</script>

<canvas bind:this={canvas} />

<style>
  canvas {
    display: block;
    width: 100vw;
    height: 100vh;
    position: fixed;
    top: 0;
    left: 0;
    z-index: -1; /* 置于页面内容下方,不影响交互 */
  }
</style>

步骤3:在页面中引入组件

在src/routes/+page.svelte中引入该组件,作为页面背景:

<script>
  import CanvasRipple from '$lib/components/CanvasRipple.svelte';
</script>

<CanvasRipple />

<!-- 页面内容 -->
<div class="content">
  <h1>Hello SvelteKit</h1>
</div>

<style>
  .content {
    position: relative;
    z-index: 1; /* 确保内容在Canvas上方 */
    padding: 2rem;
    color: white;
  }
</style>

为什么直接放app.html/app.css无效?

  1. DOM执行时机错误:SvelteKit默认服务端渲染,全局JS可能在客户端DOM未挂载完成时执行,导致无法获取Canvas元素。
  2. 依赖缺失:原代码依赖anime.js,未安装时会出现变量未定义错误。
  3. 不符合组件化规范:SvelteKit推荐将交互逻辑封装到组件中,全局JS易造成变量污染,难以维护。

内容的提问来源于stack exchange,提问作者LidlDev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 06:50:56