Sea Creatures with Elementary Functions

By Juan Carlos Ponce Campuzano, 16/Feb/2026


Introduction

I recently came across the work of the coder and artist known as @yuruyurau, and I was immediately drawn to the elegance and compactness of their visual sketches made with p5.js. In particular, a piece shared in this post beautifully demonstrates how fluid, organic motion can emerge from what appears to be a short line of code with only 261 characters.

As a mathematician, I became curious about how these animations actually work. How can such intricate, almost underwater-like forms arise from simple elementary functions? That curiosity led me to recreate and explore similar ideas using p5.js, focusing on sine, cosine, and other basic mathematical relationships.

This post is a reflection of my (almost obsessive) exploration. It is an attempt to unpack and understand the mathematical structure behind these mesmerizing animations. My goal is to show how layered simplicity, built from elementary functions, can generate surprisingly rich and organic visual behaviour.


Step 1: Basic Grid Structure

We begin with the simplest possible structure: a static grid of points. This establishes our foundation for transforming linear indices into 2D coordinates.

let t = 0;

function setup() {
  createCanvas(500, 500);
  background(0);
  stroke(255);
}

function draw() {
  background(0);
  
  // Create a grid of points
  for (let i = 0; i < 20000; i++) {
    // Convert linear index to 2D coordinates
    const x = i % 200;
    const y = floor(i / 200);
    
    // Center the grid
    const k = x - 100;
    const e = y - 50;
    
    // Place points with some spacing
    const px = width / 2 + k * 2;
    const py = height / 2 + e * 2;
    
    point(px, py);
  }
}

Key concepts

  • Linear index to 2D mapping: Converting a single loop index into grid coordinates using x = i % N and y = floor(i / N).
  • Centering the coordinate system: Shifting the grid so the origin sits at the canvas centre (k = x - a, e = y - b).
  • Structured point field: Establishing a static lattice as the foundation for later transformations.

Step 2: Introducing Polar Coordinates and a Gentle Animation

In this step, we introduce polar coordinates using the mag() function for distance calculation and atan2() for angle calculation and also the time variable t to animate.

let t = 0;

function setup() {
  createCanvas(500, 500);
  pixelDensity(2);
  background(0);
  stroke(255, 120);
}

function draw() {
  background(0, 40);

  t += 0.01; // Update time for animation
  
  for (let i = 0; i < 20000; i++) {
    const x = i % 200;
    const y = floor(i / 200);
    
    const k = x - 100;
    const e = y - 50;
    
    // Calculate distance from center (magnitude)
    const o = mag(k, e) / 50;
    
    // Convert to angle
    const c = atan2(e, k);
    
    // Simple circular pattern
    const px = width / 2 + (k + o * 10 * cos(c + t)) * 2;
    const py = height / 2 + (e + o * 10 * sin(c + t)) * 2;
    
    point(px, py);
  }
} 

Key Concepts

  • Polar coordinates: Radius r = sqrt(k² + e²) and angle θ = atan2(e, k).
  • Distance-based modulation: Using radial distance to control displacement magnitude.
  • Time parameter: Introducing a variable t that increments every frame.
  • Angular displacement: Circular motion generated with cos(θ + t) and sin(θ + t).

Step 3: Adding Time-Based Animation and Gentle Distortion

Now we create our first animated distortion effect using sinusoidal functions.

let t = 0;

function setup() {
  createCanvas(500, 500);
  pixelDensity(2);
  background(0);
  stroke(255, 80);
}

function draw() {
  background(0, 40);
  
  t += 0.02;
  
  for (let i = 0; i < 20000; i++) {
    const x = i % 200;
    const y = floor(i / 200);
    
    const k = x - 100;
    const e = y - 50;
    
    const o = mag(k, e) / 50;
    const c = atan2(e, k);
    
    // Add time-based oscillation
    const q = o * 20 * sin(c * 2 + t);
    
    const px = width / 2 + (k + q * cos(c)) * 2;
    const py = height / 2 + (e + q * sin(c)) * 2;
    
    point(px, py);
  }
} 

Key Concepts

  • Sinusoidal modulation: Creating wave-like motion with sin(2θ + t).
  • Radial amplification: Scaling oscillations by distance from the centre.
  • Directional displacement: Applying deformation along radial directions using cos(θ) and sin(θ).
  • Emergent wave patterns: Interference between angle and time produces organic motion.

Step 4: Complex Warping Functions

We increase complexity by adding multiple trigonometric functions and changing the coordinate scaling.

let t = 0;

function setup() {
  createCanvas(500, 500);
  pixelDensity(2);
  background(0);
  stroke(255, 80);
}

