Optimizing canvas for cursor rendering (#27)

* use dpr as canvas scale.

* wheel use event timestamp.

* remove vue set.

* add cursor rendering fps.
This commit is contained in:
Miroslav Šedivý 2023-04-13 12:25:36 +02:00 committed by GitHub
parent dfc998bb00
commit 1f461cb322
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 63 additions and 25 deletions

View File

@ -20,7 +20,6 @@
import { InactiveCursorDrawFunction, Dimension } from './types/cursors' import { InactiveCursorDrawFunction, Dimension } from './types/cursors'
import { getMovementXYatPercent } from './utils/canvas-movement' import { getMovementXYatPercent } from './utils/canvas-movement'
const CANVAS_SCALE = 2
// How often are position data arriving // How often are position data arriving
const POS_INTERVAL_MS = 750 const POS_INTERVAL_MS = 750
// How many pixel change is considered as movement // How many pixel change is considered as movement
@ -33,6 +32,8 @@
@Ref('overlay') readonly _overlay!: HTMLCanvasElement @Ref('overlay') readonly _overlay!: HTMLCanvasElement
private _ctx!: CanvasRenderingContext2D private _ctx!: CanvasRenderingContext2D
private canvasScale = window.devicePixelRatio
@Prop() @Prop()
private readonly sessions!: Record<string, Session> private readonly sessions!: Record<string, Session>
@ -54,6 +55,9 @@
@Prop() @Prop()
private readonly cursorDraw!: InactiveCursorDrawFunction | null private readonly cursorDraw!: InactiveCursorDrawFunction | null
@Prop()
private readonly fps!: number
mounted() { mounted() {
// get canvas overlay context // get canvas overlay context
const ctx = this._overlay.getContext('2d') const ctx = this._overlay.getContext('2d')
@ -78,9 +82,9 @@
} }
canvasResize({ width, height }: Dimension) { canvasResize({ width, height }: Dimension) {
this._overlay.width = width * CANVAS_SCALE this._overlay.width = width * this.canvasScale
this._overlay.height = height * CANVAS_SCALE this._overlay.height = height * this.canvasScale
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0)
} }
// start as undefined to prevent jumping // start as undefined to prevent jumping
@ -96,8 +100,14 @@
// request another frame // request another frame
if (this._percent <= 1) window.requestAnimationFrame(this.canvasAnimateFrame) if (this._percent <= 1) window.requestAnimationFrame(this.canvasAnimateFrame)
// calculate factor // calc elapsed time since last loop
const delta = (now - this._last_animation_time) / POS_INTERVAL_MS 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 this._last_animation_time = now
// skip very first delta to prevent jumping // skip very first delta to prevent jumping
@ -208,7 +218,7 @@
y = Math.round((y / this.screenSize.height) * height) y = Math.round((y / this.screenSize.height) * height)
// reset transformation, X and Y will be 0 again // 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 // use custom draw function, if available
if (this.cursorDraw) { if (this.cursorDraw) {
@ -234,7 +244,7 @@
canvasClear() { canvasClear() {
// reset transformation, X and Y will be 0 again // 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 const { width, height } = this._overlay
this._ctx.clearRect(0, 0, width, height) this._ctx.clearRect(0, 0, width, height)

View File

@ -18,6 +18,7 @@
:canvasSize="canvasSize" :canvasSize="canvasSize"
:cursors="state.cursors" :cursors="state.cursors"
:cursorDraw="inactiveCursorDrawFunction" :cursorDraw="inactiveCursorDrawFunction"
:fps="fps"
/> />
<neko-overlay <neko-overlay
ref="overlay" ref="overlay"
@ -34,6 +35,7 @@
:cursorDraw="cursorDrawFunction" :cursorDraw="cursorDrawFunction"
:implicitControl="state.settings.implicit_hosting && session.profile.can_host" :implicitControl="state.settings.implicit_hosting && session.profile.can_host"
:inactiveCursors="state.settings.inactive_cursors && session.profile.sends_inactive_cursor" :inactiveCursors="state.settings.inactive_cursors && session.profile.sends_inactive_cursor"
:fps="fps"
@updateKeyboardModifiers="updateKeyboardModifiers($event)" @updateKeyboardModifiers="updateKeyboardModifiers($event)"
@uploadDrop="uploadDrop($event)" @uploadDrop="uploadDrop($event)"
@mobileKeyboardOpen="state.mobile_keyboard_open = $event" @mobileKeyboardOpen="state.mobile_keyboard_open = $event"
@ -132,6 +134,10 @@
@Prop({ type: Boolean }) @Prop({ type: Boolean })
private readonly autoplay!: boolean private readonly autoplay!: boolean
// fps for cursor rendering, 0 for no cap
@Prop({ type: Number, default: 0 })
private readonly fps!: number
///////////////////////////// /////////////////////////////
// Public state // Public state
///////////////////////////// /////////////////////////////

View File

@ -58,8 +58,6 @@
const WHEEL_STEP = 53 // Delta threshold for a mouse wheel step const WHEEL_STEP = 53 // Delta threshold for a mouse wheel step
const WHEEL_LINE_HEIGHT = 19 const WHEEL_LINE_HEIGHT = 19
const CANVAS_SCALE = 2
const MOUSE_MOVE_THROTTLE = 1000 / 60 // in ms, 60fps const MOUSE_MOVE_THROTTLE = 1000 / 60 // in ms, 60fps
const INACTIVE_CURSOR_INTERVAL = 1000 / 4 // in ms, 4fps const INACTIVE_CURSOR_INTERVAL = 1000 / 4 // in ms, 4fps
@ -71,6 +69,8 @@
@Ref('textarea') readonly _textarea!: HTMLTextAreaElement @Ref('textarea') readonly _textarea!: HTMLTextAreaElement
private _ctx!: CanvasRenderingContext2D private _ctx!: CanvasRenderingContext2D
private canvasScale = window.devicePixelRatio
private keyboard!: KeyboardInterface private keyboard!: KeyboardInterface
private textInput = '' private textInput = ''
@ -109,6 +109,9 @@
@Prop() @Prop()
private readonly inactiveCursors!: boolean private readonly inactiveCursors!: boolean
@Prop()
private readonly fps!: number
get cursor(): string { get cursor(): string {
if (!this.isControling || !this.cursorImage) { if (!this.isControling || !this.cursorImage) {
return 'default' return 'default'
@ -221,12 +224,12 @@
sendMousePos(e: MouseEvent) { sendMousePos(e: MouseEvent) {
const pos = this.getMousePos(e.clientX, e.clientY) const pos = this.getMousePos(e.clientX, e.clientY)
this.webrtc.send('mousemove', pos) this.webrtc.send('mousemove', pos)
Vue.set(this, 'cursorPosition', pos) this.cursorPosition = pos
} }
private wheelX = 0 private wheelX = 0
private wheelY = 0 private wheelY = 0
private wheelDate = Date.now() private wheelTimeStamp = 0
// negative sensitivity can be acheived using increased step value // negative sensitivity can be acheived using increased step value
get wheelStep() { get wheelStep() {
@ -268,13 +271,13 @@
return return
} }
const now = Date.now() // when the last scroll was more than 250ms ago
const firstScroll = now - this.wheelDate > 250 const firstScroll = e.timeStamp - this.wheelTimeStamp > 250
if (firstScroll) { if (firstScroll) {
this.wheelX = 0 this.wheelX = 0
this.wheelY = 0 this.wheelY = 0
this.wheelDate = now this.wheelTimeStamp = e.timeStamp
} }
let dx = e.deltaX let dx = e.deltaX
@ -397,10 +400,10 @@
onMouseLeave(e: MouseEvent) { onMouseLeave(e: MouseEvent) {
if (this.isControling) { if (this.isControling) {
// save current keyboard modifiers state // save current keyboard modifiers state
Vue.set(this, 'keyboardModifiers', { this.keyboardModifiers = {
capslock: e.getModifierState('CapsLock'), capslock: e.getModifierState('CapsLock'),
numlock: e.getModifierState('NumLock'), numlock: e.getModifierState('NumLock'),
}) }
} }
this.focused = false this.focused = false
@ -493,7 +496,9 @@
private cursorImage: CursorImage | null = null private cursorImage: CursorImage | null = null
private cursorElement: HTMLImageElement = new Image() private cursorElement: HTMLImageElement = new Image()
private cursorPosition: CursorPosition | null = null private cursorPosition: CursorPosition | null = null
private cursorLastTime = 0
private canvasRequestedFrame = false private canvasRequestedFrame = false
private canvasRenderTimeout: number | null = null
@Watch('canvasSize') @Watch('canvasSize')
onCanvasSizeChange({ width, height }: Dimension) { onCanvasSizeChange({ width, height }: Dimension) {
@ -503,13 +508,13 @@
onCursorPosition(data: CursorPosition) { onCursorPosition(data: CursorPosition) {
if (!this.isControling) { if (!this.isControling) {
Vue.set(this, 'cursorPosition', data) this.cursorPosition = data
this.canvasRequestRedraw() this.canvasRequestRedraw()
} }
} }
onCursorImage(data: CursorImage) { onCursorImage(data: CursorImage) {
Vue.set(this, 'cursorImage', data) this.cursorImage = data
if (!this.isControling) { if (!this.isControling) {
this.cursorElement.src = data.uri this.cursorElement.src = data.uri
@ -517,9 +522,9 @@
} }
canvasResize({ width, height }: Dimension) { canvasResize({ width, height }: Dimension) {
this._overlay.width = width * CANVAS_SCALE this._overlay.width = width * this.canvasScale
this._overlay.height = height * CANVAS_SCALE this._overlay.height = height * this.canvasScale
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0)
} }
@Watch('hostId') @Watch('hostId')
@ -528,6 +533,23 @@
// skip rendering if there is already in progress // skip rendering if there is already in progress
if (this.canvasRequestedFrame) return 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 // request animation frame from a browser
this.canvasRequestedFrame = true this.canvasRequestedFrame = true
window.requestAnimationFrame(() => { window.requestAnimationFrame(() => {
@ -554,7 +576,7 @@
let { width, height } = this.canvasSize let { width, height } = this.canvasSize
// reset transformation, X and Y will be 0 again // 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 // get cursor position
let x = Math.round((this.cursorPosition.x / this.screenSize.width) * width) let x = Math.round((this.cursorPosition.x / this.screenSize.width) * width)
@ -596,7 +618,7 @@
canvasClear() { canvasClear() {
// reset transformation, X and Y will be 0 again // 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 const { width, height } = this._overlay
this._ctx.clearRect(0, 0, width, height) this._ctx.clearRect(0, 0, width, height)
@ -611,7 +633,7 @@
@Watch('isControling') @Watch('isControling')
onControlChange(isControling: boolean) { onControlChange(isControling: boolean) {
Vue.set(this, 'keyboardModifiers', null) this.keyboardModifiers = null
if (isControling && this.reqMouseDown) { if (isControling && this.reqMouseDown) {
this.updateKeyboardModifiers(this.reqMouseDown) this.updateKeyboardModifiers(this.reqMouseDown)