阿里云主机折上折
  • 微信号
Current Site:Index > The principle of fluid animation: from coffee dripping to page spreading

The principle of fluid animation: from coffee dripping to page spreading

Author:Chuan Chen 阅读数:25122人阅读 分类: 前端综合

Fluid Animation Principles: From Coffee Drops to Page Diffusion

The spreading process of a coffee drop on a table may seem simple, but it contains complex fluid dynamics principles. This natural phenomenon has inspired various fluid animation implementations in front-end development, from water droplet effects to ink diffusion and page transition animations, all of which can be recreated using mathematical models and web technologies.

Fundamentals of Fluid Mechanics and Front-End Implementation

The core of fluid animation lies in simulating the Navier-Stokes equations for viscous fluids, but in front-end implementations, simplified models are typically used. The most basic fluid properties include:

  1. Density field: Represents the "thickness" of the fluid at each position
  2. Velocity field: Describes the direction and speed of fluid particle movement
  3. Pressure field: A key factor influencing fluid diffusion
// Simplified 2D fluid data structure
class FluidField {
  constructor(width, height) {
    this.width = width;
    this.height = height;
    this.density = new Array(width * height).fill(0);
    this.velocityX = new Array(width * height).fill(0);
    this.velocityY = new Array(width * height).fill(0);
  }
  
  // Add density at (x,y)
  addDensity(x, y, amount) {
    const index = this._getIndex(x, y);
    this.density[index] += amount;
  }
  
  // Add velocity at (x,y)
  addVelocity(x, y, amountX, amountY) {
    const index = this._getIndex(x, y);
    this.velocityX[index] += amountX;
    this.velocityY[index] += amountY;
  }
  
  _getIndex(x, y) {
    return Math.floor(y) * this.width + Math.floor(x);
  }
}

Diffusion Algorithm Implementation

The core algorithm for fluid diffusion typically uses iterative solvers. Here's an example implementation based on a WebGL fragment shader, demonstrating how to calculate density diffusion:

precision highp float;

uniform sampler2D u_density;
uniform sampler2D u_velocity;
uniform vec2 u_resolution;
uniform float u_diffusionRate;
uniform float u_dt;

void main() {
  vec2 coord = gl_FragCoord.xy / u_resolution;
  vec4 dens = texture2D(u_density, coord);
  
  // Get neighboring pixels
  vec4 densRight = texture2D(u_density, coord + vec2(1.0, 0.0) / u_resolution);
  vec4 densLeft = texture2D(u_density, coord - vec2(1.0, 0.0) / u_resolution);
  vec4 densTop = texture2D(u_density, coord + vec2(0.0, 1.0) / u_resolution);
  vec4 densBottom = texture2D(u_density, coord - vec2(0.0, 1.0) / u_resolution);
  
  // Diffusion calculation
  vec4 diffusion = (densRight + densLeft + densTop + densBottom - 4.0 * dens) * u_diffusionRate * u_dt;
  
  gl_FragColor = dens + diffusion;
}

Simplified Canvas-Based Implementation

For scenarios that don't require physical accuracy, the Canvas 2D API can be used to achieve visually similar fluid animations. Here's a simplified implementation of an ink diffusion effect:

class InkSimulator {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.width = canvas.width;
    this.height = canvas.height;
    this.particles = [];
    
    // Initialize image data
    this.imageData = this.ctx.createImageData(this.width, this.height);
    this.buffer = new Uint32Array(this.imageData.data.buffer);
  }
  
  addInkDrop(x, y, radius, color) {
    // Create circular ink particles
    for (let i = 0; i < 500; i++) {
      const angle = Math.random() * Math.PI * 2;
      const distance = Math.random() * radius;
      const px = x + Math.cos(angle) * distance;
      const py = y + Math.sin(angle) * distance;
      
      this.particles.push({
        x: px, y: py,
        vx: (Math.random() - 0.5) * 2,
        vy: (Math.random() - 0.5) * 2,
        color: color,
        life: 100 + Math.random() * 50
      });
    }
  }
  
  update() {
    // Clear buffer
    this.buffer.fill(0);
    
    // Update particles
    this.particles = this.particles.filter(p => {
      p.x += p.vx;
      p.y += p.vy;
      p.life--;
      
      // Boundary check
      if (p.x < 0 || p.x >= this.width || p.y < 0 || p.y >= this.height) {
        return false;
      }
      
      // Render to buffer
      const index = Math.floor(p.y) * this.width + Math.floor(p.x);
      this.buffer[index] = this._mixColor(this.buffer[index], p.color);
      
      return p.life > 0;
    });
    
    // Draw to canvas
    this.ctx.putImageData(this.imageData, 0, 0);
  }
  
  _mixColor(dest, src) {
    // Simple color blending
    const a1 = ((dest >> 24) & 0xff) / 255;
    const r1 = (dest >> 16) & 0xff;
    const g1 = (dest >> 8) & 0xff;
    const b1 = dest & 0xff;
    
    const a2 = ((src >> 24) & 0xff) / 255;
    const r2 = (src >> 16) & 0xff;
    const g2 = (src >> 8) & 0xff;
    const b2 = src & 0xff;
    
    const a = a1 + a2 * (1 - a1);
    if (a < 0.001) return 0;
    
    const r = Math.round((r1 * a1 + r2 * a2 * (1 - a1)) / a);
    const g = Math.round((g1 * a1 + g2 * a2 * (1 - a1)) / a);
    const b = Math.round((b1 * a1 + b2 * a2 * (1 - a1)) / a);
    
    return (Math.round(a * 255) << 24) | (r << 16) | (g << 8) | b;
  }
}

