mirror of
https://github.com/m1k1o/neko.git
synced 2024-07-24 14:40:50 +12:00
session cursors (WIP).
This commit is contained in:
parent
3227e691ea
commit
cae092fb20
123
src/component/cursors.vue
Normal file
123
src/component/cursors.vue
Normal 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>
|
@ -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
|
// Control Events
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
<div ref="container" class="neko-container">
|
<div ref="container" class="neko-container">
|
||||||
<video v-show="!screencast" ref="video" :autoplay="autoplay" :muted="autoplay" playsinline />
|
<video v-show="!screencast" ref="video" :autoplay="autoplay" :muted="autoplay" playsinline />
|
||||||
<neko-screencast v-show="screencast" :enabled="screencast" :api="api.room" />
|
<neko-screencast v-show="screencast" :enabled="screencast" :api="api.room" />
|
||||||
|
<neko-cursors :screenSize="state.screen.size" :canvasSize="canvasSize" :cursors="state.cursors" />
|
||||||
<neko-overlay
|
<neko-overlay
|
||||||
:webrtc="connection.webrtc"
|
:webrtc="connection.webrtc"
|
||||||
:scroll="state.control.scroll"
|
:scroll="state.control.scroll"
|
||||||
@ -69,12 +70,14 @@
|
|||||||
import { Dimension, CursorDrawFunction } from './types/overlay'
|
import { Dimension, CursorDrawFunction } from './types/overlay'
|
||||||
import Overlay from './overlay.vue'
|
import Overlay from './overlay.vue'
|
||||||
import Screencast from './screencast.vue'
|
import Screencast from './screencast.vue'
|
||||||
|
import Cursors from './cursors.vue'
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
name: 'neko-canvas',
|
name: 'neko-canvas',
|
||||||
components: {
|
components: {
|
||||||
'neko-overlay': Overlay,
|
'neko-overlay': Overlay,
|
||||||
'neko-screencast': Screencast,
|
'neko-screencast': Screencast,
|
||||||
|
'neko-cursors': Cursors,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
export default class extends Vue {
|
export default class extends Vue {
|
||||||
@ -159,6 +162,7 @@
|
|||||||
},
|
},
|
||||||
session_id: null,
|
session_id: null,
|
||||||
sessions: {},
|
sessions: {},
|
||||||
|
cursors: [],
|
||||||
} as NekoState
|
} as NekoState
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -249,9 +249,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMouseMove(e: MouseEvent) {
|
onMouseMove(e: MouseEvent) {
|
||||||
if (!this.isControling) {
|
// TODO: Send less events if not controlling.
|
||||||
return
|
//if (!this.isControling) {
|
||||||
}
|
// return
|
||||||
|
//}
|
||||||
|
|
||||||
this.setMousePos(e)
|
this.setMousePos(e)
|
||||||
}
|
}
|
||||||
|
6
src/component/types/cursors.ts
Normal file
6
src/component/types/cursors.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export type CursorDrawFunction = (ctx: CanvasRenderingContext2D, x: number, y: number, id: string) => void
|
||||||
|
|
||||||
|
export interface Dimension {
|
||||||
|
width: number
|
||||||
|
height: number
|
||||||
|
}
|
@ -14,6 +14,7 @@ export const SESSION_CREATED = 'session/created'
|
|||||||
export const SESSION_DELETED = 'session/deleted'
|
export const SESSION_DELETED = 'session/deleted'
|
||||||
export const SESSION_PROFILE = 'session/profile'
|
export const SESSION_PROFILE = 'session/profile'
|
||||||
export const SESSION_STATE = 'session/state'
|
export const SESSION_STATE = 'session/state'
|
||||||
|
export const SESSION_CURSORS = 'session/cursors'
|
||||||
|
|
||||||
export const CONTROL_HOST = 'control/host'
|
export const CONTROL_HOST = 'control/host'
|
||||||
export const CONTROL_RELEASE = 'control/release'
|
export const CONTROL_RELEASE = 'control/release'
|
||||||
|
@ -87,6 +87,12 @@ export interface SessionData {
|
|||||||
is_watching: boolean
|
is_watching: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionCursor {
|
||||||
|
id: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
// Control
|
// Control
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
|
@ -9,6 +9,7 @@ export default interface State {
|
|||||||
screen: Screen
|
screen: Screen
|
||||||
session_id: string | null
|
session_id: string | null
|
||||||
sessions: Record<string, Session>
|
sessions: Record<string, Session>
|
||||||
|
cursors: SessionCursor[]
|
||||||
}
|
}
|
||||||
|
|
||||||
/////////////////////////////
|
/////////////////////////////
|
||||||
@ -118,3 +119,9 @@ export interface Session {
|
|||||||
profile: MemberProfile
|
profile: MemberProfile
|
||||||
state: SessionState
|
state: SessionState
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface SessionCursor {
|
||||||
|
id: string
|
||||||
|
x: number
|
||||||
|
y: number
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user