如何在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无效?
- DOM执行时机错误:SvelteKit默认服务端渲染,全局JS可能在客户端DOM未挂载完成时执行,导致无法获取Canvas元素。
- 依赖缺失:原代码依赖
anime.js,未安装时会出现变量未定义错误。 - 不符合组件化规范:SvelteKit推荐将交互逻辑封装到组件中,全局JS易造成变量污染,难以维护。
内容的提问来源于stack exchange,提问作者LidlDev
相关产品推荐
相关产品推荐

