Skip to content

Commit

Permalink
Add throttling to pointer move event (#164)
Browse files Browse the repository at this point in the history
* Add throttling to pointer move event

Fixes #163

Add throttling to pointer move events in the Canvas component to optimize coordinate recording.

- Modify `packages/react-sketch-canvas/src/Canvas/index.tsx` to add throttling to the `handlePointerMove` function using a custom `useThrottledCallback` function.
- Add `throttleTime` to `CanvasProps` in `packages/react-sketch-canvas/src/Canvas/types.ts` with a default value of 20.
- Add `throttleTime` to `ReactSketchCanvasProps` in `packages/react-sketch-canvas/src/ReactSketchCanvas/types.ts` with a default value of 20.
- Add `useThrottledCallback` function in `packages/react-sketch-canvas/src/Canvas/utils.tsx` to throttle a callback function.
- Update `README.md` to include documentation on the new `throttleTime` configuration option.
- Add tests to verify throttling behavior in `packages/tests/src/actions/throttling.spec.ts`.
- Write vitest tests for `useThrottledCallback` function in `packages/react-sketch-canvas/src/Canvas/__test__/utils.test.tsx`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/vinothpandian/react-sketch-canvas/issues/163?shareId=XXXX-XXXX-XXXX-XXXX).

* fix: tests

* fix: set default throttleTime to 0

* chore: remove lint in ci

* fix: workflow
  • Loading branch information
vinothpandian authored Aug 17, 2024
1 parent bcbb141 commit ef93338
Show file tree
Hide file tree
Showing 15 changed files with 8,825 additions and 5,789 deletions.
12 changes: 6 additions & 6 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
name: CI
on: [ push ]
on: [push]
jobs:
build:
name: Build, lint, and test on Node ${{ matrix.node }} and ${{ matrix.os }}

runs-on: ${{ matrix.os }}
strategy:
matrix:
node: [ "18.x" ]
os: [ ubuntu-latest ]
node: ["18.x"]
os: [ubuntu-latest]

steps:
- name: Checkout repo
Expand Down Expand Up @@ -41,12 +41,12 @@ jobs:
- name: Install dependencies
run: pnpm install --no-frozen-lockfile

- name: Lint
run: pnpm lint

- name: Install Playwright browsers
run: pnpx playwright install chromium

- name: Unit tests
run: pnpm unit:test

