Creating A Time Reversal Effect Using JavaScript


Have you ever wished you could rewind time like a VHS tape? In this fun and visual tutorial, we will show you how to simulate time reversal effect using a dynamic particle system in JavaScript. The simulation will be made of bubbles floating and moving in random in direction with some collision physics.

Whether you are a beginner looking to get hands-on with JavaScript and HTML5 canvas, or you are just here to look at something cool, this tutorial will be good for you. Let get started!

Prerequisites

Before we get started, make sure you have a basic understanding of HTML, CSS, and JavaScript.

  • Basic Understanding of HTML, CSS, and JavaScript: Familiarity with the fundamental concepts of HTML for structuring web pages, CSS for styling, and JavaScript for adding interactivity is essential. This tutorial assumes a basic knowledge of these languages.
  • Web Browser: Use a modern web browser like Google Chrome, Mozilla Firefox, or Microsoft Edge.

For the purpose of this tutorial, you can download the source files for the demo to help you follow along:

Note: As you test the source files, ensure that you load them within a suitable environment such that all the files are loaded. For offline use, we recommend that you use Live Server and vscode to load the files. Simply open the files in Vscode and run “index.html” using Live Server.

Demo

Here is what we are going to create. If you press down the rewind button, the motion of the bubbles will reverse creating a time reversal effect.

Understanding the Basics

In simple terms, here is what we are going to learn:

  • How to use JavaScript classes to create reusable objects (like vectors and bubbles).
  • How to draw and animate with HTML5 Canvas.
  • How to simulate Brownian motion (random movement).
  • How to record and reverse motion data (time sampling).
  • Add a "rewind" feature with visual effects like scanlines and jitter to mimic analog media.

Lets Get Started!

Let's start by setting up the basic HTML structure for our project as usual. Create an HTML called "index.html", with a basic HTML web format, and add a canvas element.

Creating the Canvas Element

We start with a simple structure as shown below. The canvas is where will draw our simulation. The button triggers the rewind mode. The style tag handles the CSS for centering, button styles, and background color.

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Random Motion VHS</title>
    <style>
      /* Center the content vertically and horizontally */
      body {
        display: flex;
        justify-content: center;
        align-items: center;
        height: 100vh;
        background-color: black;
        margin: 0;
      }

      /* Canvas border for visibility */
      canvas {
        border: 1px solid white;
      }

      /* Container to stack canvas and button vertically */
      div {
        display: flex;
        flex-direction: column;
        justify-content: center;
        align-items: center;
      }

      /* Style for the rewind button */
      button {
        position: relative;
        padding: 10px 20px;
        background-color: white;
        border: none;
        border-radius: 5px;
        cursor: pointer;
        margin-top: 10px;
      }

      /* Button hover effect */
      button:hover {
        background-color: #bed9f1;
        border: 1px solid #bed9f1;
      }

      /* Button click effect */
      button:active {
        background-color: #00a1fe;
        border: 1px solid #f64e00;
      }
    </style>
  </head>
  <body>
    <div>
      <!-- Drawing area for animation -->
      <canvas id="canvas" width="500" height="500" style="background: black"></canvas>
      <!-- Button to activate rewind -->
      <button id="rewind">Rewind</button>
    </div>
 
    
  </body>
</html>

Lets also add script tag just below the <canvas> section as shown below. Inside this tag, we shall add our JavaScript code.

<script type=text/javascript>


</script>

Lets Dive Into the Code

In our JavaScript code, we create classes for the vectors and bubbles to helps simulate the physics of the bubbles. We also create a number of helper functions that will help us carryout different actions such as clearing the screen, and adding special effects.

1.Setting Up the Canvas

This part sets up the canvas environment and essential global variables. These include the canvas itself, its drawing context (ctx), dimensions, and parameters for bubble behavior.

// Get canvas and setup context
      const canvas = document.getElementById("canvas");
      const ctx = canvas.getContext("2d");
      const width = canvas.width;
      const height = canvas.height;
      const button = document.getElementById("rewind");

      // Initialize parameters
      const bubbles = [];
      const bubble_count = 20;
      const sampling_count = 200;
      const sample_step = 1.5;
      const speed = 1;
  • canvas, ctx: references to the HTML canvas and its 2D context.
  • width, height: dimensions of the canvas.
  • button: the rewind button.
  • bubbles: the array storing all bubble instances.
  • bubble_count: total number of bubbles to create.
  • sampling_count: how many motion history frames are saved for rewind.
  • sample_step: frequency of motion sampling.
  • speed: general velocity multiplier for bubble movement.

2. Vector Class

