vevet
Version:
Vevet is a JavaScript library for creative development that simplifies crafting rich interactions like split text animations, carousels, marquees, preloading, and more.
483 lines • 18.2 kB
JavaScript
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
return c > 3 && r && Object.defineProperty(target, key, r), r;
};
import { Module } from '../../base/Module';
import { initVevet } from '../../global/initVevet';
import { cnAdd, cnRemove, cnToggle } from '../../internal/cn';
import { body, doc } from '../../internal/env';
import { isFiniteNumber } from '../../internal/isFiniteNumber';
import { noopIfDestroyed } from '../../internal/noopIfDestroyed';
import { getTextDirection } from '../../internal/textDirection';
import { toPixels } from '../../utils';
import { addEventListener } from '../../utils/listeners';
import { clamp, lerp } from '../../utils/math';
import { Raf } from '../Raf';
import { LERP_APPROXIMATION } from './constants';
import { CursorHoverElement } from './HoverElement';
import { CursorPath } from './Path';
import { MUTABLE_PROPS, STATIC_PROPS } from './props';
import { createCursorStyles } from './styles';
export * from './types';
/**
* A customizable custom cursor component with smooth animations and hover interactions.
* Supports dynamic appearance changes and enhanced user interaction.
*
* [Documentation](https://vevetjs.com/docs/Cursor)
*
* @group Components
*/
export class Cursor extends Module {
/** Get default static properties */
_getStatic() {
return Object.assign(Object.assign({}, super._getStatic()), STATIC_PROPS);
}
/** Get default mutable properties */
_getMutable() {
return Object.assign(Object.assign({}, super._getMutable()), MUTABLE_PROPS);
}
constructor(props, onCallbacks) {
super(props, onCallbacks);
/** Attached hover elements */
this._elements = [];
/** Active hovered element */
this._activeElements = [];
/** Defines if the cursor has been moved after initialization */
this._isFirstMove = true;
const { enabled: isEnabled } = this.props;
const { initialWidth, initialHeight } = this;
// Set default variables
this._coords = {
x: 0,
y: 0,
width: initialWidth,
height: initialHeight,
angle: 0,
velocity: 0,
};
this._rawTarget = Object.assign({}, this._coords);
this._types = [];
this._activeTypes = [];
// Create cursor path
this._path = new CursorPath(this.hasPath);
// No need to remove styles on destroy
createCursorStyles(this.prefix);
// Setup
this._setClassNames();
this._createElements();
this._setEvents();
// enable by default
this._toggle(isEnabled);
}
/**
* Classname prefix for styling elements.
*/
get prefix() {
return `${initVevet().prefix}cursor`;
}
/** The cursor container */
get container() {
return this.props.container;
}
/** Returns the DOM parent for the cursor element. */
get domContainer() {
if (this.container instanceof Window) {
return body;
}
return this.container;
}
/**
* The outer element of the custom cursor.
* This is the visual element that represents the cursor on screen.
*/
get outer() {
return this._outer;
}
/**
* The inner element of the custom cursor.
* This element is nested inside the outer element and can provide additional styling.
*/
get inner() {
return this._inner;
}
/** Cursor initial width */
get initialWidth() {
return toPixels(this.props.width);
}
/** Cursor initial width */
get initialHeight() {
return toPixels(this.props.height);
}
/**
* The current coordinates (x, y, width, height).
* These are updated during cursor movement.
*/
get coords() {
return this._coords;
}
/**
* The currently hovered element.
* Stores information about the element that the cursor is currently interacting with.
*/
get hoveredElement() {
const activeElements = this._activeElements;
return activeElements[activeElements.length - 1];
}
/** Target coordinates of the cursor (without smooth interpolation). */
get targetCoords() {
var _a, _b, _c, _d;
const { hoveredElement, initialWidth, initialHeight } = this;
let { x, y } = this._rawTarget;
const { angle, velocity } = this._rawTarget;
let width = initialWidth;
let height = initialHeight;
let padding = 0;
if (hoveredElement) {
const dimensions = hoveredElement.getDimensions();
width = (_a = dimensions.width) !== null && _a !== void 0 ? _a : initialWidth;
height = (_b = dimensions.height) !== null && _b !== void 0 ? _b : initialHeight;
x = (_c = dimensions.x) !== null && _c !== void 0 ? _c : x;
y = (_d = dimensions.y) !== null && _d !== void 0 ? _d : y;
padding = dimensions.padding;
}
width += padding * 2;
height += padding * 2;
return { x, y, width, height, angle, velocity };
}
/** Returns an SVG path element which represents the cursor movement */
get path() {
return this._path.path;
}
/** Check if the cursor has a path */
get hasPath() {
return this.props.behavior === 'path';
}
/** Handles property mutations */
_handleProps(props) {
super._handleProps(props);
this._toggle(this.props.enabled);
}
/** Sets class names */
_setClassNames() {
const { domContainer } = this;
// Hide native cursor
if (this.props.hideNative) {
domContainer.style.cursor = 'none';
this._addTempClassName(domContainer, this._cn('-hide-default'));
}
// Set class names
this._addTempClassName(domContainer, this._cn('-container'));
// Set container position
if (domContainer !== body) {
domContainer.style.position = 'relative';
}
// Reset styles
this.onDestroy(() => {
domContainer.style.cursor = '';
});
}
/** Creates the custom cursor and appends it to the DOM. */
_createElements() {
const { container, domContainer, props } = this;
const isWindow = container instanceof Window;
const cn = this._cn.bind(this);
// Create outer element
const outer = doc.createElement('div');
cnAdd(outer, cn(''));
cnAdd(outer, cn(isWindow ? '-in-window' : '-in-element'));
cnAdd(outer, cn('-disabled'));
// Append the outer element to the DOM container
if (props.append) {
domContainer.append(outer);
}
// set direction
const direction = getTextDirection(outer);
cnAdd(outer, cn(`_${direction}`));
// Create inner element
const inner = doc.createElement('div');
outer.append(inner);
cnAdd(inner, cn('__inner'));
cnAdd(inner, cn('-disabled'));
outer.append(inner);
// assign
this._outer = outer;
this._inner = inner;
// Destroy the cursor
this.onDestroy(() => {
inner.remove();
outer.remove();
});
}
/** Sets up the various event listeners for the cursor, such as mouse movements and clicks. */
_setEvents() {
const { domContainer } = this;
this._raf = new Raf({ enabled: false });
this._raf.on('frame', () => this.render());
const mouseenter = addEventListener(domContainer, 'mouseenter', this._handleMouseEnter.bind(this));
const mouseleave = addEventListener(domContainer, 'mouseleave', this._handleMouseLeave.bind(this));
const mousemove = addEventListener(domContainer, 'mousemove', this._handleMouseMove.bind(this));
const mousedown = addEventListener(domContainer, 'mousedown', this._handleMouseDown.bind(this));
const mouseup = addEventListener(domContainer, 'mouseup', this._handleMouseUp.bind(this));
const blur = addEventListener(window, 'blur', this._handleWindowBlur.bind(this));
this.onDestroy(() => {
var _a;
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.destroy();
mouseenter();
mouseleave();
mousemove();
mousedown();
mouseup();
blur();
});
}
/** Enables cursor animation. */
_toggle(enabled) {
var _a;
const className = this._cn('-disabled');
cnToggle(this.outer, className, !enabled);
cnToggle(this.inner, className, !enabled);
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.updateProps({ enabled });
}
/** Handles mouse enter events. */
_handleMouseEnter(evt) {
if (!this.props.enabled) {
return;
}
const { clientX: x, clientY: y } = evt;
const target = this._rawTarget;
this._coords.x = x;
this._coords.y = y;
target.x = x;
target.y = y;
this._path.addPoint(target, true);
cnAdd(this.outer, this._cn('-visible'));
}
/** Handles mouse leave events. */
_handleMouseLeave() {
cnRemove(this.outer, this._cn('-visible'));
}
/** Handles mouse move events. */
_handleMouseMove(evt) {
var _a;
if (!this.props.enabled) {
return;
}
const { clientX: x, clientY: y } = evt;
const target = this._rawTarget;
const { x: prevX, y: prevY } = target;
// Calculate angle
const deltaX = prevX - this._coords.x;
const deltaY = prevY - this._coords.y;
const prevAngle = target.angle;
const rawAngle = (Math.atan2(deltaY, deltaX) * 180) / Math.PI;
const targetAngle = prevAngle + ((((rawAngle - prevAngle) % 360) + 540) % 360) - 180;
// Calculate velocity
const velocity = Math.min(Math.sqrt(Math.pow(deltaX, 2) + Math.pow(deltaY, 2)) * 2, 150) / 150;
// Update target coordinates
target.x = x;
target.y = y;
target.angle = targetAngle;
target.velocity = velocity;
// Update interpolated coords if first move
if (this._isFirstMove) {
this._coords.x = target.x;
this._coords.y = target.y;
this._coords.angle = target.angle;
this._coords.velocity = target.velocity;
this._isFirstMove = false;
}
// Add path point
this._path.addPoint(target);
// Handle classnames
cnAdd(this.outer, this._cn('-visible'));
// Enable animation
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.play();
}
/** Handles mouse down events. */
_handleMouseDown(evt) {
const className = this._cn('-click');
if (evt.which === 1) {
cnAdd(this.outer, className);
cnAdd(this.inner, className);
}
}
/** Handles mouse up events. */
_handleMouseUp() {
const className = this._cn('-click');
cnRemove(this.outer, className);
cnRemove(this.inner, className);
}
/** Handles window blur events. */
_handleWindowBlur() {
this._handleMouseUp();
}
/**
* Registers an element to interact with the cursor, enabling dynamic size and position changes based on hover effects.
* @returns Returns a destructor
*/
attachHover(settings) {
const element = new CursorHoverElement(settings, (data) => this._handleElementEnter(data), (data) => this._handleElementLeave(data));
this._elements.push(element);
const destroy = () => {
this._elements = this._elements.filter((i) => i !== element);
element.destroy();
};
this.onDestroy(() => destroy());
return () => destroy();
}
/** Handle element mouse enter event */
_handleElementEnter(data) {
var _a;
if (!this.props.enabled) {
return;
}
this._activeElements.push(data);
if (data.type) {
this._toggleType(data.type, true);
}
this.callbacks.emit('hoverEnter', data);
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.play();
}
/** Handle element mouse leave event */
_handleElementLeave(data) {
var _a;
this._activeElements = this._activeElements.filter((i) => i !== data);
if (data.type) {
this._toggleType(data.type, false);
}
this.callbacks.emit('hoverLeave', data);
if (this.props.enabled) {
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.play();
}
}
/**
* Registers a cursor type.
*/
attachCursor({ element, type }) {
var _a;
this._types.push({ element, type });
(_a = this._inner) === null || _a === void 0 ? void 0 : _a.append(element);
}
/** Enable or disable a cursor type */
_toggleType(type, isEnabled) {
const targetType = this._types.find((item) => item.type === type);
if (isEnabled) {
this._activeTypes.push(type);
}
else {
this._activeTypes = this._activeTypes.filter((item) => type !== item);
}
const activeTypes = this._activeTypes;
const activeType = activeTypes.length > 0 ? activeTypes[activeTypes.length - 1] : null;
this._types.forEach((item) => {
cnToggle(item.element, 'active', item.type === activeType);
});
if (targetType) {
this.callbacks.emit(isEnabled ? 'typeShow' : 'typeHide', targetType);
}
if (!activeType) {
this.callbacks.emit('noType', undefined);
}
}
/**
* Checks if all coordinates are interpolated.
* @returns {boolean} True if all coordinates are interpolated, false otherwise.
*/
get isInterpolated() {
const { coords, targetCoords, props } = this;
const isWidthDone = coords.width === targetCoords.width;
const isHeightDone = coords.height === targetCoords.height;
const isAngleDone = coords.angle === targetCoords.angle;
const isVelocityDone = coords.velocity === targetCoords.velocity;
const isElementsDone = !this._elements.find((element) => !element.isInterpolated);
const isPathDone = this._path.isInterpolated;
const isCoordsDone = coords.x === targetCoords.x && coords.y === targetCoords.y;
return (isWidthDone &&
isHeightDone &&
isAngleDone &&
isVelocityDone &&
isElementsDone &&
(props.behavior === 'path' ? isPathDone : isCoordsDone));
}
/** Renders the cursor. */
render() {
var _a;
this._calculate();
this._renderElements();
if (this.props.autoStop && this.isInterpolated) {
(_a = this._raf) === null || _a === void 0 ? void 0 : _a.pause();
}
// Launch render events
this.callbacks.emit('render', undefined);
}
/** Recalculates current coordinates. */
_calculate() {
const { targetCoords: target, _coords: coords } = this;
const lerpFactor = this._getLerpFactor();
this._path.lerp(lerpFactor);
this._path.minimize();
try {
if (this.hasPath) {
const pathCoord = this._path.coord;
coords.x = pathCoord.x;
coords.y = pathCoord.y;
}
else {
throw new Error('No path');
}
}
catch (_a) {
coords.x = this._lerp(coords.x, target.x);
coords.y = this._lerp(coords.y, target.y);
}
coords.width = this._lerp(coords.width, target.width);
coords.height = this._lerp(coords.height, target.height);
coords.angle = this._lerp(coords.angle, target.angle);
this._rawTarget.velocity = this._lerp(this._rawTarget.velocity, 0);
coords.velocity = this._lerp(coords.velocity, this._rawTarget.velocity);
}
/** Gets the interpolation factor. */
_getLerpFactor(input = this.props.lerp) {
if (!isFiniteNumber(input)) {
return 1;
}
const lerpFactor = clamp(input, 0, 1);
return this._raf.lerpFactor(lerpFactor);
}
/** Performs linear interpolation. */
_lerp(current, target) {
const lerpFactor = this._getLerpFactor();
const value = lerp(current, target, lerpFactor, LERP_APPROXIMATION);
return value;
}
/** Renders the cursor elements. */
_renderElements() {
const { container, domContainer, outer, props, coords } = this;
const { width, height } = coords;
let { x, y } = coords;
if (!(container instanceof Window)) {
const bounding = domContainer.getBoundingClientRect();
x -= bounding.left;
y -= bounding.top;
}
// Update DOM coordinates
const { style } = outer;
style.setProperty('--cursor-w', `${width}px`);
style.setProperty('--cursor-h', `${height}px`);
style.transform = props.transformModifier(Object.assign(Object.assign({}, coords), { x, y }));
// Render element
this._elements.forEach((element) => element.render(this._getLerpFactor.bind(this)));
}
}
__decorate([
noopIfDestroyed
], Cursor.prototype, "attachHover", null);
__decorate([
noopIfDestroyed
], Cursor.prototype, "attachCursor", null);
__decorate([
noopIfDestroyed
], Cursor.prototype, "render", null);
//# sourceMappingURL=index.js.map