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) {
 | 
			
		||||
    // 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 queued = null;
 | 
			
		||||
 | 
			
		||||
    const callFn = (args) => {
 | 
			
		||||
        fn(...args);
 | 
			
		||||
 | 
			
		||||
        timeout = setTimeout(() => {
 | 
			
		||||
            timeout = null;
 | 
			
		||||
            if (queued != null) {
 | 
			
		||||
                callFn(queued);
 | 
			
		||||
                queued = null;
 | 
			
		||||
            }
 | 
			
		||||
        }, time);
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    return (...args) => {
 | 
			
		||||
        if (timeout == null) {
 | 
			
		||||
            fn(...args);
 | 
			
		||||
            timeout = setTimeout(() => {
 | 
			
		||||
                timeout = null;
 | 
			
		||||
                if (queued != null) {
 | 
			
		||||
                    fn(...queued);
 | 
			
		||||
                    queued = null;
 | 
			
		||||
                }
 | 
			
		||||
            }, time);
 | 
			
		||||
            callFn(args);
 | 
			
		||||
        } else {
 | 
			
		||||
            queued = args;
 | 
			
		||||
        }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -26,6 +26,7 @@ let connectionStatus = main.querySelector("rkgk-connection-status");
 | 
			
		|||
 | 
			
		||||
document.getElementById("js-loading").remove();
 | 
			
		||||
reticleRenderer.connectViewport(canvasRenderer.viewport);
 | 
			
		||||
reticleRenderer.updateInterval = updateInterval;
 | 
			
		||||
 | 
			
		||||
function updateUrl(session, viewport) {
 | 
			
		||||
    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) => {
 | 
			
		||||
        reportCursor(event.x, event.y);
 | 
			
		||||
    });
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -7,6 +7,12 @@ export class Reticle extends HTMLElement {
 | 
			
		|||
export class ReticleCursor extends Reticle {
 | 
			
		||||
    #container;
 | 
			
		||||
 | 
			
		||||
    #lastX = 0;
 | 
			
		||||
    #lastY = 0;
 | 
			
		||||
    #targetX = 0;
 | 
			
		||||
    #targetY = 0;
 | 
			
		||||
    #lastUpdate = 0;
 | 
			
		||||
 | 
			
		||||
    constructor(nickname) {
 | 
			
		||||
        super();
 | 
			
		||||
        this.nickname = nickname;
 | 
			
		||||
| 
						 | 
				
			
			@ -38,23 +44,39 @@ export class ReticleCursor extends Reticle {
 | 
			
		|||
    }
 | 
			
		||||
 | 
			
		||||
    setCursor(x, y) {
 | 
			
		||||
        this.x = x;
 | 
			
		||||
        this.y = 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, windowSize) {
 | 
			
		||||
        let [viewportX, viewportY] = viewport.toScreenSpace(this.x, this.y, windowSize);
 | 
			
		||||
    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");
 | 
			
		||||
| 
						 | 
				
			
			@ -72,13 +94,13 @@ export class ReticleRenderer extends HTMLElement {
 | 
			
		|||
    addReticle(reticle) {
 | 
			
		||||
        if (!this.#reticles.has(reticle)) {
 | 
			
		||||
            reticle.addEventListener(".update", () => {
 | 
			
		||||
                if (this.viewport != null) {
 | 
			
		||||
                    reticle.render(this.viewport, {
 | 
			
		||||
                        width: this.clientWidth,
 | 
			
		||||
                        height: this.clientHeight,
 | 
			
		||||
                    });
 | 
			
		||||
                let needsKickstart = this.#animatingReticles.size == 0;
 | 
			
		||||
                this.#animatingReticles.add(reticle);
 | 
			
		||||
                if (needsKickstart) {
 | 
			
		||||
                    this.#tickAnimatingReticles();
 | 
			
		||||
                }
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            this.#reticles.add(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() {
 | 
			
		||||
        if (this.viewport == null) {
 | 
			
		||||
            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 };
 | 
			
		||||
        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);
 | 
			
		||||
 | 
			
		||||
function lerp(a, b, t) {
 | 
			
		||||
    return a + (b - a) * t;
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue