canvas animate inactive cursors.

This commit is contained in:
Miroslav Šedivý 2021-11-11 23:50:35 +01:00
parent b621c685c1
commit 6afa3c68de
3 changed files with 138 additions and 53 deletions

View File

@ -16,10 +16,12 @@
<script lang="ts">
import { Vue, Component, Ref, Prop, Watch } from 'vue-property-decorator'
import { Cursor, Session } from './types/state'
import { SessionCursors, Cursor, Session } from './types/state'
import { InactiveCursorDrawFunction, Dimension } from './types/cursors'
import { getMovementXYatPercent } from './utils/canvas-movement'
const CANVAS_SCALE = 2
const POS_INTERVAL_MS = 750
@Component({
name: 'neko-cursors',
@ -41,7 +43,7 @@
private readonly canvasSize!: Dimension
@Prop()
private readonly cursors!: Cursor[]
private readonly cursors!: SessionCursors[]
@Prop()
private readonly cursorDraw!: InactiveCursorDrawFunction | null
@ -57,74 +59,106 @@
const { width, height } = this._overlay.getBoundingClientRect()
this._overlay.width = width * CANVAS_SCALE
this._overlay.height = height * CANVAS_SCALE
this._last_points = {}
}
beforeDestroy() {}
private canvasRequestedFrame = false
@Watch('canvasSize')
onCanvasSizeChange({ width, height }: Dimension) {
this._overlay.width = width * CANVAS_SCALE
this._overlay.height = height * CANVAS_SCALE
this.canvasRequestRedraw()
}
// start as undefined to prevent jumping
private _prev_time!: number
private _percent!: number
private _points!: SessionCursors[]
private _last_points!: Record<string, Cursor>
canvasAnimate(now: number = NaN) {
// request another frame
if (this._percent <= 1) requestAnimationFrame(this.canvasAnimate)
// calculate factor
const delta = (now - this._prev_time) / POS_INTERVAL_MS
this._prev_time = now
// skip very first delta to prevent jumping
if (isNaN(delta)) return
// set the animation position
this._percent += delta
this.canvasClear()
// scale
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0)
// draw current position
for (const p of this._points) {
const { x, y } = getMovementXYatPercent(p.cursors, this._percent)
this.canvasRedraw(x, y, p.id)
}
}
@Watch('cursors')
@Watch('cursorDraw')
canvasRequestRedraw() {
// skip rendering if there is already in progress
if (this.canvasRequestedFrame) return
canvasSetPosition(e: SessionCursors[]) {
console.log('consuming', e)
// request animation frame from a browser
this.canvasRequestedFrame = true
window.requestAnimationFrame(() => {
this.canvasRedraw()
this.canvasRequestedFrame = false
})
// clear on no cursor
if (e.length == 0) {
this._last_points = {}
this.canvasClear()
return
}
// create points for animation
this._points = []
for (const { id, cursors } of e) {
let pos = { id } as SessionCursors
if (id in this._last_points) {
pos.cursors = [this._last_points[id], ...cursors]
} else {
pos.cursors = [...cursors]
}
this._last_points[id] = cursors[cursors.length - 1]
this._points.push(pos)
}
const startAnimation = this._percent > 1 || this._percent == 0
this._percent = 0
if (startAnimation) this.canvasAnimate()
}
canvasRedraw() {
if (this.screenSize == null) return
// clear drawings
this.canvasClear()
canvasRedraw(x: number, y: number, id: string) {
// get intrinsic dimensions
let { width, height } = this.canvasSize
this._ctx.setTransform(CANVAS_SCALE, 0, 0, CANVAS_SCALE, 0, 0)
x = Math.round((x / this.screenSize.width) * width)
y = Math.round((y / this.screenSize.height) * height)
// draw cursors
for (let { id, x, y } of this.cursors) {
// ignore own cursor
if (id == this.sessionId) continue
// get cursor tag
const cursorTag = this.sessions[id]?.profile.name || ''
// get cursor position
x = Math.round((x / this.screenSize.width) * width)
y = Math.round((y / this.screenSize.height) * height)
// get cursor tag
const cursorTag = this.sessions[id]?.profile.name || ''
// use custom draw function, if available
if (this.cursorDraw) {
this.cursorDraw(this._ctx, x, y, cursorTag)
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(cursorTag, x, y)
this._ctx.shadowBlur = 0
this._ctx.fillStyle = 'white'
this._ctx.fillText(cursorTag, x, y)
this._ctx.restore()
// use custom draw function, if available
if (this.cursorDraw) {
this.cursorDraw(this._ctx, x, y, cursorTag)
return
}
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(cursorTag, x, y)
this._ctx.shadowBlur = 0
this._ctx.fillStyle = 'white'
this._ctx.fillText(cursorTag, x, y)
this._ctx.restore()
}
canvasClear() {

View File

@ -128,11 +128,15 @@ export interface Session {
export interface Cursors {
enabled: boolean
list: Cursor[]
list: SessionCursors[]
}
export interface SessionCursors {
id: string
cursors: Cursor[]
}
export interface Cursor {
id: string
x: number
y: number
}

View File

@ -0,0 +1,47 @@
/* eslint-disable */
export interface Point {
x: number
y: number
}
// movement: percent is 0-1
export function getMovementXYatPercent(points: Point[], percent: number): Point {
if (points.length == 0) { console.error('no points specified'); return { x:0, y:0 } }
if (points.length == 1) return points[0]
if (points.length == 2) return getLineXYatPercent(points[0], points[1], percent)
if (points.length == 3) return getQuadraticBezierXYatPercent(points[0], points[1], points[2], percent)
if (points.length == 4) return getCubicBezierXYatPercent(points[0], points[1], points[2], points[3], percent)
console.error('max 4 points supported'); return points[4]
}
// line: percent is 0-1
export function getLineXYatPercent(startPt: Point, endPt: Point, percent: number) : Point {
return {
x: startPt.x + (endPt.x - startPt.x) * percent,
y: startPt.y + (endPt.y - startPt.y) * percent,
};
}
// quadratic bezier: percent is 0-1
export function getQuadraticBezierXYatPercent(startPt: Point, controlPt: Point, endPt: Point, percent: number): Point {
return {
x: Math.pow(1 - percent, 2) * startPt.x + 2 * (1 - percent) * percent * controlPt.x + Math.pow(percent, 2) * endPt.x,
y: Math.pow(1 - percent, 2) * startPt.y + 2 * (1 - percent) * percent * controlPt.y + Math.pow(percent, 2) * endPt.y,
}
}
// cubic bezier percent is 0-1
export function getCubicBezierXYatPercent(startPt: Point, controlPt1: Point, controlPt2: Point, endPt: Point, percent: number): Point {
return {
x: cubicN(percent, startPt.x, controlPt1.x, controlPt2.x, endPt.x),
y: cubicN(percent, startPt.y, controlPt1.y, controlPt2.y, endPt.y),
}
}
// cubic helper formula at percent distance
function cubicN(pct: number, a: number, b: number, c: number, d: number): number {
var t2 = pct * pct;
var t3 = t2 * pct;
return a + (-a * 3 + pct * (3 * a - a * pct)) * pct + (3 * b + pct * (-6 * b + b * 3 * pct)) * pct + (c * 3 - c * 3 * pct) * t2 + d * t3;
}