UNPKG

@yoannchb/cattract

Version:

Animate anything just like it was attracted by the cursor

310 lines (277 loc) 9.02 kB
type With3dOptions = { axe?: "x" | "y"; inverted?: boolean | "x" | "y"; maxAngle?: number; perspective?: number; }; type Options = { elementRadius?: number; detectionRadius?: number | "full"; animation?: { ease?: string; duration?: number; }; scale?: { from?: number; to?: number; animated?: boolean; }; inverted?: boolean | "x" | "y"; axe?: "x" | "y"; with_3d?: boolean | With3dOptions; }; //all the instance of Cattract class const instances: Cattract[] = []; const defaultOptions: Options = { elementRadius: 20, detectionRadius: 50, animation: { ease: "ease-in-out", duration: 1000, }, }; const default3dOptions: With3dOptions = { maxAngle: 35, perspective: 500, }; class Cattract { /** * Create a circle attraction animation on an element * @param target The element you want to be animated * @param options The differents options */ constructor(public target: HTMLElement, public options: Options = {}) { this.options = Object.assign({}, defaultOptions, options); this.options.animation = Object.assign( {}, defaultOptions.animation, options.animation ?? {} ); if (this.options.with_3d) { this.options.with_3d = Object.assign( {}, default3dOptions, this.options.with_3d === true ? {} : this.options.with_3d ); } this.target.style.transformOrigin = "center"; this.start(); } /** * Apply translation on the target with animation * @param translation */ private applyTranslation(translation: string) { this.target.animate([{ transform: translation }], { duration: this.options.animation.duration, fill: "forwards", easing: this.options.animation.ease, }); // this.target.style.transform = translation; } /** * Return the screen radius * @returns */ private getScreenRadius() { return Math.max(window.innerWidth, window.innerHeight) / 2; } /** * Get delta for a specified invertion * @param invertion * @returns */ private getDeltaFromInvertion(invertion: boolean | "x" | "y") { const delta = { x: 1, y: 1 }; if (invertion === true) { delta.x = delta.y = -1; } else if (invertion === "x") { delta.x = -1; } else if (invertion === "y") { delta.y = -1; } return delta; } /** * Create a circle for the debug * @param radius * @returns */ private createCircle(radius: number, color: string) { const circle = document.createElement("div"); circle.setAttribute( "style", ` position: absolute; background-color: transparent; width: ${radius * 2}px; aspect-ratio: 1/1; border: thin ${color} solid; border-radius: 100%; ` .replace(/\n+/g, "") .replace(/\s+/g, " ") ); return circle; } /** * Display circle for detection zone and the element circle for better debugging * @param color */ debug(color: string = "#e1e1e130") { this.target.parentElement.style.position = "relative"; const append = (element: HTMLDivElement) => { this.target.parentNode.insertBefore(element, this.target); }; const elementRadiusCircle = this.createCircle( this.options.elementRadius, color ); append(elementRadiusCircle); if (this.options.detectionRadius !== "full") { const detectionRadiusCircle = this.createCircle( this.options.detectionRadius, color ); append(detectionRadiusCircle); } } /** * Get the actual needed transformation properties of the element * @returns */ private getTransformation() { const computedStyles = window.getComputedStyle(this.target); const transformValue = computedStyles.transform; let translationX = 0; let translationY = 0; let scaleX = 1; let scaleY = 1; if (transformValue && transformValue !== "none") { const matrixRegex = /(matrix3d|matrix)\(([^)]+)\)/; const match = transformValue.match(matrixRegex); if (match) { const matrixValues = match[2].split(",").map(parseFloat); if (match[1] === "matrix") { // 2D transformation matrix scaleX = matrixValues[0]; scaleY = matrixValues[3]; translationX = matrixValues[4]; translationY = matrixValues[5]; } else if (match[1] === "matrix3d") { // 3D transformation matrix scaleX = matrixValues[0]; scaleY = matrixValues[5]; translationX = matrixValues[12]; translationY = matrixValues[13]; // const translationZ = matrixValues[14]; // const scaleZ = matrixValues[10]; } } } return { translationX, translationY, scaleX, scaleY }; } /** * Update the position of the targer * @param x The x position of the cursor * @param y The y position of the cursor */ update(x: number, y: number) { //TODO: Fixe this shit (fuckng lagging) const rect = this.target.getBoundingClientRect(); const trans = this.getTransformation(); const width = rect.width / trans.scaleX; const height = rect.height / trans.scaleY; const targetMiddleX = rect.left - trans.translationX + width / 2; const targetMiddleY = rect.top - trans.translationY + height / 2; const [dx, dy] = [x - targetMiddleX, y - targetMiddleY]; const mouseRadius = Math.sqrt(dx * dx + dy * dy); if ( this.options.detectionRadius === "full" || mouseRadius <= this.options.detectionRadius ) { const transformations: string[] = []; const hypp = Math.sqrt(dx * dx + dy * dy); const tx = hypp === 0 ? 0 : dx / hypp; const ty = hypp === 0 ? 0 : dy / hypp; const totalRadius = this.options.detectionRadius === "full" ? this.getScreenRadius() : this.options.detectionRadius; const pourcentage = hypp / totalRadius; const computedRadius = this.options.elementRadius * pourcentage; /* Handle 3D effect */ if (this.options.with_3d) { const options3d = this.options.with_3d as With3dOptions; const delta = this.getDeltaFromInvertion(options3d.inverted); const computedAngle = options3d.maxAngle * pourcentage; transformations.push(`perspective(${options3d.perspective}px)`); if (!options3d.axe || options3d.axe === "x") transformations.push(`rotateX(${ty * computedAngle * delta.x}deg)`); if (!options3d.axe || options3d.axe === "y") transformations.push(`rotateY(${-tx * computedAngle * delta.y}deg)`); } /* Handle scale */ if (this.options.scale?.to) { const scaleOptions = this.options.scale; const scaleFrom = scaleOptions.from ?? 1; let finalScale = scaleOptions.to - scaleFrom; if (scaleOptions.animated) finalScale *= pourcentage; transformations.push(`scale(${scaleFrom + finalScale})`); } /* Handle translation */ const delta = this.getDeltaFromInvertion(this.options.inverted); const [transX, transY] = [ tx * computedRadius * delta.x, ty * computedRadius * delta.y, ]; if (!this.options.axe || this.options.axe === "x") transformations.push(`translateX(${transX}px)`); if (!this.options.axe || this.options.axe === "y") transformations.push(`translateY(${transY}px)`); this.applyTranslation(transformations.join(" ")); } else { if (this.options.scale?.from) this.applyTranslation(`scale(${this.options.scale.from})`); else this.applyTranslation("none"); } } /** * Start attraction animation */ start() { this.target.style.willChange = "transform"; if (this.options.with_3d) this.target.style.transformStyle = "preserve-3d"; if (this.options.scale?.from) this.target.style.transform = `scale(${this.options.scale.from})`; instances.push(this); } /** * Stop the attraction animation */ stop() { const index = instances.findIndex( (instance) => instance.target === this.target ); if (index !== -1) instances.splice(index, 1); } /** * Reset the element tranformation */ reset() { this.target.style.transform = "none"; } } document.addEventListener("DOMContentLoaded", function () { document.body.addEventListener("mousemove", function (event) { const [x, y] = [event.clientX, event.clientY]; for (const instance of instances) { if (!instance.target.isConnected) { instance.stop(); continue; } instance.update(x, y); } }); }); export default Cattract;