diff --git a/public/script/cursor.js b/public/script/cursor.js new file mode 100644 index 0000000..94f1a8e --- /dev/null +++ b/public/script/cursor.js @@ -0,0 +1,273 @@ +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(); diff --git a/public/script/main.js b/public/script/main.js index 95023e5..c1d5101 100644 --- a/public/script/main.js +++ b/public/script/main.js @@ -1,5 +1,6 @@ import "./header.js"; import "./config.js"; +import "./cursor.js"; function type_out(e) { const text = e.innerText; diff --git a/public/style/cursor.css b/public/style/cursor.css new file mode 100644 index 0000000..477e525 --- /dev/null +++ b/public/style/cursor.css @@ -0,0 +1,43 @@ +#cursors i.cursor { + position: fixed; + padding: 4px; + + display: block; + z-index: 1000; + + background: #0008; + border: 2px solid #808080; + border-radius: 2px; + + font-style: normal; + font-size: 10px; + font-weight: bold; + white-space: nowrap; + + user-select: none; + pointer-events: none; +} + +#cursors i.cursor .char { + position: absolute; + transform: translateY(-44px); + margin: 0; + font-size: 20px; + color: var(--on-background); +} + +#cursors .funchar { + position: fixed; + margin: 0; + + display: block; + z-index: 1000; + + font-style: normal; + font-size: 20px; + font-weight: bold; + color: var(--on-background); + + user-select: none; + pointer-events: none; +} diff --git a/public/style/main.css b/public/style/main.css index f7f2131..4e9e113 100644 --- a/public/style/main.css +++ b/public/style/main.css @@ -2,6 +2,7 @@ @import url("/style/header.css"); @import url("/style/footer.css"); @import url("/style/prideflag.css"); +@import url("/style/cursor.css"); @font-face { font-family: "Monaspace Argon";