fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
422 lines (377 loc) • 11.5 kB
text/typescript
import { config } from '../config';
import { SHARED_ATTRIBUTES } from '../parser/attributes';
import { parseAttributes } from '../parser/parseAttributes';
import type { XY } from '../Point';
import { Point } from '../Point';
import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints';
import { toFixed } from '../util/misc/toFixed';
import {
getBoundsOfCurve,
joinPath,
makePathSimpler,
parsePath,
} from '../util/path';
import { classRegistry } from '../ClassRegistry';
import { FabricObject, cacheProperties } from './Object/FabricObject';
import type {
TComplexPathData,
TPathSegmentInfo,
TSimplePathData,
} from '../util/path/typedefs';
import type { FabricObjectProps, SerializedObjectProps } from './Object/types';
import type { ObjectEvents } from '../EventTypeDefs';
import type {
TBBox,
TClassProperties,
TSVGReviver,
TOptions,
} from '../typedefs';
import { CENTER, LEFT, TOP } from '../constants';
import type { CSSRules } from '../parser/typedefs';
interface UniquePathProps {
sourcePath?: string;
path?: TSimplePathData;
}
export interface SerializedPathProps
extends SerializedObjectProps,
UniquePathProps {}
export interface PathProps extends FabricObjectProps, UniquePathProps {}
export interface IPathBBox extends TBBox {
left: number;
top: number;
pathOffset: Point;
}
export class Path<
Props extends TOptions<PathProps> = Partial<PathProps>,
SProps extends SerializedPathProps = SerializedPathProps,
EventSpec extends ObjectEvents = ObjectEvents,
> extends FabricObject<Props, SProps, EventSpec> {
/**
* Array of path points
* @type Array
* @default
*/
declare path: TSimplePathData;
declare pathOffset: Point;
declare sourcePath?: string;
declare segmentsInfo?: TPathSegmentInfo[];
static type = 'Path';
static cacheProperties = [...cacheProperties, 'path', 'fillRule'];
/**
* Constructor
* @param {TComplexPathData} path Path data (sequence of coordinates and corresponding "command" tokens)
* @param {Partial<PathProps>} [options] Options object
* @return {Path} thisArg
*/
constructor(
path: TComplexPathData | string,
// todo: evaluate this spread here
{ path: _, left, top, ...options }: Partial<Props> = {},
) {
super();
Object.assign(this, Path.ownDefaults);
this.setOptions(options);
this._setPath(path || [], true);
typeof left === 'number' && this.set(LEFT, left);
typeof top === 'number' && this.set(TOP, top);
}
/**
* @private
* @param {TComplexPathData | string} path Path data (sequence of coordinates and corresponding "command" tokens)
* @param {boolean} [adjustPosition] pass true to reposition the object according to the bounding box
* @returns {Point} top left position of the bounding box, useful for complementary positioning
*/
_setPath(path: TComplexPathData | string, adjustPosition?: boolean) {
this.path = makePathSimpler(Array.isArray(path) ? path : parsePath(path));
this.setBoundingBox(adjustPosition);
}
/**
* This function is an helper for svg import. it returns the center of the object in the svg
* untransformed coordinates, by look at the polyline/polygon points.
* @private
* @return {Point} center point from element coordinates
*/
_findCenterFromElement(): Point {
const bbox = this._calcBoundsFromPath();
return new Point(bbox.left + bbox.width / 2, bbox.top + bbox.height / 2);
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx context to render path on
*/
_renderPathCommands(ctx: CanvasRenderingContext2D) {
const l = -this.pathOffset.x,
t = -this.pathOffset.y;
ctx.beginPath();
for (const command of this.path) {
switch (
command[0] // first letter
) {
case 'L': // lineto, absolute
ctx.lineTo(command[1] + l, command[2] + t);
break;
case 'M': // moveTo, absolute
ctx.moveTo(command[1] + l, command[2] + t);
break;
case 'C': // bezierCurveTo, absolute
ctx.bezierCurveTo(
command[1] + l,
command[2] + t,
command[3] + l,
command[4] + t,
command[5] + l,
command[6] + t,
);
break;
case 'Q': // quadraticCurveTo, absolute
ctx.quadraticCurveTo(
command[1] + l,
command[2] + t,
command[3] + l,
command[4] + t,
);
break;
case 'Z':
ctx.closePath();
break;
}
}
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx context to render path on
*/
_render(ctx: CanvasRenderingContext2D) {
this._renderPathCommands(ctx);
this._renderPaintInOrder(ctx);
}
/**
* Returns string representation of an instance
* @return {string} string representation of an instance
*/
toString() {
return `#<Path (${this.complexity()}): { "top": ${this.top}, "left": ${
this.left
} }>`;
}
/**
* Returns object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toObject<
T extends Omit<Props & TClassProperties<this>, keyof SProps>,
K extends keyof T = never,
>(propertiesToInclude: K[] = []): Pick<T, K> & SProps {
return {
...super.toObject(propertiesToInclude),
path: this.path.map((pathCmd) => pathCmd.slice()),
};
}
/**
* Returns dataless object representation of an instance
* @param {Array} [propertiesToInclude] Any properties that you might want to additionally include in the output
* @return {Object} object representation of an instance
*/
toDatalessObject<
T extends Omit<Props & TClassProperties<this>, keyof SProps>,
K extends keyof T = never,
>(propertiesToInclude: K[] = []): Pick<T, K> & SProps {
const o = this.toObject<T, K>(propertiesToInclude);
if (this.sourcePath) {
delete o.path;
o.sourcePath = this.sourcePath;
}
return o;
}
/**
* Returns svg representation of an instance
* @return {Array} an array of strings with the specific svg representation
* of the instance
*/
_toSVG() {
const path = joinPath(this.path, config.NUM_FRACTION_DIGITS);
return [
'<path ',
'COMMON_PARTS',
`d="${path}" stroke-linecap="round" />\n`,
];
}
/**
* @private
* @return the path command's translate transform attribute
*/
_getOffsetTransform() {
const digits = config.NUM_FRACTION_DIGITS;
return ` translate(${toFixed(-this.pathOffset.x, digits)}, ${toFixed(
-this.pathOffset.y,
digits,
)})`;
}
/**
* Returns svg clipPath representation of an instance
* @param {Function} [reviver] Method for further parsing of svg representation.
* @return {string} svg representation of an instance
*/
toClipPathSVG(reviver?: TSVGReviver): string {
const additionalTransform = this._getOffsetTransform();
return (
'\t' +
this._createBaseClipPathSVGMarkup(this._toSVG(), {
reviver,
additionalTransform: additionalTransform,
})
);
}
/**
* Returns svg representation of an instance
* @param {Function} [reviver] Method for further parsing of svg representation.
* @return {string} svg representation of an instance
*/
toSVG(reviver?: TSVGReviver): string {
const additionalTransform = this._getOffsetTransform();
return this._createBaseSVGMarkup(this._toSVG(), {
reviver,
additionalTransform: additionalTransform,
});
}
/**
* Returns number representation of an instance complexity
* @return {number} complexity of this instance
*/
complexity() {
return this.path.length;
}
setDimensions() {
this.setBoundingBox();
}
setBoundingBox(adjustPosition?: boolean) {
const { width, height, pathOffset } = this._calcDimensions();
this.set({ width, height, pathOffset });
// using pathOffset because it match the use case.
// if pathOffset change here we need to use left + width/2 , top + height/2
adjustPosition && this.setPositionByOrigin(pathOffset, CENTER, CENTER);
}
_calcBoundsFromPath(): TBBox {
const bounds: XY[] = [];
let subpathStartX = 0,
subpathStartY = 0,
x = 0, // current x
y = 0; // current y
for (const command of this.path) {
// current instruction
switch (
command[0] // first letter
) {
case 'L': // lineto, absolute
x = command[1];
y = command[2];
bounds.push({ x: subpathStartX, y: subpathStartY }, { x, y });
break;
case 'M': // moveTo, absolute
x = command[1];
y = command[2];
subpathStartX = x;
subpathStartY = y;
break;
case 'C': // bezierCurveTo, absolute
bounds.push(
...getBoundsOfCurve(
x,
y,
command[1],
command[2],
command[3],
command[4],
command[5],
command[6],
),
);
x = command[5];
y = command[6];
break;
case 'Q': // quadraticCurveTo, absolute
bounds.push(
...getBoundsOfCurve(
x,
y,
command[1],
command[2],
command[1],
command[2],
command[3],
command[4],
),
);
x = command[3];
y = command[4];
break;
case 'Z':
x = subpathStartX;
y = subpathStartY;
break;
}
}
return makeBoundingBoxFromPoints(bounds);
}
/**
* @private
*/
_calcDimensions(): IPathBBox {
const bbox = this._calcBoundsFromPath();
return {
...bbox,
pathOffset: new Point(
bbox.left + bbox.width / 2,
bbox.top + bbox.height / 2,
),
};
}
/**
* List of attribute names to account for when parsing SVG element (used by `Path.fromElement`)
* @static
* @memberOf Path
* @see http://www.w3.org/TR/SVG/paths.html#PathElement
*/
static ATTRIBUTE_NAMES = [...SHARED_ATTRIBUTES, 'd'];
/**
* Creates an instance of Path from an object
* @static
* @memberOf Path
* @param {Object} object
* @returns {Promise<Path>}
*/
static fromObject<T extends TOptions<SerializedPathProps>>(object: T) {
return this._fromObject<Path>(object, {
extraParam: 'path',
});
}
/**
* Creates an instance of Path from an SVG <path> element
* @static
* @memberOf Path
* @param {HTMLElement} element to parse
* @param {Partial<PathProps>} [options] Options object
*/
static async fromElement(
element: HTMLElement,
options: Partial<PathProps>,
cssRules?: CSSRules,
) {
const { d, ...parsedAttributes } = parseAttributes(
element,
this.ATTRIBUTE_NAMES,
cssRules,
);
return new this(d, {
...parsedAttributes,
...options,
// we pass undefined to instruct the constructor to position the object using the bbox
left: undefined,
top: undefined,
});
}
}
classRegistry.setClass(Path);
classRegistry.setSVGClass(Path);
/* _FROM_SVG_START_ */