Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(freehand): add FreehandSmoother to optimize freehand curve #62

Merged
merged 1 commit into from
Dec 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading