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!