|
@@ -1,310 +0,0 @@
|
|
|
-import { LaserPointer } from "@excalidraw/laser-pointer";
|
|
|
-
|
|
|
-import { sceneCoordsToViewportCoords } from "../../utils";
|
|
|
-import App from "../App";
|
|
|
-import { getClientColor } from "../../clients";
|
|
|
-import { SocketId } from "../../types";
|
|
|
-
|
|
|
-// decay time in milliseconds
|
|
|
-const DECAY_TIME = 1000;
|
|
|
-// length of line in points before it starts decaying
|
|
|
-const DECAY_LENGTH = 50;
|
|
|
-
|
|
|
-const average = (a: number, b: number) => (a + b) / 2;
|
|
|
-function getSvgPathFromStroke(points: number[][], closed = true) {
|
|
|
- const len = points.length;
|
|
|
-
|
|
|
- if (len < 4) {
|
|
|
- return ``;
|
|
|
- }
|
|
|
-
|
|
|
- let a = points[0];
|
|
|
- let b = points[1];
|
|
|
- const c = points[2];
|
|
|
-
|
|
|
- let result = `M${a[0].toFixed(2)},${a[1].toFixed(2)} Q${b[0].toFixed(
|
|
|
- 2,
|
|
|
- )},${b[1].toFixed(2)} ${average(b[0], c[0]).toFixed(2)},${average(
|
|
|
- b[1],
|
|
|
- c[1],
|
|
|
- ).toFixed(2)} T`;
|
|
|
-
|
|
|
- for (let i = 2, max = len - 1; i < max; i++) {
|
|
|
- a = points[i];
|
|
|
- b = points[i + 1];
|
|
|
- result += `${average(a[0], b[0]).toFixed(2)},${average(a[1], b[1]).toFixed(
|
|
|
- 2,
|
|
|
- )} `;
|
|
|
- }
|
|
|
-
|
|
|
- if (closed) {
|
|
|
- result += "Z";
|
|
|
- }
|
|
|
-
|
|
|
- return result;
|
|
|
-}
|
|
|
-
|
|
|
-declare global {
|
|
|
- interface Window {
|
|
|
- LPM: LaserPathManager;
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-function easeOutCubic(t: number) {
|
|
|
- return 1 - Math.pow(1 - t, 3);
|
|
|
-}
|
|
|
-
|
|
|
-function instantiateCollabolatorState(): CollabolatorState {
|
|
|
- return {
|
|
|
- currentPath: undefined,
|
|
|
- finishedPaths: [],
|
|
|
- lastPoint: [-10000, -10000],
|
|
|
- svg: document.createElementNS("http://www.w3.org/2000/svg", "path"),
|
|
|
- };
|
|
|
-}
|
|
|
-
|
|
|
-function instantiatePath() {
|
|
|
- LaserPointer.constants.cornerDetectionMaxAngle = 70;
|
|
|
-
|
|
|
- return new LaserPointer({
|
|
|
- simplify: 0,
|
|
|
- streamline: 0.4,
|
|
|
- sizeMapping: (c) => {
|
|
|
- const pt = DECAY_TIME;
|
|
|
- const pl = DECAY_LENGTH;
|
|
|
- const t = Math.max(0, 1 - (performance.now() - c.pressure) / pt);
|
|
|
- const l = (pl - Math.min(pl, c.totalLength - c.currentIndex)) / pl;
|
|
|
-
|
|
|
- return Math.min(easeOutCubic(l), easeOutCubic(t));
|
|
|
- },
|
|
|
- });
|
|
|
-}
|
|
|
-
|
|
|
-type CollabolatorState = {
|
|
|
- currentPath: LaserPointer | undefined;
|
|
|
- finishedPaths: LaserPointer[];
|
|
|
- lastPoint: [number, number];
|
|
|
- svg: SVGPathElement;
|
|
|
-};
|
|
|
-
|
|
|
-export class LaserPathManager {
|
|
|
- private ownState: CollabolatorState;
|
|
|
- private collaboratorsState: Map<SocketId, CollabolatorState> = new Map();
|
|
|
-
|
|
|
- private rafId: number | undefined;
|
|
|
- private isDrawing = false;
|
|
|
- private container: SVGSVGElement | undefined;
|
|
|
-
|
|
|
- constructor(private app: App) {
|
|
|
- this.ownState = instantiateCollabolatorState();
|
|
|
- }
|
|
|
-
|
|
|
- destroy() {
|
|
|
- this.stop();
|
|
|
- this.isDrawing = false;
|
|
|
- this.ownState = instantiateCollabolatorState();
|
|
|
- this.collaboratorsState = new Map();
|
|
|
- }
|
|
|
-
|
|
|
- startPath(x: number, y: number) {
|
|
|
- this.ownState.currentPath = instantiatePath();
|
|
|
- this.ownState.currentPath.addPoint([x, y, performance.now()]);
|
|
|
- this.updatePath(this.ownState);
|
|
|
- }
|
|
|
-
|
|
|
- addPointToPath(x: number, y: number) {
|
|
|
- if (this.ownState.currentPath) {
|
|
|
- this.ownState.currentPath?.addPoint([x, y, performance.now()]);
|
|
|
- this.updatePath(this.ownState);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- endPath() {
|
|
|
- if (this.ownState.currentPath) {
|
|
|
- this.ownState.currentPath.close();
|
|
|
- this.ownState.finishedPaths.push(this.ownState.currentPath);
|
|
|
- this.updatePath(this.ownState);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private updatePath(state: CollabolatorState) {
|
|
|
- this.isDrawing = true;
|
|
|
-
|
|
|
- if (!this.isRunning) {
|
|
|
- this.start();
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- private isRunning = false;
|
|
|
-
|
|
|
- start(svg?: SVGSVGElement) {
|
|
|
- if (svg) {
|
|
|
- this.container = svg;
|
|
|
- this.container.appendChild(this.ownState.svg);
|
|
|
- }
|
|
|
-
|
|
|
- this.stop();
|
|
|
- this.isRunning = true;
|
|
|
- this.loop();
|
|
|
- }
|
|
|
-
|
|
|
- stop() {
|
|
|
- this.isRunning = false;
|
|
|
- if (this.rafId) {
|
|
|
- cancelAnimationFrame(this.rafId);
|
|
|
- }
|
|
|
- this.rafId = undefined;
|
|
|
- }
|
|
|
-
|
|
|
- loop() {
|
|
|
- this.rafId = requestAnimationFrame(this.loop.bind(this));
|
|
|
-
|
|
|
- this.updateCollabolatorsState();
|
|
|
-
|
|
|
- if (this.isDrawing) {
|
|
|
- this.update();
|
|
|
- } else {
|
|
|
- this.isRunning = false;
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- draw(path: LaserPointer) {
|
|
|
- const stroke = path
|
|
|
- .getStrokeOutline(path.options.size / this.app.state.zoom.value)
|
|
|
- .map(([x, y]) => {
|
|
|
- const result = sceneCoordsToViewportCoords(
|
|
|
- { sceneX: x, sceneY: y },
|
|
|
- this.app.state,
|
|
|
- );
|
|
|
-
|
|
|
- return [result.x, result.y];
|
|
|
- });
|
|
|
-
|
|
|
- return getSvgPathFromStroke(stroke, true);
|
|
|
- }
|
|
|
-
|
|
|
- updateCollabolatorsState() {
|
|
|
- if (!this.container || !this.app.state.collaborators.size) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- for (const [key, collabolator] of this.app.state.collaborators.entries()) {
|
|
|
- if (!this.collaboratorsState.has(key)) {
|
|
|
- const state = instantiateCollabolatorState();
|
|
|
- this.container.appendChild(state.svg);
|
|
|
- this.collaboratorsState.set(key, state);
|
|
|
-
|
|
|
- this.updatePath(state);
|
|
|
- }
|
|
|
-
|
|
|
- const state = this.collaboratorsState.get(key)!;
|
|
|
-
|
|
|
- if (collabolator.pointer && collabolator.pointer.tool === "laser") {
|
|
|
- if (collabolator.button === "down" && state.currentPath === undefined) {
|
|
|
- state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
|
- state.currentPath = instantiatePath();
|
|
|
- state.currentPath.addPoint([
|
|
|
- collabolator.pointer.x,
|
|
|
- collabolator.pointer.y,
|
|
|
- performance.now(),
|
|
|
- ]);
|
|
|
-
|
|
|
- this.updatePath(state);
|
|
|
- }
|
|
|
-
|
|
|
- if (collabolator.button === "down" && state.currentPath !== undefined) {
|
|
|
- if (
|
|
|
- collabolator.pointer.x !== state.lastPoint[0] ||
|
|
|
- collabolator.pointer.y !== state.lastPoint[1]
|
|
|
- ) {
|
|
|
- state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
|
- state.currentPath.addPoint([
|
|
|
- collabolator.pointer.x,
|
|
|
- collabolator.pointer.y,
|
|
|
- performance.now(),
|
|
|
- ]);
|
|
|
-
|
|
|
- this.updatePath(state);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (collabolator.button === "up" && state.currentPath !== undefined) {
|
|
|
- state.lastPoint = [collabolator.pointer.x, collabolator.pointer.y];
|
|
|
- state.currentPath.addPoint([
|
|
|
- collabolator.pointer.x,
|
|
|
- collabolator.pointer.y,
|
|
|
- performance.now(),
|
|
|
- ]);
|
|
|
- state.currentPath.close();
|
|
|
-
|
|
|
- state.finishedPaths.push(state.currentPath);
|
|
|
- state.currentPath = undefined;
|
|
|
-
|
|
|
- this.updatePath(state);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- update() {
|
|
|
- if (!this.container) {
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- let somePathsExist = false;
|
|
|
-
|
|
|
- for (const [key, state] of this.collaboratorsState.entries()) {
|
|
|
- if (!this.app.state.collaborators.has(key)) {
|
|
|
- state.svg.remove();
|
|
|
- this.collaboratorsState.delete(key);
|
|
|
- continue;
|
|
|
- }
|
|
|
-
|
|
|
- state.finishedPaths = state.finishedPaths.filter((path) => {
|
|
|
- const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
|
|
-
|
|
|
- return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
|
|
- });
|
|
|
-
|
|
|
- let paths = state.finishedPaths.map((path) => this.draw(path)).join(" ");
|
|
|
-
|
|
|
- if (state.currentPath) {
|
|
|
- paths += ` ${this.draw(state.currentPath)}`;
|
|
|
- }
|
|
|
-
|
|
|
- if (paths.trim()) {
|
|
|
- somePathsExist = true;
|
|
|
- }
|
|
|
-
|
|
|
- state.svg.setAttribute("d", paths);
|
|
|
- state.svg.setAttribute("fill", getClientColor(key));
|
|
|
- }
|
|
|
-
|
|
|
- this.ownState.finishedPaths = this.ownState.finishedPaths.filter((path) => {
|
|
|
- const lastPoint = path.originalPoints[path.originalPoints.length - 1];
|
|
|
-
|
|
|
- return !(lastPoint && lastPoint[2] < performance.now() - DECAY_TIME);
|
|
|
- });
|
|
|
-
|
|
|
- let paths = this.ownState.finishedPaths
|
|
|
- .map((path) => this.draw(path))
|
|
|
- .join(" ");
|
|
|
-
|
|
|
- if (this.ownState.currentPath) {
|
|
|
- paths += ` ${this.draw(this.ownState.currentPath)}`;
|
|
|
- }
|
|
|
-
|
|
|
- paths = paths.trim();
|
|
|
-
|
|
|
- if (paths) {
|
|
|
- somePathsExist = true;
|
|
|
- }
|
|
|
-
|
|
|
- this.ownState.svg.setAttribute("d", paths);
|
|
|
- this.ownState.svg.setAttribute("fill", "red");
|
|
|
-
|
|
|
- if (!somePathsExist) {
|
|
|
- this.isDrawing = false;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|