<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no, minimum-scale=1.0, maximum-scale=1.0">
<title>Ripples on Grid</title>
<style>
body { margin: 0; }
canvas { display: block; }
#key-help {
position: absolute;
top: 10px;
left: 10px;
background-color: white;
padding: 10px;
border-radius: 5px;
box-shadow: 0 0 10px rgba(0,0,0,0.2);
width: 300px;
}
#key-help footer {
font-size: 12px;
font-style: italic;
margin-top: 10px;
}
#key-help ul {
list-style: none;
padding: 0;
}
#key-help li {
margin-bottom: 5px;
}
</style>
</head>
<body>
<div id="key-help">
<h2>Key Controls</h2>
<ul>
<li><strong>[ . ]</strong> Increase Grid Resolution</li>
<li><strong>[ , ]</strong> Decrease Grid Resolution</li>
<li><strong>[ ] ]</strong> Increase Raindrop Frequency</li>
<li><strong>[ [ ]</strong> Decrease Raindrop Frequency</li>
<li><strong>[ = ]</strong> Increase Wave Frequency</li>
<li><strong>[ - ]</strong> Decrease Wave Frequency</li>
<li><strong>[ / ]</strong> Show/Hide Key Controls</li>
<li><strong>[ ' ]</strong> Increase Rotation Speed</li>
<li><strong>[ ; ]</strong> Decrease Rotation Speed</li>
<li><strong>[ d ]</strong> Reset to Defaults</li>
</ul>
<footer>Trippy Ripple by Peanutfiends 2025</footer>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
let scene = new THREE.Scene();
let camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
let renderer = new THREE.WebGLRenderer({
antialias: true
});
renderer.setSize(window.innerWidth, window.innerHeight);
document.body.appendChild(renderer.domElement);
// Create grid
let gridResolution = 150;
let grid = new THREE.GridHelper(10, 10);
grid.material.opacity = 0;
grid.material.transparent = true;
scene.add(grid);
// Create ripple effect
let geometry = new THREE.PlaneGeometry(10, 10, gridResolution, gridResolution);
let material = new THREE.MeshBasicMaterial({
vertexColors: true,
wireframe: true
});
let plane = new THREE.Mesh(geometry, material);
scene.add(plane);
// Store initial positions and random phases
let initialPositions = geometry.attributes.position.clone();
let phases = [];
let colors = [];
for (let i = 0; i < geometry.attributes.position.count; i++) {
phases.push(Math.random() * Math.PI * 2);
colors.push(new THREE.Color());
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(colors.length * 3), 3));
// Create water drops
let waterDrops = [];
let raindropInterval = 100;
let waveFrequency = 5;
let rotationSpeed = 0.001;
let defaultRaindropInterval = raindropInterval;
let defaultWaveFrequency = waveFrequency;
let defaultRotationSpeed = rotationSpeed;
function createWaterDrop() {
let x = Math.random() * 10 - 5;
let y = Math.random() * 10 - 5;
let force = Math.random() * 0.1 + 0.05;
let drop = { x, y, time: 0, force };
waterDrops.push(drop);
}
let intervalId = setInterval(createWaterDrop, raindropInterval);
camera.position.z = 1.675;
let time = 0;
function animate() {
requestAnimationFrame(animate);
// Ripple effect with random variance and height-based color
for (let i = 0; i < geometry.attributes.position.count; i++) {
let x = initialPositions.getX(i);
let y = initialPositions.getY(i);
let z = 0;
for (let j = 0; j < waterDrops.length; j++) {
let drop = waterDrops[j];
let dx = x - drop.x;
let dy = y - drop.y;
let distance = Math.sqrt(dx * dx + dy * dy);
z += Math.sin((distance - drop.time) * waveFrequency) * Math.exp(-distance / 0.5) * drop.force;
}
z += Math.sin((x + time) * 10 + phases[i]) * Math.sin((y + time) * 10 + phases[i]) * 0.05;
geometry.attributes.position.setZ(i, z);
// Height-based color with cycling hue
let height = z;
height = Math.max(-0.2, Math.min(0.2, height));
height = (height + 0.2) / 0.4;
let hue = (time + i / geometry.attributes.position.count) % 1;
let color = new THREE.Color();
color.setHSL(hue, 1 - height * 0.7, height * 0.5 + 0.3);
colors[i].copy(color);
geometry.attributes.color.setXYZ(i, colors[i].r, colors[i].g, colors[i].b);
}
geometry.attributes.position.needsUpdate = true;
geometry.attributes.color.needsUpdate = true;
// Update water drops
for (let i = waterDrops.length - 1; i >= 0; i--) {
let drop = waterDrops[i];
drop.time += 0.05;
if (drop.time > 4) {
waterDrops.splice(i, 1);
}
}
// Twist, pull, and rotate effect
plane.rotation.x = Math.sin(time) * 0.1;
plane.rotation.y = Math.cos(time) * 0.1;
plane.rotation.z += rotationSpeed;
camera.position.z = 1.675 + Math.sin(time * 2) * 0.675;
time += 0.01;
renderer.render(scene, camera);
}
animate();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
let keyHelp = document.getElementById('key-help');
setTimeout(() => {
keyHelp.style.display = 'none';
}, 10000);
document.addEventListener('keydown', (e) => {
if (e.key === '.') {
gridResolution = Math.min(gridResolution + 20, 400);
scene.remove(plane);
geometry = new THREE.PlaneGeometry(10, 10, gridResolution, gridResolution);
initialPositions = geometry.attributes.position.clone();
phases = [];
colors = [];
for (let i = 0; i < geometry.attributes.position.count; i++) {
phases.push(Math.random() * Math.PI * 2);
colors.push(new THREE.Color());
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(colors.length * 3), 3));
material = new THREE.MeshBasicMaterial({
vertexColors: true,
wireframe: true
});
plane = new THREE.Mesh(geometry, material);
scene.add(plane);
} else if (e.key === ',') {
gridResolution = Math.max(gridResolution - 20, 20);
scene.remove(plane);
geometry = new THREE.PlaneGeometry(10, 10, gridResolution, gridResolution);
initialPositions = geometry.attributes.position.clone();
phases = [];
colors = [];
for (let i = 0; i < geometry.attributes.position.count; i++) {
phases.push(Math.random() * Math.PI * 2);
colors.push(new THREE.Color());
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(colors.length * 3), 3));
material = new THREE.MeshBasicMaterial({
vertexColors: true,
wireframe: true
});
plane = new THREE.Mesh(geometry, material);
scene.add(plane);
} else if (e.key === ']') {
raindropInterval = Math.max(raindropInterval * 0.95, 1);
clearInterval(intervalId);
intervalId = setInterval(createWaterDrop, raindropInterval);
} else if (e.key === '[') {
raindropInterval = raindropInterval / 0.95;
clearInterval(intervalId);
intervalId = setInterval(createWaterDrop, raindropInterval);
} else if (e.key === '=') {
waveFrequency = waveFrequency * 1.05;
} else if (e.key === '-') {
waveFrequency = Math.max(waveFrequency / 1.05, 0);
} else if (e.key === '/') {
keyHelp.style.display = 'block';
setTimeout(() => {
keyHelp.style.display = 'none';
}, 10000);
} else if (e.key === '\'') {
rotationSpeed *= 1.05;
} else if (e.key === ';') {
rotationSpeed = Math.max(rotationSpeed / 1.05, 0);
} else if (e.key === 'd') {
raindropInterval = defaultRaindropInterval;
clearInterval(intervalId);
intervalId = setInterval(createWaterDrop, raindropInterval);
waveFrequency = defaultWaveFrequency;
rotationSpeed = defaultRotationSpeed;
gridResolution = 150;
scene.remove(plane);
geometry = new THREE.PlaneGeometry(10, 10, gridResolution, gridResolution);
initialPositions = geometry.attributes.position.clone();
phases = [];
colors = [];
for (let i = 0; i < geometry.attributes.position.count; i++) {
phases.push(Math.random() * Math.PI * 2);
colors.push(new THREE.Color());
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(new Float32Array(colors.length * 3), 3));
material = new THREE.MeshBasicMaterial({
vertexColors: true,
wireframe: true
});
plane = new THREE.Mesh(geometry, material);
scene.add(plane);
}
});
</script>
</body>
</html>