start .
me .
HTML Document File : ripple.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Ripple Tank Simulation</title>
<style>
body { margin: 0; overflow: hidden; }
canvas { display: block; }
</style>
</head>
<body>
<canvas id="rippleCanvas"></canvas>
<script>
const canvas = document.getElementById('rippleCanvas');
const ctx = canvas.getContext('2d');
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
let width = canvas.width;
let height = canvas.height;
const damping = 0.98;
let imageData = ctx.createImageData(width, height);
let data = imageData.data;
let rippleMap = new Float32Array(width * height);
let lastMap = new Float32Array(width * height);
// Create background with a gradient
let bgImageData = ctx.createImageData(width, height);
let bgData = bgImageData.data;
createBackground();
// Cache canvas bounds to avoid recalculating them on every touch event
let canvasRect = canvas.getBoundingClientRect();
// Track pending touches for better performance
let pendingTouches = [];
let touchProcessing = false;
function createBackground() {
for (let y = 0; y < height; y++) {
for (let x = 0; x < width; x++) {
const j = (y * width + x) * 4;
// Create a gradient background
const r = Math.floor(128 + 127 * Math.sin(x / 50));
const g = Math.floor(128 + 127 * Math.sin(y / 50));
const b = 255;
bgData[j] = r;
bgData[j + 1] = g;
bgData[j + 2] = b;
bgData[j + 3] = 255;
}
}
}
function disturb(x, y) {
if (x < 2 || y < 2 || x > width - 2 || y > height - 2) return;
// Create a stronger ripple effect
for (let i = -1; i <= 1; i++) {
for (let j = -1; j <= 1; j++) {
rippleMap[(y+j) * width + (x+i)] += 250;
}
}
}
function update() {
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const i = y * width + x;
rippleMap[i] = ((
lastMap[i - 1] +
lastMap[i + 1] +
lastMap[i - width] +
lastMap[i + width]
) / 2) - rippleMap[i];
rippleMap[i] *= damping;
}
}
// Swap buffers
const temp = lastMap;
lastMap = rippleMap;
rippleMap = temp;
}
function render() {
// Copy background to data first
for (let i = 0; i < data.length; i += 4) {
data[i] = bgData[i];
data[i + 1] = bgData[i + 1];
data[i + 2] = bgData[i + 2];
data[i + 3] = bgData[i + 3];
}
// Apply ripple effect
for (let y = 1; y < height - 1; y++) {
for (let x = 1; x < width - 1; x++) {
const i = y * width + x;
const displacement = rippleMap[i];
// Calculate refracted position
const offsetX = Math.round(((rippleMap[i - 1] - rippleMap[i + 1])) * 2);
const offsetY = Math.round(((rippleMap[i - width] - rippleMap[i + width])) * 2);
// Bounds checking
let sourceX = x + offsetX;
let sourceY = y + offsetY;
if (sourceX < 0) sourceX = 0;
if (sourceY < 0) sourceY = 0;
if (sourceX >= width) sourceX = width - 1;
if (sourceY >= height) sourceY = height - 1;
// Copy pixel from the source to the destination
const sourceIndex = (sourceY * width + sourceX) * 4;
const destIndex = (y * width + x) * 4;
// Only copy if we have valid indices
if (sourceIndex >= 0 && sourceIndex < bgData.length && destIndex >= 0 && destIndex < data.length) {
data[destIndex] = bgData[sourceIndex];
data[destIndex + 1] = bgData[sourceIndex + 1];
data[destIndex + 2] = bgData[sourceIndex + 2];
data[destIndex + 3] = 255;
}
}
}
ctx.putImageData(imageData, 0, 0);
}
function loop() {
update();
render();
// Process any pending touches in the animation frame for better synchronization
if (pendingTouches.length > 0) {
processPendingTouches();
}
requestAnimationFrame(loop);
}
// Process touches in batches during the animation frame
function processPendingTouches() {
// Only process the latest 10 touch points to avoid overwhelming the simulation
const touchesToProcess = pendingTouches.slice(-10);
pendingTouches = [];
for (const touch of touchesToProcess) {
disturb(touch.x, touch.y);
}
}
// Trigger ripples with mouse movements and clicks
canvas.addEventListener('mousemove', (e) => {
if (e.buttons) disturb(e.clientX, e.clientY);
});
canvas.addEventListener('click', (e) => {
disturb(e.clientX, e.clientY);
});
// Touch support with improved handling
canvas.addEventListener('touchstart', handleTouch, { passive: false });
canvas.addEventListener('touchmove', handleTouch, { passive: false });
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
// Optional: You could add a special effect for touch end
});
// Handle multi-touch events with optimized performance
function handleTouch(e) {
e.preventDefault(); // Prevent scrolling when touching the canvas
// Handle all touch points (multi-touch)
for (let i = 0; i < e.touches.length; i++) {
const touch = e.touches[i];
// Use cached canvas rect
const x = Math.floor(touch.clientX - canvasRect.left);
const y = Math.floor(touch.clientY - canvasRect.top);
// Queue touches instead of processing immediately
pendingTouches.push({x, y});
}
}
// Handle window resize
window.addEventListener('resize', () => {
// Update canvas dimensions
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
// Update simulation dimensions
width = canvas.width;
height = canvas.height;
// Create new arrays with new dimensions
rippleMap = new Float32Array(width * height);
lastMap = new Float32Array(width * height);
// Update imageData objects
imageData = ctx.createImageData(width, height);
data = imageData.data;
bgImageData = ctx.createImageData(width, height);
bgData = bgImageData.data;
// Recreate background
createBackground();
// Update cached canvas bounds
canvasRect = canvas.getBoundingClientRect();
});
// Create an initial ripple
disturb(Math.floor(width / 2), Math.floor(height / 2));
// Start the animation loop
loop();
</script>
</body>
</html>