import { AbstractCanvas } from '../Canvas'; import { Observable } from '../Observable'; import { distanceFromOrigin } from '../../Utils/Calc2D'; import uuidv4 from 'uuid/v4'; export enum CanvasObjectEvents { OnChange = 'onChange', } export abstract class AbstractCanvasObject { // base properties protected _x = 0; protected _y = 0; protected _rotation = 0; protected _scaling = 1; protected _uuid: number = uuidv4(); /** * use only to set different scaling for the adapter in setScaling() * can be useful if we have to work with a smaller preview image which should be shown in its real size, * keeping the original scaling value * Notes: * - this value should not be saved with the object data, it has to be provided explicitly * - this value has no relation to any other attribute (eg. Image.src or Image.imageData) * - the responsibility of updating always belongs to the consumer of this package */ protected _scaleFactor = 1; // important: do not modify width and height directly, // they should only have getters if needed, all the others should be done by scaling // technical properties protected _wrappedObject: any = null; protected _selected = false; protected _locked = false; protected _autoFit = false; protected _grabbed = false; protected _activeAction = ''; protected _userData = {}; protected _canvas: AbstractCanvas = null; protected _observable: Observable = new Observable(this); // this should be provided by every canvas object adapter public static type; constructor(options?: any) { this.initOptions(options); this._observable.on(CanvasObjectEvents.OnChange, this.updateConstraints.bind(this)); } initOptions(options?: any) { if (options) { for (const key of Object.keys(options)) { this[`_${key}`] = options[key]; } } return this; } async updateOptions(options?: any) { if (options) { for (const key of Object.keys(options)) { if (Object.prototype.hasOwnProperty.call(this, '_' + key) && key !== 'uuid') { this[key] = options[key]; } } } return this; } get uuid() { return this._uuid; } get type() { if (this.constructor.type) { return this.constructor.type; } throw new TypeError('Type is not defined for this canvas object!'); } set x(x: number) { if (x === this._x) { return; } this._x = x; this.setPosition(this._x, this._y); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get x(): number { return this._x; } set y(y: number) { if (y === this._y) { return; } this._y = y; this.setPosition(this._x, this._y); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get y(): number { return this._y; } set rotation(rotation: number) { const rot = (360 + rotation) % 360; if (rot === this._rotation) { return; } this._rotation = rot; this.setRotation(this._rotation); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get rotation(): number { return this._rotation; } set scaling(scaling: number) { if (scaling === this._scaling || !Number.isFinite(scaling)) { return; } this._scaling = scaling; this.setScalingWithFactor(scaling); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get scaling(): number { return this._scaling; } set wrappedObject(object: any) { this._wrappedObject = object; this.setOptionsFromObject(this.toObject()); this.initObjectEvents(); } get wrappedObject(): any { return this._wrappedObject; } set selected(selected: boolean) { this._selected = selected; if (selected) { this.canvas.setSelectedObject(this); } else { this.canvas.deselectObject(this); } this.setSelected(selected); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get selected(): boolean { return this._selected; } set locked(locked: boolean) { this._locked = locked; this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get locked(): boolean { return this._locked; } set autoFit(autoFit: boolean) { this._autoFit = autoFit; this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get autoFit(): boolean { return this._autoFit; } get userData(): Record { return this._userData; } set userData(userDataObject: Record) { this._userData = userDataObject; this._observable.triggerEvent(CanvasObjectEvents.OnChange); } set grabbed(grabbed) { this._grabbed = grabbed; this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get grabbed() { return this._grabbed; } set activeAction(action: string) { this._activeAction = action; this._observable.triggerEvent(CanvasObjectEvents.OnChange); } get activeAction(): string { return this._activeAction; } set canvas(canvas: AbstractCanvas) { this._canvas = canvas; } get canvas() { return this._canvas; } get isFirst() { return this.getIndex() === 0; } get isLast() { return this.getIndex() === this.canvas.getLength() - 1; } /** * Calls the setScaling method with the given scaling multiplied by the scaleFactor * @param scaling */ setScalingWithFactor(scaling: number): void { this.setScaling(scaling * this._scaleFactor); } abstract getOptionsFromWrappedObject(): any; abstract setPosition(x: number, y: number): void; abstract setRotation(rotation: number): void; abstract setScaling(scaling: number): void; abstract setSelected(selected: boolean): void; abstract setLocked(locked: boolean): void; abstract initObjectEvents(): void; abstract getTransformedRect(): any; abstract getCenter(): any; abstract getSize(): any; abstract getBoundSize(): any; on(eventName: string, callback: (target: any, eventData: any) => any) { this._observable.on(eventName, callback); } off(eventName: string, callback: (target: any, eventData: any) => any) { this._observable.off(eventName, callback); } getState() { const { selected, grabbed, isFirst, isLast, locked, autoFit } = this; return { ...this.toObject(), selected, grabbed, isFirst, isLast, locked, autoFit, }; } getDistanceToCenter(x: number, y: number) { const center = this.getCenter(); return distanceFromOrigin(x, y, center.x, center.y); } remove() { this.canvas.remove(this); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } getIndex() { return this.canvas.indexOfObject(this); } setIndex(newIdx: number, overwrite = false) { this.canvas.setObjectIndex(this, newIdx, overwrite); this._observable.triggerEvent(CanvasObjectEvents.OnChange); } toObject(includeWrappedObject = false) { const { type, x, y, rotation, scaling, locked, autoFit, userData, uuid } = this; const obj = { type, x, y, rotation, scaling, locked, autoFit, userData, uuid, }; if (includeWrappedObject) { obj.__wrappedObject = this.wrappedObjectToObject(); } return obj; } abstract wrappedObjectToObject(): any; setOptionsFromObject(obj: any) { const { x, y, rotation, scaling } = obj; this.setPosition(x, y); this.setRotation(rotation); this.setScalingWithFactor(scaling); } alignTo(vertical, horizontal) { let { x, y } = this; const { height, width } = this.getBoundSize(); switch (vertical) { case 'top': y = height / 2; break; case 'bottom': y = this.canvas.getSize().height - height / 2; break; case 'center': y = this.canvas.getSize().height / 2; break; } switch (horizontal) { case 'left': x = width / 2; break; case 'right': x = this.canvas.getSize().width - width / 2; break; case 'center': x = this.canvas.getSize().width / 2; break; } this.x = x; this.y = y; } fitCanvas(enlarge = false, updatePosition = true) { const { width: cw, height: ch } = this.canvas.getSize(); const { width: ow, height: oh } = this.getBoundSize(); const maxRatio = Math.max(ow / cw, oh / ch); if (maxRatio > 1 || (maxRatio < 1 && enlarge)) { this.scaling *= 1 / maxRatio; } if (updatePosition) { this.x = cw / 2; this.y = ch / 2; } } getScaledSize() { const { width: ow, height: oh } = this.getSize(); const { scaling, _scaleFactor } = this; return { width: ow * scaling * _scaleFactor, height: oh * scaling * _scaleFactor, }; } updateConstraints() { this.autoFit && this.fitCanvas(true, true); } }