@codingfactory/mediables-vue 2.19.3 → 2.21.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 (35) hide show
  1. package/dist/{PixiFrameExporter-BgKJMR1H.js → PixiFrameExporter-Bpp8rp3Q.js} +2 -2
  2. package/dist/{PixiFrameExporter-BgKJMR1H.js.map → PixiFrameExporter-Bpp8rp3Q.js.map} +1 -1
  3. package/dist/{PixiFrameExporter-CZkLc9b5.cjs → PixiFrameExporter-CLC3hJuN.cjs} +2 -2
  4. package/dist/{PixiFrameExporter-CZkLc9b5.cjs.map → PixiFrameExporter-CLC3hJuN.cjs.map} +1 -1
  5. package/dist/components/CoverFocalPointPicker.vue.d.ts +21 -0
  6. package/dist/components/CoverPhotoUploader.vue.d.ts +52 -0
  7. package/dist/editor-v2-CFLWp7RZ.cjs +2 -0
  8. package/dist/editor-v2-CFLWp7RZ.cjs.map +1 -0
  9. package/dist/editor-v2-DjhJBaCS.js +7187 -0
  10. package/dist/editor-v2-DjhJBaCS.js.map +1 -0
  11. package/dist/filters/index.d.ts +1 -0
  12. package/dist/filters/recipeToCssFilter.d.ts +71 -0
  13. package/dist/index-Bvo95NTy.cjs +357 -0
  14. package/dist/index-Bvo95NTy.cjs.map +1 -0
  15. package/dist/index-DKM5lmdH.js +39288 -0
  16. package/dist/index-DKM5lmdH.js.map +1 -0
  17. package/dist/index-Dae8SHT7.js.map +1 -1
  18. package/dist/index-QOKC8XA_.cjs.map +1 -1
  19. package/dist/index.d.ts +7 -2
  20. package/dist/mediables-vanilla.cjs +1 -1
  21. package/dist/mediables-vanilla.mjs +10 -9
  22. package/dist/mediables-vue.cjs +1 -1
  23. package/dist/mediables-vue.mjs +119 -107
  24. package/dist/style.css +1 -1
  25. package/dist/utils/videoRecipeCapabilities.d.ts +15 -0
  26. package/dist/vanilla-exports.d.ts +2 -1
  27. package/package.json +1 -1
  28. package/dist/editor-BDmZ747c.cjs +0 -2
  29. package/dist/editor-BDmZ747c.cjs.map +0 -1
  30. package/dist/editor-CcfSL2dI.js +0 -4322
  31. package/dist/editor-CcfSL2dI.js.map +0 -1
  32. package/dist/index-CSYvPx9Q.js +0 -41633
  33. package/dist/index-CSYvPx9Q.js.map +0 -1
  34. package/dist/index-LL2h0nsa.cjs +0 -357
  35. package/dist/index-LL2h0nsa.cjs.map +0 -1
