Skip to content

Commit f10799d

Browse files
committed
feat: Add perfect-freehand in free draw
1 parent 173c2ff commit f10799d

File tree

7 files changed

+127
-39
lines changed

7 files changed

+127
-39
lines changed

apps/collabydraw/canvas-engine/CanvasEngine.ts

Lines changed: 51 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import { RoughCanvas } from "roughjs/canvas";
4242
import { Options } from "roughjs/core";
4343
import rough from "roughjs/bin/rough";
4444
import { getFontSize, getLineHeight } from "@/utils/textUtils";
45+
import { generateFreeDrawPath } from "./RenderElements";
4546

4647
export class CanvasEngine {
4748
private canvas: HTMLCanvasElement;
@@ -483,8 +484,8 @@ export class CanvasEngine {
483484
shape.roughStyle,
484485
true
485486
);
486-
} else if (shape.type === "pen") {
487-
this.drawPencil(
487+
} else if (shape.type === "free-draw") {
488+
this.drawFreeDraw(
488489
shape.points,
489490
shape.strokeFill,
490491
shape.bgFill,
@@ -521,7 +522,7 @@ export class CanvasEngine {
521522

522523
mouseUpHandler = (e: MouseEvent) => {
523524
if (
524-
this.activeTool !== "pen" &&
525+
this.activeTool !== "free-draw" &&
525526
this.activeTool !== "eraser" &&
526527
this.activeTool !== "line" &&
527528
this.activeTool !== "arrow"
@@ -675,13 +676,13 @@ export class CanvasEngine {
675676
};
676677
break;
677678

678-
case "pen":
679+
case "free-draw":
679680
const currentShape =
680681
this.existingShapes[this.existingShapes.length - 1];
681-
if (currentShape?.type === "pen") {
682+
if (currentShape?.type === "free-draw") {
682683
shape = {
683684
id: uuidv4(),
684-
type: "pen",
685+
type: "free-draw",
685686
points: currentShape.points,
686687
strokeWidth: this.strokeWidth,
687688
strokeFill: this.strokeFill,
@@ -808,10 +809,10 @@ export class CanvasEngine {
808809
this.startX = x;
809810
this.startY = y;
810811

811-
if (this.activeTool === "pen") {
812+
if (this.activeTool === "free-draw") {
812813
this.existingShapes.push({
813814
id: uuidv4(),
814-
type: "pen",
815+
type: "free-draw",
815816
points: [{ x, y }],
816817
strokeWidth: this.strokeWidth,
817818
strokeFill: this.strokeFill,
@@ -927,12 +928,12 @@ export class CanvasEngine {
927928
);
928929
break;
929930

930-
case "pen":
931+
case "free-draw":
931932
const currentShape =
932933
this.existingShapes[this.existingShapes.length - 1];
933-
if (currentShape?.type === "pen") {
934+
if (currentShape?.type === "free-draw") {
934935
currentShape.points.push({ x, y });
935-
this.drawPencil(
936+
this.drawFreeDraw(
936937
currentShape.points,
937938
this.strokeFill,
938939
this.bgFill,
@@ -1211,7 +1212,7 @@ export class CanvasEngine {
12111212

12121213
return distance <= tolerance && withinLineBounds;
12131214
}
1214-
case "pen": {
1215+
case "free-draw": {
12151216
return shape.points.some(
12161217
(point) => Math.hypot(point.x - x, point.y - y) <= tolerance
12171218
);
@@ -1604,7 +1605,7 @@ export class CanvasEngine {
16041605
}
16051606
}
16061607

1607-
drawPencil(
1608+
drawFreeDraw(
16081609
points: { x: number; y: number }[],
16091610
strokeFill: string,
16101611
bgFill: string,
@@ -1613,22 +1614,31 @@ export class CanvasEngine {
16131614
strokeWidth: StrokeWidth
16141615
) {
16151616
if (!points.length) return;
1617+
1618+
// const svgPathData = generateFreeDrawPath(points, strokeWidth);
1619+
16161620
if (fillStyle === "solid") {
1617-
this.ctx.beginPath();
1618-
this.ctx.strokeStyle = strokeFill;
1619-
this.ctx.lineWidth = strokeWidth;
1620-
this.ctx.setLineDash(
1621-
strokeStyle === "dashed"
1622-
? getDashArrayDashed(strokeWidth)
1623-
: strokeStyle === "dotted"
1624-
? getDashArrayDotted(strokeWidth)
1625-
: []
1626-
);
1627-
if (points[0] === undefined) return null;
1628-
this.ctx.moveTo(points[0].x, points[0].y);
1629-
points.forEach((point) => this.ctx.lineTo(point.x, point.y));
1630-
this.ctx.stroke();
1621+
const path = new Path2D(generateFreeDrawPath(points, strokeWidth));
1622+
1623+
this.ctx.save();
1624+
this.ctx.fillStyle = strokeFill;
1625+
this.ctx.fill(path);
1626+
1627+
if (strokeStyle === "dashed" || strokeStyle === "dotted") {
1628+
this.ctx.strokeStyle = strokeFill;
1629+
this.ctx.lineWidth = 1;
1630+
this.ctx.setLineDash(
1631+
strokeStyle === "dashed"
1632+
? getDashArrayDashed(1)
1633+
: getDashArrayDotted(1)
1634+
);
1635+
this.ctx.stroke(path);
1636+
this.ctx.setLineDash([]);
1637+
}
1638+
1639+
this.ctx.restore();
16311640
} else {
1641+
// // For rough/sketchy style, use the existing implementation
16321642
const pathStr = points.reduce(
16331643
(path, point, index) =>
16341644
path +
@@ -1637,6 +1647,7 @@ export class CanvasEngine {
16371647
: ` L ${point.x} ${point.y}`),
16381648
""
16391649
);
1650+
16401651
const options = this.getRoughOptions(
16411652
strokeWidth,
16421653
strokeFill,
@@ -1646,6 +1657,19 @@ export class CanvasEngine {
16461657
fillStyle
16471658
);
16481659
this.roughCanvas.path(pathStr, options);
1660+
1661+
// For rough/sketchy style, use the improved path with rough.js
1662+
// const options = this.getRoughOptions(
1663+
// strokeWidth,
1664+
// strokeFill,
1665+
// 0,
1666+
// bgFill,
1667+
// "solid",
1668+
// fillStyle
1669+
// );
1670+
1671+
// // Use the SVG path data from perfect-freehand with rough.js
1672+
// this.roughCanvas.path(svgPathData, options);
16491673
}
16501674
}
16511675

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import getStroke, { StrokeOptions } from "perfect-freehand";
2+
3+
// This function generates a free-draw path based on points and stroke width
4+
export function generateFreeDrawPath(
5+
points: { x: number; y: number }[],
6+
strokeWidth: number
7+
): string {
8+
// Convert points to the format required by perfect-freehand
9+
const inputPoints = points.map((pt) => [pt.x, pt.y]);
10+
11+
if (!inputPoints.length) return "";
12+
13+
const options: StrokeOptions = {
14+
simulatePressure: true,
15+
size: strokeWidth * 4.25,
16+
thinning: 0.6,
17+
smoothing: 0.5,
18+
streamline: 0.5,
19+
easing: (t) => Math.sin((t * Math.PI) / 2), // easeOutSine
20+
last: true,
21+
};
22+
23+
// Get the stroke points and convert to SVG path
24+
const strokePoints = getStroke(inputPoints, options);
25+
return getSvgPathFromStroke(strokePoints);
26+
}
27+
28+
// Function to get the midpoint between two points
29+
export function getMidpoint(pointA: number[], pointB: number[]): number[] {
30+
return [(pointA[0] + pointB[0]) / 2, (pointA[1] + pointB[1]) / 2];
31+
}
32+
33+
// Regex to fix precision of numbers in SVG path
34+
export const TO_FIXED_PRECISION =
35+
/(\s?[A-Z]?,?-?[0-9]*\.[0-9]{0,2})(([0-9]|e|-)*)/g;
36+
37+
// Convert stroke points to SVG path data
38+
export function getSvgPathFromStroke(points: number[][]): string {
39+
if (!points.length) {
40+
return "";
41+
}
42+
43+
const max = points.length - 1;
44+
45+
return points
46+
.reduce(
47+
(acc, point, i, arr) => {
48+
if (i === max) {
49+
// Close the path by connecting back to the first point
50+
acc.push(point, getMidpoint(point, arr[0]), "L", arr[0], "Z");
51+
} else {
52+
// Create smooth curves between points
53+
acc.push(point, getMidpoint(point, arr[i + 1]));
54+
}
55+
return acc;
56+
},
57+
["M", points[0], "Q"]
58+
)
59+
.join(" ")
60+
.replace(TO_FIXED_PRECISION, "$1"); // Limit precision for performance
61+
}

apps/collabydraw/canvas-engine/SelectionController.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ export class SelectionController {
7171
width: number;
7272
height: number;
7373
} {
74-
if (shape.type !== "pen") {
74+
if (shape.type !== "free-draw") {
7575
const bounds = {
7676
x: shape.x,
7777
y: shape.y,
@@ -266,7 +266,7 @@ export class SelectionController {
266266
x: x - this.selectedShape.x,
267267
y: y - this.selectedShape.y,
268268
};
269-
} else if (this.selectedShape.type !== "pen") {
269+
} else if (this.selectedShape.type !== "free-draw") {
270270
this.dragOffset = {
271271
x: x - this.selectedShape.x,
272272
y: y - this.selectedShape.y,
@@ -310,7 +310,7 @@ export class SelectionController {
310310
this.selectedShape.y = dy;
311311
break;
312312

313-
case "pen":
313+
case "free-draw":
314314
this.selectedShape.points[0].x = dx;
315315
this.selectedShape.points[0].y = dy;
316316
break;

apps/collabydraw/components/StyleConfigurator.tsx

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,7 @@ export function StyleConfigurator({
8686
activeTool={activeTool}
8787
/>
8888

89-
{(activeTool === "rectangle" || activeTool === 'ellipse' || activeTool === "diamond" || activeTool === 'line' || activeTool === 'pen') && (
89+
{(activeTool === "rectangle" || activeTool === 'ellipse' || activeTool === "diamond" || activeTool === 'line' || activeTool === 'free-draw') && (
9090
<div className="Fill-Style-Selector">
9191
<ItemLabel label="Fill" />
9292
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
@@ -134,7 +134,7 @@ export function StyleConfigurator({
134134
</div>
135135
)}
136136

137-
{(activeTool !== "pen" && activeTool !== 'text') && (
137+
{(activeTool !== "free-draw" && activeTool !== 'text') && (
138138
<>
139139
<div className="Rough-Style-Selector">
140140
<ItemLabel label="Sloppiness" />
@@ -149,7 +149,10 @@ export function StyleConfigurator({
149149
))}
150150
</div>
151151
</div>
152-
152+
</>
153+
)}
154+
{(activeTool !== 'text') && (
155+
<>
153156
<div className="Stroke-Style-Selector">
154157
<ItemLabel label="Stroke Style" />
155158
<div className="flex flex-wrap gap-x-2 gap-y-2 items-center py-1">
@@ -212,7 +215,7 @@ export function StyleConfigurator({
212215
</>
213216
)}
214217
</div>
215-
</section>
218+
</section >
216219
</>
217220
)
218221
}

apps/collabydraw/components/canvas/CanvasRoot.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ export default function CanvasRoot() {
150150
"4": "ellipse",
151151
"5": "diamond",
152152
"6": "line",
153-
"7": "pen",
153+
"7": "free-draw",
154154
"8": "arrow",
155155
"9": "text",
156156
"0": "eraser"

apps/collabydraw/types/Tools.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,10 @@ export const tools: Tool[] = [
5050
label: 'Line'
5151
},
5252
{
53-
type: "pen",
53+
type: "free-draw",
5454
icon: <Pencil />,
5555
shortcut: 7,
56-
label: 'Pencil'
56+
label: 'Free Draw'
5757
},
5858
{
5959
type: "arrow",

apps/collabydraw/types/canvas.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export type ToolType =
3434
| "ellipse"
3535
| "line"
3636
| "arrow"
37-
| "pen"
37+
| "free-draw"
3838
| "eraser"
3939
| "text";
4040
export type Tool = {
@@ -181,7 +181,7 @@ export type Shape =
181181
}
182182
| {
183183
id: string | null;
184-
type: "pen";
184+
type: "free-draw";
185185
points: { x: number; y: number }[];
186186
strokeFill: string;
187187
bgFill: string;

0 commit comments

Comments
 (0)