UNPKG

fabric

Version:

Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.

437 lines (399 loc) 13.4 kB
import { config } from '../config'; import { SHARED_ATTRIBUTES } from '../parser/attributes'; import { parseAttributes } from '../parser/parseAttributes'; import { parsePointsAttribute } from '../parser/parsePointsAttribute'; import type { XY } from '../Point'; import { Point } from '../Point'; import type { Abortable, TClassProperties, TOptions } from '../typedefs'; import { classRegistry } from '../ClassRegistry'; import { makeBoundingBoxFromPoints } from '../util/misc/boundingBoxFromPoints'; import { calcDimensionsMatrix, transformPoint } from '../util/misc/matrix'; import { projectStrokeOnPoints } from '../util/misc/projectStroke'; import type { TProjectStrokeOnPointsOptions } from '../util/misc/projectStroke/types'; import { degreesToRadians } from '../util/misc/radiansDegreesConversion'; import { toFixed } from '../util/misc/toFixed'; import { FabricObject, cacheProperties } from './Object/FabricObject'; import type { FabricObjectProps, SerializedObjectProps } from './Object/types'; import type { ObjectEvents } from '../EventTypeDefs'; import { CENTER, LEFT, SCALE_X, SCALE_Y, SKEW_X, SKEW_Y, TOP, } from '../constants'; import type { CSSRules } from '../parser/typedefs'; export const polylineDefaultValues: Partial<TClassProperties<Polyline>> = { /** * @deprecated transient option soon to be removed in favor of a different design */ exactBoundingBox: false, }; export interface SerializedPolylineProps extends SerializedObjectProps { points: XY[]; } export class Polyline< Props extends TOptions<FabricObjectProps> = Partial<FabricObjectProps>, SProps extends SerializedPolylineProps = SerializedPolylineProps, EventSpec extends ObjectEvents = ObjectEvents, > extends FabricObject<Props, SProps, EventSpec> { /** * Points array * @type Array * @default */ declare points: XY[]; /** * WARNING: Feature in progress * Calculate the exact bounding box taking in account strokeWidth on acute angles * this will be turned to true by default on fabric 6.0 * maybe will be left in as an optimization since calculations may be slow * @deprecated transient option soon to be removed in favor of a different design * @type Boolean * @default false */ declare exactBoundingBox: boolean; private declare initialized: true | undefined; static ownDefaults = polylineDefaultValues; static type = 'Polyline'; static getDefaults(): Record<string, any> { return { ...super.getDefaults(), ...Polyline.ownDefaults, }; } /** * A list of properties that if changed trigger a recalculation of dimensions * @todo check if you really need to recalculate for all cases */ static layoutProperties: (keyof Polyline)[] = [ SKEW_X, SKEW_Y, 'strokeLineCap', 'strokeLineJoin', 'strokeMiterLimit', 'strokeWidth', 'strokeUniform', 'points', ]; declare pathOffset: Point; declare strokeOffset: Point; static cacheProperties = [...cacheProperties, 'points']; strokeDiff: Point; /** * Constructor * @param {Array} points Array of points (where each point is an object with x and y) * @param {Object} [options] Options object * @return {Polyline} thisArg * @example * var poly = new Polyline([ * { x: 10, y: 10 }, * { x: 50, y: 30 }, * { x: 40, y: 70 }, * { x: 60, y: 50 }, * { x: 100, y: 150 }, * { x: 40, y: 100 } * ], { * stroke: 'red', * left: 100, * top: 100 * }); */ constructor(points: XY[] = [], options: Props = {} as Props) { super(); Object.assign(this, Polyline.ownDefaults); this.setOptions(options); this.points = points; const { left, top } = options; this.initialized = true; this.setBoundingBox(true); typeof left === 'number' && this.set(LEFT, left); typeof top === 'number' && this.set(TOP, top); } protected isOpen() { return true; } private _projectStrokeOnPoints(options: TProjectStrokeOnPointsOptions) { return projectStrokeOnPoints(this.points, options, this.isOpen()); } /** * Calculate the polygon bounding box * @private */ _calcDimensions(options?: Partial<TProjectStrokeOnPointsOptions>) { options = { scaleX: this.scaleX, scaleY: this.scaleY, skewX: this.skewX, skewY: this.skewY, strokeLineCap: this.strokeLineCap, strokeLineJoin: this.strokeLineJoin, strokeMiterLimit: this.strokeMiterLimit, strokeUniform: this.strokeUniform, strokeWidth: this.strokeWidth, ...(options || {}), }; const points = this.exactBoundingBox ? this._projectStrokeOnPoints( options as TProjectStrokeOnPointsOptions, ).map((projection) => projection.projectedPoint) : this.points; if (points.length === 0) { return { left: 0, top: 0, width: 0, height: 0, pathOffset: new Point(), strokeOffset: new Point(), strokeDiff: new Point(), }; } const bbox = makeBoundingBoxFromPoints(points), // Remove scale effect, since it's applied after matrix = calcDimensionsMatrix({ ...options, scaleX: 1, scaleY: 1 }), bboxNoStroke = makeBoundingBoxFromPoints( this.points.map((p) => transformPoint(p, matrix, true)), ), scale = new Point(this.scaleX, this.scaleY); let offsetX = bbox.left + bbox.width / 2, offsetY = bbox.top + bbox.height / 2; if (this.exactBoundingBox) { offsetX = offsetX - offsetY * Math.tan(degreesToRadians(this.skewX)); // Order of those assignments is important. // offsetY relies on offsetX being already changed by the line above offsetY = offsetY - offsetX * Math.tan(degreesToRadians(this.skewY)); } return { ...bbox, pathOffset: new Point(offsetX, offsetY), strokeOffset: new Point(bboxNoStroke.left, bboxNoStroke.top) .subtract(new Point(bbox.left, bbox.top)) .multiply(scale), strokeDiff: new Point(bbox.width, bbox.height) .subtract(new Point(bboxNoStroke.width, bboxNoStroke.height)) .multiply(scale), }; } /** * 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 = makeBoundingBoxFromPoints(this.points); return new Point(bbox.left + bbox.width / 2, bbox.top + bbox.height / 2); } setDimensions() { this.setBoundingBox(); } setBoundingBox(adjustPosition?: boolean) { const { left, top, width, height, pathOffset, strokeOffset, strokeDiff } = this._calcDimensions(); this.set({ width, height, pathOffset, strokeOffset, strokeDiff }); adjustPosition && this.setPositionByOrigin( new Point(left + width / 2, top + height / 2), CENTER, CENTER, ); } /** * @deprecated intermidiate method to be removed, do not use */ protected isStrokeAccountedForInDimensions() { return this.exactBoundingBox; } /** * @override stroke is taken in account in size */ _getNonTransformedDimensions() { return this.exactBoundingBox ? // TODO: fix this new Point(this.width, this.height) : super._getNonTransformedDimensions(); } /** * @override stroke and skewing are taken into account when projecting stroke on points, * therefore we don't want the default calculation to account for skewing as well. * Though it is possible to pass `width` and `height` in `options`, doing so is very strange, use with discretion. * * @private */ _getTransformedDimensions(options: any = {}) { if (this.exactBoundingBox) { let size: Point; /* When `strokeUniform = true`, any changes to the properties require recalculating the `width` and `height` because the stroke projections are affected. When `strokeUniform = false`, we don't need to recalculate for scale transformations, as the effect of scale on projections follows a linear function (e.g. scaleX of 2 just multiply width by 2)*/ if ( Object.keys(options).some( (key) => this.strokeUniform || (this.constructor as typeof Polyline).layoutProperties.includes( key as keyof TProjectStrokeOnPointsOptions, ), ) ) { const { width, height } = this._calcDimensions(options); size = new Point(options.width ?? width, options.height ?? height); } else { size = new Point( options.width ?? this.width, options.height ?? this.height, ); } return size.multiply( new Point(options.scaleX || this.scaleX, options.scaleY || this.scaleY), ); } else { return super._getTransformedDimensions(options); } } /** * Recalculates dimensions when changing skew and scale * @private */ _set(key: string, value: any) { const changed = this.initialized && this[key as keyof this] !== value; const output = super._set(key, value); if ( this.exactBoundingBox && changed && (((key === SCALE_X || key === SCALE_Y) && this.strokeUniform && (this.constructor as typeof Polyline).layoutProperties.includes( 'strokeUniform', )) || (this.constructor as typeof Polyline).layoutProperties.includes( key as keyof Polyline, )) ) { this.setDimensions(); } return output; } /** * 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), points: this.points.map(({ x, y }) => ({ x, y })), }; } /** * Returns svg representation of an instance * @return {Array} an array of strings with the specific svg representation * of the instance */ _toSVG() { const points = [], diffX = this.pathOffset.x, diffY = this.pathOffset.y, NUM_FRACTION_DIGITS = config.NUM_FRACTION_DIGITS; for (let i = 0, len = this.points.length; i < len; i++) { points.push( toFixed(this.points[i].x - diffX, NUM_FRACTION_DIGITS), ',', toFixed(this.points[i].y - diffY, NUM_FRACTION_DIGITS), ' ', ); } return [ `<${ (this.constructor as typeof Polyline).type.toLowerCase() as | 'polyline' | 'polygon' } `, 'COMMON_PARTS', `points="${points.join('')}" />\n`, ]; } /** * @private * @param {CanvasRenderingContext2D} ctx Context to render on */ _render(ctx: CanvasRenderingContext2D) { const len = this.points.length, x = this.pathOffset.x, y = this.pathOffset.y; if (!len || isNaN(this.points[len - 1].y)) { // do not draw if no points or odd points // NaN comes from parseFloat of a empty string in parser return; } ctx.beginPath(); ctx.moveTo(this.points[0].x - x, this.points[0].y - y); for (let i = 0; i < len; i++) { const point = this.points[i]; ctx.lineTo(point.x - x, point.y - y); } !this.isOpen() && ctx.closePath(); this._renderPaintInOrder(ctx); } /** * Returns complexity of an instance * @return {Number} complexity of this instance */ complexity(): number { return this.points.length; } /* _FROM_SVG_START_ */ /** * List of attribute names to account for when parsing SVG element (used by {@link Polyline.fromElement}) * @static * @memberOf Polyline * @see: http://www.w3.org/TR/SVG/shapes.html#PolylineElement */ static ATTRIBUTE_NAMES = [...SHARED_ATTRIBUTES]; /** * Returns Polyline instance from an SVG element * @static * @memberOf Polyline * @param {HTMLElement} element Element to parser * @param {Object} [options] Options object */ static async fromElement( element: HTMLElement, options: Abortable, cssRules?: CSSRules, ) { const points = parsePointsAttribute(element.getAttribute('points')), // we omit left and top to instruct the constructor to position the object using the bbox // eslint-disable-next-line @typescript-eslint/no-unused-vars { left, top, ...parsedAttributes } = parseAttributes( element, this.ATTRIBUTE_NAMES, cssRules, ); return new this(points, { ...parsedAttributes, ...options, }); } /* _FROM_SVG_END_ */ /** * Returns Polyline instance from an object representation * @static * @memberOf Polyline * @param {Object} object Object to create an instance from * @returns {Promise<Polyline>} */ static fromObject<T extends TOptions<SerializedPolylineProps>>(object: T) { return this._fromObject<Polyline>(object, { extraParam: 'points', }); } } classRegistry.setClass(Polyline); classRegistry.setSVGClass(Polyline);