const CURSOR_TICK_RATE = 1000/30; const CURSOR_LERP_RATE = 1/100; const CURSOR_CHAR_MAX_LIFE = 5000; const CURSOR_MAX_CHARS = 50; /** @type HTMLElement */ let cursorContainer; /** @type Cursor */ let myCursor; /** @type Map */ let cursors = new Map(); /** @type Array */ let chars = new Array(); let lastCursorUpdateTime = 0; let lastCharUpdateTime = 0; class Cursor { /** @type number */ id; // real coordinates (canonical) /** @type number */ x; /** @type number */ y; // update coordinates (interpolated) /** @type number */ rx; /** @type number */ ry; /** @type HTMLElement */ #element; /** @type HTMLElement */ #char; /** * @param {number} id * @param {number} x * @param {number} y */ constructor(id, x, y) { this.id = id; this.x = x; this.y = y; this.rx = x; this.ry = y; const element = document.createElement("i"); element.classList.add("cursor"); element.id = "cursor" + id; const colour = randomColour(); element.style.borderColor = colour; element.style.color = colour; element.innerText = "0x" + navigator.userAgent.hashCode(); const char = document.createElement("p"); char.className = "char"; element.appendChild(char); this.#element = element; this.#char = char; cursorContainer.appendChild(this.#element); } destroy() { this.#element.remove(); } /** * @param {number} x * @param {number} y */ move(x, y) { this.x = x; this.y = y; } /** * @param {number} deltaTime */ update(deltaTime) { this.rx += (this.x - this.rx) * CURSOR_LERP_RATE * deltaTime; this.ry += (this.y - this.ry) * CURSOR_LERP_RATE * deltaTime; this.#element.style.left = this.rx + "px"; this.#element.style.top = this.ry + "px"; } /** * @param {string} text */ print(text) { if (text.length > 1) return; this.#char.innerText = text; } } class FunChar { /** @type number */ x; y; /** @type number */ xa; ya; /** @type number */ r; ra; /** @type HTMLElement */ element; /** @type number */ life; /** * @param {number} x * @param {number} y * @param {number} xa * @param {number} ya * @param {string} text */ constructor(x, y, xa, ya, text) { this.x = x; this.y = y; this.xa = xa + Math.random() * .2 - .1; this.ya = ya + Math.random() * -.25; this.r = this.xa * 100; this.ra = this.r * 0.01; this.life = 0; const char = document.createElement("i"); char.className = "funchar"; char.innerText = text; char.style.left = x + "px"; char.style.top = y + "px"; char.style.transform = `rotate(${this.r}deg)`; this.element = char; cursorContainer.appendChild(this.element); } /** * @param {number} deltaTime */ update(deltaTime) { this.life += deltaTime; if (this.life > CURSOR_CHAR_MAX_LIFE || this.y > window.outerHeight || this.x < 0 || this.x > window.outerWidth ) { this.destroy(); return; } this.x += this.xa * deltaTime; this.y += this.ya * deltaTime; this.r += this.ra * deltaTime; this.ya = Math.min(this.ya + 0.0005 * deltaTime, 10); this.element.style.left = this.x + "px"; this.element.style.top = this.y + "px"; this.element.style.transform = `rotate(${this.r}deg)`; } destroy() { chars = chars.filter(char => char !== this); this.element.remove(); } } String.prototype.hashCode = function() { var hash = 0; if (this.length === 0) return hash; for (let i = 0; i < this.length; i++) { const chr = this.charCodeAt(i); hash = ((hash << 5) - hash) + chr; hash |= 0; // convert to 32-bit integer } return Math.round(Math.abs(hash)).toString(16).slice(0, 8).padStart(8, '0'); }; /** * @returns string */ function randomColour() { const min = 128; const range = 100; const red = Math.round((min + Math.random() * range)).toString(16); const green = Math.round((min + Math.random() * range)).toString(16); const blue = Math.round((min + Math.random() * range)).toString(16); return "#" + red + green + blue; } /** * @param {MouseEvent} event */ function handleMouseMove(event) { if (!myCursor) return; myCursor.move(event.x, event.y); } /** * @param {KeyboardEvent} event */ function handleKeyDown(event) { if (event.key.length > 1) return; if (event.metaKey || event.ctrlKey) return; if (window.cursorFunMode === true) { const yOffset = -20; const accelMultiplier = 0.002; if (chars.length < CURSOR_MAX_CHARS) chars.push(new FunChar( myCursor.x, myCursor.y + yOffset, (myCursor.x - myCursor.rx) * accelMultiplier, (myCursor.y - myCursor.ry) * accelMultiplier, event.key)); } else { myCursor.print(event.key); } } function handleKeyUp() { if (!window.cursorFunMode) { myCursor.print(""); } } /** * @param {number} time */ function updateCursors(time) { const deltaTime = time - lastCursorUpdateTime; cursors.forEach(cursor => { cursor.update(deltaTime); }); lastCursorUpdateTime = time; requestAnimationFrame(updateCursors); } /** * @param {number} time */ function updateChars(time) { const deltaTime = time - lastCharUpdateTime; chars.forEach(char => { char.update(deltaTime); }); lastCharUpdateTime = time; requestAnimationFrame(updateChars); } function cursorSetup() { window.cursorFunMode = false; cursorContainer = document.createElement("div"); cursorContainer.id = "cursors"; document.body.appendChild(cursorContainer); myCursor = new Cursor(0, window.innerWidth / 2, window.innerHeight / 2); cursors.set(0, myCursor); document.addEventListener("mousemove", handleMouseMove); document.addEventListener("keydown", handleKeyDown); document.addEventListener("keyup", handleKeyUp); requestAnimationFrame(updateCursors); requestAnimationFrame(updateChars); console.debug(`Cursor tracking @ ${window.location.pathname}`); } function cursorDestroy() { document.removeEventListener("mousemove", handleMouseMove); document.removeEventListener("keydown", handleKeyDown); document.removeEventListener("keyup", handleKeyUp); cursors.forEach(cursor => { cursor.destroy(); }); cursors.clear(); chars.forEach(cursor => { cursor.destroy(); }); chars = new Array(); myCursor = null; console.debug(`Cursor no longer tracking.`); } cursorSetup();