← Back to list

How to Create a Digital “Data-Mode” Contour Animation in Processing

Generative art has a way of turning simple math into something strangely alive. In this tutorial, we’ll build a hypnotic “data-mode”…

Rey Rad · 2025-11-17 00:38 · 0 claps · 5.3 min read
#processing #generative-art #reyrad #rt-digital #tutorial
Open on Medium ↗
Wiki topics: 📐 · Mathematics 🎬 · Film & Television

How to Create a Digital “Data-Mode” Contour Animation in Processing

Generative art has a way of turning simple math into something strangely alive. In this tutorial, we’ll build a hypnotic “data-mode” contour animation — like watching a topographic scan flicker across a futuristic display.

Using nothing more than Processing 4.3.2, Perlin noise, and a classic algorithm called Marching Squares, we’ll create:

✔ A living digital terrain ✔ Quantized “data slice” aesthetics ✔ Smooth, seamless looping ✔ And a direct GIF export

Everything runs in Java mode with one small library, and the results look like they came straight out of a sci-fi UI panel.

🚀 Overview

We’ll build this animation in five main steps:

  1. Create a noise field
  2. Quantize it into digital steps
  3. Use Marching Squares to generate contour lines
  4. Animate the field over time
  5. Export the animation as a GIF

By the end, you’ll have a fully functional Processing sketch that produces this hypnotic effect:

A pulsing, glowing, animated digital topography built entirely from math.

📦 Requirements

You’ll need:

  • Processing 4.3.2 (Java mode)
  • GifAnimation library Install via:

Sketch → Import Library → Add Library → Search “GifAnimation” → Install

That’s it.

🧠 How It Works

Before we dive into the code, let’s break down the three ideas that make this animation possible.

1. Perlin Noise → A Smooth Height Map

Processing’s noise() function gives us a natural, organic texture. We sample it across a grid and store the values in a 2D array called field.

This becomes our digital “height map.”

2. Quantization → Digital Slices

Instead of using the raw smooth noise, we snap values into discrete levels:

0.0  
0.2  
0.4  
0.6  
0.8  
1.0

These slices feel like:

  • medical imaging
  • sonar scans
  • elevation data
  • glitchy machine-vision layers

This is the “data mode” aesthetic.

3. Marching Squares → Contour Lines

Marching Squares is an algorithm that:

  • looks at four corners of a tiny square
  • checks which corners are above a threshold
  • draws a line segment where the field crosses that threshold

We run Marching Squares for several thresholds (“iso-values”) to build layered contour lines.

The magic is that every tiny segment aligns perfectly with its neighbors, producing smooth, continuous curves.

🧾 Full Working Code (Ready to Run)

Create a new empty sketch in Processing and paste this entire code:

