Skip to content
Open
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
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
# Changelog

## 1.2.0

The bucket fill tool.

### Added

- The bucket fill tool: `tool="bucket-fill"`, `Tools.BucketFill` and `Types.Tool.BUCKET_FILL`. A click fills
the area which is closed around it with `fillColor` (with `lineColor` while `fillColor` is transparent, its
default). Lines, rectangles, circles, polygons, strokes of the pencil, texts, the overlay image and any other
object close an area, whatever their color. The background color and the background image do not, and nor
does a dashed line, which has gaps. The area has to be closed within the canvas as it is shown (with the zoom
and the position of the view): when the color would get out, a click fills nothing. The fill is an object of
the sketch (a path, saved by `toJSON()` as a `BucketFill` and loaded by `value`, `defaultValue` and
`fromJSON()`), one undo step, and it scales with the canvas. It is under the lines which close the area, a
pixel under their edge, and over the object the area is in, so a click in a filled rectangle fills what its
lines leave of it, and an area which has a fill is filled again over it. The cursor is a bucket with a drop
of the color.
- The showcase has a Bucket fill button in the tool selector (key F), with the fill color only in its
properties, as the other properties do not matter to it.

### Changed

- Undo of a removal puts the object back where it was among the objects, and not on top of them. Redo of an
addition does the same for an object which was added under the others, as the fill of the bucket is, so a fill
never covers the lines which close it after an undo and a redo.

### Packaging and tooling

- The bucket fill is in the private package `packages/bucket` (`@react-sketch/bucket`), which `react-sketch`
bundles too, so nothing new is installed for the users of the library. `yarn build:packages` builds the three
private packages, and `yarn typecheck`, `yarn build` and `yarn test` run it first. Its tests are in
`react-sketch`: the ones with real pixels run in the node environment of Jest, with the node build of fabric.

## 1.1.0 (2026-11-04)

