session cursors (WIP).

This commit is contained in:
Miroslav Šedivý 2021-10-26 18:02:01 +02:00
parent 3227e691ea
commit cae092fb20
8 changed files with 156 additions and 3 deletions

123
src/component/cursors.vue Normal file
View File

@ -0,0 +1,123 @@
<template>
<canvas ref="overlay" class="neko-cursors" tabindex="0" />
</template>
<style lang="scss" scoped>
.neko-cursors {
position: absolute;
top: 0;
bottom: 0;
width: 100%;
height: 100%;
outline: 0;
}
</style>
<script lang="ts">
import { Vue, Component, Ref, Prop, Watch } from 'vue-property-decorator'
import { SessionCursor } from './types/state'
import { CursorDrawFunction, Dimension } from './types/cursors'
const CANVAS_SCALE = 2
@Component({
name: 'neko-cursors',
})
export default class extends Vue {
@Ref('overlay') readonly _overlay!: HTMLCanvasElement
private _ctx!: CanvasRenderingContext2D
@Prop()
private readonly screenSize!: Dimension
@Prop()
private readonly canvasSize!: Dimension
@Prop()
private readonly cursors!: SessionCursor[]
@Prop()
private readonly cursorDraw!: CursorDrawFunction | null
mounted() {
// 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 * CANVAS_SCALE
this._overlay.height = height * CANVAS_SCALE
}
beforeDestroy() {}
private canvasRequestedFrame = false
@Watch('canvasSize')
onCanvasSizeChange({ width, height }: Dimension) {
this._overlay.width = width * CANVAS_SCALE
this._overlay.height = height * CANVAS_SCALE
this.canvasRequestRedraw()
}
@Watch('cursors')
@Watch('cursorDraw')
canvasRequestRedraw() {
// skip rendering if there is already in progress
if (this.canvasRequestedFrame) return
// request animation frame from a browser
this.canvasRequestedFrame = true
window.requestAnimationFrame(() => {
this.canvasRedraw()
this.canvasRequestedFrame = false
})
}
canvasRedraw() {
if (this.screenSize == null) return
// clear drawings
this.canvasClear()
// get intrinsic dimensions
let { width, height } = this.canvasSize
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0)
// draw cursors
for (let { id, x, y } of this.cursors) {
// get cursor position
x = Math.round((x / this.screenSize.width) * width)
y = Math.round((y / this.screenSize.height) * height)
// use custom draw function, if available
if (this.cursorDraw) {
this.cursorDraw(this._ctx, x, y, id)
continue
}
this._ctx.save()
this._ctx.font = '14px Arial, sans-serif'
this._ctx.textBaseline = 'top'
this._ctx.shadowColor = 'black'
this._ctx.shadowBlur = 2
this._ctx.lineWidth = 2
this._ctx.fillStyle = 'black'
this._ctx.strokeText(id, x, y)
this._ctx.shadowBlur = 0
this._ctx.fillStyle = 'white'
this._ctx.fillText(id, x, y)
this._ctx.restore()
}
}
canvasClear() {
const { width, height } = this._overlay
this._ctx.clearRect(0, 0, width, height)
}
}
</script>

View File

@ -188,6 +188,11 @@ export class NekoMessages extends EventEmitter<NekoEvents> {
}
}
protected [EVENT.SESSION_CURSORS](cursors: message.SessionCursor[]) {
// TODO: State retention logic.
Vue.set(this._state, 'cursors', cursors)
}
/////////////////////////////
// Control Events
/////////////////////////////

View File

@ -3,6 +3,7 @@
<div ref="container" class="neko-container">
<video v-show="!screencast" ref="video" :autoplay="autoplay" :muted="autoplay" playsinline />
<neko-screencast v-show="screencast" :enabled="screencast" :api="api.room" />
<neko-cursors :screenSize="state.screen.size" :canvasSize="canvasSize" :cursors="state.cursors" />
<neko-overlay
:webrtc="connection.webrtc"
:scroll="state.control.scroll"
@ -69,12 +70,14 @@
import { Dimension, CursorDrawFunction } from './types/overlay'
import Overlay from './overlay.vue'
import Screencast from './screencast.vue'
import Cursors from './cursors.vue'
@Component({
name: 'neko-canvas',
components: {
'neko-overlay': Overlay,
'neko-screencast': Screencast,
'neko-cursors': Cursors,
},
})
export default class extends Vue {
@ -159,6 +162,7 @@
},
session_id: null,
sessions: {},
cursors: [],
} as NekoState
/////////////////////////////

View File

@ -249,9 +249,10 @@
}
onMouseMove(e: MouseEvent) {
if (!this.isControling) {
return
}
// TODO: Send less events if not controlling.
//if (!this.isControling) {
// return
//}
this.setMousePos(e)
}

View File

@ -0,0 +1,6 @@
export type CursorDrawFunction = (ctx: CanvasRenderingContext2D, x: number, y: number, id: string) => void
export interface Dimension {
width: number
height: number
}

View File

@ -14,6 +14,7 @@ export const SESSION_CREATED = 'session/created'
export const SESSION_DELETED = 'session/deleted'
export const SESSION_PROFILE = 'session/profile'
export const SESSION_STATE = 'session/state'
export const SESSION_CURSORS = 'session/cursors'
export const CONTROL_HOST = 'control/host'
export const CONTROL_RELEASE = 'control/release'

View File

@ -87,6 +87,12 @@ export interface SessionData {
is_watching: boolean
}
export interface SessionCursor {
id: string
x: number
y: number
}
/////////////////////////////
// Control
/////////////////////////////

View File

@ -9,6 +9,7 @@ export default interface State {
screen: Screen
session_id: string | null
sessions: Record<string, Session>
cursors: SessionCursor[]
}
/////////////////////////////
@ -118,3 +119,9 @@ export interface Session {
profile: MemberProfile
state: SessionState
}
export interface SessionCursor {
id: string
x: number
y: number
}