将鼠标流体动画从JS转换为单页Svelte实现求助
完整Svelte页面实现流体拖拽动画
以下是适配后的单个Svelte页面代码,直接复制即可运行:
<script> import { onMount, onDestroy } from 'svelte'; let canvas; let ctx; let animationId; const mouse = { x: 0, y: 0, px: 0, py: 0, down: false }; const canvas_width = 500; const canvas_height = 500; const resolution = 10; const pen_size = 40; const num_cols = canvas_width / resolution; const num_rows = canvas_height / resolution; const speck_count = 5000; let vec_cells = []; let particles = []; class Cell { constructor(x, y, res) { this.x = x; this.y = y; this.r = res; this.col = 0; this.row = 0; this.xv = 0; this.yv = 0; this.pressure = 0; } } class Particle { constructor(x, y) { this.x = this.px = x; this.y = this.py = y; this.xv = this.yv = 0; } } function init() { ctx = canvas.getContext('2d'); canvas.width = canvas_width; canvas.height = canvas_height; // 初始化粒子 for (let i = 0; i < speck_count; i++) { particles.push(new Particle(Math.random() * canvas_width, Math.random() * canvas_height)); } // 初始化网格单元 for (let col = 0; col < num_cols; col++) { vec_cells[col] = []; for (let row = 0; row < num_rows; row++) { const cell_data = new Cell(col * resolution, row * resolution, resolution); vec_cells[col][row] = cell_data; vec_cells[col][row].col = col; vec_cells[col][row].row = row; } } // 建立网格单元的邻接关系 for (let col = 0; col < num_cols; col++) { for (let row = 0; row < num_rows; row++) { const cell_data = vec_cells[col][row]; const row_up = (row - 1 >= 0) ? row - 1 : num_rows - 1; const col_left = (col - 1 >= 0) ? col - 1 : num_cols - 1; const col_right = (col + 1 < num_cols) ? col + 1 : 0; const up = vec_cells[col][row_up]; const left = vec_cells[col_left][row]; const up_left = vec_cells[col_left][row_up]; const up_right = vec_cells[col_right][row_up]; cell_data.up = up; cell_data.left = left; cell_data.up_left = up_left; cell_data.up_right = up_right; up.down = vec_cells[col][row]; left.right = vec_cells[col][row]; up_left.down_right = vec_cells[col][row]; up_right.down_left = vec_cells[col][row]; } } startAnimation(); } function updateParticle() { for (let i = 0; i < particles.length; i++) { const p = particles[i]; if (p.x >= 0 && p.x < canvas_width && p.y >= 0 && p.y < canvas_height) { const col = parseInt(p.x / resolution); const row = parseInt(p.y / resolution); const cell_data = vec_cells[col][row]; const ax = (p.x % resolution) / resolution; const ay = (p.y % resolution) / resolution; p.xv += (1 - ax) * cell_data.xv * 0.05; p.yv += (1 - ay) * cell_data.yv * 0.05; p.xv += ax * cell_data.right.xv * 0.05; p.yv += ax * cell_data.right.yv * 0.05; p.xv += ay * cell_data.down.xv * 0.05; p.yv += ay * cell_data.down.yv * 0.05; p.x += p.xv; p.y += p.yv; const dx = p.px - p.x; const dy = p.py - p.y; const dist = Math.sqrt(dx * dx + dy * dy); const limit = Math.random() * 0.5; ctx.lineWidth = 1; ctx.beginPath(); if (dist > limit) { ctx.moveTo(p.x, p.y); ctx.lineTo(p.px, p.py); } else { ctx.moveTo(p.x, p.y); ctx.lineTo(p.x + limit, p.y + limit); } ctx.stroke(); p.px = p.x; p.py = p.y; } else { p.x = p.px = Math.random() * canvas_width; p.y = p.py = Math.random() * canvas_height; p.xv = 0; p.yv = 0; } p.xv *= 0.5; p.yv *= 0.5; } } function draw() { const mouse_xv = mouse.x - mouse.px; const mouse_yv = mouse.y - mouse.py; for (let i = 0; i < vec_cells.length; i++) { const cell_datas = vec_cells[i]; for (let j = 0; j < cell_datas.length; j++) { const cell_data = cell_datas[j]; if (mouse.down) { changeCellVelocity(cell_data, mouse_xv, mouse_yv, pen_size); } updatePressure(cell_data); } } ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.strokeStyle = '#00FFFF'; updateParticle(); for (let i = 0; i < vec_cells.length; i++) { const cell_datas = vec_cells[i]; for (let j = 0; j < cell_datas.length; j++) { const cell_data = cell_datas[j]; updateVelocity(cell_data); } } mouse.px = mouse.x; mouse.py = mouse.y; animationId = requestAnimationFrame(draw); } function changeCellVelocity(cell_data, mvelX, mvelY, pen_size) { const dx = cell_data.x - mouse.x; const dy = cell_data.y - mouse.y; const dist = Math.sqrt(dy * dy + dx * dx); if (dist < pen_size) { if (dist < 4) dist = pen_size; const power = pen_size / dist; cell_data.xv += mvelX * power; cell_data.yv += mvelY * power; } } function updatePressure(cell_data) { const pressure_x = ( cell_data.up_left.xv * 0.5 + cell_data.left.xv + cell_data.down_left.xv * 0.5 - cell_data.up_right.xv * 0.5 - cell_data.right.xv - cell_data.down_right.xv * 0.5 ); const pressure_y = ( cell_data.up_left.yv * 0.5 + cell_data.up.yv + cell_data.up_right.yv * 0.5 - cell_data.down_left.yv * 0.5 - cell_data.down.yv - cell_data.down_right.yv * 0.5 ); cell_data.pressure = (pressure_x + pressure_y) * 0.25; } function updateVelocity(cell_data) { cell_data.xv += ( cell_data.up_left.pressure * 0.5 + cell_data.left.pressure + cell_data.down_left.pressure * 0.5 - cell_data.up_right.pressure * 0.5 - cell_data.right.pressure - cell_data.down_right.pressure * 0.5 ) * 0.25; cell_data.yv += ( cell_data.up_left.pressure * 0.5 + cell_data.up.pressure + cell_data.up_right.pressure * 0.5 - cell_data.down_left.pressure * 0.5 - cell_data.down.pressure - cell_data.down_right.pressure * 0.5 ) * 0.25; cell_data.xv *= 0.99; cell_data.yv *= 0.99; } function startAnimation() { draw(); } function stopAnimation() { if (animationId) cancelAnimationFrame(animationId); } function handleMouseDown(e) { e.preventDefault(); mouse.down = true; } function handleMouseUp() { mouse.down = false; } function handleMouseMove(e) { e.preventDefault(); mouse.px = mouse.x; mouse.py = mouse.y; mouse.x = e.offsetX || e.layerX; mouse.y = e.offsetY || e.layerY; } function handleTouchStart(e) { e.preventDefault(); const rect = canvas.getBoundingClientRect(); mouse.x = mouse.px = e.touches[0].pageX - rect.left; mouse.y = mouse.py = e.touches[0].pageY - rect.top; mouse.down = true; } function handleTouchEnd(e) { if (!e.touches) mouse.down = false; } function handleTouchMove(e) { e.preventDefault(); mouse.px = mouse.x; mouse.py = mouse.y; const rect = canvas.getBoundingClientRect(); mouse.x = e.touches[0].pageX - rect.left; mouse.y = e.touches[0].pageY - rect.top; } onMount(() => { init(); return () => { stopAnimation(); }; }); onDestroy(() => { stopAnimation(); }); </script> <style> @import url("https://fonts.googleapis.com/css?family=Open+Sans:300,400,600,700"); body { margin: 0; background: #000; font-family: "Open Sans"; } h1 { color: #ffffff; text-align: center; font-size: 20px; } canvas { position: absolute; top: 0; right: 0; bottom: 0; left: 0; margin: auto; border: 2px solid white; } </style> <main> <h1>Click and drag!</h1> <canvas bind:this={canvas} on:mousedown={handleMouseDown} on:mouseup={handleMouseUp} on:mousemove={handleMouseMove} on:touchstart={handleTouchStart} on:touchend={handleTouchEnd} on:touchmove={handleTouchMove} /> </main>
关键适配说明
- 使用Svelte的
bind:this获取canvas元素,替代原生的getElementById - 用
onMount钩子初始化动画逻辑,组件销毁时用onDestroy取消动画帧,避免内存泄漏 - 将原生的构造函数改为ES6类,更符合现代JS规范
- 用Svelte的事件绑定处理鼠标和触摸事件,替代原生的
addEventListener
内容的提问来源于stack exchange,提问作者Louis
相关产品推荐
相关产品推荐

