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.
- package/LICENSE +1 -1
- package/README.md +31 -8
- package/dist/CanvicoEditor.d.ts +45 -24
- package/dist/CanvicoEditor.d.ts.map +1 -1
- package/dist/CanvicoEditor.js +262 -87
- package/dist/CanvicoEditor.js.map +1 -1
- package/dist/modules/BaseModule.d.ts +5 -4
- package/dist/modules/BaseModule.d.ts.map +1 -1
- package/dist/modules/BaseModule.js +7 -6
- package/dist/modules/BaseModule.js.map +1 -1
- package/dist/modules/CropModule.d.ts +32 -31
- package/dist/modules/CropModule.d.ts.map +1 -1
- package/dist/modules/CropModule.js +147 -106
- package/dist/modules/CropModule.js.map +1 -1
- package/dist/modules/ResizeModule.d.ts +12 -26
- package/dist/modules/ResizeModule.d.ts.map +1 -1
- package/dist/modules/ResizeModule.js +27 -35
- package/dist/modules/ResizeModule.js.map +1 -1
- package/dist/modules/TransformModule.d.ts +38 -0
- package/dist/modules/TransformModule.d.ts.map +1 -0
- package/dist/modules/TransformModule.js +55 -0
- package/dist/modules/TransformModule.js.map +1 -0
- package/dist/state/CanvasState.d.ts +34 -0
- package/dist/state/CanvasState.d.ts.map +1 -0
- package/dist/state/CanvasState.js +67 -0
- package/dist/state/CanvasState.js.map +1 -0
- package/dist/state/EditorReducer.d.ts +60 -0
- package/dist/state/EditorReducer.d.ts.map +1 -0
- package/dist/state/EditorReducer.js +127 -0
- package/dist/state/EditorReducer.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/utils/dom-manager.d.ts +12 -1
- package/dist/utils/dom-manager.d.ts.map +1 -1
- package/dist/utils/dom-manager.js +22 -14
- package/dist/utils/dom-manager.js.map +1 -1
- package/dist/utils/error-handler.d.ts +18 -7
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +34 -7
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/validation.d.ts +0 -7
- package/dist/utils/validation.d.ts.map +1 -1
- package/dist/utils/validation.js +33 -29
- package/dist/utils/validation.js.map +1 -1
- package/package.json +6 -4
package/LICENSE
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
The MIT License (MIT)
|
|
2
2
|
|
|
3
|
-
Copyright (c) 2025
|
|
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
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
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-
|
|
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
|
package/dist/CanvicoEditor.d.ts
CHANGED
|
@@ -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
|
-
/**
|
|
8
|
-
private
|
|
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
|
-
*
|
|
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
|
|
63
|
-
*
|
|
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.
|
|
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;
|
|
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"}
|
package/dist/CanvicoEditor.js
CHANGED
|
@@ -1,34 +1,48 @@
|
|
|
1
|
-
import { ErrorHandler as
|
|
2
|
-
import { DOMManager as
|
|
3
|
-
import { createFeatureNotSupportedError as
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
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
|
-
/**
|
|
12
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
68
|
-
this.errorHandler.handle(
|
|
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
|
|
72
|
-
|
|
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
|
|
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
|
|
91
|
-
|
|
92
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
119
|
-
|
|
120
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
132
|
-
|
|
133
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
this.errorHandler.handle(
|
|
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
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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.
|
|
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(
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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)
|
|
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
|
-
|
|
362
|
+
T as CanvicoEditor
|
|
188
363
|
};
|
|
189
364
|
//# sourceMappingURL=CanvicoEditor.js.map
|