UNPKG

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
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