@@ -1,4322 +0,0 @@
1
- var Me = Object.defineProperty;
2
- var Fe = (g, e, t) => e in g ? Me(g, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : g[e] = t;
3
- var ae = (g, e, t) => Fe(g, typeof e != "symbol" ? e + "" : e, t);
4
- import { E as ne, d as Se } from "./index-Dae8SHT7.js";
5
- const Re = Object.freeze(["normal", "multiply", "screen", "overlay"]), E = Object.freeze({
6
- stroke: Object.freeze({ enabled: !1, color: "#000000", width: 0 }),
7
- shadow: Object.freeze({ enabled: !1, color: "#000000", alpha: 0.5, blur: 0, distance: 0, angle: 45 }),
8
- glow: Object.freeze({ enabled: !1, color: "#ffffff", alpha: 0.5, blur: 0 }),
9
- backdrop: Object.freeze({ enabled: !1, color: "#000000", opacity: 0.5, padding: 0, radius: 0, blur: 0 })
10
- });
11
- class Te extends ne {
12
- constructor() {
13
- super(), this.app = null, this.sprite = null, this.originalTexture = null, this.baseTexture = null, this.fitScale = 1, this.zoom = 1, this._container = null, this._lastExportDimensions = null, this._mountPromise = null, this._mountToken = 0, this._destroyed = !1, this._layerDocument = null, this._layerDisplayObjects = /* @__PURE__ */ new Map(), this._renderableLayerIds = [], this._loadedTextureUrl = null, this._layerRenderToken = 0, this._layerFilterFactory = null;
14
- }
15
- /**
16
- * Whether the renderer is fully initialized and ready for use.
17
- * @returns {boolean}
18
- */
19
- get isReady() {
20
- var e;
21
- return !!((e = this.app) != null && e.renderer);
22
- }
23
- /**
24
- * Get CSS size of the canvas (for proper coordinate mapping).
25
- * Accesses renderer.canvas directly to avoid PIXI Application's
26
- * canvas getter which throws when renderer is undefined.
27
- */
28
- _getCssSize() {
29
- var s, r, o, n, a, l;
30
- const e = (r = (s = this.app) == null ? void 0 : s.renderer) == null ? void 0 : r.canvas, t = (e == null ? void 0 : e.clientWidth) ?? ((n = (o = this.app) == null ? void 0 : o.screen) == null ? void 0 : n.width) ?? 0, i = (e == null ? void 0 : e.clientHeight) ?? ((l = (a = this.app) == null ? void 0 : a.screen) == null ? void 0 : l.height) ?? 0;
31
- return { w: t, h: i };
32
- }
33
- /**
34
- * Force render the stage
35
- */
36
- render() {
37
- var e, t;
38
- try {
39
- (e = this.app) != null && e.renderer && ((t = this.app) != null && t.stage) && this.app.renderer.render(this.app.stage);
40
- } catch {
41
- }
42
- }
43
- /**
44
- * Wait for the next animation frame, with a timeout fallback for environments
45
- * where the frame callback never arrives while the editor is open.
46
- * @param {number} timeoutMs
47
- * @returns {Promise<void>}
48
- */
49
- _waitForNextFrame(e = 32) {
50
- return new Promise((t) => {
51
- if (typeof window > "u") {
52
- t();
53
- return;
54
- }
55
- let i = !1, s = 0;
56
- const r = () => {
57
- i || (i = !0, window.clearTimeout(s), t());
58
- };
59
- if (s = window.setTimeout(r, e), typeof window.requestAnimationFrame == "function") {
60
- window.requestAnimationFrame(() => r());
61
- return;
62
- }
63
- r();
64
- });
65
- }
66
- /**
67
- * Calculate fit scale for a texture
68
- * @param {PIXI.Texture} tex
69
- * @returns {number}
70
- */
71
- getFitScaleFor(e) {
72
- if (!this.app || !e) return 1;
73
- const { w: t, h: i } = this._getCssSize();
74
- if (t <= 1 || i <= 1) return 1;
75
- let s = Math.min(t / e.width, i / e.height) * 0.9;
76
- return (!Number.isFinite(s) || s <= 0) && (s = 1), s;
77
- }
78
- /**
79
- * Apply view transform (position and scale) to sprite
80
- */
81
- applyViewTransform(e = {}) {
82
- if (!this.app || !this.sprite || !this.originalTexture) return;
83
- const t = this.fitScale * this.zoom, { w: i, h: s } = this._getCssSize(), r = e.keepCenter ? { x: this.sprite.x + this.sprite.width / 2, y: this.sprite.y + this.sprite.height / 2 } : e.center ?? { x: i / 2, y: s / 2 };
84
- this.sprite.width = this.originalTexture.width * t, this.sprite.height = this.originalTexture.height * t, this.sprite.x = r.x - this.sprite.width / 2, this.sprite.y = r.y - this.sprite.height / 2, this._syncLayerDisplayObjects(), this.render();
85
- }
86
- /**
87
- * Set zoom level
88
- * @param {number} z - Zoom level (0.1 to 8)
89
- * @param {Object} opts - Options
90
- */
91
- setZoom(e, t = {}) {
92
- this.zoom = Math.max(0.1, Math.min(8, e)), this.applyViewTransform({ keepCenter: t.keepCenter ?? !0 }), this.emit("zoomChange", this.zoom);
93
- }
94
- /**
95
- * Fit image to screen
96
- */
97
- fitToScreen() {
98
- this.originalTexture && (this.fitScale = this.getFitScaleFor(this.originalTexture), this.setZoom(1, { keepCenter: !1 }));
99
- }
100
- /**
101
- * Mount PIXI application to container
102
- * @param {HTMLElement} container
103
- * @param {number} backgroundColor
104
- * @param {Object} [options]
105
- * @param {number} [options.backgroundAlpha=1] - 0 makes the canvas transparent
106
- * so a host-rendered background (e.g. CSS checkerboard) shows through.
107
- */
108
- async mount(e, t = 16777215, i = {}) {
109
- if (!e) return;
110
- this._destroyed = !1, this._container = e;
111
- const s = ++this._mountToken, r = async () => {
112
- var p;
113
- const o = window.PIXI;
114
- if (!o)
115
- throw new Error("PIXI.js not found. Please ensure PIXI is loaded globally.");
116
- (p = o.Assets) != null && p.setPreferences && o.Assets.setPreferences({ crossOrigin: "anonymous" });
117
- const n = e.clientWidth || 600, a = e.clientHeight || 400, l = typeof i.backgroundAlpha == "number" ? i.backgroundAlpha : 1, h = new o.Application();
118
- if (this.app = h, await h.init({
119
- width: n,
120
- height: a,
121
- backgroundColor: t,
122
- backgroundAlpha: l,
123
- antialias: !0,
124
- autoDensity: !0,
125
- resolution: window.devicePixelRatio || 1
126
- }), this._destroyed || this._mountToken !== s || this.app !== h) {
127
- try {
128
- h.destroy(!0, { children: !0, texture: !0 });
129
- } catch {
130
- }
131
- return !1;
132
- }
133
- e.appendChild(h.canvas);
134
- const c = h.canvas;
135
- return c.classList.add("pixi-canvas"), c.style.width = "100%", c.style.height = "100%", c.style.transform = "translateZ(0)", c.style.willChange = "transform", c.setAttribute("role", "img"), c.setAttribute("aria-label", "Image editor canvas — use toolbar controls to edit the image"), this.emit("mounted", { width: n, height: a }), !0;
136
- };
137
- return this._mountPromise = r(), await this._mountPromise;
138
- }
139
- /**
140
- * Load an image texture
141
- * @param {string} imageUrl - URL, data URL, or blob URL of the image
142
- * @param {Object} [options]
143
- * @param {() => boolean} [options.isCurrent] - Returns false when this load
144
- * has been superseded and must not mutate the renderer.
145
- */
146
- async loadTexture(e, t = {}) {
147
- var h, c, p;
148
- const i = () => typeof t.isCurrent != "function" || t.isCurrent() !== !1;
149
- if (this._mountPromise && await this._mountPromise, !((h = this.app) != null && h.renderer) || !i()) return !1;
150
- const s = window.PIXI, r = await this._loadImageElement(e);
151
- if (!r || !((c = this.app) != null && c.renderer) || !i() || (await this._waitForNextFrame(), !((p = this.app) != null && p.renderer) || !i())) return !1;
152
- const o = s.Texture.from(r);
153
- if (!o)
154
- return !1;
155
- this.originalTexture = o, this.baseTexture = o, this._loadedTextureUrl = typeof e == "string" ? e : null, this.sprite && (this.app.stage.removeChild(this.sprite), this.sprite.destroy()), this.sprite = new s.Sprite(o), this.app.stage.addChild(this.sprite), this.fitScale = this.getFitScaleFor(o), this.zoom = 1;
156
- const { w: n, h: a } = this._getCssSize();
157
- this.applyViewTransform({ center: { x: n / 2, y: a / 2 } });
158
- const l = this.getFitScaleFor(o);
159
- return Math.abs(l - this.fitScale) / Math.max(1e-6, l) > 0.02 && (this.fitScale = l, this.applyViewTransform({ center: { x: n / 2, y: a / 2 } })), this.render(), this.emit("textureLoaded", { width: o.width, height: o.height }), !0;
160
- }
161
- /**
162
- * Render a v2 layer document in preview order. The existing subject sprite
163
- * remains the filter/crop target; auxiliary layers are synced to its preview
164
- * geometry until arbitrary per-layer transforms are introduced.
165
- * @param {Object|null} document
166
- * @returns {Promise<boolean>}
167
- */
168
- async renderLayerDocument(e) {
169
- var o;
170
- if (!((o = this.app) != null && o.stage) || !this.sprite || !e || !Array.isArray(e.layers))
171
- return !1;
172
- const t = window.PIXI;
173
- if (!t) return !1;
174
- const i = ++this._layerRenderToken, s = () => i === this._layerRenderToken;
175
- this._destroyLayerDisplayObjects(), this._layerDocument = JSON.parse(JSON.stringify(e)), this._renderableLayerIds = [];
176
- try {
177
- this.app.stage.removeChild(this.sprite);
178
- } catch {
179
- }
180
- let r = !1;
181
- for (const n of this._layerDocument.layers) {
182
- if (!n || n.visible === !1) {
183
- (n == null ? void 0 : n.role) === "subject" && (this.sprite.visible = !1, this.sprite.__layerId = n.id, r = !0);
184
- continue;
185
- }
186
- if (this._isSubjectLayer(n)) {
187
- const l = await this._createSubjectDisplayObjectForLayer(t, n);
188
- if (!s())
189
- return this._destroyTransientDisplayObject(l), !1;
190
- if (l) {
191
- this.sprite.visible = !1, l.__layerId = n.id, l.visible = !0, l.alpha = this._layerOpacity(n), this._layerDisplayObjects.set(n.id, { layer: n, displayObject: l }), this._syncLayerDisplayObject(n, l), this.app.stage.addChild(l), this._renderableLayerIds.push(n.id), r = !0;
192
- continue;
193
- }
194
- this.sprite.__layerId = n.id, this.sprite.visible = !0, this.sprite.alpha = this._layerOpacity(n), this._applyLayerFilterInstances(this.sprite, n), this.app.stage.addChild(this.sprite), this._renderableLayerIds.push(n.id), r = !0;
195
- continue;
196
- }
197
- const a = await this._createDisplayObjectForLayer(t, n);
198
- if (!s())
199
- return this._destroyTransientDisplayObject(a), !1;
200
- a && (a.__layerId = n.id, a.visible = !0, a.alpha = this._layerOpacity(n), this._layerDisplayObjects.set(n.id, { layer: n, displayObject: a }), this._syncLayerDisplayObject(n, a), this.app.stage.addChild(a), this._renderableLayerIds.push(n.id));
201
- }
202
- return r || (this.sprite.visible = !1), s() ? (this.render(), !0) : !1;
203
- }
204
- getRenderableLayerIds() {
205
- return [...this._renderableLayerIds];
206
- }
207
- setLayerFilterFactory(e) {
208
- this._layerFilterFactory = typeof e == "function" ? e : null;
209
- }
210
- applyLayerFilters(e = this._layerDocument) {
211
- var t;
212
- if (!e || !Array.isArray(e.layers)) return !1;
213
- this._layerDocument = JSON.parse(JSON.stringify(e));
214
- for (const i of this._layerDocument.layers) {
215
- if (!i) continue;
216
- const s = this._layerDisplayObjects.get(i.id) || null;
217
- s && (s.layer = i);
218
- let r = (s == null ? void 0 : s.displayObject) || null;
219
- !r && this._isSubjectLayer(i) && ((t = this.sprite) == null ? void 0 : t.__layerId) === i.id && (r = this.sprite), r && this._applyLayerFilterInstances(r, i);
220
- }
221
- return this.render(), !0;
222
- }
223
- _isSubjectLayer(e) {
224
- return (e == null ? void 0 : e.role) === "subject" || (e == null ? void 0 : e.id) === "subject-layer";
225
- }
226
- _layerSupportsImageFilters(e) {
227
- var t;
228
- return !e || typeof e != "object" ? !1 : e.type === "image" ? !0 : e.type === "background" || e.role === "background" ? ((t = e.fill) == null ? void 0 : t.kind) === "media" : !1;
229
- }
230
- _layerOpacity(e) {
231
- const t = Number(e == null ? void 0 : e.opacity);
232
- return Number.isFinite(t) ? Math.max(0, Math.min(1, t)) : 1;
233
- }
234
- _applyLayerFilterInstances(e, t, i = {}) {
235
- if (!e || !this._layerSupportsImageFilters(t)) return;
236
- let s = [];
237
- if (this._layerFilterFactory)
238
- try {
239
- s = this._layerFilterFactory(t, {
240
- displayObject: e,
241
- forExport: i.forExport === !0,
242
- previewToNativeScale: i.previewToNativeScale || 1,
243
- sourceWidth: i.sourceWidth,
244
- sourceHeight: i.sourceHeight
245
- }) || [];
246
- } catch {
247
- s = [];
248
- }
249
- const r = Array.isArray(s) ? s.filter(Boolean) : [];
250
- for (const a of r)
251
- if (!(!a || typeof a != "object" && typeof a != "function"))
252
- try {
253
- Object.defineProperty(a, "__mediablesLayerImageFilter", {
254
- value: !0,
255
- configurable: !0
256
- });
257
- } catch {
258
- try {
259
- a.__mediablesLayerImageFilter = !0;
260
- } catch {
261
- }
262
- }
263
- const n = [...Array.isArray(e.filters) ? e.filters.filter((a) => (a == null ? void 0 : a.__mediablesLayerImageFilter) !== !0) : [], ...r];
264
- e.filters = n.length > 0 ? n : null;
265
- }
266
- async _createDisplayObjectForLayer(e, t) {
267
- var o, n;
268
- const i = this._createTextDisplayObjectForLayer(e, t);
269
- if (i) return i;
270
- const s = await this._createImageDisplayObjectForLayer(e, t);
271
- if (s) return s;
272
- const r = this._backgroundFillForLayer(t);
273
- if ((r == null ? void 0 : r.kind) === "media") {
274
- const a = await this._textureForLayerSource(e, r.source);
275
- if (!a) return null;
276
- const l = this._createBackgroundSprite(e, a, r.fit);
277
- return l.__sourceWidth = Number((o = r.source) == null ? void 0 : o.originalWidth) || a.width || 1, l.__sourceHeight = Number((n = r.source) == null ? void 0 : n.originalHeight) || a.height || 1, this._applyLayerEffects(e, l, t), this._applyLayerFilterInstances(l, t), l;
278
- }
279
- if ((r == null ? void 0 : r.kind) === "gradient") {
280
- const a = this._createGradientTexture(e, r, 512, 512);
281
- if (!a || typeof e.Sprite != "function") return null;
282
- const l = new e.Sprite(a);
283
- return l.__ownsTexture = !0, l.__sourceWidth = 512, l.__sourceHeight = 512, this._applyLayerEffects(e, l, t), l;
284
- }
285
- if (this._backgroundColorForLayer(t) && typeof e.Graphics == "function") {
286
- const a = new e.Graphics();
287
- return this._applyLayerEffects(e, a, t), a;
288
- }
289
- return null;
290
- }
291
- _isTextLayer(e) {
292
- return (e == null ? void 0 : e.type) === "text" || (e == null ? void 0 : e.role) === "text";
293
- }
294
- _normalizeTextBox(e) {
295
- var i;
296
- const t = (i = e == null ? void 0 : e.text) != null && i.box && typeof e.text.box == "object" ? e.text.box : {};
297
- return {
298
- x: this._clampNumber(t.x, 0, 1, 0.2),
299
- y: this._clampNumber(t.y, 0, 1, 0.2),
300
- width: this._clampNumber(t.width, 0.05, 1, 0.6),
301
- height: this._clampNumber(t.height, 0.05, 1, 0.18)
302
- };
303
- }
304
- _clampNumber(e, t, i, s) {
305
- const r = Number(e);
306
- return Number.isFinite(r) ? Math.max(t, Math.min(i, r)) : s;
307
- }
308
- _supportedTextFontFamily(e) {
309
- const t = ["Inter", "Arial", "Helvetica", "Georgia", "Times New Roman", "Courier New", "Verdana"], i = typeof e == "string" ? e.trim() : "";
310
- return t.includes(i) ? i : "Inter";
311
- }
312
- _supportedTextColor(e, t = "#000000") {
313
- return typeof e == "string" && /^#[0-9a-f]{6}$/i.test(e) ? e : t;
314
- }
315
- _textBlendMode(e) {
316
- const t = e == null ? void 0 : e.blendMode;
317
- return Re.includes(t) ? t : "normal";
318
- }
319
- _textContentForLayer(e) {
320
- var i;
321
- const t = (i = e == null ? void 0 : e.text) == null ? void 0 : i.content;
322
- return t == null ? "" : String(t);
323
- }
324
- _textStyleForLayer(e, t = 1, i = 1, s = {}) {
325
- var m;
326
- const r = (m = e == null ? void 0 : e.text) != null && m.style && typeof e.text.style == "object" ? e.text.style : {}, o = this._textEffectsForLayer(e), n = this._clampNumber(r.fontSize, 8, 400, 64), a = this._clampNumber(r.lineHeight, 0.8, 3, 1.2), l = this._clampNumber(r.letterSpacing, -100, 100, 0), h = this._normalizeTextFontWeight(r.fontWeight), c = ["normal", "italic"].includes(r.fontStyle) ? r.fontStyle : "normal", p = ["left", "center", "right"].includes(r.align) ? r.align : "center", u = s.fill || (typeof r.fill == "string" && /^#[0-9a-f]{6}$/i.test(r.fill) ? r.fill : "#ffffff"), f = {
327
- fontFamily: this._supportedTextFontFamily(r.fontFamily),
328
- fontSize: n * t,
329
- fontWeight: h,
330
- fontStyle: c,
331
- fill: u,
332
- align: p,
333
- lineHeight: n * a * t,
334
- letterSpacing: l * t,
335
- wordWrap: !0,
336
- wordWrapWidth: Math.max(1, i),
337
- breakWords: !0
338
- };
339
- return s.stroke !== !1 && o.stroke.enabled && o.stroke.width > 0 && (f.stroke = {
340
- color: o.stroke.color,
341
- width: o.stroke.width * t
342
- }), f;
343
- }
344
- _normalizeTextFontWeight(e) {
345
- if (e === "normal") return 400;
346
- if (e === "bold") return 700;
347
- const t = Number(e);
348
- return !Number.isFinite(t) || t < 100 || t > 900 || t % 100 !== 0 ? 700 : t;
349
- }
350
- _textEffectsForLayer(e) {
351
- var n;
352
- const t = (n = e == null ? void 0 : e.text) != null && n.effects && typeof e.text.effects == "object" ? e.text.effects : {}, i = t.stroke && typeof t.stroke == "object" ? t.stroke : {}, s = t.shadow && typeof t.shadow == "object" ? t.shadow : {}, r = t.glow && typeof t.glow == "object" ? t.glow : {}, o = t.backdrop && typeof t.backdrop == "object" ? t.backdrop : {};
353
- return {
354
- stroke: {
355
- enabled: i.enabled === !0,
356
- color: this._supportedTextColor(i.color, E.stroke.color),
357
- width: this._clampNumber(i.width, 0, 80, E.stroke.width)
358
- },
359
- shadow: {
360
- enabled: s.enabled === !0,
361
- color: this._supportedTextColor(s.color, E.shadow.color),
362
- alpha: this._clampNumber(s.alpha, 0, 1, E.shadow.alpha),
363
- blur: this._clampNumber(s.blur, 0, 120, E.shadow.blur),
364
- distance: this._clampNumber(s.distance, 0, 200, E.shadow.distance),
365
- angle: this._normalizeDegrees(s.angle ?? E.shadow.angle)
366
- },
367
- glow: {
368
- enabled: r.enabled === !0,
369
- color: this._supportedTextColor(r.color, E.glow.color),
370
- alpha: this._clampNumber(r.alpha, 0, 1, E.glow.alpha),
371
- blur: this._clampNumber(r.blur, 0, 120, E.glow.blur)
372
- },
373
- backdrop: {
374
- enabled: o.enabled === !0,
375
- color: this._supportedTextColor(o.color, E.backdrop.color),
376
- opacity: this._clampNumber(o.opacity, 0, 1, E.backdrop.opacity),
377
- padding: this._clampNumber(o.padding, 0, 200, E.backdrop.padding),
378
- radius: this._clampNumber(o.radius, 0, 200, E.backdrop.radius),
379
- blur: this._clampNumber(o.blur, 0, 80, E.backdrop.blur)
380
- }
381
- };
382
- }
383
- _createTextBlurFilter(e, t) {
384
- if (t <= 0 || typeof (e == null ? void 0 : e.BlurFilter) != "function") return null;
385
- try {
386
- let i;
387
- try {
388
- i = new e.BlurFilter({ strength: t, quality: 4 });
389
- } catch {
390
- i = new e.BlurFilter(t);
391
- }
392
- return typeof i.blur == "number" && (i.blur = t), typeof i.strength == "number" && (i.strength = t), i.padding = Math.max(Number(i.padding) || 0, Math.ceil(t * 2)), i;
393
- } catch {
394
- return null;
395
- }
396
- }
397
- _textEffectPadding(e, t) {
398
- const i = e.stroke.enabled ? e.stroke.width * t : 0, s = e.shadow.enabled ? (e.shadow.distance + e.shadow.blur * 2) * t : 0, r = e.glow.enabled ? e.glow.blur * 2 * t : 0, o = e.backdrop.enabled ? (e.backdrop.padding + e.backdrop.blur * 2) * t : 0;
399
- return Math.ceil(Math.max(i, s, r, o, 0));
400
- }
401
- _createTextDisplayObjectForLayer(e, t) {
402
- if (!this._isTextLayer(t) || typeof (e == null ? void 0 : e.Text) != "function") return null;
403
- const i = this._createTextPrimitive(e, t);
404
- if (!i) return null;
405
- if (typeof e.Container != "function" || typeof e.Graphics != "function")
406
- return i;
407
- const s = new e.Container(), r = new e.Graphics(), o = new e.Graphics(), n = new e.Graphics(), a = this._createTextPrimitive(e, t), l = this._createTextPrimitive(e, t);
408
- return s.__isTextLayerContainer = !0, s.__textObject = i, s.__textMask = r, s.__textEffectMask = o, s.__textBackdrop = n, s.__textShadow = a, s.__textGlow = l, n.visible = !1, a && (a.visible = !1), l && (l.visible = !1), i.mask = r, s.addChild(n), a && s.addChild(a), l && s.addChild(l), s.addChild(i), s.addChild(o), s.addChild(r), s;
409
- }
410
- _createTextPrimitive(e, t) {
411
- try {
412
- return new e.Text({
413
- text: this._textContentForLayer(t),
414
- style: this._textStyleForLayer(t)
415
- });
416
- } catch {
417
- try {
418
- return new e.Text(this._textContentForLayer(t), this._textStyleForLayer(t));
419
- } catch {
420
- return null;
421
- }
422
- }
423
- }
424
- async _createSubjectDisplayObjectForLayer(e, t) {
425
- const i = this._sourceUrlForLayerSource(t == null ? void 0 : t.source);
426
- return !i || i === this._loadedTextureUrl ? null : this._createImageDisplayObjectForLayer(e, t);
427
- }
428
- async _createImageDisplayObjectForLayer(e, t) {
429
- var r, o;
430
- if ((t == null ? void 0 : t.type) !== "image") return null;
431
- const i = await this._textureForLayerSource(e, t.source);
432
- if (!i || typeof e.Sprite != "function") return null;
433
- const s = new e.Sprite(i);
434
- return s.__sourceWidth = Number((r = t.source) == null ? void 0 : r.originalWidth) || i.width || 1, s.__sourceHeight = Number((o = t.source) == null ? void 0 : o.originalHeight) || i.height || 1, this._applyLayerEffects(e, s, t), this._applyLayerFilterInstances(s, t), s;
435
- }
436
- _backgroundFillForLayer(e) {
437
- const t = e == null ? void 0 : e.fill;
438
- if (t && typeof t == "object") return t;
439
- const i = e == null ? void 0 : e.source;
440
- return (i == null ? void 0 : i.kind) === "color" && typeof i.color == "string" ? { kind: "color", value: i.color } : null;
441
- }
442
- _backgroundColorForLayer(e) {
443
- const t = e == null ? void 0 : e.source;
444
- if ((t == null ? void 0 : t.kind) === "color" && typeof t.color == "string")
445
- return t.color;
446
- const i = e == null ? void 0 : e.fill;
447
- return (i == null ? void 0 : i.kind) === "color" && typeof i.value == "string" ? i.value : null;
448
- }
449
- _sourceUrlForLayerSource(e) {
450
- if (!e || typeof e != "object") return null;
451
- for (const t of ["previewUrl", "sourceUrl", "url"]) {
452
- const i = e[t];
453
- if (typeof i == "string" && i.trim() !== "")
454
- return i;
455
- }
456
- return null;
457
- }
458
- async _textureForLayerSource(e, t) {
459
- const i = this._sourceUrlForLayerSource(t);
460
- if (!i) return null;
461
- const s = await this._loadImageElement(i);
462
- return s ? e.Texture.from(s) : null;
463
- }
464
- _createBackgroundSprite(e, t, i = "cover") {
465
- if (i === "tile" && typeof e.TilingSprite == "function")
466
- try {
467
- const s = new e.TilingSprite({ texture: t, width: 1, height: 1 });
468
- return s.__isTilingBackground = !0, s;
469
- } catch {
470
- try {
471
- const r = new e.TilingSprite(t, 1, 1);
472
- return r.__isTilingBackground = !0, r;
473
- } catch {
474
- }
475
- }
476
- return new e.Sprite(t);
477
- }
478
- _createGradientTexture(e, t, i, s) {
479
- var l;
480
- if (typeof document > "u" || !((l = e == null ? void 0 : e.Texture) != null && l.from)) return null;
481
- const r = document.createElement("canvas");
482
- r.width = Math.max(1, Math.round(i)), r.height = Math.max(1, Math.round(s));
483
- const o = r.getContext("2d");
484
- if (!o) return null;
485
- const n = Array.isArray(t == null ? void 0 : t.stops) && t.stops.length > 0 ? t.stops : [
486
- { offset: 0, color: "#111827" },
487
- { offset: 1, color: "#f59e0b" }
488
- ];
489
- let a;
490
- if ((t == null ? void 0 : t.gradientType) === "radial") {
491
- const h = Math.max(r.width, r.height) / 2;
492
- a = o.createRadialGradient(
493
- r.width / 2,
494
- r.height / 2,
495
- 0,
496
- r.width / 2,
497
- r.height / 2,
498
- h
499
- );
500
- } else {
501
- const h = (Number(t == null ? void 0 : t.angle) || 0) * Math.PI / 180, c = r.width / 2, p = r.height / 2, u = Math.sqrt(r.width ** 2 + r.height ** 2) / 2, f = Math.cos(h) * u, m = Math.sin(h) * u;
502
- a = o.createLinearGradient(c - f, p - m, c + f, p + m);
503
- }
504
- for (const h of n) {
505
- if (!h || typeof h.color != "string") continue;
506
- const c = Number.isFinite(Number(h.offset)) ? Math.max(0, Math.min(1, Number(h.offset))) : 0;
507
- a.addColorStop(c, h.color);
508
- }
509
- return o.fillStyle = a, o.fillRect(0, 0, r.width, r.height), e.Texture.from(r);
510
- }
511
- _backgroundBlurAmount(e) {
512
- var r;
513
- const i = (Array.isArray(e == null ? void 0 : e.effects) ? e.effects : []).find((o) => (o == null ? void 0 : o.id) === "background-blur" || (o == null ? void 0 : o.type) === "blur"), s = Number((r = i == null ? void 0 : i.params) == null ? void 0 : r.amount);
514
- return Number.isFinite(s) ? Math.max(0, s) : 0;
515
- }
516
- _applyLayerEffects(e, t, i) {
517
- const s = this._backgroundBlurAmount(i);
518
- if (!(s <= 0 || typeof (e == null ? void 0 : e.BlurFilter) != "function"))
519
- try {
520
- let r;
521
- try {
522
- r = new e.BlurFilter({ strength: s, quality: 4 });
523
- } catch {
524
- r = new e.BlurFilter(s);
525
- }
526
- typeof r.blur == "number" && (r.blur = s), typeof r.strength == "number" && (r.strength = s), r.padding = Math.max(Number(r.padding) || 0, Math.ceil(s * 2)), t.filters = Array.isArray(t.filters) ? [...t.filters, r] : [r];
527
- } catch {
528
- }
529
- }
530
- _syncLayerDisplayObjects() {
531
- if (this.sprite)
532
- for (const { layer: e, displayObject: t } of this._layerDisplayObjects.values())
533
- this._syncLayerDisplayObject(e, t);
534
- }
535
- _syncLayerDisplayObject(e, t) {
536
- if (!this.sprite || !t) return;
537
- if (this._isTextLayer(e)) {
538
- this._fitTextDisplayObjectToRect(t, e, {
539
- x: this.sprite.x,
540
- y: this.sprite.y,
541
- width: this.sprite.width,
542
- height: this.sprite.height
543
- });
544
- return;
545
- }
546
- const i = this._backgroundColorForLayer(e);
547
- if (i && typeof t.clear == "function") {
548
- t.clear(), t.rect(this.sprite.x, this.sprite.y, this.sprite.width, this.sprite.height).fill(i);
549
- return;
550
- }
551
- this._fitDisplayObjectToRect(t, e, {
552
- x: this.sprite.x,
553
- y: this.sprite.y,
554
- width: this.sprite.width,
555
- height: this.sprite.height
556
- });
557
- }
558
- _fillTextEffectGraphics(e, t, i = 1) {
559
- if (!(!e || typeof e.fill != "function"))
560
- try {
561
- e.fill({ color: t, alpha: i });
562
- } catch {
563
- e.fill(t), e.alpha = i;
564
- }
565
- }
566
- _syncTextEffectPrimitive(e, t, i, s, r, o, n, a) {
567
- if (!t) return;
568
- const l = s[r];
569
- if (t.visible = (l == null ? void 0 : l.enabled) === !0, t.filters = null, t.mask = t.visible ? a : null, !!t.visible) {
570
- if (t.text = this._textContentForLayer(i), t.rotation = 0, r === "shadow") {
571
- const h = l.angle * Math.PI / 180;
572
- t.x = Math.cos(h) * l.distance * o, t.y = Math.sin(h) * l.distance * o, t.alpha = l.alpha, t.style = this._textStyleForLayer(i, o, n, {
573
- fill: l.color,
574
- stroke: !1
575
- });
576
- const c = this._createTextBlurFilter(e, l.blur * o);
577
- t.filters = c ? [c] : null;
578
- return;
579
- }
580
- if (r === "glow") {
581
- t.x = 0, t.y = 0, t.alpha = l.alpha, t.style = this._textStyleForLayer(i, o, n, {
582
- fill: l.color,
583
- stroke: !1
584
- });
585
- const h = this._createTextBlurFilter(e, l.blur * o);
586
- t.filters = h ? [h] : null;
587
- }
588
- }
589
- }
590
- _syncTextBackdrop(e, t, i, s, r, o, n) {
591
- if (!t) return;
592
- const a = i.backdrop;
593
- if (t.visible = a.enabled === !0, t.filters = null, t.mask = t.visible ? n : null, t.alpha = 1, typeof t.clear == "function" && t.clear(), !t.visible) return;
594
- const l = a.padding * s, h = a.radius * s, c = -l, p = -l, u = r + l * 2, f = o + l * 2;
595
- h > 0 && typeof t.roundRect == "function" ? t.roundRect(c, p, u, f, h) : typeof t.rect == "function" && t.rect(c, p, u, f), this._fillTextEffectGraphics(t, a.color, a.opacity);
596
- const m = this._createTextBlurFilter(e, a.blur * s);
597
- t.filters = m ? [m] : null;
598
- }
599
- _fitTextDisplayObjectToRect(e, t, i) {
600
- var m, b;
601
- const s = this._normalizeTextBox(t), r = i.x + s.x * i.width, o = i.y + s.y * i.height, n = s.width * i.width, a = s.height * i.height, l = Number((m = this.originalTexture) == null ? void 0 : m.width) || i.width, h = l > 0 ? i.width / l : 1, c = this._textEffectsForLayer(t);
602
- e.x = r, e.y = o, e.rotation = this._normalizeDegrees(Number((b = t == null ? void 0 : t.transform) == null ? void 0 : b.rotation) || 0) * Math.PI / 180, e.blendMode = this._textBlendMode(t), e.__textBox = s, e.__textLayout = { x: r, y: o, width: n, height: a, scale: h };
603
- const p = e.__textObject || e, u = e.__textMask || null, f = e.__textEffectMask || null;
604
- if (p.text = this._textContentForLayer(t), p.style = this._textStyleForLayer(t, h, n), p.x = 0, p.y = 0, p.rotation = 0, u && typeof u.clear == "function" && typeof u.rect == "function" && typeof u.fill == "function" && (u.clear(), u.rect(0, 0, n, a).fill(16777215), p.mask = u), f && typeof f.clear == "function" && typeof f.rect == "function" && typeof f.fill == "function") {
605
- const k = this._textEffectPadding(c, h);
606
- f.clear(), f.rect(-k, -k, n + k * 2, a + k * 2).fill(16777215);
607
- }
608
- this._syncTextBackdrop(window.PIXI, e.__textBackdrop, c, h, n, a, f), this._syncTextEffectPrimitive(window.PIXI, e.__textShadow, t, c, "shadow", h, n, f), this._syncTextEffectPrimitive(window.PIXI, e.__textGlow, t, c, "glow", h, n, f);
609
- }
610
- _fitDisplayObjectToRect(e, t, i) {
611
- var c, p, u, f, m, b, k, x;
612
- const s = this._backgroundFillForLayer(t), r = (s == null ? void 0 : s.fit) || "cover";
613
- if (e.__isTilingBackground) {
614
- e.x = i.x, e.y = i.y, e.width = i.width, e.height = i.height, (p = (c = e.tilePosition) == null ? void 0 : c.set) == null || p.call(c, 0, 0), (f = (u = e.tileScale) == null ? void 0 : u.set) == null || f.call(u, 1, 1);
615
- return;
616
- }
617
- if (r === "stretch" || (s == null ? void 0 : s.kind) === "gradient" || (s == null ? void 0 : s.kind) === "color") {
618
- e.x = i.x, e.y = i.y, e.width = i.width, e.height = i.height;
619
- return;
620
- }
621
- const o = Number(e.__sourceWidth || ((m = s == null ? void 0 : s.source) == null ? void 0 : m.originalWidth) || ((b = e.texture) == null ? void 0 : b.width) || i.width), n = Number(e.__sourceHeight || ((k = s == null ? void 0 : s.source) == null ? void 0 : k.originalHeight) || ((x = e.texture) == null ? void 0 : x.height) || i.height);
622
- if (!Number.isFinite(o) || !Number.isFinite(n) || o <= 0 || n <= 0) {
623
- e.x = i.x, e.y = i.y, e.width = i.width, e.height = i.height;
624
- return;
625
- }
626
- const a = r === "contain" ? Math.min(i.width / o, i.height / n) : Math.max(i.width / o, i.height / n), l = o * a, h = n * a;
627
- e.x = i.x + (i.width - l) / 2, e.y = i.y + (i.height - h) / 2, e.width = l, e.height = h;
628
- }
629
- _destroyLayerDisplayObjects() {
630
- var e, t, i, s, r;
631
- if ((e = this._layerDisplayObjects) != null && e.size) {
632
- for (const { displayObject: o } of this._layerDisplayObjects.values()) {
633
- try {
634
- (s = (i = (t = this.app) == null ? void 0 : t.stage) == null ? void 0 : i.removeChild) == null || s.call(i, o);
635
- } catch {
636
- }
637
- const n = (o == null ? void 0 : o.__ownsTexture) === !0;
638
- (r = o == null ? void 0 : o.destroy) == null || r.call(o, { children: !0, texture: n, textureSource: n });
639
- }
640
- this._layerDisplayObjects.clear(), this._renderableLayerIds = [];
641
- }
642
- }
643
- _destroyTransientDisplayObject(e) {
644
- var i, s, r, o;
645
- if (!e) return;
646
- try {
647
- (r = (s = (i = this.app) == null ? void 0 : i.stage) == null ? void 0 : s.removeChild) == null || r.call(s, e);
648
- } catch {
649
- }
650
- const t = (e == null ? void 0 : e.__ownsTexture) === !0;
651
- (o = e == null ? void 0 : e.destroy) == null || o.call(e, { children: !0, texture: t, textureSource: t });
652
- }
653
- /**
654
- * Normalize degrees to the 0..360 range.
655
- * @param {number} degrees
656
- * @returns {number}
657
- */
658
- _normalizeDegrees(e) {
659
- if (!Number.isFinite(e)) return 0;
660
- const t = e % 360;
661
- return t < 0 ? t + 360 : t;
662
- }
663
- /**
664
- * Calculate the axis-aligned bounds for a rotated rectangle.
665
- * @param {number} width
666
- * @param {number} height
667
- * @param {number} degrees
668
- * @returns {{ width: number, height: number }}
669
- */
670
- getRotatedBounds(e, t, i) {
671
- const s = this._normalizeDegrees(i);
672
- if (s === 0)
673
- return {
674
- width: Math.max(1, Math.round(e)),
675
- height: Math.max(1, Math.round(t))
676
- };
677
- const r = s * Math.PI / 180, o = Math.abs(Math.cos(r)), n = Math.abs(Math.sin(r)), a = o < 1e-10 ? 0 : o, l = n < 1e-10 ? 0 : n;
678
- return {
679
- width: Math.max(1, Math.ceil(e * a + t * l)),
680
- height: Math.max(1, Math.ceil(e * l + t * a))
681
- };
682
- }
683
- /**
684
- * Permanently rotate the working texture. This keeps crop math simple:
685
- * after rotation, sprite coordinates and texture dimensions both describe
686
- * the visible image exactly.
687
- *
688
- * @param {number} degrees Clockwise degrees to add to the working texture.
689
- * @returns {boolean}
690
- */
691
- rotateBy(e) {
692
- var m, b, k;
693
- const t = this._normalizeDegrees(e);
694
- if (t === 0) return !0;
695
- if (!((m = this.app) != null && m.renderer) || !this.originalTexture || !this.sprite)
696
- return !1;
697
- const i = window.PIXI, s = Math.round(this.originalTexture.width), r = Math.round(this.originalTexture.height);
698
- if (s <= 0 || r <= 0) return !1;
699
- const { width: o, height: n } = this.getRotatedBounds(s, r, t), a = t * Math.PI / 180, l = new i.Container(), h = new i.Sprite(this.originalTexture);
700
- (k = (b = h.anchor) == null ? void 0 : b.set) == null || k.call(b, 0.5), h.x = o / 2, h.y = n / 2, h.rotation = a, l.addChild(h);
701
- const c = i.RenderTexture.create({ width: o, height: n, resolution: 1 });
702
- try {
703
- this.app.renderer.render({
704
- container: l,
705
- target: c,
706
- clear: !0,
707
- clearColor: [0, 0, 0, 0]
708
- });
709
- } catch {
710
- return c.destroy(!0), !1;
711
- } finally {
712
- l.removeChildren(), h.destroy({ children: !1, texture: !1, textureSource: !1 }), l.destroy({ children: !1 });
713
- }
714
- const p = this.originalTexture, u = Array.isArray(this.sprite.filters) ? [...this.sprite.filters] : null;
715
- p && p !== this.baseTexture && p.destroy(!0), this.originalTexture = c, this._lastExportDimensions = null, this.app.stage.removeChild(this.sprite), this.sprite.destroy({ children: !1, texture: !1, textureSource: !1 });
716
- const f = new i.Sprite(c);
717
- return u != null && u.length && (f.filters = u), this.app.stage.addChild(f), this.sprite = f, this.fitScale = this.getFitScaleFor(c), this.zoom = 1, this.applyViewTransform({ keepCenter: !1 }), this.render(), this.emit("textureRotated", { width: o, height: n, degrees: t }), !0;
718
- }
719
- /**
720
- * Export the current image with filters applied.
721
- *
722
- * @param {string} format - 'png' or 'jpeg'
723
- * @param {number} quality - Quality for jpeg (0-1)
724
- * @param {number} maxEdge - Maximum edge size in pixels (0 for original)
725
- * @param {boolean} dontUpscale - Don't upscale smaller images
726
- * @param {number} [maxPixels=0] - Max total pixel count (width*height). 0 disables.
727
- * Backend API often limits payload size (blowglass caps at 8M pixels); set this
728
- * to keep the export under that cap while preserving aspect ratio.
729
- * @returns {string|null} Data URL
730
- */
731
- exportImage(e = "png", t = 0.92, i = 0, s = !0, r = 0) {
732
- var S;
733
- if (!((S = this.app) != null && S.renderer) || !this.originalTexture || !this.sprite)
734
- return null;
735
- const o = window.PIXI, n = Math.round(this.originalTexture.width), a = Math.round(this.originalTexture.height);
736
- if (n <= 0 || a <= 0)
737
- return null;
738
- const l = this.sprite.width, h = this.sprite.height;
739
- if (l <= 0 || h <= 0)
740
- return null;
741
- const c = l / n, p = c > 0 ? 1 / c : 1, u = this._layerDocument, f = u && Array.isArray(u.layers), m = [];
742
- if (!f && Array.isArray(this.sprite.filters)) {
743
- for (const y of this.sprite.filters)
744
- if (y)
745
- if (typeof y.createExportFilter == "function") {
746
- const w = y.createExportFilter({ previewToNativeScale: p });
747
- w && m.push(w);
748
- } else
749
- m.push(y);
750
- }
751
- let b = 0;
752
- for (const y of m) {
753
- const w = typeof y.getExportPadding == "function" ? Number(y.getExportPadding()) || 0 : typeof y._exportPadding == "number" ? y._exportPadding : typeof y.padding == "number" ? y.padding : 0;
754
- w > 0 && (b += w);
755
- }
756
- const k = Math.max(Math.ceil(b), 64), x = (y) => {
757
- var Q, X, L, ee;
758
- const w = Math.ceil(n + 2 * y), R = Math.ceil(a + 2 * y), M = new o.Container(), N = [], z = (_ = null) => {
759
- const v = new o.Sprite(this.originalTexture);
760
- v.x = y, v.y = y, v.scale.set(1, 1), v.alpha = _ ? this._layerOpacity(_) : 1, _ && this._applyLayerFilterInstances(v, _, {
761
- forExport: !0,
762
- previewToNativeScale: p,
763
- sourceWidth: n,
764
- sourceHeight: a
765
- }), m.length > 0 && (v.filters = m, v.filterArea = new o.Rectangle(0, 0, n, a)), M.addChild(v), N.push(v);
766
- }, F = (_) => {
767
- var Y, P, U;
768
- const v = (Y = this._layerDisplayObjects.get(_.id)) == null ? void 0 : Y.displayObject, H = this._isSubjectLayer(_), I = (v == null ? void 0 : v.texture) || (H ? this.originalTexture : null);
769
- if (!I || typeof o.Sprite != "function") return !1;
770
- const C = new o.Sprite(I), te = (v == null ? void 0 : v.__sourceWidth) || (H ? null : (P = _.source) == null ? void 0 : P.originalWidth) || I.width || n, ie = (v == null ? void 0 : v.__sourceHeight) || (H ? null : (U = _.source) == null ? void 0 : U.originalHeight) || I.height || a;
771
- return C.__sourceWidth = Number(te), C.__sourceHeight = Number(ie), this._fitDisplayObjectToRect(C, _, { x: y, y, width: n, height: a }), C.alpha = this._layerOpacity(_), !f && H && m.length > 0 && (C.filters = m, C.filterArea = new o.Rectangle(0, 0, n, a)), this._applyLayerEffects(o, C, _), this._applyLayerFilterInstances(C, _, {
772
- forExport: !0,
773
- previewToNativeScale: p,
774
- sourceWidth: n,
775
- sourceHeight: a
776
- }), M.addChild(C), N.push(C), !0;
777
- }, T = (_) => {
778
- var te, ie, Y;
779
- const v = this._backgroundFillForLayer(_), H = this._backgroundColorForLayer(_), I = { x: y, y, width: n, height: a };
780
- let C = null;
781
- if (H && typeof o.Graphics == "function")
782
- C = new o.Graphics(), C.rect(y, y, n, a).fill(H);
783
- else if ((v == null ? void 0 : v.kind) === "gradient" && typeof o.Sprite == "function") {
784
- const P = this._createGradientTexture(o, v, n, a);
785
- if (!P) return !1;
786
- C = new o.Sprite(P), C.__ownsTexture = !0, C.__sourceWidth = n, C.__sourceHeight = a, this._fitDisplayObjectToRect(C, _, I);
787
- } else if ((v == null ? void 0 : v.kind) === "media") {
788
- const P = (te = this._layerDisplayObjects.get(_.id)) == null ? void 0 : te.displayObject, U = P == null ? void 0 : P.texture;
789
- if (!U) return !1;
790
- C = this._createBackgroundSprite(o, U, v.fit), C.__sourceWidth = Number(((ie = v.source) == null ? void 0 : ie.originalWidth) || P.__sourceWidth || U.width || 1), C.__sourceHeight = Number(((Y = v.source) == null ? void 0 : Y.originalHeight) || P.__sourceHeight || U.height || 1), this._fitDisplayObjectToRect(C, _, I);
791
- }
792
- return C ? (C.alpha = this._layerOpacity(_), this._applyLayerEffects(o, C, _), this._applyLayerFilterInstances(C, _, {
793
- forExport: !0,
794
- previewToNativeScale: p,
795
- sourceWidth: n,
796
- sourceHeight: a
797
- }), M.addChild(C), N.push(C), !0) : !1;
798
- }, O = (_) => {
799
- const v = this._createTextDisplayObjectForLayer(o, _);
800
- return v ? (this._fitTextDisplayObjectToRect(v, _, { x: y, y, width: n, height: a }), v.alpha = this._layerOpacity(_), M.addChild(v), N.push(v), !0) : !1;
801
- }, W = u, G = f;
802
- if (G) {
803
- for (const _ of W.layers)
804
- if (!(!_ || _.visible === !1)) {
805
- if (this._isTextLayer(_)) {
806
- O(_);
807
- continue;
808
- }
809
- if (_.type === "image") {
810
- !F(_) && this._isSubjectLayer(_) && z(_);
811
- continue;
812
- }
813
- if (this._isSubjectLayer(_)) {
814
- F(_) || z(_);
815
- continue;
816
- }
817
- T(_);
818
- }
819
- } else
820
- z();
821
- !G && ((Q = M.children) == null ? void 0 : Q.length) === 0 && z();
822
- const K = o.RenderTexture.create({
823
- width: w,
824
- height: R,
825
- resolution: 1
826
- }), oe = ((X = M.children) == null ? void 0 : X.length) ?? 0;
827
- try {
828
- this.app.renderer.render({
829
- container: M,
830
- target: K,
831
- clear: !0,
832
- clearColor: [0, 0, 0, 0]
833
- });
834
- const _ = (ee = (L = this.app.renderer.extract) == null ? void 0 : L.canvas) == null ? void 0 : ee.call(L, K);
835
- return _ ? { canvas: _, width: w, height: R, margin: y, childCount: oe } : null;
836
- } finally {
837
- for (const _ of N)
838
- _.filters = null;
839
- M.removeChildren();
840
- for (const _ of N) {
841
- const v = (_ == null ? void 0 : _.__ownsTexture) === !0;
842
- _.destroy({ children: !1, texture: v, textureSource: v });
843
- }
844
- M.destroy({ children: !1 }), K.destroy(!0);
845
- }
846
- }, B = (y, w, R) => {
847
- const M = typeof y.getContext == "function" ? y.getContext("2d") : null;
848
- if (!M) return null;
849
- let N;
850
- try {
851
- N = M.getImageData(0, 0, w, R).data;
852
- } catch {
853
- return null;
854
- }
855
- const z = 1;
856
- let F = w, T = R, O = -1, W = -1;
857
- const G = 4, K = w * G;
858
- for (let L = 0; L < R; L++) {
859
- const ee = L * K;
860
- for (let _ = 0; _ < w; _++)
861
- N[ee + _ * G + 3] >= z && (_ < F && (F = _), _ > O && (O = _), L < T && (T = L), L > W && (W = L));
862
- }
863
- if (O < 0) return null;
864
- const oe = O - F + 1, Q = W - T + 1, X = F === 0 || T === 0 || O === w - 1 || W === R - 1;
865
- return { x: F, y: T, width: oe, height: Q, touchesEdge: X };
866
- };
867
- let A = null;
868
- try {
869
- let y = x(k);
870
- if (!y)
871
- return null;
872
- let w = B(y.canvas, y.width, y.height);
873
- if (!w)
874
- if (f && y.childCount === 0)
875
- w = {
876
- x: y.margin,
877
- y: y.margin,
878
- width: n,
879
- height: a,
880
- touchesEdge: !1
881
- };
882
- else
883
- return null;
884
- if (w.touchesEdge && k < n && k < a) {
885
- const F = Math.max(k * 4, 256), T = x(F);
886
- if (T) {
887
- const O = B(T.canvas, T.width, T.height);
888
- O && !O.touchesEdge ? (y = T, w = O) : (y = T, w = O ?? w);
889
- }
890
- }
891
- let R = w.width, M = w.height;
892
- if (i > 0) {
893
- const F = Math.max(R, M);
894
- let T = i / F;
895
- s && (T = Math.min(1, T)), R = Math.max(1, Math.round(R * T)), M = Math.max(1, Math.round(M * T));
896
- }
897
- if (r > 0 && R * M > r) {
898
- const F = Math.sqrt(r / (R * M));
899
- R = Math.max(1, Math.floor(R * F)), M = Math.max(1, Math.floor(M * F));
900
- }
901
- const N = document.createElement("canvas");
902
- N.width = R, N.height = M;
903
- const z = N.getContext("2d");
904
- return z ? (z.drawImage(
905
- y.canvas,
906
- w.x,
907
- w.y,
908
- w.width,
909
- w.height,
910
- 0,
911
- 0,
912
- R,
913
- M
914
- ), A = N.toDataURL(`image/${e}`, t), this._lastExportDimensions = { width: R, height: M }, A || null) : null;
915
- } catch {
916
- return null;
917
- }
918
- }
919
- /**
920
- * Get the actual export dimensions at the current filter state.
921
- *
922
- * When an export has been performed, returns the exact trimmed dimensions
923
- * from that export. Otherwise falls back to source texture size plus a
924
- * rough filter-padding estimate — the real dimensions are only known for
925
- * certain after exportImage() runs the bbox scan.
926
- *
927
- * @returns {{ width: number, height: number }}
928
- */
929
- getExportDimensions() {
930
- var o;
931
- if (this._lastExportDimensions)
932
- return { ...this._lastExportDimensions };
933
- const e = this.originalTexture, t = Math.round((e == null ? void 0 : e.width) || 0), i = Math.round((e == null ? void 0 : e.height) || 0);
934
- let s = 0;
935
- const r = (o = this.sprite) == null ? void 0 : o.filters;
936
- if (Array.isArray(r))
937
- for (const n of r) {
938
- if (!n) continue;
939
- const a = typeof n._exportPadding == "number" ? n._exportPadding : typeof n.padding == "number" ? n.padding : 0;
940
- a > s && (s = a);
941
- }
942
- return {
943
- width: t + 2 * s,
944
- height: i + 2 * s
945
- };
946
- }
947
- /**
948
- * Export the current image as a Blob with correct dimensions.
949
- * @param {string} format - 'png' or 'jpeg'
950
- * @param {number} quality - Quality for jpeg (0-1)
951
- * @param {{ maxPixels?: number, maxEdge?: number, dontUpscale?: boolean }} [options]
952
- * @returns {Promise<{ blob: Blob, width: number, height: number } | null>}
953
- */
954
- async exportBlob(e = "png", t = 0.92, i = {}) {
955
- const s = this.exportImage(
956
- e,
957
- t,
958
- i.maxEdge ?? 0,
959
- i.dontUpscale !== !1,
960
- i.maxPixels ?? 0
961
- );
962
- if (!s) return null;
963
- const o = await (await fetch(s)).blob(), n = this.getExportDimensions();
964
- return { blob: o, width: n.width, height: n.height };
965
- }
966
- /**
967
- * Resize renderer to container
968
- * @param {HTMLElement} container
969
- */
970
- resizeTo(e) {
971
- var s;
972
- if (!((s = this.app) != null && s.renderer) || !e) return;
973
- const t = e.clientWidth, i = e.clientHeight;
974
- if (!(t === Math.round(this.app.screen.width) && i === Math.round(this.app.screen.height)) && !(t <= 0 || i <= 0) && (this.app.renderer.resize(t, i), this.originalTexture && this.sprite)) {
975
- const r = {
976
- x: this.sprite.x + this.sprite.width / 2,
977
- y: this.sprite.y + this.sprite.height / 2
978
- };
979
- this.fitScale = this.getFitScaleFor(this.originalTexture), this.applyViewTransform({ center: r });
980
- }
981
- }
982
- /**
983
- * Set background color
984
- * @param {number} color - Hex color
985
- */
986
- setBackgroundColor(e) {
987
- var t, i;
988
- (i = (t = this.app) == null ? void 0 : t.renderer) != null && i.background && (this.app.renderer.background.color = e);
989
- }
990
- /**
991
- * Load an image URL into an HTMLImageElement.
992
- * Handles blob:, data:, and http(s): URLs.
993
- * @param {string} url
994
- * @returns {Promise<HTMLImageElement|null>}
995
- */
996
- _loadImageElement(e) {
997
- return new Promise((t) => {
998
- const i = new Image();
999
- typeof e == "string" && /^https?:\/\//.test(e) && (i.crossOrigin = "anonymous"), i.onload = () => t(i), i.onerror = () => {
1000
- t(null);
1001
- }, i.src = e;
1002
- });
1003
- }
1004
- /**
1005
- * Clean up and destroy
1006
- */
1007
- destroy() {
1008
- if (this._destroyed = !0, this._mountToken += 1, this._mountPromise = null, this._destroyLayerDisplayObjects(), this.app) {
1009
- try {
1010
- this.app.destroy(!0, { children: !0, texture: !0 });
1011
- } catch {
1012
- }
1013
- this.app = null;
1014
- }
1015
- this.sprite = null, this.originalTexture = null, this.baseTexture = null, this._container = null, this.removeAllListeners();
1016
- }
1017
- }
1018
- const _e = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M328 112L184 256l144 144"/></svg>', me = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="48" d="M184 112l144 144-144 144"/></svg>', gt = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M221.09 64a157.09 157.09 0 10157.09 157.09A157.1 157.1 0 00221.09 64z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M338.29 338.29L448 448M256 184v74m-37-37h74"/></svg>', _t = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M221.09 64a157.09 157.09 0 10157.09 157.09A157.1 157.1 0 00221.09 64z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M338.29 338.29L448 448M184 221h74"/></svg>', mt = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M432 320v112H320M80 192V80h112M320 80h112v112M192 432H80V320"/></svg>', Ae = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M262.29 192.31a64 64 0 1057.4 57.4 64.13 64.13 0 00-57.4-57.4z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M416.39 256a154.34 154.34 0 01-1.53 20.79l45.21 35.46a10.81 10.81 0 012.45 13.75l-42.77 74a10.81 10.81 0 01-13.14 4.59l-44.9-18.08a16.11 16.11 0 00-15.17 1.75A164.48 164.48 0 01325 400.8a15.94 15.94 0 00-8.82 12.14l-6.73 47.89a11.08 11.08 0 01-10.68 9.17h-85.54a11.11 11.11 0 01-10.69-8.87l-6.72-47.82a16.07 16.07 0 00-9-12.22 155.3 155.3 0 01-21.46-12.57 16 16 0 00-15.11-1.71l-44.89 18.07a10.81 10.81 0 01-13.14-4.58l-42.77-74a10.8 10.8 0 012.45-13.75l38.21-30a16.05 16.05 0 006-14.08c-.36-4.17-.58-8.33-.58-12.5s.21-8.27.58-12.35a16 16 0 00-6.07-13.94l-38.19-30A10.81 10.81 0 0149.48 186l42.77-74a10.81 10.81 0 0113.14-4.59l44.9 18.08a16.11 16.11 0 0015.17-1.75A164.48 164.48 0 01187 111.2a15.94 15.94 0 008.82-12.14l6.73-47.89A11.08 11.08 0 01213.23 42h85.54a11.11 11.11 0 0110.69 8.87l6.72 47.82a16.07 16.07 0 009 12.22 155.3 155.3 0 0121.46 12.57 16 16 0 0015.11 1.71l44.89-18.07a10.81 10.81 0 0113.14 4.58l42.77 74a10.8 10.8 0 01-2.45 13.75l-38.21 30a16.05 16.05 0 00-6.05 14.08c.33 4.14.55 8.3.55 12.47z"/></svg>', Ne = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M400 320c0 88.37-55.63 144-144 144s-144-55.63-144-144c0-94.83 103.23-222.85 134.89-259.88a12 12 0 0118.23 0C296.77 97.15 400 225.17 400 320z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M344 328a72 72 0 01-72 72"/></svg>', Be = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32" d="M430.11 347.9c-6.6-6.1-16.3-7.6-24.6-9-11.5-1.9-15.9-4-22.6-10-14.3-12.7-14.3-31.1 0-43.8l30.3-26.9c46.4-41 46.4-108.2 0-149.2-34.2-30.1-80.1-45-127.8-45-55.7 0-113.9 20.3-158.8 60.1-83.5 73.8-83.5 194.7 0 268.5 41.5 36.7 97.5 55 152.9 55.4h1.7c55.4 0 110-17.9 148.8-52.4 14.4-12.7 11.99-36.6.1-47.7z"/><circle cx="144" cy="208" r="32"/><circle cx="152" cy="311" r="32"/><circle cx="224" cy="144" r="32"/><circle cx="256" cy="367" r="32"/><circle cx="328" cy="144" r="32"/></svg>', Ee = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M259.92 262.91L216.4 149.77a9 9 0 00-16.8 0l-43.52 113.14a9 9 0 01-5.17 5.17L37.77 311.6a9 9 0 000 16.8l113.14 43.52a9 9 0 015.17 5.17l43.52 113.14a9 9 0 0016.8 0l43.52-113.14a9 9 0 015.17-5.17l113.14-43.52a9 9 0 000-16.8l-113.14-43.52a9 9 0 01-5.17-5.17zM108 68L88 16 68 68 16 88l52 20 20 52 20-52 52-20-52-20zM426.67 117.33L400 48l-26.67 69.33L304 144l69.33 26.67L400 240l26.67-69.33L496 144l-69.33-26.67z"/></svg>', Le = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M176 112l80-80 80 80M255.98 32l.02 448M176 400l80 80 80-80M400 176l80 80-80 80M112 176l-80 80 80 80M32 256h448"/></svg>', De = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M452.37 59.63h0a40.49 40.49 0 00-57.26 0L184 294.74c23.08 4.7 46.12 27.29 49.26 49.26l219.11-227.11a40.49 40.49 0 000-57.26z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M138 336c-29.88 0-54 24.5-54 54.86 0 23.95-20.67 36.57-34 44.78 15.08 8.08 32.23 12.36 50 12.36 48.49 0 88-38.89 88-88 0-30.36-24.12-54-50-54z"/></svg>', je = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M315.27 33L96 304h128l-31.51 173.23a2.36 2.36 0 002.33 2.77h0a2.36 2.36 0 001.89-.95L416 208H288l31.66-173.25a2.45 2.45 0 00-2.44-2.75h0a2.42 2.42 0 00-1.95 1z"/></svg>', be = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M144 48v272a48 48 0 0048 48h272"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 464V192a48 48 0 00-48-48H48"/></svg>', Oe = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M380.93 57.37A32 32 0 00358.3 48H94.22A46.21 46.21 0 0048 94.22v323.56A46.21 46.21 0 0094.22 464h323.56A46.36 46.36 0 00464 417.78V153.7a32 32 0 00-9.37-22.63zM256 416a64 64 0 1164-64 63.92 63.92 0 01-64 64zm48-224H112a16 16 0 01-16-16v-64a16 16 0 0116-16h192a16 16 0 0116 16v64a16 16 0 01-16 16z"/></svg>', ue = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M368 368L144 144M368 144L144 368"/></svg>', ye = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M320 146s24.36-12-64-12a160 160 0 10160 160"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M256 58l80 80-80 80"/></svg>', ze = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M176 112l-64 64 64 64"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M112 176h176a112 112 0 110 224H160"/></svg>', Pe = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M336 112l64 64-64 64"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M400 176H224a112 112 0 100 224h128"/></svg>', He = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M112 112l20 320c.95 18.49 14.4 32 32 32h184c17.67 0 30.87-13.51 32-32l20-320"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M80 112h352"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M192 112V72h0a23.93 23.93 0 0124-24h80a23.93 23.93 0 0124 24h0v40M256 176v224M184 176l8 224M328 176l-8 224"/></svg>', Ie = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M64 192v-72a40 40 0 0140-40h75.89a40 40 0 0122.19 6.72l27.84 18.56a40 40 0 0022.19 6.72H408a40 40 0 0140 40v40"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M479.9 226.55L463.68 392a40 40 0 01-39.93 40H88.25a40 40 0 01-39.93-40L32.1 226.55A32 32 0 0164 192h384.1a32 32 0 0131.8 34.55z"/></svg>', ce = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M416 128L192 384l-96-96"/></svg>', Ue = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M48 256l208 112 208-112-208-112L48 256z"/><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M48 336l208 112 208-112M48 176L256 64l208 112"/></svg>', bt = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M48 256s80-144 208-144 208 144 208 144-80 144-208 144S48 256 48 256z"/><circle cx="256" cy="256" r="64" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/></svg>', yt = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M432 448L80 64M160 160c-67.2 36.8-112 96-112 96s80 144 208 144c35.8 0 68.4-11.3 96-28.1M224.4 113.8A199.6 199.6 0 01256 112c128 0 208 144 208 144a334.2 334.2 0 01-65.1 78.6"/><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M213.3 213.3A64 64 0 10298.7 298.7"/></svg>', vt = '<svg viewBox="0 0 512 512" width="20" height="20"><rect x="96" y="208" width="320" height="240" rx="32" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M160 208v-64a96 96 0 01192 0v64"/></svg>', wt = '<svg viewBox="0 0 512 512" width="20" height="20"><rect x="96" y="208" width="320" height="240" rx="32" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M160 208v-64a96 96 0 01168-63.7"/></svg>', xt = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M96 352l-32 96 96-32L421.5 154.5a45.3 45.3 0 000-64h0a45.3 45.3 0 00-64 0L96 352z"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M320 128l64 64"/></svg>', Ct = '<svg viewBox="0 0 512 512" width="20" height="20"><rect x="144" y="144" width="288" height="288" rx="32" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32" d="M80 368V112a32 32 0 0132-32h256"/></svg>', Ve = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M96 112V80h320v32M256 80v352M184 432h144"/></svg>', We = '<svg viewBox="0 0 512 512" width="20" height="20"><rect x="64" y="64" width="384" height="384" rx="48" fill="none" stroke="currentColor" stroke-linejoin="round" stroke-width="32"/></svg>', $e = '<svg viewBox="0 0 512 512" width="20" height="20"><circle cx="256" cy="256" r="208" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/></svg>', Ze = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M80 96h64l64 320h64l64-160h96"/></svg>', fe = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32" d="M256 48v48M256 416v48M403.08 108.92l-33.94 33.94M142.86 369.14l-33.94 33.94M464 256h-48M96 256H48M403.08 403.08l-33.94-33.94M142.86 142.86l-33.94-33.94"/><circle cx="256" cy="256" r="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-miterlimit="10" stroke-width="32"/></svg>', ge = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M160 136c0-30.62 4.51-61.61 16-88C99.57 81.27 48 159.32 48 248c0 119.29 96.71 216 216 216 88.68 0 166.73-51.57 200-128-26.39 11.49-57.38 16-88 16-119.29 0-216-96.71-216-216z"/></svg>', qe = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M436 80H76a44.05 44.05 0 00-44 44v264a44.05 44.05 0 0044 44h360a44.05 44.05 0 0044-44V124a44.05 44.05 0 00-44-44z"/><circle cx="256" cy="256" r="80" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32"/><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M220 80v56M292 80v56M220 376v56M292 376v56M80 144h56M80 224h56M80 304h56M376 144h56M376 224h56M376 304h56"/></svg>', Ge = '<svg viewBox="0 0 512 512" width="20" height="20"><path fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="32" d="M344 144c-3.92 52.87-44 96-88 96s-84.15-43.12-88-96c-4-55 35-96 88-96s92 42 88 96z"/><path fill="none" stroke="currentColor" stroke-miterlimit="10" stroke-width="32" d="M256 304c-87 0-175.3 48-191.64 138.6C62.39 453.52 68.57 464 80 464h352c11.44 0 17.62-10.48 15.65-21.4C431.3 352 343 304 256 304z"/></svg>', pe = [
1019
- {
1020
- id: "adjust",
1021
- name: "Adjust",
1022
- icon: Ae,
1023
- registryCategories: ["adjust", "advanced"]
1024
- // adjustment, adjustmentAdvanced, alpha, colorMatrix
1025
- },
1026
- {
1027
- id: "blur",
1028
- name: "Blur",
1029
- icon: Ne,
1030
- registryCategories: ["blur"]
1031
- },
1032
- {
1033
- id: "color",
1034
- name: "Color",
1035
- icon: Be,
1036
- registryCategories: ["color"]
1037
- },
1038
- {
1039
- id: "effects",
1040
- name: "Effects",
1041
- icon: Ee,
1042
- registryCategories: ["effects"]
1043
- },
1044
- {
1045
- id: "distortion",
1046
- name: "Distortion",
1047
- icon: Le,
1048
- registryCategories: ["distortion"]
1049
- },
1050
- {
1051
- id: "light",
1052
- name: "Light",
1053
- icon: je,
1054
- registryCategories: ["light"]
1055
- },
1056
- {
1057
- id: "stylize",
1058
- name: "Stylize",
1059
- icon: qe,
1060
- registryCategories: ["stylize"]
1061
- },
1062
- {
1063
- id: "text",
1064
- name: "Text",
1065
- icon: Ve
1066
- },
1067
- {
1068
- id: "background",
1069
- name: "Background",
1070
- icon: De
1071
- },
1072
- {
1073
- id: "layers",
1074
- name: "Layers",
1075
- icon: Ue
1076
- },
1077
- {
1078
- id: "crop",
1079
- name: "Crop",
1080
- icon: be
1081
- }
1082
- ], Ke = Object.fromEntries(
1083
- pe.filter((g) => Array.isArray(g.registryCategories) && g.registryCategories.length > 0).map((g) => [g.id, g.registryCategories])
1084
- ), Ye = Object.fromEntries(
1085
- pe.filter((g) => Array.isArray(g.registryCategories)).flatMap((g) => g.registryCategories.map((e) => [e, g.id]))
1086
- );
1087
- function Je(g) {
1088
- return Ke[g] || [g];
1089
- }
1090
- class Qe extends ne {
1091
- constructor(e, t) {
1092
- super(), this.state = e, this.renderer = t, this.instances = {}, this._filterRegistry = null;
1093
- }
1094
- /**
1095
- * Set the filter registry (from mediables/filters)
1096
- * @param {Object} registry - { getFilter, getAllFilters, getFiltersByCategory }
1097
- */
1098
- setRegistry(e) {
1099
- var t, i;
1100
- this._filterRegistry = e, (i = (t = this.renderer) == null ? void 0 : t.setLayerFilterFactory) == null || i.call(t, (s, r) => this.createFiltersForLayer(s, r));
1101
- }
1102
- /**
1103
- * Get filter definition by ID
1104
- * @param {string} filterId
1105
- * @returns {Object|null}
1106
- */
1107
- getFilterDef(e) {
1108
- return this._filterRegistry ? this._filterRegistry.getFilter(e) : null;
1109
- }
1110
- /**
1111
- * Get all filters
1112
- * @returns {Array}
1113
- */
1114
- getAllFilters() {
1115
- var e;
1116
- return ((e = this._filterRegistry) == null ? void 0 : e.getAllFilters()) || [];
1117
- }
1118
- /**
1119
- * Get filters by category
1120
- * Maps UI category names to registry category names
1121
- * @param {string} category - UI category ID
1122
- * @returns {Array}
1123
- */
1124
- getFiltersByCategory(e) {
1125
- if (!this._filterRegistry) return [];
1126
- const t = Je(e), i = [], s = /* @__PURE__ */ new Set();
1127
- for (const r of t) {
1128
- const o = this._filterRegistry.getFiltersByCategory(r) || [];
1129
- for (const n of o)
1130
- s.has(n.id) || (s.add(n.id), i.push(n));
1131
- }
1132
- return i;
1133
- }
1134
- /**
1135
- * Normalize UI values from various input types
1136
- * @param {*} value
1137
- * @returns {*}
1138
- */
1139
- _normalizeValue(e) {
1140
- return Array.isArray(e) ? e.length > 0 && typeof e[0] == "object" ? e : Number(e[0] ?? 0) : typeof e == "string" ? e.startsWith("#") ? e : e.trim() === "" ? 0 : Number(e) : e;
1141
- }
1142
- /**
1143
- * Get a deep property from an object.
1144
- * Returns undefined if any segment along the path is missing.
1145
- * @param {Object} target
1146
- * @param {string} path
1147
- * @returns {*}
1148
- */
1149
- _getDeepProp(e, t) {
1150
- if (!e || !t) return;
1151
- if (!t.includes(".") && !t.includes("["))
1152
- return e[t];
1153
- const i = t.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
1154
- let s = e;
1155
- for (const r of i) {
1156
- if (s == null) return;
1157
- s = s[r];
1158
- }
1159
- return s;
1160
- }
1161
- /**
1162
- * Set a deep property on an object
1163
- * @param {Object} target
1164
- * @param {string} path
1165
- * @param {*} value
1166
- */
1167
- _setDeepProp(e, t, i) {
1168
- if (!e || !t) return;
1169
- if (!t.includes(".") && !t.includes("[")) {
1170
- e[t] = i;
1171
- return;
1172
- }
1173
- const s = t.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
1174
- let r = e;
1175
- for (let n = 0; n < s.length - 1; n++) {
1176
- const a = s[n];
1177
- if (!(a in r) || (r = r[a], r == null)) return;
1178
- }
1179
- const o = s[s.length - 1];
1180
- r[o] = i;
1181
- }
1182
- /**
1183
- * Initialize filter values from definition defaults.
1184
- * Seeds BOTH defaultParams (e.g. colorStops) and control defaults so that
1185
- * filters with dynamic parameters not backed by a static control still have
1186
- * their state populated.
1187
- * @param {string} filterId
1188
- */
1189
- initializeValues(e, t = void 0) {
1190
- const i = this.getFilterDef(e);
1191
- if (!i) return;
1192
- const s = this.state.getFilterValues(e), r = new Set((i.controls || []).map((o) => o.id));
1193
- if (i.defaultParams)
1194
- for (const [o, n] of Object.entries(i.defaultParams))
1195
- !r.has(o) && !(o in s) && this.state.setFilterValue(e, o, n, t);
1196
- i.controls && i.controls.forEach((o) => {
1197
- o.id in s || this.state.setFilterValue(e, o.id, o.default, t);
1198
- });
1199
- }
1200
- /**
1201
- * Reset all filter values to their defaults (unconditionally).
1202
- * Resets BOTH defaultParams and control defaults.
1203
- * @param {string} filterId
1204
- */
1205
- resetValues(e, t = void 0) {
1206
- const i = this.getFilterDef(e);
1207
- if (!i) return;
1208
- const s = new Set((i.controls || []).map((r) => r.id));
1209
- if (i.defaultParams)
1210
- for (const [r, o] of Object.entries(i.defaultParams))
1211
- s.has(r) || this.state.setFilterValue(e, r, o, t);
1212
- i.controls && i.controls.forEach((r) => {
1213
- this.state.setFilterValue(e, r.id, r.default, t);
1214
- });
1215
- }
1216
- /**
1217
- * Toggle a filter on/off
1218
- * @param {string} filterId
1219
- * @param {boolean} enabled
1220
- */
1221
- toggle(e, t, i = void 0) {
1222
- return this.state.toggleFilter(e, t, i) === !1 ? !1 : (t ? this.initializeValues(e, i) : delete this.instances[e], this.applyFilters(), this.emit("filterToggled", { filterId: e, enabled: t }), !0);
1223
- }
1224
- /**
1225
- * Update a filter control value.
1226
- *
1227
- * The `controlId` may be either a static control id (from def.controls) or a
1228
- * dynamic path (e.g. 'colorStops[0].color') produced by a filter's
1229
- * getDynamicControls(). Static control ids are stored in state keyed by the
1230
- * control id. Dynamic paths are NOT stored as flat path keys — instead, we
1231
- * apply them to the live instance and then sync the filter's serializable
1232
- * params back into state, so recreation via applyFilters() rebuilds from
1233
- * the canonical shape (e.g. a colorStops array, not 'colorStops[0].color').
1234
- *
1235
- * Returns true when the filter was updated in-place. Returns false when the
1236
- * caller should invoke applyFilters() to recreate the filter from state —
1237
- * this happens when the property target is a method (which would be
1238
- * silently corrupted by a direct write) or when the property doesn't exist.
1239
- *
1240
- * @param {string} filterId
1241
- * @param {string} controlId - Static control id or dynamic property path
1242
- * @param {*} value
1243
- * @returns {boolean} True if updated in-place, false if caller must recreate
1244
- */
1245
- updateValue(e, t, i, s = void 0) {
1246
- const r = this._normalizeValue(i);
1247
- if (!this._matchesExpectedFilterLayer(s)) return !1;
1248
- const o = this.getFilterDef(e), n = o == null ? void 0 : o.controls.find((p) => p.id === t), a = !n && (t.includes("[") || t.includes("."));
1249
- if (!a && this.state.setFilterValue(e, t, r, s) === !1)
1250
- return !1;
1251
- const l = this.instances[e];
1252
- if (!l) return !1;
1253
- const h = (n == null ? void 0 : n.property) || t;
1254
- if (typeof l.updateUIParam == "function") {
1255
- if (l.updateUIParam(h, r), a && typeof l.getSerializableParams == "function") {
1256
- const p = l.getSerializableParams();
1257
- for (const [u, f] of Object.entries(p))
1258
- this.state.setFilterValue(e, u, f, s);
1259
- }
1260
- return !0;
1261
- }
1262
- const c = this._getDeepProp(l, h);
1263
- return typeof c == "function" ? !1 : l.uniforms && h in l.uniforms ? (l.uniforms[h] = r, !0) : c !== void 0 ? (this._setDeepProp(l, h, r), !0) : !1;
1264
- }
1265
- /**
1266
- * Perform a filter action (e.g. addColorStop, removeColorStop) and sync
1267
- * any resulting state changes back into State.js before recreating the
1268
- * filter. Without this sync, recreation via applyFilters() would rebuild
1269
- * from stale state and undo the action.
1270
- *
1271
- * @param {string} filterId
1272
- * @param {string} action - Action name understood by filter.updateUIParam
1273
- */
1274
- performFilterAction(e, t, i = void 0) {
1275
- if (!this._matchesExpectedFilterLayer(i)) return !1;
1276
- const s = this.instances[e];
1277
- if (!s || typeof s.updateUIParam != "function") return !1;
1278
- if (s.updateUIParam(t, !0), typeof s.getSerializableParams == "function") {
1279
- const r = s.getSerializableParams();
1280
- for (const [o, n] of Object.entries(r))
1281
- this.state.setFilterValue(e, o, n, i);
1282
- }
1283
- return this.applyFilters(), this.emit("filterActionPerformed", { filterId: e, action: t }), !0;
1284
- }
1285
- _matchesExpectedFilterLayer(e) {
1286
- if (e === void 0 || !this._layerDocument()) return !0;
1287
- if (e === null)
1288
- return typeof this.state.getActiveFilterLayerId == "function" ? this.state.getActiveFilterLayerId() === null : this.state.get("activeLayerId") === null;
1289
- const i = typeof e == "string" ? e.trim() : "";
1290
- return i === "" ? !1 : typeof this.state.getActiveFilterLayerId == "function" ? this.state.getActiveFilterLayerId() === i : this.state.get("activeLayerId") === i;
1291
- }
1292
- _layerDocument() {
1293
- var t, i;
1294
- const e = (i = (t = this.state).getSerializableDocument) == null ? void 0 : i.call(t);
1295
- return e && e.version === 2 && Array.isArray(e.layers) ? e : null;
1296
- }
1297
- _sourceDimensionsForLayer(e, t = {}) {
1298
- var s, r, o, n, a, l, h, c, p;
1299
- const i = (e == null ? void 0 : e.source) || ((s = e == null ? void 0 : e.fill) == null ? void 0 : s.source) || null;
1300
- return {
1301
- width: Math.round(Number(t.sourceWidth || (i == null ? void 0 : i.originalWidth) || ((o = (r = this.renderer) == null ? void 0 : r.originalTexture) == null ? void 0 : o.width) || ((a = (n = this.renderer) == null ? void 0 : n.sprite) == null ? void 0 : a.width) || 0)),
1302
- height: Math.round(Number(t.sourceHeight || (i == null ? void 0 : i.originalHeight) || ((h = (l = this.renderer) == null ? void 0 : l.originalTexture) == null ? void 0 : h.height) || ((p = (c = this.renderer) == null ? void 0 : c.sprite) == null ? void 0 : p.height) || 0))
1303
- };
1304
- }
1305
- _paramsForFilterState(e, t, i) {
1306
- const s = t != null && t.values && typeof t.values == "object" ? t.values : {}, r = e.defaultParams ? { ...e.defaultParams } : {};
1307
- e.controls && Array.isArray(e.controls) && e.controls.forEach((n) => {
1308
- const a = n.property || n.id;
1309
- r[a] = s[n.id] ?? n.default;
1310
- });
1311
- const o = new Set((e.controls || []).map((n) => n.id));
1312
- for (const [n, a] of Object.entries(s))
1313
- o.has(n) || (r[n] = a);
1314
- return r._sourceWidth = i.width, r._sourceHeight = i.height, r;
1315
- }
1316
- createFiltersForLayer(e, t = {}) {
1317
- if (!Array.isArray(e == null ? void 0 : e.filters) || e.filters.length === 0) return [];
1318
- const i = [], s = this.state.get("activeLayerId"), r = e.id === s && t.forExport !== !0, o = this._sourceDimensionsForLayer(e, t);
1319
- for (const n of e.filters)
1320
- if (!(!n || n.enabled === !1 || typeof n.id != "string"))
1321
- try {
1322
- const a = this.getFilterDef(n.id);
1323
- if (!(a != null && a.createFilter) || typeof a.createFilter != "function") continue;
1324
- const l = a.createFilter(this._paramsForFilterState(a, n, o));
1325
- if (!l) continue;
1326
- let h = l;
1327
- t.forExport === !0 && typeof l.createExportFilter == "function" && (h = l.createExportFilter({
1328
- previewToNativeScale: t.previewToNativeScale || 1
1329
- }) || l), i.push(h), r && (this.instances[n.id] = l);
1330
- } catch {
1331
- }
1332
- return i;
1333
- }
1334
- /**
1335
- * Apply all active filters to the sprite
1336
- *
1337
- * This method recreates all filter instances on each call to ensure
1338
- * PIXI v8's filter pipeline correctly processes changes.
1339
- */
1340
- applyFilters() {
1341
- var o, n, a, l, h, c, p;
1342
- const e = this._layerDocument();
1343
- if (e) {
1344
- for (const f in this.instances)
1345
- delete this.instances[f];
1346
- (n = (o = this.renderer) == null ? void 0 : o.setLayerFilterFactory) == null || n.call(o, (f, m) => this.createFiltersForLayer(f, m));
1347
- const u = (l = (a = this.renderer) == null ? void 0 : a.applyLayerFilters) == null ? void 0 : l.call(a, e);
1348
- this.emit("filtersApplied", {
1349
- count: ((h = this.state.get("activeFilters")) == null ? void 0 : h.size) ?? 0,
1350
- failed: [],
1351
- scopedToLayer: !0
1352
- }), u || (p = (c = this.renderer) == null ? void 0 : c.render) == null || p.call(c);
1353
- return;
1354
- }
1355
- const t = this.renderer.sprite;
1356
- if (!t) return;
1357
- for (const u in this.instances)
1358
- delete this.instances[u];
1359
- const i = [], s = [];
1360
- this.state.get("activeFilters").forEach((u) => {
1361
- try {
1362
- const f = this.getFilterDef(u);
1363
- if (!f || !f.createFilter || typeof f.createFilter != "function")
1364
- return;
1365
- const m = this.state.getFilterValues(u), b = f.defaultParams ? { ...f.defaultParams } : {};
1366
- f.controls && Array.isArray(f.controls) && f.controls.forEach((S) => {
1367
- const J = S.property || S.id;
1368
- b[J] = m[S.id] ?? S.default;
1369
- });
1370
- const k = new Set((f.controls || []).map((S) => S.id));
1371
- for (const [S, J] of Object.entries(m))
1372
- k.has(S) || (b[S] = J);
1373
- const x = this.renderer.sprite, B = this.renderer.originalTexture;
1374
- b._sourceWidth = Math.round((B == null ? void 0 : B.width) || (x == null ? void 0 : x.width) || 0), b._sourceHeight = Math.round((B == null ? void 0 : B.height) || (x == null ? void 0 : x.height) || 0);
1375
- const A = f.createFilter(b);
1376
- A && (i.push(A), this.instances[u] = A);
1377
- } catch {
1378
- s.push(u);
1379
- }
1380
- });
1381
- try {
1382
- t.filters = null, t.filters = i.length ? i : null, this.renderer.render(), this.emit("filtersApplied", { count: i.length, failed: s });
1383
- } catch (u) {
1384
- try {
1385
- t.filters = null, this.renderer.render();
1386
- } catch {
1387
- }
1388
- this.emit("filtersError", { error: u, failedFilters: s });
1389
- }
1390
- s.length > 0;
1391
- }
1392
- /**
1393
- * Reset all filters.
1394
- * IMPORTANT: Only clears filter instances and sprite.filters.
1395
- * Does NOT touch baseTexture/originalTexture/sprite — that would
1396
- * destroy any applied crop, which is a separate concern.
1397
- */
1398
- resetAll(e = void 0) {
1399
- for (const s in this.instances)
1400
- delete this.instances[s];
1401
- return this.state.resetFilters(e) === !1 ? !1 : this._layerDocument() ? (this.applyFilters(), this.emit("filtersReset"), !0) : (this.renderer.sprite && (this.renderer.sprite.filters = null), this.renderer.render(), this.emit("filtersReset"), !0);
1402
- }
1403
- /**
1404
- * Get a filter instance
1405
- * @param {string} filterId
1406
- * @returns {Object|null}
1407
- */
1408
- getInstance(e) {
1409
- return this.instances[e] || null;
1410
- }
1411
- }
1412
- const V = class V extends ne {
1413
- constructor(e, t) {
1414
- super(), this.state = e, this.renderer = t, this._overlayCanvas = null, this._isDragging = !1, this._dragStart = null, this._dragMode = null, this._startRect = null, this._hoverMode = null, this._lastAutoZoomCheck = 0, this.HANDLE_SIZE = 14, this.EDGE_HIT_PAD = 10, this._onPointerDown = this._handlePointerDown.bind(this), this._onPointerMove = this._handlePointerMove.bind(this), this._onPointerUp = this._handlePointerUp.bind(this);
1415
- }
1416
- /**
1417
- * Set the overlay canvas element
1418
- * @param {HTMLCanvasElement} canvas
1419
- */
1420
- setOverlayCanvas(e) {
1421
- this._overlayCanvas = e;
1422
- }
1423
- /**
1424
- * Get aspect ratio value from string
1425
- * @param {string} aspect
1426
- * @returns {number|null}
1427
- */
1428
- _getAspectRatio(e) {
1429
- if (!e || e === "free") return null;
1430
- const t = {
1431
- "1:1": 1,
1432
- "4:3": 4 / 3,
1433
- "16:9": 16 / 9,
1434
- "3:2": 3 / 2,
1435
- "2:3": 2 / 3
1436
- };
1437
- if (t[e] !== void 0) return t[e];
1438
- const i = e.split(":");
1439
- if (i.length === 2) {
1440
- const s = parseFloat(i[0]), r = parseFloat(i[1]);
1441
- if (Number.isFinite(s) && Number.isFinite(r) && r > 0)
1442
- return s / r;
1443
- }
1444
- return null;
1445
- }
1446
- /**
1447
- * Return the numeric aspect ratio that the crop rect must obey, accounting
1448
- * for both the shape (circle/square force 1:1) and the explicit aspect
1449
- * setting. Returns null when the crop is free-form.
1450
- */
1451
- _getEffectiveTargetAspect() {
1452
- const e = this.state.get("crop.shape");
1453
- if (e === "circle" || e === "square") return 1;
1454
- const t = this.state.get("crop.aspect");
1455
- return !t || t === "free" ? null : this._getAspectRatio(t);
1456
- }
1457
- /**
1458
- * Apply aspect ratio constraint to crop rect. Honours shape (circle/square
1459
- * force 1:1) so the post-drag correction agrees with the effective ratio
1460
- * the edge-handle drag code targets.
1461
- */
1462
- applyAspectRatio() {
1463
- const e = this.state.get("crop.rect");
1464
- if (!e) return;
1465
- const t = this._getEffectiveTargetAspect();
1466
- if (!t) return;
1467
- const i = e.width / e.height;
1468
- Math.abs(i - t) < 0.01 || (i > t ? e.width = e.height * t : e.height = e.width / t, this.state.set("crop.rect", e));
1469
- }
1470
- /**
1471
- * Constrain crop rect to bounds
1472
- */
1473
- constrainCropRect() {
1474
- const e = this.state.get("crop.rect");
1475
- if (!e) return;
1476
- const t = this.renderer.sprite, i = this.renderer.app;
1477
- if (!t || !i) return;
1478
- const s = this.state.get("crop.shape"), r = { x: 0, y: 0, w: i.screen.width, h: i.screen.height };
1479
- if (e.x = Math.max(r.x, Math.min(e.x, r.x + r.w - e.width)), e.y = Math.max(r.y, Math.min(e.y, r.y + r.h - e.height)), e.width = Math.min(e.width, r.w), e.height = Math.min(e.height, r.h), s !== "free" || this.state.get("crop.aspect") === "1:1") {
1480
- const o = Math.min(e.width, e.height);
1481
- e.width = o, e.height = o;
1482
- }
1483
- this.state.set("crop.rect", e);
1484
- }
1485
- /**
1486
- * Calculate the target zoom level for auto-zoom when crop exceeds image bounds.
1487
- * Pure function — returns the new zoom level, or null if no adjustment is needed.
1488
- *
1489
- * @param {{ width: number, height: number }} cropRect - Current crop dimensions
1490
- * @param {number} spriteWidth - Current sprite width on screen
1491
- * @param {number} spriteHeight - Current sprite height on screen
1492
- * @param {number} texWidth - Original texture width (pixels)
1493
- * @param {number} texHeight - Original texture height (pixels)
1494
- * @param {number} fitScale - Current fit-to-screen scale
1495
- * @param {number} currentZoom - Current zoom level
1496
- * @returns {number|null} Target zoom level, or null if no zoom needed
1497
- */
1498
- static calcAutoZoom(e, t, i, s, r, o, n) {
1499
- if (!e || !s || !r || !o) return null;
1500
- const a = e.width > t, l = e.height > i;
1501
- if (!a && !l) return null;
1502
- const h = V.AUTO_ZOOM_PADDING;
1503
- let c = n;
1504
- if (a) {
1505
- const p = e.width / (h * s * o);
1506
- c = Math.min(c, p);
1507
- }
1508
- if (l) {
1509
- const p = e.height / (h * r * o);
1510
- c = Math.min(c, p);
1511
- }
1512
- return c = Math.max(0.1, c), c >= n - 0.01 ? null : c;
1513
- }
1514
- /**
1515
- * Check if auto-zoom is needed and apply it (throttled).
1516
- * Called during crop resize drags when autoZoomOnCropOverflow is enabled.
1517
- */
1518
- _checkAutoZoom() {
1519
- if (!this.state.get("autoZoomOnCropOverflow")) return;
1520
- const e = Date.now();
1521
- if (e - this._lastAutoZoomCheck < V.AUTO_ZOOM_THROTTLE_MS) return;
1522
- this._lastAutoZoomCheck = e;
1523
- const t = this.state.get("crop.rect"), i = this.renderer.sprite, s = this.renderer.originalTexture;
1524
- if (!t || !i || !s) return;
1525
- const r = V.calcAutoZoom(
1526
- t,
1527
- i.width,
1528
- i.height,
1529
- s.width,
1530
- s.height,
1531
- this.renderer.fitScale,
1532
- this.renderer.zoom
1533
- );
1534
- r !== null && this.renderer.setZoom(r, { keepCenter: !0 });
1535
- }
1536
- /**
1537
- * Draw crop overlay on canvas
1538
- */
1539
- drawOverlay() {
1540
- const e = this._overlayCanvas;
1541
- if (!e) return;
1542
- const t = this.renderer.app;
1543
- if (!t) return;
1544
- const i = t.canvas, s = i.clientWidth, r = i.clientHeight, o = window.devicePixelRatio || 1;
1545
- (e.width !== Math.max(1, Math.floor(s * o)) || e.height !== Math.max(1, Math.floor(r * o))) && (e.width = Math.max(1, Math.floor(s * o)), e.height = Math.max(1, Math.floor(r * o)), e.style.width = s + "px", e.style.height = r + "px");
1546
- const n = e.getContext("2d");
1547
- if (!n) return;
1548
- n.setTransform(o, 0, 0, o, 0, 0), n.clearRect(0, 0, s, r), n.fillStyle = "rgba(0, 0, 0, 0.5)", n.fillRect(0, 0, s, r);
1549
- const a = this.state.get("crop.rect");
1550
- if (!a) return;
1551
- const l = this.state.get("crop.shape");
1552
- if (n.save(), l === "circle") {
1553
- const b = a.x + a.width / 2, k = a.y + a.height / 2, x = Math.min(a.width, a.height) / 2;
1554
- n.beginPath(), n.arc(b, k, x, 0, Math.PI * 2), n.clip();
1555
- } else
1556
- n.beginPath(), n.rect(a.x, a.y, a.width, a.height), n.clip();
1557
- n.clearRect(a.x, a.y, a.width, a.height), n.restore(), n.strokeStyle = "#ffffff", n.lineWidth = 2, n.setLineDash([5, 5]), l === "circle" ? (n.beginPath(), n.arc(
1558
- a.x + a.width / 2,
1559
- a.y + a.height / 2,
1560
- Math.min(a.width, a.height) / 2,
1561
- 0,
1562
- Math.PI * 2
1563
- ), n.stroke()) : n.strokeRect(a.x, a.y, a.width, a.height), n.setLineDash([]), n.strokeStyle = "rgba(255,255,255,0.3)", n.lineWidth = 1;
1564
- const h = a.width / 3, c = a.height / 3;
1565
- for (let b = 1; b <= 2; b++)
1566
- n.beginPath(), n.moveTo(a.x + h * b, a.y), n.lineTo(a.x + h * b, a.y + a.height), n.stroke(), n.beginPath(), n.moveTo(a.x, a.y + c * b), n.lineTo(a.x + a.width, a.y + c * b), n.stroke();
1567
- const p = this.HANDLE_SIZE, u = [
1568
- { x: a.x, y: a.y, m: "resize-nw" },
1569
- { x: a.x + a.width, y: a.y, m: "resize-ne" },
1570
- { x: a.x, y: a.y + a.height, m: "resize-sw" },
1571
- { x: a.x + a.width, y: a.y + a.height, m: "resize-se" }
1572
- ], f = [
1573
- { x: a.x + a.width / 2, y: a.y, m: "n" },
1574
- { x: a.x + a.width / 2, y: a.y + a.height, m: "s" },
1575
- { x: a.x, y: a.y + a.height / 2, m: "w" },
1576
- { x: a.x + a.width, y: a.y + a.height / 2, m: "e" }
1577
- ], m = [...u, ...f];
1578
- for (const b of m) {
1579
- const k = this._hoverMode === b.m, x = k ? p + 4 : p;
1580
- n.beginPath(), n.rect(b.x - x / 2, b.y - x / 2, x, x), n.fillStyle = k ? "#4da3ff" : "#ffffff", n.strokeStyle = "rgba(0,0,0,0.6)", n.lineWidth = 1, n.fill(), n.stroke();
1581
- }
1582
- }
1583
- /**
1584
- * Hit test for handles
1585
- * @param {number} px
1586
- * @param {number} py
1587
- * @returns {string|null}
1588
- */
1589
- _hitHandle(e, t) {
1590
- const i = this.state.get("crop.rect");
1591
- if (!i) return null;
1592
- const s = (r, o, n, a, l) => Math.abs(r - n) <= l && Math.abs(o - a) <= l;
1593
- return s(e, t, i.x, i.y, this.HANDLE_SIZE) ? "resize-nw" : s(e, t, i.x + i.width, i.y, this.HANDLE_SIZE) ? "resize-ne" : s(e, t, i.x, i.y + i.height, this.HANDLE_SIZE) ? "resize-sw" : s(e, t, i.x + i.width, i.y + i.height, this.HANDLE_SIZE) ? "resize-se" : Math.abs(t - i.y) <= this.EDGE_HIT_PAD && e >= i.x && e <= i.x + i.width ? "n" : Math.abs(t - (i.y + i.height)) <= this.EDGE_HIT_PAD && e >= i.x && e <= i.x + i.width ? "s" : Math.abs(e - i.x) <= this.EDGE_HIT_PAD && t >= i.y && t <= i.y + i.height ? "w" : Math.abs(e - (i.x + i.width)) <= this.EDGE_HIT_PAD && t >= i.y && t <= i.y + i.height ? "e" : e >= i.x && e <= i.x + i.width && t >= i.y && t <= i.y + i.height ? "move" : null;
1594
- }
1595
- /**
1596
- * Handle pointer down event
1597
- */
1598
- _handlePointerDown(e) {
1599
- const t = this.state.get("crop.rect");
1600
- if (!t) return;
1601
- const i = e.global;
1602
- this._dragMode = this._hitHandle(i.x, i.y), this._dragMode && (this._isDragging = !0, this._dragStart = { x: i.x, y: i.y }, this._startRect = { ...t }, this._lastAutoZoomCheck = 0);
1603
- }
1604
- /**
1605
- * Handle pointer move event
1606
- */
1607
- _handlePointerMove(e) {
1608
- var l, h, c, p;
1609
- const t = this.renderer.app;
1610
- if (!t) return;
1611
- const i = e.global;
1612
- if (!this._isDragging || !this._dragStart || !this._startRect) {
1613
- this._hoverMode = this._hitHandle(i.x, i.y), t.stage.cursor = this._hoverMode === "move" ? "move" : this._hoverMode === "n" || this._hoverMode === "s" ? "ns-resize" : this._hoverMode === "e" || this._hoverMode === "w" ? "ew-resize" : (l = this._hoverMode) != null && l.endsWith("nw") || (h = this._hoverMode) != null && h.endsWith("se") ? "nwse-resize" : (c = this._hoverMode) != null && c.endsWith("ne") || (p = this._hoverMode) != null && p.endsWith("sw") ? "nesw-resize" : "crosshair", this.drawOverlay();
1614
- return;
1615
- }
1616
- const s = this.state.get("crop.rect");
1617
- if (!s) return;
1618
- const r = i.x - this._dragStart.x, o = i.y - this._dragStart.y, n = this._getEffectiveTargetAspect();
1619
- switch (this._dragMode) {
1620
- case "move":
1621
- s.x = this._startRect.x + r, s.y = this._startRect.y + o;
1622
- break;
1623
- case "n": {
1624
- const u = this._startRect.height - o;
1625
- if (s.y = this._startRect.y + o, s.height = u, n) {
1626
- const f = u * n;
1627
- s.x = this._startRect.x + (this._startRect.width - f) / 2, s.width = f;
1628
- }
1629
- break;
1630
- }
1631
- case "s": {
1632
- const u = this._startRect.height + o;
1633
- if (s.height = u, n) {
1634
- const f = u * n;
1635
- s.x = this._startRect.x + (this._startRect.width - f) / 2, s.width = f;
1636
- }
1637
- break;
1638
- }
1639
- case "w": {
1640
- const u = this._startRect.width - r;
1641
- if (s.x = this._startRect.x + r, s.width = u, n) {
1642
- const f = u / n;
1643
- s.y = this._startRect.y + (this._startRect.height - f) / 2, s.height = f;
1644
- }
1645
- break;
1646
- }
1647
- case "e": {
1648
- const u = this._startRect.width + r;
1649
- if (s.width = u, n) {
1650
- const f = u / n;
1651
- s.y = this._startRect.y + (this._startRect.height - f) / 2, s.height = f;
1652
- }
1653
- break;
1654
- }
1655
- case "resize-nw":
1656
- s.x = this._startRect.x + r, s.y = this._startRect.y + o, s.width = this._startRect.width - r, s.height = this._startRect.height - o;
1657
- break;
1658
- case "resize-ne":
1659
- s.y = this._startRect.y + o, s.width = this._startRect.width + r, s.height = this._startRect.height - o;
1660
- break;
1661
- case "resize-sw":
1662
- s.x = this._startRect.x + r, s.width = this._startRect.width - r, s.height = this._startRect.height + o;
1663
- break;
1664
- case "resize-se":
1665
- s.width = this._startRect.width + r, s.height = this._startRect.height + o;
1666
- break;
1667
- }
1668
- s.width = Math.max(50, s.width), s.height = Math.max(50, s.height), this.state.set("crop.rect", s), this.state.set("crop.dirty", !0), this.state.get("crop.shape") === "circle" && this.state.get("crop.aspect") !== "1:1" && this.state.set("crop.aspect", "1:1"), this.applyAspectRatio(), this._dragMode !== "move" && this._checkAutoZoom(), this.constrainCropRect(), this.drawOverlay();
1669
- }
1670
- /**
1671
- * Handle pointer up event
1672
- */
1673
- _handlePointerUp() {
1674
- this._isDragging = !1, this._dragMode = null, this._dragStart = null, this._startRect = null;
1675
- }
1676
- /**
1677
- * Enable crop mode
1678
- */
1679
- enable() {
1680
- const e = this.renderer.app, t = this.renderer.sprite;
1681
- if (!e || !t) return;
1682
- let i = this.state.get("crop.rect");
1683
- if (!i) {
1684
- this.state.get("crop.shape") === "circle" && this.state.set("crop.aspect", "1:1");
1685
- const o = this.state.get("crop.aspect"), n = this._getAspectRatio(o);
1686
- let a, l;
1687
- if (n) {
1688
- const p = t.width * 0.9, u = t.height * 0.9;
1689
- p / u > n ? (l = u, a = l * n) : (a = p, l = a / n);
1690
- } else {
1691
- const p = Math.min(e.screen.width, e.screen.height) * 0.7;
1692
- a = p, l = p;
1693
- }
1694
- const h = t.x + (t.width - a) / 2, c = t.y + (t.height - l) / 2;
1695
- i = { x: h, y: c, width: a, height: l }, this.state.set("crop.rect", i), o !== "free" && this.constrainCropRect();
1696
- }
1697
- this.state.set("crop.dirty", !1);
1698
- const s = e.stage;
1699
- s.eventMode = "static", s.hitArea = e.screen, s.cursor = "crosshair", s.on("pointerdown", this._onPointerDown), s.on("pointermove", this._onPointerMove), s.on("pointerup", this._onPointerUp), s.on("pointerupoutside", this._onPointerUp), this.state.set("mode", "crop"), this.drawOverlay(), this.emit("enabled");
1700
- }
1701
- /**
1702
- * Disable crop mode
1703
- */
1704
- disable() {
1705
- const e = this.renderer.app;
1706
- if (!e) return;
1707
- const t = e.stage;
1708
- if (t.off("pointerdown", this._onPointerDown), t.off("pointermove", this._onPointerMove), t.off("pointerup", this._onPointerUp), t.off("pointerupoutside", this._onPointerUp), t.eventMode = "auto", t.cursor = "default", this.state.set("crop.rect", null), this.state.set("crop.dirty", !1), this._overlayCanvas) {
1709
- const i = this._overlayCanvas.getContext("2d");
1710
- i == null || i.clearRect(0, 0, this._overlayCanvas.width, this._overlayCanvas.height);
1711
- }
1712
- this.state.set("mode", "filters"), this.emit("disabled");
1713
- }
1714
- /**
1715
- * Apply the crop
1716
- * @returns {{ texture: PIXI.Texture, preservedZoom: number }|null}
1717
- */
1718
- apply() {
1719
- const e = this.renderer.app, t = this.renderer.sprite, i = this.renderer.originalTexture, s = this.state.get("crop.rect");
1720
- if (!s || !t || !e || !i) return null;
1721
- const r = window.PIXI, o = this.renderer.zoom, n = i.width / t.width, a = i.height / t.height, l = (s.x - t.x) * n, h = (s.y - t.y) * a;
1722
- let c = Math.round(Math.max(1, s.width * n)), p = Math.round(Math.max(1, s.height * a)), u = Math.round(l), f = Math.round(h);
1723
- if (c <= 0 || p <= 0) return null;
1724
- const m = new r.Container(), b = new r.Sprite(i);
1725
- if (this.state.get("crop.shape") === "circle") {
1726
- const M = Math.round(Math.max(c, p)), N = u + c / 2, z = f + p / 2;
1727
- u = Math.round(N - M / 2), f = Math.round(z - M / 2), c = p = M;
1728
- const F = new r.Graphics();
1729
- typeof F.circle == "function" && typeof F.fill == "function" ? F.circle(c / 2, p / 2, c / 2).fill(16777215) : (F.beginFill(16777215, 1), F.drawCircle(c / 2, p / 2, c / 2), F.endFill()), b.mask = F, m.addChild(F);
1730
- }
1731
- b.x = -u, b.y = -f, m.addChild(b);
1732
- const x = r.RenderTexture.create({ width: c, height: p });
1733
- e.renderer.render({
1734
- container: m,
1735
- target: x,
1736
- clear: !0
1737
- }), m.destroy({ children: !0 });
1738
- const B = this.renderer.originalTexture;
1739
- B && B !== this.renderer.baseTexture && B.destroy(!0), this.renderer.originalTexture = x, e.stage.removeChild(t), t.destroy();
1740
- const A = new r.Sprite(x);
1741
- e.stage.addChild(A), this.renderer.sprite = A, this.renderer.fitScale = this.renderer.getFitScaleFor(x), this.renderer.setZoom(o, { keepCenter: !1 }), this.renderer.applyViewTransform(), this.renderer.render();
1742
- const S = this.state.get("crop.appliedRect"), y = S && Number.isFinite(S.x) && Number.isFinite(S.y) && Number.isFinite(S.width) && Number.isFinite(S.height) ? {
1743
- x: Math.round(S.x + u),
1744
- y: Math.round(S.y + f),
1745
- width: c,
1746
- height: p
1747
- } : { x: u, y: f, width: c, height: p }, w = this.state.get("crop.shape") || "free", R = this.state.get("crop.aspect") || "free";
1748
- return this.disable(), this.state.set("crop.appliedRect", y), this.state.set("crop.appliedShape", w), this.state.set("crop.appliedAspect", R), this.state.set("crop.dirty", !1), this.emit("applied", { width: c, height: p }), { texture: x, preservedZoom: o };
1749
- }
1750
- /**
1751
- * Apply a crop from saved texture-pixel coordinates (for state rehydration).
1752
- * Unlike apply(), this does not read from crop.rect or require the interactive overlay.
1753
- * @param {{ x: number, y: number, width: number, height: number }} pixelRect
1754
- * @param {'free'|'circle'} shape
1755
- * @returns {{ texture: Object, preservedZoom: number }|null}
1756
- */
1757
- applyFromPixelRect(e, t = "free") {
1758
- const i = this.renderer.app, s = this.renderer.sprite, r = this.renderer.originalTexture;
1759
- if (!e || !s || !i || !r) return null;
1760
- const o = window.PIXI, n = this.renderer.zoom;
1761
- let a = Math.round(Math.max(1, e.width)), l = Math.round(Math.max(1, e.height)), h = Math.round(e.x), c = Math.round(e.y);
1762
- if (a <= 0 || l <= 0) return null;
1763
- const p = new o.Container(), u = new o.Sprite(r);
1764
- if (t === "circle") {
1765
- const k = Math.round(Math.max(a, l)), x = h + a / 2, B = c + l / 2;
1766
- h = Math.round(x - k / 2), c = Math.round(B - k / 2), a = l = k;
1767
- const A = new o.Graphics();
1768
- typeof A.circle == "function" && typeof A.fill == "function" ? A.circle(a / 2, l / 2, a / 2).fill(16777215) : (A.beginFill(16777215, 1), A.drawCircle(a / 2, l / 2, a / 2), A.endFill()), u.mask = A, p.addChild(A);
1769
- }
1770
- u.x = -h, u.y = -c, p.addChild(u);
1771
- const f = o.RenderTexture.create({ width: a, height: l });
1772
- i.renderer.render({ container: p, target: f, clear: !0 }), p.destroy({ children: !0 });
1773
- const m = this.renderer.originalTexture;
1774
- m && m !== this.renderer.baseTexture && m.destroy(!0), this.renderer.originalTexture = f, i.stage.removeChild(s), s.destroy();
1775
- const b = new o.Sprite(f);
1776
- return i.stage.addChild(b), this.renderer.sprite = b, this.renderer.fitScale = this.renderer.getFitScaleFor(f), this.renderer.setZoom(n, { keepCenter: !1 }), this.renderer.applyViewTransform(), this.renderer.render(), { texture: f, preservedZoom: n };
1777
- }
1778
- /**
1779
- * Cancel crop
1780
- */
1781
- cancel() {
1782
- this.disable(), this.emit("cancelled");
1783
- }
1784
- /**
1785
- * Set crop shape
1786
- * @param {'free'|'square'|'circle'} shape
1787
- */
1788
- setShape(e) {
1789
- this.state.get("lockCropShape") || (this.state.set("crop.shape", e), this.state.set("crop.dirty", !0), (e === "circle" || e === "square") && this.state.set("crop.aspect", "1:1"), this.applyAspectRatio(), this.constrainCropRect(), this.drawOverlay());
1790
- }
1791
- /**
1792
- * Set aspect ratio
1793
- * @param {'free'|'1:1'|'4:3'|'16:9'|'3:2'|'2:3'|string} aspect
1794
- */
1795
- setAspect(e) {
1796
- if (this.state.get("lockAspectRatio")) return;
1797
- if (this.state.get("crop.shape") !== "free") {
1798
- if (this.state.get("lockCropShape")) return;
1799
- this.state.set("crop.shape", "free");
1800
- }
1801
- this.state.set("crop.aspect", e), this.state.set("crop.dirty", !0), this.applyAspectRatio(), this.constrainCropRect(), this.drawOverlay();
1802
- }
1803
- };
1804
- /**
1805
- * Padding factor for auto-zoom: image will be ~91% of crop size (1/1.1).
1806
- * Higher values = more aggressive zoom-out, more padding around image.
1807
- */
1808
- ae(V, "AUTO_ZOOM_PADDING", 1.1), /**
1809
- * Minimum interval (ms) between auto-zoom adjustments during drag.
1810
- */
1811
- ae(V, "AUTO_ZOOM_THROTTLE_MS", 100);
1812
- let de = V;
1813
- class Xe {
1814
- /**
1815
- * Create a new RemoveBgManager
1816
- * @param {Object} options
1817
- * @param {string} [options.endpoint] - API endpoint for uploaded preview background removal
1818
- * @param {string} [options.optionsEndpoint] - API endpoint for background-removal capability/options
1819
- * @param {string} [options.savedEndpoint] - API endpoint template for saved-media removal
1820
- * @param {string} [options.fallbackEndpoint] - Fallback endpoint if primary fails
1821
- */
1822
- constructor(e = {}) {
1823
- this._endpoint = e.endpoint || "/api/v1/media/background-removal/preview", this._optionsEndpoint = e.optionsEndpoint || "/api/v1/media/background-removal/options", this._savedEndpoint = e.savedEndpoint || "/api/v1/media/{media}/background-removal", this._fallbackEndpoint = e.fallbackEndpoint || null, this._activeControllers = /* @__PURE__ */ new Set();
1824
- }
1825
- _createAbortController() {
1826
- if (typeof AbortController > "u")
1827
- return null;
1828
- const e = new AbortController();
1829
- return this._activeControllers.add(e), e;
1830
- }
1831
- _releaseAbortController(e) {
1832
- e && this._activeControllers.delete(e);
1833
- }
1834
- cancelActiveRequests(e = "cancelled") {
1835
- const t = Array.from(this._activeControllers);
1836
- this._activeControllers.clear();
1837
- for (const i of t)
1838
- i.signal.aborted || i.abort(e);
1839
- return t.length;
1840
- }
1841
- /**
1842
- * Build a stable preview request so callers can stale-guard the response.
1843
- * @param {string|Blob} imageData
1844
- * @param {Object} options
1845
- * @returns {Promise<Object>}
1846
- */
1847
- async preparePreviewRequest(e, t = {}) {
1848
- const i = typeof e == "string" ? await this._dataUrlToBlob(e) : e;
1849
- return {
1850
- ...t,
1851
- blob: i,
1852
- operationId: t.operationId || this._uuid(),
1853
- sessionKey: t.sessionKey || this._uuid(),
1854
- sourceHash: t.sourceHash || await this._sha256Hex(i),
1855
- targetRef: t.targetRef || null,
1856
- tier: t.tier || "balanced"
1857
- };
1858
- }
1859
- /**
1860
- * @param {string|Blob} imageData
1861
- * @returns {Promise<string>}
1862
- */
1863
- async computeImageDataHash(e) {
1864
- const t = typeof e == "string" ? await this._dataUrlToBlob(e) : e;
1865
- return this._sha256Hex(t);
1866
- }
1867
- /**
1868
- * Remove background from an image
1869
- * @param {string} imageData - Data URL or Blob of the image
1870
- * @param {Object} options
1871
- * @param {string} [options.tier='balanced'] - Quality tier: 'fast', 'balanced', 'best'
1872
- * @param {string} [options.model] - Explicit model name (overrides tier)
1873
- * @param {boolean} [options.alpha_matting=false] - Enable alpha matting for edge refinement
1874
- * @param {number} [options.alpha_f=10] - Alpha matting foreground threshold
1875
- * @param {number} [options.alpha_fr=15] - Alpha matting foreground radius
1876
- * @param {number} [options.alpha_erode_size=10] - Alpha matting erode size
1877
- * @returns {Promise<{dataUrl: string, model: string, processMs: string}>}
1878
- */
1879
- async removeBackground(e, t = {}) {
1880
- const i = typeof Blob < "u" && t.blob instanceof Blob ? t : await this.preparePreviewRequest(e, t), s = i.blob, r = i.operationId, o = i.sessionKey, n = i.sourceHash, a = new FormData();
1881
- a.append("image_file", s, "image.png"), a.append("operation_id", r), a.append("session_key", o), a.append("source_hash", n), a.append("tier", i.tier || "balanced"), i.targetRef && a.append("target_ref", i.targetRef), i.model && a.append("model", i.model), i.alpha_matting && (a.append("alpha_matting", "true"), a.append("alpha_f", String(i.alpha_f ?? 10)), a.append("alpha_fr", String(i.alpha_fr ?? 15)), a.append("alpha_erode_size", String(i.alpha_erode_size ?? 10))), i.max_megapixels !== void 0 && a.append("max_megapixels", String(i.max_megapixels));
1882
- let l, h;
1883
- const c = this._createAbortController(), p = (u) => c ? { ...u, signal: c.signal } : u;
1884
- try {
1885
- try {
1886
- l = await fetch(this._endpoint, p({
1887
- method: "POST",
1888
- body: a,
1889
- credentials: "include"
1890
- // For Sanctum auth
1891
- }));
1892
- } catch (m) {
1893
- if (c != null && c.signal.aborted)
1894
- throw m;
1895
- h = m;
1896
- }
1897
- if ((!l || !l.ok) && this._fallbackEndpoint)
1898
- try {
1899
- l = await fetch(this._fallbackEndpoint, p({
1900
- method: "POST",
1901
- body: a
1902
- }));
1903
- } catch (m) {
1904
- if (c != null && c.signal.aborted)
1905
- throw m;
1906
- h || (h = m);
1907
- }
1908
- if (!l)
1909
- throw h || new Error("Network error: Unable to connect to background removal service");
1910
- if (!l.ok) {
1911
- let m = `Background removal failed (HTTP ${l.status})`;
1912
- try {
1913
- const b = await l.text();
1914
- b && (m += `: ${b}`);
1915
- } catch {
1916
- }
1917
- throw new Error(m);
1918
- }
1919
- const u = await l.blob();
1920
- return {
1921
- dataUrl: await this._blobToDataUrl(u),
1922
- model: l.headers.get("X-Model-Used") || "unknown",
1923
- processMs: l.headers.get("X-Process-Ms") || "0",
1924
- operationId: l.headers.get("X-Operation-Id") || r,
1925
- sessionKey: l.headers.get("X-Session-Key") || o,
1926
- sourceHash: l.headers.get("X-Source-Hash") || n,
1927
- targetRef: l.headers.get("X-Target-Ref") || i.targetRef || null
1928
- };
1929
- } finally {
1930
- this._releaseAbortController(c);
1931
- }
1932
- }
1933
- /**
1934
- * Remove the background from a persisted editor-document layer.
1935
- * @param {Object} options
1936
- * @param {string} options.targetMediaUuid
1937
- * @param {string} options.operationId
1938
- * @param {string} options.documentId
1939
- * @param {string} options.documentRevisionId
1940
- * @param {string} options.targetLayerId
1941
- * @param {string} options.expectedSourceHash
1942
- * @param {string} [options.tier]
1943
- * @returns {Promise<Object>}
1944
- */
1945
- async removeSavedMediaBackground(e) {
1946
- const t = this._savedEndpoint.replace("{media}", encodeURIComponent(e.targetMediaUuid)), i = {
1947
- operation_id: e.operationId,
1948
- document_id: e.documentId,
1949
- document_revision_id: e.documentRevisionId,
1950
- target_layer_id: e.targetLayerId,
1951
- expected_source_hash: e.expectedSourceHash,
1952
- tier: e.tier || "balanced"
1953
- };
1954
- e.model && (i.model = e.model), e.alpha_matting !== void 0 && (i.alpha_matting = !!e.alpha_matting), e.max_megapixels !== void 0 && (i.max_megapixels = Number(e.max_megapixels));
1955
- const s = this._createAbortController();
1956
- let r;
1957
- try {
1958
- if (r = await fetch(t, {
1959
- method: "POST",
1960
- body: JSON.stringify(i),
1961
- credentials: "include",
1962
- headers: {
1963
- Accept: "application/json",
1964
- "Content-Type": "application/json"
1965
- },
1966
- ...s ? { signal: s.signal } : {}
1967
- }), !r.ok) {
1968
- let o = `Background removal failed (HTTP ${r.status})`;
1969
- try {
1970
- const n = await r.json();
1971
- n != null && n.message && (o += `: ${n.message}`);
1972
- } catch {
1973
- }
1974
- throw new Error(o);
1975
- }
1976
- return await r.json();
1977
- } finally {
1978
- this._releaseAbortController(s);
1979
- }
1980
- }
1981
- /**
1982
- * Convert a data URL to a Blob
1983
- * @param {string} dataUrl
1984
- * @returns {Promise<Blob>}
1985
- */
1986
- async _dataUrlToBlob(e) {
1987
- if (e.startsWith("data:")) {
1988
- const [i, s] = e.split(","), r = i.match(/:(.*?);/), o = r ? r[1] : "image/png", n = atob(s), a = new Uint8Array(n.length);
1989
- for (let l = 0; l < n.length; l++)
1990
- a[l] = n.charCodeAt(l);
1991
- return new Blob([a], { type: o });
1992
- }
1993
- return (await fetch(e)).blob();
1994
- }
1995
- /**
1996
- * Convert a Blob to a data URL
1997
- * @param {Blob} blob
1998
- * @returns {Promise<string>}
1999
- */
2000
- _blobToDataUrl(e) {
2001
- return new Promise((t, i) => {
2002
- const s = new FileReader();
2003
- s.onload = () => t(s.result), s.onerror = () => i(new Error("Failed to read blob as data URL")), s.readAsDataURL(e);
2004
- });
2005
- }
2006
- /**
2007
- * Check if the background removal service is available
2008
- * @returns {Promise<boolean>}
2009
- */
2010
- async isAvailable() {
2011
- var t;
2012
- const e = await this.getOptions();
2013
- return e.enabled === !0 && ((t = e.service) == null ? void 0 : t.available) === !0;
2014
- }
2015
- /**
2016
- * @returns {Promise<Object>}
2017
- */
2018
- async getOptions() {
2019
- try {
2020
- const e = await fetch(this._optionsEndpoint, {
2021
- method: "GET",
2022
- credentials: "include"
2023
- });
2024
- if (!e.ok)
2025
- return { enabled: !1, service: { available: !1 } };
2026
- const t = await e.json();
2027
- return t && typeof t == "object" ? t : { enabled: !1, service: { available: !1 } };
2028
- } catch {
2029
- if (this._fallbackEndpoint)
2030
- try {
2031
- const t = await fetch(this._fallbackEndpoint, {
2032
- method: "OPTIONS"
2033
- });
2034
- return {
2035
- enabled: t.ok,
2036
- service: { available: t.ok }
2037
- };
2038
- } catch {
2039
- }
2040
- return { enabled: !1, service: { available: !1 } };
2041
- }
2042
- }
2043
- _uuid() {
2044
- var e;
2045
- return (e = globalThis.crypto) != null && e.randomUUID ? globalThis.crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (t) => {
2046
- const i = Math.floor(Math.random() * 16);
2047
- return (t === "x" ? i : i & 3 | 8).toString(16);
2048
- });
2049
- }
2050
- async _sha256Hex(e) {
2051
- const t = await e.arrayBuffer(), i = await globalThis.crypto.subtle.digest("SHA-256", t);
2052
- return [...new Uint8Array(i)].map((s) => s.toString(16).padStart(2, "0")).join("");
2053
- }
2054
- }
2055
- function d(g, e = {}, ...t) {
2056
- const i = document.createElement(g);
2057
- for (const [s, r] of Object.entries(e))
2058
- if (r != null)
2059
- if (s === "className")
2060
- i.className = r;
2061
- else if (s === "style" && typeof r == "object")
2062
- Object.assign(i.style, r);
2063
- else if (s.startsWith("on") && typeof r == "function") {
2064
- const o = s.slice(2).toLowerCase();
2065
- i.addEventListener(o, r);
2066
- } else s === "dataset" && typeof r == "object" ? Object.assign(i.dataset, r) : i.setAttribute(s, r);
2067
- for (const s of t)
2068
- typeof s == "string" ? i.appendChild(document.createTextNode(s)) : s instanceof HTMLElement && i.appendChild(s);
2069
- return i;
2070
- }
2071
- function ve({ id: g, label: e, min: t = 0, max: i = 1, step: s = 0.01, value: r = 0.5, onChange: o }) {
2072
- const n = g.includes("-") ? g.split("-").slice(1).join("-") : g, a = d("div", {
2073
- className: "slider-control slider-wrapper",
2074
- "data-control": n,
2075
- "data-testid": `slider-${n}`
2076
- }), l = d(
2077
- "div",
2078
- { className: "slider-header" },
2079
- d("label", { for: g, className: "slider-label" }, e),
2080
- d("span", { className: "slider-value", id: `${g}-value` }, le(r))
2081
- ), h = d("input", {
2082
- type: "range",
2083
- id: g,
2084
- className: "slider-input",
2085
- min: String(t),
2086
- max: String(i),
2087
- step: String(s),
2088
- value: String(r),
2089
- onInput: (c) => {
2090
- const p = parseFloat(c.target.value), u = a.querySelector(".slider-value");
2091
- u && (u.textContent = le(p)), o == null || o(p);
2092
- }
2093
- });
2094
- return a.appendChild(l), a.appendChild(h), a.setValue = (c) => {
2095
- h.value = String(c);
2096
- const p = a.querySelector(".slider-value");
2097
- p && (p.textContent = le(c));
2098
- }, a;
2099
- }
2100
- function le(g) {
2101
- return Number.isInteger(g) ? String(g) : g.toFixed(2);
2102
- }
2103
- function we({ id: g, label: e, checked: t = !1, onChange: i }) {
2104
- const s = d("div", { className: "toggle-control" }), r = d("label", { className: "toggle-label", for: g }, e), o = d("input", {
2105
- type: "checkbox",
2106
- id: g,
2107
- className: "toggle-input",
2108
- checked: t ? "checked" : void 0,
2109
- onChange: (l) => i == null ? void 0 : i(l.target.checked)
2110
- }), n = d("div", {
2111
- className: "toggle-switch",
2112
- onClick: (l) => {
2113
- l.target !== o && (o.checked = !o.checked, i == null || i(o.checked));
2114
- }
2115
- }), a = d("span", { className: "toggle-slider" });
2116
- return n.appendChild(o), n.appendChild(a), s.appendChild(r), s.appendChild(n), s.setChecked = (l) => {
2117
- o.checked = l;
2118
- }, s;
2119
- }
2120
- function xe({ id: g, label: e, value: t = "#000000", onChange: i }) {
2121
- const s = d("div", { className: "color-control" }), r = d("label", { className: "color-label", for: g }, e), o = d("input", {
2122
- type: "color",
2123
- id: g,
2124
- className: "color-input",
2125
- value: t,
2126
- onInput: (n) => i == null ? void 0 : i(n.target.value)
2127
- });
2128
- return s.appendChild(r), s.appendChild(o), s.setValue = (n) => {
2129
- o.value = n;
2130
- }, s;
2131
- }
2132
- function Ce({ id: g, label: e, value: t = "", placeholder: i = "", onCommit: s }) {
2133
- const r = d("div", {
2134
- className: "text-control",
2135
- "data-control": g,
2136
- "data-testid": `text-${g}`
2137
- }), o = d("label", { className: "text-label", for: g }, e);
2138
- let n = t;
2139
- const a = d("input", {
2140
- type: "text",
2141
- id: g,
2142
- className: "text-input",
2143
- value: t,
2144
- placeholder: i,
2145
- onKeyDown: (h) => {
2146
- h.key === "Enter" && (h.preventDefault(), l(), a.blur());
2147
- },
2148
- onBlur: () => l()
2149
- });
2150
- function l() {
2151
- const h = a.value;
2152
- h !== n && (n = h, s == null || s(h));
2153
- }
2154
- return r.appendChild(o), r.appendChild(a), r.setValue = (h) => {
2155
- const c = h == null ? "" : String(h);
2156
- a.value = c, n = c;
2157
- }, r;
2158
- }
2159
- function ke({ id: g, label: e, options: t = [], value: i, onChange: s }) {
2160
- const r = d("div", { className: "select-control" }), o = d("label", { className: "select-label", for: g }, e), n = d("select", {
2161
- id: g,
2162
- className: "select-input",
2163
- onChange: (a) => s == null ? void 0 : s(a.target.value)
2164
- });
2165
- for (const a of t) {
2166
- const l = d("option", { value: a.value }, a.label);
2167
- a.value === i && (l.selected = !0), n.appendChild(l);
2168
- }
2169
- return r.appendChild(o), r.appendChild(n), r.setValue = (a) => {
2170
- n.value = a;
2171
- }, r;
2172
- }
2173
- function D({ label: g, className: e = "", onClick: t, icon: i = null, disabled: s = !1 }) {
2174
- const r = d("button", {
2175
- type: "button",
2176
- className: `btn ${e}`.trim(),
2177
- onClick: t,
2178
- disabled: s ? "disabled" : void 0
2179
- });
2180
- if (i) {
2181
- const o = d("span", { className: "btn-icon" });
2182
- o.innerHTML = i;
2183
- const n = o.querySelector("svg");
2184
- n && n.setAttribute("aria-hidden", "true"), r.appendChild(o);
2185
- }
2186
- return g && r.appendChild(document.createTextNode(g)), r;
2187
- }
2188
- function j({ icon: g, title: e, className: t = "", onClick: i, disabled: s = !1, testId: r = null, ariaLabel: o = null }) {
2189
- const n = {
2190
- type: "button",
2191
- className: `icon-btn ${t}`.trim(),
2192
- title: e,
2193
- "aria-label": o || e,
2194
- onClick: i,
2195
- disabled: s ? "disabled" : void 0
2196
- };
2197
- r && (n.dataset = { testid: r });
2198
- const a = d("button", n);
2199
- a.innerHTML = g;
2200
- const l = a.querySelector("svg");
2201
- return l && l.setAttribute("aria-hidden", "true"), a;
2202
- }
2203
- function re({ label: g, icon: e, active: t = !1, onClick: i }) {
2204
- const s = d("button", {
2205
- type: "button",
2206
- className: `chip ${t ? "active" : ""}`.trim(),
2207
- onClick: i
2208
- });
2209
- if (e) {
2210
- const r = d("span", { className: "chip-icon" });
2211
- r.innerHTML = e;
2212
- const o = r.querySelector("svg");
2213
- o && o.setAttribute("aria-hidden", "true"), s.appendChild(r);
2214
- }
2215
- return s.appendChild(d("span", { className: "chip-label" }, g)), s.setActive = (r) => {
2216
- s.classList.toggle("active", r);
2217
- }, s;
2218
- }
2219
- class et {
2220
- constructor(e, t) {
2221
- this.state = e, this.editor = t, this.element = null, this._removeBgBtn = null, this._removeBgTierSelect = null, this._removeBgStatus = null, this._removeBgTier = "balanced", this._unsubscribers = [];
2222
- }
2223
- /**
2224
- * Create and render the toolbar
2225
- * @returns {HTMLElement}
2226
- */
2227
- render() {
2228
- this.element = d("div", { className: "editor-toolbar" });
2229
- const e = d("div", { className: "toolbar-section toolbar-left" }), t = j({
2230
- icon: Ie,
2231
- title: "Open Image",
2232
- className: "toolbar-btn",
2233
- testId: "btn-open-image",
2234
- ariaLabel: "Open image file",
2235
- onClick: () => this.editor.openFilePicker()
2236
- });
2237
- e.appendChild(t);
2238
- const i = d("div", { className: "toolbar-section toolbar-right" });
2239
- this._themeBtn = j({
2240
- icon: this.state.get("isDarkMode") ? fe : ge,
2241
- title: "Toggle Theme",
2242
- className: "toolbar-btn toolbar-btn-theme",
2243
- testId: "btn-toggle-theme",
2244
- ariaLabel: "Toggle theme",
2245
- onClick: () => this.editor.toggleTheme()
2246
- }), this._cropBtn = j({
2247
- icon: be,
2248
- title: "Crop",
2249
- className: "toolbar-btn toolbar-btn-crop",
2250
- testId: "btn-crop",
2251
- ariaLabel: "Crop image",
2252
- onClick: () => {
2253
- this.state.get("mode") === "crop" ? this.editor.setMode("filters") : this.editor.setMode("crop");
2254
- }
2255
- });
2256
- const s = d("div", { className: "remove-bg-control" });
2257
- this._removeBgTierSelect = d("select", {
2258
- className: "remove-bg-tier-select",
2259
- title: "Background removal quality",
2260
- "aria-label": "Background removal quality",
2261
- dataset: { testid: "select-remove-background-tier" },
2262
- onChange: (h) => {
2263
- this._removeBgTier = h.target.value;
2264
- }
2265
- });
2266
- for (const h of [
2267
- ["fast", "Fast"],
2268
- ["balanced", "Balanced"],
2269
- ["best", "Best"]
2270
- ]) {
2271
- const c = d("option", { value: h[0] }, h[1]);
2272
- h[0] === this._removeBgTier && (c.selected = !0), this._removeBgTierSelect.appendChild(c);
2273
- }
2274
- this._removeBgBtn = j({
2275
- icon: Ge,
2276
- title: "Remove Background",
2277
- className: "toolbar-btn toolbar-btn-remove-bg",
2278
- testId: "btn-remove-background",
2279
- ariaLabel: "Remove background",
2280
- disabled: !this._canRemoveBackground(),
2281
- onClick: async () => {
2282
- if (this._canRemoveBackground()) {
2283
- this._setRemoveBgStatus("Removing background...");
2284
- try {
2285
- await this.editor.removeBackground({ tier: this._removeBgTier }), this._setRemoveBgStatus("Background removed");
2286
- } catch {
2287
- this._setRemoveBgStatus("Background removal failed");
2288
- } finally {
2289
- this._updateRemoveBgButton();
2290
- }
2291
- }
2292
- }
2293
- }), this._removeBgStatus = d("span", {
2294
- className: "sr-only",
2295
- role: "status",
2296
- "aria-live": "polite",
2297
- "aria-atomic": "true"
2298
- }), s.appendChild(this._removeBgTierSelect), s.appendChild(this._removeBgBtn), s.appendChild(this._removeBgStatus);
2299
- const r = j({
2300
- icon: ye,
2301
- title: "Reset All",
2302
- className: "toolbar-btn",
2303
- testId: "btn-reset-all",
2304
- ariaLabel: "Reset all changes",
2305
- onClick: () => this.editor.resetAll()
2306
- }), o = d("button", {
2307
- type: "button",
2308
- className: "icon-btn toolbar-btn toolbar-btn-primary toolbar-save-labeled",
2309
- title: "Save Image",
2310
- "aria-label": "Save image",
2311
- dataset: { testid: "btn-save-edit" },
2312
- onClick: () => this.editor.save()
2313
- }), n = d("span", { className: "btn-icon" });
2314
- n.innerHTML = Oe;
2315
- const a = n.querySelector("svg");
2316
- a && a.setAttribute("aria-hidden", "true"), o.appendChild(n), o.appendChild(d("span", { className: "toolbar-save-label" }, "Save"));
2317
- const l = j({
2318
- icon: ue,
2319
- title: "Close",
2320
- className: "toolbar-btn",
2321
- testId: "btn-cancel-edit",
2322
- ariaLabel: "Close editor",
2323
- onClick: () => this.editor.close()
2324
- });
2325
- return i.appendChild(this._themeBtn), i.appendChild(this._cropBtn), i.appendChild(s), i.appendChild(r), i.appendChild(o), i.appendChild(l), this.element.appendChild(e), this.element.appendChild(i), this._subscribeToState(), this.element;
2326
- }
2327
- /**
2328
- * Subscribe to state changes
2329
- */
2330
- _subscribeToState() {
2331
- const e = this.state.on("change:isDarkMode", ({ value: l }) => {
2332
- this._themeBtn.innerHTML = l ? fe : ge;
2333
- });
2334
- this._unsubscribers.push(e);
2335
- const t = this.state.on("change:mode", ({ value: l }) => {
2336
- this._cropBtn && this._cropBtn.classList.toggle("toolbar-btn-primary", l === "crop");
2337
- });
2338
- this._unsubscribers.push(t);
2339
- const i = this.state.on("change:hasImage", () => this._updateRemoveBgButton());
2340
- this._unsubscribers.push(i);
2341
- const s = this.state.on("change:isProcessing", () => this._updateRemoveBgButton());
2342
- this._unsubscribers.push(s);
2343
- const r = this.state.on("change:backgroundRemovalAvailable", () => this._updateRemoveBgButton());
2344
- this._unsubscribers.push(r);
2345
- const o = this.state.on("change:editorDocument", () => this._updateRemoveBgButton());
2346
- this._unsubscribers.push(o);
2347
- const n = this.state.on("change:layers", () => this._updateRemoveBgButton());
2348
- this._unsubscribers.push(n);
2349
- const a = this.state.on("change:activeLayerId", () => this._updateRemoveBgButton());
2350
- this._unsubscribers.push(a);
2351
- }
2352
- _canRemoveBackground() {
2353
- return typeof this.editor.canRemoveBackground == "function" ? this.editor.canRemoveBackground() : !!this.state.get("hasImage") && typeof this.editor.removeBackground == "function";
2354
- }
2355
- _updateRemoveBgButton() {
2356
- if (!this._removeBgBtn) return;
2357
- const e = this.state.get("isProcessing") === !0, t = !this._canRemoveBackground() || e;
2358
- this._removeBgBtn.disabled = t, this._removeBgBtn.setAttribute("aria-disabled", t ? "true" : "false"), this._removeBgBtn.setAttribute("aria-busy", e ? "true" : "false"), this._removeBgTierSelect && (this._removeBgTierSelect.disabled = t, this._removeBgTierSelect.setAttribute("aria-disabled", t ? "true" : "false"));
2359
- }
2360
- _setRemoveBgStatus(e) {
2361
- this._removeBgStatus && (this._removeBgStatus.textContent = e);
2362
- }
2363
- /**
2364
- * Clean up
2365
- */
2366
- destroy() {
2367
- var e;
2368
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], (e = this.element) == null || e.remove(), this.element = null;
2369
- }
2370
- }
2371
- const se = pe;
2372
- class tt {
2373
- constructor(e, t) {
2374
- this.state = e, this.editor = t, this.element = null, this._chips = /* @__PURE__ */ new Map(), this._scrollIndex = 0, this._unsubscribers = [];
2375
- }
2376
- /**
2377
- * Create and render the category carousel
2378
- * @param {Function} onSelect - Callback when category is selected
2379
- * @returns {HTMLElement}
2380
- */
2381
- render(e) {
2382
- this._onSelect = e, this.element = d("div", {
2383
- className: "category-carousel-container",
2384
- "data-testid": "category-carousel"
2385
- }), this._leftBtn = j({
2386
- icon: _e,
2387
- title: "Previous categories",
2388
- className: "carousel-nav carousel-nav-left",
2389
- onClick: () => this._scrollLeft()
2390
- }), this._carousel = d("div", { className: "category-carousel" }), se.forEach((i) => {
2391
- const s = re({
2392
- label: i.name,
2393
- icon: i.icon,
2394
- active: this.state.get("selectedCategory") === i.id,
2395
- onClick: () => this._selectCategory(i.id)
2396
- });
2397
- s.dataset.categoryId = i.id, s.dataset.category = i.id, s.dataset.testid = `category-${i.id}`, this._chips.set(i.id, s), this._carousel.appendChild(s);
2398
- }), this._rightBtn = j({
2399
- icon: me,
2400
- title: "Next categories",
2401
- className: "carousel-nav carousel-nav-right",
2402
- onClick: () => this._scrollRight()
2403
- }), this._pagination = d("div", {
2404
- className: "carousel-pagination",
2405
- role: "tablist",
2406
- "aria-label": "Category pages"
2407
- });
2408
- const t = Math.ceil(se.length / 3);
2409
- for (let i = 0; i < t; i++) {
2410
- const s = d("button", {
2411
- type: "button",
2412
- className: `pagination-dot ${i === 0 ? "active" : ""}`,
2413
- role: "tab",
2414
- "aria-label": `Page ${i + 1} of ${t}`,
2415
- "aria-selected": i === 0 ? "true" : "false",
2416
- onClick: () => this._scrollToPage(i)
2417
- });
2418
- this._pagination.appendChild(s);
2419
- }
2420
- return this.element.appendChild(this._leftBtn), this.element.appendChild(this._carousel), this.element.appendChild(this._rightBtn), this.element.appendChild(this._pagination), this._subscribeToState(), this._updateNavButtons(), this.element;
2421
- }
2422
- /**
2423
- * Subscribe to state changes
2424
- */
2425
- _subscribeToState() {
2426
- const e = this.state.on("change:selectedCategory", ({ value: t }) => {
2427
- this._chips.forEach((i, s) => {
2428
- i.setActive(s === t);
2429
- });
2430
- });
2431
- this._unsubscribers.push(e);
2432
- }
2433
- /**
2434
- * Select a category
2435
- * @param {string} categoryId
2436
- */
2437
- _selectCategory(e) {
2438
- var s;
2439
- this.state.set("selectedCategory", e);
2440
- const t = this.state.get("mode");
2441
- e === "crop" ? t !== "crop" && this.editor.setMode("crop") : t !== "filters" && this.editor.setMode("filters");
2442
- const i = this._chips.get(e);
2443
- i && this._carousel && i.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" }), (s = this._onSelect) == null || s.call(this, e);
2444
- }
2445
- /**
2446
- * Scroll carousel left
2447
- */
2448
- _scrollLeft() {
2449
- this._scrollIndex > 0 && (this._scrollIndex--, this._scrollCarousel());
2450
- }
2451
- /**
2452
- * Scroll carousel right
2453
- */
2454
- _scrollRight() {
2455
- this._scrollIndex < se.length - 3 && (this._scrollIndex++, this._scrollCarousel());
2456
- }
2457
- /**
2458
- * Scroll to a specific page
2459
- * @param {number} pageIndex
2460
- */
2461
- _scrollToPage(e) {
2462
- this._scrollIndex = e * 3, this._scrollCarousel();
2463
- }
2464
- /**
2465
- * Animate carousel scroll
2466
- */
2467
- _scrollCarousel() {
2468
- if (this._carousel) {
2469
- const e = this._carousel.querySelector(".chip"), t = (e == null ? void 0 : e.clientWidth) || 100;
2470
- this._carousel.scrollTo({
2471
- left: this._scrollIndex * (t + 8),
2472
- // 8px gap
2473
- behavior: "smooth"
2474
- });
2475
- }
2476
- this._updateNavButtons(), this._updatePagination();
2477
- }
2478
- /**
2479
- * Update navigation button states
2480
- */
2481
- _updateNavButtons() {
2482
- this._leftBtn && (this._leftBtn.disabled = this._scrollIndex === 0), this._rightBtn && (this._rightBtn.disabled = this._scrollIndex >= se.length - 3);
2483
- }
2484
- /**
2485
- * Update pagination dot states
2486
- */
2487
- _updatePagination() {
2488
- if (this._pagination) {
2489
- const e = this._pagination.querySelectorAll(".pagination-dot"), t = Math.floor(this._scrollIndex / 3);
2490
- e.forEach((i, s) => {
2491
- const r = s === t;
2492
- i.classList.toggle("active", r), i.setAttribute("aria-selected", r ? "true" : "false");
2493
- });
2494
- }
2495
- }
2496
- /**
2497
- * Set the selected category
2498
- * @param {string} categoryId
2499
- */
2500
- setSelected(e) {
2501
- this._selectCategory(e);
2502
- }
2503
- /**
2504
- * Clean up
2505
- */
2506
- destroy() {
2507
- var e;
2508
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._chips.clear(), (e = this.element) == null || e.remove(), this.element = null;
2509
- }
2510
- }
2511
- class it {
2512
- constructor(e, t) {
2513
- this.state = e, this.filterManager = t, this.element = null, this._filterCards = /* @__PURE__ */ new Map(), this._scrollIndex = 0, this._unsubscribers = [], this._onToggle = null, this._onSelect = null;
2514
- }
2515
- /**
2516
- * Create and render the filter carousel
2517
- * @param {Object} options - { onToggle, onSelect }
2518
- * @returns {HTMLElement}
2519
- */
2520
- render({ onToggle: e, onSelect: t }) {
2521
- return this._onToggle = e, this._onSelect = t, this.element = d("div", { className: "filter-carousel-container" }), this._leftBtn = j({
2522
- icon: _e,
2523
- title: "Previous filters",
2524
- className: "carousel-nav carousel-nav-left",
2525
- onClick: () => this._scrollLeft()
2526
- }), this._carousel = d("div", { className: "filter-carousel" }), this._rightBtn = j({
2527
- icon: me,
2528
- title: "Next filters",
2529
- className: "carousel-nav carousel-nav-right",
2530
- onClick: () => this._scrollRight()
2531
- }), this.element.appendChild(this._leftBtn), this.element.appendChild(this._carousel), this.element.appendChild(this._rightBtn), this._subscribeToState(), this._renderFilters(), this.element;
2532
- }
2533
- /**
2534
- * Subscribe to state changes
2535
- */
2536
- _subscribeToState() {
2537
- const e = this.state.on("change:selectedCategory", () => {
2538
- this._renderFilters();
2539
- });
2540
- this._unsubscribers.push(e);
2541
- const t = this.state.on("change:activeFilters", () => {
2542
- this._updateActiveStates();
2543
- });
2544
- this._unsubscribers.push(t);
2545
- const i = this.state.on("change:selectedFilter", () => {
2546
- this._updateSelectedState();
2547
- });
2548
- this._unsubscribers.push(i);
2549
- }
2550
- /**
2551
- * Render filters for current category
2552
- */
2553
- _renderFilters() {
2554
- this._carousel.innerHTML = "", this._filterCards.clear(), this._scrollIndex = 0;
2555
- const e = this.state.get("selectedCategory");
2556
- if (e === "crop") return;
2557
- const t = this.filterManager.getFiltersByCategory(e), i = this.state.get("activeFilters"), s = this.state.get("selectedFilter");
2558
- t.forEach((r) => {
2559
- const o = this._createFilterCard(r, {
2560
- isActive: i.has(r.id),
2561
- isSelected: s === r.id
2562
- });
2563
- this._filterCards.set(r.id, o), this._carousel.appendChild(o);
2564
- }), this._updateNavButtons();
2565
- }
2566
- /**
2567
- * Create a filter card
2568
- * @param {Object} filter
2569
- * @param {Object} options
2570
- * @returns {HTMLElement}
2571
- */
2572
- _createFilterCard(e, { isActive: t, isSelected: i }) {
2573
- const s = d("div", {
2574
- className: `filter-card ${t ? "active" : ""} ${i ? "selected" : ""}`,
2575
- "data-filter": e.id,
2576
- "data-testid": `filter-${e.id}`,
2577
- onClick: () => this._handleCardClick(e.id)
2578
- }), r = d("div", { className: "filter-preview" }), o = d("span", { className: "filter-preview-text" }, e.name.charAt(0));
2579
- r.appendChild(o);
2580
- const n = d("span", { className: "filter-name", title: e.name }, e.name), a = d("button", {
2581
- className: `filter-toggle ${t ? "active" : ""}`,
2582
- onClick: (l) => {
2583
- var p;
2584
- l.stopPropagation();
2585
- const c = !this.state.get("activeFilters").has(e.id);
2586
- this._handleToggle(e.id, c), c && (this.state.set("selectedFilter", e.id), (p = this._onSelect) == null || p.call(this, e.id));
2587
- }
2588
- });
2589
- return a.innerHTML = t ? ce : "", s.appendChild(r), s.appendChild(n), s.appendChild(a), s._toggle = a, s._isActive = t, s;
2590
- }
2591
- /**
2592
- * Handle filter card click (select)
2593
- * @param {string} filterId
2594
- */
2595
- _handleCardClick(e) {
2596
- var i;
2597
- this.state.get("activeFilters").has(e) || this._handleToggle(e, !0), this.state.set("selectedFilter", e), (i = this._onSelect) == null || i.call(this, e);
2598
- }
2599
- /**
2600
- * Handle filter toggle
2601
- * @param {string} filterId
2602
- * @param {boolean} enabled
2603
- */
2604
- _handleToggle(e, t) {
2605
- var i;
2606
- (i = this._onToggle) == null || i.call(this, e, t);
2607
- }
2608
- /**
2609
- * Update active states based on state
2610
- */
2611
- _updateActiveStates() {
2612
- const e = this.state.get("activeFilters");
2613
- this._filterCards.forEach((t, i) => {
2614
- const s = e.has(i);
2615
- t.classList.toggle("active", s), t._toggle.classList.toggle("active", s), t._toggle.innerHTML = s ? ce : "", t._isActive = s;
2616
- });
2617
- }
2618
- /**
2619
- * Update selected state based on state
2620
- */
2621
- _updateSelectedState() {
2622
- const e = this.state.get("selectedFilter");
2623
- this._filterCards.forEach((t, i) => {
2624
- t.classList.toggle("selected", i === e);
2625
- });
2626
- }
2627
- /**
2628
- * Scroll carousel left
2629
- */
2630
- _scrollLeft() {
2631
- this._scrollIndex > 0 && (this._scrollIndex--, this._scrollCarousel());
2632
- }
2633
- /**
2634
- * Scroll carousel right
2635
- */
2636
- _scrollRight() {
2637
- const e = Math.max(0, this._filterCards.size - 3);
2638
- this._scrollIndex < e && (this._scrollIndex++, this._scrollCarousel());
2639
- }
2640
- /**
2641
- * Animate carousel scroll
2642
- */
2643
- _scrollCarousel() {
2644
- if (this._carousel) {
2645
- const e = this._carousel.querySelector(".filter-card"), t = (e == null ? void 0 : e.clientWidth) || 120;
2646
- this._carousel.scrollTo({
2647
- left: this._scrollIndex * (t + 8),
2648
- behavior: "smooth"
2649
- });
2650
- }
2651
- this._updateNavButtons();
2652
- }
2653
- /**
2654
- * Update navigation button states
2655
- */
2656
- _updateNavButtons() {
2657
- const e = Math.max(0, this._filterCards.size - 3);
2658
- this._leftBtn && (this._leftBtn.disabled = this._scrollIndex === 0), this._rightBtn && (this._rightBtn.disabled = this._scrollIndex >= e);
2659
- }
2660
- /**
2661
- * Clean up
2662
- */
2663
- destroy() {
2664
- var e;
2665
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._filterCards.clear(), (e = this.element) == null || e.remove(), this.element = null;
2666
- }
2667
- }
2668
- class st {
2669
- constructor(e, t) {
2670
- this.state = e, this.filterManager = t, this.element = null, this._controls = /* @__PURE__ */ new Map(), this._onChange = null, this._onReset = null, this._onAction = null, this._unsubscribers = [];
2671
- }
2672
- /**
2673
- * Create and render the filter adjustments panel
2674
- * @param {Object} options - { onChange, onReset, onAction }
2675
- * @returns {HTMLElement}
2676
- */
2677
- render({ onChange: e, onReset: t, onAction: i }) {
2678
- return this._onChange = e, this._onReset = t, this._onAction = i, this.element = d("div", { className: "filter-adjustments" }), this._subscribeToState(), this._renderControls(), this.element;
2679
- }
2680
- /**
2681
- * Subscribe to state changes
2682
- */
2683
- _subscribeToState() {
2684
- const e = this.state.on("change:selectedFilter", () => {
2685
- this._renderControls();
2686
- });
2687
- this._unsubscribers.push(e);
2688
- const t = this.state.on("change:filterValues", () => {
2689
- this._updateValues();
2690
- });
2691
- this._unsubscribers.push(t);
2692
- }
2693
- /**
2694
- * Render controls for the selected filter.
2695
- *
2696
- * Renders both the static controls from def.controls and any dynamic
2697
- * controls the filter instance exposes via getDynamicControls() (e.g.
2698
- * per-stop color/offset/alpha rows for Color Gradient).
2699
- */
2700
- _renderControls() {
2701
- this.element.innerHTML = "", this._controls.clear();
2702
- const e = this.state.get("selectedFilter");
2703
- if (!e) {
2704
- this.element.appendChild(
2705
- d("div", { className: "no-filter-selected" }, "Select a filter to adjust")
2706
- );
2707
- return;
2708
- }
2709
- const t = this.filterManager.getFilterDef(e);
2710
- if (!t) {
2711
- this.element.appendChild(
2712
- d("div", { className: "no-filter-selected" }, "Filter not found")
2713
- );
2714
- return;
2715
- }
2716
- const i = d("div", { className: "adjustments-header" });
2717
- i.appendChild(d("span", { className: "adjustments-title" }, t.name)), i.appendChild(D({
2718
- label: "Reset",
2719
- className: "btn-text",
2720
- onClick: () => this._handleReset(e)
2721
- })), this.element.appendChild(i);
2722
- const s = d("div", { className: "adjustments-grid" }), r = this.state.getFilterValues(e);
2723
- (t.controls || []).forEach((n) => {
2724
- const a = r[n.id] ?? n.default, l = this._createControl(e, n, a);
2725
- l && (this._controls.set(n.id, l), s.appendChild(l));
2726
- });
2727
- const o = this.filterManager.getInstance(e);
2728
- if (o && typeof o.getDynamicControls == "function") {
2729
- let n = [];
2730
- try {
2731
- n = o.getDynamicControls() || [];
2732
- } catch {
2733
- }
2734
- n.forEach((a) => {
2735
- const l = a.property || a.id, h = r[l] ?? a.default, c = this._createControl(e, a, h);
2736
- c && (this._controls.set(a.id, c), s.appendChild(c));
2737
- });
2738
- }
2739
- this.element.appendChild(s);
2740
- }
2741
- /**
2742
- * Create a control element based on type.
2743
- *
2744
- * The update key passed to _handleChange is `ctl.property || ctl.id`.
2745
- * Dynamic controls (e.g. from getDynamicControls) use their property
2746
- * path (e.g. 'colorStops[0].color') so FilterManager.updateValue can
2747
- * route the write correctly. Static controls without a property fall
2748
- * back to the control id, matching the legacy contract.
2749
- *
2750
- * @param {string} filterId
2751
- * @param {Object} ctl - Control definition
2752
- * @param {*} value - Current value
2753
- * @returns {HTMLElement|null}
2754
- */
2755
- _createControl(e, t, i) {
2756
- if (t.hidden) return null;
2757
- const s = t.label || t.id, r = this._normalizeControlType(t.type), o = t.property || t.id;
2758
- switch (r) {
2759
- case "slider":
2760
- return ve({
2761
- id: `${e}-${t.id}`,
2762
- label: s,
2763
- min: t.min ?? 0,
2764
- max: t.max ?? 1,
2765
- step: t.step ?? 0.01,
2766
- value: typeof i == "number" ? i : t.default ?? 0,
2767
- onChange: (l) => this._handleChange(e, o, l)
2768
- });
2769
- case "toggle":
2770
- return we({
2771
- id: `${e}-${t.id}`,
2772
- label: s,
2773
- checked: !!i,
2774
- onChange: (l) => this._handleChange(e, o, l)
2775
- });
2776
- case "color":
2777
- return xe({
2778
- id: `${e}-${t.id}`,
2779
- label: s,
2780
- value: typeof i == "string" && i.startsWith("#") ? i : t.default || "#000000",
2781
- onChange: (l) => this._handleChange(e, o, l)
2782
- });
2783
- case "select":
2784
- const n = this._normalizeOptions(t.options);
2785
- return ke({
2786
- id: `${e}-${t.id}`,
2787
- label: s,
2788
- options: n,
2789
- value: i ?? t.default,
2790
- onChange: (l) => this._handleChange(e, o, l)
2791
- });
2792
- case "text":
2793
- return Ce({
2794
- id: `${e}-${t.id}`,
2795
- label: s,
2796
- value: typeof i == "string" ? i : t.default ?? "",
2797
- placeholder: t.placeholder || "",
2798
- onCommit: (l) => this._handleChange(e, o, l)
2799
- });
2800
- case "button":
2801
- const a = d("div", { className: "button-control" });
2802
- return a.appendChild(D({
2803
- label: s,
2804
- className: "btn-secondary",
2805
- onClick: () => this._handleAction(e, t.action || t.id)
2806
- })), a;
2807
- default:
2808
- return null;
2809
- }
2810
- }
2811
- /**
2812
- * Normalize control type (handle aliases)
2813
- * @param {string} type
2814
- * @returns {string}
2815
- */
2816
- _normalizeControlType(e) {
2817
- return {
2818
- slider: "slider",
2819
- range: "slider",
2820
- toggle: "toggle",
2821
- checkbox: "toggle",
2822
- color: "color",
2823
- select: "select",
2824
- dropdown: "select",
2825
- button: "button",
2826
- text: "text"
2827
- }[e] || e;
2828
- }
2829
- /**
2830
- * Normalize options array format
2831
- * @param {Array} options
2832
- * @returns {Array}
2833
- */
2834
- _normalizeOptions(e) {
2835
- return !e || !Array.isArray(e) ? [] : e.map((t) => typeof t == "object" && t.value !== void 0 ? { value: t.value, label: t.label || String(t.value) } : typeof t == "string" ? { value: t, label: t } : typeof t == "number" ? { value: t, label: String(t) } : { value: t, label: String(t) });
2836
- }
2837
- /**
2838
- * Handle control value change
2839
- * @param {string} filterId
2840
- * @param {string} controlId
2841
- * @param {*} value
2842
- */
2843
- _handleChange(e, t, i) {
2844
- var s;
2845
- (s = this._onChange) == null || s.call(this, e, t, i);
2846
- }
2847
- /**
2848
- * Handle reset button
2849
- * @param {string} filterId
2850
- */
2851
- _handleReset(e) {
2852
- var t;
2853
- (t = this._onReset) == null || t.call(this, e), this._renderControls();
2854
- }
2855
- /**
2856
- * Handle button action. Dynamic controls (e.g. color stop rows) depend on
2857
- * filter state that can change as a result of the action (add/remove stop),
2858
- * so re-render after the action completes.
2859
- *
2860
- * @param {string} filterId
2861
- * @param {string} action
2862
- */
2863
- _handleAction(e, t) {
2864
- var i;
2865
- (i = this._onAction) == null || i.call(this, e, t), this._renderControls();
2866
- }
2867
- /**
2868
- * Update control values from state
2869
- */
2870
- _updateValues() {
2871
- const e = this.state.get("selectedFilter");
2872
- if (!e) return;
2873
- const t = this.state.getFilterValues(e), i = this.filterManager.getFilterDef(e);
2874
- i && i.controls.forEach((s) => {
2875
- const r = this._controls.get(s.id);
2876
- if (r && typeof r.setValue == "function") {
2877
- const o = t[s.id] ?? s.default;
2878
- r.setValue(o);
2879
- }
2880
- });
2881
- }
2882
- /**
2883
- * Show panel
2884
- */
2885
- show() {
2886
- this.element && (this.element.style.display = "");
2887
- }
2888
- /**
2889
- * Hide panel
2890
- */
2891
- hide() {
2892
- this.element && (this.element.style.display = "none");
2893
- }
2894
- /**
2895
- * Clean up
2896
- */
2897
- destroy() {
2898
- var e;
2899
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._controls.clear(), (e = this.element) == null || e.remove(), this.element = null;
2900
- }
2901
- }
2902
- class rt {
2903
- constructor(e, t) {
2904
- this.state = e, this.filterManager = t, this._drawer = null, this._body = null, this._titleEl = null, this._isOpen = !1, this._currentFilterId = null, this._controls = /* @__PURE__ */ new Map(), this._onChange = null, this._onReset = null, this._onRemove = null, this._onAction = null, this._container = null;
2905
- }
2906
- /**
2907
- * Build the drawer DOM and append to container (should be the controls section)
2908
- * @param {HTMLElement} container - Parent element (controls-section)
2909
- * @param {Object} callbacks - { onChange, onReset, onRemove, onAction }
2910
- */
2911
- build(e, { onChange: t, onReset: i, onRemove: s, onAction: r }) {
2912
- this._onChange = t, this._onReset = i, this._onRemove = s, this._onAction = r, this._container = e, this._drawer = d("div", { className: "mobile-filter-drawer" });
2913
- const o = d("div", { className: "drawer-header" });
2914
- this._titleEl = d("span", { className: "drawer-title" }, "Filter");
2915
- const n = d("div", { className: "drawer-header-actions" }), a = D({
2916
- label: "Reset",
2917
- className: "btn-text",
2918
- onClick: () => {
2919
- var c;
2920
- this._currentFilterId && ((c = this._onReset) == null || c.call(this, this._currentFilterId), this._renderControls());
2921
- }
2922
- }), l = D({
2923
- label: "Remove",
2924
- className: "btn-text btn-danger",
2925
- onClick: () => {
2926
- var c;
2927
- this._currentFilterId && ((c = this._onRemove) == null || c.call(this, this._currentFilterId), this.close());
2928
- }
2929
- }), h = j({
2930
- icon: ue,
2931
- title: "Close",
2932
- className: "btn-icon-sm",
2933
- ariaLabel: "Close drawer",
2934
- onClick: () => this.close()
2935
- });
2936
- n.appendChild(a), n.appendChild(l), n.appendChild(h), o.appendChild(this._titleEl), o.appendChild(n), this._drawer.appendChild(o), this._body = d("div", { className: "drawer-body" }), this._drawer.appendChild(this._body), e.appendChild(this._drawer);
2937
- }
2938
- /**
2939
- * Open the drawer for a specific filter
2940
- * @param {string} filterId
2941
- */
2942
- open(e) {
2943
- this._isOpen && this._currentFilterId === e || (this._currentFilterId = e, this._renderControls(), this._isOpen || (this._isOpen = !0, this._sizeToContainer(), this._animateOpen()));
2944
- }
2945
- /**
2946
- * Close the drawer
2947
- */
2948
- close() {
2949
- this._isOpen && (this._isOpen = !1, this._animateClose(), this._currentFilterId = null);
2950
- }
2951
- /**
2952
- * Check if drawer is open
2953
- * @returns {boolean}
2954
- */
2955
- get isOpen() {
2956
- return this._isOpen;
2957
- }
2958
- /**
2959
- * Size the drawer to match the container (controls section) height
2960
- */
2961
- _sizeToContainer() {
2962
- if (!this._container || !this._drawer) return;
2963
- const e = this._container.offsetHeight;
2964
- e > 0 && (this._drawer.style.height = e + "px");
2965
- }
2966
- /**
2967
- * Animate drawer open (CSS transition)
2968
- */
2969
- _animateOpen() {
2970
- this._drawer.style.transition = "none", this._drawer.style.transform = "translateY(100%)", this._drawer.offsetHeight, this._drawer.style.transition = "transform 0.25s cubic-bezier(0.33, 1, 0.68, 1)", this._drawer.style.transform = "translateY(0)";
2971
- }
2972
- /**
2973
- * Animate drawer close (CSS transition)
2974
- */
2975
- _animateClose() {
2976
- this._drawer.style.transition = "transform 0.2s cubic-bezier(0.32, 0, 0.67, 0)", this._drawer.style.transform = "translateY(100%)";
2977
- }
2978
- /**
2979
- * Render controls for the current filter.
2980
- * Renders both static (def.controls) and dynamic (instance.getDynamicControls)
2981
- * controls so filters like Color Gradient show per-stop rows.
2982
- */
2983
- _renderControls() {
2984
- if (this._body.innerHTML = "", this._controls.clear(), !this._currentFilterId) return;
2985
- const e = this.filterManager.getFilterDef(this._currentFilterId);
2986
- if (!e) return;
2987
- this._titleEl.textContent = e.name;
2988
- const t = d("div", { className: "adjustments-grid" }), i = this.state.getFilterValues(this._currentFilterId);
2989
- (e.controls || []).forEach((r) => {
2990
- if (r.hidden) return;
2991
- const o = this._createControl(this._currentFilterId, r, i[r.id] ?? r.default);
2992
- o && (this._controls.set(r.id, o), t.appendChild(o));
2993
- });
2994
- const s = this.filterManager.getInstance(this._currentFilterId);
2995
- if (s && typeof s.getDynamicControls == "function") {
2996
- let r = [];
2997
- try {
2998
- r = s.getDynamicControls() || [];
2999
- } catch {
3000
- }
3001
- r.forEach((o) => {
3002
- const n = o.property || o.id, a = i[n] ?? o.default, l = this._createControl(this._currentFilterId, o, a);
3003
- l && (this._controls.set(o.id, l), t.appendChild(l));
3004
- });
3005
- }
3006
- this._body.appendChild(t);
3007
- }
3008
- /**
3009
- * Create a control element based on type. See FilterAdjustments._createControl
3010
- * for the update-key contract (property path for dynamic controls, id for static).
3011
- * @param {string} filterId
3012
- * @param {Object} ctl
3013
- * @param {*} value
3014
- * @returns {HTMLElement|null}
3015
- */
3016
- _createControl(e, t, i) {
3017
- if (t.hidden) return null;
3018
- const s = t.label || t.id, r = this._normalizeControlType(t.type), o = t.property || t.id;
3019
- switch (r) {
3020
- case "slider":
3021
- return ve({
3022
- id: `drawer-${e}-${t.id}`,
3023
- label: s,
3024
- min: t.min ?? 0,
3025
- max: t.max ?? 1,
3026
- step: t.step ?? 0.01,
3027
- value: typeof i == "number" ? i : t.default ?? 0,
3028
- onChange: (n) => {
3029
- var a;
3030
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3031
- }
3032
- });
3033
- case "toggle":
3034
- return we({
3035
- id: `drawer-${e}-${t.id}`,
3036
- label: s,
3037
- checked: !!i,
3038
- onChange: (n) => {
3039
- var a;
3040
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3041
- }
3042
- });
3043
- case "color":
3044
- return xe({
3045
- id: `drawer-${e}-${t.id}`,
3046
- label: s,
3047
- value: typeof i == "string" && i.startsWith("#") ? i : t.default || "#000000",
3048
- onChange: (n) => {
3049
- var a;
3050
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3051
- }
3052
- });
3053
- case "select": {
3054
- const n = this._normalizeOptions(t.options);
3055
- return ke({
3056
- id: `drawer-${e}-${t.id}`,
3057
- label: s,
3058
- options: n,
3059
- value: i ?? t.default,
3060
- onChange: (a) => {
3061
- var l;
3062
- return (l = this._onChange) == null ? void 0 : l.call(this, e, o, a);
3063
- }
3064
- });
3065
- }
3066
- case "text":
3067
- return Ce({
3068
- id: `drawer-${e}-${t.id}`,
3069
- label: s,
3070
- value: typeof i == "string" ? i : t.default ?? "",
3071
- placeholder: t.placeholder || "",
3072
- onCommit: (n) => {
3073
- var a;
3074
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3075
- }
3076
- });
3077
- case "button": {
3078
- const n = d("div", { className: "button-control" });
3079
- return n.appendChild(D({
3080
- label: s,
3081
- className: "btn-secondary",
3082
- onClick: () => {
3083
- var a;
3084
- (a = this._onAction) == null || a.call(this, e, t.action || t.id), this._renderControls();
3085
- }
3086
- })), n;
3087
- }
3088
- default:
3089
- return null;
3090
- }
3091
- }
3092
- /**
3093
- * Normalize control type
3094
- * @param {string} type
3095
- * @returns {string}
3096
- */
3097
- _normalizeControlType(e) {
3098
- return {
3099
- slider: "slider",
3100
- range: "slider",
3101
- toggle: "toggle",
3102
- checkbox: "toggle",
3103
- color: "color",
3104
- select: "select",
3105
- dropdown: "select",
3106
- button: "button",
3107
- text: "text"
3108
- }[e] || e;
3109
- }
3110
- /**
3111
- * Normalize options array
3112
- * @param {Array} options
3113
- * @returns {Array}
3114
- */
3115
- _normalizeOptions(e) {
3116
- return !e || !Array.isArray(e) ? [] : e.map((t) => typeof t == "object" && t.value !== void 0 ? { value: t.value, label: t.label || String(t.value) } : typeof t == "string" ? { value: t, label: t } : typeof t == "number" ? { value: t, label: String(t) } : { value: t, label: String(t) });
3117
- }
3118
- /**
3119
- * Clean up
3120
- */
3121
- destroy() {
3122
- var e;
3123
- this._controls.clear(), (e = this._drawer) == null || e.remove(), this._drawer = null, this._isOpen = !1, this._currentFilterId = null, this._container = null;
3124
- }
3125
- }
3126
- class nt {
3127
- constructor(e, t) {
3128
- this.state = e, this.filterManager = t, this.element = null, this._scrollContainer = null, this._chips = /* @__PURE__ */ new Map(), this._unsubscribers = [], this._onToggle = null, this._onSelect = null;
3129
- }
3130
- /**
3131
- * @param {Object} callbacks - { onToggle(filterId, enabled), onSelect(filterId) }
3132
- * @returns {HTMLElement}
3133
- */
3134
- render({ onToggle: e, onSelect: t }) {
3135
- return this._onToggle = e, this._onSelect = t, this.element = d("div", {
3136
- className: "mobile-active-filters",
3137
- "data-testid": "mobile-active-filters"
3138
- }), this._scrollContainer = d("div", { className: "mobile-active-filters-scroll" }), this.element.appendChild(this._scrollContainer), this._subscribeToState(), this._renderChips(), this.element;
3139
- }
3140
- _subscribeToState() {
3141
- const e = this.state.on("change:activeFilters", () => {
3142
- this._renderChips();
3143
- });
3144
- this._unsubscribers.push(e);
3145
- const t = this.state.on("change:selectedFilter", () => {
3146
- this._updateSelectedState();
3147
- });
3148
- this._unsubscribers.push(t);
3149
- }
3150
- _renderChips() {
3151
- this._scrollContainer.innerHTML = "", this._chips.clear();
3152
- const e = this.state.get("activeFilters");
3153
- if (!e || e.size === 0) {
3154
- const t = d("div", {
3155
- className: "active-filter-placeholder"
3156
- }, "No active filters");
3157
- this._scrollContainer.appendChild(t);
3158
- return;
3159
- }
3160
- e.forEach((t) => {
3161
- const i = this.filterManager.getFilterDef(t);
3162
- if (!i) return;
3163
- const s = this._createChip(t, i);
3164
- this._chips.set(t, s), this._scrollContainer.appendChild(s);
3165
- }), this._updateSelectedState();
3166
- }
3167
- _createChip(e, t) {
3168
- const i = d("div", {
3169
- className: "active-filter-chip",
3170
- "data-filter-id": e,
3171
- "data-testid": `active-chip-${e}`
3172
- }), s = d("span", {
3173
- className: "active-filter-chip-label",
3174
- onClick: (o) => {
3175
- var n;
3176
- o.stopPropagation(), (n = this._onSelect) == null || n.call(this, e);
3177
- }
3178
- }, t.name), r = d("button", {
3179
- className: "active-filter-chip-check",
3180
- "aria-label": `Remove ${t.name} filter`,
3181
- onClick: (o) => {
3182
- var n;
3183
- o.stopPropagation(), (n = this._onToggle) == null || n.call(this, e, !1);
3184
- }
3185
- });
3186
- return r.innerHTML = '<svg width="10" height="10" viewBox="0 0 12 12"><path d="M2 6l3 3 5-5" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>', i.appendChild(s), i.appendChild(r), i;
3187
- }
3188
- _updateSelectedState() {
3189
- const e = this.state.get("selectedFilter");
3190
- this._chips.forEach((t, i) => {
3191
- t.classList.toggle("selected", i === e);
3192
- });
3193
- }
3194
- destroy() {
3195
- var e;
3196
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._chips.clear(), (e = this.element) == null || e.remove(), this.element = null;
3197
- }
3198
- }
3199
- const ot = [
3200
- { id: "free", name: "Free", icon: Ze },
3201
- { id: "square", name: "Square", icon: We },
3202
- { id: "circle", name: "Circle", icon: $e }
3203
- ], at = [
3204
- { id: "free", name: "Free" },
3205
- { id: "1:1", name: "1:1" },
3206
- { id: "4:3", name: "4:3" },
3207
- { id: "16:9", name: "16:9" },
3208
- { id: "3:2", name: "3:2" },
3209
- { id: "2:3", name: "2:3" }
3210
- ], lt = [
3211
- { id: "0", label: "0°", angle: 0 },
3212
- { id: "90", label: "90°", angle: 90 },
3213
- { id: "180", label: "180°", angle: 180 },
3214
- { id: "270", label: "270°", angle: 270 }
3215
- ];
3216
- function $(g) {
3217
- if (!Number.isFinite(Number(g))) return 0;
3218
- const e = Number(g) % 360;
3219
- return e < 0 ? e + 360 : e;
3220
- }
3221
- class ht {
3222
- constructor(e, t, i = null) {
3223
- this.state = e, this.cropManager = t, this.editor = i, this.element = null, this._shapeChips = /* @__PURE__ */ new Map(), this._aspectChips = /* @__PURE__ */ new Map(), this._rotationPresetChips = /* @__PURE__ */ new Map(), this._rotationRange = null, this._rotationNumber = null, this._rotationValue = null, this._unsubscribers = [];
3224
- }
3225
- /**
3226
- * Apply crop and return the editor UI to filter mode.
3227
- * If crop application cannot complete, force-disable crop mode to avoid
3228
- * leaving the editor stuck in crop controls.
3229
- */
3230
- _applyCropAndReturnToFilters() {
3231
- this.cropManager.apply() === null && typeof this.cropManager.disable == "function" && this.cropManager.disable(), this.state.set("selectedCategory", "adjust");
3232
- }
3233
- /**
3234
- * Create and render the crop controls
3235
- * @returns {HTMLElement}
3236
- */
3237
- render() {
3238
- this.element = d("div", {
3239
- className: "crop-controls",
3240
- "data-testid": "crop-controls"
3241
- }), this.element.appendChild(this._renderRotationSection());
3242
- const e = d("div", { className: "crop-section" });
3243
- e.appendChild(d("label", { className: "section-label" }, "Shape"));
3244
- const t = d("div", { className: "chip-row" }), i = this.state.get("crop.shape");
3245
- ot.forEach((l) => {
3246
- const h = re({
3247
- label: l.name,
3248
- icon: l.icon,
3249
- active: i === l.id,
3250
- onClick: () => this._selectShape(l.id)
3251
- });
3252
- h.dataset.shape = l.id, h.dataset.testid = `crop-shape-${l.id}`, this._shapeChips.set(l.id, h), t.appendChild(h);
3253
- }), e.appendChild(t), this.element.appendChild(e), this.state.get("lockCropShape") && (e.style.display = "none"), this._shapeSection = e, this._aspectSection = d("div", { className: "crop-section" }), this._aspectSection.appendChild(d("label", { className: "section-label" }, "Aspect Ratio"));
3254
- const s = d("div", { className: "chip-row aspect-row" }), r = this.state.get("crop.aspect");
3255
- at.forEach((l) => {
3256
- const h = re({
3257
- label: l.name,
3258
- active: r === l.id,
3259
- onClick: () => this._selectAspect(l.id)
3260
- });
3261
- h.dataset.ratio = l.id, h.dataset.testid = `crop-ratio-${l.id}`, this._aspectChips.set(l.id, h), s.appendChild(h);
3262
- }), this._aspectSection.appendChild(s), this.element.appendChild(this._aspectSection), this._updateAspectVisibility();
3263
- const o = d("div", { className: "crop-actions" }), n = D({
3264
- label: "Cancel",
3265
- className: "btn-secondary crop-cancel-btn",
3266
- icon: ue,
3267
- onClick: () => this.cropManager.cancel()
3268
- });
3269
- n.dataset.testid = "cancel-crop";
3270
- const a = D({
3271
- label: "Apply Crop",
3272
- className: "btn-primary crop-apply-btn",
3273
- icon: ce,
3274
- onClick: () => this._applyCropAndReturnToFilters()
3275
- });
3276
- return a.dataset.testid = "apply-crop", o.appendChild(n), o.appendChild(a), this.element.appendChild(o), this._subscribeToState(), this.element;
3277
- }
3278
- _renderRotationSection() {
3279
- const e = d("div", { className: "crop-section rotation-section" });
3280
- e.appendChild(d("label", { className: "section-label" }, "Rotate"));
3281
- const t = d("div", { className: "rotation-action-row" }), i = D({
3282
- label: "Left 90°",
3283
- className: "btn-secondary rotation-action-btn",
3284
- icon: ze,
3285
- onClick: () => this._rotateBy(-90)
3286
- });
3287
- i.dataset.testid = "rotate-left-90";
3288
- const s = D({
3289
- label: "Right 90°",
3290
- className: "btn-secondary rotation-action-btn",
3291
- icon: Pe,
3292
- onClick: () => this._rotateBy(90)
3293
- });
3294
- s.dataset.testid = "rotate-right-90";
3295
- const r = D({
3296
- label: "Rotate 180°",
3297
- className: "btn-secondary rotation-action-btn",
3298
- onClick: () => this._rotateBy(180)
3299
- });
3300
- r.dataset.testid = "rotate-180";
3301
- const o = D({
3302
- label: "Rotate 270°",
3303
- className: "btn-secondary rotation-action-btn",
3304
- onClick: () => this._rotateBy(270)
3305
- });
3306
- o.dataset.testid = "rotate-270", t.appendChild(i), t.appendChild(s), t.appendChild(r), t.appendChild(o), e.appendChild(t);
3307
- const n = d("div", { className: "chip-row rotation-preset-row" }), a = $(this.state.get("transform.rotation") ?? 0);
3308
- lt.forEach((u) => {
3309
- const f = re({
3310
- label: u.label,
3311
- active: Math.round(a) === u.angle,
3312
- onClick: () => this._setRotation(u.angle)
3313
- });
3314
- f.dataset.angle = u.id, f.dataset.testid = `rotation-preset-${u.id}`, this._rotationPresetChips.set(u.id, f), n.appendChild(f);
3315
- }), e.appendChild(n);
3316
- const l = d("div", { className: "rotation-arbitrary" }), h = d(
3317
- "div",
3318
- { className: "rotation-angle-header" },
3319
- d("label", { className: "rotation-angle-label", for: "image-editor-rotation-angle" }, "Angle"),
3320
- d("span", { className: "rotation-angle-value" }, this._formatAngle(a))
3321
- );
3322
- this._rotationValue = h.querySelector(".rotation-angle-value"), this._rotationRange = d("input", {
3323
- id: "image-editor-rotation-angle",
3324
- type: "range",
3325
- className: "slider-input rotation-range",
3326
- min: "0",
3327
- max: "360",
3328
- step: "1",
3329
- value: String(Math.round(a)),
3330
- "aria-label": "Rotation angle",
3331
- onInput: (u) => this._previewAngleValue(u.target.value),
3332
- onChange: (u) => this._setRotation(Number(u.target.value))
3333
- }), this._rotationNumber = d("input", {
3334
- type: "number",
3335
- className: "text-input rotation-number-input",
3336
- min: "-360",
3337
- max: "360",
3338
- step: "1",
3339
- value: this._formatPlainAngle(a),
3340
- "aria-label": "Rotation angle in degrees",
3341
- onKeyDown: (u) => {
3342
- u.key === "Enter" && (u.preventDefault(), this._setRotation(Number(u.target.value)), u.target.blur());
3343
- },
3344
- onBlur: (u) => this._setRotation(Number(u.target.value))
3345
- });
3346
- const c = d(
3347
- "div",
3348
- { className: "rotation-angle-row" },
3349
- this._rotationRange,
3350
- this._rotationNumber
3351
- );
3352
- l.appendChild(h), l.appendChild(c);
3353
- const p = D({
3354
- label: "Reset rotation",
3355
- className: "btn-text rotation-reset-btn",
3356
- onClick: () => this._setRotation(0)
3357
- });
3358
- return p.dataset.testid = "reset-rotation", l.appendChild(p), e.appendChild(l), e;
3359
- }
3360
- /**
3361
- * Subscribe to state changes
3362
- */
3363
- _subscribeToState() {
3364
- const e = this.state.on("change:crop.shape", ({ value: s }) => {
3365
- this._shapeChips.forEach((r, o) => {
3366
- r.setActive(o === s);
3367
- }), this._updateAspectVisibility();
3368
- });
3369
- this._unsubscribers.push(e);
3370
- const t = this.state.on("change:crop.aspect", ({ value: s }) => {
3371
- this._aspectChips.forEach((r, o) => {
3372
- r.setActive(o === s);
3373
- });
3374
- });
3375
- this._unsubscribers.push(t);
3376
- const i = this.state.on("change:transform.rotation", ({ value: s }) => {
3377
- this._syncRotationControls(s);
3378
- });
3379
- this._unsubscribers.push(i);
3380
- }
3381
- _rotateBy(e) {
3382
- var i, s;
3383
- ((s = (i = this.editor) == null ? void 0 : i.rotateBy) == null ? void 0 : s.call(i, e)) !== !1 && this._syncRotationControls(this.state.get("transform.rotation") ?? 0);
3384
- }
3385
- _setRotation(e) {
3386
- var s, r;
3387
- const t = $(e);
3388
- if (((r = (s = this.editor) == null ? void 0 : s.setRotationAngle) == null ? void 0 : r.call(s, t)) === !1) {
3389
- this._syncRotationControls(this.state.get("transform.rotation") ?? 0);
3390
- return;
3391
- }
3392
- this._syncRotationControls(t);
3393
- }
3394
- _previewAngleValue(e) {
3395
- const t = $(Number(e));
3396
- this._rotationValue && (this._rotationValue.textContent = this._formatAngle(t)), this._rotationNumber && (this._rotationNumber.value = this._formatPlainAngle(t)), this._refreshPresetChips(t);
3397
- }
3398
- _syncRotationControls(e) {
3399
- const t = $(e);
3400
- this._rotationValue && (this._rotationValue.textContent = this._formatAngle(t)), this._rotationRange && (this._rotationRange.value = String(Math.round(t))), this._rotationNumber && (this._rotationNumber.value = this._formatPlainAngle(t)), this._refreshPresetChips(t);
3401
- }
3402
- _refreshPresetChips(e) {
3403
- const t = Math.round($(e)) % 360;
3404
- this._rotationPresetChips.forEach((i, s) => {
3405
- i.setActive(Number(s) === t);
3406
- });
3407
- }
3408
- _formatAngle(e) {
3409
- return `${this._formatPlainAngle(e)}°`;
3410
- }
3411
- _formatPlainAngle(e) {
3412
- const t = $(e);
3413
- return Number.isInteger(t) ? String(t) : t.toFixed(1);
3414
- }
3415
- /**
3416
- * Select a crop shape
3417
- * @param {string} shapeId
3418
- */
3419
- _selectShape(e) {
3420
- this.cropManager.setShape(e);
3421
- }
3422
- /**
3423
- * Select an aspect ratio
3424
- * @param {string} aspectId
3425
- */
3426
- _selectAspect(e) {
3427
- this.cropManager.setAspect(e);
3428
- }
3429
- /**
3430
- * Update aspect ratio section visibility
3431
- */
3432
- _updateAspectVisibility() {
3433
- const e = this.state.get("crop.shape"), t = this.state.get("lockAspectRatio");
3434
- this._aspectSection && (this._aspectSection.style.display = e === "free" && !t ? "" : "none");
3435
- }
3436
- /**
3437
- * Show controls
3438
- */
3439
- show() {
3440
- this.element && (this.element.style.display = "");
3441
- }
3442
- /**
3443
- * Hide controls
3444
- */
3445
- hide() {
3446
- this.element && (this.element.style.display = "none");
3447
- }
3448
- /**
3449
- * Clean up
3450
- */
3451
- destroy() {
3452
- var e;
3453
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._shapeChips.clear(), this._aspectChips.clear(), this._rotationPresetChips.clear(), (e = this.element) == null || e.remove(), this.element = null;
3454
- }
3455
- }
3456
- class ct {
3457
- constructor(e, t) {
3458
- this.state = e, this.filterManager = t, this.element = null, this._filterItems = /* @__PURE__ */ new Map(), this._unsubscribers = [], this._onRemove = null, this._onReset = null, this._onClearAll = null, this._onUpdateValue = null, this._onSelect = null;
3459
- }
3460
- /**
3461
- * Create and render the active filters panel
3462
- * @param {Object} options - { onRemove, onReset, onClearAll, onUpdateValue, onSelect }
3463
- * @returns {HTMLElement}
3464
- */
3465
- render({ onRemove: e, onReset: t, onClearAll: i, onUpdateValue: s, onSelect: r }) {
3466
- this._onRemove = e, this._onReset = t, this._onClearAll = i, this._onUpdateValue = s, this._onSelect = r, this.element = d("div", {
3467
- className: "active-filters-panel",
3468
- "data-testid": "active-filters-panel"
3469
- });
3470
- const o = d("div", { className: "panel-header" });
3471
- o.appendChild(d("h3", { className: "panel-title" }, "Active Filters"));
3472
- const n = D({
3473
- label: "Clear All",
3474
- className: "btn-text btn-danger",
3475
- onClick: () => this._handleClearAll()
3476
- });
3477
- return n.dataset.testid = "clear-all-filters", o.appendChild(n), this.element.appendChild(o), this._listContainer = d("div", { className: "active-filters-list" }), this.element.appendChild(this._listContainer), this._subscribeToState(), this._renderFilterList(), this.element;
3478
- }
3479
- /**
3480
- * Subscribe to state changes
3481
- */
3482
- _subscribeToState() {
3483
- const e = this.state.on("change:activeFilters", () => {
3484
- this._renderFilterList();
3485
- });
3486
- this._unsubscribers.push(e);
3487
- const t = this.state.on("change:filterValues", () => {
3488
- this._updateValuesDisplay();
3489
- });
3490
- this._unsubscribers.push(t);
3491
- const i = this.state.on("change:selectedFilter", () => {
3492
- this._updateSelectedState();
3493
- });
3494
- this._unsubscribers.push(i);
3495
- }
3496
- /**
3497
- * Update selected state for all items
3498
- */
3499
- _updateSelectedState() {
3500
- const e = this.state.get("selectedFilter");
3501
- this._filterItems.forEach((t, i) => {
3502
- t.classList.toggle("selected", i === e);
3503
- });
3504
- }
3505
- /**
3506
- * Render the filter list
3507
- */
3508
- _renderFilterList() {
3509
- this._listContainer.innerHTML = "", this._filterItems.clear();
3510
- const e = this.state.get("activeFilters");
3511
- if (e.size === 0) {
3512
- this._listContainer.appendChild(
3513
- d("div", { className: "no-filters-message" }, "No filters active")
3514
- );
3515
- return;
3516
- }
3517
- e.forEach((t) => {
3518
- const i = this.filterManager.getFilterDef(t);
3519
- if (!i) return;
3520
- const s = this._createFilterItem(t, i);
3521
- this._filterItems.set(t, s), this._listContainer.appendChild(s);
3522
- });
3523
- }
3524
- /**
3525
- * Create a filter item
3526
- * @param {string} filterId
3527
- * @param {Object} def - Filter definition
3528
- * @returns {HTMLElement}
3529
- */
3530
- _createFilterItem(e, t) {
3531
- const s = this.state.get("selectedFilter") === e, r = d("div", {
3532
- className: `active-filter-item ${s ? "selected" : ""}`,
3533
- "data-active-filter": e,
3534
- "data-testid": `active-filter-${e}`,
3535
- onClick: (u) => {
3536
- u.target.closest(".filter-item-actions") || this._handleSelect(e);
3537
- }
3538
- }), o = d("div", { className: "filter-item-header" }), n = d("span", { className: "filter-item-name" }, t.name), a = d("div", { className: "filter-item-actions" }), l = j({
3539
- icon: ye,
3540
- title: "Reset filter",
3541
- className: "btn-icon-sm",
3542
- onClick: () => this._handleReset(e)
3543
- }), h = j({
3544
- icon: He,
3545
- title: "Remove filter",
3546
- className: "btn-icon-sm btn-danger",
3547
- onClick: () => this._handleRemove(e)
3548
- });
3549
- a.appendChild(l), a.appendChild(h), o.appendChild(n), o.appendChild(a), r.appendChild(o);
3550
- const c = this.state.getFilterValues(e), p = d("div", { className: "filter-item-summary" });
3551
- return p.textContent = this._getValuesSummary(t, c), r.appendChild(p), r._summaryEl = p, r;
3552
- }
3553
- /**
3554
- * Normalize control type (handle aliases)
3555
- * @param {string} type
3556
- * @returns {string}
3557
- */
3558
- _normalizeControlType(e) {
3559
- return {
3560
- slider: "slider",
3561
- range: "slider",
3562
- toggle: "toggle",
3563
- checkbox: "toggle",
3564
- color: "color",
3565
- select: "select",
3566
- dropdown: "select",
3567
- button: "button"
3568
- }[e] || e;
3569
- }
3570
- /**
3571
- * Get a summary of filter values
3572
- * @param {Object} def - Filter definition
3573
- * @param {Object} values - Current values
3574
- * @returns {string}
3575
- */
3576
- _getValuesSummary(e, t) {
3577
- if (!e.controls || !Array.isArray(e.controls))
3578
- return "Default values";
3579
- const i = [];
3580
- return e.controls.forEach((s) => {
3581
- const r = this._normalizeControlType(s.type);
3582
- if (r === "button") return;
3583
- const o = s.label || s.id, n = t[s.id] ?? s.default;
3584
- n !== s.default && (r === "slider" ? i.push(`${o}: ${this._formatValue(n)}`) : r === "toggle" ? n && i.push(o) : r === "color" ? i.push(`${o}: ${n}`) : r === "select" && i.push(`${o}: ${n}`));
3585
- }), i.length > 0 ? i.join(", ") : "Default values";
3586
- }
3587
- /**
3588
- * Format a numeric value for display
3589
- * @param {number} value
3590
- * @returns {string}
3591
- */
3592
- _formatValue(e) {
3593
- return typeof e != "number" || Number.isInteger(e) ? String(e) : e.toFixed(2);
3594
- }
3595
- /**
3596
- * Update values display for all items
3597
- */
3598
- _updateValuesDisplay() {
3599
- this._filterItems.forEach((e, t) => {
3600
- const i = this.filterManager.getFilterDef(t);
3601
- if (!i || !e._summaryEl) return;
3602
- const s = this.state.getFilterValues(t);
3603
- e._summaryEl.textContent = this._getValuesSummary(i, s);
3604
- });
3605
- }
3606
- /**
3607
- * Handle filter removal
3608
- * @param {string} filterId
3609
- */
3610
- _handleRemove(e) {
3611
- var t;
3612
- (t = this._onRemove) == null || t.call(this, e);
3613
- }
3614
- /**
3615
- * Handle filter reset
3616
- * @param {string} filterId
3617
- */
3618
- _handleReset(e) {
3619
- var t;
3620
- (t = this._onReset) == null || t.call(this, e);
3621
- }
3622
- /**
3623
- * Handle clear all
3624
- */
3625
- _handleClearAll() {
3626
- var e;
3627
- (e = this._onClearAll) == null || e.call(this);
3628
- }
3629
- /**
3630
- * Handle filter selection
3631
- * @param {string} filterId
3632
- */
3633
- _handleSelect(e) {
3634
- var t;
3635
- this.state.set("selectedFilter", e), (t = this._onSelect) == null || t.call(this, e);
3636
- }
3637
- /**
3638
- * Show panel
3639
- */
3640
- show() {
3641
- this.element && (this.element.style.display = "");
3642
- }
3643
- /**
3644
- * Hide panel
3645
- */
3646
- hide() {
3647
- this.element && (this.element.style.display = "none");
3648
- }
3649
- /**
3650
- * Clean up
3651
- */
3652
- destroy() {
3653
- var e;
3654
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._filterItems.clear(), (e = this.element) == null || e.remove(), this.element = null;
3655
- }
3656
- }
3657
- const Z = {
3658
- name: "free",
3659
- initialMode: "filters",
3660
- cropShape: "free",
3661
- aspectRatio: "free",
3662
- autoZoomOnCropOverflow: !1,
3663
- lockCropShape: !1,
3664
- lockAspectRatio: !1,
3665
- showFilters: !0,
3666
- showCropControls: !0,
3667
- maxExportWidth: void 0,
3668
- maxExportHeight: void 0
3669
- }, he = {
3670
- free: {
3671
- ...Z
3672
- },
3673
- avatar: {
3674
- ...Z,
3675
- name: "avatar",
3676
- initialMode: "crop",
3677
- cropShape: "circle",
3678
- aspectRatio: "1:1",
3679
- autoZoomOnCropOverflow: !0,
3680
- lockCropShape: !0,
3681
- lockAspectRatio: !0
3682
- },
3683
- banner: {
3684
- ...Z,
3685
- name: "banner",
3686
- initialMode: "crop",
3687
- cropShape: "square",
3688
- aspectRatio: "16:9",
3689
- autoZoomOnCropOverflow: !1,
3690
- lockCropShape: !1,
3691
- lockAspectRatio: !0
3692
- },
3693
- cover: {
3694
- ...Z,
3695
- name: "cover",
3696
- initialMode: "crop",
3697
- cropShape: "free",
3698
- aspectRatio: "24:5",
3699
- autoZoomOnCropOverflow: !0,
3700
- lockCropShape: !0,
3701
- lockAspectRatio: !0
3702
- },
3703
- product: {
3704
- ...Z,
3705
- name: "product",
3706
- initialMode: "filters",
3707
- cropShape: "square",
3708
- aspectRatio: "1:1",
3709
- autoZoomOnCropOverflow: !1,
3710
- lockCropShape: !1,
3711
- lockAspectRatio: !1
3712
- }
3713
- };
3714
- function dt(g) {
3715
- if (!g)
3716
- return { ...he.free };
3717
- if (typeof g == "object" && g !== null)
3718
- return { ...Z, ...g };
3719
- const e = he[g];
3720
- return e ? { ...e } : { ...he.free };
3721
- }
3722
- function q(g) {
3723
- if (!Number.isFinite(Number(g))) return 0;
3724
- const e = Number(g) % 360;
3725
- return e < 0 ? e + 360 : e;
3726
- }
3727
- function ut(g, e) {
3728
- const t = q(g);
3729
- return (q(e) - t + 540) % 360 - 180;
3730
- }
3731
- class kt extends ne {
3732
- /**
3733
- * Create a new VanillaImageEditor
3734
- * @param {HTMLElement} container - Container element to mount the editor
3735
- * @param {Object} options - Configuration options
3736
- */
3737
- constructor(e, t = {}) {
3738
- var r, o, n, a;
3739
- if (super(), !e)
3740
- throw new Error("VanillaImageEditor: container element is required");
3741
- this._container = e, this._destroyed = !1, this._loadVersion = 0, this._objectUrls = /* @__PURE__ */ new Set(), this._activeObjectUrl = null;
3742
- const i = t.preset ? dt(t.preset) : null, s = i ? {
3743
- initialMode: i.initialMode === "crop" ? "crop" : "adjust",
3744
- cropShape: i.cropShape,
3745
- initialAspectRatio: i.aspectRatio
3746
- } : {};
3747
- this._options = {
3748
- theme: "auto",
3749
- initialImage: null,
3750
- initialMode: "adjust",
3751
- cropShape: "free",
3752
- initialAspectRatio: "free",
3753
- backgroundRemoval: {
3754
- enabled: !0,
3755
- endpoint: "/api/v1/media/background-removal/preview",
3756
- optionsEndpoint: "/api/v1/media/background-removal/options",
3757
- fallbackEndpoint: null
3758
- },
3759
- ...s,
3760
- ...t
3761
- }, this._preset = i, this._state = Se(), this._state.set("theme", this._options.theme), this._state.set("crop.shape", this._options.cropShape), this._state.set("crop.aspect", this._options.initialAspectRatio), this._preset && (this._state.set("lockCropShape", !!this._preset.lockCropShape), this._state.set("lockAspectRatio", !!this._preset.lockAspectRatio), this._state.set("showFilters", this._preset.showFilters !== !1), this._state.set("showCropControls", this._preset.showCropControls !== !1), this._state.set("autoZoomOnCropOverflow", !!this._preset.autoZoomOnCropOverflow)), this._renderer = new Te(), this._filterManager = new Qe(this._state, this._renderer), this._cropManager = new de(this._state, this._renderer), this._removeBgManager = null, this._backgroundRemovalAvailable = !1, ((r = this._options.backgroundRemoval) == null ? void 0 : r.enabled) !== !1 && (this._removeBgManager = new Xe({
3762
- endpoint: (o = this._options.backgroundRemoval) == null ? void 0 : o.endpoint,
3763
- optionsEndpoint: (n = this._options.backgroundRemoval) == null ? void 0 : n.optionsEndpoint,
3764
- fallbackEndpoint: (a = this._options.backgroundRemoval) == null ? void 0 : a.fallbackEndpoint
3765
- })), this._loadingOverlay = null, this._isMobile = typeof window < "u" ? window.innerWidth <= 768 : !1, this._toolbar = null, this._categoryCarousel = null, this._filterCarousel = null, this._filterAdjustments = null, this._mobileFilterDrawer = null, this._mobileActiveFilters = null, this._cropControls = null, this._activeFiltersPanel = null, this._editorEl = null, this._canvasContainer = null, this._cropOverlay = null, this._controlsSection = null, this._filterControlsEl = null, this._fileInput = null, this._resizeObserver = null, this._initPromise = Promise.resolve(this._init()).catch((l) => {
3766
- this._destroyed || this.emit("error", l instanceof Error ? l : new Error(String(l)));
3767
- });
3768
- }
3769
- /**
3770
- * Initialize the editor
3771
- */
3772
- async _init() {
3773
- this._state.detectTheme(), this._buildDOM(), !(!await this._renderer.mount(
3774
- this._canvasContainer,
3775
- this._state.get("isDarkMode") ? 657930 : 16777215
3776
- ) || this._destroyed) && (this._cropManager.setOverlayCanvas(this._cropOverlay), this._initUI(), this._setupResizeObserver(), this._setupWindowResize(), this._subscribeToState(), this._refreshBackgroundRemovalAvailability(), this._options.initialImage && await this.loadImage(this._options.initialImage), this.emit("ready"));
3777
- }
3778
- /**
3779
- * Build DOM structure
3780
- */
3781
- _buildDOM() {
3782
- this._container.innerHTML = "", this._editorEl = d("div", {
3783
- className: `vanilla-image-editor ${this._state.get("isDarkMode") ? "dark" : "light"}`
3784
- });
3785
- const e = d("div", { className: "editor-toolbar-container" });
3786
- this._editorEl.appendChild(e), this._toolbarContainer = e;
3787
- const t = d("div", { className: "editor-content" }), i = d("div", { className: "canvas-section" });
3788
- this._canvasContainer = d("div", { className: "canvas-container" }), this._cropOverlay = d("canvas", { className: "crop-overlay", "aria-hidden": "true" }), i.appendChild(this._canvasContainer), i.appendChild(this._cropOverlay), t.appendChild(i), this._controlsSection = d("div", { className: "controls-section" }), t.appendChild(this._controlsSection), this._editorEl.appendChild(t), this._fileInput = d("input", {
3789
- type: "file",
3790
- accept: "image/*",
3791
- className: "hidden-file-input",
3792
- "aria-label": "Choose image file to edit",
3793
- onChange: (s) => this._handleFileSelect(s)
3794
- }), this._editorEl.appendChild(this._fileInput), this._container.appendChild(this._editorEl);
3795
- }
3796
- /**
3797
- * Initialize UI components
3798
- */
3799
- _initUI() {
3800
- this._toolbar = new et(this._state, this), this._toolbarContainer.appendChild(this._toolbar.render()), this._filterControlsEl = d("div", { className: "filter-controls-container" }), this._categoryCarousel = new tt(this._state, this), this._filterControlsEl.appendChild(this._categoryCarousel.render((s) => {
3801
- }));
3802
- const e = d("div", { className: "filter-layout" }), t = d("div", { className: "filter-left-column" });
3803
- this._filterCarousel = new it(this._state, this._filterManager), t.appendChild(this._filterCarousel.render({
3804
- onToggle: (s, r) => this._handleFilterToggle(s, r),
3805
- onSelect: (s) => {
3806
- this._handleFilterSelect(s), this._isMobile && this._mobileFilterDrawer && this._mobileFilterDrawer.open(s);
3807
- }
3808
- })), this._filterAdjustments = new st(this._state, this._filterManager), t.appendChild(this._filterAdjustments.render({
3809
- onChange: (s, r, o) => this._handleFilterChange(s, r, o),
3810
- onReset: (s) => this._handleFilterReset(s),
3811
- onAction: (s, r) => this._handleFilterAction(s, r)
3812
- })), e.appendChild(t);
3813
- const i = d("div", { className: "filter-right-column" });
3814
- this._activeFiltersPanel = new ct(this._state, this._filterManager), i.appendChild(this._activeFiltersPanel.render({
3815
- onRemove: (s) => this._handleFilterToggle(s, !1),
3816
- onReset: (s) => this._handleFilterReset(s),
3817
- onClearAll: () => this.resetAll(),
3818
- onSelect: (s) => this._handleFilterSelect(s)
3819
- })), e.appendChild(i), this._filterControlsEl.appendChild(e), this._mobileActiveFilters = new nt(this._state, this._filterManager), this._filterControlsEl.appendChild(this._mobileActiveFilters.render({
3820
- onToggle: (s, r) => this._handleFilterToggle(s, r),
3821
- onSelect: (s) => {
3822
- this._handleFilterSelect(s), this._isMobile && this._mobileFilterDrawer && this._mobileFilterDrawer.open(s);
3823
- }
3824
- })), this._cropControls = new ht(this._state, this._cropManager, this), this._cropControlsEl = this._cropControls.render(), this._cropControlsEl.style.display = "none", this._controlsSection.appendChild(this._filterControlsEl), this._controlsSection.appendChild(this._cropControlsEl), this._mobileFilterDrawer = new rt(this._state, this._filterManager), this._mobileFilterDrawer.build(this._controlsSection, {
3825
- onChange: (s, r, o) => this._handleFilterChange(s, r, o),
3826
- onReset: (s) => this._handleFilterReset(s),
3827
- onRemove: (s) => {
3828
- this._handleFilterToggle(s, !1), this._state.set("selectedFilter", null);
3829
- },
3830
- onAction: (s, r) => this._handleFilterAction(s, r)
3831
- });
3832
- }
3833
- /**
3834
- * Set up resize observer with debounce to prevent infinite loops.
3835
- * Without debounce, PIXI renderer.resize() changes the canvas buffer
3836
- * which triggers another ResizeObserver notification, causing a loop
3837
- * that floods the console with "ResizeObserver loop completed" errors
3838
- * and prevents the image from rendering (gray canvas).
3839
- */
3840
- _setupResizeObserver() {
3841
- let e = null;
3842
- this._resizeObserver = new ResizeObserver(() => {
3843
- clearTimeout(e), e = setTimeout(() => {
3844
- this._canvasContainer && (this._renderer.resizeTo(this._canvasContainer), this._state.get("mode") === "crop" && this._cropManager.drawOverlay());
3845
- }, 50);
3846
- }), this._resizeObserver.observe(this._canvasContainer);
3847
- }
3848
- /**
3849
- * Set up window resize listener for mobile detection
3850
- */
3851
- _setupWindowResize() {
3852
- this._handleWindowResize = () => {
3853
- var e;
3854
- this._isMobile = window.innerWidth <= 768, !this._isMobile && ((e = this._mobileFilterDrawer) != null && e.isOpen) && this._mobileFilterDrawer.close();
3855
- }, window.addEventListener("resize", this._handleWindowResize);
3856
- }
3857
- /**
3858
- * Subscribe to state changes
3859
- */
3860
- _subscribeToState() {
3861
- this._state.on("change:mode", ({ value: e }) => {
3862
- this._updateModeUI(e), e === "filters" && this._filterManager.applyFilters();
3863
- }), this._state.on("change:isDarkMode", ({ value: e }) => {
3864
- this._editorEl.classList.toggle("dark", e), this._editorEl.classList.toggle("light", !e), this._renderer.setBackgroundColor(e ? 657930 : 16777215);
3865
- }), this._state.on("change:hasImage", ({ value: e }) => {
3866
- this._editorEl.classList.toggle("has-image", e);
3867
- }), this._renderer.on("zoomChange", (e) => {
3868
- this._state.set("zoom", e);
3869
- });
3870
- }
3871
- /**
3872
- * Update UI based on mode
3873
- * @param {string} mode
3874
- */
3875
- _updateModeUI(e) {
3876
- var t;
3877
- e === "crop" ? (this._filterControlsEl.style.display = "none", this._cropControlsEl.style.display = "", (t = this._mobileFilterDrawer) != null && t.isOpen && this._mobileFilterDrawer.close()) : (this._filterControlsEl.style.display = "", this._cropControlsEl.style.display = "none");
3878
- }
3879
- /**
3880
- * Handle file selection
3881
- * @param {Event} event
3882
- */
3883
- async _handleFileSelect(e) {
3884
- var i;
3885
- const t = (i = e.target.files) == null ? void 0 : i[0];
3886
- t && await this.loadImage(t), this._fileInput.value = "";
3887
- }
3888
- /**
3889
- * Handle filter toggle
3890
- * @param {string} filterId
3891
- * @param {boolean} enabled
3892
- */
3893
- _handleFilterToggle(e, t) {
3894
- this._filterManager.toggle(e, t);
3895
- }
3896
- /**
3897
- * Handle filter select
3898
- * @param {string} filterId
3899
- */
3900
- _handleFilterSelect(e) {
3901
- this._state.set("selectedFilter", e);
3902
- }
3903
- /**
3904
- * Handle filter value change
3905
- * @param {string} filterId
3906
- * @param {string} controlId
3907
- * @param {*} value
3908
- */
3909
- _handleFilterChange(e, t, i) {
3910
- this._filterManager.updateValue(e, t, i) || this._filterManager.applyFilters(), this._renderer.render();
3911
- }
3912
- /**
3913
- * Handle filter reset
3914
- * @param {string} filterId
3915
- */
3916
- _handleFilterReset(e) {
3917
- this._filterManager.resetValues(e), this._filterManager.applyFilters();
3918
- }
3919
- /**
3920
- * Handle filter action (button click).
3921
- * Delegates to FilterManager.performFilterAction which syncs state after
3922
- * the action so the result survives filter recreation.
3923
- * @param {string} filterId
3924
- * @param {string} action
3925
- */
3926
- _handleFilterAction(e, t) {
3927
- this._filterManager.performFilterAction(e, t);
3928
- }
3929
- _revokeObjectUrl(e) {
3930
- !e || !this._objectUrls.has(e) || (URL.revokeObjectURL(e), this._objectUrls.delete(e), this._activeObjectUrl === e && (this._activeObjectUrl = null));
3931
- }
3932
- _replaceActiveObjectUrl(e) {
3933
- const t = this._activeObjectUrl;
3934
- this._activeObjectUrl = e, this._objectUrls.add(e), t && t !== e && this._revokeObjectUrl(t);
3935
- }
3936
- _clearActiveObjectUrl() {
3937
- this._activeObjectUrl && this._revokeObjectUrl(this._activeObjectUrl);
3938
- }
3939
- // ==================== Public API ====================
3940
- /**
3941
- * Set the filter registry (from mediables/filters)
3942
- * @param {Object} registry - { getFilter, getAllFilters, getFiltersByCategory }
3943
- */
3944
- setFilterRegistry(e) {
3945
- this._filterManager.setRegistry(e);
3946
- }
3947
- /**
3948
- * Load an image into the editor
3949
- * @param {string|Blob|File} imageSource - URL, data URL, Blob, or File object
3950
- * @param {Object} [options] - Load options
3951
- * @param {Object|null} [options.state] - ImageEditorSessionState to restore
3952
- * @param {string|number} [options.sessionKey] - Session boundary identifier
3953
- */
3954
- async loadImage(e, t = {}) {
3955
- var n, a;
3956
- if (this._destroyed) return;
3957
- (a = (n = this._removeBgManager) == null ? void 0 : n.cancelActiveRequests) == null || a.call(n, "image-load");
3958
- const i = ++this._loadVersion;
3959
- let s = e, r = null;
3960
- this._resetEditorState(), this._state.set("hasImage", !1), this._state.set("imageUrl", null), e instanceof Blob && (s = URL.createObjectURL(e), r = s, this._objectUrls.add(s));
3961
- const o = await this._renderer.loadTexture(s, {
3962
- isCurrent: () => !this._destroyed && this._loadVersion === i
3963
- });
3964
- if (this._destroyed || this._loadVersion !== i) {
3965
- this._revokeObjectUrl(r);
3966
- return;
3967
- }
3968
- if (!o) {
3969
- this._revokeObjectUrl(r), this.emit("error", new Error("Failed to load image"));
3970
- return;
3971
- }
3972
- r ? this._replaceActiveObjectUrl(r) : this._clearActiveObjectUrl(), this._state.set("hasImage", !0), this._state.set("imageUrl", s), t.state && this._hydrateState(t.state), this._options.initialMode === "crop" && this.setMode("crop"), this.emit("imageLoaded", { url: s });
3973
- }
3974
- /**
3975
- * Reset all editor state (filters + crop) to a clean baseline.
3976
- * Called at the start of loadImage() to prevent stale state bleed between sessions.
3977
- */
3978
- _resetEditorState() {
3979
- this._filterManager.resetAll(), this._state.set("crop.rect", null), this._state.set("crop.appliedRect", null), this._state.set("crop.appliedShape", null), this._state.set("crop.appliedAspect", null), this._state.set("crop.dirty", !1), this._state.set("crop.shape", this._options.cropShape || "free"), this._state.set("crop.aspect", this._options.initialAspectRatio || "free"), this._state.set("transform.rotation", 0), this._state.get("mode") === "crop" && (this._cropManager.disable(), this._state.set("mode", "filters"));
3980
- }
3981
- /**
3982
- * Hydrate editor state from a canonical ImageEditorSessionState object.
3983
- * Restores filter stack + values and crop metadata (shape, aspect, rect).
3984
- * @param {Object} state - ImageEditorSessionState
3985
- */
3986
- _hydrateState(e) {
3987
- var i;
3988
- if (!e || e.version !== 1) return;
3989
- const t = q(((i = e.transform) == null ? void 0 : i.rotation) ?? 0);
3990
- if (t !== 0 && this._renderer.rotateBy(t) && this._state.set("transform.rotation", t), e.crop) {
3991
- const s = e.crop.shape || "free", r = e.crop.aspectRatio || "free";
3992
- e.crop.rect && (this._cropManager.applyFromPixelRect(e.crop.rect, s), this._state.set("crop.appliedRect", { ...e.crop.rect }), this._state.set("crop.appliedShape", s), this._state.set("crop.appliedAspect", r)), this._state.set("crop.shape", s), this._state.set("crop.aspect", r);
3993
- }
3994
- if (Array.isArray(e.filters)) {
3995
- let s = null;
3996
- for (const r of e.filters)
3997
- if (r.enabled && (s === null && (s = r.id), this._state.toggleFilter(r.id, !0), this._filterManager.initializeValues(r.id), r.values))
3998
- for (const [o, n] of Object.entries(r.values))
3999
- this._state.setFilterValue(r.id, o, n);
4000
- if (s !== null) {
4001
- this._state.set("selectedFilter", s);
4002
- const r = this._filterManager.getFilterDef(s);
4003
- if (typeof (r == null ? void 0 : r.category) == "string") {
4004
- const o = Ye[r.category] ?? r.category;
4005
- this._state.set("selectedCategory", o);
4006
- }
4007
- }
4008
- this._filterManager.applyFilters();
4009
- }
4010
- }
4011
- /**
4012
- * Open the file picker dialog
4013
- */
4014
- openFilePicker() {
4015
- var e;
4016
- (e = this._fileInput) == null || e.click();
4017
- }
4018
- /**
4019
- * Export the current image.
4020
- *
4021
- * @param {string} format - 'png' or 'jpeg'
4022
- * @param {number} quality - Quality for jpeg (0-1)
4023
- * @param {Object} [options]
4024
- * @param {number} [options.maxEdge] - Cap the longest edge to this pixel count
4025
- * @param {number} [options.maxPixels] - Cap total pixel count (width*height)
4026
- * @returns {string|null} Data URL
4027
- */
4028
- exportImage(e = "png", t = 0.92, i = {}) {
4029
- return !this._state.get("hasImage") || !this._applyPendingCropBeforeExport("VanillaImageEditor.exportImage") ? null : this._renderer.exportImage(
4030
- e,
4031
- t,
4032
- i.maxEdge ?? 0,
4033
- i.dontUpscale !== !1,
4034
- i.maxPixels ?? 0
4035
- );
4036
- }
4037
- /**
4038
- * Export the current image as a Blob after finalizing pending crop edits.
4039
- *
4040
- * @param {string} format - 'png' or 'jpeg'
4041
- * @param {number} quality - Quality for jpeg (0-1)
4042
- * @param {Object} [options]
4043
- * @returns {Promise<{blob: Blob, width: number, height: number}|null>}
4044
- */
4045
- async exportBlob(e = "png", t = 0.92, i = {}) {
4046
- return !this._state.get("hasImage") || !this._applyPendingCropBeforeExport("VanillaImageEditor.exportBlob") ? null : this._renderer.exportBlob(e, t, i);
4047
- }
4048
- _applyPendingCropBeforeExport(e) {
4049
- return !(this._state.get("crop.rect") && !this._cropManager.apply());
4050
- }
4051
- /**
4052
- * Save the image (triggers download and emits 'save' event)
4053
- */
4054
- async save() {
4055
- if (!this._state.get("hasImage")) return;
4056
- this._state.set("isSaving", !0);
4057
- let e = null;
4058
- try {
4059
- const t = await this.exportBlob("png", 0.92);
4060
- if (!t) throw new Error("Failed to export image");
4061
- const i = document.createElement("a");
4062
- e = URL.createObjectURL(t.blob), i.href = e, i.download = `edited-image-${Date.now()}.png`, document.body.appendChild(i), i.click(), document.body.removeChild(i), this.emit("save", {
4063
- blob: t.blob,
4064
- mimeType: t.blob.type || "image/png",
4065
- dimensions: { width: t.width, height: t.height },
4066
- state: this.getSerializableState()
4067
- });
4068
- } catch (t) {
4069
- this.emit("error", { error: t });
4070
- } finally {
4071
- e && URL.revokeObjectURL(e), this._state.set("isSaving", !1);
4072
- }
4073
- }
4074
- /**
4075
- * Get a canonical, serializable snapshot of the current editor state.
4076
- * Safe for persistence — returns a deep copy with no mutable references.
4077
- * @returns {Object} ImageEditorSessionState
4078
- */
4079
- getSerializableState() {
4080
- const e = this._state.get("activeFilters"), t = [];
4081
- if (e)
4082
- for (const o of e) {
4083
- const n = this._state.getFilterValues(o);
4084
- t.push({
4085
- id: o,
4086
- enabled: !0,
4087
- values: { ...n }
4088
- });
4089
- }
4090
- const i = this._state.get("crop.appliedRect"), s = this._state.get("crop.rect"), r = i || s;
4091
- return {
4092
- version: 1,
4093
- crop: {
4094
- rect: r ? { ...r } : null,
4095
- aspectRatio: this._state.get("crop.appliedAspect") || this._state.get("crop.aspect") || "free",
4096
- shape: this._state.get("crop.appliedShape") || this._state.get("crop.shape") || "free"
4097
- },
4098
- transform: {
4099
- rotation: q(this._state.get("transform.rotation") ?? 0)
4100
- },
4101
- filters: t
4102
- };
4103
- }
4104
- /**
4105
- * Close the editor (emits 'cancel' event)
4106
- */
4107
- close() {
4108
- this.emit("cancel");
4109
- }
4110
- /**
4111
- * Set zoom level
4112
- * @param {number} zoom
4113
- */
4114
- setZoom(e) {
4115
- this._renderer.setZoom(e);
4116
- }
4117
- /**
4118
- * Fit image to screen
4119
- */
4120
- fitToScreen() {
4121
- this._renderer.fitToScreen();
4122
- }
4123
- /**
4124
- * Reset all filters
4125
- */
4126
- resetAll() {
4127
- this._filterManager.resetAll();
4128
- }
4129
- rotateBy(e) {
4130
- const t = this._state.get("transform.rotation") ?? 0;
4131
- return this.setRotationAngle(Number(t) + Number(e));
4132
- }
4133
- setRotationAngle(e) {
4134
- if (!this._state.get("hasImage")) return !1;
4135
- const t = q(this._state.get("transform.rotation") ?? 0), i = q(e), s = ut(t, i), r = this._state.get("mode") === "crop";
4136
- if (Math.abs(s) > 1e-3) {
4137
- if (r && this._cropManager.disable(), !this._renderer.rotateBy(s))
4138
- return r && this._cropManager.enable(), !1;
4139
- this._filterManager.applyFilters(), this._state.set("crop.rect", null), this._state.set("crop.dirty", !1), r && this._cropManager.enable();
4140
- }
4141
- return this._state.set("transform.rotation", i), this.emit("transformChanged", { rotation: i }), !0;
4142
- }
4143
- resetRotation() {
4144
- return this.setRotationAngle(0);
4145
- }
4146
- /**
4147
- * Toggle theme
4148
- */
4149
- toggleTheme() {
4150
- const e = this._state.get("isDarkMode");
4151
- this._state.set("isDarkMode", !e);
4152
- }
4153
- /**
4154
- * Set theme
4155
- * @param {'light'|'dark'|'auto'} theme
4156
- */
4157
- setTheme(e) {
4158
- this._state.set("theme", e), this._state.detectTheme();
4159
- }
4160
- /**
4161
- * Set editor mode
4162
- * @param {'filters'|'crop'} mode
4163
- */
4164
- setMode(e) {
4165
- e === "crop" ? this._cropManager.enable() : this._cropManager.disable(), this._state.set("mode", e);
4166
- }
4167
- /**
4168
- * Get current state (for debugging)
4169
- * @returns {Object}
4170
- */
4171
- getState() {
4172
- return this._state.getAll();
4173
- }
4174
- // ==================== Background Removal ====================
4175
- /**
4176
- * Remove the background from the current image
4177
- * @param {Object} options
4178
- * @param {string} [options.tier='balanced'] - Quality tier: 'fast', 'balanced', 'best'
4179
- * @param {string} [options.model] - Explicit model name (overrides tier)
4180
- * @param {boolean} [options.alpha_matting=false] - Enable alpha matting for edge refinement
4181
- * @returns {Promise<{model: string, processMs: string}>}
4182
- */
4183
- async removeBackground(e = {}) {
4184
- var t, i;
4185
- if (!this._removeBgManager)
4186
- throw new Error("Background removal is not enabled");
4187
- if (!this._state.get("hasImage"))
4188
- throw new Error("No image loaded");
4189
- if (!this.canRemoveBackground())
4190
- throw new Error("Background removal is not available for the current editor state");
4191
- this._state.set("isProcessing", !0), this._showLoadingOverlay("Removing background...");
4192
- try {
4193
- const s = this.exportImage("png");
4194
- if (!s)
4195
- throw new Error("Failed to export image for background removal");
4196
- const r = await this._removeBgManager.preparePreviewRequest(s, e), o = this._buildPreviewBackgroundRemovalContext(r), n = await this._removeBgManager.removeBackground(r.blob, r);
4197
- return await this._isCurrentPreviewBackgroundRemovalContext(o, n) ? (await this.loadImage(n.dataUrl), this.emit("background-removed", {
4198
- model: n.model,
4199
- processMs: n.processMs
4200
- }), {
4201
- model: n.model,
4202
- processMs: n.processMs
4203
- }) : (this.emit("background-removal-stale", {
4204
- operationId: o.operationId,
4205
- sourceHash: o.sourceHash
4206
- }), { stale: !0 });
4207
- } catch (s) {
4208
- throw this.emit("error", { error: s, context: "background-removal" }), s;
4209
- } finally {
4210
- (i = (t = this._state) == null ? void 0 : t.set) == null || i.call(t, "isProcessing", !1), this._hideLoadingOverlay();
4211
- }
4212
- }
4213
- /**
4214
- * Check if background removal is available
4215
- * @returns {Promise<boolean>}
4216
- */
4217
- async isBackgroundRemovalAvailable() {
4218
- return this._removeBgManager ? this._refreshBackgroundRemovalAvailability() : !1;
4219
- }
4220
- canRemoveBackground() {
4221
- return !!this._removeBgManager && this._state.get("hasImage") === !0 && this._backgroundRemovalAvailable === !0;
4222
- }
4223
- async _refreshBackgroundRemovalAvailability() {
4224
- if (!this._removeBgManager)
4225
- return this._backgroundRemovalAvailable = !1, this._state.set("backgroundRemovalAvailable", !1), !1;
4226
- const e = await this._removeBgManager.isAvailable();
4227
- return this._destroyed ? !1 : (this._backgroundRemovalAvailable = e, this._state.set("backgroundRemovalAvailable", e), e);
4228
- }
4229
- _buildPreviewBackgroundRemovalContext(e) {
4230
- return {
4231
- loadVersion: this._loadVersion,
4232
- imageUrl: this._state.get("imageUrl") ?? null,
4233
- operationId: e.operationId,
4234
- sessionKey: e.sessionKey,
4235
- sourceHash: e.sourceHash,
4236
- targetRef: e.targetRef ?? null
4237
- };
4238
- }
4239
- async _isCurrentPreviewBackgroundRemovalContext(e, t = null) {
4240
- if (this._destroyed || this._loadVersion !== e.loadVersion || this._state.get("hasImage") !== !0 || (this._state.get("imageUrl") ?? null) !== e.imageUrl || t && (t.operationId && t.operationId !== e.operationId || t.sessionKey && t.sessionKey !== e.sessionKey || t.sourceHash && t.sourceHash !== e.sourceHash || t.targetRef && e.targetRef && t.targetRef !== e.targetRef))
4241
- return !1;
4242
- const i = this.exportImage("png");
4243
- return i ? await this._removeBgManager.computeImageDataHash(i) === e.sourceHash : !1;
4244
- }
4245
- // ==================== Loading Overlay ====================
4246
- /**
4247
- * Show the loading overlay
4248
- * @param {string} message - Message to display
4249
- */
4250
- _showLoadingOverlay(e) {
4251
- var i;
4252
- if (!this._loadingOverlay)
4253
- this._loadingOverlay = d("div", { className: "editor-loading-overlay" }, [
4254
- d("div", { className: "editor-loading-spinner" }),
4255
- d("div", { className: "editor-loading-text" }, e)
4256
- ]);
4257
- else {
4258
- const s = this._loadingOverlay.querySelector(".editor-loading-text");
4259
- s && (s.textContent = e);
4260
- }
4261
- const t = (i = this._canvasContainer) == null ? void 0 : i.parentElement;
4262
- t && !this._loadingOverlay.parentElement && t.appendChild(this._loadingOverlay);
4263
- }
4264
- /**
4265
- * Hide the loading overlay
4266
- */
4267
- _hideLoadingOverlay() {
4268
- var e;
4269
- (e = this._loadingOverlay) == null || e.remove();
4270
- }
4271
- /**
4272
- * Destroy the editor and clean up
4273
- */
4274
- destroy() {
4275
- var e, t, i, s, r, o, n, a, l, h, c, p;
4276
- this._destroyed = !0, (t = (e = this._removeBgManager) == null ? void 0 : e.cancelActiveRequests) == null || t.call(e, "editor-destroy");
4277
- for (const u of this._objectUrls)
4278
- try {
4279
- URL.revokeObjectURL(u);
4280
- } catch {
4281
- }
4282
- this._objectUrls.clear(), this._activeObjectUrl = null, (i = this._resizeObserver) == null || i.disconnect(), this._renderer.destroy(), this._cropManager.disable(), this._handleWindowResize && window.removeEventListener("resize", this._handleWindowResize), (s = this._toolbar) == null || s.destroy(), (r = this._categoryCarousel) == null || r.destroy(), (o = this._filterCarousel) == null || o.destroy(), (n = this._filterAdjustments) == null || n.destroy(), (a = this._mobileFilterDrawer) == null || a.destroy(), (l = this._mobileActiveFilters) == null || l.destroy(), (h = this._cropControls) == null || h.destroy(), (c = this._activeFiltersPanel) == null || c.destroy(), (p = this._editorEl) == null || p.remove(), this._container = null, this._state = null, this.emit("destroyed"), this.removeAllListeners();
4283
- }
4284
- }
4285
- export {
4286
- Ye as A,
4287
- Oe as B,
4288
- de as C,
4289
- pe as E,
4290
- Qe as F,
4291
- Te as P,
4292
- Xe as R,
4293
- et as T,
4294
- kt as V,
4295
- me as a,
4296
- D as b,
4297
- ce as c,
4298
- Ce as d,
4299
- d as e,
4300
- ke as f,
4301
- xe as g,
4302
- we as h,
4303
- ve as i,
4304
- ue as j,
4305
- bt as k,
4306
- yt as l,
4307
- vt as m,
4308
- wt as n,
4309
- _e as o,
4310
- xt as p,
4311
- Ct as q,
4312
- Ue as r,
4313
- Le as s,
4314
- He as t,
4315
- j as u,
4316
- gt as v,
4317
- mt as w,
4318
- dt as x,
4319
- ht as y,
4320
- _t as z
4321
- };
4322
- //# sourceMappingURL=editor-CcfSL2dI.js.map