The eraser, laser pointer and polygon tools, pinch zoom, and features that forks of react-sketch had, written again for the TypeScript code and fabric 7.
Expand Down
30 changes: 26 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -257,7 +257,7 @@ All properties are optional.
| `lineColor` | `string` | `"black"` | Color of the line. |
| `lineWidth` | `number` | `10` | Width of the line. |
| `lineStyle` | `Types.LineStyle` | `"solid"` | Style of the line: `"solid"`, `"dashed"`, `"dotted"` or `"dash-dot"`. See [Line style](#line-style). |
| `fillColor` | `string` | `"transparent"` | Fill color of the shapes, where applicable (rectangle, rectangle-label, circle). |
| `fillColor` | `string` | `"transparent"` | Fill color of the shapes, where applicable (rectangle, rectangle-label, circle), and the color of the bucket fill, which uses `lineColor` while it is transparent. |
| `backgroundColor` | `string` | `"transparent"` | Background color of the sketch, in hex or rgba format. |
| `backgroundImage` | `string` | | URL or data URL of the background image. It is set when the component mounts and when it changes, and removed when it is gone. It takes the place of the background image of `value` and `defaultValue`. |
| `backgroundImageOptions` | `Types.BackgroundOptions` | | Options of `backgroundImage`, those of `setBackgroundFromDataUrl`. Options written inline in a render do not load the image again when their content is the same. |
Expand Down Expand Up @@ -344,6 +344,7 @@ Use the tools by name (`"pencil"`), with `Tools` (`Tools.Pencil`) or with the en
| Pan | `Tools.Pan` | `pan` | Disables drawing. Dragging moves the whole canvas, useful after zooming (contributed by [wmaillard](https://github.com/wmaillard)). |
| Highlighter | `Tools.Highlighter` | `highlighter` | Free drawing in a semi-transparent color. `lineColor` must be a hex code or a color name; any other value falls back to the default color. |
| Eraser | `Tools.Eraser` | `eraser` | Erases parts of the objects, with the width of `lineWidth`. The cursor shows the size of the brush. The background color and the background image are not erased. See [Eraser](#eraser). |
| Bucket fill | `Tools.BucketFill` | `bucket-fill` | Fills the area which is closed around a click with `fillColor` (`lineColor` while it is transparent). See [Bucket fill](#bucket-fill). |
| Laser pointer | `Tools.LaserPointer` | `laser-pointer` | A line that follows the pointer, fades out and disappears. It is not part of the drawing. See [Laser pointer](#laser-pointer). |
| Default tool | `Tools.DefaultTool` | `default-tool` | Disables drawing and selection. Use it to show a drawing that is not edited. |

Expand All @@ -358,8 +359,8 @@ thick line has long dashes and wide dots.
```

The pencil, line, arrow, rectangle, rectangle label, circle and polygon draw it. The head of an arrow and the
text box of a rectangle label are not dashed, and the highlighter, the eraser and the laser pointer do not
draw it. A change of `lineStyle` is for the next drawing: what is drawn already keeps its style. The style is
text box of a rectangle label are not dashed, and the highlighter, the eraser, the bucket fill and the laser
pointer do not draw it. A change of `lineStyle` is for the next drawing: what is drawn already keeps its style. The style is
saved by `toJSON()` and loaded by `fromJSON()` with the object (`strokeDashArray` and `strokeLineCap`), and
the polygon in progress is shown in it.

Expand Down Expand Up @@ -430,6 +431,26 @@ A stroke is one undo step. The erased parts are kept on the objects, so `toJSON(
`defaultValue` and `fromJSON()` load them. An object is only cut where the eraser crosses it, the rest of
the sketch stays, and the background color and the background image are not erased.

### Bucket fill

The bucket fill fills the area around the point of a click with `fillColor`, or with `lineColor` while
`fillColor` is transparent (its default). What closes the area is what is drawn: lines, rectangles, circles,
polygons, strokes of the pencil, texts, the overlay image, and any other object, whatever its color. The
background color and the background image do not close an area.

The area has to be closed within the canvas, as it is shown with the zoom and the position of the view. Where
the lines do not meet, or the area reaches the edge of the canvas, the color would get out: a click there fills
nothing, and `onChange` is not called. A dashed or dotted line has gaps, so it does not close an area.

The fill is an object of the sketch, a path of the shape of the area. It is one undo step, `onObjectAdded` and
`onChange` are called for it, `toJSON()` saves it (as a `BucketFill` object), `value`, `defaultValue` and
`fromJSON()` load it, and it scales with the canvas. It is over the object the area is in and under the objects
around it, and goes a pixel under their lines, so a line is never covered and there is no seam at its edge. A
click in an area which has a fill fills it again, over the first one. The select tool moves and removes the fills,
and the eraser cuts them, as it does the other objects.

The fill is made by the private package [`@react-sketch/bucket`](packages/bucket).

### Laser pointer

The laser pointer points at something on the sketch: the line follows the pointer, gets thinner towards its
Expand All @@ -445,7 +466,7 @@ not called when it is drawn. Switching to another tool clears what is left of th
| Export | Description |
| -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SketchField` | The component (named export). Also usable as a type, for the `ref`. |
| `Tools` | The tools by name: `Select`, `Pencil`, `Line`, `Arrow`, `Rectangle`, `RectangleLabel`, `Polygon`, `Circle`, `Pan`, `DefaultTool`, `Highlighter`, `Eraser`, `LaserPointer`. |
| `Tools` | The tools by name: `Select`, `Pencil`, `Line`, `Arrow`, `Rectangle`, `RectangleLabel`, `Polygon`, `Circle`, `Pan`, `DefaultTool`, `Highlighter`, `Eraser`, `BucketFill`, `LaserPointer`. |
| `Types` | Namespace of the types: `Tool` (enumeration), `Tools`, `LineStyle`, `SketchProperties`, `SketchEvent`, `SketchObject`, `SketchValue`, `SketchData`, `DataUrlOptions`, `ImageOptions`, `BackgroundOptions`, `TextOptions`. |
| default export | The component, which also carries `SketchField` and `Tools` (as in 0.5.x). |

Expand All @@ -469,6 +490,7 @@ This is a Yarn 1 workspaces monorepo.
| Path | Description |
| ----------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `packages/react-sketch` | The library, published to npm as `react-sketch`. The build output is `dist/index.js` (UMD bundle, `react` and `fabric` are external) and `dist/index.d.ts` (type declarations). |
| `packages/bucket` | The bucket fill, which finds the area closed around a point and makes its fill (private, bundled into `react-sketch`). |
| `packages/eraser` | The eraser brush, from `@erase2d/fabric` (private, bundled into `react-sketch`). |
| `packages/laserpointer` | The laser pointer brush (private, bundled into `react-sketch`). |
| `packages/showcase` | Demo application (not published), live on GitHub Pages: a canvas with floating tools, properties and settings, and the `defaultValue`, `value`, `addImg` and `backgroundImage` scenarios. |
Expand Down
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@
},
"scripts": {
"start": "yarn workspace showcase start",
"build:bucket": "yarn workspace @react-sketch/bucket build",
"build:eraser": "yarn workspace @react-sketch/eraser build",
"build:laserpointer": "yarn workspace @react-sketch/laserpointer build",
"build:packages": "yarn build:eraser && yarn build:laserpointer",
"build:packages": "yarn build:bucket && yarn build:eraser && yarn build:laserpointer",
"build": "yarn build:packages && yarn workspace react-sketch build && yarn workspace showcase build",
"typecheck": "yarn workspace @react-sketch/eraser typecheck && yarn workspace @react-sketch/laserpointer typecheck && yarn build:packages && yarn workspace react-sketch typecheck && yarn workspace showcase typecheck",
"clean": "node -e \"for (const d of ['packages/eraser/dist', 'packages/laserpointer/dist', 'packages/react-sketch/dist', 'packages/showcase/build']) require('fs').rmSync(d, { recursive: true, force: true })\"",
"typecheck": "yarn workspace @react-sketch/bucket typecheck && yarn workspace @react-sketch/eraser typecheck && yarn workspace @react-sketch/laserpointer typecheck && yarn build:packages && yarn workspace react-sketch typecheck && yarn workspace showcase typecheck",
"clean": "node -e \"for (const d of ['packages/bucket/dist', 'packages/eraser/dist', 'packages/laserpointer/dist', 'packages/react-sketch/dist', 'packages/showcase/build']) require('fs').rmSync(d, { recursive: true, force: true })\"",
"lint": "eslint \"packages/**/*.{ts,tsx}\"",
"lint:fix": "eslint --fix \"packages/**/*.{ts,tsx}\"",
"format": "prettier --write .",
Expand Down
60 changes: 60 additions & 0 deletions packages/bucket/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# @react-sketch/bucket

The bucket fill of react-sketch: it finds the area which is closed around a point of a canvas, by what is
drawn on it, and makes the fill of that area.

The package is private. `react-sketch` bundles it, so nothing is installed for it by the users of the
library. It needs `fabric` 7.4 or later.

```ts
import { bucketFill } from "@react-sketch/bucket";

const filled = bucketFill(canvas, canvas.getViewportPoint(event), {
color: "#2f9e44",
});
if (filled) canvas.insertAt(filled.index, filled.fill);
```

`bucketFill(canvas, point, options)` returns the fill and where it goes among the objects of the canvas, or
`null` when there is nothing to fill. It does not change the canvas: the caller adds the fill, so that it is an
action of its own (the tool of `react-sketch` does it, and the history of the sketch records it).

- `point` is a pixel of the canvas as it is seen, with the zoom and the position of its view
(`canvas.getViewportPoint(event)`).
- The options are `color` (any color the canvas knows, and not a transparent one), `tolerance` (how different
a pixel can be from the one pressed, 0 to 255, 32 by default), `overlap` (how many pixels the fill goes under
the lines around it, 1 by default) and `simplify` (how far, in pixels, the outline of the fill can be from
the pixels it is taken from, 0.5 by default).

## How it works

1. The objects of the canvas are drawn on a canvas of their own, one pixel for each pixel of the canvas (not of
the screen). The background color and the background image are not drawn, the overlay image is, and the
fills are not: what closes an area is what is drawn, whatever its color.
2. The area is the one of the pixels of the color of the pixel pressed which are connected to it (up, down, left
or right, so a line of pixels which touch at their corners closes it). A color has its alpha applied before
it is compared, so a faint pixel of a red line is as near to a transparent one as a faint pixel of a black one.
3. When the area reaches the edge of the canvas, it is not closed, and there is no fill.
4. The area grows by `overlap` pixels, over the pixels which an object above the fill draws, and no further. The
edge of a line is smooth, so an area which stops at the line leaves a thin seam: the fill goes under the line.
5. The outline of the area is followed pixel edge by pixel edge (one loop for the outer edge and one for each
hole, so a circle in a rectangle is a ring), and the steps of a diagonal or a curve are made a line.
6. The fill is a `FillPath` (a fabric `Path`, of the type `BucketFill`) in the coordinates of the objects, so
it scales with the canvas and is saved by `toJSON()`. Importing the package registers the type, which is
what loads it.

It goes over the topmost object which is drawn at the point pressed (an older fill of the area, or a filled
rectangle the area is in) and under the objects which are above that one: the lines which close the area.

## Limits

- The area has to be closed within the canvas as it is shown. When the view is zoomed in so that a rectangle is
partly out of it, the inside reaches the edge and there is no fill.
- The pixels of a canvas which is tainted (an image of a server without CORS headers) cannot be read. The
images are left out then, and the area is closed by the other objects.
- A line with a gap lets the color out, however small the gap: a dashed line does not close an area.

## Pieces

`floodFill`, `grow`, `traceOutline`, `simplify` and `outlinePath` are exported too. They work on the pixels of
an image (`{ data, width, height }`, as `getImageData` returns them) and do not need a canvas.
19 changes: 19 additions & 0 deletions packages/bucket/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@react-sketch/bucket",
"version": "1.2.0",
"private": true,
"description": "The bucket fill for fabric, which fills the area closed by the objects around a point with a color, used by react-sketch",
"license": "MIT",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"typecheck": "tsc --noEmit"
},
"peerDependencies": {
"fabric": "^7.4.0"
},
"devDependencies": {
"fabric": "^7.4.0"
}
}
13 changes: 13 additions & 0 deletions packages/bucket/src/FillPath.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { Path, classRegistry } from "fabric";

/**
* The fill of an area: a path of the color, which is under the objects
* around the area, and over the ones it is in. It is a class of its own, so
* that a fill is known among the objects (it is saved with its type, and
* loaded as one).
*/
export class FillPath extends Path {
static type = "BucketFill";
}

classRegistry.setClass(FillPath);
Loading
Loading