npm package: @mlbrgn/image-editor
Description: An image editor web component / custom element
Author: Evertjan Garretsen
Latest version: 1.0.126

# Image Editor

The ImageEditor is a custom web component, built with JavaScript, that allows users to perform various edits on images, such as cropping, resizing, rotating, and applying filters.

# Development

This package is developed inside the mlbrgn-node-workspace monorepo.
Do not run `npm run dev` or `npm install` inside this repository.

Clone the monorepo instead:
https://github.com/evertjanmlbrgn/mlbrgn-node-workspace

---

## Installation and Import

To use the ImageEditor, import it as a "side effect import":

```javascript
import "path/to/ImageEditor.js"
```

This class extends `ImageEditorCustomElement` and does not export anything directly, but registers the custom element.

---

## local / dev test page

to visit the local dev / test page run:

```shell
npm run dev
```

and click the link shown at "Local", e.g. http://localhost:xxxx/ the x's should be replaced with the port number.

## Key Features

The ImageEditor provides the following functionalities:

* **Image Loading and Display:** Loads images from various sources and displays them within the editor.
* **Cropping/Selection:** Allows users to select a specific area of the image for cropping, with support for aspect ratio locking and snapping.
* **Resizing:** Enables resizing of images to specific dimensions.
* **Rotation:** Supports image rotation.
* **Flipping and Flopping:** Provides options to horizontally or vertically flip the image.
* **Image Filtering:** Applies various filters to the image.
* **Dynamic UI Updates:** Reacts to user interactions and updates the UI in real-time.
* **File Size and Format Information:** Displays current and target image file sizes and allows conversion to different formats.
* **Responsive Design:** Adjusts handle sizes for touch devices.

---

## Core Concepts

### Custom Element

The `ImageEditor` is a web component, meaning it's a reusable HTML element (`<image-editor>`) that encapsulates its functionality and styling. It extends `ImageEditorCustomElement`, providing a base for its custom element behavior.

### Shadow DOM

The editor uses **Shadow DOM** (`static shadowTemplate = mainTemplate`) to encapsulate its internal structure and styles, preventing them from interfering with the rest of the document.

### Canvases

The editor utilizes two main HTML canvases:

* **`canvasImage`**: Used for drawing and manipulating the actual image.
* **`canvasDraw`**: Used for drawing selection boxes, handles, grid lines, and other interactive elements on top of the image.

### Image Dimensions and Scaling

The editor carefully manages various dimensions and scaling factors to ensure accurate drawing and display across different resolutions and zoom levels:

* **`editorWidth`, `editorHeight`**: Dimensions of the container holding the canvases.
* **`imageNaturalWidth`, `imageNaturalHeight`**: Original dimensions of the loaded image.
* **`canvasImageWidth`, `canvasImageHeight`**: Calculated dimensions for `canvasImage` to fit within boundaries, considering rotation.
* **`canvasDrawWidth`, `canvasDrawHeight`**: Dimensions for `canvasDraw`, scaled for sharp drawing on potentially low-resolution images.
* **`canvasesCSSScaleRatio`**: Ratio by which the `canvasesWrapper` needs to be scaled for CSS display.
* **`canvasDrawRatio`**: Scaling factor between `canvasImage` and `canvasDraw` for drawing elements correctly.
* **`zoomRatio`**: User-controlled zoom level.

### Selection and Handles

The image editor provides robust selection functionality:

* **`selectionArea`**: Represents the currently selected area of the image in original image coordinates.
* **`selectionAreaScaled`**: The selected area scaled to the `canvasDraw` dimensions for accurate drawing of the selection box and handles.
* **`selectionHandleAreas`**: A collection of `Area` objects representing the interactive handles (corners and edges) for resizing the selection. Each handle has a `type` (corner/edge), `mode` (grab/resize), and `action` (e.g., 'nw-resize').
* **`selectionMode`**: Determines the current interaction mode (e.g., 'select', 'resize', 'grab').
* **`selectionAction`**: Specifies the active resizing or grabbing action (e.g., 'nw-resize', 'grab').
* **`selectionAnimationController`**: Manages the animation of the selection box dashed lines.

### Event Handling

The `ImageEditor` uses an `EventBus` for communication and handling various events, such as status messages (`onCanvasStatusMessage`). It also utilizes `ResizeObserver` to react to changes in the editor's container size.

---

## Public Methods

