fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
276 lines (258 loc) • 8.97 kB
text/typescript
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { Canvas } from '../canvas/Canvas';
import { PencilBrush } from './PencilBrush';
import { parsePath } from '../util';
import { Path } from '../shapes/Path';
import { Point } from '../Point';
import { createPointerEvent } from '../../test/utils';
describe('PencilBrush', () => {
let canvas: Canvas;
beforeEach(() => {
canvas = new Canvas();
});
afterEach(() => {
canvas.cancelRequestedRender();
canvas.off();
});
it('initializes constructor correctly', () => {
expect(PencilBrush).toBeTruthy();
const brush = new PencilBrush(canvas);
expect(brush.canvas, 'assigns canvas').toBe(canvas);
// @ts-expect-error -- protected
expect(brush._points, 'points is an empty array').toEqual([]);
});
it('decimates points correctly', () => {
const brush = new PencilBrush(canvas);
const points = [
new Point(1, 0),
new Point(2, 0),
new Point(3, 0),
new Point(4, 0),
new Point(5, 0),
];
const distance = 6;
const newPoints = brush.decimatePoints(points, distance);
expect(newPoints[0], 'first point is always present').toBe(points[0]);
expect(newPoints[1], 'last point is always present').toBe(
points[points.length - 1],
);
expect(newPoints.length, 'All points removed except first and last').toBe(
2,
);
});
describe.each([true, false])(
'with canvas.enableRetinaScaling = %s',
(retinaScaling) => {
beforeEach(() => {
canvas.enableRetinaScaling = retinaScaling;
});
it('draws a point correctly', () => {
const brush = new PencilBrush(canvas);
const e = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 10,
clientY: 10,
});
const pointer = canvas.getScenePoint(e);
brush.onMouseDown(pointer, { e });
// @ts-expect-error -- protected
const pathData = brush.convertPointsToSVGPath(brush._points);
expect(
pathData,
'path data create a small line that looks like a point',
).toEqual(parsePath('M 9.999 10 L 10.001 10'));
});
it('handles multiple coincident points', () => {
const brush = new PencilBrush(canvas);
const e = createPointerEvent({
target: canvas.upperCanvasEl,
clientX: 10,
clientY: 10,
});
const pointer = canvas.getScenePoint(e);
brush.onMouseDown(pointer, { e });
brush.onMouseMove(pointer, { e });
brush.onMouseMove(pointer, { e });
brush.onMouseMove(pointer, { e });
brush.onMouseMove(pointer, { e });
// @ts-expect-error -- protected
const pathData = brush.convertPointsToSVGPath(brush._points);
expect(
pathData,
'path data create a small line that looks like a point',
).toEqual(parsePath('M 9.999 10 L 10.001 10'));
// @ts-expect-error -- protected
expect(brush._points.length, 'concident points are discarded').toBe(2);
});
it('handles multiple non-coincident points', () => {
const brush = new PencilBrush(canvas);
const e = createPointerEvent({ target: canvas.upperCanvasEl });
const pointer = canvas.getScenePoint({
...e,
clientX: 10,
clientY: 10,
});
const pointer2 = canvas.getScenePoint({
...e,
clientX: 15,
clientY: 15,
});
const pointer3 = canvas.getScenePoint({
...e,
clientX: 20,
clientY: 20,
});
brush.onMouseDown(pointer, { e });
brush.onMouseMove(pointer2, { e });
brush.onMouseMove(pointer3, { e });
brush.onMouseMove(pointer2, { e });
brush.onMouseMove(pointer3, { e });
// @ts-expect-error -- protected
const pathData = brush.convertPointsToSVGPath(brush._points);
expect(pathData, 'path data create a complex path').toEqual(
parsePath(
'M 9.999 9.999 Q 10 10 12.5 12.5 Q 15 15 17.5 17.5 Q 20 20 17.5 17.5 Q 15 15 17.5 17.5 L 20.001 20.001',
),
);
// @ts-expect-error -- protected
expect(brush._points.length, 'concident points are discarded').toBe(6);
});
it('handles points outside canvas', () => {
const brush = new PencilBrush(canvas);
const e = createPointerEvent({ target: canvas.upperCanvasEl });
const pointer = canvas.getScenePoint({
...e,
clientX: 10,
clientY: 10,
});
const pointer2 = canvas.getScenePoint({
...e,
clientX: 15,
clientY: 100,
});
const pointer3 = canvas.getScenePoint({
...e,
clientX: 20,
clientY: 160,
});
const pointer4 = canvas.getScenePoint({
...e,
clientX: 320,
clientY: 100,
});
const pointer5 = canvas.getScenePoint({
...e,
clientX: 100,
clientY: 100,
});
brush.onMouseDown(pointer, { e });
brush.onMouseMove(pointer2, { e });
brush.onMouseMove(pointer3, { e });
brush.onMouseMove(pointer4, { e });
brush.onMouseMove(pointer5, { e });
// @ts-expect-error -- protected
const pathData = brush.convertPointsToSVGPath(brush._points);
expect(
pathData,
'path data create a path that goes beyond canvas',
).toEqual(
parsePath(
'M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 17.5 130 Q 20 160 170 130 Q 320 100 210 100 L 99.999 100',
),
);
// @ts-expect-error -- protected
expect(brush._points.length, 'all points are available').toBe(6);
});
it('limits points to canvas size when limitedToCanvasSize is true', () => {
const brush = new PencilBrush(canvas);
brush.limitedToCanvasSize = true;
const e = createPointerEvent({ target: canvas.upperCanvasEl });
const pointer = canvas.getScenePoint({
...e,
clientX: 10,
clientY: 10,
});
const pointer2 = canvas.getScenePoint({
...e,
clientX: 15,
clientY: 100,
});
const pointer3 = canvas.getScenePoint({
...e,
clientX: 20,
clientY: 160,
});
const pointer4 = canvas.getScenePoint({
...e,
clientX: 320,
clientY: 100,
});
const pointer5 = canvas.getScenePoint({
...e,
clientX: 100,
clientY: 100,
});
brush.onMouseDown(pointer, { e });
brush.onMouseMove(pointer2, { e });
brush.onMouseMove(pointer3, { e });
brush.onMouseMove(pointer4, { e });
brush.onMouseMove(pointer5, { e });
// @ts-expect-error -- protected
const pathData = brush.convertPointsToSVGPath(brush._points);
expect(
pathData,
'path data create a path that does not go beyond canvas',
).toEqual(
parsePath(
'M 9.999 9.999 Q 10 10 12.5 55 Q 15 100 57.5 100 L 100.001 100',
),
);
// @ts-expect-error -- protected
expect(brush._points.length, '2 points have been discarded').toBe(4);
});
it('fires events and creates path on mouse up', () => {
let fireBeforePathCreatedEvent = false;
let firePathCreatedEvent = false;
let added = null;
canvas.on('before:path:created', () => {
fireBeforePathCreatedEvent = true;
});
canvas.on('path:created', (opt) => {
firePathCreatedEvent = true;
added = opt.path;
});
const brush = new PencilBrush(canvas);
const e = createPointerEvent({ target: canvas.upperCanvasEl });
const pointer = canvas.getScenePoint({
...e,
clientX: 10,
clientY: 10,
});
const pointer2 = canvas.getScenePoint({
...e,
clientX: 15,
clientY: 15,
});
const pointer3 = canvas.getScenePoint({
...e,
clientX: 20,
clientY: 20,
});
brush.onMouseDown(pointer, { e });
brush.onMouseMove(pointer2, { e });
brush.onMouseMove(pointer3, { e });
brush.onMouseMove(pointer2, { e });
brush.onMouseMove(pointer3, { e });
brush.onMouseUp({ e });
expect(
fireBeforePathCreatedEvent,
'before:path:created event is fired',
).toBe(true);
expect(firePathCreatedEvent, 'path:created event is fired').toBe(true);
expect(added, 'a path is added').toBeInstanceOf(Path);
expect(added!.path).toMatchSnapshot();
expect(added!.path.length, 'path has 6 steps').toBe(4);
});
},
);
});