UNPKG

@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.

246 lines (245 loc) 9.02 kB
import { ShapeState } from "../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. */ export class Shape { /** The shape definition, proxied to trigger observer notifications on change. */ _definition; /** The style settings for the shape, proxied to trigger observer notifications on change. */ _style; /** The options for configuring the shape, proxied to trigger observer notifications on change. */ _options; /** The current state of the shape, representing its visual or interactive status. */ _state = ShapeState.Default; /** List of observer functions to be notified on shape changes. */ observers = []; /** * 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) { 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 = { ...this._style }; // Remove state-specific keys from the base style object Object.values(ShapeState).forEach((state) => { delete baseStyle[state]; }); // If we are in a non-default state, apply the state-specific overrides if (this._state !== 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); } }