proteuscursor
Version:
Proteus Cursor is a dynamic JavaScript library that revolutionizes web user interaction by transforming the mouse cursor based on HTML element interactions. Inspired by the shape-shifting god Proteus, this library allows the cursor to change into various
269 lines (267 loc) • 16.1 kB
JavaScript
/*!
* Proteus Cursor v1.1.5
* https://github.com/Shuriken933/proteus-cursor
*
* A dynamic JavaScript library that transforms the default mouse cursor
* into interactive shapes based on HTML element interactions.
* Inspired by Proteus, the Greek god of change, this library provides
* a flexible way to customize the user’s pointer experience on the web.
*
* Features:
* - Cursor shape customization (dot, circle, fluid, text, etc.)
* - Magnetic effects
* - Smooth shadow animations
* - Easy integration via ES module or browser script tag
*
* Author: Eros Agostini (https://github.com/Shuriken933)
* License: MIT
* Released: July 2025
*
* © 2025 Eros Agostini. All rights reserved.
*/
class x {
//region 🔹 initialization
// internal state
velocity = 0;
_x = 0;
_y = 0;
mouseX = 0;
mouseY = 0;
cursorX = 0;
cursorY = 0;
prevMouseX = 0;
prevMouseY = 0;
//region 🏗️ constructor
constructor(t = {}) {
this.testMode = !1, this.shape = t.shape || "default", this.shape_size = t.shape_size || "10px", this.shape_color = t.shape_color || "#fff", this.hasShadow = t.hasShadow ?? !0, this.hasShadow ? (this.shadow_delay = t.shadow_delay || "0.3s", this.shadow_size = t.shadow_size || "40px", this.shadow_color = t.shadow_color || "#ffffff") : (this.shadow_delay = "0s", document.querySelector(".proteus-cursor-shadow").style.display = "none"), this.text = "", this.text_color = "", this.text_weight = "", this.text_size = "", this.speed = 0.9, this.maxVelocity = 10, this.isMagnetic = !1, this.eventListeners = [], this.animationIds = [], this.intervals = [], this.timeouts = [], this.isDestroyed = !1, this.boundMouseMove = this.handleMouseMove.bind(this), this.boundMouseEnter = this.handleMouseEnter.bind(this), this.boundMouseLeave = this.handleMouseLeave.bind(this), this.boundAnimateCircle = this.animateCircleShadow.bind(this), this.boundAnimateFluid = this.animateFluidCursor.bind(this), this.init(), this.dataAttributeEvents();
}
//endregion
init() {
this.init_HTMLcursorAndShadow(), this.$shape = document.getElementById("proteus-cursor-shape"), this.$shadow = document.getElementById("proteus-cursor-shadow"), this.$shape.style.width = this.shape_size || "20px", this.$shape.style.height = this.shape_size || "20px", this.$shadow.style.width = this.shadow_size || "40px", this.$shadow.style.height = this.shadow_size || "40px", this.setShape(this.shape);
}
init_HTMLcursorAndShadow() {
if (document.getElementById("proteus-cursor-shape")) return;
const t = document.createElement("div");
t.className = "proteus-cursor-shape", t.id = "proteus-cursor-shape";
const e = document.createElement("div");
e.className = "proteus-cursor-shadow", e.id = "proteus-cursor-shadow";
const s = document.body;
s.prepend(t), s.prepend(e);
}
// Metodo helper per aggiungere event listeners tracciabili
addEventListenerTracked(t, e, s, i = !1) {
this.isDestroyed || (t.addEventListener(e, s, i), this.eventListeners.push({
element: t,
event: e,
handler: s,
options: i
}));
}
// Metodo helper per requestAnimationFrame tracciabile
requestAnimationFrameTracked(t) {
if (this.isDestroyed) return;
const e = requestAnimationFrame(t);
return this.animationIds.push(e), e;
}
setShape(t) {
switch (document.querySelector("body").classList.remove("proteus-is-a-fluid"), document.querySelector("body").classList.remove("proteus-is-a-circle"), console.log("setShape executed"), this.shape = t, this.shape) {
case "default":
break;
case "circle":
this.setShape__circle(this.shape);
break;
case "fluid":
this.setShape__fluid();
break;
}
$(this.shape);
}
//endregion
/* -------------------------------------------------------------------------------- */
/* ! Type Shape */
/* -------------------------------------------------------------------------------- */
//region 🧩 Type shape CIRCLE
setShape__circle(t) {
this.delay = 8, this._x = 0, this._y = 0, this.endX = window.innerWidth / 2, this.endY = window.innerHeight / 2, this.cursorVisible = !0, this.cursorEnlarged = !1, document.querySelector("body").classList.add("proteus-is-a-circle"), document.body.style.cursor = "none", this.shape__circle__interactions(), this.shape__circle__animateShadow();
}
shape__circle__interactions() {
this.isDestroyed || (document.querySelectorAll("a, button, input").forEach((t) => {
this.addEventListenerTracked(t, "mouseover", this.boundMouseEnter), this.addEventListenerTracked(t, "mouseout", this.boundMouseLeave);
}), this.addEventListenerTracked(document, "mousemove", this.boundMouseMove));
}
handleMouseMove(t) {
this.isDestroyed || (this.cursorVisible = !0, this.toggleCursorVisibility(), this.endX = t.pageX, this.endY = t.pageY, this.$shape && (this.$shape.style.top = this.endY + "px", this.$shape.style.left = this.endX + "px"));
}
handleMouseEnter() {
this.isDestroyed || (this.cursorEnlarged = !0, this.toggleCursorSize());
}
handleMouseLeave() {
this.isDestroyed || (this.cursorEnlarged = !1, this.toggleCursorSize());
}
// Modifica il metodo di animazione del circle shadow
animateCircleShadow() {
this.isDestroyed || (this._x += (this.endX - this._x) / this.delay, this._y += (this.endY - this._y) / this.delay, this.$shadow && (this.$shadow.style.top = this._y + "px", this.$shadow.style.left = this._x + "px"), this.requestAnimationFrameTracked(this.boundAnimateCircle));
}
shape__circle__animateShadow() {
this.animateCircleShadow();
}
toggleCursorSize() {
this.cursorEnlarged ? (this.$shape.style.transform = "translate(-50%, -50%) scale(1.5)", this.$shadow.style.transform = "translate(-50%, -50%) scale(1.5)") : (this.$shape.style.transform = "translate(-50%, -50%) scale(1)", this.$shadow.style.transform = "translate(-50%, -50%) scale(1)");
}
toggleCursorVisibility() {
this.cursorVisible ? (this.$shape.style.opacity = 1, this.$shadow.style.opacity = 1) : (this.$shape.style.opacity = 0, this.$shadow.style.opacity = 0);
}
//endregion
//region 🧩 Type shape FLUID
setShape__fluid__animateCursor__calcVelocity() {
const t = (e) => {
if (this.isDestroyed) return;
const s = e.clientX - this.prevMouseX, i = e.clientY - this.prevMouseY;
this.velocity = Math.sqrt(s * s + i * i), this.prevMouseX = this.mouseX, this.prevMouseY = this.mouseY, this.mouseX = e.clientX, this.mouseY = e.clientY;
};
this.addEventListenerTracked(document, "mousemove", t);
}
setShape__fluid__animateCursor() {
if (this.isDestroyed) return;
this.velocityInitialized || (this.setShape__fluid__animateCursor__calcVelocity(), this.velocityInitialized = !0), this.cursorX += (this.mouseX - this.cursorX) * this.speed, this.cursorY += (this.mouseY - this.cursorY) * this.speed;
const t = Math.min(this.velocity / this.maxVelocity, 1);
if (t > 0.01) {
const e = this.mouseX - this.cursorX, s = this.mouseY - this.cursorY, i = Math.sqrt(e * e + s * s);
if (i > 0) {
const o = e / i, h = s / i, r = 1 + t * 1.5, n = 1 - t * 0.3, d = o * o * (r - 1) + 1, c = o * h * (r - 1), p = o * h * (r - 1), y = h * h * (r - 1) + 1, l = -h, u = o, f = d + l * l * (n - 1), m = c + l * u * (n - 1), _ = p + l * u * (n - 1), b = y + u * u * (n - 1);
this.$shape && (this.$shape.style.transform = `matrix(${f}, ${m}, ${_}, ${b}, 0, 0)`);
} else this.$shape && (this.$shape.style.transform = "matrix(1, 0, 0, 1, 0, 0)");
} else this.$shape && (this.$shape.style.transform = "matrix(1, 0, 0, 1, 0, 0)");
this.$shape && (this.$shape.style.left = this.cursorX - this.$shape.offsetWidth / 2 + "px", this.$shape.style.top = this.cursorY - this.$shape.offsetHeight / 2 + "px"), this.velocity *= 0.95, this.requestAnimationFrameTracked(this.boundAnimateFluid);
}
animateFluidCursor() {
this.setShape__fluid__animateCursor();
}
setShape__fluid() {
if (document.querySelector("body").classList.add("proteus-is-a-fluid"), document.body.style.cursor = "none", !this.$shape) {
console.error("Elemento con id 'cursor' non trovato!");
return;
}
this.$shape.style.position = "fixed", this.$shape.style.width = this.shape_size || "20px", this.$shape.style.height = this.shape_size || "20px", this.$shape.style.backgroundColor = this.shape_color || "#fff", this.$shape.style.borderRadius = "50%", this.$shape.style.pointerEvents = "none", this.$shape.style.zIndex = "9999", this.$shape.style.transition = "all 0.3s cubic-bezier(0.23, 1, 0.320, 1)", this.hasShadow && (this.$shape.style.boxShadow = `0 0 ${this.shadow_size} ${this.shadow_color}`), this.velocityInitialized = !1, this.cursorX = window.innerWidth / 2, this.cursorY = window.innerHeight / 2, this.setShape__fluid__animateCursor();
}
//endregion
//region ❌ destroy Proteus
destroy() {
console.log("🔴 Destroying ProteusCursor instance..."), this.isDestroyed = !0, this.animationIds.forEach((e) => {
cancelAnimationFrame(e);
}), this.animationIds = [], this.intervals.forEach((e) => clearInterval(e)), this.timeouts.forEach((e) => clearTimeout(e)), this.intervals = [], this.timeouts = [], this.eventListeners.forEach(({ element: e, event: s, handler: i, options: o }) => {
try {
e.removeEventListener(s, i, o);
} catch (h) {
console.warn("Error removing event listener:", h);
}
}), this.eventListeners = [], document.body.style.cursor = "";
const t = document.querySelector("body");
t && (t.classList.remove("proteus-is-a-fluid"), t.classList.remove("proteus-is-a-circle")), this.$shape && (this.$shape.style.cssText = "", this.$shape.style.display = "none", this.$shape.style.opacity = "0", this.$shape.style.transform = "", this.$shape.style.left = "", this.$shape.style.top = "", this.$shape.style.width = "", this.$shape.style.height = "", this.$shape.style.backgroundColor = "", this.$shape.style.borderRadius = "", this.$shape.style.boxShadow = "", this.$shape.textContent = ""), this.$shadow && (this.$shadow.style.cssText = "", this.$shadow.style.display = "none", this.$shadow.style.opacity = "0", this.$shadow.style.transform = "", this.$shadow.style.left = "", this.$shadow.style.top = "", this.$shadow.style.width = "", this.$shadow.style.height = "", this.$shadow.style.backgroundColor = ""), this.$shape = null, this.$shadow = null, this.boundMouseMove = null, this.boundMouseEnter = null, this.boundMouseLeave = null, this.boundAnimateCircle = null, this.boundAnimateFluid = null, this.velocity = 0, this._x = 0, this._y = 0, this.mouseX = 0, this.mouseY = 0, this.cursorX = 0, this.cursorY = 0, this.prevMouseX = 0, this.prevMouseY = 0, this.velocityInitialized = !1, console.log("✅ ProteusCursor instance completely destroyed");
}
//endregion
/* -------------------------------------------------------------------------------- */
//region 🏷️ Setters
/* -------------------------------------------------------------------------------- */
setShapeSize(t, e, s = !1) {
console.log("setShapeSize executed"), S(this), s ? (this.shape_size = t || "20px", this.shadow_size = e || "20px", this.$shape.style.width = t || "20px", this.$shape.style.height = e || "20px") : (this.$shape.style.width = t || "20px", this.$shape.style.height = e || "20px");
}
setShapeColor(t, e = !1) {
e ? (this.shape_color = t, this.$shape.style.backgroundColor = t) : this.$shape.style.backgroundColor = t;
}
setShadowEnabled(t, e = !1) {
this.shape === "circle" ? e ? (this.hasShadow = t, this.hasShadow ? this.$shadow.style.display = "block" : this.$shadow.style.display = "none") : t ? this.$shadow.style.display = "block" : this.$shadow.style.display = "none" : this.shape === "fluid" && e && (this.$shape.style.boxShadow = `0 0 ${this.shadow_size} ${this.shadow_color}`);
}
setShadowSize(t, e) {
this.$shadow.style.width = t || "20px", this.$shadow.style.height = e || "20px";
}
setShadowColor(t, e = 0.5) {
const s = w(t, e);
this.$shadow.style.backgroundColor = s;
}
setText(t, e = !1) {
e ? (this.text = t, document.querySelector(".proteus-cursor-shape").textContent = this.text) : document.querySelector(".proteus-cursor-shape").textContent = t;
}
setTextColor(t, e = !1) {
e && (this.text_color = t), document.querySelector(".proteus-cursor-shape").style.color = t;
}
setTextWeight(t, e = !1) {
e && (this.text_weight = t), document.querySelector(".proteus-cursor-shape").style.fontWeight = t;
}
setTextSize(t, e = !1) {
e && (this.text_size = t), document.querySelector(".proteus-cursor-shape").style.fontSize = t;
}
setSpeed(t) {
this.speed = t;
}
setMaxVelocity(t) {
this.maxVelocity = t;
}
//endregion
/* -------------------------------------------------------------------------------- */
//region ✨ Data Attribute
/* -------------------------------------------------------------------------------- */
dataAttributeEvents() {
document.querySelectorAll(
"[data-proteus-shapeSize], [data-proteus-shapeColor], [data-proteus-text], [data-proteus-textColor], [data-proteus-textSize], [data-proteus-textWeight]"
).forEach((t) => {
t.addEventListener("mouseenter", () => {
const e = t.getAttribute("data-proteus-shapeSize"), s = t.getAttribute("data-proteus-shapeColor"), i = t.getAttribute("data-proteus-text"), o = t.getAttribute("data-proteus-textColor"), h = t.getAttribute("data-proteus-textSize"), r = t.getAttribute("data-proteus-textWeight"), n = t.getAttribute("data-proteus-shadowIsEnabled");
e && this.setShapeSize(e, e), s && this.setShapeColor(s), i && this.setText(i), o && this.setTextColor(o), h && this.setTextSize(h), r && this.setTextWeight(r), n && this.setShadowEnabled(!0);
}), t.addEventListener("mouseleave", () => {
this.setShapeSize(this.shape_size, this.shape_size), this.setShapeColor(this.shape_color), this.setText(this.text), this.setTextColor(this.text_color), this.setTextSize(this.text_size), this.setTextWeight(this.text_weight);
});
});
}
//endregion
/* -------------------------------------------------------------------------------- */
//region 🧪 TEST MODE
/* -------------------------------------------------------------------------------- */
enableTestMode() {
this.testMode = !0, this.enableTestMode__generateHTML();
const t = document.querySelector("#proteus-panel-test"), e = document.querySelector("#proteus-button-test");
e.addEventListener("click", () => {
t.classList.toggle("open");
}), document.querySelector("#button-setShape-dot"), document.querySelector("#button-setShape-circle"), e.classList.add("active");
}
enableTestMode__generateHTML() {
const t = document.querySelector("body");
t.insertAdjacentHTML("beforeend", `
<button id="proteus-button-test">
<img src="icons/icon-cursor.svg" alt="icon" width="35" height="35" />
</button>
`), t.insertAdjacentHTML("beforeend", `
<div id="proteus-panel-test">
<p>Type cursor</p>
<ul>
<li><button id="button-setShape-circle">circle</button></li>
<li><button id="button-setShape-dot">dot</button></li>
<li><button></button></li>
<li><button></button></li>
</ul>
<p>Modifiers</p>
<ul>
<li><button>magnetic</button></li>
<li><button>parallax hover</button></li>
<li><button>text</button></li>
</ul>
</div>
`);
}
disableTestMode() {
this.testMode = !1, document.querySelector("#proteus-button-test").classList.remove("active");
}
}
function $(a) {
console.log("This is the type: ", a);
}
function w(a, t = 1) {
const e = parseInt(a.slice(1, 3), 16), s = parseInt(a.slice(3, 5), 16), i = parseInt(a.slice(5, 7), 16);
return `rgba(${e}, ${s}, ${i}, ${t})`;
}
function S(a) {
console.log(a);
}
export {
x as default
};