From f57cc13cd1e0ce31a2f21e23b25af317979bcf8e Mon Sep 17 00:00:00 2001 From: nick-delirium Date: Tue, 25 Apr 2023 13:50:39 +0200 Subject: [PATCH] feat(player): added mice trail --- frontend/app/player/web/Screen/Screen.ts | 7 +- frontend/app/player/web/addons/MouseTrail.ts | 149 ++++++++++++++++++ .../player/web/managers/MouseMoveManager.ts | 27 +++- .../app/player/web/managers/trail.module.css | 9 ++ 4 files changed, 187 insertions(+), 5 deletions(-) create mode 100644 frontend/app/player/web/addons/MouseTrail.ts create mode 100644 frontend/app/player/web/managers/trail.module.css diff --git a/frontend/app/player/web/Screen/Screen.ts b/frontend/app/player/web/Screen/Screen.ts index 92479a4af..185b7c769 100644 --- a/frontend/app/player/web/Screen/Screen.ts +++ b/frontend/app/player/web/Screen/Screen.ts @@ -62,6 +62,7 @@ export default class Screen { private readonly iframe: HTMLIFrameElement; private readonly screen: HTMLDivElement; private parentElement: HTMLElement | null = null + private onUpdateHook: (w: number, h: number) => void constructor(isMobile: boolean, private scaleMode: ScaleMode = ScaleMode.Embed) { const iframe = document.createElement('iframe'); @@ -132,7 +133,7 @@ export default class Screen { return this.iframe.style } - private boundingRect: DOMRect | null = null; + public boundingRect: DOMRect | null = null; private getBoundingClientRect(): DOMRect { if (this.boundingRect === null) { // TODO: use this.screen instead in order to separate overlay functionality @@ -246,6 +247,10 @@ export default class Screen { }) this.boundingRect = this.overlay.getBoundingClientRect(); + this.onUpdateHook(width, height) } + setOnUpdate(cb: any) { + this.onUpdateHook = cb + } } diff --git a/frontend/app/player/web/addons/MouseTrail.ts b/frontend/app/player/web/addons/MouseTrail.ts new file mode 100644 index 000000000..139199290 --- /dev/null +++ b/frontend/app/player/web/addons/MouseTrail.ts @@ -0,0 +1,149 @@ +/** + * Inspired by Bryan C (@bryjch at codepen) + * */ + +const LINE_DURATION = 3; +const LINE_WIDTH_START = 5; + +export default class MouseTrail { + public isActive = true; + public context: CanvasRenderingContext2D; + private dimensions = { width: 0, height: 0 }; + /** + * 1 - every frame, + * 2 - every 2nd frame + * and so on, doesn't always work properly + * but 1 doesnt affect performance so we fine + * */ + private drawOnFrame = 1; + private currentFrame = 0; + private points: Point[] = []; + + constructor(private readonly canvas: HTMLCanvasElement) { + // @ts-ignore patching window + window.requestAnimFrame = + window.requestAnimationFrame || + // @ts-ignore + window.webkitRequestAnimationFrame || + // @ts-ignore + window.mozRequestAnimationFrame || + // @ts-ignore + window.oRequestAnimationFrame || + // @ts-ignore + window.msRequestAnimationFrame || + function (callback: any) { + window.setTimeout(callback, 1000 / 60); + }; + } + + resizeCanvas = (w: number, h: number) => { + if (this.context !== undefined) { + this.context.canvas.width = w; + this.context.canvas.height = h; + this.canvas.width = w; + this.canvas.height = h; + + this.dimensions.width = w; + this.dimensions.height = h; + } + }; + + createContext = () => { + if (this.canvas) { + this.context = this.canvas.getContext('2d')!; + this.init(); + } else { + console.error('Canvas element not found'); + } + }; + + leaveTrail = (x: number, y: number) => { + if (this.currentFrame === this.drawOnFrame) { + this.addPoint(x + 7, y + 7); + this.currentFrame = 0; + } + this.currentFrame++; + }; + + init = () => { + if (this.isActive) { + this.animatePoints(); + // @ts-ignore patched + window.requestAnimFrame(this.init); + } + }; + + animatePoints = () => { + this.context.clearRect(0, 0, this.context.canvas.width, this.context.canvas.height); + + const duration = (LINE_DURATION * 1000) / 60; + let point, lastPoint; + + for (let i = 0; i < this.points.length; i++) { + point = this.points[i]; + + if (this.points[i - 1] !== undefined) { + lastPoint = this.points[i - 1]; + } else { + lastPoint = this.points[i]; + } + + point.lifetime! += 1; + + if (point.lifetime! > duration) { + this.points.splice(i, 1); + continue; + } + + const inc = point.lifetime! / duration; // 0 to 1 over lineDuration + const dec = 1 - inc; + + const spreadRate = LINE_WIDTH_START * (1 - inc); + this.context.lineJoin = 'round'; + this.context.lineWidth = spreadRate; + this.context.strokeStyle = `rgb(255, ${Math.floor(200 - 255 * dec)}, ${Math.floor(200 - 255 * inc)})` + + this.context.beginPath(); + this.context.moveTo(lastPoint.x, lastPoint.y); + this.context.lineTo(point.x, point.y); + this.context.stroke(); + this.context.closePath(); + } + }; + + addPoint = (x: number, y: number) => { + const point = new Point(x, y, 0); + this.points.push(point); + }; +} + +type Coords = { x: number; y: number }; + +class Point { + constructor(public x: number, public y: number, public lifetime?: number) {} + + static distance(a: Coords, b: Coords) { + const dx = a.x - b.x; + const dy = a.y - b.y; + + return Math.sqrt(dx * dx + dy * dy); + } + + static midPoint(a: Coords, b: Coords) { + const mx = a.x + (b.x - a.x) * 0.5; + const my = a.y + (b.y - a.y) * 0.5; + + return new Point(mx, my); + } + + static angle(a: Coords, b: Coords) { + const dx = a.x - b.x; + const dy = a.y - b.y; + + return Math.atan2(dy, dx); + } + + get pos() { + return this.x + ',' + this.y; + } +} \ No newline at end of file diff --git a/frontend/app/player/web/managers/MouseMoveManager.ts b/frontend/app/player/web/managers/MouseMoveManager.ts index 2cb508f59..03f3bbc1b 100644 --- a/frontend/app/player/web/managers/MouseMoveManager.ts +++ b/frontend/app/player/web/managers/MouseMoveManager.ts @@ -2,15 +2,33 @@ import type Screen from '../Screen/Screen' import type { MouseMove } from "../messages"; import { HOVER_CLASSNAME } from '../messages/rewriter/constants' import ListWalker from '../../common/ListWalker' - +import MouseTrail from '../addons/MouseTrail' +import styles from './trail.module.css' export default class MouseMoveManager extends ListWalker { private hoverElements: Array = [] + private mouseTrail: MouseTrail - constructor(private screen: Screen) {super()} + constructor(private screen: Screen) { + super() + const canvas = document.createElement('canvas') + canvas.id = 'openreplay-mouse-trail' + canvas.className = styles.canvas + + this.mouseTrail = new MouseTrail(canvas) + + this.screen.overlay.appendChild(canvas) + this.mouseTrail.createContext() + + const updateSize = (w: number, h: number) => { + return this.mouseTrail.resizeCanvas(w, h) + } + + this.screen.setOnUpdate(updateSize) + } private getCursorTargets() { - return this.screen.getElementsFromInternalPoint(this.current) + return this.screen.getElementsFromInternalPoint(this.current!) } private updateHover(): void { @@ -34,8 +52,9 @@ export default class MouseMoveManager extends ListWalker { const lastMouseMove = this.moveGetLast(t) if (!!lastMouseMove) { this.screen.cursor.move(lastMouseMove) - //window.getComputedStyle(this.screen.getCursorTarget()).cursor === 'pointer' // might nfluence performance though + //window.getComputedStyle(this.screen.getCursorTarget()).cursor === 'pointer' // might influence performance though this.updateHover() + this.mouseTrail.leaveTrail(lastMouseMove.x, lastMouseMove.y) } } } diff --git a/frontend/app/player/web/managers/trail.module.css b/frontend/app/player/web/managers/trail.module.css new file mode 100644 index 000000000..418c71119 --- /dev/null +++ b/frontend/app/player/web/managers/trail.module.css @@ -0,0 +1,9 @@ +.canvas { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 11; + pointer-events: none; +} \ No newline at end of file