Skip to content
Closed
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
11 changes: 11 additions & 0 deletions apps/docs/src/content/docs/examples/custom-path-generation.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
title: Custom Path Generation
description: Demonstrates how to provide a custom function to generate SVG path strings from points using the getSvgPathFromPoints prop.
---
import { CustomPathGenerationExample } from "../../../examples/CustomPathGeneration";

The `getSvgPathFromPoints` prop allows you to define your own logic for converting a series of points into an SVG path string. This gives you full control over the appearance of the strokes, enabling you to create straight lines, dashed lines, or any other custom path shape.

Below is an example that uses `getSvgPathFromPoints` to render dashed lines.

<CustomPathGenerationExample client:load />
10 changes: 10 additions & 0 deletions apps/docs/src/content/docs/props/ReactSketchCanvas.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
title: ReactSketchCanvas Props
description: Props for the ReactSketchCanvas component.
---

The `ReactSketchCanvas` component accepts the following props:

- **`getSvgPathFromPoints`**:
- **Type**: `(points: Point[]) => string`
- **Description**: Optional. Callback to generate a custom SVG path string from points. This allows you to override the default bezier curve smoothing and implement any path generation logic you need. The function receives an array of `Point` objects and should return a valid SVG path string.
77 changes: 77 additions & 0 deletions apps/docs/src/examples/CustomPathGeneration.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from "react";
import { ReactSketchCanvas, Point } from "react-sketch-canvas";

const styles = {
border: "0.0625rem solid #9c9c9c",
borderRadius: "0.25rem",
};

const generateDashedLinePath = (points: Point[]): string => {
if (points.length < 2) {
return points.length === 1 ? `M ${points[0].x} ${points[0].y}` : "";
}

let path = `M ${points[0].x} ${points[0].y}`;
const dashLength = 10;
const gapLength = 5;
let currentSegmentLength = 0;
let drawingDash = true;

for (let i = 1; i < points.length; i++) {
const p1 = points[i-1];
const p2 = points[i];
const segmentDx = p2.x - p1.x;
const segmentDy = p2.y - p1.y;
const segmentLength = Math.sqrt(segmentDx * segmentDx + segmentDy * segmentDy);

if (segmentLength === 0) continue;

const dxNormalized = segmentDx / segmentLength;
const dyNormalized = segmentDy / segmentLength;

let drawnLengthOnSegment = 0;
while(drawnLengthOnSegment < segmentLength) {
const lengthToDraw = drawingDash ? dashLength : gapLength;
const remainingLengthInCurrentPart = lengthToDraw - currentSegmentLength;

if (drawnLengthOnSegment + remainingLengthInCurrentPart >= segmentLength) {
// Finish this segment
if (drawingDash) {
path += ` L ${p2.x} ${p2.y}`;
} else {
path += ` M ${p2.x} ${p2.y}`;
}
currentSegmentLength += (segmentLength - drawnLengthOnSegment);
drawnLengthOnSegment = segmentLength;
} else {
// Draw part of dash/gap and switch
drawnLengthOnSegment += remainingLengthInCurrentPart;
currentSegmentLength = 0;

const currentX = p1.x + dxNormalized * drawnLengthOnSegment;
const currentY = p1.y + dyNormalized * drawnLengthOnSegment;

if (drawingDash) {
path += ` L ${currentX} ${currentY}`;
} else {
path += ` M ${currentX} ${currentY}`;
}
drawingDash = !drawingDash;
}
}
}
return path;
};

const CustomPathGenerationExample = () => (
<ReactSketchCanvas
style={styles}
width="100%"
height="400px"
strokeWidth={4}
strokeColor="blue"
getSvgPathFromPoints={generateDashedLinePath}
/>
);