- name: Test
run: pnpm test

Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,5 @@ packages/tests/cypress/downloads/
.idea/
.turbo/cookies/
/apps/docs/src/content/docs/api/
.turbo/cache/
.turbo/daemon/
38 changes: 19 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,26 @@ const Canvas = class extends React.Component {

## List of Props

| Props | Expected datatype | Default value | Description |
|------------------------------------| ----------------- | --------------------- |----------------------------------------------------------------------------------------------------|
| width | PropTypes.string | 100% | canvas width (em/rem/px) |
| height | PropTypes.string | 100% | canvas width (em/rem/px) |
| id | PropTypes.string | "react-sketch-canvas" | ID field to uniquely identify a SVG canvas (Supports multiple canvases in a single page) |
| className | PropTypes.string | "" | Class for using with CSS selectors |
| strokeColor | PropTypes.string | black | Pen color |
| canvasColor | PropTypes.string | white | canvas color (HTML colors) |
| backgroundImage | PropTypes.string | '' | Set SVG background with image URL |
| exportWithBackgroundImage | PropTypes.bool | false | Keep background image on image/SVG export (on false, canvasColor will be set as background) |
| Props | Expected datatype | Default value | Description |
| ---------------------------------- | ----------------- | --------------------- | --------------------------------------------------------------------------------------------------- |
| width | PropTypes.string | 100% | canvas width (em/rem/px) |
| height | PropTypes.string | 100% | canvas width (em/rem/px) |
| id | PropTypes.string | "react-sketch-canvas" | ID field to uniquely identify a SVG canvas (Supports multiple canvases in a single page) |
| className | PropTypes.string | "" | Class for using with CSS selectors |
| strokeColor | PropTypes.string | black | Pen color |
| canvasColor | PropTypes.string | white | canvas color (HTML colors) |
| backgroundImage | PropTypes.string | '' | Set SVG background with image URL |
| exportWithBackgroundImage | PropTypes.bool | false | Keep background image on image/SVG export (on false, canvasColor will be set as background) |
| preserveBackgroundImageAspectRatio | PropTypes.string | none | Set aspect ratio of the background image. For possible values check [MDN docs][preserveaspectratio] |
| strokeWidth | PropTypes.number | 4 | Pen stroke size |
| eraserWidth | PropTypes.number | 8 | Erase size |
| allowOnlyPointerType | PropTypes.string | all | allow pointer type ("all"/"mouse"/"pen"/"touch") |
| onChange | PropTypes.func | | Returns the current sketch path in `CanvasPath` type on every path change |
| onStroke | PropTypes.func | | Returns the the last stroke path and whether it is an eraser stroke on every pointer up event |
| style | PropTypes.object | false | Add CSS styling as CSS-in-JS object |
| 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.) |
| strokeWidth | PropTypes.number | 4 | Pen stroke size |
| eraserWidth | PropTypes.number | 8 | Erase size |
| allowOnlyPointerType | PropTypes.string | all | allow pointer type ("all"/"mouse"/"pen"/"touch") |
| onChange | PropTypes.func | | Returns the current sketch path in `CanvasPath` type on every path change |
| onStroke | PropTypes.func | | Returns the the last stroke path and whether it is an eraser stroke on every pointer up event |
| style | PropTypes.object | false | Add CSS styling as CSS-in-JS object |
| 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.) |

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

Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"preview": "turbo run preview",
"lint": "turbo run lint",
"test": "turbo run build test",
"unit:test": "turbo run unit:test",
"ci:test": "turbo run build ci:test",
"format": "prettier --write \"**/*.{ts,tsx,md}\"",
"ci:build": "cp README.md packages/react-sketch-canvas/ && pnpm --filter react-sketch-canvas ci:build",
Expand All @@ -20,15 +21,15 @@
},
"devDependencies": {
"eslint-config-custom": "workspace:*",
"prettier": "^3.2.5",
"turbo": "^1.12.4"
"prettier": "^3.3.3",
"turbo": "^2.0.14"
},
"engines": {
"node": ">=18.0.0",
"pnpm": ">=8.0.0"
},
"packageManager": "pnpm@8.15.3",
"dependencies": {
"@changesets/cli": "^2.27.1"
"@changesets/cli": "^2.27.7"
}
}
1 change: 1 addition & 0 deletions packages/eslint-config-custom/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module.exports = {
"react/jsx-filename-extension": [1, { extensions: [".tsx", ".jsx"] }],
"import/extensions": [1, "never"],
"import/prefer-default-export": "off",
"import/no-extraneous-dependencies": "off",
},
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint"],
Expand Down
39 changes: 20 additions & 19 deletions packages/react-sketch-canvas/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,26 +108,27 @@ const Canvas = class extends React.Component {

## List of Props

| Props | Expected datatype | Default value | Description |
|------------------------------------| ----------------- | --------------------- |----------------------------------------------------------------------------------------------------|
| width | PropTypes.string | 100% | canvas width (em/rem/px) |
| height | PropTypes.string | 100% | canvas width (em/rem/px) |
| id | PropTypes.string | "react-sketch-canvas" | ID field to uniquely identify a SVG canvas (Supports multiple canvases in a single page) |
| className | PropTypes.string | "" | Class for using with CSS selectors |
| strokeColor | PropTypes.string | black | Pen color |
| canvasColor | PropTypes.string | white | canvas color (HTML colors) |
| backgroundImage | PropTypes.string | '' | Set SVG background with image URL |
| exportWithBackgroundImage | PropTypes.bool | false | Keep background image on image/SVG export (on false, canvasColor will be set as background) |
| Props | Expected datatype | Default value | Description |
| ---------------------------------- | ----------------- | --------------------- | --------------------------------------------------------------------------------------------------- |
| width | PropTypes.string | 100% | canvas width (em/rem/px) |
| height | PropTypes.string | 100% | canvas width (em/rem/px) |
| id | PropTypes.string | "react-sketch-canvas" | ID field to uniquely identify a SVG canvas (Supports multiple canvases in a single page) |
| className | PropTypes.string | "" | Class for using with CSS selectors |
| strokeColor | PropTypes.string | black | Pen color |
| canvasColor | PropTypes.string | white | canvas color (HTML colors) |
| backgroundImage | PropTypes.string | '' | Set SVG background with image URL |
| exportWithBackgroundImage | PropTypes.bool | false | Keep background image on image/SVG export (on false, canvasColor will be set as background) |
| preserveBackgroundImageAspectRatio | PropTypes.string | none | Set aspect ratio of the background image. For possible values check [MDN docs][preserveaspectratio] |
| strokeWidth | PropTypes.number | 4 | Pen stroke size |
| eraserWidth | PropTypes.number | 8 | Erase size |
| allowOnlyPointerType | PropTypes.string | all | allow pointer type ("all"/"mouse"/"pen"/"touch") |
| onChange | PropTypes.func | | Returns the current sketch path in `CanvasPath` type on every path change |
| onStroke | PropTypes.func | | Returns the the last stroke path and whether it is an eraser stroke on every pointer up event |
| style | PropTypes.object | false | Add CSS styling as CSS-in-JS object |
| 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.) |
| strokeWidth | PropTypes.number | 4 | Pen stroke size |
| eraserWidth | PropTypes.number | 8 | Erase size |
| allowOnlyPointerType | PropTypes.string | all | allow pointer type ("all"/"mouse"/"pen"/"touch") |
| onChange | PropTypes.func | | Returns the current sketch path in `CanvasPath` type on every path change |
| onStroke | PropTypes.func | | Returns the the last stroke path and whether it is an eraser stroke on every pointer up event |
| style | PropTypes.object | false | Add CSS styling as CSS-in-JS object |
| 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.) |
| throttleTime | PropTypes.number | 0 | Throttle time for pointer move events in milliseconds |

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

