From 90f5a3fe0d4ad74eb17e3e1d076eae74e7e6ac28 Mon Sep 17 00:00:00 2001 From: Samuel MT Date: Wed, 6 Nov 2024 23:22:58 -0300 Subject: [PATCH] use catmull-rom algorithm for drawing gesture --- package-lock.json | 8 +- package.json | 3 +- src/client/render/attachGesture.ts | 40 +++++---- src/client/util/geometry/index.ts | 81 +++++++++++++++++++ .../component/app/Editor/Component.ts | 2 +- 5 files changed, 103 insertions(+), 31 deletions(-) diff --git a/package-lock.json b/package-lock.json index 96f5576933..c7a5355e6d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,8 +18,7 @@ "fuzzy": "0.1.3", "glob": "7.1.6", "http-errors": "1.8.0", - "jsdom": "24.0.0", - "perfect-freehand": "1.0.16" + "jsdom": "24.0.0" }, "bin": { "unit": "lib/server/index.js" @@ -5911,11 +5910,6 @@ "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", "dev": true }, - "node_modules/perfect-freehand": { - "version": "1.0.16", - "resolved": "https://registry.npmjs.org/perfect-freehand/-/perfect-freehand-1.0.16.tgz", - "integrity": "sha512-D4+avUeR8CHSl2vaPbPYX/dNpSMRYO3VOFp7qSSc+LRkSgzQbLATVnXosy7VxtsSHEh1C5t8K8sfmo0zCVnfWQ==" - }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", diff --git a/package.json b/package.json index 244b8de110..4d3433f9f0 100644 --- a/package.json +++ b/package.json @@ -55,8 +55,7 @@ "fuzzy": "0.1.3", "glob": "7.1.6", "http-errors": "1.8.0", - "jsdom": "24.0.0", - "perfect-freehand": "1.0.16" + "jsdom": "24.0.0" }, "devDependencies": { "@types/chrome": "0.0.178", diff --git a/src/client/render/attachGesture.ts b/src/client/render/attachGesture.ts index 13d179f372..e757e507c8 100644 --- a/src/client/render/attachGesture.ts +++ b/src/client/render/attachGesture.ts @@ -1,28 +1,23 @@ -import { getStroke } from 'perfect-freehand' import { System } from '../../system' import { Unlisten } from '../../types/Unlisten' import { namespaceURI } from '../component/namespaceURI' import { _addEventListener } from '../event' import { UnitPointerEvent } from '../event/pointer' +import { catmullRomSplineSegment } from '../util/geometry' import { Point } from '../util/geometry/types' -function getSvgPathFromStroke(stroke): string { +function pathFromSpline(stroke: number[][]): string { if (!stroke.length) { return '' } - const d = stroke.reduce( - (acc, [x0, y0], i, arr) => { - const [x1, y1] = arr[(i + 1) % arr.length] - acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2) - return acc - }, - ['M', ...stroke[0], 'Q'] - ) + let d = `M ${stroke[0][0]} ${stroke[0][1]}` - d.push('Z') + for (let i = 1; i < stroke.length; i++) { + d = d + ` L ${stroke[i][0]} ${stroke[i][1]}` + } - return d.join(' ') + return d } const STROKE_OPT = { @@ -60,26 +55,27 @@ export function attachGesture(system: System): void { } = {}, callback: (event: PointerEvent, track: Point[]) => void ): Unlisten => { - const { pointerId, screenX, screenY, pageX, pageY } = event - - // svg.setPointerCapture(pointerId) + const { pointerId, pageX, pageY } = event const path = createElementNS(namespaceURI, 'path') svg.appendChild(path) - const { lineWidth = 2, strokeStyle = '#d1d1d1' } = opt + const { lineWidth = 10, strokeStyle = '#d1d1d1' } = opt const color = strokeStyle path.style.stroke = color path.style.strokeWidth = `${lineWidth}px` - path.style.fill = color + path.style.fill = 'none' + path.style.strokeLinecap = 'round' let active = true const track: Point[] = [{ x: pageX, y: pageY }] + let d = '' + const pointerMoveListener = (_event: PointerEvent) => { // console.log('attachGesture', 'pointerMoveListener') @@ -88,13 +84,15 @@ export function attachGesture(system: System): void { if (_pointerId === pointerId) { const { pageX, pageY } = _event - const outline = getStroke(track, STROKE_OPT) + track.push({ x: pageX, y: pageY }) - const d = getSvgPathFromStroke(outline) + if (track.length > 3) { + const segment = catmullRomSplineSegment(track.slice(-4)) - path.setAttribute('d', d) + d += pathFromSpline(segment) + } - track.push({ x: pageX, y: pageY }) + path.setAttribute('d', d) } } diff --git a/src/client/util/geometry/index.ts b/src/client/util/geometry/index.ts index 9b2865ef61..750e1042fb 100644 --- a/src/client/util/geometry/index.ts +++ b/src/client/util/geometry/index.ts @@ -773,3 +773,84 @@ export const roundPoint = (position: Position): Position => { y: Math.round(position.y), } } + +export function catmullRom( + p0: number, + p1: number, + p2: number, + p3: number, + t: number +): number { + const tensionFactor = 0.5 + const startingPointWeight = 2 + const linearTerm1 = -1 + const linearTerm2 = 1 + const quadraticTerm1 = 2 + const quadraticTerm2 = -5 + const quadraticTerm3 = 4 + const quadraticTerm4 = -1 + const cubicTerm1 = -1 + const cubicTerm2 = 3 + const cubicTerm3 = -3 + const cubicTerm4 = 1 + + const t2 = t * t + const t3 = t2 * t + + return ( + tensionFactor * + (startingPointWeight * p1 + + (linearTerm1 * p0 + linearTerm2 * p2) * t + + (quadraticTerm1 * p0 + + quadraticTerm2 * p1 + + quadraticTerm3 * p2 + + quadraticTerm4 * p3) * + t2 + + (cubicTerm1 * p0 + cubicTerm2 * p1 + cubicTerm3 * p2 + cubicTerm4 * p3) * + t3) + ) +} + +export function catmullRomSpline(points: Point[]): number[][] { + if (points.length < 4) return + + const spline: number[][] = [] + + spline.push([points[0].x, points[0].y]) + + for (let i = 0; i < points.length - 3; i++) { + const fourPoints = points.slice(i, i + 4) + + const segment = catmullRomSplineSegment(fourPoints) + + for (let i = 0; i < segment.length; i++) { + spline.push(segment[i]) + } + } + + return spline +} + +export function catmullRomSplineSegment( + lastFourPoints: Point[], + step: number = 0.1 +): number[][] { + if (lastFourPoints.length < 4) { + return [] + } + + const spline: number[][] = [] + + spline.push([lastFourPoints[0].x, lastFourPoints[0].y]) + + const [p0, p1, p2, p3] = lastFourPoints + + for (let t = 0; t <= 1; t += step) { + const x = catmullRom(p0.x, p1.x, p2.x, p3.x, t) + const y = catmullRom(p0.y, p1.y, p2.y, p3.y, t) + + spline.push([x, y]) + } + + return spline +} diff --git a/src/system/platform/component/app/Editor/Component.ts b/src/system/platform/component/app/Editor/Component.ts index 436f28c1c2..6bc7698ba7 100644 --- a/src/system/platform/component/app/Editor/Component.ts +++ b/src/system/platform/component/app/Editor/Component.ts @@ -52614,7 +52614,7 @@ export class Editor_ extends Element { const color = this._get_color() const strokeStyle = getThemeModeColor($theme, this._mode, color) - const lineWidth = 2 + const lineWidth = 4 const unlisten = captureGesture( event,