Core Concept
The interaction involves a draggable circular thumb moving along a horizontal track. As the thumb passes over numerical markers, the markers dynamically elevate vertically and increase their visibility based on their proximity to the thumb's center. Once the thumb moves away, the markers recede to their baseline position and lower opacity.
Implementing Drag Behavior
To ensure smooth dragging without losing the pointer when moving quickly, the mousemove and mouseup events are bound to the document rather than the thumb itself. The drag sequence initiates on mousedown within the thumb element.
const thumb = document.getElementById('draggable-thumb');
const thumbDiameter = thumb.offsetWidth;
const markers = Array.from(document.querySelectorAll('.marker'));
const radius = thumbDiameter / 2;
let initialPointerX = 0;
let baselineOffset = 0;
const handleDragStart = (event) => {
event.preventDefault();
baselineOffset = parseInt(thumb.style.left) || 0;
initialPointerX = event.clientX;
document.addEventListener('mousemove', handleDragMove);
document.addEventListener('mouseup', handleDragEnd);
};
const handleDragEnd = () => {
document.removeEventListener('mousemove', handleDragMove);
document.removeEventListener('mouseup', handleDragEnd);
};
thumb.addEventListener('mousedown', handleDragStart);
Calculating Proximity and Vertical Offset
During the drag movement, the thumb's horizontal position is updated while enforcing boundary limits. Simultaneously, each marker's vertical displacement and transparency are calculated relative to the thumb's center point.
If a marker falls within the thumb's radius, its vertical shift is derived from its distance to the center. Ideally, representing a semicircular edge requires a quadratic equation y = sqrt(R^2 - x^2), where R is the radius and x is the horizontal offset. For simplicity, a linear approximation y = |x| - R is applied directly to the element's top style, where a negative value elevates the marker upwards.
const handleDragMove = (event) => {
event.preventDefault();
const trackBounds = document.getElementById('slider-track').getBoundingClientRect();
const maxTravel = trackBounds.width - thumbDiameter;
const deltaX = event.clientX - initialPointerX;
let currentOffset = Math.max(0, Math.min(baselineOffset + deltaX, maxTravel));
thumb.style.left = `${currentOffset}px`;
const thumbCenter = currentOffset + radius;
markers.forEach((marker) => {
const markerPosition = marker.offsetLeft + marker.offsetWidth / 2;
const offsetFromCenter = markerPosition - thumbCenter;
if (Math.abs(offsetFromCenter) <= radius) {
const verticalShift = Math.abs(offsetFromCenter) - radius;
const dynamicOpacity = 0.3 + Math.abs(1 - offsetFromCenter / radius) * 0.7;
marker.style.top = `${verticalShift}px`;
marker.style.opacity = dynamicOpacity;
} else {
marker.style.top = '0px';
marker.style.opacity = 0.3;
}
});
};