mirror of
https://github.com/excalidraw/excalidraw.git
synced 2025-05-03 10:00:07 -04:00
* lasso without 'real' shape detection * select a single linear el * improve ux * feed segments to worker * simplify path threshold adaptive to zoom * add a tiny threshold for checks * refactor code * lasso tests * fix: ts * do not capture lasso tool * try worker-loader in next config * update config * refactor * lint * feat: show active tool when using "more tools" * keep lasso if selected from toolbar * fix incorrect checks for resetting to selection * shift for additive selection * bound text related fixes * lint * keep alt toggled lasso selection if shift pressed * fix regression * fix 'dead' lassos * lint * use workerpool and polyfill * fix worker bundled with window related code * refactor * add file extension for worker constructor error * another attempt at constructor error * attempt at build issue * attempt with dynamic import * test not importing from math * narrow down imports * Reusing existing workers infrastructure (fallback to the main thread, type-safety) * Points on curve inside the shared chunk * Give up on experimental code splitting * Remove potentially unnecessary optimisation * Removing workers as the complexit is much worse, while perf. does not seem to be much better * fix selecting text containers and containing frames together * render fill directly from animated trail * do not re-render static when setting selected element ids in lasso * remove unnecessary property * tweak trail animation * slice points to remove notch * always start alt-lasso from initial point * revert build & worker changes (unused) * remove `lasso` from `hasStrokeColor` * label change * remove unused props * remove unsafe optimization * snaps --------- Co-authored-by: dwelle <5153846+dwelle@users.noreply.github.com> Co-authored-by: Marcel Mraz <marcel@excalidraw.com>
203 lines
5.2 KiB
TypeScript
203 lines
5.2 KiB
TypeScript
import { LaserPointer } from "@excalidraw/laser-pointer";
|
|
|
|
import {
|
|
SVG_NS,
|
|
getSvgPathFromStroke,
|
|
sceneCoordsToViewportCoords,
|
|
} from "@excalidraw/common";
|
|
|
|
import type { LaserPointerOptions } from "@excalidraw/laser-pointer";
|
|
|
|
import type { AnimationFrameHandler } from "./animation-frame-handler";
|
|
import type App from "./components/App";
|
|
import type { AppState } from "./types";
|
|
|
|
export interface Trail {
|
|
start(container: SVGSVGElement): void;
|
|
stop(): void;
|
|
|
|
startPath(x: number, y: number): void;
|
|
addPointToPath(x: number, y: number): void;
|
|
endPath(): void;
|
|
}
|
|
|
|
export interface AnimatedTrailOptions {
|
|
fill: (trail: AnimatedTrail) => string;
|
|
stroke?: (trail: AnimatedTrail) => string;
|
|
animateTrail?: boolean;
|
|
}
|
|
|
|
export class AnimatedTrail implements Trail {
|
|
private currentTrail?: LaserPointer;
|
|
private pastTrails: LaserPointer[] = [];
|
|
|
|
private container?: SVGSVGElement;
|
|
private trailElement: SVGPathElement;
|
|
private trailAnimation?: SVGAnimateElement;
|
|
|
|
constructor(
|
|
private animationFrameHandler: AnimationFrameHandler,
|
|
protected app: App,
|
|
private options: Partial<LaserPointerOptions> &
|
|
Partial<AnimatedTrailOptions>,
|
|
) {
|
|
this.animationFrameHandler.register(this, this.onFrame.bind(this));
|
|
|
|
this.trailElement = document.createElementNS(SVG_NS, "path");
|
|
if (this.options.animateTrail) {
|
|
this.trailAnimation = document.createElementNS(SVG_NS, "animate");
|
|
// TODO: make this configurable
|
|
this.trailAnimation.setAttribute("attributeName", "stroke-dashoffset");
|
|
this.trailElement.setAttribute("stroke-dasharray", "7 7");
|
|
this.trailElement.setAttribute("stroke-dashoffset", "10");
|
|
this.trailAnimation.setAttribute("from", "0");
|
|
this.trailAnimation.setAttribute("to", `-14`);
|
|
this.trailAnimation.setAttribute("dur", "0.3s");
|
|
this.trailElement.appendChild(this.trailAnimation);
|
|
}
|
|
}
|
|
|
|
get hasCurrentTrail() {
|
|
return !!this.currentTrail;
|
|
}
|
|
|
|
hasLastPoint(x: number, y: number) {
|
|
if (this.currentTrail) {
|
|
const len = this.currentTrail.originalPoints.length;
|
|
return (
|
|
this.currentTrail.originalPoints[len - 1][0] === x &&
|
|
this.currentTrail.originalPoints[len - 1][1] === y
|
|
);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
start(container?: SVGSVGElement) {
|
|
if (container) {
|
|
this.container = container;
|
|
}
|
|
|
|
if (this.trailElement.parentNode !== this.container && this.container) {
|
|
this.container.appendChild(this.trailElement);
|
|
}
|
|
|
|
this.animationFrameHandler.start(this);
|
|
}
|
|
|
|
stop() {
|
|
this.animationFrameHandler.stop(this);
|
|
|
|
if (this.trailElement.parentNode === this.container) {
|
|
this.container?.removeChild(this.trailElement);
|
|
}
|
|
}
|
|
|
|
startPath(x: number, y: number) {
|
|
this.currentTrail = new LaserPointer(this.options);
|
|
|
|
this.currentTrail.addPoint([x, y, performance.now()]);
|
|
|
|
this.update();
|
|
}
|
|
|
|
addPointToPath(x: number, y: number) {
|
|
if (this.currentTrail) {
|
|
this.currentTrail.addPoint([x, y, performance.now()]);
|
|
this.update();
|
|
}
|
|
}
|
|
|
|
endPath() {
|
|
if (this.currentTrail) {
|
|
this.currentTrail.close();
|
|
this.currentTrail.options.keepHead = false;
|
|
this.pastTrails.push(this.currentTrail);
|
|
this.currentTrail = undefined;
|
|
this.update();
|
|
}
|
|
}
|
|
|
|
getCurrentTrail() {
|
|
return this.currentTrail;
|
|
}
|
|
|
|
clearTrails() {
|
|
this.pastTrails = [];
|
|
this.currentTrail = undefined;
|
|
this.update();
|
|
}
|
|
|
|
private update() {
|
|
this.pastTrails = [];
|
|
this.start();
|
|
if (this.trailAnimation) {
|
|
this.trailAnimation.setAttribute("begin", "indefinite");
|
|
this.trailAnimation.setAttribute("repeatCount", "indefinite");
|
|
}
|
|
}
|
|
|
|
private onFrame() {
|
|
const paths: string[] = [];
|
|
|
|
for (const trail of this.pastTrails) {
|
|
paths.push(this.drawTrail(trail, this.app.state));
|
|
}
|
|
|
|
if (this.currentTrail) {
|
|
const currentPath = this.drawTrail(this.currentTrail, this.app.state);
|
|
|
|
paths.push(currentPath);
|
|
}
|
|
|
|
this.pastTrails = this.pastTrails.filter((trail) => {
|
|
return trail.getStrokeOutline().length !== 0;
|
|
});
|
|
|
|
if (paths.length === 0) {
|
|
this.stop();
|
|
}
|
|
|
|
const svgPaths = paths.join(" ").trim();
|
|
|
|
this.trailElement.setAttribute("d", svgPaths);
|
|
if (this.trailAnimation) {
|
|
this.trailElement.setAttribute(
|
|
"fill",
|
|
(this.options.fill ?? (() => "black"))(this),
|
|
);
|
|
this.trailElement.setAttribute(
|
|
"stroke",
|
|
(this.options.stroke ?? (() => "black"))(this),
|
|
);
|
|
} else {
|
|
this.trailElement.setAttribute(
|
|
"fill",
|
|
(this.options.fill ?? (() => "black"))(this),
|
|
);
|
|
}
|
|
}
|
|
|
|
private drawTrail(trail: LaserPointer, state: AppState): string {
|
|
const _stroke = trail
|
|
.getStrokeOutline(trail.options.size / state.zoom.value)
|
|
.map(([x, y]) => {
|
|
const result = sceneCoordsToViewportCoords(
|
|
{ sceneX: x, sceneY: y },
|
|
state,
|
|
);
|
|
|
|
return [result.x, result.y];
|
|
});
|
|
|
|
const stroke = this.trailAnimation
|
|
? _stroke.slice(
|
|
// slicing from 6th point to get rid of the initial notch type of thing
|
|
Math.min(_stroke.length, 6),
|
|
_stroke.length / 2,
|
|
)
|
|
: _stroke;
|
|
|
|
return getSvgPathFromStroke(stroke, true);
|
|
}
|
|
}
|