<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lake Ripple Enhanced</title>
<style>
body { margin: 0; overflow: hidden; background: #000; }
canvas { display: block; width: 100vw; height: 100vh; }
</style>
</head>
<body>
<canvas id="lakeCanvas"></canvas>
<script>
const img = new Image();
const reflectionStart = 0.68;
const buffer = document.createElement('canvas');
const bctx = buffer.getContext('2d');
img.src = 'pexels-eberhardgross-449461.jpg';
img.onload = () => {
const canvas = document.getElementById('lakeCanvas');
const ctx = canvas.getContext('2d');
canvas.width = img.width;
canvas.height = img.height;
buffer.width = img.width;
buffer.height = img.height;
bctx.drawImage(img, 0, 0); // single time
// ✨ Control how far down the reflection begins (0.0 to 1.0)
const midY = img.height * reflectionStart;
const rippleHeight = img.height - midY;
const sliceHeight = 2;
const subtleGradientColor = '50,200,255';
// Glints
const glints = [];
const maxGlints = 60;
function createGlint() {
const x = Math.random() * canvas.width;
const y = midY + Math.random() * rippleHeight;
const relY = (y - midY) / rippleHeight;
const sampleY = Math.floor((img.height * reflectionStart) * relY);
let r = 255, g = 255, b = 255;
try {
const sample = bctx.getImageData(x, sampleY, 1, 1).data;
r = sample[0]; g = sample[1]; b = sample[2];
} catch (e) {
// fallback color if sampling fails
}
return {
x, y,
color: `rgba(${r},${g},${b},`,
opacity: Math.random() * 0.8 + 0.2,
lifetime: Math.random() * 2000 + 500,
born: performance.now(),
radius: 2 + Math.random() * 3
};
}
let hue = 0;
function draw(time) {
ctx.globalCompositeOperation = "source-over";
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(img, 0, 0);
for (let y = 0; y < rippleHeight; y += sliceHeight) {
const distFactor = 1 - (y / rippleHeight);
const dx = Math.sin((time / 400) + y / 12) * (1-distFactor) * 4;
const dy = Math.cos((time / 100) + y / 8) * (1-distFactor) * 0.75;
ctx.drawImage(
img,
0, midY + y, img.width, sliceHeight,
dx, midY + y + dy, img.width, sliceHeight
);
}
// ✨ Reflection glare gradient adjusted to match new reflection start
hue+=0.5; if (hue>360) hue = 0;
const gradient = ctx.createLinearGradient(0, midY, 0, img.height);
gradient.addColorStop(0, `HSL(${hue}, 100%, 50%, 0)`);
gradient.addColorStop(0.01, `HSL(${hue}, 100%, 50%, 0.12)`);
gradient.addColorStop(0.15, `HSL(${hue}, 100%, 50%, 0.05)`);
gradient.addColorStop(1, `HSL(${hue}, 100%, 50%, 0)`);
ctx.globalCompositeOperation = 'difference';
ctx.fillStyle = gradient;
ctx.fillRect(0, midY, canvas.width, rippleHeight);
// ✨ Glints (unchanged)
while (glints.length < maxGlints) glints.push(createGlint());
const now = performance.now();
for (let i = glints.length - 1; i >= 0; i--) {
const g = glints[i];
const age = now - g.born;
if (age > g.lifetime) {
glints.splice(i, 1);
continue;
}
const fade = 1 - (age / g.lifetime);
const sparkle = Math.sin(age / 400 * Math.PI);
ctx.beginPath();
ctx.globalCompositeOperation = 'screen';
ctx.fillStyle = `${g.color}${g.opacity * fade * sparkle})`;
ctx.arc(g.x, g.y, g.radius, 0, Math.PI * 2);
ctx.fill();
}
requestAnimationFrame(draw);
}
requestAnimationFrame(draw);
};
</script>
</body>
</html>
Some Rise interactions I've been tinkering with:
And nor is my website! It is in a constant state of flux. I'm always trying one thing or another. In 1995 it was a perl script. In 2001 it was a blog running on classic asp. In 2011 it was a Wordpress thing, which got hacked/spammed pretty much every day. In 2021 it was the default apache file listing without a site in front. And now, it's a sprawl.
Use it like you would a map, albeit a map without cardinality, meaningful coordinates, or a legend, or topology .. ok now that I think of it, it's not at all like a map. But use it as if it was. Well, I'm glad that's sorted out.
Use the clipboard icon (top-left) to share your position (copies a link to your clipboard). Use the wierd square icon next to it to reset the zoom. Click on the animated logo (a lissajous warping rotozoomer with colour morphing, in canvas/vanilla js) if it's getting annoying. The bottom-right has a kind of overview thing with a draggable rectangle to power-navigate and show you an overview. The website is supposed to work on mobile, but if it doesn't I'll attempt to leave a contact form lying about on here somewhere. There's also some spatial audio - whatever that means.
Also somewhere out there are any of the following: