From 1f461cb3229ebaa85d68449b3cdde5504797442e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=A0ediv=C3=BD?= Date: Thu, 13 Apr 2023 12:25:36 +0200 Subject: [PATCH] Optimizing canvas for cursor rendering (#27) * use dpr as canvas scale. * wheel use event timestamp. * remove vue set. * add cursor rendering fps. --- src/component/cursors.vue | 26 ++++++++++++------ src/component/main.vue | 6 +++++ src/component/overlay.vue | 56 +++++++++++++++++++++++++++------------ 3 files changed, 63 insertions(+), 25 deletions(-) diff --git a/src/component/cursors.vue b/src/component/cursors.vue index 17591ee8..dfce399c 100644 --- a/src/component/cursors.vue +++ b/src/component/cursors.vue @@ -20,7 +20,6 @@ import { InactiveCursorDrawFunction, Dimension } from './types/cursors' import { getMovementXYatPercent } from './utils/canvas-movement' - const CANVAS_SCALE = 2 // How often are position data arriving const POS_INTERVAL_MS = 750 // How many pixel change is considered as movement @@ -33,6 +32,8 @@ @Ref('overlay') readonly _overlay!: HTMLCanvasElement private _ctx!: CanvasRenderingContext2D + private canvasScale = window.devicePixelRatio + @Prop() private readonly sessions!: Record @@ -54,6 +55,9 @@ @Prop() private readonly cursorDraw!: InactiveCursorDrawFunction | null + @Prop() + private readonly fps!: number + mounted() { // get canvas overlay context const ctx = this._overlay.getContext('2d') @@ -78,9 +82,9 @@ } canvasResize({ width, height }: Dimension) { - this._overlay.width = width * CANVAS_SCALE - this._overlay.height = height * CANVAS_SCALE - this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) + this._overlay.width = width * this.canvasScale + this._overlay.height = height * this.canvasScale + this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0) } // start as undefined to prevent jumping @@ -96,8 +100,14 @@ // request another frame if (this._percent <= 1) window.requestAnimationFrame(this.canvasAnimateFrame) - // calculate factor - const delta = (now - this._last_animation_time) / POS_INTERVAL_MS + // calc elapsed time since last loop + const elapsed = now - this._last_animation_time + + // skip if fps is set and elapsed time is less than fps + if (this.fps > 0 && elapsed < 1000 / this.fps) return + + // calc current animation progress + const delta = elapsed / POS_INTERVAL_MS this._last_animation_time = now // skip very first delta to prevent jumping @@ -208,7 +218,7 @@ y = Math.round((y / this.screenSize.height) * height) // reset transformation, X and Y will be 0 again - this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) + this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0) // use custom draw function, if available if (this.cursorDraw) { @@ -234,7 +244,7 @@ canvasClear() { // reset transformation, X and Y will be 0 again - this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) + this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0) const { width, height } = this._overlay this._ctx.clearRect(0, 0, width, height) diff --git a/src/component/main.vue b/src/component/main.vue index 2d0c43df..49471643 100644 --- a/src/component/main.vue +++ b/src/component/main.vue @@ -18,6 +18,7 @@ :canvasSize="canvasSize" :cursors="state.cursors" :cursorDraw="inactiveCursorDrawFunction" + :fps="fps" /> 250 + // when the last scroll was more than 250ms ago + const firstScroll = e.timeStamp - this.wheelTimeStamp > 250 if (firstScroll) { this.wheelX = 0 this.wheelY = 0 - this.wheelDate = now + this.wheelTimeStamp = e.timeStamp } let dx = e.deltaX @@ -397,10 +400,10 @@ onMouseLeave(e: MouseEvent) { if (this.isControling) { // save current keyboard modifiers state - Vue.set(this, 'keyboardModifiers', { + this.keyboardModifiers = { capslock: e.getModifierState('CapsLock'), numlock: e.getModifierState('NumLock'), - }) + } } this.focused = false @@ -493,7 +496,9 @@ private cursorImage: CursorImage | null = null private cursorElement: HTMLImageElement = new Image() private cursorPosition: CursorPosition | null = null + private cursorLastTime = 0 private canvasRequestedFrame = false + private canvasRenderTimeout: number | null = null @Watch('canvasSize') onCanvasSizeChange({ width, height }: Dimension) { @@ -503,13 +508,13 @@ onCursorPosition(data: CursorPosition) { if (!this.isControling) { - Vue.set(this, 'cursorPosition', data) + this.cursorPosition = data this.canvasRequestRedraw() } } onCursorImage(data: CursorImage) { - Vue.set(this, 'cursorImage', data) + this.cursorImage = data if (!this.isControling) { this.cursorElement.src = data.uri @@ -517,9 +522,9 @@ } canvasResize({ width, height }: Dimension) { - this._overlay.width = width * CANVAS_SCALE - this._overlay.height = height * CANVAS_SCALE - this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) + this._overlay.width = width * this.canvasScale + this._overlay.height = height * this.canvasScale + this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0) } @Watch('hostId') @@ -528,6 +533,23 @@ // skip rendering if there is already in progress if (this.canvasRequestedFrame) return + // throttle rendering according to fps + if (this.fps > 0) { + if (this.canvasRenderTimeout) { + window.clearTimeout(this.canvasRenderTimeout) + this.canvasRenderTimeout = null + } + + const now = Date.now() + if (now - this.cursorLastTime < 1000 / this.fps) { + // ensure that last frame is rendered + this.canvasRenderTimeout = window.setTimeout(this.canvasRequestRedraw, 1000 / this.fps) + return + } + + this.cursorLastTime = now + } + // request animation frame from a browser this.canvasRequestedFrame = true window.requestAnimationFrame(() => { @@ -554,7 +576,7 @@ let { width, height } = this.canvasSize // reset transformation, X and Y will be 0 again - this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) + this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0) // get cursor position let x = Math.round((this.cursorPosition.x / this.screenSize.width) * width) @@ -596,7 +618,7 @@ canvasClear() { // reset transformation, X and Y will be 0 again - this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) + this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0) const { width, height } = this._overlay this._ctx.clearRect(0, 0, width, height) @@ -611,7 +633,7 @@ @Watch('isControling') onControlChange(isControling: boolean) { - Vue.set(this, 'keyboardModifiers', null) + this.keyboardModifiers = null if (isControling && this.reqMouseDown) { this.updateKeyboardModifiers(this.reqMouseDown)