// Quantized "Data Mode" Noise Contour Field — WITH GIF EXPORT
// Processing 4.3.2 (Java Mode)
// Press 'g' to export a seamless looping GIF
import gifAnimation.*;
int W = 540;
int H = 960;
float cellSize = 10;
int cols, rows;
float[][] field;
float noiseScale = 0.015;
float radius = 2.0;
int loopFrames = 240;    // frames per full seamless loop
int quantLevels = 6;     // discrete noise steps
GifMaker gif;
boolean exporting = false;
int gifFrame = 0;
void setup() {
  size(W, H);            // safest in Processing 4.3.2
  cols = int(W / cellSize) + 2;
  rows = int(H / cellSize) + 2;
  field = new float[cols][rows];
  smooth(8);
  colorMode(RGB, 255);
  strokeCap(ROUND);
  noFill();
}
void draw() {
  background(0);
  // time variable (looping)
  float u = exporting ? gifFrame / float(loopFrames)
                      : (frameCount % loopFrames) / float(loopFrames);
  float angle = TWO_PI * u;
  float nzx = cos(angle) * radius;
  float nzy = sin(angle) * radius;
  // ---- quantized noise sampling ----
  for (int j = 0; j < rows; j++) {
    for (int i = 0; i < cols; i++) {
      float x = i * cellSize;
      float y = j * cellSize;
      float n = noise(x * noiseScale + nzx, y * noiseScale + nzy);
      float q = floor(n * quantLevels);     // quantize
      q = q / (quantLevels - 1.0);          // normalize to 0..1
      field[i][j] = q;
    }
  }
  // ---- draw contours between levels ----
  int levels = quantLevels - 1;
  for (int k = 0; k < levels; k++) {
    float iso = (k + 0.5) / (quantLevels - 1.0);
    float alpha = map(k, 0, levels - 1, 60, 220);
    stroke(255, 220, 120, alpha);
    strokeWeight(1.8);
    marchingSquares(iso);
  }
  // ---- GIF capture ----
  if (exporting) {
    gif.addFrame();
    gifFrame++;
    if (gifFrame >= loopFrames) {
      gif.finish();
      exporting = false;
      println("GIF export completed ✔ Saved as export.gif");
    }
  }
}
// marching squares
void marchingSquares(float iso) {
  for (int j = 0; j < rows - 1; j++) {
    for (int i = 0; i < cols - 1; i++) {
      float a = field[i][j];
      float b = field[i+1][j];
      float c = field[i+1][j+1];
      float d = field[i][j+1];
      int state = 0;
      if (a > iso) state |= 1;
      if (b > iso) state |= 2;
      if (c > iso) state |= 4;
      if (d > iso) state |= 8;
      if (state == 0 || state == 15) continue;
      PVector pA = new PVector(i * cellSize,       j * cellSize);
      PVector pB = new PVector((i+1)*cellSize,     j * cellSize);
      PVector pC = new PVector((i+1)*cellSize,    (j+1)*cellSize);
      PVector pD = new PVector(i * cellSize,      (j+1)*cellSize);
      PVector p0 = interp(pA, pB, a, b, iso);
      PVector p1 = interp(pB, pC, b, c, iso);
      PVector p2 = interp(pC, pD, c, d, iso);
      PVector p3 = interp(pD, pA, d, a, iso);
      switch (state) {
        case 1:  drawSeg(p3, p0); break;
        case 2:  drawSeg(p0, p1); break;
        case 3:  drawSeg(p3, p1); break;
        case 4:  drawSeg(p1, p2); break;
        case 5:  drawSeg(p3, p0); drawSeg(p1, p2); break;
        case 6:  drawSeg(p0, p2); break;
        case 7:  drawSeg(p3, p2); break;
        case 8:  drawSeg(p2, p3); break;
        case 9:  drawSeg(p0, p2); break;
        case 10: drawSeg(p0, p1); drawSeg(p2, p3); break;
        case 11: drawSeg(p1, p2); break;
        case 12: drawSeg(p3, p1); break;
        case 13: drawSeg(p0, p1); break;
        case 14: drawSeg(p3, p0); break;
      }
    }
  }
}
PVector interp(PVector p1, PVector p2, float v1, float v2, float iso) {
  float denom = (v2 - v1);
  float t = (abs(denom) < 1e-6) ? 0.5 : (iso - v1) / denom;
  return PVector.lerp(p1, p2, constrain(t, 0, 1));
}
// draw line segment
void drawSeg(PVector a, PVector b) {
  beginShape();
  vertex(a.x, a.y);
  vertex(b.x, b.y);
  endShape();
}
// ---------- press 'g' to export ----------
void keyPressed() {
  if (key == 'g' || key == 'G') {
    println("Starting GIF export...");
    exporting = true;
    gifFrame = 0;
    gif = new GifMaker(this, "export.gif");
    gif.setRepeat(0);   // loop forever
    gif.setQuality(10);
    gif.setDelay(1000/60);   // ~60 FPS
  }
}

🔍 How the Contour Lines Are Actually Made

If you’re curious how the animation generates those silky contour lines, here’s the breakdown.

🔸 Step 1 — Build a 2D height map

We store quantized noise values in a grid:

field[i][j] = q;

Each value is a discrete “height” from 0 to 1.

🔸 Step 2 — Choose iso-values (the boundaries)

To create a contour, we pick a threshold:

float iso = 0.3;

The contour line is drawn everywhere that the field crosses this value.

🔸 Step 3 — Look at a 2×2 block of the grid

Each block has four corners:

a ---- b
|      |
d ---- c

We ask:

Which corners are above the iso level?

This gives a 4-bit “state” between 0 and 15.

🔸 Step 4 — Marching Squares connects the dots

Depending on the state, the contour crosses certain edges.

We compute the exact crossing point with interpolation:

PVector p0 = interp(pA, pB, a, b, iso);

Then we draw a small segment between two edges.

Do this across the entire grid, and the segments merge into smooth curves.

That’s it. That’s how the map is drawn.

🎬 Exporting a Seamless GIF

Once the animation is running:

  1. Press G
  2. Processing records one full loop
  3. When finished, you’ll see:
GIF export completed ✔ Saved as export.gif

The GIF is ready to post anywhere.

Variations to Explore

Once you understand the core idea, try modifying:

quantLevels

More levels → more contour lines Fewer levels → big graphic shapes

✔ Color palette

Change RGB values for neon, cyan, magenta, amber, grayscale…

✔ Swirl or warp the coordinates

Add rotation based on distance from center for vortex effects.

✔ Add glow

Switch to P2D + blendMode(ADD) (some systems only).

🎉 Final Thoughts

These kinds of generative animations let you explore the boundary between math and aesthetics. A simple noise field combined with contour extraction can look like:

  • magnetic fields
  • alien terrain
  • medical imaging
  • glitchy machine vision

The power comes from layering the simplest ideas possible.

If you create something cool with this, definitely share it — the variations are endless.


메타데이터
post_id
c3ebc98cd34d
slug
how-to-create-a-digital-data-mode-contour-animation-in-processing-c3ebc98cd34d
url
https://medium.com/@rh.h.rad/how-to-create-a-digital-data-mode-contour-animation-in-processing-c3ebc98cd34d
canonical_url
https://medium.com/@rh.h.rad/how-to-create-a-digital-data-mode-contour-animation-in-processing-c3ebc98cd34d
author_url
https://medium.com/@rh.h.rad
status
ok
fetched_at
2026-06-26 21:52:29