add interpolation to cursor reticles
cursor reticles are now interpolated to the update interval, so they should be smooth at > 60 fps
This commit is contained in:
parent
bebc2daa95
commit
a40480a464
3 changed files with 81 additions and 19 deletions
|
@ -20,18 +20,34 @@ export function listen(...listenerSpecs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function debounce(time, fn) {
|
export function debounce(time, fn) {
|
||||||
|
// This function is kind of tricky, but basically:
|
||||||
|
//
|
||||||
|
// - we want to guarantee `fn` is called at most once every `time` milliseconds
|
||||||
|
// - at the same time, in case debounced `fn` is called during an ongoing timeout, we want to
|
||||||
|
// queue up another run, and run it immediately after `time` passes
|
||||||
|
// - at the same time, in case this catch-up condition occurs, we must also ensure there's a
|
||||||
|
// delay after `fn` is called
|
||||||
|
//
|
||||||
|
// yielding the recursive solution below.
|
||||||
|
|
||||||
let timeout = null;
|
let timeout = null;
|
||||||
let queued = null;
|
let queued = null;
|
||||||
|
|
||||||
|
const callFn = (args) => {
|
||||||
|
fn(...args);
|
||||||
|
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
timeout = null;
|
||||||
|
if (queued != null) {
|
||||||
|
callFn(queued);
|
||||||
|
queued = null;
|
||||||
|
}
|
||||||
|
}, time);
|
||||||
|
};
|
||||||
|
|
||||||
return (...args) => {
|
return (...args) => {
|
||||||
if (timeout == null) {
|
if (timeout == null) {
|
||||||
fn(...args);
|
callFn(args);
|
||||||
timeout = setTimeout(() => {
|
|
||||||
timeout = null;
|
|
||||||
if (queued != null) {
|
|
||||||
fn(...queued);
|
|
||||||
queued = null;
|
|
||||||
}
|
|
||||||
}, time);
|
|
||||||
} else {
|
} else {
|
||||||
queued = args;
|
queued = args;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ let connectionStatus = main.querySelector("rkgk-connection-status");
|
||||||
|
|
||||||
document.getElementById("js-loading").remove();
|
document.getElementById("js-loading").remove();
|
||||||
reticleRenderer.connectViewport(canvasRenderer.viewport);
|
reticleRenderer.connectViewport(canvasRenderer.viewport);
|
||||||
|
reticleRenderer.updateInterval = updateInterval;
|
||||||
|
|
||||||
function updateUrl(session, viewport) {
|
function updateUrl(session, viewport) {
|
||||||
let url = new URL(window.location);
|
let url = new URL(window.location);
|
||||||
|
@ -231,7 +232,7 @@ function readUrl(urlString) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y));
|
let reportCursor = debounce(updateInterval, (x, y) => session.sendCursor(x, y), console.log);
|
||||||
canvasRenderer.addEventListener(".cursor", async (event) => {
|
canvasRenderer.addEventListener(".cursor", async (event) => {
|
||||||
reportCursor(event.x, event.y);
|
reportCursor(event.x, event.y);
|
||||||
});
|
});
|
||||||
|
|
|
@ -7,6 +7,12 @@ export class Reticle extends HTMLElement {
|
||||||
export class ReticleCursor extends Reticle {
|
export class ReticleCursor extends Reticle {
|
||||||
#container;
|
#container;
|
||||||
|
|
||||||
|
#lastX = 0;
|
||||||
|
#lastY = 0;
|
||||||
|
#targetX = 0;
|
||||||
|
#targetY = 0;
|
||||||
|
#lastUpdate = 0;
|
||||||
|
|
||||||
constructor(nickname) {
|
constructor(nickname) {
|
||||||
super();
|
super();
|
||||||
this.nickname = nickname;
|
this.nickname = nickname;
|
||||||
|
@ -38,23 +44,39 @@ export class ReticleCursor extends Reticle {
|
||||||
}
|
}
|
||||||
|
|
||||||
setCursor(x, y) {
|
setCursor(x, y) {
|
||||||
this.x = x;
|
this.#lastX = this.#targetX;
|
||||||
this.y = y;
|
this.#lastY = this.#targetY;
|
||||||
|
this.#targetX = x;
|
||||||
|
this.#targetY = y;
|
||||||
|
this.#lastUpdate = performance.now();
|
||||||
this.dispatchEvent(new Event(".update"));
|
this.dispatchEvent(new Event(".update"));
|
||||||
}
|
}
|
||||||
|
|
||||||
render(viewport, windowSize) {
|
render(viewport, updateInterval, windowSize) {
|
||||||
let [viewportX, viewportY] = viewport.toScreenSpace(this.x, this.y, 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)`;
|
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);
|
customElements.define("rkgk-reticle-cursor", ReticleCursor);
|
||||||
|
|
||||||
export class ReticleRenderer extends HTMLElement {
|
export class ReticleRenderer extends HTMLElement {
|
||||||
|
updateInterval = 1000 / 10; // a really wrong value, you should definitely override this
|
||||||
|
|
||||||
#reticles = new Set();
|
#reticles = new Set();
|
||||||
#reticlesDiv;
|
#reticlesDiv;
|
||||||
|
|
||||||
|
#animatingReticles = new Set();
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this.#reticlesDiv = this.appendChild(document.createElement("div"));
|
this.#reticlesDiv = this.appendChild(document.createElement("div"));
|
||||||
this.#reticlesDiv.classList.add("reticles");
|
this.#reticlesDiv.classList.add("reticles");
|
||||||
|
@ -72,13 +94,13 @@ export class ReticleRenderer extends HTMLElement {
|
||||||
addReticle(reticle) {
|
addReticle(reticle) {
|
||||||
if (!this.#reticles.has(reticle)) {
|
if (!this.#reticles.has(reticle)) {
|
||||||
reticle.addEventListener(".update", () => {
|
reticle.addEventListener(".update", () => {
|
||||||
if (this.viewport != null) {
|
let needsKickstart = this.#animatingReticles.size == 0;
|
||||||
reticle.render(this.viewport, {
|
this.#animatingReticles.add(reticle);
|
||||||
width: this.clientWidth,
|
if (needsKickstart) {
|
||||||
height: this.clientHeight,
|
this.#tickAnimatingReticles();
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.#reticles.add(reticle);
|
this.#reticles.add(reticle);
|
||||||
this.#reticlesDiv.appendChild(reticle);
|
this.#reticlesDiv.appendChild(reticle);
|
||||||
}
|
}
|
||||||
|
@ -91,6 +113,25 @@ export class ReticleRenderer extends HTMLElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#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() {
|
render() {
|
||||||
if (this.viewport == null) {
|
if (this.viewport == null) {
|
||||||
console.debug("viewport is disconnected, skipping transform update");
|
console.debug("viewport is disconnected, skipping transform update");
|
||||||
|
@ -99,9 +140,13 @@ export class ReticleRenderer extends HTMLElement {
|
||||||
|
|
||||||
let windowSize = { width: this.clientWidth, height: this.clientHeight };
|
let windowSize = { width: this.clientWidth, height: this.clientHeight };
|
||||||
for (let reticle of this.#reticles.values()) {
|
for (let reticle of this.#reticles.values()) {
|
||||||
reticle.render(this.viewport, windowSize);
|
reticle.render(this.viewport, this.updateInterval, windowSize);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("rkgk-reticle-renderer", ReticleRenderer);
|
customElements.define("rkgk-reticle-renderer", ReticleRenderer);
|
||||||
|
|
||||||
|
function lerp(a, b, t) {
|
||||||
|
return a + (b - a) * t;
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue