From 0bc6a20120a6c8c2bf3fc745c2a679a868b2a3a1 Mon Sep 17 00:00:00 2001 From: pubuzhixing8 Date: Tue, 17 Dec 2024 19:22:03 +0800 Subject: [PATCH] feat(freehand): add FreehandSmoother to optimize freehand curve (#62) --- .../plugins/freehand/freehand.generator.ts | 2 +- .../drawnix/src/plugins/freehand/smoother.ts | 136 ++++++++++++++++++ .../plugins/freehand/with-freehand-create.ts | 33 ++--- 3 files changed, 151 insertions(+), 20 deletions(-) create mode 100644 packages/drawnix/src/plugins/freehand/smoother.ts diff --git a/packages/drawnix/src/plugins/freehand/freehand.generator.ts b/packages/drawnix/src/plugins/freehand/freehand.generator.ts index 01457e0..e8de926 100644 --- a/packages/drawnix/src/plugins/freehand/freehand.generator.ts +++ b/packages/drawnix/src/plugins/freehand/freehand.generator.ts @@ -8,7 +8,7 @@ export class FreehandGenerator extends Generator { protected draw(element: Freehand): SVGGElement | undefined { const option: Options = { ...DefaultFreehand }; const g = PlaitBoard.getRoughSVG(this.board).curve( - gaussianSmooth(element.points, 1.2, 4), + gaussianSmooth(element.points, 3, 9), option ); setStrokeLinecap(g, 'round'); diff --git a/packages/drawnix/src/plugins/freehand/smoother.ts b/packages/drawnix/src/plugins/freehand/smoother.ts new file mode 100644 index 0000000..328e621 --- /dev/null +++ b/packages/drawnix/src/plugins/freehand/smoother.ts @@ -0,0 +1,136 @@ +import { Point } from '@plait/core'; + +export interface FreehandSmootherConfig { + smoothing: number; // 基础平滑系数 + velocityWeight: number; // 速度权重 + curvatureWeight: number; // 曲率权重 + maxPoints: number; // 最大历史点数 + minDistance: number; // 最小距离阈值 +} + +export class FreehandSmoother { + private config: FreehandSmootherConfig; + private points: Point[]; + private velocities: number[]; + + constructor(options: Partial = {}) { + // 默认配置 + const defaultConfig: FreehandSmootherConfig = { + smoothing: 0.9, // 接近 1,最大平滑度 + velocityWeight: 0.1, // 很小的速度影响,保持一致的平滑度 + curvatureWeight: 0.1, // 很小的曲率影响,让转角也变得圆滑 + maxPoints: 10, // 更多的历史点参与计算 + minDistance: 1.0, // 强力过滤小幅度抖动 + }; + + this.config = { + ...defaultConfig, + ...options, + }; + + this.points = []; + this.velocities = []; + } + + public smoothPoint(point: Point): Point { + this.points.push(point); + if (this.points.length > this.config.maxPoints) { + this.points.shift(); + } + + if (this.points.length < 2) return point; + + // 计算速度 + const velocity = this.calculateVelocity(point); + this.velocities.push(velocity); + if (this.velocities.length > this.config.maxPoints) { + this.velocities.shift(); + } + + // 计算曲率 + const curvature = this.calculateCurvature(); + + // 动态调整平滑系数 + const adaptiveSmoothing = this.getAdaptiveSmoothing(velocity, curvature); + + let smoothX = point[0]; + let smoothY = point[1]; + let totalWeight = 1; + let weight = 1; + + // 指数加权移动平均 + for (let i = this.points.length - 2; i >= 0; i--) { + weight *= adaptiveSmoothing; + totalWeight += weight; + + smoothX += this.points[i][0] * weight; + smoothY += this.points[i][1] * weight; + } + + return [smoothX / totalWeight, smoothY / totalWeight]; + } + + private calculateVelocity(point: Point): number { + if (this.points.length < 2) return 0; + + const prev = this.points[this.points.length - 2]; + const dx = point[0] - prev[0]; + const dy = point[1] - prev[1]; + const dt = 1; // 假设时间间隔恒定 + + return Math.sqrt(dx * dx + dy * dy) / dt; + } + + private calculateCurvature(): number { + if (this.points.length < 3) return 0; + + const p1 = this.points[this.points.length - 3]; + const p2 = this.points[this.points.length - 2]; + const p3 = this.points[this.points.length - 1]; + + // 使用三点法计算曲率 + const dx1 = p2[0] - p1[0]; + const dy1 = p2[1] - p1[1]; + const dx2 = p3[0] - p2[0]; + const dy2 = p3[1] - p2[1]; + + // 使用叉积估算曲率 + const cross = dx1 * dy2 - dy1 * dx2; + const velocity = Math.sqrt(dx1 * dx1 + dy1 * dy1); + + return Math.abs(cross) / (velocity * velocity + this.config.minDistance); + } + + private getAdaptiveSmoothing(velocity: number, curvature: number): number { + // 基于速度和曲率动态调整平滑系数 + const velocityFactor = Math.exp(-velocity * this.config.velocityWeight); + const curvatureFactor = Math.exp(-curvature * this.config.curvatureWeight); + + // 结合基础平滑系数和动态因子 + return this.config.smoothing * velocityFactor * curvatureFactor; + } + + // 获取当前配置 + public getConfig(): FreehandSmootherConfig { + return { ...this.config }; + } + + // 更新配置 + public updateConfig(newConfig: Partial): void { + this.config = { + ...this.config, + ...newConfig, + }; + } + + // 重置状态 + public reset(): void { + this.points = []; + this.velocities = []; + } + + // 获取当前点的数量 + public getPointCount(): number { + return this.points.length; + } +} diff --git a/packages/drawnix/src/plugins/freehand/with-freehand-create.ts b/packages/drawnix/src/plugins/freehand/with-freehand-create.ts index bfab490..3c55a68 100644 --- a/packages/drawnix/src/plugins/freehand/with-freehand-create.ts +++ b/packages/drawnix/src/plugins/freehand/with-freehand-create.ts @@ -2,8 +2,6 @@ import { PlaitBoard, Point, Transforms, - distanceBetweenPointAndPoint, - throttleRAF, toHostPoint, toViewBoxPoint, } from '@plait/core'; @@ -11,6 +9,7 @@ import { isDrawingMode } from '@plait/common'; import { createFreehandElement, getFreehandPointers } from './utils'; import { Freehand, FreehandShape } from './type'; import { FreehandGenerator } from './freehand.generator'; +import { FreehandSmoother } from './smoother'; export const withFreehandCreate = (board: PlaitBoard) => { const { pointerDown, pointerMove, pointerUp, globalPointerUp } = board; @@ -21,9 +20,9 @@ export const withFreehandCreate = (board: PlaitBoard) => { const generator = new FreehandGenerator(board); - let temporaryElement: Freehand | null = null; + const smoother = new FreehandSmoother(); - let previousScreenPoint: Point | null = null; + let temporaryElement: Freehand | null = null; const complete = (cancel?: boolean) => { if (isDrawing) { @@ -37,7 +36,7 @@ export const withFreehandCreate = (board: PlaitBoard) => { temporaryElement = null; isDrawing = false; points = []; - previousScreenPoint = null; + smoother.reset(); }; board.pointerDown = (event: PointerEvent) => { @@ -45,29 +44,25 @@ export const withFreehandCreate = (board: PlaitBoard) => { const isFreehandPointer = PlaitBoard.isInPointer(board, freehandPointers); if (isFreehandPointer && isDrawingMode(board)) { isDrawing = true; - const point = toViewBoxPoint(board, toHostPoint(board, event.x, event.y)); + const originPoint: Point = [event.x, event.y]; + const smoothingPoint = smoother.smoothPoint(originPoint); + const point = toViewBoxPoint( + board, + toHostPoint(board, smoothingPoint[0], smoothingPoint[1]) + ); points.push(point); - previousScreenPoint = [event.x, event.y]; } pointerDown(event); }; board.pointerMove = (event: PointerEvent) => { - if (isDrawing && previousScreenPoint) { - const distance = distanceBetweenPointAndPoint( - previousScreenPoint[0], - previousScreenPoint[1], - event.x, - event.y - ); - if (distance <= 0.5) { - return; - } - previousScreenPoint = [event.x, event.y]; + if (isDrawing) { + const originPoint: Point = [event.x, event.y]; + const smoothingPoint = smoother.smoothPoint(originPoint); generator?.destroy(); const newPoint = toViewBoxPoint( board, - toHostPoint(board, event.x, event.y) + toHostPoint(board, smoothingPoint[0], smoothingPoint[1]) ); points.push(newPoint); const pointer = PlaitBoard.getPointer(board) as FreehandShape;