2021-10-27 05:02:01 +13:00
|
|
|
<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'
|
|
|
|
|
2021-11-02 08:56:10 +13:00
|
|
|
import { Cursor, Session } from './types/state'
|
2021-11-02 09:15:57 +13:00
|
|
|
import { InactiveCursorDrawFunction, Dimension } from './types/cursors'
|
2021-10-27 05:02:01 +13:00
|
|
|
|
|
|
|
const CANVAS_SCALE = 2
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
name: 'neko-cursors',
|
|
|
|
})
|
|
|
|
export default class extends Vue {
|
|
|
|
@Ref('overlay') readonly _overlay!: HTMLCanvasElement
|
|
|
|
private _ctx!: CanvasRenderingContext2D
|
|
|
|
|
2021-11-02 08:56:10 +13:00
|
|
|
@Prop()
|
|
|
|
private readonly sessions!: Record<string, Session>
|
|
|
|
|
2021-10-27 09:55:22 +13:00
|
|
|
@Prop()
|
|
|
|
private readonly sessionId!: string
|
|
|
|
|
2021-10-27 05:02:01 +13:00
|
|
|
@Prop()
|
|
|
|
private readonly screenSize!: Dimension
|
|
|
|
|
|
|
|
@Prop()
|
|
|
|
private readonly canvasSize!: Dimension
|
|
|
|
|
|
|
|
@Prop()
|
2021-11-02 08:49:59 +13:00
|
|
|
private readonly cursors!: Cursor[]
|
2021-10-27 05:02:01 +13:00
|
|
|
|
|
|
|
@Prop()
|
2021-11-02 09:15:57 +13:00
|
|
|
private readonly cursorDraw!: InactiveCursorDrawFunction | null
|
2021-10-27 05:02:01 +13:00
|
|
|
|
|
|
|
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) {
|
2021-10-27 09:55:22 +13:00
|
|
|
// ignore own cursor
|
|
|
|
if (id == this.sessionId) continue
|
|
|
|
|
2021-10-27 05:02:01 +13:00
|
|
|
// get cursor position
|
|
|
|
x = Math.round((x / this.screenSize.width) * width)
|
|
|
|
y = Math.round((y / this.screenSize.height) * height)
|
|
|
|
|
2021-11-02 08:56:10 +13:00
|
|
|
// get cursor tag
|
|
|
|
const cursorTag = this.sessions[id]?.profile.name || ''
|
|
|
|
|
2021-10-27 05:02:01 +13:00
|
|
|
// use custom draw function, if available
|
|
|
|
if (this.cursorDraw) {
|
2021-11-02 08:56:10 +13:00
|
|
|
this.cursorDraw(this._ctx, x, y, cursorTag)
|
2021-10-27 05:02:01 +13:00
|
|
|
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'
|
2021-11-02 08:56:10 +13:00
|
|
|
this._ctx.strokeText(cursorTag, x, y)
|
2021-10-27 05:02:01 +13:00
|
|
|
this._ctx.shadowBlur = 0
|
|
|
|
this._ctx.fillStyle = 'white'
|
2021-11-02 08:56:10 +13:00
|
|
|
this._ctx.fillText(cursorTag, x, y)
|
2021-10-27 05:02:01 +13:00
|
|
|
this._ctx.restore()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
canvasClear() {
|
|
|
|
const { width, height } = this._overlay
|
|
|
|
this._ctx.clearRect(0, 0, width, height)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
</script>
|