* **`constructor()`**: Initializes the custom element, creates canvases, disables controls, sets up filters, determines file format support, and adds event listeners.
* **`getImageFileConfiguration()`**: Returns an object defining image file constraints (e.g., `formatsRegex`, `minWidth`, `maxWidth`, `maxUploadFileSize`).
* **`connectedCallback()`**: Lifecycle method called when the custom element is added to the DOM.
* **`disconnectedCallback()`**: Lifecycle method called when the custom element is removed from the DOM.
* **`enableControls(enable)`**: Enables or disables the editor's control elements (menu and canvas buttons).
* **`updateFilter(canvasImageFilter)`**: Applies a CSS filter string to the image canvas and redraws the image.

---

## Private Methods

* **`#createCanvases()`**: Creates and appends the `canvasImage` and `canvasDraw` elements to the DOM.
* **`#initializeResizeObserver()`**: Sets up a `ResizeObserver` to redraw the canvas when the editor's container resizes.
* **`#calculateFileSize(mimeType)`**: Calculates the size of the image on the canvas in the specified MIME type.
* **`#checkConversionSupport(mimeType)`**: Checks if a given MIME type is supported for image conversion by the browser.
* **`#formatBytes(bytes, decimals = 2)`**: Formats a number of bytes into a human-readable string (e.g., "1.2 MB").
* **`#getImageAspectRatioLabel(width, height)`**: Calculates and formats the aspect ratio of an image as a fraction, including common aspect ratio labels.
* **`#calculateCanvasImageSize(imageWidth, imageHeight, rotationAngle)`**: Calculates the required canvas size to fit a rotated image without cropping.
* **`#updateImageDimensions()`**: Recalculates all relevant image and canvas dimensions based on current state (rotation, original image size, editor size).
* **`#calculateCanvasesCssDimensions()`**: Determines the CSS dimensions for the canvases wrapper to fit within the editor's boundaries.
* **`#calculateCssScaleRatio()`**: Calculates the CSS scale factor needed to fit the image within the editor's display area.
* **`#getCorrectedDimension(value)`**: Corrects a dimension value for zooming, CSS scaling, and `canvasDrawRatio`.
* **`#getCorrectedDimension2(value)`**: Corrects a dimension value for zooming and CSS scaling, but not `canvasDrawRatio`.
* **`#dispatchStatusMessage(message)`**: Fires an `onCanvasStatusMessage` event with the given message.
* **`#applyImageDimensions()`**: Applies the calculated width and height to the canvas elements' attributes and CSS styles.
* **`#draw()`**: Main drawing function, which calls `#drawImage()` and `#drawDrawing()`.
* **`#drawImage()`**: Draws the current image onto `canvasImage`, applying rotation, flipping, flopping, and filters.
* **`#resizeImage(resizeWidth, resizeHeight)`**: Resizes the current image to the specified dimensions and updates the editor.
* **`#updateSelectionHandles()`**: Calculates and updates the positions and sizes of the selection handles based on the `selectionArea`.
* **`#calculateGridLines()`**: Calculates the positions of grid and sub-grid lines.
* **`#drawDrawing()`**: Draws the selection box, handles, and grid lines onto `canvasDraw`.
* **`#limitAndSnapSelectedArea(selectedArea)`**: Adjusts the selected area to snap to canvas edges if within a defined threshold.
* **`#checkOutOfBounds(area)`**: Checks if a given area is outside the bounds of the image canvas.
* **`#updateSelectedArea(selectedArea)`**: Updates the `selectionArea` and its scaled version, and then updates selection properties.
* **`#validateSelectedArea(selectedArea)`**: Checks if the `selectedArea` meets the minimum and maximum width/height constraints.
* **`#drawSelectionBox()`**: Draws the dashed selection box on `canvasDraw`.
* **`#startSelectionAnimation()`**: Starts the animation for the selection box dashed lines.
* **`animateSelection()`**: Updates the `animationOffset` for the selection box animation.
* **`#stopSelectionAnimation()`**: Pauses the selection box animation.
* **`#drawGrabCross()`**: Draws a crosshair in the center of the selection area (not currently used in the provided code).
* **`#drawSelectionHandles()`**: Draws the interactive handles around the selection area.
* **`#drawGrid()`**: Draws the grid and sub-grid lines on `canvasDraw`.
* **`#getPointerRelativeToCanvasStart(clientX, clientY)`**: Converts client-side pointer coordinates to coordinates relative to the canvas origin, considering scaling.
* **`#updatePointerStart(clientX, clientY)`**: Updates the starting pointer position for selection/resizing.
* **`#updatePointer(clientX, clientY)`**: Updates the current pointer position.
* **`#getPointerOverArea()`**: Determines which selection handle (if any) the pointer is currently over and updates the cursor.
* **`#getAspectRatioFromCurrentSelection()`**: Returns the aspect ratio of the current selection.
* **`#getAspectRatioValueFromSelect()`**: Retrieves the selected aspect ratio value from the aspect ratio dropdown.
* **`#drawAnchorPoint(anchorPoint)`**: Draws a small rectangle at a given anchor point (used for debugging/visualization).
* **`#applySelectBoxRatioToWidth(area)`**: Adjusts the width of an area to maintain a specific aspect ratio.
* **`#applySelectBoxRatioToHeight(area)`**: Adjusts the height of an area to maintain a specific aspect ratio.
* **`#determineSelectionMode()`**: Determines whether the user is in 'select', 'resize', or 'grab' mode based on pointer position.
* **`#selectStart()`**: Initiates a new selection, clearing any previous selection.
* **`#selectMove(shiftKey = false)`**: Handles pointer movement during selection, adjusting the selected area and redrawing.
* **`#resizeStart()`**: Prepares for a resizing operation by storing the initial selection and determining the anchor point.