Fluid Effects in Page Transitions

Modern web animations often use fluid effects for page transitions. Here's an example of a "liquid" page transition effect implemented with CSS and JavaScript:

<div class="page-transition">
  <div class="liquid-layer"></div>
  <div class="content"></div>
</div>

<style>
.page-transition {
  position: relative;
  overflow: hidden;
}

.liquid-layer {
  position: absolute;
  top: 0;
  left: 0;
  width: 200%;
  height: 100%;
  background: linear-gradient(90deg, #3498db, #9b59b6);
  border-radius: 0 0 50% 50%;
  transform: translateX(-100%);
  transition: transform 1.5s cubic-bezier(0.77, 0, 0.175, 1);
}

.page-transition.active .liquid-layer {
  transform: translateX(0%);
}

.content {
  position: relative;
  z-index: 1;
  opacity: 0;
  transition: opacity 0.5s 0.8s;
}

.page-transition.active .content {
  opacity: 1;
}
</style>

<script>
function activateTransition() {
  const container = document.querySelector('.page-transition');
  container.classList.add('active');
  
  // Simulate loading new content
  setTimeout(() => {
    container.querySelector('.content').innerHTML = '<h1>New Content Loaded</h1>';
  }, 1000);
}
</script>

Performance Optimization Strategies

Fluid animations are often computationally intensive and require special attention to performance:

  1. Resolution Control: Use downsampling for large canvases
// Create low-resolution offscreen canvas
const offscreenCanvas = document.createElement('canvas');
offscreenCanvas.width = mainCanvas.width / 2;
offscreenCanvas.height = mainCanvas.height / 2;
  1. Adaptive Time Step:
let lastTime = 0;
function animate(currentTime) {
  const deltaTime = Math.min((currentTime - lastTime) / 1000, 0.016);
  lastTime = currentTime;
  
  // Update simulation using deltaTime
  fluidSimulator.update(deltaTime);
  requestAnimationFrame(animate);
}
  1. Web Worker Offloading:
// Main thread
const worker = new Worker('fluid-worker.js');
worker.postMessage({
  type: 'init',
  width: 256,
  height: 256
});

// Worker thread (fluid-worker.js)
self.onmessage = function(e) {
  if (e.data.type === 'init') {
    // Initialize fluid simulation
    const simulator = new FluidSimulator(e.data.width, e.data.height);
    
    self.onmessage = function(e) {
      if (e.data.type === 'update') {
        simulator.update(e.data.dt);
        self.postMessage({
          type: 'update',
          density: simulator.getDensityArray()
        });
      }
    };
  }
};

Creative Application Examples

Applying fluid effects to UI interactions can create unique experiences. Here's a button click effect that produces ink ripples:

class RippleButton {
  constructor(button) {
    this.button = button;
    this.canvas = document.createElement('canvas');
    this.ctx = this.canvas.getContext('2d');
    
    // Set canvas dimensions
    this.resize();
    window.addEventListener('resize', () => this.resize());
    
    // Add to button
    this.button.style.position = 'relative';
    this.button.style.overflow = 'hidden';
    this.canvas.style.position = 'absolute';
    this.canvas.style.top = '0';
    this.canvas.style.left = '0';
    this.canvas.style.pointerEvents = 'none';
    this.button.prepend(this.canvas);
    
    // Fluid simulator
    this.simulator = new FluidSimulator(
      Math.floor(this.canvas.width / 4),
      Math.floor(this.canvas.height / 4)
    );
    
    // Click event
    this.button.addEventListener('click', (e) => {
      const rect = this.button.getBoundingClientRect();
      const x = (e.clientX - rect.left) / this.canvas.width;
      const y = (e.clientY - rect.top) / this.canvas.height;
      
      // Add ink at click position
      this.simulator.addInk(
        x * this.simulator.width,
        y * this.simulator.height,
        10, // Radius
        [0.2, 0.5, 0.8, 1.0] // RGBA color
      );
    });
    
    // Animation loop
    this.animate();
  }
  
  resize() {
    this.canvas.width = this.button.offsetWidth;
    this.canvas.height = this.button.offsetHeight;
  }
  
  animate() {
    this.simulator.update(1/60);
    this.render();
    requestAnimationFrame(() => this.animate());
  }
  
  render() {
    this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    
    // Get density field and render
    const density = this.simulator.getDensity();
    const cellWidth = this.canvas.width / this.simulator.width;
    const cellHeight = this.canvas.height / this.simulator.height;
    
    for (let y = 0; y < this.simulator.height; y++) {
      for (let x = 0; x < this.simulator.width; x++) {
        const index = y * this.simulator.width + x;
        const d = density[index];
        
        if (d > 0.01) {
          this.ctx.fillStyle = `rgba(50, 150, 255, ${d})`;
          this.ctx.beginPath();
          this.ctx.arc(
            x * cellWidth + cellWidth/2,
            y * cellHeight + cellHeight/2,
            Math.min(cellWidth, cellHeight) * 0.8 * d,
            0, Math.PI * 2
          );
          this.ctx.fill();
        }
      }
    }
  }
}

In-Depth Discussion of Mathematical Models

More precise fluid simulations require solving the complete Navier-Stokes equations. Here's the simplified form for two-dimensional cases:

  1. Continuity equation (mass conservation): ∇·u = 0

  2. Momentum equation: ∂u/∂t + (u·∇)u = -∇p + ν∇²u + f

Implementing these equations in code requires discretization. Here's an example implementation of the pressure projection step:

class FluidSolver {
  // ...Other methods
  
  project() {
    const { width, height, velocityX, velocityY } = this;
    const divergence = new Array(width * height).fill(0);
    const pressure = new Array(width * height).fill(0);
    
    // Calculate divergence field
    for (let y = 1; y < height - 1; y++) {
      for (let x = 1; x < width - 1; x++) {
        const index = y * width + x;
        divergence[index] = -0.5 * (
          velocityX[index + 1] - velocityX[index - 1] +
          velocityY[index + width] - velocityY[index - width]
        );
      }
    }
    
    // Iteratively solve pressure field (Jacobi method)
    for (let iter = 0; iter < 20; iter++) {
      for (let y = 1; y < height - 1; y++) {
        for (let x = 1; x < width - 1; x++) {
          const index = y * width + x;
          pressure[index] = (
            pressure[index - 1] +
            pressure[index + 1] +
            pressure[index - width] +
            pressure[index + width] -
            divergence[index]
          ) / 4;
        }
      }
    }
    
    // Correct velocity field with pressure field
    for (let y = 1; y < height - 1; y++) {
      for (let x = 1; x < width - 1; x++) {
        const index = y * width + x;
        velocityX[index] -= 0.5 * (pressure[index + 1] - pressure[index - 1]);
        velocityY[index] -= 0.5 * (pressure[index + width] - pressure[index - width]);
      }
    }
  }
}

Application of Modern Web APIs

New Web APIs like WebGPU can significantly improve fluid simulation performance. Here's the basic structure of a WebGPU compute shader:

// Fluid simulation compute shader
@group(0) @binding(0) var<storage, read_write> density: array<f32>;
@group(0) @binding(1) var<storage, read_write> velocityX: array<f32>;
@group(0) @binding(2) var<storage, read_write> velocityY: array<f32>;
@group(0) @binding(3) var<uniform> params: {width: u32, height: u32, dt: f32};

@compute @workgroup_size(8, 8)
fn main(@builtin(global_invocation_id) id: vec3<u32>) {
  let index = id.y * params.width + id.x;
  
  // Boundary check
  if (id.x >= params.width || id.y >= params.height) {
    return;
  }
  
  // Diffusion calculation
  var sum = 0.0;
  var count = 0;
  
  // Check neighboring cells
  for (var dy = -1; dy <= 1; dy++) {
    for (var dx = -1; dx <= 1; dx++) {
      let nx = i32(id.x) + dx;
      let ny = i32(id.y) + dy;
      
      if (nx >= 0 && nx < i32(params.width) && 
          ny >= 0 && ny < i32(params.height)) {
        let neighborIndex = u32(ny) * params.width + u32(nx);
        sum += density[neighborIndex];
        count += 1;
      }
    }
  }
  
  // Update density
  density[index] = mix(density[index], sum / f32(count), 0.1);
  
  // Advection calculation (simplified semi-Lagrangian method)
  let velX = velocityX[index];
  let velY = velocityY[index];
  let srcX = f32(id.x) - velX * params.dt;
  let srcY = f32(id.y) - velY * params.dt;
  
  // Interpolation code omitted here...
}

Interactive Fluid Art Creation

Combining fluid simulation with user input can create interactive art installations. Here's a complete example of fluid motion generated by mouse movement:

class InteractiveFluid {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.width = canvas.width;
    this.height = canvas.height;
    
    // Create offscreen canvas for effect processing
    this.offscreen = document.createElement('canvas');
    this.offscreen.width = this.width;
    this.offscreen.height = this.height;
    this.offscreenCtx = this.offscreen.getContext('2d');
    
    // Fluid parameters
    this.simulator = new FluidSimulator(
      Math.floor(this.width / 4),
      Math.floor(this.height / 4)
    );
    
    // Mouse tracking
    this.lastMouse = { x: 0, y: 0 };
    canvas.addEventListener('mousemove', (e) => {
      const rect = canvas.getBoundingClientRect();
      this.lastMouse = {
        x: (e.clientX - rect.left) / this.width,
        y: (e.clientY - rect.top) / this.height
      };
    });
    
    // Touch support
    canvas.addEventListener('touchmove', (e) =>

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.