export default CustomPathGenerationExample;
53 changes: 53 additions & 0 deletions packages/react-sketch-canvas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const Canvas = class extends React.Component {
| svgStyle | PropTypes.object | {} | Add CSS styling as CSS-in-JS object for the SVG |
| withTimestamp | PropTypes.bool | false | Add timestamp to individual strokes for measuring sketching time |
| readOnly | PropTypes.bool | false | Disable drawing on the canvas (undo/redo, clear & reset will still work.) |
| `getSvgPathFromPoints` | `(points: Point[]) => string` | `undefined` | Optional. Callback to generate a custom SVG path string from points. |

Set SVG background using CSS [background][css-bg] value

Expand Down Expand Up @@ -157,6 +158,12 @@ _Use ref to access the element and call the following functions to export image_
## Types

```ts
// -- Point -- (This type is already defined above, adding for context if needed)
// interface Point {
// x: number;
// y: number;
// }

type ExportImageType = "jpeg" | "png";

interface Point {
Expand All @@ -176,6 +183,52 @@ interface CanvasPath {

---

## Advanced Usage

### Custom Path Generation

You can provide a custom function to generate the SVG path string from an array of points. This allows you to override the default bezier curve smoothing and implement any path generation logic you need.

The `getSvgPathFromPoints` prop takes a function that receives an array of `Point` objects and should return a valid SVG path string.

```jsx
import * as React from "react";
import { ReactSketchCanvas, Point } from "react-sketch-canvas";

const MyCustomSketch = () => {
const styles = {
border: "0.0625rem solid #9c9c9c",
borderRadius: "0.25rem",
};

const generateStraightLinePath = (points: Point[]): string => {
if (points.length === 0) {
return "";
}
let path = `M ${points[0].x} ${points[0].y}`;
for (let i = 1; i < points.length; i++) {
path += ` L ${points[i].x} ${points[i].y}`;
}
return path;
};

return (
<ReactSketchCanvas
style={styles}
width="600"
height="400"
strokeWidth={4}
strokeColor="red"
getSvgPathFromPoints={generateStraightLinePath}
/>
);
};

export default MyCustomSketch;
```

---

## Thanks to

- Philipp Spiess' [tutorial][based-on]
Expand Down
7 changes: 6 additions & 1 deletion packages/react-sketch-canvas/src/Canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ export const Canvas = React.forwardRef<CanvasRef, CanvasProps>((props, ref) => {
svgStyle = {},
withViewBox = false,
readOnly = false,
getSvgPathFromPoints,
} = props;

const canvasRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -408,7 +409,11 @@ release drawing even when point goes out of canvas */
key={`${id}__stroke-group-${i}`}
mask={`${eraserPaths[i] && `url(#${id}__eraser-mask-${i})`}`}
>
<Paths id={`${id}__stroke-group-${i}__paths`} paths={pathGroup} />
<Paths
id={`${id}__stroke-group-${i}__paths`}
paths={pathGroup}
getSvgPathFromPoints={getSvgPathFromPoints}
/>
</g>
))}
</svg>
Expand Down
6 changes: 6 additions & 0 deletions packages/react-sketch-canvas/src/Canvas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,12 @@ export interface CanvasProps {
* @defaultValue false
*/
readOnly?: boolean;
/**
* Optional function to generate custom SVG path string from points.
* @param points - Array of points to generate the path from.
* @returns SVG path string.
*/
getSvgPathFromPoints?: (points: Point[]) => string;
}

/**
Expand Down
24 changes: 18 additions & 6 deletions packages/react-sketch-canvas/src/Paths/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,11 @@ type ControlPoints = {
type PathProps = {
id: string;
paths: CanvasPath[];
getSvgPathFromPoints?: (points: Point[]) => string;
};

// This export was removed in the prompt, but it is used in other files.
// Keeping it to avoid breaking changes.
Comment on lines +17 to +18
Copy link

Copilot AI May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment references the prompt context and is not meaningful in the codebase. Remove it to keep the file clean.

Suggested change
// This export was removed in the prompt, but it is used in other files.
// Keeping it to avoid breaking changes.

Copilot uses AI. Check for mistakes.
export const line = (pointA: Point, pointB: Point) => {
const lengthX = pointB.x - pointA.x;
const lengthY = pointB.y - pointA.y;
Expand Down Expand Up @@ -88,6 +91,8 @@ export type SvgPathProps = {
strokeColor: string;
// Bezier command to smoothen the line
command?: (point: Point, i: number, a: Point[]) => string;
// Custom path generator
getSvgPathFromPoints?: (points: Point[]) => string;
};

/**
Expand All @@ -99,6 +104,7 @@ export function SvgPath({
strokeWidth,
strokeColor,
command = bezierCommand,
getSvgPathFromPoints,
}: SvgPathProps): JSX.Element {
if (paths.length === 1) {
const { x, y } = paths[0];
Expand All @@ -118,11 +124,16 @@ export function SvgPath({
);
}

const d = paths.reduce(
(acc, point, i, a) =>
i === 0 ? `M ${point.x},${point.y}` : `${acc} ${command(point, i, a)}`,
"",
);
let d: string;
if (getSvgPathFromPoints) {
d = getSvgPathFromPoints(paths);
} else {
d = paths.reduce(
(acc, point, i, a) =>
i === 0 ? `M ${point.x},${point.y}` : `${acc} ${command(point, i, a)}`,
"",
);
}

return (
<path
Expand All @@ -137,7 +148,7 @@ export function SvgPath({
);
}

function Paths({ id, paths }: PathProps): JSX.Element {
function Paths({ id, paths, getSvgPathFromPoints }: PathProps): JSX.Element {
return (
<>
{paths.map((path: CanvasPath, index: number) => (
Expand All @@ -149,6 +160,7 @@ function Paths({ id, paths }: PathProps): JSX.Element {
strokeWidth={path.strokeWidth}
strokeColor={path.strokeColor}
command={bezierCommand}
getSvgPathFromPoints={getSvgPathFromPoints}
/>
))}
</>
Expand Down
2 changes: 2 additions & 0 deletions packages/react-sketch-canvas/src/ReactSketchCanvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const ReactSketchCanvas = React.forwardRef<
withTimestamp = false,
withViewBox = false,
readOnly = false,
getSvgPathFromPoints,
} = props;

const svgCanvas = React.createRef<CanvasRef>();
Expand Down Expand Up @@ -335,6 +336,7 @@ export const ReactSketchCanvas = React.forwardRef<
onPointerUp={handlePointerUp}
withViewBox={withViewBox}
readOnly={readOnly}
getSvgPathFromPoints={getSvgPathFromPoints}
/>
);
});
Expand Down
6 changes: 6 additions & 0 deletions packages/react-sketch-canvas/src/ReactSketchCanvas/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,12 @@ export interface ReactSketchCanvasProps
* @defaultValue false
*/
withTimestamp?: boolean;
/**
* Optional function to generate custom SVG path string from points.
* @param points - Array of points to generate the path from.
* @returns SVG path string.
*/
getSvgPathFromPoints?: (points: Point[]) => string;
}

/**
Expand Down
51 changes: 51 additions & 0 deletions packages/tests/playwright/customPathGeneration.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { test, expect } from '@playwright/test';

test.describe('ReactSketchCanvas - E2E Custom Path Generation', () => {
test('should render with straight line custom path generator', async ({ page }) => {
await page.goto('/?name=straightLine'); // Navigate to the new test case

const canvas = page.locator('#straight-line-canvas');
await expect(canvas).toBeVisible();

// Simulate drawing
const canvasBoundingBox = await canvas.boundingBox();
if (!canvasBoundingBox) throw new Error('Canvas not found or not visible');

await page.mouse.move(canvasBoundingBox.x + 10, canvasBoundingBox.y + 10);
await page.mouse.down();
await page.mouse.move(canvasBoundingBox.x + 50, canvasBoundingBox.y + 50);
await page.mouse.move(canvasBoundingBox.x + 100, canvasBoundingBox.y + 20);
await page.mouse.up();

// The path ID is constructed by the component like: {canvasId}__{strokeGroupIndex}__{pathIndex}
// For a single stroke, this should be {canvasId}__0__0 if using older versions,
// or more likely {canvasId}__stroke-group-0__paths__0
// We'll use a selector that finds the path within the specific canvas.
const pathElement = page.locator('#straight-line-canvas path');
await expect(pathElement).toHaveCount(1); // Ensure only one path is drawn
const dAttribute = await pathElement.getAttribute('d');
expect(dAttribute).toBe('M 10 10 L 50 50 L 100 20');
});

test('should render with zig-zag line custom path generator', async ({ page }) => {
await page.goto('/?name=zigZagLine'); // Navigate to the other new test case

const canvas = page.locator('#zigzag-line-canvas');
await expect(canvas).toBeVisible();

const canvasBoundingBox = await canvas.boundingBox();
if (!canvasBoundingBox) throw new Error('Canvas not found or not visible');

await page.mouse.move(canvasBoundingBox.x + 10, canvasBoundingBox.y + 10);
await page.mouse.down();
await page.mouse.move(canvasBoundingBox.x + 50, canvasBoundingBox.y + 50);
await page.mouse.move(canvasBoundingBox.x + 100, canvasBoundingBox.y + 20);
await page.mouse.up();

const pathElement = page.locator('#zigzag-line-canvas path');
await expect(pathElement).toHaveCount(1);
const dAttribute = await pathElement.getAttribute('d');
// Expected path: M 10 10 L 50 50+5 L 100 20
expect(dAttribute).toBe('M 10 10 L 50 55 L 100 20');
});
});
Loading
Loading