Expand Down
9 changes: 7 additions & 2 deletions packages/react-sketch-canvas/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,21 +42,26 @@
"ci:build": "pnpm clean && tsup src/index.tsx --env.NODE_ENV production && npm run size",
"dev": "tsup src/index.tsx --env.NODE_ENV development",
"lint": "eslint *.ts*",
"size": "size-limit"
"size": "size-limit",
"unit:test": "vitest --no-watch"
},
"devDependencies": {
"@size-limit/preset-small-lib": "^11.0.2",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.0.0",
"@types/react": "^18.2.55",
"@types/react-dom": "^18.2.19",
"eslint": "^8.56.0",
"eslint-config-custom": "workspace:*",
"jsdom": "^24.1.1",
"react": "^18.2.0",
"rimraf": "^5.0.5",
"size-limit": "^11.0.2",
"tsconfig": "workspace:*",
"tslib": "^2.6.2",
"tsup": "^8.0.2",
"typescript": "^5.3.3"
"typescript": "^5.3.3",
"vitest": "^2.0.5"
},
"peerDependencies": {
"react": ">=16.8"
Expand Down
39 changes: 39 additions & 0 deletions packages/react-sketch-canvas/src/Canvas/__test__/utils.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { describe, it, expect, vi } from "vitest";
import { renderHook, act } from "@testing-library/react";

import { useThrottledCallback } from "../utils";

describe("useThrottledCallback", () => {
it("should call the callback immediately if delay is 0", () => {
const callback = vi.fn();
const { result } = renderHook(() => useThrottledCallback(callback, 0, []));

act(() => {
result.current();
});

expect(callback).toHaveBeenCalledTimes(1);
});

it("should throttle the callback", () => {
vi.useFakeTimers();
const callback = vi.fn();
const { result } = renderHook(() => useThrottledCallback(callback, 20, []));

act(() => {
result.current();
result.current();
result.current();
});

expect(callback).toHaveBeenCalledTimes(1);

act(() => {
vi.advanceTimersByTime(20);
});

expect(callback).toHaveBeenCalledTimes(2);

vi.useRealTimers();
});
});
7 changes: 5 additions & 2 deletions packages/react-sketch-canvas/src/Canvas/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
Point,
} from "../types";
import { CanvasProps, CanvasRef } from "./types";
import { useThrottledCallback } from "./utils";

const loadImage = (url: string): Promise<HTMLImageElement> =>
new Promise((resolve, reject) => {
Expand Down Expand Up @@ -70,6 +71,7 @@ export const Canvas = React.forwardRef<CanvasRef, CanvasProps>((props, ref) => {
svgStyle = {},
withViewBox = false,
readOnly = false,
throttleTime = 0,
} = props;

const canvasRef = React.useRef<HTMLDivElement>(null);
Expand Down Expand Up @@ -128,7 +130,7 @@ export const Canvas = React.forwardRef<CanvasRef, CanvasProps>((props, ref) => {
[allowOnlyPointerType, getCoordinates, onPointerDown],
);

const handlePointerMove = useCallback(
const handlePointerMove = useThrottledCallback(
(event: React.PointerEvent<HTMLDivElement>): void => {
if (!isDrawing) return;

Expand All @@ -144,6 +146,7 @@ export const Canvas = React.forwardRef<CanvasRef, CanvasProps>((props, ref) => {

onPointerMove(point);
},
throttleTime,
[allowOnlyPointerType, getCoordinates, isDrawing, onPointerMove],
);

Expand Down Expand Up @@ -263,7 +266,7 @@ export const Canvas = React.forwardRef<CanvasRef, CanvasProps>((props, ref) => {
}));

/* Add event listener to Mouse up and Touch up to
release drawing even when point goes out of canvas */
release drawing even when point goes out of canvas */
React.useEffect(() => {
document.addEventListener("pointerup", handlePointerUp);
return () => {
Expand Down
Loading

0 comments on commit ef93338

Please sign in to comment.