This utility class represents a 2D vector and includes standard math operations like addition, subtraction, normalization, and scaling. It's used for calculating positions and velocities in a clean, object-oriented way.

 // 2D vector utility class for math operations
      class vector {
        constructor(x, y) {
          this.x = x;
          this.y = y;
        }

        add(v) {
          return new vector(this.x + v.x, this.y + v.y);
        }

        sub(v) {
          return new vector(this.x - v.x, this.y - v.y);
        }

        copy() {
          return new vector(this.x, this.y);
        }

        mult(scalar) {
          return new vector(this.x * scalar, this.y * scalar);
        }

        div(scalar) {
          return new vector(this.x / scalar, this.y / scalar);
        }

        normalize() {
          const len = Math.sqrt(this.x * this.x + this.y * this.y);
          return new vector(this.x / len, this.y / len);
        }

        magnitude() {
          return Math.sqrt(this.x * this.x + this.y * this.y);
        }
      }

3. Bubble Class

Each bubble object has a position, velocity, size, color, and a buffer storing its movement history. It can draw itself, handle collisions, sample motion, update, and rewind.

// Bubble class representing moving elements
      class bubble {
        constructor(position, size = 20) {
          this.position = position;
          this.size = size;
          this.velocity = new vector(
            Math.random() * 2 * speed - speed,
            Math.random() * 2 * speed - speed
          );
          this.buffer = []; // stores motion history for rewind
          this.opacity = Math.random() * 0.5 + 0.5;

          // Random RGB color
          const r = Math.floor(Math.random() * 255);
          const g = Math.floor(Math.random() * 255);
          const b = Math.floor(Math.random() * 255);
          this.color = `rgb(${r}, ${g}, ${b})`;
        }

        // Draw the bubble and its label
        draw(index) {
          const gradient = ctx.createRadialGradient(
            this.position.x - this.size * 0.3,
            this.position.y - this.size * 0.3,
            this.size * 0.1,
            this.position.x,
            this.position.y,
            this.size
          );

          // Add colors to gradient
          gradient.addColorStop(0, `rgba(255,255,255,${this.opacity * 0.5})`);
          gradient.addColorStop(
            0.5,
            this.color.replace("rgb", "rgba").replace(")", `,${this.opacity * 0.3})`)
          );
          gradient.addColorStop(1, `rgba(255,255,255,${this.opacity * 0.1})`);

          // Draw circle
          ctx.beginPath();
          ctx.fillStyle = gradient;
          ctx.arc(this.position.x, this.position.y, this.size, 0, Math.PI * 2);
          ctx.fill();
          ctx.strokeStyle = `rgba(255, 255, 255, 0.5)`;
          ctx.lineWidth = 0.5;
          ctx.stroke();
          ctx.closePath();

          // Draw text at center
          ctx.fillStyle = "white";
          ctx.font = "18px Arial";
          ctx.textAlign = "center";
          ctx.textBaseline = "middle";
          ctx.fillText(index + 1, this.position.x, this.position.y);
        }

        // Handle collision with other bubbles
        detect_collision() {
          for (let i = 0; i < bubbles.length; i++) {
            if (this === bubbles[i]) continue;
            const distance = this.position.sub(bubbles[i].position);
            if (distance.magnitude() <= this.size + bubbles[i].size) {
              const dir = distance.normalize();
              this.position = this.position.add(dir.mult(distance.magnitude() * 0.5));
              bubbles[i].position = bubbles[i].position.add(dir.mult(-distance.magnitude() * 0.5));
              this.velocity = dir.mult(this.velocity.magnitude());
              bubbles[i].velocity = dir.mult(-bubbles[i].velocity.magnitude());
            }
          }
        }

        // Step back one frame from buffer (rewind)
        rewind() {
          if (this.buffer.length > 0) {
            this.position = this.buffer[this.buffer.length - 1].position.copy();
            this.velocity = this.buffer[this.buffer.length - 1].velocity.copy();
            this.buffer.pop();
          }
        }

        // Update position and velocity
        update(index) {
          this.detect_collision();
          this.position = this.position.add(this.velocity);

          // Bounce off walls
          if (this.position.x - this.size < 0) {
            this.position.x = this.size + 1;
            this.velocity.x *= -1;
          }
          if (this.position.x + this.size > width) {
            this.position.x = width - this.size - 1;
            this.velocity.x *= -1;
          }
          if (this.position.y - this.size < 0) {
            this.position.y = this.size + 1;
            this.velocity.y *= -1;
          }
          if (this.position.y + this.size > height) {
            this.position.y = height - this.size - 1;
            this.velocity.y *= -1;
          }

          this.draw(index);
        }

        // Save current position and velocity for rewind
        sample() {
          this.buffer.push({
            position: this.position.copy(),
            velocity: this.velocity.copy(),
          });
          if (this.buffer.length > sampling_count) {
            this.buffer.shift();
          }
        }
      }

