start . me .
Directory path . web , canvas .

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>