canvico-editor 1.0.2 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +31 -8
  3. package/dist/CanvicoEditor.d.ts +45 -24
  4. package/dist/CanvicoEditor.d.ts.map +1 -1
  5. package/dist/CanvicoEditor.js +262 -87
  6. package/dist/CanvicoEditor.js.map +1 -1
  7. package/dist/modules/BaseModule.d.ts +5 -4
  8. package/dist/modules/BaseModule.d.ts.map +1 -1
  9. package/dist/modules/BaseModule.js +7 -6
  10. package/dist/modules/BaseModule.js.map +1 -1
  11. package/dist/modules/CropModule.d.ts +32 -31
  12. package/dist/modules/CropModule.d.ts.map +1 -1
  13. package/dist/modules/CropModule.js +147 -106
  14. package/dist/modules/CropModule.js.map +1 -1
  15. package/dist/modules/ResizeModule.d.ts +12 -26
  16. package/dist/modules/ResizeModule.d.ts.map +1 -1
  17. package/dist/modules/ResizeModule.js +27 -35
  18. package/dist/modules/ResizeModule.js.map +1 -1
  19. package/dist/modules/TransformModule.d.ts +38 -0
  20. package/dist/modules/TransformModule.d.ts.map +1 -0
  21. package/dist/modules/TransformModule.js +55 -0
  22. package/dist/modules/TransformModule.js.map +1 -0
  23. package/dist/state/CanvasState.d.ts +34 -0
  24. package/dist/state/CanvasState.d.ts.map +1 -0
  25. package/dist/state/CanvasState.js +67 -0
  26. package/dist/state/CanvasState.js.map +1 -0
  27. package/dist/state/EditorReducer.d.ts +60 -0
  28. package/dist/state/EditorReducer.d.ts.map +1 -0
  29. package/dist/state/EditorReducer.js +127 -0
  30. package/dist/state/EditorReducer.js.map +1 -0
  31. package/dist/types.d.ts +33 -0
  32. package/dist/types.d.ts.map +1 -1
  33. package/dist/utils/dom-manager.d.ts +12 -1
  34. package/dist/utils/dom-manager.d.ts.map +1 -1
  35. package/dist/utils/dom-manager.js +22 -14
  36. package/dist/utils/dom-manager.js.map +1 -1
  37. package/dist/utils/error-handler.d.ts +18 -7
  38. package/dist/utils/error-handler.d.ts.map +1 -1
  39. package/dist/utils/error-handler.js +34 -7
  40. package/dist/utils/error-handler.js.map +1 -1
  41. package/dist/utils/validation.d.ts +0 -7
  42. package/dist/utils/validation.d.ts.map +1 -1
  43. package/dist/utils/validation.js +33 -29
  44. package/dist/utils/validation.js.map +1 -1
  45. package/package.json +6 -4
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  The MIT License (MIT)
2
2
 
3
- Copyright (c) 2025 [Your Name or GitHub username]
3
+ Copyright (c) 2025 Krzysztof Umiński
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -6,13 +6,21 @@ A simple and extensible image editor built with TypeScript and the Canvas API. D
6
6
 
7
7
  ## Features
8
8
 
9
- - **Image Loading**: Load images from a local file input.
10
- - **Resizing**: Dynamically resize the image with an option to keep the aspect ratio.
11
- - **Cropping**: User-friendly crop tool with a movable and resizable selection box.
12
- - **Saving**: Download the edited image as a PNG file.
13
- - **Modularity**: Easily extend the editor with new modules (e.g., filters, text, etc.).
14
- - **Robust Error Handling**: Centralized error management for consistent and descriptive messages.
15
- - **Strong Typing**: Fully typed API for improved Developer Experience.
9
+ - **Canvas Editing Core**: Load, resize, crop, rotate, flip, and export images in-browser.
10
+ - **Cross-Input Crop UX**: Crop interactions support mouse and touch input.
11
+ - **Modular Design**: Resize, Crop, and Transform are isolated modules for easier maintenance and extension.
12
+ - **State-Driven Architecture**: Central `CanvasState` store with reducer-based updates for predictable editor behavior.
13
+ - **Type-Safe API**: Fully typed configuration and public interfaces for better DX in TypeScript projects.
14
+ - **Input Validation**: Built-in file type and file size checks before processing.
15
+ - **Structured Error Reporting**: Optional `onError` callback with contextual metadata (`source`, `operation`, `timestamp`).
16
+ - **Flexible Logging**: Toggle internal console logging with `logErrorsToConsole`.
17
+ - **Lifecycle-Safe Cleanup**: `destroy()` removes internal listeners and avoids leaking host app handlers.
18
+
19
+ Behavior note:
20
+ Resize values and aspect-ratio lock work in output image space (`currentImage` dimensions), while the on-screen canvas is a fit-to-container preview.
21
+
22
+ Supported input formats:
23
+ `image/jpeg`, `image/png`, `image/webp`.
16
24
 
17
25
  ## Installation
18
26
 
@@ -28,11 +36,18 @@ npm install canvico-editor
28
36
  import { CanvicoEditor } from "canvico-editor";
29
37
 
