fabric
Version:
Object model for HTML5 canvas, and SVG-to-canvas parser. Backed by jsdom and node-canvas.
293 lines (264 loc) • 7.93 kB
text/typescript
import { SHARED_ATTRIBUTES } from '../parser/attributes';
import { parseAttributes } from '../parser/parseAttributes';
import type { Abortable, TClassProperties, TOptions } from '../typedefs';
import { classRegistry } from '../ClassRegistry';
import { FabricObject, cacheProperties } from './Object/FabricObject';
import { Point } from '../Point';
import { isFiller } from '../util/typeAssertions';
import type { FabricObjectProps, SerializedObjectProps } from './Object/types';
import type { ObjectEvents } from '../EventTypeDefs';
import { makeBoundingBoxFromPoints } from '../util';
import { CENTER, LEFT, TOP } from '../constants';
import type { CSSRules } from '../parser/typedefs';
// @TODO this code is terrible and Line should be a special case of polyline.
const coordProps = ['x1', 'x2', 'y1', 'y2'] as const;
interface UniqueLineProps {
x1: number;
x2: number;
y1: number;
y2: number;
}
export interface SerializedLineProps
extends SerializedObjectProps,
UniqueLineProps {}
export class Line<
Props extends TOptions<FabricObjectProps> = Partial<FabricObjectProps>,
SProps extends SerializedLineProps = SerializedLineProps,
EventSpec extends ObjectEvents = ObjectEvents,
>
extends FabricObject<Props, SProps, EventSpec>
implements UniqueLineProps
{
/**
* x value or first line edge
* @type number
* @default
*/
declare x1: number;
/**
* y value or first line edge
* @type number
* @default
*/
declare y1: number;
/**
* x value or second line edge
* @type number
* @default
*/
declare x2: number;
/**
* y value or second line edge
* @type number
* @default
*/
declare y2: number;
static type = 'Line';
static cacheProperties = [...cacheProperties, ...coordProps];
/**
* Constructor
* @param {Array} [points] Array of points
* @param {Object} [options] Options object
* @return {Line} thisArg
*/
constructor([x1, y1, x2, y2] = [0, 0, 0, 0], options: Partial<Props> = {}) {
super();
Object.assign(this, Line.ownDefaults);
this.setOptions(options);
this.x1 = x1;
this.x2 = x2;
this.y1 = y1;
this.y2 = y2;
this._setWidthHeight();
const { left, top } = options;
typeof left === 'number' && this.set(LEFT, left);
typeof top === 'number' && this.set(TOP, top);
}
/**
* @private
* @param {Object} [options] Options
*/
_setWidthHeight() {
const { x1, y1, x2, y2 } = this;
this.width = Math.abs(x2 - x1);
this.height = Math.abs(y2 - y1);
const { left, top, width, height } = makeBoundingBoxFromPoints([
{ x: x1, y: y1 },
{ x: x2, y: y2 },
]);
const position = new Point(left + width / 2, top + height / 2);
this.setPositionByOrigin(position, CENTER, CENTER);
}
/**
* @private
* @param {String} key
* @param {*} value
*/
_set(key: string, value: any) {
super._set(key, value);
if (coordProps.includes(key as keyof UniqueLineProps)) {
// this doesn't make sense very much, since setting x1 when top or left
// are already set, is just going to show a strange result since the
// line will move way more than the developer expect.
// in fabric5 it worked only when the line didn't have extra transformations,
// in fabric6 too. With extra transform they behave bad in different ways.
// This needs probably a good rework or a tutorial if you have to create a dynamic line
this._setWidthHeight();
}
return this;
}
/**
* @private
* @param {CanvasRenderingContext2D} ctx Context to render on
*/
_render(ctx: CanvasRenderingContext2D) {
ctx.beginPath();
const p = this.calcLinePoints();
ctx.moveTo(p.x1, p.y1);
ctx.lineTo(p.x2, p.y2);
ctx.lineWidth = this.strokeWidth;
// TODO: test this
// make sure setting "fill" changes color of a line
// (by copying fillStyle to strokeStyle, since line is stroked, not filled)
const origStrokeStyle = ctx.strokeStyle;
if (isFiller(this.stroke)) {
ctx.strokeStyle = this.stroke.toLive(ctx)!;
} else {
ctx.strokeStyle = this.stroke ?? ctx.fillStyle;
}
this.stroke && this._renderStroke(ctx);
ctx.strokeStyle = origStrokeStyle;
}
/**
* This function is an helper for svg import. it returns the center of the object in the svg
* untransformed coordinates
* @private
* @return {Point} center point from element coordinates
*/
_findCenterFromElement(): Point {
return new Point((this.x1 + this.x2) / 2, (this.y1 + this.y2) / 2);
}
/**
* Returns object representation of an instance
* @method toObject
* @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),
...this.calcLinePoints(),
};
}
/*
* Calculate object dimensions from its properties
* @private
*/
_getNonTransformedDimensions(): Point {
const dim = super._getNonTransformedDimensions();
if (this.strokeLineCap === 'butt') {
if (this.width === 0) {
dim.y -= this.strokeWidth;
}
if (this.height === 0) {
dim.x -= this.strokeWidth;
}
}
return dim;
}
/**
* Recalculates line points given width and height
* Those points are simply placed around the center,
* This is not useful outside internal render functions and svg output
* Is not meant to be for the developer.
* @private
*/
calcLinePoints(): UniqueLineProps {
const { x1: _x1, x2: _x2, y1: _y1, y2: _y2, width, height } = this;
const xMult = _x1 <= _x2 ? -1 : 1,
yMult = _y1 <= _y2 ? -1 : 1,
x1 = (xMult * width) / 2,
y1 = (yMult * height) / 2,
x2 = (xMult * -width) / 2,
y2 = (yMult * -height) / 2;
return {
x1,
x2,
y1,
y2,
};
}
/* _FROM_SVG_START_ */
/**
* Returns svg representation of an instance
* @return {Array} an array of strings with the specific svg representation
* of the instance
*/
_toSVG() {
const { x1, x2, y1, y2 } = this.calcLinePoints();
return [
'<line ',
'COMMON_PARTS',
`x1="${x1}" y1="${y1}" x2="${x2}" y2="${y2}" />\n`,
];
}
/**
* List of attribute names to account for when parsing SVG element (used by {@link Line.fromElement})
* @static
* @memberOf Line
* @see http://www.w3.org/TR/SVG/shapes.html#LineElement
*/
static ATTRIBUTE_NAMES = SHARED_ATTRIBUTES.concat(coordProps);
/**
* Returns Line instance from an SVG element
* @static
* @memberOf Line
* @param {HTMLElement} element Element to parse
* @param {Object} [options] Options object
* @param {Function} [callback] callback function invoked after parsing
*/
static async fromElement(
element: HTMLElement,
options: Abortable,
cssRules?: CSSRules,
) {
const {
x1 = 0,
y1 = 0,
x2 = 0,
y2 = 0,
...parsedAttributes
} = parseAttributes(element, this.ATTRIBUTE_NAMES, cssRules);
return new this([x1, y1, x2, y2], parsedAttributes);
}
/* _FROM_SVG_END_ */
/**
* Returns Line instance from an object representation
* @static
* @memberOf Line
* @param {Object} object Object to create an instance from
* @returns {Promise<Line>}
*/
static fromObject<T extends TOptions<SerializedLineProps>>({
x1,
y1,
x2,
y2,
...object
}: T) {
return this._fromObject<Line>(
{
...object,
points: [x1, y1, x2, y2],
},
{
extraParam: 'points',
},
);
}
}
classRegistry.setClass(Line);
classRegistry.setSVGClass(Line);