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;
}