30
38
  const canvico = new CanvicoEditor({
31
- containerSelector: `.canvico-containerr`,
39
+ containerSelector: `.canvico-container`,
32
40
  imageFileInputSelector: ".input-upload-file",
33
41
  resetEditsButtonSelector: "#resetEdit",
34
42
  clearCanvasButtonSelector: "#cleanAll",
35
43
  saveButtonSelector: "#saveBtn",
44
+ maxFileSizeMB: 5,
45
+ strictModuleSelectors: true, // optional: fail fast on invalid module selectors
46
+ logErrorsToConsole: true,
47
+ onError: ({ error, source, operation, timestamp }) => {
48
+ // Integrate with your logger/monitoring tool here
49
+ console.error("[CanvicoEditor]", timestamp.toISOString(), source, operation, error);
50
+ },
36
51
  modules: {
37
52
  resize: {
38
53
  widthInputSelector: "#widthInput",
@@ -45,8 +60,16 @@ const canvico = new CanvicoEditor({
45
60
  frameColor: "#d84cb9",
46
61
  outsideOverlayColor: "rgba(0,0,0,0.2)",
47
62
  },
63
+ transform: {
64
+ rotateInputSelector: "#rotateDeg",
65
+ flipHorizontalButtonSelector: "#flipH",
66
+ flipVerticalButtonSelector: "#flipV",
67
+ },
48
68
  },
49
69
  });
70
+
71
+ // In SPA/framework apps, clean up on unmount:
72
+ // canvico.destroy();
50
73
  ```
51
74
 
52
75
  ## Documentation
@@ -1,21 +1,26 @@
1
1
  import { CanvicoEditorConfig } from './types.js';
2
2
  export declare class CanvicoEditor {
3
3
  /** Manages all DOM element interactions and selections. */
4
- private dom;
5
- private canvas;
6
- private ctx;
7
- /** The original, unmodified image loaded by the user. */
8
- private initialImage?;
9
- /** The image currently being displayed and edited, including all modifications. */
10
- private currentImage?;
4
+ private readonly dom;
5
+ private readonly canvas;
6
+ private readonly ctx;
7
+ /** Central shared document state. */
8
+ private readonly state;
11
9
  /** A map holding all registered and initialized modules. */
12
- private modules;
10
+ private readonly modules;
11
+ private resizeModule?;
12
+ private cropModule?;
13
+ private transformModule?;
13
14
  /** Handles and logs errors that occur within the editor. */
14
- private errorHandler;
15
+ private readonly errorHandler;
15
16
  /** The configuration options passed to the editor upon instantiation. */
16
- private config;
17
- private DEFAULT_MODULE;
17
+ private readonly config;
18
+ private readonly DEFAULT_MODULE;
18
19
  private activeModuleName;
20
+ private renderScheduled;
21
+ private cleanupCallbacks;
22
+ private isDestroyed;
23
+ private asyncOperationVersion;
19
24
  /**
20
25
  * Creates an instance of CanvasImageEditor.
21
26
  * @param config - The configuration object for the editor.
@@ -52,36 +57,52 @@ export declare class CanvicoEditor {
52
57
  * Initializes and registers all available modules based on the provided options.
53
58
  */
54
59
  private _registerModules;
60
+ private _addManagedListener;
61
+ private _getOutputDimensions;
62
+ private _getPreviewDimensions;
55
63
  /**
56
64
  * Resets the canvas to display the given image, scaled to fit the container.
57
- * @param {HTMLImageElement} image - The image to display.
58
- * @internal
65
+ * This should ONLY be called when loading a new image or explicitly resetting the view.
59
66
  */
60
67
  private _resetCanvasView;
61
68
  /**
62
- * Sets which module is currently active, deactivating all others.
63
- * If the same module is activated again (and it's not the default), it toggles it off,
64
- * returning to the default module.
65
- * @param {ModuleName | null} moduleName - The name of the module to activate, or null to deactivate all.
66
- * @internal
69
+ * Sets which module is currently active.
70
+ * Crop mode disables interactions with the remaining modules until exited or applied.
67
71
  */
68
72
  private _setActiveModule;
73
+ /**
74
+ * Disables or enables UI elements for Resize and Transform modules.
75
+ */
76
+ private _toggleNonCropInteractions;
69
77
  /**
70
78
  * Handles the file input change event to load, validate, and display an image.
71
- * @param {Event} event - The file input change event.
72
- * @internal
73
79
  */
74
80
  private _loadImage;
75
81
  /**
76
- * Callback for the CropModule after a crop is applied. Updates the current image with the cropped version.
77
- * @param {string} newImageDataUrl - The data URL of the newly cropped image.
78
- * @internal
82
+ * Callback for the CropModule after a crop is applied.
79
83
  */
80
84
  private _handleCropApplied;
85
+ private _applyCropAndExit;
86
+ private _applyImageDataUrl;
87
+ private _onStateChange;
88
+ private _enterCropMode;
89
+ private _requestRender;
81
90
  /**
82
91
  * Clears the canvas, redraws the base image, and then draws the overlay for the currently active module.
83
- * @internal
84
92
  */
85
93
  private _redraw;
94
+ private _drawImageLayer;
95
+ private _exportCurrentViewDataUrl;
96
+ /**
97
+ * Creates a new image based on the current crop state.
98
+ * Crop rectangle is defined in preview space and remapped to output space.
99
+ */
100
+ private _bakeCrop;
101
+ private _normalizeRect;
102
+ private _beginAsyncOperation;
103
+ private _cancelPendingAsyncOperations;
104
+ private _isAsyncOperationActive;
105
+ private _createAbortError;
106
+ private _isAbortError;
86
107
  }
87
108
  //# sourceMappingURL=CanvicoEditor.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"CanvicoEditor.d.ts","sourceRoot":"","sources":["../src/CanvicoEditor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAW,MAAM,YAAY,CAAC;AAc/D,qBAAa,aAAa;IACtB,2DAA2D;IAC3D,OAAO,CAAC,GAAG,CAAa;IAExB,OAAO,CAAC,MAAM,CAAoB;IAClC,OAAO,CAAC,GAAG,CAA2B;IAEtC,yDAAyD;IACzD,OAAO,CAAC,YAAY,CAAC,CAAmB;IACxC,mFAAmF;IACnF,OAAO,CAAC,YAAY,CAAC,CAAmB;IAExC,4DAA4D;IAC5D,OAAO,CAAC,OAAO,CAAmC;IAElD,4DAA4D;IAC5D,OAAO,CAAC,YAAY,CAAe;IAEnC,yEAAyE;IACzE,OAAO,CAAC,MAAM,CAAsB;IAEpC,OAAO,CAAC,cAAc,CAA2B;IACjD,OAAO,CAAC,gBAAgB,CAA2B;IAEnD;;;;OAIG;gBACS,MAAM,EAAE,mBAAmB;IAgBvC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAczB;;OAEG;IACI,OAAO,IAAI,IAAI;IAatB;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACH,OAAO,CAAC,SAAS;IAoBjB;;OAEG;IACH,OAAO,CAAC,UAAU;IAelB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IAezB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAqCxB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAqBxB;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IA4BxB;;;;OAIG;IACH,OAAO,CAAC,UAAU;IAsClB;;;;OAIG;IACH,OAAO,CAAC,kBAAkB;IAc1B;;;OAGG;IACH,OAAO,CAAC,OAAO;CAclB"}
1
+ {"version":3,"file":"CanvicoEditor.d.ts","sourceRoot":"","sources":["../src/CanvicoEditor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAAW,MAAM,YAAY,CAAC;AAiB/D,qBAAa,aAAa;IACtB,2DAA2D;IAC3D,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAa;IAEjC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAA2B;IAE/C,qCAAqC;IACrC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAkC;IAExD,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;IAC3D,OAAO,CAAC,YAAY,CAAC,CAAe;IACpC,OAAO,CAAC,UAAU,CAAC,CAAa;IAChC,OAAO,CAAC,eAAe,CAAC,CAAkB;IAE1C,4DAA4D;IAC5D,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAe;IAE5C,yEAAyE;IACzE,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAsB;IAE7C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2B;IAC1D,OAAO,CAAC,gBAAgB,CAA2B;IACnD,OAAO,CAAC,eAAe,CAAS;IAChC,OAAO,CAAC,gBAAgB,CAAyB;IACjD,OAAO,CAAC,WAAW,CAAS;IAC5B,OAAO,CAAC,qBAAqB,CAAK;IAElC;;;;OAIG;gBACS,MAAM,EAAE,mBAAmB;IAsBvC;;;;OAIG;IACH,OAAO,CAAC,iBAAiB;IAczB;;OAEG;IACI,OAAO,IAAI,IAAI;IAgBtB;;OAEG;IACH,OAAO,CAAC,WAAW;IAQnB;;OAEG;IACH,OAAO,CAAC,SAAS;IAmBjB;;OAEG;IACH,OAAO,CAAC,UAAU;IAsBlB;;OAEG;IACH,OAAO,CAAC,iBAAiB;IASzB;;OAEG;IACH,OAAO,CAAC,gBAAgB;IAsCxB,OAAO,CAAC,mBAAmB;IAK3B,OAAO,CAAC,oBAAoB;IAW5B,OAAO,CAAC,qBAAqB;IAW7B;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAexB;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAqCxB;;OAEG;IACH,OAAO,CAAC,0BAA0B;IAwBlC;;OAEG;IACH,OAAO,CAAC,UAAU;IA+DlB;;OAEG;IACH,OAAO,CAAC,kBAAkB;YAOZ,iBAAiB;IA4B/B,OAAO,CAAC,kBAAkB;IAwB1B,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,cAAc;IAmBtB;;OAEG;IACH,OAAO,CAAC,OAAO;IAef,OAAO,CAAC,eAAe;IAavB,OAAO,CAAC,yBAAyB;IAejC;;;OAGG;IACH,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,6BAA6B;IAIrC,OAAO,CAAC,uBAAuB;IAI/B,OAAO,CAAC,iBAAiB;IAMzB,OAAO,CAAC,aAAa;CAGxB"}
@@ -1,34 +1,48 @@
1
- import { ErrorHandler as h } from "./utils/error-handler.js";
2
- import { DOMManager as l } from "./utils/dom-manager.js";
3
- import { createFeatureNotSupportedError as c, createCanvasContextError as d, createImageSaveError as m, validateFile as u, createImageLoadError as n } from "./utils/validation.js";
4
- import { ResizeModule as g } from "./modules/ResizeModule.js";
5
- import { CropModule as v } from "./modules/CropModule.js";
6
- class M {
1
+ import { ErrorHandler as E } from "./utils/error-handler.js";
2
+ import { DOMManager as y } from "./utils/dom-manager.js";
3
+ import { createFeatureNotSupportedError as A, createCanvasContextError as C, createImageSaveError as _, validateFile as I, createImageLoadError as m } from "./utils/validation.js";
4
+ import { CanvasState as b } from "./state/CanvasState.js";
5
+ import { ResizeModule as D } from "./modules/ResizeModule.js";
6
+ import { CropModule as x } from "./modules/CropModule.js";
7
+ import { TransformModule as z } from "./modules/TransformModule.js";
8
+ class T {
7
9
  /** Manages all DOM element interactions and selections. */
8
10
  dom;
9
11
  canvas;
10
12
  ctx;
11
- /** The original, unmodified image loaded by the user. */
12
- initialImage;
13
- /** The image currently being displayed and edited, including all modifications. */
14
- currentImage;
13
+ /** Central shared document state. */
14
+ state = new b();
15
15
  /** A map holding all registered and initialized modules. */
16
16
  modules = /* @__PURE__ */ new Map();
17
+ resizeModule;
18
+ cropModule;
19
+ transformModule;
17
20
  /** Handles and logs errors that occur within the editor. */
18
21
  errorHandler;
19
22
  /** The configuration options passed to the editor upon instantiation. */
20
23
  config;
21
24
  DEFAULT_MODULE = null;
22
25
  activeModuleName = null;
26
+ renderScheduled = !1;
27
+ cleanupCallbacks = [];
28
+ isDestroyed = !1;
29
+ asyncOperationVersion = 0;
23
30
  /**
24
31
  * Creates an instance of CanvasImageEditor.
25
32
  * @param config - The configuration object for the editor.
26
33
  * @throws {Error} If a required feature like FileReader is not supported by the browser.
27
34
  */
28
35
  constructor(e) {
29
- if (this.config = e, this.errorHandler = new h(), !window.FileReader)
30
- throw c("FileReader API is not supported by this browser.");
31
- this.dom = new l(e, this.errorHandler), [this.canvas, this.ctx] = this._initializeCanvas(), this._registerModules(), this._bindGlobalEvents();
36
+ if (this.config = e, this.errorHandler = new E({
37
+ onError: e.onError,
38
+ logToConsole: e.logErrorsToConsole
39
+ }), !globalThis.FileReader) {
40
+ const i = A("FileReader API is not supported by this browser.");
41
+ throw this.errorHandler.handle(i, { source: "validation", operation: "constructor:file-reader-check" }), i;
42
+ }
43
+ this.dom = new y(e, this.errorHandler), [this.canvas, this.ctx] = this._initializeCanvas(), this._registerModules(), this._bindGlobalEvents();
44
+ const t = this.state.subscribe((i) => this._onStateChange(i));
45
+ this.cleanupCallbacks.push(t);
32
46
  }
33
47
  /**
34
48
  * Creates the canvas element and its 2D rendering context.
@@ -38,7 +52,7 @@ class M {
38
52
  _initializeCanvas() {
39
53
  const e = document.createElement("canvas"), t = e.getContext("2d");
40
54
  if (!t)
41
- throw d();
55
+ throw C();
42
56
  return this.dom.elements.container.appendChild(e), [e, t];
43
57
  }
44
58
  // --- Public API & User Actions ---
@@ -46,40 +60,42 @@ class M {
46
60
  * Cleans up all resources, event listeners, and modules to safely remove the editor instance.
47
61
  */
48
62
  destroy() {
49
- this._setActiveModule(null), this.dom.elements.imageFileInput.replaceWith(this.dom.elements.imageFileInput.cloneNode(!0)), this.dom.elements.clearCanvasButton.replaceWith(this.dom.elements.clearCanvasButton.cloneNode(!0)), this.dom.elements.saveButton.replaceWith(this.dom.elements.saveButton.cloneNode(!0)), this.modules.forEach((e) => e.destroy()), this.modules.clear();
63
+ this.isDestroyed || (this.isDestroyed = !0, this._cancelPendingAsyncOperations(), this._setActiveModule(null, !1, !0), this.cleanupCallbacks.forEach((e) => e()), this.cleanupCallbacks = [], this.modules.forEach((e) => e.destroy()), this.modules.clear());
50
64
  }
51
65
  /**
52
66
  * Resets the current image to its original state, discarding all changes.
53
67
  */
54
68
  _resetImage() {
55
- this.initialImage && (this.currentImage = this.initialImage, this._resetCanvasView(this.initialImage), this._setActiveModule(this.DEFAULT_MODULE));
69
+ this.state.getInitial() && (this.state.resetToInitial(), this._setActiveModule(this.DEFAULT_MODULE, !1, !0));
56
70
  }
57
71
  /**
58
72
  * Clears the canvas and resets the entire editor state, including loaded images and module states.
59
73
  */
60
74
  _cleanAll() {
61
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.canvas.width = 0, this.canvas.height = 0, this.initialImage = void 0, this.currentImage = void 0, this.dom.resizeElements && (this.dom.resizeElements.widthInput.value = "", this.dom.resizeElements.heightInput.value = "", this.dom.resizeElements.lockAspectRatio && (this.dom.resizeElements.lockAspectRatio.checked = !1)), this.dom.elements.imageFileInput.value = "", this._setActiveModule(this.DEFAULT_MODULE);
75
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.canvas.width = 0, this.canvas.height = 0, this.state.clear(), this.dom.resizeElements && (this.dom.resizeElements.widthInput.value = "", this.dom.resizeElements.heightInput.value = "", this.dom.resizeElements.lockAspectRatio && (this.dom.resizeElements.lockAspectRatio.checked = !1)), this.dom.elements.imageFileInput.value = "", this._setActiveModule(null, !1, !0);
62
76
  }
63
77
  /**
64
78
  * Triggers a download of the current canvas content as a PNG image.
65
79
  */
66
80
  _saveImage() {
67
- if (!this.currentImage) {
68
- this.errorHandler.handle(m());
81
+ if (!this.state.getCurrent()) {
82
+ this.errorHandler.handle(_(), { source: "editor", operation: "save-image:no-image" });
83
+ return;
84
+ }
85
+ const e = this._exportCurrentViewDataUrl();
86
+ if (!e) {
87
+ this.errorHandler.handle(_(), { source: "editor", operation: "save-image:export-failed" });
69
88
  return;
70
89
  }
71
- const e = document.createElement("a"), t = this.dom.elements.imageFileInput.files?.[0], i = t ? t.name.replace(/\.[^/.]+$/, "") : "image";
72
- e.download = `${i}-edited.png`, e.href = this.canvas.toDataURL("image/png"), e.click();
90
+ const t = document.createElement("a"), i = this.dom.elements.imageFileInput.files?.[0], s = i ? i.name.replace(/\.[^/.]+$/, "") : "image";
91
+ t.download = `${s}-edited.png`, t.href = e, t.click();
73
92
  }
74
93
  // --- Initialization Methods ---
75
94
  /**
76
95
  * Binds event listeners to the main control elements like file input, save, and reset buttons.
77
96
  */
78
97
  _bindGlobalEvents() {
79
- this.dom.elements.imageFileInput.addEventListener("change", (e) => this._loadImage(e)), this.dom.cropElements && this.dom.cropElements.activateButton.addEventListener("click", () => this.currentImage && this._setActiveModule(
80
- "crop"
81
- /* CROP */
82
- )), this.dom.elements.resetEditsButton.addEventListener("click", () => this._resetImage()), this.dom.elements.clearCanvasButton.addEventListener("click", () => this._cleanAll()), this.dom.elements.saveButton.addEventListener("click", () => this._saveImage());
98
+ this._addManagedListener(this.dom.elements.imageFileInput, "change", (e) => this._loadImage(e)), this._addManagedListener(this.dom.elements.resetEditsButton, "click", () => this._resetImage()), this._addManagedListener(this.dom.elements.clearCanvasButton, "click", () => this._cleanAll()), this._addManagedListener(this.dom.elements.saveButton, "click", () => this._saveImage());
83
99
  }
84
100
  // --- Core Drawing & State Logic ---
85
101
  /**
@@ -87,103 +103,262 @@ class M {
87
103
  */
88
104
  _registerModules() {
89
105
  if (this.dom.resizeElements) {
90
- const e = new g(this.dom.resizeElements, {
91
- container: this.dom.elements.container,
92
- canvas: this.canvas,
93
- ctx: this.ctx,
94
- getCurrentImage: () => this.currentImage
106
+ const e = new D(this.dom.resizeElements, {
107
+ state: this.state,
108
+ errorHandler: this.errorHandler
95
109
  });
96
- this.modules.set("resize", e), this.DEFAULT_MODULE = "resize";
110
+ this.modules.set("resize", e), this.resizeModule = e;
97
111
  }
98
112
  if (this.dom.cropElements && this.config.modules?.crop) {
99
- const e = new v(this.dom.cropElements, {
113
+ const e = new x(this.dom.cropElements, {
100
114
  canvas: this.canvas,
101
115
  ctx: this.ctx,
102
116
  frameColor: this.config.modules.crop.frameColor,
103
117
  outsideOverlayColor: this.config.modules.crop.outsideOverlayColor,
104
- requestRedraw: () => this._redraw(),
105
- getCurrentImage: () => this.currentImage,
106
- onCropApplied: (t) => this._handleCropApplied(t)
118
+ state: this.state,
119
+ onCropApplied: () => this._handleCropApplied()
107
120
  });
108
- this.modules.set("crop", e);
121
+ this.modules.set("crop", e), this.cropModule = e, this._addManagedListener(this.dom.cropElements.activateButton, "click", () => this._enterCropMode());
109
122
  }
110
- this.modules.forEach((e) => e.init()), console.log(this.modules), this._setActiveModule(this.DEFAULT_MODULE);
123
+ if (this.dom.transformElements) {
124
+ const e = new z(this.dom.transformElements, {
125
+ state: this.state
126
+ });
127
+ this.modules.set("transform", e), this.transformModule = e;
128
+ }
129
+ this.modules.forEach((e) => e.init()), this._setActiveModule(this.DEFAULT_MODULE, !1, !0);
130
+ }
131
+ _addManagedListener(e, t, i) {
132
+ e.addEventListener(t, i), this.cleanupCallbacks.push(() => e.removeEventListener(t, i));
133
+ }
134
+ _getOutputDimensions(e) {
135
+ const t = this.state.getResizeState(), i = t.width > 0 ? t.width : e.width, s = t.height > 0 ? t.height : e.height;
136
+ return {
137
+ width: Math.max(1, Math.round(i)),
138
+ height: Math.max(1, Math.round(s))
139
+ };
140
+ }
141
+ _getPreviewDimensions(e, t) {
142
+ const i = Math.max(this.dom.elements.container.clientWidth, 1), s = Math.max(this.dom.elements.container.clientHeight, 1), r = Math.min(i / e, s / t, 1);
143
+ return {
144
+ width: Math.max(1, Math.round(e * r)),
145
+ height: Math.max(1, Math.round(t * r))
146
+ };
111
147
  }
112
148
  /**
113
149
  * Resets the canvas to display the given image, scaled to fit the container.
114
- * @param {HTMLImageElement} image - The image to display.
115
- * @internal
150
+ * This should ONLY be called when loading a new image or explicitly resetting the view.
116
151
  */
117
152
  _resetCanvasView(e) {
118
- this.currentImage = e;
119
- const t = this.dom.elements.container.clientWidth, i = this.dom.elements.container.clientHeight, r = Math.min(t / e.width, i / e.height), a = e.width * r, s = e.height * r;
120
- this.canvas.width = a, this.canvas.height = s, this._redraw(), this.dom.resizeElements && (this.dom.resizeElements.widthInput.value = Math.round(a).toString(), this.dom.resizeElements.heightInput.value = Math.round(s).toString());
153
+ const { width: t, height: i } = this._getOutputDimensions(e), { width: s, height: r } = this._getPreviewDimensions(t, i);
154
+ this.canvas.width = s, this.canvas.height = r, this.dom.resizeElements && (this.dom.resizeElements.widthInput.value = t.toString(), this.dom.resizeElements.heightInput.value = i.toString()), this._requestRender();
155
+ }
156
+ /**
157
+ * Sets which module is currently active.
158
+ * Crop mode disables interactions with the remaining modules until exited or applied.
159
+ */
160
+ _setActiveModule(e, t = !0, i = !1) {
161
+ let s = e;
162
+ if (t && this.activeModuleName === e && e !== this.DEFAULT_MODULE && (s = this.DEFAULT_MODULE), !i && this.activeModuleName === s)
163
+ return;
164
+ this.activeModuleName = s;
165
+ const r = !!this.state.getCurrent();
166
+ this.activeModuleName === "crop" && r ? (this.state.setMode("crop"), this.resizeModule?.deactivate(), this.transformModule?.deactivate(), this.cropModule?.activate(), this._toggleNonCropInteractions(!1)) : (this.state.setMode("edit"), this.cropModule?.deactivate(), this._toggleNonCropInteractions(!0), r ? (this.resizeModule?.activate(), this.transformModule?.activate()) : (this.resizeModule?.deactivate(), this.transformModule?.deactivate())), this._requestRender();
121
167
  }
122
168
  /**
123
- * Sets which module is currently active, deactivating all others.
124
- * If the same module is activated again (and it's not the default), it toggles it off,
125
- * returning to the default module.
126
- * @param {ModuleName | null} moduleName - The name of the module to activate, or null to deactivate all.
127
- * @internal
169
+ * Disables or enables UI elements for Resize and Transform modules.
128
170
  */
129
- _setActiveModule(e) {
130
- let t = e;
131
- this.activeModuleName === e && e !== this.DEFAULT_MODULE && (t = this.DEFAULT_MODULE), this.activeModuleName !== t && (this.activeModuleName = t, this.modules.forEach((i, r) => {
132
- r === this.activeModuleName ? i.activate() : i.deactivate();
133
- }), this._redraw());
171
+ _toggleNonCropInteractions(e) {
172
+ const t = !e;
173
+ if (this.dom.resizeElements && (this.dom.resizeElements.widthInput.disabled = t, this.dom.resizeElements.heightInput.disabled = t, this.dom.resizeElements.lockAspectRatio && (this.dom.resizeElements.lockAspectRatio.disabled = t)), this.dom.transformElements) {
174
+ const i = this.dom.transformElements;
175
+ i.rotateInput && (i.rotateInput.disabled = t), i.flipHorizontalButton && (i.flipHorizontalButton.disabled = t), i.flipVerticalButton && (i.flipVerticalButton.disabled = t);
176
+ }
177
+ this.dom.elements.clearCanvasButton.disabled = t, this.dom.elements.resetEditsButton.disabled = t;
134
178
  }
135
179
  // --- Event Handlers & Callbacks ---
136
180
  /**
137
181
  * Handles the file input change event to load, validate, and display an image.
138
- * @param {Event} event - The file input change event.
139
- * @internal
140
182
  */
141
183
  _loadImage(e) {
142
- try {
143
- const i = e.target.files?.[0];
144
- if (!i)
145
- return;
146
- u(i, this.config.maxFileSizeMB || 5);
147
- const r = new FileReader();
148
- r.onload = (a) => {
149
- const s = new Image();
150
- s.onload = () => {
151
- try {
152
- this.currentImage = s, this.initialImage = s, this._resetCanvasView(s);
153
- } catch (o) {
154
- this.errorHandler.handle(o);
184
+ if (!this.isDestroyed)
185
+ try {
186
+ const i = e.target.files?.[0];
187
+ if (!i)
188
+ return;
189
+ I(i, this.config.maxFileSizeMB || 5);
190
+ const s = this._beginAsyncOperation(), r = new FileReader();
191
+ r.onload = (o) => {
192
+ if (!this._isAsyncOperationActive(s))
193
+ return;
194
+ const a = o.target?.result;
195
+ if (typeof a != "string") {
196
+ this.errorHandler.handle(m("Unexpected image format from FileReader."), {
197
+ source: "editor",
198
+ operation: "load-image:reader-result"
199
+ });
200
+ return;
155
201
  }
156
- }, s.onerror = () => {
157
- this.errorHandler.handle(n("Error reading image file."));
158
- }, s.src = a.target.result;
159
- }, r.onerror = () => {
160
- this.errorHandler.handle(n("Error reading file with FileReader."));
161
- }, r.readAsDataURL(i);
162
- } catch (t) {
163
- this.errorHandler.handle(t);
164
- }
202
+ const n = new Image();
203
+ n.onload = () => {
204
+ if (this._isAsyncOperationActive(s))
205
+ try {
206
+ this.state.setInitial(n), this._setActiveModule(this.DEFAULT_MODULE, !1, !0);
207
+ } catch (l) {
208
+ this.errorHandler.handle(l, { source: "state", operation: "load-image:set-initial" });
209
+ }
210
+ }, n.onerror = () => {
211
+ this._isAsyncOperationActive(s) && this.errorHandler.handle(m("Error reading image file."), { source: "editor", operation: "load-image:image-read" });
212
+ }, n.src = a;
213
+ }, r.onerror = () => {
214
+ this._isAsyncOperationActive(s) && this.errorHandler.handle(m("Error reading file with FileReader."), { source: "editor", operation: "load-image:file-reader" });
215
+ }, r.readAsDataURL(i);
216
+ } catch (t) {
217
+ this.errorHandler.handle(t, { source: "validation", operation: "load-image:validate-file" });
218
+ }
165
219
  }
166
220
  /**
167
- * Callback for the CropModule after a crop is applied. Updates the current image with the cropped version.
168
- * @param {string} newImageDataUrl - The data URL of the newly cropped image.
169
- * @internal
221
+ * Callback for the CropModule after a crop is applied.
170
222
  */
171
- _handleCropApplied(e) {
172
- const t = new Image();
173
- t.onload = () => {
174
- this.currentImage = t, this._setActiveModule(this.DEFAULT_MODULE), this._resetCanvasView(t);
175
- }, t.src = e;
223
+ _handleCropApplied() {
224
+ const e = this._bakeCrop();
225
+ e && this._applyCropAndExit(e);
226
+ }
227
+ async _applyCropAndExit(e) {
228
+ try {
229
+ await this._applyImageDataUrl(e);
230
+ } catch (i) {
231
+ this._isAbortError(i) || this.errorHandler.handle(i, { source: "editor", operation: "apply-crop:apply-image" });
232
+ return;
233
+ }
234
+ if (this.isDestroyed)
235
+ return;
236
+ this._setActiveModule(this.DEFAULT_MODULE, !1, !0);
237
+ const t = this.state.getCurrent();
238
+ if (t) {
239
+ const { lockAspectRatio: i } = this.state.getResizeState();
240
+ this.state.setResizeState({
241
+ width: t.width,
242
+ height: t.height,
243
+ lockAspectRatio: i
244
+ });
245
+ }
246
+ this.state.setTransformState({ rotate: 0, flipH: !1, flipV: !1 }), this.transformModule?.activate();
247
+ }
248
+ _applyImageDataUrl(e) {
249
+ const t = this._beginAsyncOperation();
250
+ return new Promise((i, s) => {
251
+ const r = new Image();
252
+ r.onload = () => {
253
+ if (!this._isAsyncOperationActive(t)) {
254
+ s(this._createAbortError("Image apply operation was canceled."));
255
+ return;
256
+ }
257
+ this.state.setCurrent(r), i();
258
+ }, r.onerror = () => {
259
+ if (!this._isAsyncOperationActive(t)) {
260
+ s(this._createAbortError("Image apply operation was canceled."));
261
+ return;
262
+ }
263
+ s(m("Error applying image data to canvas."));
264
+ }, r.src = e;
265
+ });
266
+ }
267
+ _onStateChange(e) {
268
+ if (e === "clear") {
269
+ this.transformModule?.syncControlsWithState(), this._requestRender();
270
+ return;
271
+ }
272
+ const t = this.state.getCurrent();
273
+ if (!t) {
274
+ this.transformModule?.syncControlsWithState(), this._requestRender();
275
+ return;
276
+ }
277
+ if (e === "image" || e === "resize") {
278
+ e === "image" && this.transformModule?.syncControlsWithState(), this._resetCanvasView(t);
279
+ return;
280
+ }
281
+ this._requestRender();
282
+ }
283
+ _enterCropMode() {
284
+ this.state.getCurrent() && this._setActiveModule(
285
+ "crop"
286
+ /* CROP */
287
+ );
176
288
  }
177
289
  // --- Module Management ---
290
+ _requestRender() {
291
+ this.isDestroyed || this.renderScheduled || (this.renderScheduled = !0, globalThis.requestAnimationFrame(() => {
292
+ this.renderScheduled = !1, !this.isDestroyed && this._redraw();
293
+ }));
294
+ }
178
295
  /**
179
296
  * Clears the canvas, redraws the base image, and then draws the overlay for the currently active module.
180
- * @internal
181
297
  */
182
298
  _redraw() {
183
- this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height), this.currentImage && (this.ctx.drawImage(this.currentImage, 0, 0, this.currentImage.width, this.currentImage.height, 0, 0, this.canvas.width, this.canvas.height), this.activeModuleName && this.modules.get(this.activeModuleName)?.drawOverlay?.());
299
+ this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
300
+ const e = this.state.getCurrent();
301
+ e && (this._drawImageLayer(this.ctx, e, this.canvas.width, this.canvas.height), this.activeModuleName === "crop" && this.cropModule?.drawOverlay?.());
302
+ }
303
+ _drawImageLayer(e, t, i, s) {
304
+ const r = this.state.getTransformState(), o = i / 2, a = s / 2;
305
+ e.save(), e.translate(o, a), e.rotate(r.rotate * Math.PI / 180), e.scale(r.flipH ? -1 : 1, r.flipV ? -1 : 1), e.drawImage(t, -o, -a, i, s), e.restore();
306
+ }
307
+ _exportCurrentViewDataUrl() {
308
+ const e = this.state.getCurrent();
309
+ if (!e) return null;
310
+ const { width: t, height: i } = this._getOutputDimensions(e), s = document.createElement("canvas");
311
+ s.width = t, s.height = i;
312
+ const r = s.getContext("2d");
313
+ return r ? (this._drawImageLayer(r, e, s.width, s.height), s.toDataURL("image/png")) : null;
314
+ }
315
+ /**
316
+ * Creates a new image based on the current crop state.
317
+ * Crop rectangle is defined in preview space and remapped to output space.
318
+ */
319
+ _bakeCrop() {
320
+ const e = this.state.getCurrent(), t = this.state.getCropState();
321
+ if (!e || !t.active) return null;
322
+ const i = this._normalizeRect(t.rect), s = this.canvas.width, r = this.canvas.height;
323
+ if (s <= 0 || r <= 0) return null;
324
+ const { width: o, height: a } = this._getOutputDimensions(e), n = o / s, l = a / r, p = Math.max(0, Math.floor(i.x * n)), f = Math.max(0, Math.floor(i.y * l)), M = o - p, w = a - f, d = Math.min(Math.floor(i.w * n), Math.floor(M)), c = Math.min(Math.floor(i.h * l), Math.floor(w));
325
+ if (d <= 0 || c <= 0) return null;
326
+ const h = document.createElement("canvas");
327
+ h.width = o, h.height = a;
328
+ const g = h.getContext("2d");
329
+ if (!g) return null;
330
+ this._drawImageLayer(g, e, h.width, h.height);
331
+ const u = document.createElement("canvas");
332
+ u.width = d, u.height = c;
333
+ const v = u.getContext("2d");
334
+ return v ? (v.drawImage(h, p, f, d, c, 0, 0, d, c), u.toDataURL("image/png")) : null;
335
+ }
336
+ _normalizeRect(e) {
337
+ return {
338
+ x: e.w >= 0 ? e.x : e.x + e.w,
339
+ y: e.h >= 0 ? e.y : e.y + e.h,
340
+ w: Math.abs(e.w),
341
+ h: Math.abs(e.h)
342
+ };
343
+ }
344
+ _beginAsyncOperation() {
345
+ return this.asyncOperationVersion += 1, this.asyncOperationVersion;
346
+ }
347
+ _cancelPendingAsyncOperations() {
348
+ this.asyncOperationVersion += 1;
349
+ }
350
+ _isAsyncOperationActive(e) {
351
+ return !this.isDestroyed && e === this.asyncOperationVersion;
352
+ }
353
+ _createAbortError(e) {
354
+ const t = new Error(e);
355
+ return t.name = "AbortError", t;
356
+ }
357
+ _isAbortError(e) {
358
+ return e instanceof Error && e.name === "AbortError";
184
359
  }
185
360
  }
186
361
  export {
187
- M as CanvicoEditor
362
+ T as CanvicoEditor
188
363
  };
189
364
  //# sourceMappingURL=CanvicoEditor.js.map