function draw() {
  background(0, 40);
  
  t += 0.03;
  
  for (let i = 0; i < 30000; i++) {
    const x = i % 250;
    const y = floor(i / 250);
    
    // Different centering and scaling
    const k = x / 2 - 62.5;
    const e = y / 2 - 50;
    
    const o = mag(k, e) / 20;
    const c = o * e / 20 - t / 5;
    
    // More complex warping function
    const q = x + o * k * sin(2 * o - t);
    
    const px = width / 2 + q * sin(c);
    const py = height / 2 + (y / 2) * cos(2 * c - t) - q * cos(c);
    
    point(px, py);
  }
} 

Key Concepts

  • Rescaling and reframing: Adjusting coordinate scaling to alter density and proportions.
  • Composite angle definitions: Building angles from multiple variables (distance, position, time).
  • Layered trigonometric transformations: Combining sine and cosine functions in nested expressions.
  • Nonlinear warping: Distorting the grid using multiplicative interactions such as o * k * sin(...).

Step 5: Final Organic Animation

The complete sketch with all transformations combined, creating the organic sea-creature motion.

let t = 0;

function setup() {
  createCanvas(500, 500);
  pixelDensity(2);
  background(0);
  stroke(255, 80);
}

function draw() {
  background(0, 60);

  t += 0.05;

  for (let i = 0; i < 40000; i++) {
    // High density grid with different modulo division
    const x = i / 4 % 100;
    const y = floor(i / 150);

    // Recenter with scaling
    const k = x / 4 - 12.5;
    const e = y / 9 - 9;

    // Polar coordinate calculations
    const o = mag(k, e) / 9;
    const c = o * e / 30 - t / 8;

    // Complex displacement function
    const q =
      x +
      cos(9 / (k + 1e-6)) +  // Avoid division by zero
      o * k *
      sin(4 * o - t);

    // Final position with multiple transformations
    const px = 0.9 * q * sin(c) + width / 2;
    const py =
      height / 2 +
      (y / 3.6) * cos(3 * c - t / 2) -
      (q / 2) * cos(c);

    point(px, py);
  }
} 

Key Concepts

  • High-density sampling: Increasing point count for smoother visual texture.
  • Singularity handling: Preventing division by zero with a small epsilon value.
  • Multi-layered deformation: Combining reciprocal, trigonometric, and radial terms.
  • Interference of frequencies: Multiple time-dependent components interacting.
  • Emergent complexity: Organic motion arising from stacked elementary functions.

Final Mathematical Breakdown

The complete transformation used in the last step can be expressed as: \begin{align*} x &= \frac{i}{4} \mod 100 \\ y &= \lfloor \frac{i}{150} \rfloor \\ k &= \frac{x}{4} - 12.5 \\ e &= \frac{y}{9} - 9 \\ o &= \frac{\sqrt{k^2 + e^2}}{9} \\ c &= \frac{o \times e}{30} - \frac{t}{8} \\ q &= x + \cos\left(\frac{9}{k + \epsilon}\right) + o \times k \times \sin(4o - t) \\ x' &= 0.9 \times q \times \sin(c) + \frac{\text{width}}{2} \\ y' &= \frac{\text{height}}{2} + \frac{y}{3.6} \times \cos(3c - t/2) - \frac{q}{2} \times \cos(c) \end{align*} where $\epsilon = 10^{-6}$ prevents division by zero.


Creating Variations Through Experimentation

Now that we have deconstructed the animation to its mathematical core, the creative process becomes a game of exploration. By adjusting the various constants, scaling factors, and frequencies within the formulas, you can generate an endless variety of patterns and motions. Try modifying values like the divisor in mag(k, e) / 9, the coefficients in front of t, or the numbers inside trigonometric functions such as sin(4 * o - t). You can even introduce new terms or replace existing ones with different functions like tan() or noise() from the p5.js library. This process of trial and error is where unexpected and fantastic mathematical patterns emerge, transforming the code from a static algorithm into a dynamic tool for artistic discovery.


Final remarks

I hope the steps described above provide a clearer understanding of how complex organic motion emerges from the careful layering of simple mathematical transformations. By starting with basic concepts and progressively adding complexity, we can create sophisticated animations that mimic natural phenomena. The key insight is that intricate patterns arise from the interplay of trigonometric functions, polar coordinates, and time-based variations.

Below are several variations of the same underlying structure. All of them are adaptations inspired by @yuruyurau's elegant visual sketches. Each version emerges from small adjustments in constants, frequencies, and scaling factors. What fascinates me most is how delicate the balance is — tiny numerical changes can completely reshape the creature. This is where mathematics stops feeling mechanical and starts feeling alive.

🪼 See the full Gallery HERE


Finally, if you find this content useful, please consider supporting my work using the links below.

I sincerely appreciate it. Thank you! 😃