mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
custom cursor draw function
This commit is contained in:
parent
f69e3dfdb9
commit
3227e691ea
@ -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> {
|
||||
|
@ -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 })
|
||||
|
@ -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
|
||||
|
13
src/component/types/overlay.ts
Normal file
13
src/component/types/overlay.ts
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user