Skip to content

Commit

Permalink
feat(freehand): add FreehandSmoother to optimize freehand curve (#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
pubuzhixing8 authored Dec 17, 2024
1 parent 0e302aa commit 0bc6a20
Show file tree
Hide file tree
Showing 3 changed files with 151 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export class FreehandGenerator extends Generator<Freehand> {
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');
Expand Down
136 changes: 136 additions & 0 deletions packages/drawnix/src/plugins/freehand/smoother.ts
Original file line number Diff line number Diff line change
@@ -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<FreehandSmootherConfig> = {}) {
// 默认配置
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<FreehandSmootherConfig>): void {
this.config = {
...this.config,
...newConfig,
};
}

// 重置状态
public reset(): void {
this.points = [];
this.velocities = [];
}

// 获取当前点的数量
public getPointCount(): number {
return this.points.length;
}
}
33 changes: 14 additions & 19 deletions packages/drawnix/src/plugins/freehand/with-freehand-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@ import {
PlaitBoard,
Point,
Transforms,
distanceBetweenPointAndPoint,
throttleRAF,
toHostPoint,
toViewBoxPoint,
} from '@plait/core';
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;
Expand All @@ -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) {
Expand All @@ -37,37 +36,33 @@ export const withFreehandCreate = (board: PlaitBoard) => {
temporaryElement = null;
isDrawing = false;
points = [];
previousScreenPoint = null;
smoother.reset();
};

board.pointerDown = (event: PointerEvent) => {
const freehandPointers = getFreehandPointers();
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;
Expand Down

0 comments on commit 0bc6a20

Please sign in to comment.