custom cursor draw function

This commit is contained in:
Miroslav Šedivý 2021-10-18 11:14:05 +00:00
parent f69e3dfdb9
commit 3227e691ea
5 changed files with 112 additions and 66 deletions

View File

@ -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<NekoWebRTCEvents> {

View File

@ -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 })

View File

@ -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

View File

@ -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
}

View File

@ -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
}