diff --git a/src/components/VisualizationP5.ts b/src/components/VisualizationP5.ts
index d0fb101..e3a02f4 100644
--- a/src/components/VisualizationP5.ts
+++ b/src/components/VisualizationP5.ts
@@ -38,10 +38,18 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 		height: 64,
 		name: sf.name,
 	}));
+	const visButton = {
+		x: screenPadding,
+		y: element.clientHeight - screenPadding - 64,
+		width: 64,
+		height: 64,
+	};
 	let selectedInstrument =
 		soundFonts[Math.floor(Math.random() * soundFonts.length)].name;
+	let selectedVis: "splash" | "ripple" | "grid" = "splash";
 
 	let touchEffects: SplashEffects;
+	let rippleGrid: RippleGrid;
 	let grid: Grid;
 
 	p.setup = () => {
@@ -57,6 +65,7 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 			p.height / 2,
 		);
 		touchEffects = new SplashEffects(p);
+		rippleGrid = new RippleGrid(p);
 		grid = new Grid(p);
 	};
 
@@ -72,8 +81,13 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 			return;
 		}
 		p.background("black");
-		touchEffects.draw(p);
-		// grid.draw(p);
+		if (selectedVis === "splash") {
+			touchEffects.draw(p);
+		} else if (selectedVis === "ripple") {
+			rippleGrid.draw(p);
+		} else if (selectedVis === "grid") {
+			grid.draw(p);
+		}
 		for (const [recordingId, line] of Object.entries(lines)) {
 			const colour = colours[Number.parseInt(recordingId) % colours.length];
 			const red = (colour >> 16) & 0xff;
@@ -126,6 +140,7 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 				instrument.height,
 			);
 		}
+		p.text("vis", visButton.x, visButton.y, visButton.width, visButton.height);
 		p.stroke("white");
 		p.line(
 			screenPadding,
@@ -165,6 +180,20 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 				return true;
 			}
 		}
+		if (didClick(visButton)) {
+			switch (selectedVis) {
+				case "splash":
+					selectedVis = "ripple";
+					break;
+				case "ripple":
+					selectedVis = "grid";
+					break;
+				case "grid":
+					selectedVis = "splash";
+					break;
+			}
+			return true;
+		}
 	}
 
 	p.mousePressed = () => {
@@ -211,6 +240,7 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 			for (const touch of event.changedTouches) {
 				drawListener(touch.clientX, touch.clientY, touch.identifier);
 				touchEffects.add(touch.clientX, touch.clientY);
+				rippleGrid.touched(touch.clientX, touch.clientY);
 				grid.touched(touch.clientX, touch.clientY);
 			}
 		}
@@ -222,6 +252,7 @@ export const VisualizationP5 = (p: p5, element: HTMLElement) => {
 		}
 		drawListener(p.mouseX, p.mouseY, 0);
 		touchEffects.add(p.mouseX, p.mouseY);
+		rippleGrid.touched(p.mouseX, p.mouseY);
 		grid.touched(p.mouseX, p.mouseY);
 	};
 
@@ -413,3 +444,62 @@ class Grid {
 		p.image(this.pg, 0, 0);
 	}
 }
+
+class RippleGrid {
+	private readonly distanceBetweenPoints = 32;
+	private readonly pg: p5.Graphics;
+	private points: p5.Vector[][] = [];
+	private oldPoints: p5.Vector[][] = [];
+
+	constructor(p: p5) {
+		this.pg = p.createGraphics(p.width, p.height);
+		for (let y = 0; y < p.height; y += this.distanceBetweenPoints) {
+			const row: p5.Vector[] = [];
+			const oldRow: p5.Vector[] = [];
+			for (let x = 0; x < p.width; x += this.distanceBetweenPoints) {
+				row.push(new p5.Vector(x, y));
+				oldRow.push(new p5.Vector(0, 0));
+			}
+			this.points.push(row);
+			this.oldPoints.push(oldRow);
+		}
+	}
+
+	touched(x: number, y: number) {
+		const touchPoint = new p5.Vector(x, y);
+		for (let j = 0; j < this.points.length; j++) {
+			for (let i = 0; i < this.points[j].length; i++) {
+				const point = this.points[j][i];
+				const distanceToTouch = point.dist(touchPoint);
+				point.z += this.distanceBetweenPoints / distanceToTouch / 2;
+			}
+		}
+	}
+
+	draw(p: p5) {
+		this.pg.background("black");
+		this.pg.stroke("#444");
+		this.pg.strokeWeight(1);
+		this.pg.fill("white");
+		for (let j = 0; j < this.points.length; j++) {
+			for (let i = 0; i < this.points[j].length; i++) {
+				const point = this.points[j][i];
+				const left = this.points[j][i - 1]?.z ?? 0;
+				const right = this.points[j][i + 1]?.z ?? 0;
+				const top = this.points[j + 1]?.[i]?.z ?? 0;
+				const bottom = this.points[j - 1]?.[i]?.z ?? 0;
+				this.oldPoints[j][i].z =
+					((left + right + top + bottom) / 2 - this.oldPoints[j][i].z) * 0.9;
+				this.pg.circle(
+					point.x,
+					point.y,
+					(this.distanceBetweenPoints / 4) * this.oldPoints[j][i].z,
+				);
+			}
+		}
+		const cache = this.oldPoints;
+		this.oldPoints = this.points;
+		this.points = cache;
+		p.image(this.pg, 0, 0);
+	}
+}