Interactive Ferrofluid separator

As the theme of the website is related to ink, I wanted to have animated waves of ink at the bottom of the page. I found no code which would allow me to do so with randomized wave crests, and then I stumbled upon SVG animations. The following code finds all separator elements on the page and embeds a path element which interacts with the cursor upon movement. The result is a 2D, ferrofluidic ink pool which rises as the mouse moves close to it. Currently the only separator I am using is in my footer, but I thought the idea of having it ready anywhere I want to use a separator is nice. Also when playing around with the element, it seems as if it lifts up the page, as if it is a curtain, or an incision, which I find beautiful. The code went into the footer.

HTML:

<div id="wave-container">
	<svg id="wave"></svg>
</div>
JAVASCRIPT:

document.addEventListener("DOMContentLoaded", function() {
const container = document.getElementById('wave-container');
container.style.display = 'none';
//check if we are in mobile view  
const headerActions = document.querySelector('.header-actions');
let isTouch = false;
function checkHeader() {
const styles = window.getComputedStyle(headerActions);
isTouch = styles.getPropertyValue('display') !== 'flex';
}
checkHeader();
if (isTouch !== true) {
const lines = document.querySelectorAll('.horizontalrule-block');
lines.forEach(line => {
const clonedContainer = container.cloneNode(true);
line.appendChild(clonedContainer);
clonedContainer.style.display = 'block';
const svg = clonedContainer.querySelector('#wave');
const width = clonedContainer.clientWidth;
const height = clonedContainer.clientHeight / 2;
const waveHeight = 0;
const numPoints = 300;
const points = [];
for (let i = 0; i <= numPoints; i++) {
points.push({
x: (i / numPoints) * width,
y: height,
originalY: height + Math.random() * waveHeight - waveHeight / 2
});
}
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
svg.appendChild(path);
function createPath(points) {
let d = `M ${points[0].x} ${points[0].y}`;
for (let i = 1; i < points.length; i++) {
d += ` L ${points[i].x} ${points[i].y}`;
}
return d;
}
function animate() {
for (let i = 0; i < points.length; i++) {
points[i].y += (points[i].originalY - points[i].y) * 0.05;
}
path.setAttribute('d', createPath(points));
requestAnimationFrame(animate);
}
clonedContainer.addEventListener('mousemove', function(event) {
const rect = clonedContainer.getBoundingClientRect();
const mouseX = event.clientX - rect.left;
const mouseY = event.clientY - rect.top;
for (let i = 0; i < points.length; i++) {
const dx = mouseX - points[i].x;
const dy = mouseY - points[i].y;
const distance = Math.sqrt(dx * dx + dy * dy);
const force = Math.exp(-distance * 0.045);
const scale = Math.min(i / numPoints, 1 - (i / numPoints));
points[i].y -= force * 20 * scale; // Apply scaled force
}
});
animate();
});
}
});
CSS:

.horizontalrule-block {
  justify-content: center;
  position: relative;
  z-index: 9999999;
}
#wave-container {
  width: 75%;
  height: 200px;
  display: flex;
  justify-content: center;
  align-items: center;
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  padding: 0; 
}
#wave-container svg {
  width: 100%;
  height: 100%; 
  top: 0;
  position: absolute;
}
#wave-container path {
  fill: #000000;
  stroke: transparent;
  stroke-width: 2;  
}

Notice how there is a variable for the number of points. This allows potential scaling for devices which are less powerful. However, I currently disable this code on mobile as it hits differently on touch screens.

Previous
Previous

Disable right click on images only

Next
Next

Removing prefixes (select, from, comma)