diff --git a/src/component/internal/webrtc.ts b/src/component/internal/webrtc.ts index c477890f..f125a146 100644 --- a/src/component/internal/webrtc.ts +++ b/src/component/internal/webrtc.ts @@ -1,5 +1,5 @@ import EventEmitter from 'eventemitter3' -import { WebRTCStats } from '../types/webrtc' +import { WebRTCStats, CursorPosition, CursorImage } from '../types/webrtc' import { Logger } from '../utils/logger' export const OPCODE = { @@ -23,8 +23,8 @@ export interface NekoWebRTCEvents { track: (event: RTCTrackEvent) => void candidate: (candidate: RTCIceCandidateInit) => void stats: (stats: WebRTCStats) => void - ['cursor-position']: (data: { x: number; y: number }) => void - ['cursor-image']: (data: { width: number; height: number; x: number; y: number; uri: string }) => void + ['cursor-position']: (data: CursorPosition) => void + ['cursor-image']: (data: CursorImage) => void } export class NekoWebRTC extends EventEmitter { diff --git a/src/component/main.vue b/src/component/main.vue index 5904fff2..824d6e3f 100644 --- a/src/component/main.vue +++ b/src/component/main.vue @@ -14,6 +14,7 @@ ? state.sessions[state.control.host_id].profile.name : '' " + :cursorDraw="cursorDrawFunction" :implicitControl="state.control.implicit_hosting && state.sessions[state.session_id].profile.can_host" @implicitControlRequest="connection.websocket.send('control/request')" @implicitControlRelease="connection.websocket.send('control/release')" @@ -65,6 +66,7 @@ import { ReconnectorConfig } from './types/reconnector' import NekoState from './types/state' + import { Dimension, CursorDrawFunction } from './types/overlay' import Overlay from './overlay.vue' import Screencast from './screencast.vue' @@ -82,10 +84,8 @@ api = new NekoApi() observer = new ResizeObserver(this.onResize.bind(this)) - canvasSize: { width: number; height: number } = { - width: 0, - height: 0, - } + canvasSize: Dimension = { width: 0, height: 0 } + cursorDrawFunction: CursorDrawFunction | null = null @Prop({ type: String }) private readonly server!: string @@ -341,6 +341,10 @@ Vue.set(this.state.control, 'keyboard', { layout, variant }) } + public setCursorDrawFunction(fn?: CursorDrawFunction) { + Vue.set(this, 'cursorDrawFunction', fn) + } + // TODO: Remove? Use REST API only? public setScreenSize(width: number, height: number, rate: number) { this.connection.websocket.send(EVENT.SCREEN_SET, { width, height, rate }) diff --git a/src/component/overlay.vue b/src/component/overlay.vue index d66c287b..6e3f5887 100644 --- a/src/component/overlay.vue +++ b/src/component/overlay.vue @@ -38,25 +38,19 @@ import { getFilesFromDataTansfer } from './utils/file-upload' import { NekoWebRTC } from './internal/webrtc' import { Scroll } from './types/state' - - const INACTIVE_CURSOR = - 'url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAACEUlEQVR4nOzWz6sSURQH8O+89zJ5C32LKbAgktCSaPpBSL' + - 'uSNtHqLcOV+BeIGxei0oCtFME/wI0bF4GCK6mNuAghH7xFlBAO7bQoA/Vik3riyghTaCQzTsLzbIZZDPdzzj3nzt3Df44dYDsBRNSYTqcn5XL5KoADy1VERL' + - 'Is02g0+phIJG4BsFkOEEVxjhgOh59kWb5rKWIBWCAGg0EnFovdtgyhB+grkU6n7wA4ZzlgCWKzlVgGsLQnVgE2gVh7xvP5PH9ciUajFQDHyWTyHQDVKOS3+F' + - 'sF/pyOcDh83Uhj/nMFBEFANpuF0+nUQ92SJD0G8AXAdwAz0wE+nw8OhwPNZhPj8RiBQOC0Vqu9EgSBcrnc11Qq9R7AeW5cd/GVsdgCr9dLiqJQtVqdv/v9fm' + - 'KM9UVRfArgJoBrAC4DsJsOcLlc1Gg0qNVqVRljI0mS5oh6vU6lUukFgEta5gemLr4AFAoF6nQ6b20223G73X6ZyWTmgFAoRL1ej3f+DQ1gfqiq+qbf73/weD' + - 'zPADwoFouPut3uzO12UyQSoclkotrt9ocAHKZnr8UhAP4bvg/gIs+UMfaaMTZTFOUkHo8/B/AEwAWjl5pV+j1dZ//g4xUMBo8YY/cqlcqhNvffAJxq40dmA5' + - 'bFPoAjrev5EfwZQNfoKbju/u1ri/PvfgKYGMl+K2I7b8U7wA5wpgC/AgAA///Yyif1MZXzRQAAAABJRU5ErkJggg==) 4 4, crosshair' + import { CursorPosition, CursorImage } from './types/webrtc' + import { CursorDrawFunction, Dimension, KeyboardModifiers } from './types/overlay' const WHEEL_STEP = 53 // Delta threshold for a mouse wheel step const WHEEL_LINE_HEIGHT = 19 + const CANVAS_SCALE = 2 @Component({ name: 'neko-overlay', }) export default class extends Vue { @Ref('overlay') readonly _overlay!: HTMLCanvasElement - private _ctx: any = null + private _ctx!: CanvasRenderingContext2D private keyboard = GuacamoleKeyboard() private focused = false @@ -68,14 +62,17 @@ private readonly scroll!: Scroll @Prop() - private readonly screenSize!: { width: number; height: number } + private readonly screenSize!: Dimension @Prop() - private readonly canvasSize!: { width: number; height: number } + private readonly canvasSize!: Dimension @Prop() private readonly cursorTag!: string + @Prop() + private readonly cursorDraw!: CursorDrawFunction | null + @Prop() private readonly isControling!: boolean @@ -83,11 +80,7 @@ private readonly implicitControl!: boolean get cursor(): string { - if (!this.isControling) { - return INACTIVE_CURSOR - } - - if (!this.cursorImage) { + if (!this.isControling || !this.cursorImage) { return 'auto' } @@ -96,12 +89,16 @@ } mounted() { - this._ctx = this._overlay.getContext('2d') + // get canvas overlay context + const ctx = this._overlay.getContext('2d') + if (ctx != null) { + this._ctx = ctx + } // synchronize intrinsic with extrinsic dimensions const { width, height } = this._overlay.getBoundingClientRect() - this._overlay.width = width - this._overlay.height = height + this._overlay.width = width * CANVAS_SCALE + this._overlay.height = height * CANVAS_SCALE // Initialize Guacamole Keyboard this.keyboard.onkeydown = (key: number) => { @@ -136,7 +133,7 @@ this.webrtc.addListener('cursor-position', this.onCursorPosition) this.webrtc.addListener('cursor-image', this.onCursorImage) this.webrtc.addListener('disconnected', this.canvasClear) - this.cursorElement.onload = this.canvasRedraw + this.cursorElement.onload = this.canvasRequestRedraw } beforeDestroy() { @@ -331,7 +328,7 @@ // keyboard modifiers // - private keyboardModifiers: { capslock: boolean; numlock: boolean } | null = null + private keyboardModifiers: KeyboardModifiers | null = null updateKeyboardModifiers(e: MouseEvent) { const capslock = e.getModifierState('CapsLock') @@ -350,37 +347,26 @@ // canvas // - private cursorImage: { width: number; height: number; x: number; y: number; uri: string } | null = null + private cursorImage: CursorImage | null = null private cursorElement: HTMLImageElement = new Image() - private cursorPosition: { x: number; y: number } | null = null - - @Watch('cursorTag') - onCursorTagChange() { - if (!this.isControling) { - this.canvasRedraw() - } - } + private cursorPosition: CursorPosition | null = null + private canvasRequestedFrame = false @Watch('canvasSize') - onCanvasSizeChange({ width, height }: { width: number; height: number }) { - this._overlay.width = width - this._overlay.height = height - - if (this.isControling) { - this.canvasClear() - } else { - this.canvasRedraw() - } + onCanvasSizeChange({ width, height }: Dimension) { + this._overlay.width = width * CANVAS_SCALE + this._overlay.height = height * CANVAS_SCALE + this.canvasRequestRedraw() } - onCursorPosition(data: { x: number; y: number }) { + onCursorPosition(data: CursorPosition) { if (!this.isControling) { Vue.set(this, 'cursorPosition', data) - this.canvasRedraw() + this.canvasRequestRedraw() } } - onCursorImage(data: { width: number; height: number; x: number; y: number; uri: string }) { + onCursorImage(data: CursorImage) { Vue.set(this, 'cursorImage', data) if (!this.isControling) { @@ -388,27 +374,61 @@ } } + @Watch('cursorDraw') + @Watch('cursorTag') + canvasRequestRedraw() { + // skip rendering if there is already in progress + if (this.canvasRequestedFrame) return + + // request animation frame from a browser + this.canvasRequestedFrame = true + window.requestAnimationFrame(() => { + if (this.isControling) { + this.canvasClear() + } else { + this.canvasRedraw() + } + + this.canvasRequestedFrame = false + }) + } + canvasRedraw() { if (this.cursorPosition == null || this.screenSize == null || this.cursorImage == null) return - // get intrinsic dimensions - const { width, height } = this._overlay - // clear drawings - this._ctx.clearRect(0, 0, width, height) + this.canvasClear() // ignore hidden cursor if (this.cursorImage.width <= 1 && this.cursorImage.height <= 1) return - // redraw cursor - let x = Math.round((this.cursorPosition.x / this.screenSize.width) * width - this.cursorImage.x) - let y = Math.round((this.cursorPosition.y / this.screenSize.height) * height - this.cursorImage.y) - this._ctx.drawImage(this.cursorElement, x, y, this.cursorImage.width, this.cursorImage.height) + // get intrinsic dimensions + let { width, height } = this.canvasSize + this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0) - // redraw cursor tag + // get cursor position + let x = Math.round((this.cursorPosition.x / this.screenSize.width) * width) + let y = Math.round((this.cursorPosition.y / this.screenSize.height) * height) + + // use custom draw function, if available + if (this.cursorDraw) { + this.cursorDraw(this._ctx, x, y, this.cursorElement, this.cursorImage, this.cursorTag) + return + } + + // draw cursor image + this._ctx.drawImage( + this.cursorElement, + x - this.cursorImage.x, + y - this.cursorImage.y, + this.cursorImage.width, + this.cursorImage.height, + ) + + // draw cursor tag if (this.cursorTag) { - x += this.cursorImage.width + this.cursorImage.x - y += this.cursorImage.height + this.cursorImage.y + x += this.cursorImage.width + y += this.cursorImage.height this._ctx.save() this._ctx.font = '14px Arial, sans-serif' @@ -451,11 +471,7 @@ this.webrtc.send('mouseup', { key: this.reqMouseUp.button + 1 }) } - if (isControling) { - this.canvasClear() - } else { - this.canvasRedraw() - } + this.canvasRequestRedraw() this.reqMouseDown = null this.reqMouseUp = null diff --git a/src/component/types/overlay.ts b/src/component/types/overlay.ts new file mode 100644 index 00000000..57c1d414 --- /dev/null +++ b/src/component/types/overlay.ts @@ -0,0 +1,13 @@ +import { CursorImage } from './webrtc' + +export type CursorDrawFunction = (ctx: CanvasRenderingContext2D, x: number, y: number, cursorElement: HTMLImageElement, cursorImage: CursorImage, cursorTag: string) => void + +export interface Dimension { + width: number + height: number +} + +export interface KeyboardModifiers { + capslock: boolean + numlock: boolean +} diff --git a/src/component/types/webrtc.ts b/src/component/types/webrtc.ts index 8c4fb5ca..0c707a55 100644 --- a/src/component/types/webrtc.ts +++ b/src/component/types/webrtc.ts @@ -6,3 +6,16 @@ export interface WebRTCStats { height: number muted?: boolean } + +export interface CursorPosition { + x: number + y: number +} + +export interface CursorImage { + width: number + height: number + x: number + y: number + uri: string +}