fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
214 lines (183 loc) • 5.08 kB
text/typescript
import type { Point } from '../Point';
import { Group } from '../shapes/Group';
import { Shadow } from '../Shadow';
import { Rect } from '../shapes/Rect';
import { getRandomInt } from '../util/internals/getRandomInt';
import type { Canvas } from '../canvas/Canvas';
import { BaseBrush } from './BaseBrush';
import type { SprayBrushPoint } from './typedefs';
import { CENTER } from '../constants';
/**
*
* @param rects
* @returns
*/
function getUniqueRects(rects: Rect[]) {
const uniqueRects: Record<string, boolean> = {};
const uniqueRectsArray: Rect[] = [];
for (let i = 0, key: string; i < rects.length; i++) {
key = `${rects[i].left}${rects[i].top}`;
if (!uniqueRects[key]) {
uniqueRects[key] = true;
uniqueRectsArray.push(rects[i]);
}
}
return uniqueRectsArray;
}
export class SprayBrush extends BaseBrush {
/**
* Width of a spray
* @type Number
* @default
*/
width = 10;
/**
* Density of a spray (number of dots per chunk)
* @type Number
* @default
*/
density = 20;
/**
* Width of spray dots
* @type Number
* @default
*/
dotWidth = 1;
/**
* Width variance of spray dots
* @type Number
* @default
*/
dotWidthVariance = 1;
/**
* Whether opacity of a dot should be random
* @type Boolean
* @default
*/
randomOpacity = false;
/**
* Whether overlapping dots (rectangles) should be removed (for performance reasons)
* @type Boolean
* @default
*/
optimizeOverlapping = true;
private declare sprayChunks: SprayBrushPoint[][];
private declare sprayChunk: SprayBrushPoint[];
/**
* Constructor
* @param {Canvas} canvas
* @return {SprayBrush} Instance of a spray brush
*/
constructor(canvas: Canvas) {
super(canvas);
this.sprayChunks = [];
this.sprayChunk = [];
}
/**
* Invoked on mouse down
* @param {Point} pointer
*/
onMouseDown(pointer: Point) {
this.sprayChunks = [];
this.canvas.clearContext(this.canvas.contextTop);
this._setShadow();
this.addSprayChunk(pointer);
this.renderChunck(this.sprayChunk);
}
/**
* Invoked on mouse move
* @param {Point} pointer
*/
onMouseMove(pointer: Point) {
if (this.limitedToCanvasSize === true && this._isOutSideCanvas(pointer)) {
return;
}
this.addSprayChunk(pointer);
this.renderChunck(this.sprayChunk);
}
/**
* Invoked on mouse up
*/
onMouseUp() {
const originalRenderOnAddRemove = this.canvas.renderOnAddRemove;
this.canvas.renderOnAddRemove = false;
const rects: Rect[] = [];
for (let i = 0; i < this.sprayChunks.length; i++) {
const sprayChunk = this.sprayChunks[i];
for (let j = 0; j < sprayChunk.length; j++) {
const chunck = sprayChunk[j];
const rect = new Rect({
width: chunck.width,
height: chunck.width,
left: chunck.x + 1,
top: chunck.y + 1,
originX: CENTER,
originY: CENTER,
fill: this.color,
});
rects.push(rect);
}
}
const group = new Group(
this.optimizeOverlapping ? getUniqueRects(rects) : rects,
{
objectCaching: true,
subTargetCheck: false,
interactive: false,
},
);
this.shadow && group.set('shadow', new Shadow(this.shadow));
this.canvas.fire('before:path:created', { path: group });
this.canvas.add(group);
this.canvas.fire('path:created', { path: group });
this.canvas.clearContext(this.canvas.contextTop);
this._resetShadow();
this.canvas.renderOnAddRemove = originalRenderOnAddRemove;
this.canvas.requestRenderAll();
}
renderChunck(sprayChunck: SprayBrushPoint[]) {
const ctx = this.canvas.contextTop;
ctx.fillStyle = this.color;
this._saveAndTransform(ctx);
for (let i = 0; i < sprayChunck.length; i++) {
const point = sprayChunck[i];
ctx.globalAlpha = point.opacity;
ctx.fillRect(point.x, point.y, point.width, point.width);
}
ctx.restore();
}
/**
* Render all spray chunks
*/
_render() {
const ctx = this.canvas.contextTop;
ctx.fillStyle = this.color;
this._saveAndTransform(ctx);
for (let i = 0; i < this.sprayChunks.length; i++) {
this.renderChunck(this.sprayChunks[i]);
}
ctx.restore();
}
/**
* @param {Point} pointer
*/
addSprayChunk(pointer: Point) {
this.sprayChunk = [];
const radius = this.width / 2;
for (let i = 0; i < this.density; i++) {
this.sprayChunk.push({
x: getRandomInt(pointer.x - radius, pointer.x + radius),
y: getRandomInt(pointer.y - radius, pointer.y + radius),
width: this.dotWidthVariance
? getRandomInt(
// bottom clamp width to 1
Math.max(1, this.dotWidth - this.dotWidthVariance),
this.dotWidth + this.dotWidthVariance,
)
: this.dotWidth,
opacity: this.randomOpacity ? getRandomInt(0, 100) / 100 : 1,
});
}
this.sprayChunks.push(this.sprayChunk);
}
}