---

## Properties

### Static Properties

* **`shadowTemplate`**: Reference to the HTML template for the Shadow DOM.
* **`elementLookup`**: An array of CSS selectors used to find and store references to key DOM elements within the component.
* **`translationsPath`**: The path to the translation files.
* **`observedAttributes`**: An array of attribute names that the component should observe for changes (currently only `disabled`).

### Instance Properties

* **`#overRideAspectRatio`**: Internal property to temporarily override the aspect ratio for selection (e.g., when Shift key is pressed).
* **`#fileSizeOriginal`**: Stores the original file size of the loaded image.
* **`canvasFilters`**: An instance of `CanvasFilters` for managing image filters.
* **`#originalImage`**: Stores the original `Image` object.
* **`#originalImageFile`**: Stores the original `ImageFile` object.
* **`#currentImage`**: Stores the currently displayed `Image` object.
* **`#currentImageId`**: ID of the current image.
* **`#currentImageFile`**: Stores the `ImageFile` object for the currently displayed image.
* **`#initiallySelected`**: Stores the selection area at the start of a resize operation.
* **`#initiallySelectedHalfway`**: Stores the midpoint of the selection area at the start of a resize operation.
* **`selectionAnimationController`**: Controls the animation of the selection box.
* **`editorWidth`**, **`editorHeight`**: Width and height of the `editor` element.
* **`imageNaturalWidth`**, **`imageNaturalHeight`**: Natural (unscaled) width and height of the current image.
* **`canvasImageWidth`**, **`canvasImageHeight`**: Width and height of `canvasImage`.
* **`canvasDrawRatio`**: Scaling ratio between `canvasImage` and `canvasDraw`.
* **`canvasDrawWidth`**, **`canvasDrawHeight`**: Width and height of `canvasDraw`.
* **`canvasesCSSScaleRatio`**: CSS scaling factor for the canvases.
* **`canvasesCSSWidth`**, **`canvasesCSSHeight`**: CSS width and height of the canvases.
* **`canvasImageXOrigin`**, **`canvasImageYOrigin`**: Center point of `canvasImage`.
* **`canvasImageDrawStart`**: Starting point for drawing the image on `canvasImage`.
* **`flipXAxisDirection`**, **`flipYAxisDirection`**: Direction multipliers for horizontal and vertical flipping.
* **`flipXOrigin`**, **`flipYOrigin`**: Origin points for flipping transformations.
* **`imageAspectRatio`**: Calculated aspect ratio of the current image.
* **`imageOrientation`**: Orientation of the current image (Landscape, Portrait, Square).
* **`gridGap`**: Spacing between major grid lines.
* **`lineWidth`**: Line width for drawing on canvases.
* **`selectionLineDashSize`**: Dash size for the selection box line.
* **`crossLineSize`**: Size of the crosshair lines.
* **`handleCornerSize`**, **`handleEdgeSize`**, **`handleEdgeMargin`**: Dimensions for selection handles.
* **`zoomPercentage`**: Current zoom level as a percentage.
* **`canvasImageFilter`**: Current CSS filter string applied to the image.
* **`showGrid`**: Boolean indicating whether the grid should be displayed.
* **`selectionArea`**: An `Area` object representing the current selection.
* **`selectionAreaScaled`**: A scaled version of `selectionArea` for drawing.
* **`selectionHandleAreas`**: An object containing `Area` objects for each selection handle.
* **`selectionValid`**: Boolean indicating if the current selection meets size constraints.
* **`animationOffset`**: Offset for the selection box dash animation.
* **`gridLines`**: Array of objects defining grid line start and end points.
* **`selectionPointerStart`**: `Point` object storing the starting pointer position for an interaction.
* **`selectionPointerCurrent`**: `Point` object storing the current pointer position.
* **`selectionStartPointerOver`**: Information about which handle the pointer was over when an interaction started.
* **`selectionWasTouchEvent`**: Boolean indicating if the last selection interaction was a touch event.

