neko/src/component/cursors.vue

254 lines
7.1 KiB
Vue
Raw Normal View History

2021-10-27 05:02:01 +13:00
<template>
<canvas ref="overlay" class="neko-cursors" tabindex="0" />
</template>
<style lang="scss">
2021-10-27 05:02:01 +13:00
.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-12 11:50:35 +13:00
import { SessionCursors, Cursor, Session } from './types/state'
2021-11-02 09:15:57 +13:00
import { InactiveCursorDrawFunction, Dimension } from './types/cursors'
2021-11-12 11:50:35 +13:00
import { getMovementXYatPercent } from './utils/canvas-movement'
2021-10-27 05:02:01 +13:00
// How often are position data arriving
2021-11-12 11:50:35 +13:00
const POS_INTERVAL_MS = 750
// How many pixel change is considered as movement
const POS_THRESHOLD_PX = 20
2021-10-27 05:02:01 +13:00
@Component({
name: 'neko-cursors',
})
export default class extends Vue {
@Ref('overlay') readonly _overlay!: HTMLCanvasElement
private _ctx!: CanvasRenderingContext2D
private canvasScale = window.devicePixelRatio
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-11-12 12:04:32 +13:00
@Prop()
private readonly hostId!: string | null
2021-10-27 05:02:01 +13:00
@Prop()
private readonly screenSize!: Dimension
@Prop()
private readonly canvasSize!: Dimension
@Prop()
2021-11-12 11:50:35 +13:00
private readonly cursors!: SessionCursors[]
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
@Prop()
private readonly fps!: number
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.canvasResize({ width, height })
2021-11-12 11:50:35 +13:00
2021-11-15 07:16:19 +13:00
// store last drawing points
2021-11-12 11:50:35 +13:00
this._last_points = {}
2021-10-27 05:02:01 +13:00
}
beforeDestroy() {}
@Watch('canvasSize')
onCanvasSizeChange({ width, height }: Dimension) {
this.canvasResize({ width, height })
this.canvasUpdateCursors()
}
canvasResize({ width, height }: Dimension) {
this._overlay.width = width * this.canvasScale
this._overlay.height = height * this.canvasScale
this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0)
2021-10-27 05:02:01 +13:00
}
2021-11-12 11:50:35 +13:00
// start as undefined to prevent jumping
2021-11-15 07:16:19 +13:00
private _last_animation_time!: number
// current animation progress (0-1)
2021-11-12 11:50:35 +13:00
private _percent!: number
2021-11-15 07:16:19 +13:00
// points to be animated for each session
2021-11-12 11:50:35 +13:00
private _points!: SessionCursors[]
2021-11-15 07:16:19 +13:00
// last points coordinates for each session
2021-11-12 11:50:35 +13:00
private _last_points!: Record<string, Cursor>
canvasAnimateFrame(now: number = NaN) {
2021-11-12 11:50:35 +13:00
// request another frame
if (this._percent <= 1) window.requestAnimationFrame(this.canvasAnimateFrame)
2021-11-12 11:50:35 +13:00
// calc elapsed time since last loop
const elapsed = now - this._last_animation_time
// skip if fps is set and elapsed time is less than fps
if (this.fps > 0 && elapsed < 1000 / this.fps) return
// calc current animation progress
const delta = elapsed / POS_INTERVAL_MS
2021-11-15 07:16:19 +13:00
this._last_animation_time = now
2021-10-27 05:02:01 +13:00
2021-11-12 11:50:35 +13:00
// skip very first delta to prevent jumping
if (isNaN(delta)) return
// set the animation position
this._percent += delta
2021-10-27 05:02:01 +13:00
// draw points for current frame
this.canvasDrawPoints(this._percent)
}
canvasDrawPoints(percent: number = 1) {
// clear canvas
2021-10-27 05:02:01 +13:00
this.canvasClear()
2021-11-12 11:50:35 +13:00
// draw current position
for (const p of this._points) {
const { x, y } = getMovementXYatPercent(p.cursors, percent)
2021-11-12 12:04:32 +13:00
this.canvasDrawCursor(x, y, p.id)
2021-11-12 11:50:35 +13:00
}
}
2021-10-27 05:02:01 +13:00
2021-11-12 12:04:32 +13:00
@Watch('hostId')
2021-11-12 11:50:35 +13:00
@Watch('cursors')
2021-11-12 12:04:32 +13:00
canvasUpdateCursors() {
let new_last_points = {} as Record<string, Cursor>
// track unchanged cursors
let unchanged = 0
2021-11-12 11:50:35 +13:00
// create points for animation
this._points = []
2021-11-12 12:04:32 +13:00
for (const { id, cursors } of this.cursors) {
if (
// if there are no positions
cursors.length == 0 ||
// ignore own cursor
id == this.sessionId ||
// ignore host's cursor
id == this.hostId
) {
unchanged++
continue
}
// get last point
const new_last_point = cursors[cursors.length - 1]
2021-11-12 12:04:32 +13:00
// add last cursor position to cursors (if available)
2021-11-12 11:50:35 +13:00
let pos = { id } as SessionCursors
if (id in this._last_points) {
const last_point = this._last_points[id]
// if cursor did not move considerably
if (
Math.abs(last_point.x - new_last_point.x) < POS_THRESHOLD_PX &&
Math.abs(last_point.y - new_last_point.y) < POS_THRESHOLD_PX
) {
// we knew that this cursor did not change, but other
// might, so we keep only one point to be drawn
pos.cursors = [new_last_point]
// and increase unchanged counter
unchanged++
} else {
// if cursor moved, we want to include last point
// in the animation, so that movement can be seamless
pos.cursors = [last_point, ...cursors]
}
2021-11-12 11:50:35 +13:00
} else {
// if cursor does not have last point, it is not
// displayed in canvas and it should be now
2021-11-12 11:50:35 +13:00
pos.cursors = [...cursors]
2021-10-27 05:02:01 +13:00
}
2021-11-12 12:04:32 +13:00
new_last_points[id] = new_last_point
2021-11-12 11:50:35 +13:00
this._points.push(pos)
}
// apply new last points
this._last_points = new_last_points
2021-11-12 11:54:11 +13:00
// no cursors to animate
if (this._points.length == 0) {
2021-11-12 12:04:32 +13:00
this.canvasClear()
2021-11-12 11:54:11 +13:00
return
}
// if all cursors are unchanged
if (unchanged == this.cursors.length) {
// draw only last known position without animation
this.canvasDrawPoints()
return
}
2021-11-12 11:54:11 +13:00
// start animation if not running
const percent = this._percent
2021-11-12 11:50:35 +13:00
this._percent = 0
if (percent > 1 || !percent) {
this.canvasAnimateFrame()
2021-11-12 11:54:11 +13:00
}
2021-11-12 11:50:35 +13:00
}
2021-10-27 05:02:01 +13:00
2021-11-12 12:04:32 +13:00
canvasDrawCursor(x: number, y: number, id: string) {
2021-11-12 11:50:35 +13:00
// get intrinsic dimensions
2023-04-19 21:50:35 +12:00
const { width, height } = this.canvasSize
2021-11-12 11:50:35 +13:00
x = Math.round((x / this.screenSize.width) * width)
y = Math.round((y / this.screenSize.height) * height)
// reset transformation, X and Y will be 0 again
this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0)
2021-11-12 11:50:35 +13:00
// use custom draw function, if available
if (this.cursorDraw) {
this.cursorDraw(this._ctx, x, y, id)
2021-11-12 11:50:35 +13:00
return
2021-10-27 05:02:01 +13:00
}
2021-11-12 11:50:35 +13:00
// get cursor tag
const cursorTag = this.sessions[id]?.profile.name || ''
2021-11-15 07:16:19 +13:00
// draw inactive cursor tag
2021-11-12 11:50:35 +13:00
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(cursorTag, x, y)
this._ctx.shadowBlur = 0
this._ctx.fillStyle = 'white'
this._ctx.fillText(cursorTag, x, y)
2021-10-27 05:02:01 +13:00
}
canvasClear() {
// reset transformation, X and Y will be 0 again
this._ctx.setTransform(this.canvasScale, 0, 0, this.canvasScale, 0, 0)
2023-04-19 21:50:35 +12:00
const { width, height } = this.canvasSize
2021-10-27 05:02:01 +13:00
this._ctx.clearRect(0, 0, width, height)
}
}
</script>