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
34 changes: 34 additions & 0 deletions extensions/centering_guidelines/README.MD
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Centering Guidelines

Shows guidelines and snaps objects when dragged to the canvas center.

## How to use it

```ts
import { CenteringGuidelines } from 'fabric/extensions';

const centeringGuidelines = new CenteringGuidelines(canvas);

// To disable later:
centeringGuidelines.dispose();
```

## Options

```ts
new CenteringGuidelines(canvas, {
margin: 4, // Snap distance in pixels
color: 'rgba(255,0,241,0.5)', // Line color
width: 1, // Line width
});
```

## Using with script tags

```html
<script src="https://cdn.jsdelivr.net/npm/fabric@7/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fabric@7/dist-extensions/fabric-extensions.min.js"></script>
<script>
new fabricExtensions.CenteringGuidelines(canvas);
</script>
```
26 changes: 26 additions & 0 deletions extensions/centering_guidelines/demo.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE html>
<html>

Check warning on line 2 in extensions/centering_guidelines/demo.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Add "lang" and/or "xml:lang" attributes to this "<html>" element

See more on https://sonarcloud.io/project/issues?id=fabricjs_fabric.js&issues=AZzys4QpWT_PiSiG0vOs&open=AZzys4QpWT_PiSiG0vOs&pullRequest=10846
<head>
<title>Centering Guidelines</title>
<style>
body { font-family: system-ui; padding: 20px; background: #f5f5f5; }
p { color: #666; margin: 0 0 12px; }
</style>
</head>
<body>
<p>Drag objects to canvas center to see snap guidelines</p>
<canvas id="c" width="800" height="600"></canvas>

<script src="https://cdn.jsdelivr.net/npm/fabric@7/dist/index.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fabric@7/dist-extensions/fabric-extensions.min.js"></script>
<script>
const canvas = new fabric.Canvas('c', { backgroundColor: '#fff' });

new fabricExtensions.CenteringGuidelines(canvas);

Check warning on line 19 in extensions/centering_guidelines/demo.html

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Either remove this useless object instantiation of "fabricExtensions.CenteringGuidelines" or use it.

See more on https://sonarcloud.io/project/issues?id=fabricjs_fabric.js&issues=AZzys4QpWT_PiSiG0vOv&open=AZzys4QpWT_PiSiG0vOv&pullRequest=10846

canvas.add(new fabric.Rect({ left: 100, top: 100, width: 100, height: 100, fill: '#3b82f6' }));
canvas.add(new fabric.Circle({ left: 500, top: 150, radius: 50, fill: '#22c55e' }));
canvas.add(new fabric.Triangle({ left: 300, top: 400, width: 100, height: 100, fill: '#f59e0b' }));
</script>
</body>
</html>
74 changes: 74 additions & 0 deletions extensions/centering_guidelines/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { Canvas } from '../../src/canvas/Canvas';
import { Rect } from '../../src/shapes/Rect';
import { CenteringGuidelines } from './index';

describe('CenteringGuidelines', () => {
let canvas: Canvas;

beforeEach(() => {
canvas = new Canvas();
canvas.setDimensions({ width: 800, height: 600 });
});

it('creates with default options', () => {
const guidelines = new CenteringGuidelines(canvas);
expect(guidelines.margin).toBe(4);
expect(guidelines.color).toBe('rgba(255,0,241,0.5)');
expect(guidelines.width).toBe(1);
guidelines.dispose();
});

it('accepts custom options', () => {
const guidelines = new CenteringGuidelines(canvas, {
margin: 10,
color: 'red',
width: 2,
});
expect(guidelines.margin).toBe(10);
expect(guidelines.color).toBe('red');
expect(guidelines.width).toBe(2);
guidelines.dispose();
});

it('snaps object to horizontal center', () => {
const guidelines = new CenteringGuidelines(canvas);
const rect = new Rect({ width: 100, height: 100, left: 100, top: 299 });
canvas.add(rect);

rect.set({ top: 299 });
rect.setCoords();
(canvas as any).fire('object:moving', { target: rect });

const center = rect.getCenterPoint();
expect(center.y).toBe(300);
guidelines.dispose();
});

it('snaps object to vertical center', () => {
const guidelines = new CenteringGuidelines(canvas);
const rect = new Rect({ width: 100, height: 100, left: 399, top: 100 });
canvas.add(rect);

rect.set({ left: 399 });
rect.setCoords();
(canvas as any).fire('object:moving', { target: rect });

const center = rect.getCenterPoint();
expect(center.x).toBe(400);
guidelines.dispose();
});

it('disposes event listeners', () => {
const guidelines = new CenteringGuidelines(canvas);
const offSpy = vi.spyOn(canvas, 'off');

guidelines.dispose();

expect(offSpy).toHaveBeenCalledWith('mouse:down', expect.any(Function));
expect(offSpy).toHaveBeenCalledWith('mouse:up', expect.any(Function));
expect(offSpy).toHaveBeenCalledWith('object:moving', expect.any(Function));
expect(offSpy).toHaveBeenCalledWith('before:render', expect.any(Function));
expect(offSpy).toHaveBeenCalledWith('after:render', expect.any(Function));
});
});
140 changes: 140 additions & 0 deletions extensions/centering_guidelines/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { type Canvas, type FabricObject, Point } from 'fabric';

type MovingEvent = { target: FabricObject };

export type CenteringGuidelinesConfig = {
/** Margin in pixels for snap detection */
margin: number;
/** Line color */
color: string;
/** Line width */
width: number;
};

export class CenteringGuidelines {
canvas: Canvas;
margin = 4;
color = 'rgba(255,0,241,0.5)';
width = 1;

private isInVerticalCenter = false;
private isInHorizontalCenter = false;
private viewportTransform: number[] = [1, 0, 0, 1, 0, 0];

constructor(canvas: Canvas, options: Partial<CenteringGuidelinesConfig> = {}) {
this.canvas = canvas;
Object.assign(this, options);

this.mouseDown = this.mouseDown.bind(this);
this.mouseUp = this.mouseUp.bind(this);
this.objectMoving = this.objectMoving.bind(this);
this.beforeRender = this.beforeRender.bind(this);
this.afterRender = this.afterRender.bind(this);

this.canvas.on('mouse:down', this.mouseDown);
this.canvas.on('mouse:up', this.mouseUp);
this.canvas.on('object:moving', this.objectMoving);
this.canvas.on('before:render', this.beforeRender);
this.canvas.on('after:render', this.afterRender);
}

private get canvasWidthCenter() {
return this.canvas.getWidth() / 2;
}

private get canvasHeightCenter() {
return this.canvas.getHeight() / 2;
}

private isNearCenter(value: number, center: number): boolean {
return Math.abs(Math.round(value) - Math.round(center)) <= this.margin;
}

private mouseDown() {
this.viewportTransform = this.canvas.viewportTransform;
}

private mouseUp() {
this.isInVerticalCenter = false;
this.isInHorizontalCenter = false;
this.canvas.renderAll();
}

private objectMoving(e: MovingEvent) {
const object = e.target;
const objectCenter = object.getCenterPoint();

this.isInVerticalCenter = this.isNearCenter(
objectCenter.x,
this.canvasWidthCenter,
);
this.isInHorizontalCenter = this.isNearCenter(
objectCenter.y,
this.canvasHeightCenter,
);

if (this.isInVerticalCenter || this.isInHorizontalCenter) {
object.setPositionByOrigin(
new Point(
this.isInVerticalCenter ? this.canvasWidthCenter : objectCenter.x,
this.isInHorizontalCenter ? this.canvasHeightCenter : objectCenter.y,
),
'center',
'center',
);
}
}

private beforeRender() {
if (this.canvas.contextTop) {
this.canvas.clearContext(this.canvas.contextTop);
}
}

private afterRender() {
if (this.isInVerticalCenter) {
this.drawVerticalCenterLine();
}
if (this.isInHorizontalCenter) {
this.drawHorizontalCenterLine();
}
}

private drawVerticalCenterLine() {
this.drawLine(
this.canvasWidthCenter + 0.5,
0,
this.canvasWidthCenter + 0.5,
this.canvas.getHeight(),
);
}

private drawHorizontalCenterLine() {
this.drawLine(
0,
this.canvasHeightCenter + 0.5,
this.canvas.getWidth(),
this.canvasHeightCenter + 0.5,
);
}

private drawLine(x1: number, y1: number, x2: number, y2: number) {
const ctx = this.canvas.getSelectionContext();
ctx.save();
ctx.strokeStyle = this.color;
ctx.lineWidth = this.width;
ctx.beginPath();
ctx.moveTo(x1 * this.viewportTransform[0], y1 * this.viewportTransform[3]);
ctx.lineTo(x2 * this.viewportTransform[0], y2 * this.viewportTransform[3]);
ctx.stroke();
ctx.restore();
}

dispose() {
this.canvas.off('mouse:down', this.mouseDown);
this.canvas.off('mouse:up', this.mouseUp);
this.canvas.off('object:moving', this.objectMoving);
this.canvas.off('before:render', this.beforeRender);
this.canvas.off('after:render', this.afterRender);
}
}
3 changes: 3 additions & 0 deletions extensions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
export { AligningGuidelines } from './aligning_guidelines';
export type * from './aligning_guidelines/typedefs';

export { CenteringGuidelines } from './centering_guidelines';
export type { CenteringGuidelinesConfig } from './centering_guidelines';

export {
originUpdaterWrapper,
installOriginWrapperUpdater,
Expand Down
90 changes: 0 additions & 90 deletions lib/centering_guidelines.js

This file was deleted.