mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
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:
parent
dfc998bb00
commit
1f461cb322
@ -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)
|
||||||
|
@ -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
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -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)
|
||||||
|
Loading…
Reference in New Issue
Block a user