ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

弹簧质点系统(Mass-Spring System):Canvas 模拟软胶果冻物理抖动

弹簧质点系统(Mass-Spring System):Canvas 模拟软胶果冻物理抖动 弹簧质点系统Mass-Spring SystemCanvas 模拟软胶果冻物理抖动在现代高阶 UI 微交互、生动吉祥物萌宠动画以及先锋触控界面设计中“果冻/软胶布丁般的柔体抖动质感Soft-Body Jiggle Physics”是一种能极大提升界面趣味性、亲和力与用户触摸满足感的顶级动效。然而许多前端在手写“果冻效果”时往往只是简单地在 CSS 中修改scaleX和scaleY如scale(1.1, 0.9)简单往复变换。这种伪果冻动效在人眼看来极其单调和机械——因为整个形状是刚性均匀缩放的没有任何局部波浪传递与体积形变。在计算物理与柔体动力学Soft Body Simulation中弹簧-质点系统Mass-Spring System是以极简算力实现真实布料、软胶与果冻物理形变的经典皇冠模型。本文将深入推导弹簧质点力学模型与韦尔莱数值积分Verlet Integration并在 HTML5 Canvas 中手写一个单屏 120fps、鼠标拖拽时产生真实弹性波浪传递的软胶果冻模拟器。弹簧质点系统的三层网格拓扑力学结构要让一个二维物体在受到外力拉扯时既能弹性形变、又不会被扯碎或瞬间坍塌质点之间必须建立三种互相交织的弹簧约束P(0,0) ──[结构弹簧]── P(0,1) ──[结构弹簧]── P(0,2) │ ╲ ╱ │ ╲ ╱ │ │ ╲ [剪切弹簧] ╱ │ ╲ [剪切弹簧] ╱ │ │ ╲ ╱ │ ╲ ╱ │ │ ╲ ╱ │ ╲ ╱ │ P(1,0) ──[结构弹簧]── P(1,1) ──[结构弹簧]── P(1,2) │ │ │ └─── [弯曲弹簧] ────┴─── [弯曲弹簧] ────┘ (跨越两个质点防止过度折叠萎缩)结构弹簧Structural Springs连接相邻的水平与垂直质点维持物体的基础网格轮廓剪切弹簧Shear Springs连接对角线质点防止正方形网格发生压扁平移形变弯曲弹簧Bending Springs跨越一个质点连接隔行隔列的节点维持果冻内部的抗弯刚度与充盈“体积感”弹簧胡克定律与阻尼力方程连接质点 $i$ 与质点 $j$ 的弹簧合力为$$\mathbf{F}_{ij} -k \left( |\mathbf{p}_i - \mathbf{p}_j| - L_0 \right) \frac{\mathbf{p}_i - \mathbf{p}_j}{|\mathbf{p}_i - \mathbf{p}_j|} - c (\mathbf{v}_i - \mathbf{v}_j)$$其中 $L_0$ 为弹簧静止原长$k$ 为刚度系数$c$ 为阻尼耗散系数。韦尔莱数值积分Verlet Integration绝对稳定的物理步进相比于容易在大幅度拉伸时发生能量爆炸崩溃的显式欧拉法位置韦尔莱积分Position Verlet无需显式存储速度具有极高的数值稳定性$$\mathbf{p}_{n1} 2\mathbf{p}n - \mathbf{p}{n-1} \mathbf{a}_n \cdot \Delta t^2$$引入速度衰减因子空气阻力与内部摩擦$$\mathbf{p}_{n1} \mathbf{p}_n (\mathbf{p}n - \mathbf{p}{n-1}) \cdot (1 - \text{friction}) \mathbf{a}_n \cdot \Delta t^2$$// softbody-mass-spring.ts export class PointMass { public x: number; public y: number; public oldX: number; public oldY: number; public pinned: boolean false; // 是否被鼠标钉住抓取 constructor(x: number, y: number) { this.x x; this.y y; this.oldX x; this.oldY y; } public update(friction: number 0.02, gravity: number 0.15) { if (this.pinned) return; const vx (this.x - this.oldX) * (1 - friction); const vy (this.y - this.oldY) * (1 - friction); this.oldX this.x; this.oldY this.y; this.x vx; this.y vy gravity; } } export class SpringLink { public p1: PointMass; public p2: PointMass; public restLength: number; public stiffness: number; constructor(p1: PointMass, p2: PointMass, stiffness: number 0.5) { this.p1 p1; this.p2 p2; this.restLength Math.hypot(p1.x - p2.x, p1.y - p2.y); this.stiffness stiffness; } // 刚度距离约束求解 (Relaxation Pass) public solve() { const dx this.p2.x - this.p1.x; const dy this.p2.y - this.p1.y; const dist Math.hypot(dx, dy) || 1; const diff (dist - this.restLength) / dist; const offsetX dx * diff * 0.5 * this.stiffness; const offsetY dy * diff * 0.5 * this.stiffness; if (!this.p1.pinned) { this.p1.x offsetX; this.p1.y offsetY; } if (!this.p2.pinned) { this.p2.x - offsetX; this.p2.y - offsetY; } } }Canvas 果冻网格平滑轮廓渲染// jelly-canvas-renderer.ts export class JellyStage { private canvas: HTMLCanvasElement; private ctx: CanvasRenderingContext2D; private points: PointMass[] []; private springs: SpringLink[] []; private cols 5; private rows 5; private spacing 32; constructor(canvas: HTMLCanvasElement) { this.canvas canvas; this.ctx canvas.getContext(2d)!; this.buildJellyGrid(200, 150); } private buildJellyGrid(startX: number, startY: number) { // 1. 初始化 5x5 质点网格 for (let r 0; r this.rows; r) { for (let c 0; c this.cols; c) { this.points.push(new PointMass(startX c * this.spacing, startY r * this.spacing)); } } // 2. 编织结构弹簧与剪切弹簧 for (let r 0; r this.rows; r) { for (let c 0; c this.cols; c) { const idx r * this.cols c; // 水平结构弹簧 if (c this.cols - 1) { this.springs.push(new SpringLink(this.points[idx], this.points[idx 1], 0.6)); } // 垂直结构弹簧 if (r this.rows - 1) { this.springs.push(new SpringLink(this.points[idx], this.points[idx this.cols], 0.6)); } // 对角剪切弹簧 if (c this.cols - 1 r this.rows - 1) { this.springs.push(new SpringLink(this.points[idx], this.points[idx this.cols 1], 0.4)); this.springs.push(new SpringLink(this.points[idx 1], this.points[idx this.cols], 0.4)); } } } } public stepAndRender() { // 1. 物理步进 for (const p of this.points) { p.update(0.015, 0.05); // 微重力与低摩擦 } // 2. 多轮约束松弛迭代 (保障果冻刚度) for (let iter 0; iter 4; iter) { for (const s of this.springs) { s.solve(); } } // 3. Canvas 渲染晶莹果冻外观 const w this.canvas.width; const h this.canvas.height; this.ctx.fillStyle #090d16; this.ctx.fillRect(0, 0, w, h); // 绘制外圈平滑轮廓闭包 this.ctx.beginPath(); // 提取外边框质点 const perimeterIndices [0, 1, 2, 3, 4, 9, 14, 19, 24, 23, 22, 21, 20, 15, 10, 5]; for (let i 0; i perimeterIndices.length; i) { const p this.points[perimeterIndices[i]]; if (i 0) this.ctx.moveTo(p.x, p.y); else this.ctx.lineTo(p.x, p.y); } this.ctx.closePath(); // 填充水晶果冻渐变色 const grad this.ctx.createLinearGradient(150, 100, 350, 300); grad.addColorStop(0, rgba(99, 102, 241, 0.85)); grad.addColorStop(1, rgba(236, 72, 153, 0.85)); this.ctx.fillStyle grad; this.ctx.fill(); this.ctx.strokeStyle #ffffff; this.ctx.lineWidth 3; this.ctx.stroke(); } }总结真实的弹性形变与波浪传递是柔体动力学赋予数字交互最神奇的魔法。看清结构弹簧、剪切弹簧与弯曲弹簧在空间中的抗变形约束用韦尔莱数值积分接管质点的物理步进我们就能在纯前端 Canvas 画布上以极轻的算力复现出如水晶软胶果冻般灵动弹颤的 AAA 级物理微交互体验。
返回列表