@avolutions/canvas-painter
Version:
CanvasPainter.js is a simple yet powerful JavaScript library for drawing basic shapes (rectangles, circles, etc.) on HTML5 Canvas with ease. Perfect for creating 2D graphics in your web projects.
244 lines (243 loc) • 8.9 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Shape = void 0;
const ShapeState_js_1 = require("../common/ShapeState.js");
/**
* Abstract class representing a generic shape with observer functionality.
*
* @typeParam TDefinition - The type of shape definition implementing IShapeDefinition.
* @typeParam TStyle - The type of shape style implementing IShapeStyle.
* @typeParam TOptions - The type of shape options implementing IShapeOptions.
*/
class Shape {
/**
* Constructs a Shape instance and wraps the definition, style, and options in a Proxy to handle change notifications.
*
* @param definition - The shape definition instance to be wrapped in a Proxy.
* @param style - Optional style settings for the shape.
* @param options - Optional configuration options for the shape.
*/
constructor(definition, style, options) {
/** The current state of the shape, representing its visual or interactive status. */
this._state = ShapeState_js_1.ShapeState.Default;
/** List of observer functions to be notified on shape changes. */
this.observers = [];
this._definition = this._createProxy(definition);
this._style = this._createProxy(style); // Default to an empty object if no style is provided
this._options = this._createProxy(options); // Default to an empty object if no options are provided
}
/**
* Creates a proxy for the given object to track changes and notify observers.
*
* @param obj - The object to be proxied.
* @returns A proxied object that triggers observer notifications on change.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_createProxy(obj) {
if (typeof obj === 'object' && obj !== null) {
// Recursively apply proxy to all nested objects
Object.keys(obj).forEach(key => {
obj[key] = this._createProxy(obj[key]);
});
return new Proxy(obj, {
set: (target, prop, value) => {
const oldValue = target[prop];
// Only notify if the value actually changes
if (oldValue !== value) {
target[prop] = value;
this.notifyObservers();
return true;
}
return true;
},
get: (target, prop) => {
const value = target[prop];
// Ensure nested properties are also proxied
if (typeof value === 'object' && value !== null) {
return this._createProxy(value); // Return the proxy for nested objects
}
return value;
}
});
}
return obj; // If it's not an object, just return the value as is
}
/**
* Converts the shape's definition to an array.
*
* @returns An array representation of the shape's definition.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
toArray() {
return this._definition.toArray();
}
/**
* Converts the shape's definition to a JSON string.
*
* @returns A JSON string representation of the shape's definition.
*/
toJson() {
return this._definition.toJson();
}
/**
* Makes the shape visible, allowing it to be rendered on the canvas.
* If the shape was previously hidden, calling this method will make it appear
* during the next rendering cycle.
*/
show() {
this._options.visible = true;
}
/**
* Hides the shape, preventing it from being rendered on the canvas.
* The shape will still exist and retain its properties, but it will not
* appear during rendering until `show()` is called.
*/
hide() {
this._options.visible = false;
}
/**
* Checks whether the shape is currently visible.
*
* @returns Returns true if the shape is visible and will be rendered on the canvas.
* Returns false if the shape is hidden and will not be rendered.
*/
isVisible() {
return !!this._options.visible;
}
/**
* Checks whether the shape is draggable or not.
*
* @returns Returns true if the shape is draggable.
* Returns false if the shape is not draggable.
*/
isDraggable() {
return !!this._options.draggable;
}
/**
* Adds an observer function that will be called when the shape's state changes.
*
* @param observer - The observer callback function.
*/
addObserver(observer) {
if (!this.observers.includes(observer)) {
this.observers.push(observer);
}
}
/**
* Removes a previously added observer function.
*
* @param observer - The observer callback function to be removed.
*/
removeObserver(observer) {
const index = this.observers.indexOf(observer);
if (index !== -1) {
this.observers.splice(index, 1);
}
}
/**
* Notifies all registered observers of a change in the shape's state.
* This method is triggered when a property of the shape definition, style, or options is changed.
*/
notifyObservers() {
this.observers.forEach(observer => observer());
}
/**
* Gets the style settings of the shape.
*
* @returns The current style settings.
*/
get style() {
return this._style;
}
/**
* Updates the style settings of the shape and notifies observers.
*
* @param style - The new style settings to apply.
*/
set style(style) {
Object.assign(this._style, style);
}
/**
* Gets the configuration options of the shape.
*
* @returns The current options.
*/
get options() {
return this._options;
}
/**
* Updates the configuration options of the shape and notifies observers.
*
* @param options - The new options to apply.
*/
set options(options) {
Object.assign(this._options, options);
}
/**
* Gets the current state of the shape.
*
* @returns The current state of the shape.
*/
get state() {
return this._state;
}
/**
* Sets a new state for the shape.
*
* @param state - The new state to assign to the shape.
*/
set state(state) {
// Update state if not already in this state and notify observers to apply state styles
if (state !== this._state) {
this._state = state;
this.notifyObservers();
}
}
/**
* Retrieves the effective style of the shape based on its current state.
*
* @returns The computed style object for the current shape state, with state-specific overrides merged in as necessary.
*/
get stateStyle() {
// Start with a shallow copy of the default style, excluding state-specific keys
const baseStyle = Object.assign({}, this._style);
// Remove state-specific keys from the base style object
Object.values(ShapeState_js_1.ShapeState).forEach((state) => {
delete baseStyle[state];
});
// If we are in a non-default state, apply the state-specific overrides
if (this._state !== ShapeState_js_1.ShapeState.Default) {
const stateOverrides = this._style[this._state];
if (stateOverrides) {
// Merge the overrides into the base style to apply state-specific values
for (const key in stateOverrides) {
if (stateOverrides[key] !== undefined) {
baseStyle[key] = stateOverrides[key];
}
}
}
}
// Return a new Proxy to handle both single property access and full object access
return new Proxy(baseStyle, {
get: (target, prop) => {
// Directly return properties on baseStyle for single property access
return prop in target ? target[prop] : undefined;
},
});
}
/**
* Determines if the current state style includes a visible border.
*
* @returns `true` if `borderColor` and `borderWidth` are defined and indicate a visible border; otherwise, `false`.
*/
hasBorder() {
return (this.stateStyle &&
'borderColor' in this.stateStyle &&
'borderWidth' in this.stateStyle &&
typeof this.stateStyle.borderColor === 'string' &&
typeof this.stateStyle.borderWidth === 'number' &&
this.stateStyle.borderColor !== '' &&
this.stateStyle.borderWidth > 0);
}
}
exports.Shape = Shape;