export class Reticle extends HTMLElement { render(_viewport, _windowSize) { throw new Error("Reticle.render must be overridden"); } } export class ReticleCursor extends Reticle { #container; #lastX = 0; #lastY = 0; #targetX = 0; #targetY = 0; #lastUpdate = 0; constructor(nickname) { super(); this.nickname = nickname; } connectedCallback() { this.style.setProperty("--color", this.getColor()); this.#container = this.appendChild(document.createElement("div")); this.#container.classList.add("container"); this.classList.add("cursor"); let arrow = this.#container.appendChild(document.createElement("div")); arrow.classList.add("arrow"); let nickname = this.#container.appendChild(document.createElement("div")); nickname.classList.add("nickname"); nickname.textContent = this.nickname; } getColor() { let hash = 8803; for (let i = 0; i < this.nickname.length; ++i) { hash = (hash << 5) - hash + this.nickname.charCodeAt(i); hash |= 0; } return `oklch(65% 0.2 ${(hash / 0x7fffffff) * 360}deg)`; } setCursor(x, y) { this.#lastX = this.#targetX; this.#lastY = this.#targetY; this.#targetX = x; this.#targetY = y; this.#lastUpdate = performance.now(); this.dispatchEvent(new Event(".update")); } render(viewport, updateInterval, windowSize) { let sinceLastUpdate = performance.now() - this.#lastUpdate; let t = Math.max(0, Math.min(1, sinceLastUpdate / updateInterval)); let x = lerp(this.#lastX, this.#targetX, t); let y = lerp(this.#lastY, this.#targetY, t); let [viewportX, viewportY] = viewport.toScreenSpace(x, y, windowSize); this.style.transform = `translate(${viewportX}px, ${viewportY}px)`; let needsRerender = t < 1; // if the linear animation isn't yet done return needsRerender; } } customElements.define("rkgk-reticle-cursor", ReticleCursor); export class ReticleRenderer extends HTMLElement { updateInterval = 1000 / 10; // a really wrong value, you should definitely override this #reticles = new Set(); #reticlesDiv; #animatingReticles = new Set(); connectedCallback() { this.#reticlesDiv = this.appendChild(document.createElement("div")); this.#reticlesDiv.classList.add("reticles"); this.render(); let resizeObserver = new ResizeObserver(() => this.render()); resizeObserver.observe(this); } connectViewport(viewport) { this.viewport = viewport; this.render(); } addReticle(reticle) { if (!this.#reticles.has(reticle)) { reticle.addEventListener(".update", () => { let needsKickstart = this.#animatingReticles.size == 0; this.#animatingReticles.add(reticle); if (needsKickstart) { this.#tickAnimatingReticles(); } }); this.#reticles.add(reticle); this.#reticlesDiv.appendChild(reticle); } } removeReticle(reticle) { if (this.#reticles.has(reticle)) { this.#reticles.delete(reticle); this.#reticlesDiv.removeChild(reticle); } } #tickAnimatingReticles() { if (this.viewport == null) return; let windowSize = { width: this.clientWidth, height: this.clientHeight, }; for (let reticle of this.#animatingReticles) { let needsUpdate = reticle.render(this.viewport, this.updateInterval, windowSize); if (!needsUpdate) { this.#animatingReticles.delete(reticle); } } if (this.#animatingReticles.size > 0) { requestAnimationFrame(() => this.#tickAnimatingReticles()); } } render() { if (this.viewport == null) { console.debug("viewport is disconnected, skipping transform update"); return; } let windowSize = { width: this.clientWidth, height: this.clientHeight }; for (let reticle of this.#reticles.values()) { reticle.render(this.viewport, this.updateInterval, windowSize); } } } customElements.define("rkgk-reticle-renderer", ReticleRenderer); function lerp(a, b, t) { return a + (b - a) * t; }