import config from './config.js'; 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 running = false; 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(); this.#char.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; } /** * @param {boolean} active */ click(active) { if (active) this.#element.classList.add('click'); else this.#element.classList.remove('click'); } } 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 > document.body.clientHeight || this.x < 0 || this.x > document.body.clientWidth ) { 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 - window.scrollX) + 'px'; this.element.style.top = (this.y - window.scrollY) + '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); } function handleMouseDown() { myCursor.click(true); } function handleMouseUp() { myCursor.click(false); } /** * @param {KeyboardEvent} event */ function handleKeyDown(event) { if (event.key.length > 1) return; if (event.metaKey || event.ctrlKey) return; if (config.cursorFunMode === true) { const yOffset = -20; const accelMultiplier = 0.002; if (chars.length < CURSOR_MAX_CHARS) chars.push(new FunChar( myCursor.x + window.scrollX, myCursor.y + window.scrollY + yOffset, (myCursor.x - myCursor.rx) * accelMultiplier, (myCursor.y - myCursor.ry) * accelMultiplier, event.key)); } else { myCursor.print(event.key); } } function handleKeyUp() { if (!config.cursorFunMode) { myCursor.print(''); } } /** * @param {number} time */ function updateCursors(time) { if (!running) return; const deltaTime = time - lastCursorUpdateTime; cursors.forEach(cursor => { cursor.update(deltaTime); }); lastCursorUpdateTime = time; requestAnimationFrame(updateCursors); } /** * @param {number} time */ function updateChars(time) { if (!running) return; const deltaTime = time - lastCharUpdateTime; chars.forEach(char => { char.update(deltaTime); }); lastCharUpdateTime = time; requestAnimationFrame(updateChars); } function cursorSetup() { if (running) throw new Error('Only one instance of Cursor can run at a time.'); running = true; 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('mousedown', handleMouseDown); document.addEventListener('mouseup', handleMouseUp); document.addEventListener('keydown', handleKeyDown); document.addEventListener('keyup', handleKeyUp); requestAnimationFrame(updateCursors); requestAnimationFrame(updateChars); console.debug(`Cursor tracking @ ${window.location.pathname}`); } function cursorDestroy() { if (!running) return; document.removeEventListener('mousemove', handleMouseMove); document.removeEventListener('mousedown', handleMouseDown); document.removeEventListener('mouseup', handleMouseUp); document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keyup', handleKeyUp); chars.forEach(char => { char.destroy(); }); chars = new Array(); cursors.forEach(cursor => { cursor.destroy(); }); cursors.clear(); myCursor = null; cursorContainer.remove(); console.debug(`Cursor no longer tracking.`); running = false; } if (config.cursor === true) { cursorSetup(); } config.addListener('cursor', enabled => { if (enabled === true) cursorSetup(); else cursorDestroy(); });