---

## Configuration (`this.config`)

The `ImageEditor` relies on a `config` object (likely inherited from `ImageEditorCustomElement`) for various settings, including:

* **`animateFPS`**: Frames per second for selection animation.
* **`aspectRatioTolerance`**: Tolerance for matching aspect ratios.
* **`minWidth`**, **`maxWidth`**, **`minHeight`**, **`maxHeight`**: Minimum and maximum dimensions for image editing.
* **`maxUploadFileSize`**, **`maxEditFileSize`**: Maximum file sizes for upload and editing.
* **`drawCanvasWidth`**: Target width for the `canvasDraw` element.
* **`gridLineCount`**: Number of major grid lines.
* **`lineWidth`**: Default line width for drawing.
* **`selectionLineDashSize`**: Default dash size for selection box.
* **`crossLineSize`**: Default size for crosshair lines.
* **`handleCornerSize`**, **`handleEdgeSize`**, **`handleEdgeMargin`**: Default sizes for selection handles.
* **`touchHandleMultiplierBreakpoint`**: Breakpoint for adjusting handle sizes on touch devices.
* **`touchHandleMultiplier`**: Multiplier for handle sizes on touch devices.
* **`selectionBoxStrokeStyle`**: CSS stroke style for the selection box.
* **`selectionBoxLineDashStrokeStyle`**: CSS stroke style for the dashed selection box.
* **`selectionBoxInvalidLineDashStrokeStyle`**: CSS stroke style for invalid selection box.
* **`crossStrokeStyle`**: CSS stroke style for the crosshair.
* **`selectionHandleStrokeStyle`**: CSS stroke style for selection handles.
* **`selectionHandleOverFillStyle`**: CSS fill style for hovered selection handles.
* **`selectionHandleLineDashStrokeStyle`**: CSS stroke style for dashed selection handles.
* **`gridStrokeStyle`**: CSS stroke style for major grid lines.
* **`showSubGrid`**: Boolean to enable or disable sub-grid display.
* **`subGridStrokeStyle`**: CSS stroke style for sub-grid lines.
* **`snapThresholdPercentage`**: Percentage threshold for snapping selection to canvas edges.
* **`animateSelection`**: Boolean to enable or disable selection animation.

---

## Usage Example (Conceptual)

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Image Editor</title>
    <style>
        image-editor {
            display: block;
            width: 800px;
            height: 600px;
            border: 1px solid #ccc;
        }
    </style>
</head>
<body>

    <image-editor id="myImageEditor"></image-editor>

    <script type="module">
        import "./path/to/ImageEditor.js"; // Import as side effect

        const imageEditor = document.getElementById('myImageEditor');

        // Example: Load an image
        // This functionality would typically be exposed via methods on the ImageEditorCustomElement
        // or through external controls interacting with the custom element.
        // For demonstration, imagine a function to load an image:
        // imageEditor.loadImage('/path/to/your/image.jpg');

        // Example: Listen for status messages
        document.addEventListener('onCanvasStatusMessage', (event) => {
            console.log('Status:', event.detail.message);
        });

        // You would interact with the image editor through its exposed properties and methods
        // or through events dispatched by the custom element.
    </script>
</body>
</html>


## Events

The ImageEditor custom element dispatches the following events that can be listened to from outside the custom element:

### canvasStatusMessage

Fired when there's a status message from the canvas.

```javascript
imageEditor.addEventListener('canvasStatusMessage', function(event) {
  console.log('Canvas status message:', event.detail.message);
});
```

### closeImageEditor

Fired when the image editor is closed.

```javascript
imageEditor.addEventListener('closeImageEditor', function(event) {
  console.log('Image editor closed');
});
```

### imageSave

Fired when an image is saved.

```javascript
imageEditor.addEventListener('imageSave', function(event) {
  console.log('Image saved:', event.detail.id, event.detail.file);
});
```

## Usage Example

```javascript
document.addEventListener("DOMContentLoaded", function () {
  let imageEditor = document.querySelector('#image-editor');

  // Listen to events from the custom element
  imageEditor.addEventListener('canvasStatusMessage', function(event) {
    console.log('Canvas status message:', event.detail.message);
  });

  imageEditor.addEventListener('closeImageEditor', function(event) {
    console.log('Image editor closed');
  });

  imageEditor.addEventListener('imageSave', function(event) {
    console.log('Image saved:', event.detail.id, event.detail.file);
  });

  imageEditor.setImage('heart', 'test/images/heart.png', 'heart');
}, false);
```

```

---