@lostisworld/svelte-interactive-cursor
Version:
A Svelte component for creating interactive custom cursors. Enhance user experience with customizable cursor effects and animations.
110 lines (109 loc) • 4.43 kB
JavaScript
const interactiveCursor = (cursor, options) => {
// set default cursor options
const { defaultSize, scaleOnActive = [], duration, useDataElementRect = [] } = options;
// set initial state
const state = $state({
pointerCoords: { x: 0, y: 0 },
isActive: false,
isHoveringDataElementRect: false,
activeDataElement: null,
activeDataName: '',
dataElementRect: null
});
let currentAnimation;
const triggerAreas = document.querySelectorAll('[data-interactive-cursor-area]');
const animateCursor = (target) => {
if (target.closest('[data-interactive-cursor]')) {
state.activeDataElement = target.closest('[data-interactive-cursor]');
state.activeDataName = state.activeDataElement.getAttribute('data-interactive-cursor') || '';
state.isHoveringDataElementRect =
state.activeDataName !== '' && useDataElementRect.includes(state.activeDataName);
state.dataElementRect = state.activeDataElement.getBoundingClientRect();
}
else {
state.activeDataElement = null;
state.activeDataName = '';
state.isHoveringDataElementRect = false;
}
// Get cursor element and set animation options
const animationKeyframes = () => {
if (state.isHoveringDataElementRect) {
return {
width: `${state.dataElementRect.width}px`,
height: `${state.dataElementRect.height}px`,
transform: `translate3D(${state.dataElementRect.left}px, ${state.dataElementRect.top}px, 0) scale3D(1,1,1)`
};
}
if (scaleOnActive.find((key) => key.element === state.activeDataName)) {
// get the active size multiplicator
const getActiveSizeMultiplicator = scaleOnActive.find((key) => key.element === state.activeDataName)?.scaleMultiplicator;
return {
width: `${defaultSize}px`,
height: `${defaultSize}px`,
transform: `translate3D(${state.pointerCoords.x}px, ${state.pointerCoords.y}px, 0) scale3D(${getActiveSizeMultiplicator ?? 3}, ${getActiveSizeMultiplicator ?? 3}, 1)`
};
}
return {
width: `${defaultSize}px`,
height: `${defaultSize}px`,
transform: `translate3D(${state.pointerCoords.x}px, ${state.pointerCoords.y}px, 0) scale3D(1,1,1)`
};
};
const animationTiming = {
duration: duration,
fill: 'forwards'
};
// animate cursor
currentAnimation = cursor.animate(animationKeyframes(), animationTiming);
};
// start cursor tracking
const startCursorTracking = (event) => {
const { clientX, clientY, target } = event;
state.pointerCoords = {
x: clientX - cursor.offsetWidth / 2,
y: clientY - cursor.offsetHeight / 2
};
state.isActive = true;
// Get the active data element
animateCursor(target);
};
// stop cursor tracking
const stopCursorTracking = () => {
state.pointerCoords = { x: 0, y: 0 };
state.isActive = false;
state.activeDataElement = null;
state.activeDataName = '';
state.isHoveringDataElementRect = false;
currentAnimation?.cancel();
};
// setup event listeners
const init = () => {
triggerAreas.forEach((triggerArea) => {
triggerArea.addEventListener('mousemove', startCursorTracking, { passive: true });
triggerArea.addEventListener('mouseleave', stopCursorTracking);
});
};
// cleanup event listeners
const cleanup = () => {
triggerAreas.forEach((triggerArea) => {
triggerArea.removeEventListener('mousemove', startCursorTracking);
triggerArea.removeEventListener('mouseleave', stopCursorTracking);
});
};
return {
get isActive() {
return state.isActive;
},
get activeDataValue() {
return {
activeDataName: state.activeDataName,
activeDataElement: state.activeDataElement
};
},
init,
destroy() {
cleanup();
}
};
};
export { interactiveCursor };