3. Create Bubbles Function

This function initializes the scene by creating and pushing a set number of bubble instances into the bubbles array.Each bubble starts at a random position within canvas bounds.

   // Create multiple bubbles
      function createBubbles() {
        for (let i = 0; i < bubble_count; i++) {
          const x = Math.random() * width;
          const y = Math.random() * height;
          bubbles.push(new bubble(new vector(x, y)));
        }
      }

4. The Clear Screen and Rewind Symbol functions

The clearScreen function fills the canvas with a translucent color. The background becomes different depending on whether rewind is active, enhancing the visual cue. drawRewindSymbol adds a "⏪" symbol in the top-right corner during rewind.

   // Clear screen with different background for rewind effect
      function clearScreen() {
        ctx.fillStyle = is_rewind ? "#0592" : "#069";
        ctx.globalAlpha = is_rewind ? 0.7 : 1;
        ctx.fillRect(0, 0, width, height);
      }

      // Draw rewind symbol in top-right
      function drawRewindSymbol() {
        ctx.fillStyle = "blue";
        ctx.font = "24px Arial";
        ctx.textAlign = "right";
        ctx.fillText("⏪", width - 10, 30);
      }

5. The Scanlines and Jitter Effect Functions

The jitterEffect function adds visual distortion by randomly shifting thin horizontal slices of the canvas, emulating the look of a glitchy VHS tape. The drawScanlines function adds an additional effect of VHS scan lines.

// Apply jitter effect to simulate VHS distortion
      function jitterEffect() {
        const imageData = ctx.getImageData(0, 0, width, height);
        const offsetY = Math.floor(Math.random() * 4) - 2;

        for (let i = 0; i < 3; i++) {
          const sliceHeight = Math.floor(Math.random() * 5 + 1);
          const y = Math.floor(Math.random() * (height - sliceHeight));
          const slice = ctx.getImageData(0, y, width, sliceHeight);
          const dx = Math.floor(Math.random() * 8) - 4;
          ctx.putImageData(slice, dx, y + offsetY);
        }
      }
      // Simulate VHS scanlines
      function drawScanlines() {
        ctx.save();
        ctx.strokeStyle = "rgba(255,255,255,0.01)";
        for (let y = 0; y < height; y += 4) {
          ctx.beginPath();
          ctx.moveTo(0, y);
          ctx.lineTo(width, y);
          ctx.stroke();
        }
        ctx.restore();
      }

6. The Button Events

The button event listeners toggle the is_rewind variable when the rewind button is held down. Pressing initiates rewind while releasing the button resumes forward motion.

   // Handle rewind button interactions
      button.addEventListener("mousedown", () => (is_rewind = true));
      button.addEventListener("mouseup", () => (is_rewind = false));

7. The Update Function

This is the animation loop. It manages drawing, movement, sampling for rewind, and switching between normal and rewind modes.

// Initialize bubbles
      createBubbles();

      let is_rewind = false;
      let sampling_track = 0;
      let sample_init = false;

      // Main animation loop
      function update() {
        ctx.save();
        clearScreen();

        // Sampling logic for motion history
        if (sample_init && is_rewind == false) {
          sampling = true;
          sampling_track += 1;
        } else if (is_rewind == false) {
          if (sampling_track > sample_step) {
            sampling = true;
            sampling_track = 0;
          } else {
            sampling_track += 1;
            sampling = false;
          }
        }

        // Update or rewind each bubble
        for (let i = 0; i < bubbles.length; i++) {
          if (is_rewind) {
            bubbles[i].rewind();
            bubbles[i].draw(i);
          } else {
            bubbles[i].update(i);
            if (sampling) {
              bubbles[i].sample();
            }
          }
        }

        // Apply rewind visual effects
        if (is_rewind) {
          drawScanlines();
          jitterEffect();
          drawRewindSymbol();
        }

        ctx.restore();

        requestAnimationFrame(update);
      }

      update(); 

Conclusion

Hopefully, this breakdown gave you a deeper understanding of how motion, effects, and interactivity can come together in creative web animations. As a fun challenge, consider experimenting with new VHS-style effects or even adding sound to enhance the rewind experience. Think about optimizing the rewind logic or adding more bubble behaviors like gravity or attraction. Don’t forget to share your tweaks or projects, we would love to see your creative spin on it. Happy coding!

Leave a Reply

Your email address will not be published. Required fields are marked *