@codingfactory/mediables-vue 2.19.2 → 2.20.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 (33) hide show
  1. package/dist/{PixiFrameExporter-CRe8z6ua.js → PixiFrameExporter-CGQJH9OE.js} +2 -2
  2. package/dist/{PixiFrameExporter-CRe8z6ua.js.map → PixiFrameExporter-CGQJH9OE.js.map} +1 -1
  3. package/dist/{PixiFrameExporter-R6iQjzVw.cjs → PixiFrameExporter-CfUbYDmD.cjs} +2 -2
  4. package/dist/{PixiFrameExporter-R6iQjzVw.cjs.map → PixiFrameExporter-CfUbYDmD.cjs.map} +1 -1
  5. package/dist/editor-v2-CFLWp7RZ.cjs +2 -0
  6. package/dist/editor-v2-CFLWp7RZ.cjs.map +1 -0
  7. package/dist/editor-v2-DjhJBaCS.js +7187 -0
  8. package/dist/editor-v2-DjhJBaCS.js.map +1 -0
  9. package/dist/filters/index.d.ts +1 -0
  10. package/dist/filters/recipeToCssFilter.d.ts +71 -0
  11. package/dist/index-Dae8SHT7.js.map +1 -1
  12. package/dist/index-QOKC8XA_.cjs.map +1 -1
  13. package/dist/index-ST4ukv22.cjs +357 -0
  14. package/dist/index-ST4ukv22.cjs.map +1 -0
  15. package/dist/index-rw6mdaKW.js +38906 -0
  16. package/dist/index-rw6mdaKW.js.map +1 -0
  17. package/dist/index.d.ts +3 -2
  18. package/dist/mediables-vanilla.cjs +1 -1
  19. package/dist/mediables-vanilla.mjs +10 -9
  20. package/dist/mediables-vue.cjs +1 -1
  21. package/dist/mediables-vue.mjs +97 -87
  22. package/dist/style.css +1 -1
  23. package/dist/utils/videoRecipeCapabilities.d.ts +15 -0
  24. package/dist/vanilla-exports.d.ts +2 -1
  25. package/package.json +1 -1
  26. package/dist/editor-BQ_nY4P6.js +0 -4302
  27. package/dist/editor-BQ_nY4P6.js.map +0 -1
  28. package/dist/editor-CUV1pIsV.cjs +0 -2
  29. package/dist/editor-CUV1pIsV.cjs.map +0 -1
  30. package/dist/index-B8LxZ37n.js +0 -41633
  31. package/dist/index-B8LxZ37n.js.map +0 -1
  32. package/dist/index-DutUeSES.cjs +0 -357
  33. package/dist/index-DutUeSES.cjs.map +0 -1
@@ -1,4302 +0,0 @@
1
- var Me = Object.defineProperty;
2
- var Fe = (f, e, t) => e in f ? Me(f, e, { enumerable: !0, configurable: !0, writable: !0, value: t }) : f[e] = t;
3
- var oe = (f, e, t) => Fe(f, typeof e != "symbol" ? e + "" : e, t);
4
- import { E as se, 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 se {
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 r, s, o, n, a, l;
30
- const e = (s = (r = this.app) == null ? void 0 : r.renderer) == null ? void 0 : s.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, r = 0;
56
- const s = () => {
57
- i || (i = !0, window.clearTimeout(r), t());
58
- };
59
- if (r = window.setTimeout(s, e), typeof window.requestAnimationFrame == "function") {
60
- window.requestAnimationFrame(() => s());
61
- return;
62
- }
63
- s();
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 r = Math.min(t / e.width, i / e.height) * 0.9;
76
- return (!Number.isFinite(r) || r <= 0) && (r = 1), r;
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: r } = this._getCssSize(), s = e.keepCenter ? { x: this.sprite.x + this.sprite.width / 2, y: this.sprite.y + this.sprite.height / 2 } : e.center ?? { x: i / 2, y: r / 2 };
84
- this.sprite.width = this.originalTexture.width * t, this.sprite.height = this.originalTexture.height * t, this.sprite.x = s.x - this.sprite.width / 2, this.sprite.y = s.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 r = ++this._mountToken, s = async () => {
112
- var u;
113
- const o = window.PIXI;
114
- if (!o)
115
- throw new Error("PIXI.js not found. Please ensure PIXI is loaded globally.");
116
- (u = o.Assets) != null && u.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 !== r || 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 = s(), 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, u;
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 r = window.PIXI, s = await this._loadImageElement(e);
151
- if (!s || !((c = this.app) != null && c.renderer) || !i() || (await this._waitForNextFrame(), !((u = this.app) != null && u.renderer) || !i())) return !1;
152
- const o = r.Texture.from(s);
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 r.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, r = () => 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 s = !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, s = !0);
184
- continue;
185
- }
186
- if (this._isSubjectLayer(n)) {
187
- const l = await this._createSubjectDisplayObjectForLayer(t, n);
188
- if (!r())
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), s = !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), s = !0;
195
- continue;
196
- }
197
- const a = await this._createDisplayObjectForLayer(t, n);
198
- if (!r())
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 s || (this.sprite.visible = !1), r() ? (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 r = this._layerDisplayObjects.get(i.id) || null;
217
- r && (r.layer = i);
218
- let s = (r == null ? void 0 : r.displayObject) || null;
219
- !s && this._isSubjectLayer(i) && ((t = this.sprite) == null ? void 0 : t.__layerId) === i.id && (s = this.sprite), s && this._applyLayerFilterInstances(s, 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 r = [];
237
- if (this._layerFilterFactory)
238
- try {
239
- r = 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
- r = [];
248
- }
249
- const s = Array.isArray(r) ? r.filter(Boolean) : [];
250
- for (const a of s)
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) : [], ...s];
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 r = await this._createImageDisplayObjectForLayer(e, t);
271
- if (r) return r;
272
- const s = this._backgroundFillForLayer(t);
273
- if ((s == null ? void 0 : s.kind) === "media") {
274
- const a = await this._textureForLayerSource(e, s.source);
275
- if (!a) return null;
276
- const l = this._createBackgroundSprite(e, a, s.fit);
277
- return l.__sourceWidth = Number((o = s.source) == null ? void 0 : o.originalWidth) || a.width || 1, l.__sourceHeight = Number((n = s.source) == null ? void 0 : n.originalHeight) || a.height || 1, this._applyLayerEffects(e, l, t), this._applyLayerFilterInstances(l, t), l;
278
- }
279
- if ((s == null ? void 0 : s.kind) === "gradient") {
280
- const a = this._createGradientTexture(e, s, 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, r) {
305
- const s = Number(e);
306
- return Number.isFinite(s) ? Math.max(t, Math.min(i, s)) : r;
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, r = {}) {
325
- var m;
326
- const s = (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(s.fontSize, 8, 400, 64), a = this._clampNumber(s.lineHeight, 0.8, 3, 1.2), l = this._clampNumber(s.letterSpacing, -100, 100, 0), h = this._normalizeTextFontWeight(s.fontWeight), c = ["normal", "italic"].includes(s.fontStyle) ? s.fontStyle : "normal", u = ["left", "center", "right"].includes(s.align) ? s.align : "center", p = r.fill || (typeof s.fill == "string" && /^#[0-9a-f]{6}$/i.test(s.fill) ? s.fill : "#ffffff"), g = {
327
- fontFamily: this._supportedTextFontFamily(s.fontFamily),
328
- fontSize: n * t,
329
- fontWeight: h,
330
- fontStyle: c,
331
- fill: p,
332
- align: u,
333
- lineHeight: n * a * t,
334
- letterSpacing: l * t,
335
- wordWrap: !0,
336
- wordWrapWidth: Math.max(1, i),
337
- breakWords: !0
338
- };
339
- return r.stroke !== !1 && o.stroke.enabled && o.stroke.width > 0 && (g.stroke = {
340
- color: o.stroke.color,
341
- width: o.stroke.width * t
342
- }), g;
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 : {}, r = t.shadow && typeof t.shadow == "object" ? t.shadow : {}, s = 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: r.enabled === !0,
361
- color: this._supportedTextColor(r.color, E.shadow.color),
362
- alpha: this._clampNumber(r.alpha, 0, 1, E.shadow.alpha),
363
- blur: this._clampNumber(r.blur, 0, 120, E.shadow.blur),
364
- distance: this._clampNumber(r.distance, 0, 200, E.shadow.distance),
365
- angle: this._normalizeDegrees(r.angle ?? E.shadow.angle)
366
- },
367
- glow: {
368
- enabled: s.enabled === !0,
369
- color: this._supportedTextColor(s.color, E.glow.color),
370
- alpha: this._clampNumber(s.alpha, 0, 1, E.glow.alpha),
371
- blur: this._clampNumber(s.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, r = e.shadow.enabled ? (e.shadow.distance + e.shadow.blur * 2) * t : 0, s = 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, r, s, 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 r = new e.Container(), s = new e.Graphics(), o = new e.Graphics(), n = new e.Graphics(), a = this._createTextPrimitive(e, t), l = this._createTextPrimitive(e, t);
408
- return r.__isTextLayerContainer = !0, r.__textObject = i, r.__textMask = s, r.__textEffectMask = o, r.__textBackdrop = n, r.__textShadow = a, r.__textGlow = l, n.visible = !1, a && (a.visible = !1), l && (l.visible = !1), i.mask = s, r.addChild(n), a && r.addChild(a), l && r.addChild(l), r.addChild(i), r.addChild(o), r.addChild(s), r;
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 s, 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 r = new e.Sprite(i);
434
- return r.__sourceWidth = Number((s = t.source) == null ? void 0 : s.originalWidth) || i.width || 1, r.__sourceHeight = Number((o = t.source) == null ? void 0 : o.originalHeight) || i.height || 1, this._applyLayerEffects(e, r, t), this._applyLayerFilterInstances(r, t), r;
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 r = await this._loadImageElement(i);
462
- return r ? e.Texture.from(r) : null;
463
- }
464
- _createBackgroundSprite(e, t, i = "cover") {
465
- if (i === "tile" && typeof e.TilingSprite == "function")
466
- try {
467
- const r = new e.TilingSprite({ texture: t, width: 1, height: 1 });
468
- return r.__isTilingBackground = !0, r;
469
- } catch {
470
- try {
471
- const s = new e.TilingSprite(t, 1, 1);
472
- return s.__isTilingBackground = !0, s;
473
- } catch {
474
- }
475
- }
476
- return new e.Sprite(t);
477
- }
478
- _createGradientTexture(e, t, i, r) {
479
- var l;
480
- if (typeof document > "u" || !((l = e == null ? void 0 : e.Texture) != null && l.from)) return null;
481
- const s = document.createElement("canvas");
482
- s.width = Math.max(1, Math.round(i)), s.height = Math.max(1, Math.round(r));
483
- const o = s.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(s.width, s.height) / 2;
492
- a = o.createRadialGradient(
493
- s.width / 2,
494
- s.height / 2,
495
- 0,
496
- s.width / 2,
497
- s.height / 2,
498
- h
499
- );
500
- } else {
501
- const h = (Number(t == null ? void 0 : t.angle) || 0) * Math.PI / 180, c = s.width / 2, u = s.height / 2, p = Math.sqrt(s.width ** 2 + s.height ** 2) / 2, g = Math.cos(h) * p, m = Math.sin(h) * p;
502
- a = o.createLinearGradient(c - g, u - m, c + g, u + 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, s.width, s.height), e.Texture.from(s);
510
- }
511
- _backgroundBlurAmount(e) {
512
- var s;
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"), r = Number((s = i == null ? void 0 : i.params) == null ? void 0 : s.amount);
514
- return Number.isFinite(r) ? Math.max(0, r) : 0;
515
- }
516
- _applyLayerEffects(e, t, i) {
517
- const r = this._backgroundBlurAmount(i);
518
- if (!(r <= 0 || typeof (e == null ? void 0 : e.BlurFilter) != "function"))
519
- try {
520
- let s;
521
- try {
522
- s = new e.BlurFilter({ strength: r, quality: 4 });
523
- } catch {
524
- s = new e.BlurFilter(r);
525
- }
526
- typeof s.blur == "number" && (s.blur = r), typeof s.strength == "number" && (s.strength = r), s.padding = Math.max(Number(s.padding) || 0, Math.ceil(r * 2)), t.filters = Array.isArray(t.filters) ? [...t.filters, s] : [s];
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, r, s, o, n, a) {
567
- if (!t) return;
568
- const l = r[s];
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, s === "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 (s === "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, r, s, 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 * r, h = a.radius * r, c = -l, u = -l, p = s + l * 2, g = o + l * 2;
595
- h > 0 && typeof t.roundRect == "function" ? t.roundRect(c, u, p, g, h) : typeof t.rect == "function" && t.rect(c, u, p, g), this._fillTextEffectGraphics(t, a.color, a.opacity);
596
- const m = this._createTextBlurFilter(e, a.blur * r);
597
- t.filters = m ? [m] : null;
598
- }
599
- _fitTextDisplayObjectToRect(e, t, i) {
600
- var m, b;
601
- const r = this._normalizeTextBox(t), s = i.x + r.x * i.width, o = i.y + r.y * i.height, n = r.width * i.width, a = r.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 = s, 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 = r, e.__textLayout = { x: s, y: o, width: n, height: a, scale: h };
603
- const u = e.__textObject || e, p = e.__textMask || null, g = e.__textEffectMask || null;
604
- if (u.text = this._textContentForLayer(t), u.style = this._textStyleForLayer(t, h, n), u.x = 0, u.y = 0, u.rotation = 0, p && typeof p.clear == "function" && typeof p.rect == "function" && typeof p.fill == "function" && (p.clear(), p.rect(0, 0, n, a).fill(16777215), u.mask = p), g && typeof g.clear == "function" && typeof g.rect == "function" && typeof g.fill == "function") {
605
- const C = this._textEffectPadding(c, h);
606
- g.clear(), g.rect(-C, -C, n + C * 2, a + C * 2).fill(16777215);
607
- }
608
- this._syncTextBackdrop(window.PIXI, e.__textBackdrop, c, h, n, a, g), this._syncTextEffectPrimitive(window.PIXI, e.__textShadow, t, c, "shadow", h, n, g), this._syncTextEffectPrimitive(window.PIXI, e.__textGlow, t, c, "glow", h, n, g);
609
- }
610
- _fitDisplayObjectToRect(e, t, i) {
611
- var c, u, p, g, m, b, C, x;
612
- const r = this._backgroundFillForLayer(t), s = (r == null ? void 0 : r.fit) || "cover";
613
- if (e.__isTilingBackground) {
614
- e.x = i.x, e.y = i.y, e.width = i.width, e.height = i.height, (u = (c = e.tilePosition) == null ? void 0 : c.set) == null || u.call(c, 0, 0), (g = (p = e.tileScale) == null ? void 0 : p.set) == null || g.call(p, 1, 1);
615
- return;
616
- }
617
- if (s === "stretch" || (r == null ? void 0 : r.kind) === "gradient" || (r == null ? void 0 : r.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 = r == null ? void 0 : r.source) == null ? void 0 : m.originalWidth) || ((b = e.texture) == null ? void 0 : b.width) || i.width), n = Number(e.__sourceHeight || ((C = r == null ? void 0 : r.source) == null ? void 0 : C.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 = s === "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, r, s;
631
- if ((e = this._layerDisplayObjects) != null && e.size) {
632
- for (const { displayObject: o } of this._layerDisplayObjects.values()) {
633
- try {
634
- (r = (i = (t = this.app) == null ? void 0 : t.stage) == null ? void 0 : i.removeChild) == null || r.call(i, o);
635
- } catch {
636
- }
637
- const n = (o == null ? void 0 : o.__ownsTexture) === !0;
638
- (s = o == null ? void 0 : o.destroy) == null || s.call(o, { children: !0, texture: n, textureSource: n });
639
- }
640
- this._layerDisplayObjects.clear(), this._renderableLayerIds = [];
641
- }
642
- }
643
- _destroyTransientDisplayObject(e) {
644
- var i, r, s, o;
645
- if (!e) return;
646
- try {
647
- (s = (r = (i = this.app) == null ? void 0 : i.stage) == null ? void 0 : r.removeChild) == null || s.call(r, 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 r = this._normalizeDegrees(i);
672
- if (r === 0)
673
- return {
674
- width: Math.max(1, Math.round(e)),
675
- height: Math.max(1, Math.round(t))
676
- };
677
- const s = r * Math.PI / 180, o = Math.abs(Math.cos(s)), n = Math.abs(Math.sin(s)), 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, C;
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, r = Math.round(this.originalTexture.width), s = Math.round(this.originalTexture.height);
698
- if (r <= 0 || s <= 0) return !1;
699
- const { width: o, height: n } = this.getRotatedBounds(r, s, t), a = t * Math.PI / 180, l = new i.Container(), h = new i.Sprite(this.originalTexture);
700
- (C = (b = h.anchor) == null ? void 0 : b.set) == null || C.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 u = this.originalTexture, p = Array.isArray(this.sprite.filters) ? [...this.sprite.filters] : null;
715
- u && u !== this.baseTexture && u.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 g = new i.Sprite(c);
717
- return p != null && p.length && (g.filters = p), this.app.stage.addChild(g), this.sprite = g, 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, r = !0, s = 0) {
732
- var F;
733
- if (!((F = this.app) != null && F.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, u = c > 0 ? 1 / c : 1, p = this._layerDocument, g = p && Array.isArray(p.layers), m = [];
742
- if (!g && 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: u });
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 C = Math.max(Math.ceil(b), 64), x = (y) => {
757
- var Q, X, L, ee;
758
- const w = Math.ceil(n + 2 * y), S = Math.ceil(a + 2 * y), k = new o.Container(), N = [], P = (_ = 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: u,
763
- sourceWidth: n,
764
- sourceHeight: a
765
- }), m.length > 0 && (v.filters = m, v.filterArea = new o.Rectangle(0, 0, n, a)), k.addChild(v), N.push(v);
766
- }, M = (_) => {
767
- var R, K, Y;
768
- const v = (R = this._layerDisplayObjects.get(_.id)) == null ? void 0 : R.displayObject, I = (v == null ? void 0 : v.texture) || (this._isSubjectLayer(_) ? this.originalTexture : null);
769
- if (!I || typeof o.Sprite != "function") return !1;
770
- const D = new o.Sprite(I);
771
- return D.__sourceWidth = Number(((K = _.source) == null ? void 0 : K.originalWidth) || (v == null ? void 0 : v.__sourceWidth) || I.width || n), D.__sourceHeight = Number(((Y = _.source) == null ? void 0 : Y.originalHeight) || (v == null ? void 0 : v.__sourceHeight) || I.height || a), this._fitDisplayObjectToRect(D, _, { x: y, y, width: n, height: a }), D.alpha = this._layerOpacity(_), !g && this._isSubjectLayer(_) && m.length > 0 && (D.filters = m, D.filterArea = new o.Rectangle(0, 0, n, a)), this._applyLayerEffects(o, D, _), this._applyLayerFilterInstances(D, _, {
772
- forExport: !0,
773
- previewToNativeScale: u,
774
- sourceWidth: n,
775
- sourceHeight: a
776
- }), k.addChild(D), N.push(D), !0;
777
- }, T = (_) => {
778
- var K, Y, pe;
779
- const v = this._backgroundFillForLayer(_), I = this._backgroundColorForLayer(_), D = { x: y, y, width: n, height: a };
780
- let R = null;
781
- if (I && typeof o.Graphics == "function")
782
- R = new o.Graphics(), R.rect(y, y, n, a).fill(I);
783
- else if ((v == null ? void 0 : v.kind) === "gradient" && typeof o.Sprite == "function") {
784
- const H = this._createGradientTexture(o, v, n, a);
785
- if (!H) return !1;
786
- R = new o.Sprite(H), R.__ownsTexture = !0, R.__sourceWidth = n, R.__sourceHeight = a, this._fitDisplayObjectToRect(R, _, D);
787
- } else if ((v == null ? void 0 : v.kind) === "media") {
788
- const H = (K = this._layerDisplayObjects.get(_.id)) == null ? void 0 : K.displayObject, te = H == null ? void 0 : H.texture;
789
- if (!te) return !1;
790
- R = this._createBackgroundSprite(o, te, v.fit), R.__sourceWidth = Number(((Y = v.source) == null ? void 0 : Y.originalWidth) || H.__sourceWidth || te.width || 1), R.__sourceHeight = Number(((pe = v.source) == null ? void 0 : pe.originalHeight) || H.__sourceHeight || te.height || 1), this._fitDisplayObjectToRect(R, _, D);
791
- }
792
- return R ? (R.alpha = this._layerOpacity(_), this._applyLayerEffects(o, R, _), this._applyLayerFilterInstances(R, _, {
793
- forExport: !0,
794
- previewToNativeScale: u,
795
- sourceWidth: n,
796
- sourceHeight: a
797
- }), k.addChild(R), N.push(R), !0) : !1;
798
- }, z = (_) => {
799
- const v = this._createTextDisplayObjectForLayer(o, _);
800
- return v ? (this._fitTextDisplayObjectToRect(v, _, { x: y, y, width: n, height: a }), v.alpha = this._layerOpacity(_), k.addChild(v), N.push(v), !0) : !1;
801
- }, V = p, q = g;
802
- if (q) {
803
- for (const _ of V.layers)
804
- if (!(!_ || _.visible === !1)) {
805
- if (this._isTextLayer(_)) {
806
- z(_);
807
- continue;
808
- }
809
- if (_.type === "image") {
810
- !M(_) && this._isSubjectLayer(_) && P(_);
811
- continue;
812
- }
813
- if (this._isSubjectLayer(_)) {
814
- M(_) || P(_);
815
- continue;
816
- }
817
- T(_);
818
- }
819
- } else
820
- P();
821
- !q && ((Q = k.children) == null ? void 0 : Q.length) === 0 && P();
822
- const G = o.RenderTexture.create({
823
- width: w,
824
- height: S,
825
- resolution: 1
826
- }), ne = ((X = k.children) == null ? void 0 : X.length) ?? 0;
827
- try {
828
- this.app.renderer.render({
829
- container: k,
830
- target: G,
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, G);
835
- return _ ? { canvas: _, width: w, height: S, margin: y, childCount: ne } : null;
836
- } finally {
837
- for (const _ of N)
838
- _.filters = null;
839
- k.removeChildren();
840
- for (const _ of N) {
841
- const v = (_ == null ? void 0 : _.__ownsTexture) === !0;
842
- _.destroy({ children: !1, texture: v, textureSource: v });
843
- }
844
- k.destroy({ children: !1 }), G.destroy(!0);
845
- }
846
- }, B = (y, w, S) => {
847
- const k = typeof y.getContext == "function" ? y.getContext("2d") : null;
848
- if (!k) return null;
849
- let N;
850
- try {
851
- N = k.getImageData(0, 0, w, S).data;
852
- } catch {
853
- return null;
854
- }
855
- const P = 1;
856
- let M = w, T = S, z = -1, V = -1;
857
- const q = 4, G = w * q;
858
- for (let L = 0; L < S; L++) {
859
- const ee = L * G;
860
- for (let _ = 0; _ < w; _++)
861
- N[ee + _ * q + 3] >= P && (_ < M && (M = _), _ > z && (z = _), L < T && (T = L), L > V && (V = L));
862
- }
863
- if (z < 0) return null;
864
- const ne = z - M + 1, Q = V - T + 1, X = M === 0 || T === 0 || z === w - 1 || V === S - 1;
865
- return { x: M, y: T, width: ne, height: Q, touchesEdge: X };
866
- };
867
- let A = null;
868
- try {
869
- let y = x(C);
870
- if (!y)
871
- return null;
872
- let w = B(y.canvas, y.width, y.height);
873
- if (!w)
874
- if (g && 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 && C < n && C < a) {
885
- const M = Math.max(C * 4, 256), T = x(M);
886
- if (T) {
887
- const z = B(T.canvas, T.width, T.height);
888
- z && !z.touchesEdge ? (y = T, w = z) : (y = T, w = z ?? w);
889
- }
890
- }
891
- let S = w.width, k = w.height;
892
- if (i > 0) {
893
- const M = Math.max(S, k);
894
- let T = i / M;
895
- r && (T = Math.min(1, T)), S = Math.max(1, Math.round(S * T)), k = Math.max(1, Math.round(k * T));
896
- }
897
- if (s > 0 && S * k > s) {
898
- const M = Math.sqrt(s / (S * k));
899
- S = Math.max(1, Math.floor(S * M)), k = Math.max(1, Math.floor(k * M));
900
- }
901
- const N = document.createElement("canvas");
902
- N.width = S, N.height = k;
903
- const P = N.getContext("2d");
904
- return P ? (P.drawImage(
905
- y.canvas,
906
- w.x,
907
- w.y,
908
- w.width,
909
- w.height,
910
- 0,
911
- 0,
912
- S,
913
- k
914
- ), A = N.toDataURL(`image/${e}`, t), this._lastExportDimensions = { width: S, height: k }, 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 r = 0;
935
- const s = (o = this.sprite) == null ? void 0 : o.filters;
936
- if (Array.isArray(s))
937
- for (const n of s) {
938
- if (!n) continue;
939
- const a = typeof n._exportPadding == "number" ? n._exportPadding : typeof n.padding == "number" ? n.padding : 0;
940
- a > r && (r = a);
941
- }
942
- return {
943
- width: t + 2 * r,
944
- height: i + 2 * r
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 r = this.exportImage(
956
- e,
957
- t,
958
- i.maxEdge ?? 0,
959
- i.dontUpscale !== !1,
960
- i.maxPixels ?? 0
961
- );
962
- if (!r) return null;
963
- const o = await (await fetch(r)).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 r;
972
- if (!((r = this.app) != null && r.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 s = {
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: s });
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>', 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="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>', 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="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>', ue = [
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
- ue.filter((f) => Array.isArray(f.registryCategories) && f.registryCategories.length > 0).map((f) => [f.id, f.registryCategories])
1084
- ), Ye = Object.fromEntries(
1085
- ue.filter((f) => Array.isArray(f.registryCategories)).flatMap((f) => f.registryCategories.map((e) => [e, f.id]))
1086
- );
1087
- function Je(f) {
1088
- return Ke[f] || [f];
1089
- }
1090
- class Qe extends se {
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, (r, s) => this.createFiltersForLayer(r, s));
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 = [], r = /* @__PURE__ */ new Set();
1127
- for (const s of t) {
1128
- const o = this._filterRegistry.getFiltersByCategory(s) || [];
1129
- for (const n of o)
1130
- r.has(n.id) || (r.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 r = e;
1155
- for (const s of i) {
1156
- if (r == null) return;
1157
- r = r[s];
1158
- }
1159
- return r;
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 r = t.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
1174
- let s = e;
1175
- for (let n = 0; n < r.length - 1; n++) {
1176
- const a = r[n];
1177
- if (!(a in s) || (s = s[a], s == null)) return;
1178
- }
1179
- const o = r[r.length - 1];
1180
- s[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 r = this.state.getFilterValues(e), s = new Set((i.controls || []).map((o) => o.id));
1193
- if (i.defaultParams)
1194
- for (const [o, n] of Object.entries(i.defaultParams))
1195
- !s.has(o) && !(o in r) && this.state.setFilterValue(e, o, n, t);
1196
- i.controls && i.controls.forEach((o) => {
1197
- o.id in r || 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 r = new Set((i.controls || []).map((s) => s.id));
1209
- if (i.defaultParams)
1210
- for (const [s, o] of Object.entries(i.defaultParams))
1211
- r.has(s) || this.state.setFilterValue(e, s, o, t);
1212
- i.controls && i.controls.forEach((s) => {
1213
- this.state.setFilterValue(e, s.id, s.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, r = void 0) {
1246
- const s = this._normalizeValue(i);
1247
- if (!this._matchesExpectedFilterLayer(r)) return !1;
1248
- const o = this.getFilterDef(e), n = o == null ? void 0 : o.controls.find((u) => u.id === t), a = !n && (t.includes("[") || t.includes("."));
1249
- if (!a && this.state.setFilterValue(e, t, s, r) === !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, s), a && typeof l.getSerializableParams == "function") {
1256
- const u = l.getSerializableParams();
1257
- for (const [p, g] of Object.entries(u))
1258
- this.state.setFilterValue(e, p, g, r);
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] = s, !0) : c !== void 0 ? (this._setDeepProp(l, h, s), !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 r = this.instances[e];
1277
- if (!r || typeof r.updateUIParam != "function") return !1;
1278
- if (r.updateUIParam(t, !0), typeof r.getSerializableParams == "function") {
1279
- const s = r.getSerializableParams();
1280
- for (const [o, n] of Object.entries(s))
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 r, s, o, n, a, l, h, c, u;
1299
- const i = (e == null ? void 0 : e.source) || ((r = e == null ? void 0 : e.fill) == null ? void 0 : r.source) || null;
1300
- return {
1301
- width: Math.round(Number(t.sourceWidth || (i == null ? void 0 : i.originalWidth) || ((o = (s = this.renderer) == null ? void 0 : s.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) || ((u = (c = this.renderer) == null ? void 0 : c.sprite) == null ? void 0 : u.height) || 0))
1303
- };
1304
- }
1305
- _paramsForFilterState(e, t, i) {
1306
- const r = t != null && t.values && typeof t.values == "object" ? t.values : {}, s = e.defaultParams ? { ...e.defaultParams } : {};
1307
- e.controls && Array.isArray(e.controls) && e.controls.forEach((n) => {
1308
- const a = n.property || n.id;
1309
- s[a] = r[n.id] ?? n.default;
1310
- });
1311
- const o = new Set((e.controls || []).map((n) => n.id));
1312
- for (const [n, a] of Object.entries(r))
1313
- o.has(n) || (s[n] = a);
1314
- return s._sourceWidth = i.width, s._sourceHeight = i.height, s;
1315
- }
1316
- createFiltersForLayer(e, t = {}) {
1317
- if (!Array.isArray(e == null ? void 0 : e.filters) || e.filters.length === 0) return [];
1318
- const i = [], r = this.state.get("activeLayerId"), s = e.id === r && 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), s && (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, u;
1342
- const e = this._layerDocument();
1343
- if (e) {
1344
- for (const g in this.instances)
1345
- delete this.instances[g];
1346
- (n = (o = this.renderer) == null ? void 0 : o.setLayerFilterFactory) == null || n.call(o, (g, m) => this.createFiltersForLayer(g, m));
1347
- const p = (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
- }), p || (u = (c = this.renderer) == null ? void 0 : c.render) == null || u.call(c);
1353
- return;
1354
- }
1355
- const t = this.renderer.sprite;
1356
- if (!t) return;
1357
- for (const p in this.instances)
1358
- delete this.instances[p];
1359
- const i = [], r = [];
1360
- this.state.get("activeFilters").forEach((p) => {
1361
- try {
1362
- const g = this.getFilterDef(p);
1363
- if (!g || !g.createFilter || typeof g.createFilter != "function")
1364
- return;
1365
- const m = this.state.getFilterValues(p), b = g.defaultParams ? { ...g.defaultParams } : {};
1366
- g.controls && Array.isArray(g.controls) && g.controls.forEach((F) => {
1367
- const J = F.property || F.id;
1368
- b[J] = m[F.id] ?? F.default;
1369
- });
1370
- const C = new Set((g.controls || []).map((F) => F.id));
1371
- for (const [F, J] of Object.entries(m))
1372
- C.has(F) || (b[F] = 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 = g.createFilter(b);
1376
- A && (i.push(A), this.instances[p] = A);
1377
- } catch {
1378
- r.push(p);
1379
- }
1380
- });
1381
- try {
1382
- t.filters = null, t.filters = i.length ? i : null, this.renderer.render(), this.emit("filtersApplied", { count: i.length, failed: r });
1383
- } catch (p) {
1384
- try {
1385
- t.filters = null, this.renderer.render();
1386
- } catch {
1387
- }
1388
- this.emit("filtersError", { error: p, failedFilters: r });
1389
- }
1390
- r.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 r in this.instances)
1400
- delete this.instances[r];
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 U = class U extends se {
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 r = parseFloat(i[0]), s = parseFloat(i[1]);
1441
- if (Number.isFinite(r) && Number.isFinite(s) && s > 0)
1442
- return r / s;
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 r = this.state.get("crop.shape"), s = { x: 0, y: 0, w: i.screen.width, h: i.screen.height };
1479
- if (e.x = Math.max(s.x, Math.min(e.x, s.x + s.w - e.width)), e.y = Math.max(s.y, Math.min(e.y, s.y + s.h - e.height)), e.width = Math.min(e.width, s.w), e.height = Math.min(e.height, s.h), r !== "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, r, s, o, n) {
1499
- if (!e || !r || !s || !o) return null;
1500
- const a = e.width > t, l = e.height > i;
1501
- if (!a && !l) return null;
1502
- const h = U.AUTO_ZOOM_PADDING;
1503
- let c = n;
1504
- if (a) {
1505
- const u = e.width / (h * r * o);
1506
- c = Math.min(c, u);
1507
- }
1508
- if (l) {
1509
- const u = e.height / (h * s * o);
1510
- c = Math.min(c, u);
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 < U.AUTO_ZOOM_THROTTLE_MS) return;
1522
- this._lastAutoZoomCheck = e;
1523
- const t = this.state.get("crop.rect"), i = this.renderer.sprite, r = this.renderer.originalTexture;
1524
- if (!t || !i || !r) return;
1525
- const s = U.calcAutoZoom(
1526
- t,
1527
- i.width,
1528
- i.height,
1529
- r.width,
1530
- r.height,
1531
- this.renderer.fitScale,
1532
- this.renderer.zoom
1533
- );
1534
- s !== null && this.renderer.setZoom(s, { 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, r = i.clientWidth, s = i.clientHeight, o = window.devicePixelRatio || 1;
1545
- (e.width !== Math.max(1, Math.floor(r * o)) || e.height !== Math.max(1, Math.floor(s * o))) && (e.width = Math.max(1, Math.floor(r * o)), e.height = Math.max(1, Math.floor(s * o)), e.style.width = r + "px", e.style.height = s + "px");
1546
- const n = e.getContext("2d");
1547
- if (!n) return;
1548
- n.setTransform(o, 0, 0, o, 0, 0), n.clearRect(0, 0, r, s), n.fillStyle = "rgba(0, 0, 0, 0.5)", n.fillRect(0, 0, r, s);
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, C = a.y + a.height / 2, x = Math.min(a.width, a.height) / 2;
1554
- n.beginPath(), n.arc(b, C, 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 u = this.HANDLE_SIZE, p = [
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
- ], g = [
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 = [...p, ...g];
1578
- for (const b of m) {
1579
- const C = this._hoverMode === b.m, x = C ? u + 4 : u;
1580
- n.beginPath(), n.rect(b.x - x / 2, b.y - x / 2, x, x), n.fillStyle = C ? "#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 r = (s, o, n, a, l) => Math.abs(s - n) <= l && Math.abs(o - a) <= l;
1593
- return r(e, t, i.x, i.y, this.HANDLE_SIZE) ? "resize-nw" : r(e, t, i.x + i.width, i.y, this.HANDLE_SIZE) ? "resize-ne" : r(e, t, i.x, i.y + i.height, this.HANDLE_SIZE) ? "resize-sw" : r(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 a, l, h, c;
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" : (a = this._hoverMode) != null && a.endsWith("nw") || (l = this._hoverMode) != null && l.endsWith("se") ? "nwse-resize" : (h = this._hoverMode) != null && h.endsWith("ne") || (c = this._hoverMode) != null && c.endsWith("sw") ? "nesw-resize" : "crosshair", this.drawOverlay();
1614
- return;
1615
- }
1616
- const r = this.state.get("crop.rect");
1617
- if (!r) return;
1618
- const s = i.x - this._dragStart.x, o = i.y - this._dragStart.y;
1619
- switch (this._dragMode) {
1620
- case "move":
1621
- r.x = this._startRect.x + s, r.y = this._startRect.y + o;
1622
- break;
1623
- case "n":
1624
- r.y = this._startRect.y + o, r.height = this._startRect.height - o;
1625
- break;
1626
- case "s":
1627
- r.height = this._startRect.height + o;
1628
- break;
1629
- case "w":
1630
- r.x = this._startRect.x + s, r.width = this._startRect.width - s;
1631
- break;
1632
- case "e":
1633
- r.width = this._startRect.width + s;
1634
- break;
1635
- case "resize-nw":
1636
- r.x = this._startRect.x + s, r.y = this._startRect.y + o, r.width = this._startRect.width - s, r.height = this._startRect.height - o;
1637
- break;
1638
- case "resize-ne":
1639
- r.y = this._startRect.y + o, r.width = this._startRect.width + s, r.height = this._startRect.height - o;
1640
- break;
1641
- case "resize-sw":
1642
- r.x = this._startRect.x + s, r.width = this._startRect.width - s, r.height = this._startRect.height + o;
1643
- break;
1644
- case "resize-se":
1645
- r.width = this._startRect.width + s, r.height = this._startRect.height + o;
1646
- break;
1647
- }
1648
- r.width = Math.max(50, r.width), r.height = Math.max(50, r.height), this.state.set("crop.rect", r), 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();
1649
- }
1650
- /**
1651
- * Handle pointer up event
1652
- */
1653
- _handlePointerUp() {
1654
- this._isDragging = !1, this._dragMode = null, this._dragStart = null, this._startRect = null;
1655
- }
1656
- /**
1657
- * Enable crop mode
1658
- */
1659
- enable() {
1660
- const e = this.renderer.app, t = this.renderer.sprite;
1661
- if (!e || !t) return;
1662
- let i = this.state.get("crop.rect");
1663
- if (!i) {
1664
- this.state.get("crop.shape") === "circle" && this.state.set("crop.aspect", "1:1");
1665
- const o = this.state.get("crop.aspect"), n = this._getAspectRatio(o);
1666
- let a, l;
1667
- if (n) {
1668
- const u = t.width * 0.9, p = t.height * 0.9;
1669
- u / p > n ? (l = p, a = l * n) : (a = u, l = a / n);
1670
- } else {
1671
- const u = Math.min(e.screen.width, e.screen.height) * 0.7;
1672
- a = u, l = u;
1673
- }
1674
- const h = t.x + (t.width - a) / 2, c = t.y + (t.height - l) / 2;
1675
- i = { x: h, y: c, width: a, height: l }, this.state.set("crop.rect", i), o !== "free" && this.constrainCropRect();
1676
- }
1677
- this.state.set("crop.dirty", !1);
1678
- const r = e.stage;
1679
- r.eventMode = "static", r.hitArea = e.screen, r.cursor = "crosshair", r.on("pointerdown", this._onPointerDown), r.on("pointermove", this._onPointerMove), r.on("pointerup", this._onPointerUp), r.on("pointerupoutside", this._onPointerUp), this.state.set("mode", "crop"), this.drawOverlay(), this.emit("enabled");
1680
- }
1681
- /**
1682
- * Disable crop mode
1683
- */
1684
- disable() {
1685
- const e = this.renderer.app;
1686
- if (!e) return;
1687
- const t = e.stage;
1688
- 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) {
1689
- const i = this._overlayCanvas.getContext("2d");
1690
- i == null || i.clearRect(0, 0, this._overlayCanvas.width, this._overlayCanvas.height);
1691
- }
1692
- this.state.set("mode", "filters"), this.emit("disabled");
1693
- }
1694
- /**
1695
- * Apply the crop
1696
- * @returns {{ texture: PIXI.Texture, preservedZoom: number }|null}
1697
- */
1698
- apply() {
1699
- const e = this.renderer.app, t = this.renderer.sprite, i = this.renderer.originalTexture, r = this.state.get("crop.rect");
1700
- if (!r || !t || !e || !i) return null;
1701
- const s = window.PIXI, o = this.renderer.zoom, n = i.width / t.width, a = i.height / t.height, l = (r.x - t.x) * n, h = (r.y - t.y) * a;
1702
- let c = Math.round(Math.max(1, r.width * n)), u = Math.round(Math.max(1, r.height * a)), p = Math.round(l), g = Math.round(h);
1703
- if (c <= 0 || u <= 0) return null;
1704
- const m = new s.Container(), b = new s.Sprite(i);
1705
- if (this.state.get("crop.shape") === "circle") {
1706
- const k = Math.round(Math.max(c, u)), N = p + c / 2, P = g + u / 2;
1707
- p = Math.round(N - k / 2), g = Math.round(P - k / 2), c = u = k;
1708
- const M = new s.Graphics();
1709
- typeof M.circle == "function" && typeof M.fill == "function" ? M.circle(c / 2, u / 2, c / 2).fill(16777215) : (M.beginFill(16777215, 1), M.drawCircle(c / 2, u / 2, c / 2), M.endFill()), b.mask = M, m.addChild(M);
1710
- }
1711
- b.x = -p, b.y = -g, m.addChild(b);
1712
- const x = s.RenderTexture.create({ width: c, height: u });
1713
- e.renderer.render({
1714
- container: m,
1715
- target: x,
1716
- clear: !0
1717
- }), m.destroy({ children: !0 });
1718
- const B = this.renderer.originalTexture;
1719
- B && B !== this.renderer.baseTexture && B.destroy(!0), this.renderer.originalTexture = x, e.stage.removeChild(t), t.destroy();
1720
- const A = new s.Sprite(x);
1721
- 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();
1722
- const F = this.state.get("crop.appliedRect"), y = F && Number.isFinite(F.x) && Number.isFinite(F.y) && Number.isFinite(F.width) && Number.isFinite(F.height) ? {
1723
- x: Math.round(F.x + p),
1724
- y: Math.round(F.y + g),
1725
- width: c,
1726
- height: u
1727
- } : { x: p, y: g, width: c, height: u }, w = this.state.get("crop.shape") || "free", S = this.state.get("crop.aspect") || "free";
1728
- return this.disable(), this.state.set("crop.appliedRect", y), this.state.set("crop.appliedShape", w), this.state.set("crop.appliedAspect", S), this.state.set("crop.dirty", !1), this.emit("applied", { width: c, height: u }), { texture: x, preservedZoom: o };
1729
- }
1730
- /**
1731
- * Apply a crop from saved texture-pixel coordinates (for state rehydration).
1732
- * Unlike apply(), this does not read from crop.rect or require the interactive overlay.
1733
- * @param {{ x: number, y: number, width: number, height: number }} pixelRect
1734
- * @param {'free'|'circle'} shape
1735
- * @returns {{ texture: Object, preservedZoom: number }|null}
1736
- */
1737
- applyFromPixelRect(e, t = "free") {
1738
- const i = this.renderer.app, r = this.renderer.sprite, s = this.renderer.originalTexture;
1739
- if (!e || !r || !i || !s) return null;
1740
- const o = window.PIXI, n = this.renderer.zoom;
1741
- 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);
1742
- if (a <= 0 || l <= 0) return null;
1743
- const u = new o.Container(), p = new o.Sprite(s);
1744
- if (t === "circle") {
1745
- const C = Math.round(Math.max(a, l)), x = h + a / 2, B = c + l / 2;
1746
- h = Math.round(x - C / 2), c = Math.round(B - C / 2), a = l = C;
1747
- const A = new o.Graphics();
1748
- 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()), p.mask = A, u.addChild(A);
1749
- }
1750
- p.x = -h, p.y = -c, u.addChild(p);
1751
- const g = o.RenderTexture.create({ width: a, height: l });
1752
- i.renderer.render({ container: u, target: g, clear: !0 }), u.destroy({ children: !0 });
1753
- const m = this.renderer.originalTexture;
1754
- m && m !== this.renderer.baseTexture && m.destroy(!0), this.renderer.originalTexture = g, i.stage.removeChild(r), r.destroy();
1755
- const b = new o.Sprite(g);
1756
- return i.stage.addChild(b), this.renderer.sprite = b, this.renderer.fitScale = this.renderer.getFitScaleFor(g), this.renderer.setZoom(n, { keepCenter: !1 }), this.renderer.applyViewTransform(), this.renderer.render(), { texture: g, preservedZoom: n };
1757
- }
1758
- /**
1759
- * Cancel crop
1760
- */
1761
- cancel() {
1762
- this.disable(), this.emit("cancelled");
1763
- }
1764
- /**
1765
- * Set crop shape
1766
- * @param {'free'|'square'|'circle'} shape
1767
- */
1768
- setShape(e) {
1769
- 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());
1770
- }
1771
- /**
1772
- * Set aspect ratio
1773
- * @param {'free'|'1:1'|'4:3'|'16:9'|'3:2'|'2:3'|string} aspect
1774
- */
1775
- setAspect(e) {
1776
- if (this.state.get("lockAspectRatio")) return;
1777
- if (this.state.get("crop.shape") !== "free") {
1778
- if (this.state.get("lockCropShape")) return;
1779
- this.state.set("crop.shape", "free");
1780
- }
1781
- this.state.set("crop.aspect", e), this.state.set("crop.dirty", !0), this.applyAspectRatio(), this.constrainCropRect(), this.drawOverlay();
1782
- }
1783
- };
1784
- /**
1785
- * Padding factor for auto-zoom: image will be ~91% of crop size (1/1.1).
1786
- * Higher values = more aggressive zoom-out, more padding around image.
1787
- */
1788
- oe(U, "AUTO_ZOOM_PADDING", 1.1), /**
1789
- * Minimum interval (ms) between auto-zoom adjustments during drag.
1790
- */
1791
- oe(U, "AUTO_ZOOM_THROTTLE_MS", 100);
1792
- let ce = U;
1793
- class Xe {
1794
- /**
1795
- * Create a new RemoveBgManager
1796
- * @param {Object} options
1797
- * @param {string} [options.endpoint] - API endpoint for uploaded preview background removal
1798
- * @param {string} [options.optionsEndpoint] - API endpoint for background-removal capability/options
1799
- * @param {string} [options.savedEndpoint] - API endpoint template for saved-media removal
1800
- * @param {string} [options.fallbackEndpoint] - Fallback endpoint if primary fails
1801
- */
1802
- constructor(e = {}) {
1803
- 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();
1804
- }
1805
- _createAbortController() {
1806
- if (typeof AbortController > "u")
1807
- return null;
1808
- const e = new AbortController();
1809
- return this._activeControllers.add(e), e;
1810
- }
1811
- _releaseAbortController(e) {
1812
- e && this._activeControllers.delete(e);
1813
- }
1814
- cancelActiveRequests(e = "cancelled") {
1815
- const t = Array.from(this._activeControllers);
1816
- this._activeControllers.clear();
1817
- for (const i of t)
1818
- i.signal.aborted || i.abort(e);
1819
- return t.length;
1820
- }
1821
- /**
1822
- * Build a stable preview request so callers can stale-guard the response.
1823
- * @param {string|Blob} imageData
1824
- * @param {Object} options
1825
- * @returns {Promise<Object>}
1826
- */
1827
- async preparePreviewRequest(e, t = {}) {
1828
- const i = typeof e == "string" ? await this._dataUrlToBlob(e) : e;
1829
- return {
1830
- ...t,
1831
- blob: i,
1832
- operationId: t.operationId || this._uuid(),
1833
- sessionKey: t.sessionKey || this._uuid(),
1834
- sourceHash: t.sourceHash || await this._sha256Hex(i),
1835
- targetRef: t.targetRef || null,
1836
- tier: t.tier || "balanced"
1837
- };
1838
- }
1839
- /**
1840
- * @param {string|Blob} imageData
1841
- * @returns {Promise<string>}
1842
- */
1843
- async computeImageDataHash(e) {
1844
- const t = typeof e == "string" ? await this._dataUrlToBlob(e) : e;
1845
- return this._sha256Hex(t);
1846
- }
1847
- /**
1848
- * Remove background from an image
1849
- * @param {string} imageData - Data URL or Blob of the image
1850
- * @param {Object} options
1851
- * @param {string} [options.tier='balanced'] - Quality tier: 'fast', 'balanced', 'best'
1852
- * @param {string} [options.model] - Explicit model name (overrides tier)
1853
- * @param {boolean} [options.alpha_matting=false] - Enable alpha matting for edge refinement
1854
- * @param {number} [options.alpha_f=10] - Alpha matting foreground threshold
1855
- * @param {number} [options.alpha_fr=15] - Alpha matting foreground radius
1856
- * @param {number} [options.alpha_erode_size=10] - Alpha matting erode size
1857
- * @returns {Promise<{dataUrl: string, model: string, processMs: string}>}
1858
- */
1859
- async removeBackground(e, t = {}) {
1860
- const i = typeof Blob < "u" && t.blob instanceof Blob ? t : await this.preparePreviewRequest(e, t), r = i.blob, s = i.operationId, o = i.sessionKey, n = i.sourceHash, a = new FormData();
1861
- a.append("image_file", r, "image.png"), a.append("operation_id", s), 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));
1862
- let l, h;
1863
- const c = this._createAbortController(), u = (p) => c ? { ...p, signal: c.signal } : p;
1864
- try {
1865
- try {
1866
- l = await fetch(this._endpoint, u({
1867
- method: "POST",
1868
- body: a,
1869
- credentials: "include"
1870
- // For Sanctum auth
1871
- }));
1872
- } catch (m) {
1873
- if (c != null && c.signal.aborted)
1874
- throw m;
1875
- h = m;
1876
- }
1877
- if ((!l || !l.ok) && this._fallbackEndpoint)
1878
- try {
1879
- l = await fetch(this._fallbackEndpoint, u({
1880
- method: "POST",
1881
- body: a
1882
- }));
1883
- } catch (m) {
1884
- if (c != null && c.signal.aborted)
1885
- throw m;
1886
- h || (h = m);
1887
- }
1888
- if (!l)
1889
- throw h || new Error("Network error: Unable to connect to background removal service");
1890
- if (!l.ok) {
1891
- let m = `Background removal failed (HTTP ${l.status})`;
1892
- try {
1893
- const b = await l.text();
1894
- b && (m += `: ${b}`);
1895
- } catch {
1896
- }
1897
- throw new Error(m);
1898
- }
1899
- const p = await l.blob();
1900
- return {
1901
- dataUrl: await this._blobToDataUrl(p),
1902
- model: l.headers.get("X-Model-Used") || "unknown",
1903
- processMs: l.headers.get("X-Process-Ms") || "0",
1904
- operationId: l.headers.get("X-Operation-Id") || s,
1905
- sessionKey: l.headers.get("X-Session-Key") || o,
1906
- sourceHash: l.headers.get("X-Source-Hash") || n,
1907
- targetRef: l.headers.get("X-Target-Ref") || i.targetRef || null
1908
- };
1909
- } finally {
1910
- this._releaseAbortController(c);
1911
- }
1912
- }
1913
- /**
1914
- * Remove the background from a persisted editor-document layer.
1915
- * @param {Object} options
1916
- * @param {string} options.targetMediaUuid
1917
- * @param {string} options.operationId
1918
- * @param {string} options.documentId
1919
- * @param {string} options.documentRevisionId
1920
- * @param {string} options.targetLayerId
1921
- * @param {string} options.expectedSourceHash
1922
- * @param {string} [options.tier]
1923
- * @returns {Promise<Object>}
1924
- */
1925
- async removeSavedMediaBackground(e) {
1926
- const t = this._savedEndpoint.replace("{media}", encodeURIComponent(e.targetMediaUuid)), i = {
1927
- operation_id: e.operationId,
1928
- document_id: e.documentId,
1929
- document_revision_id: e.documentRevisionId,
1930
- target_layer_id: e.targetLayerId,
1931
- expected_source_hash: e.expectedSourceHash,
1932
- tier: e.tier || "balanced"
1933
- };
1934
- 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));
1935
- const r = this._createAbortController();
1936
- let s;
1937
- try {
1938
- if (s = await fetch(t, {
1939
- method: "POST",
1940
- body: JSON.stringify(i),
1941
- credentials: "include",
1942
- headers: {
1943
- Accept: "application/json",
1944
- "Content-Type": "application/json"
1945
- },
1946
- ...r ? { signal: r.signal } : {}
1947
- }), !s.ok) {
1948
- let o = `Background removal failed (HTTP ${s.status})`;
1949
- try {
1950
- const n = await s.json();
1951
- n != null && n.message && (o += `: ${n.message}`);
1952
- } catch {
1953
- }
1954
- throw new Error(o);
1955
- }
1956
- return await s.json();
1957
- } finally {
1958
- this._releaseAbortController(r);
1959
- }
1960
- }
1961
- /**
1962
- * Convert a data URL to a Blob
1963
- * @param {string} dataUrl
1964
- * @returns {Promise<Blob>}
1965
- */
1966
- async _dataUrlToBlob(e) {
1967
- if (e.startsWith("data:")) {
1968
- const [i, r] = e.split(","), s = i.match(/:(.*?);/), o = s ? s[1] : "image/png", n = atob(r), a = new Uint8Array(n.length);
1969
- for (let l = 0; l < n.length; l++)
1970
- a[l] = n.charCodeAt(l);
1971
- return new Blob([a], { type: o });
1972
- }
1973
- return (await fetch(e)).blob();
1974
- }
1975
- /**
1976
- * Convert a Blob to a data URL
1977
- * @param {Blob} blob
1978
- * @returns {Promise<string>}
1979
- */
1980
- _blobToDataUrl(e) {
1981
- return new Promise((t, i) => {
1982
- const r = new FileReader();
1983
- r.onload = () => t(r.result), r.onerror = () => i(new Error("Failed to read blob as data URL")), r.readAsDataURL(e);
1984
- });
1985
- }
1986
- /**
1987
- * Check if the background removal service is available
1988
- * @returns {Promise<boolean>}
1989
- */
1990
- async isAvailable() {
1991
- var t;
1992
- const e = await this.getOptions();
1993
- return e.enabled === !0 && ((t = e.service) == null ? void 0 : t.available) === !0;
1994
- }
1995
- /**
1996
- * @returns {Promise<Object>}
1997
- */
1998
- async getOptions() {
1999
- try {
2000
- const e = await fetch(this._optionsEndpoint, {
2001
- method: "GET",
2002
- credentials: "include"
2003
- });
2004
- if (!e.ok)
2005
- return { enabled: !1, service: { available: !1 } };
2006
- const t = await e.json();
2007
- return t && typeof t == "object" ? t : { enabled: !1, service: { available: !1 } };
2008
- } catch {
2009
- if (this._fallbackEndpoint)
2010
- try {
2011
- const t = await fetch(this._fallbackEndpoint, {
2012
- method: "OPTIONS"
2013
- });
2014
- return {
2015
- enabled: t.ok,
2016
- service: { available: t.ok }
2017
- };
2018
- } catch {
2019
- }
2020
- return { enabled: !1, service: { available: !1 } };
2021
- }
2022
- }
2023
- _uuid() {
2024
- var e;
2025
- return (e = globalThis.crypto) != null && e.randomUUID ? globalThis.crypto.randomUUID() : "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (t) => {
2026
- const i = Math.floor(Math.random() * 16);
2027
- return (t === "x" ? i : i & 3 | 8).toString(16);
2028
- });
2029
- }
2030
- async _sha256Hex(e) {
2031
- const t = await e.arrayBuffer(), i = await globalThis.crypto.subtle.digest("SHA-256", t);
2032
- return [...new Uint8Array(i)].map((r) => r.toString(16).padStart(2, "0")).join("");
2033
- }
2034
- }
2035
- function d(f, e = {}, ...t) {
2036
- const i = document.createElement(f);
2037
- for (const [r, s] of Object.entries(e))
2038
- if (s != null)
2039
- if (r === "className")
2040
- i.className = s;
2041
- else if (r === "style" && typeof s == "object")
2042
- Object.assign(i.style, s);
2043
- else if (r.startsWith("on") && typeof s == "function") {
2044
- const o = r.slice(2).toLowerCase();
2045
- i.addEventListener(o, s);
2046
- } else r === "dataset" && typeof s == "object" ? Object.assign(i.dataset, s) : i.setAttribute(r, s);
2047
- for (const r of t)
2048
- typeof r == "string" ? i.appendChild(document.createTextNode(r)) : r instanceof HTMLElement && i.appendChild(r);
2049
- return i;
2050
- }
2051
- function ve({ id: f, label: e, min: t = 0, max: i = 1, step: r = 0.01, value: s = 0.5, onChange: o }) {
2052
- const n = f.includes("-") ? f.split("-").slice(1).join("-") : f, a = d("div", {
2053
- className: "slider-control slider-wrapper",
2054
- "data-control": n,
2055
- "data-testid": `slider-${n}`
2056
- }), l = d(
2057
- "div",
2058
- { className: "slider-header" },
2059
- d("label", { for: f, className: "slider-label" }, e),
2060
- d("span", { className: "slider-value", id: `${f}-value` }, ae(s))
2061
- ), h = d("input", {
2062
- type: "range",
2063
- id: f,
2064
- className: "slider-input",
2065
- min: String(t),
2066
- max: String(i),
2067
- step: String(r),
2068
- value: String(s),
2069
- onInput: (c) => {
2070
- const u = parseFloat(c.target.value), p = a.querySelector(".slider-value");
2071
- p && (p.textContent = ae(u)), o == null || o(u);
2072
- }
2073
- });
2074
- return a.appendChild(l), a.appendChild(h), a.setValue = (c) => {
2075
- h.value = String(c);
2076
- const u = a.querySelector(".slider-value");
2077
- u && (u.textContent = ae(c));
2078
- }, a;
2079
- }
2080
- function ae(f) {
2081
- return Number.isInteger(f) ? String(f) : f.toFixed(2);
2082
- }
2083
- function we({ id: f, label: e, checked: t = !1, onChange: i }) {
2084
- const r = d("div", { className: "toggle-control" }), s = d("label", { className: "toggle-label", for: f }, e), o = d("input", {
2085
- type: "checkbox",
2086
- id: f,
2087
- className: "toggle-input",
2088
- checked: t ? "checked" : void 0,
2089
- onChange: (l) => i == null ? void 0 : i(l.target.checked)
2090
- }), n = d("div", {
2091
- className: "toggle-switch",
2092
- onClick: (l) => {
2093
- l.target !== o && (o.checked = !o.checked, i == null || i(o.checked));
2094
- }
2095
- }), a = d("span", { className: "toggle-slider" });
2096
- return n.appendChild(o), n.appendChild(a), r.appendChild(s), r.appendChild(n), r.setChecked = (l) => {
2097
- o.checked = l;
2098
- }, r;
2099
- }
2100
- function xe({ id: f, label: e, value: t = "#000000", onChange: i }) {
2101
- const r = d("div", { className: "color-control" }), s = d("label", { className: "color-label", for: f }, e), o = d("input", {
2102
- type: "color",
2103
- id: f,
2104
- className: "color-input",
2105
- value: t,
2106
- onInput: (n) => i == null ? void 0 : i(n.target.value)
2107
- });
2108
- return r.appendChild(s), r.appendChild(o), r.setValue = (n) => {
2109
- o.value = n;
2110
- }, r;
2111
- }
2112
- function Ce({ id: f, label: e, value: t = "", placeholder: i = "", onCommit: r }) {
2113
- const s = d("div", {
2114
- className: "text-control",
2115
- "data-control": f,
2116
- "data-testid": `text-${f}`
2117
- }), o = d("label", { className: "text-label", for: f }, e);
2118
- let n = t;
2119
- const a = d("input", {
2120
- type: "text",
2121
- id: f,
2122
- className: "text-input",
2123
- value: t,
2124
- placeholder: i,
2125
- onKeyDown: (h) => {
2126
- h.key === "Enter" && (h.preventDefault(), l(), a.blur());
2127
- },
2128
- onBlur: () => l()
2129
- });
2130
- function l() {
2131
- const h = a.value;
2132
- h !== n && (n = h, r == null || r(h));
2133
- }
2134
- return s.appendChild(o), s.appendChild(a), s.setValue = (h) => {
2135
- const c = h == null ? "" : String(h);
2136
- a.value = c, n = c;
2137
- }, s;
2138
- }
2139
- function ke({ id: f, label: e, options: t = [], value: i, onChange: r }) {
2140
- const s = d("div", { className: "select-control" }), o = d("label", { className: "select-label", for: f }, e), n = d("select", {
2141
- id: f,
2142
- className: "select-input",
2143
- onChange: (a) => r == null ? void 0 : r(a.target.value)
2144
- });
2145
- for (const a of t) {
2146
- const l = d("option", { value: a.value }, a.label);
2147
- a.value === i && (l.selected = !0), n.appendChild(l);
2148
- }
2149
- return s.appendChild(o), s.appendChild(n), s.setValue = (a) => {
2150
- n.value = a;
2151
- }, s;
2152
- }
2153
- function j({ label: f, className: e = "", onClick: t, icon: i = null, disabled: r = !1 }) {
2154
- const s = d("button", {
2155
- type: "button",
2156
- className: `btn ${e}`.trim(),
2157
- onClick: t,
2158
- disabled: r ? "disabled" : void 0
2159
- });
2160
- if (i) {
2161
- const o = d("span", { className: "btn-icon" });
2162
- o.innerHTML = i;
2163
- const n = o.querySelector("svg");
2164
- n && n.setAttribute("aria-hidden", "true"), s.appendChild(o);
2165
- }
2166
- return f && s.appendChild(document.createTextNode(f)), s;
2167
- }
2168
- function O({ icon: f, title: e, className: t = "", onClick: i, disabled: r = !1, testId: s = null, ariaLabel: o = null }) {
2169
- const n = {
2170
- type: "button",
2171
- className: `icon-btn ${t}`.trim(),
2172
- title: e,
2173
- "aria-label": o || e,
2174
- onClick: i,
2175
- disabled: r ? "disabled" : void 0
2176
- };
2177
- s && (n.dataset = { testid: s });
2178
- const a = d("button", n);
2179
- a.innerHTML = f;
2180
- const l = a.querySelector("svg");
2181
- return l && l.setAttribute("aria-hidden", "true"), a;
2182
- }
2183
- function re({ label: f, icon: e, active: t = !1, onClick: i }) {
2184
- const r = d("button", {
2185
- type: "button",
2186
- className: `chip ${t ? "active" : ""}`.trim(),
2187
- onClick: i
2188
- });
2189
- if (e) {
2190
- const s = d("span", { className: "chip-icon" });
2191
- s.innerHTML = e;
2192
- const o = s.querySelector("svg");
2193
- o && o.setAttribute("aria-hidden", "true"), r.appendChild(s);
2194
- }
2195
- return r.appendChild(d("span", { className: "chip-label" }, f)), r.setActive = (s) => {
2196
- r.classList.toggle("active", s);
2197
- }, r;
2198
- }
2199
- class et {
2200
- constructor(e, t) {
2201
- this.state = e, this.editor = t, this.element = null, this._removeBgBtn = null, this._removeBgTierSelect = null, this._removeBgStatus = null, this._removeBgTier = "balanced", this._unsubscribers = [];
2202
- }
2203
- /**
2204
- * Create and render the toolbar
2205
- * @returns {HTMLElement}
2206
- */
2207
- render() {
2208
- this.element = d("div", { className: "editor-toolbar" });
2209
- const e = d("div", { className: "toolbar-section toolbar-left" }), t = O({
2210
- icon: Ie,
2211
- title: "Open Image",
2212
- className: "toolbar-btn",
2213
- testId: "btn-open-image",
2214
- ariaLabel: "Open image file",
2215
- onClick: () => this.editor.openFilePicker()
2216
- });
2217
- e.appendChild(t);
2218
- const i = d("div", { className: "toolbar-section toolbar-right" });
2219
- this._themeBtn = O({
2220
- icon: this.state.get("isDarkMode") ? fe : ge,
2221
- title: "Toggle Theme",
2222
- className: "toolbar-btn toolbar-btn-theme",
2223
- testId: "btn-toggle-theme",
2224
- ariaLabel: "Toggle theme",
2225
- onClick: () => this.editor.toggleTheme()
2226
- }), this._cropBtn = O({
2227
- icon: be,
2228
- title: "Crop",
2229
- className: "toolbar-btn toolbar-btn-crop",
2230
- testId: "btn-crop",
2231
- ariaLabel: "Crop image",
2232
- onClick: () => {
2233
- this.state.get("mode") === "crop" ? this.editor.setMode("filters") : this.editor.setMode("crop");
2234
- }
2235
- });
2236
- const r = d("div", { className: "remove-bg-control" });
2237
- this._removeBgTierSelect = d("select", {
2238
- className: "remove-bg-tier-select",
2239
- title: "Background removal quality",
2240
- "aria-label": "Background removal quality",
2241
- dataset: { testid: "select-remove-background-tier" },
2242
- onChange: (h) => {
2243
- this._removeBgTier = h.target.value;
2244
- }
2245
- });
2246
- for (const h of [
2247
- ["fast", "Fast"],
2248
- ["balanced", "Balanced"],
2249
- ["best", "Best"]
2250
- ]) {
2251
- const c = d("option", { value: h[0] }, h[1]);
2252
- h[0] === this._removeBgTier && (c.selected = !0), this._removeBgTierSelect.appendChild(c);
2253
- }
2254
- this._removeBgBtn = O({
2255
- icon: Ge,
2256
- title: "Remove Background",
2257
- className: "toolbar-btn toolbar-btn-remove-bg",
2258
- testId: "btn-remove-background",
2259
- ariaLabel: "Remove background",
2260
- disabled: !this._canRemoveBackground(),
2261
- onClick: async () => {
2262
- if (this._canRemoveBackground()) {
2263
- this._setRemoveBgStatus("Removing background...");
2264
- try {
2265
- await this.editor.removeBackground({ tier: this._removeBgTier }), this._setRemoveBgStatus("Background removed");
2266
- } catch {
2267
- this._setRemoveBgStatus("Background removal failed");
2268
- } finally {
2269
- this._updateRemoveBgButton();
2270
- }
2271
- }
2272
- }
2273
- }), this._removeBgStatus = d("span", {
2274
- className: "sr-only",
2275
- role: "status",
2276
- "aria-live": "polite",
2277
- "aria-atomic": "true"
2278
- }), r.appendChild(this._removeBgTierSelect), r.appendChild(this._removeBgBtn), r.appendChild(this._removeBgStatus);
2279
- const s = O({
2280
- icon: ye,
2281
- title: "Reset All",
2282
- className: "toolbar-btn",
2283
- testId: "btn-reset-all",
2284
- ariaLabel: "Reset all changes",
2285
- onClick: () => this.editor.resetAll()
2286
- }), o = d("button", {
2287
- type: "button",
2288
- className: "icon-btn toolbar-btn toolbar-btn-primary toolbar-save-labeled",
2289
- title: "Save Image",
2290
- "aria-label": "Save image",
2291
- dataset: { testid: "btn-save-edit" },
2292
- onClick: () => this.editor.save()
2293
- }), n = d("span", { className: "btn-icon" });
2294
- n.innerHTML = Oe;
2295
- const a = n.querySelector("svg");
2296
- a && a.setAttribute("aria-hidden", "true"), o.appendChild(n), o.appendChild(d("span", { className: "toolbar-save-label" }, "Save"));
2297
- const l = O({
2298
- icon: de,
2299
- title: "Close",
2300
- className: "toolbar-btn",
2301
- testId: "btn-cancel-edit",
2302
- ariaLabel: "Close editor",
2303
- onClick: () => this.editor.close()
2304
- });
2305
- return i.appendChild(this._themeBtn), i.appendChild(this._cropBtn), i.appendChild(r), i.appendChild(s), i.appendChild(o), i.appendChild(l), this.element.appendChild(e), this.element.appendChild(i), this._subscribeToState(), this.element;
2306
- }
2307
- /**
2308
- * Subscribe to state changes
2309
- */
2310
- _subscribeToState() {
2311
- const e = this.state.on("change:isDarkMode", ({ value: l }) => {
2312
- this._themeBtn.innerHTML = l ? fe : ge;
2313
- });
2314
- this._unsubscribers.push(e);
2315
- const t = this.state.on("change:mode", ({ value: l }) => {
2316
- this._cropBtn && this._cropBtn.classList.toggle("toolbar-btn-primary", l === "crop");
2317
- });
2318
- this._unsubscribers.push(t);
2319
- const i = this.state.on("change:hasImage", () => this._updateRemoveBgButton());
2320
- this._unsubscribers.push(i);
2321
- const r = this.state.on("change:isProcessing", () => this._updateRemoveBgButton());
2322
- this._unsubscribers.push(r);
2323
- const s = this.state.on("change:backgroundRemovalAvailable", () => this._updateRemoveBgButton());
2324
- this._unsubscribers.push(s);
2325
- const o = this.state.on("change:editorDocument", () => this._updateRemoveBgButton());
2326
- this._unsubscribers.push(o);
2327
- const n = this.state.on("change:layers", () => this._updateRemoveBgButton());
2328
- this._unsubscribers.push(n);
2329
- const a = this.state.on("change:activeLayerId", () => this._updateRemoveBgButton());
2330
- this._unsubscribers.push(a);
2331
- }
2332
- _canRemoveBackground() {
2333
- return typeof this.editor.canRemoveBackground == "function" ? this.editor.canRemoveBackground() : !!this.state.get("hasImage") && typeof this.editor.removeBackground == "function";
2334
- }
2335
- _updateRemoveBgButton() {
2336
- if (!this._removeBgBtn) return;
2337
- const e = this.state.get("isProcessing") === !0, t = !this._canRemoveBackground() || e;
2338
- 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"));
2339
- }
2340
- _setRemoveBgStatus(e) {
2341
- this._removeBgStatus && (this._removeBgStatus.textContent = e);
2342
- }
2343
- /**
2344
- * Clean up
2345
- */
2346
- destroy() {
2347
- var e;
2348
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], (e = this.element) == null || e.remove(), this.element = null;
2349
- }
2350
- }
2351
- const ie = ue;
2352
- class tt {
2353
- constructor(e, t) {
2354
- this.state = e, this.editor = t, this.element = null, this._chips = /* @__PURE__ */ new Map(), this._scrollIndex = 0, this._unsubscribers = [];
2355
- }
2356
- /**
2357
- * Create and render the category carousel
2358
- * @param {Function} onSelect - Callback when category is selected
2359
- * @returns {HTMLElement}
2360
- */
2361
- render(e) {
2362
- this._onSelect = e, this.element = d("div", {
2363
- className: "category-carousel-container",
2364
- "data-testid": "category-carousel"
2365
- }), this._leftBtn = O({
2366
- icon: _e,
2367
- title: "Previous categories",
2368
- className: "carousel-nav carousel-nav-left",
2369
- onClick: () => this._scrollLeft()
2370
- }), this._carousel = d("div", { className: "category-carousel" }), ie.forEach((i) => {
2371
- const r = re({
2372
- label: i.name,
2373
- icon: i.icon,
2374
- active: this.state.get("selectedCategory") === i.id,
2375
- onClick: () => this._selectCategory(i.id)
2376
- });
2377
- r.dataset.categoryId = i.id, r.dataset.category = i.id, r.dataset.testid = `category-${i.id}`, this._chips.set(i.id, r), this._carousel.appendChild(r);
2378
- }), this._rightBtn = O({
2379
- icon: me,
2380
- title: "Next categories",
2381
- className: "carousel-nav carousel-nav-right",
2382
- onClick: () => this._scrollRight()
2383
- }), this._pagination = d("div", {
2384
- className: "carousel-pagination",
2385
- role: "tablist",
2386
- "aria-label": "Category pages"
2387
- });
2388
- const t = Math.ceil(ie.length / 3);
2389
- for (let i = 0; i < t; i++) {
2390
- const r = d("button", {
2391
- type: "button",
2392
- className: `pagination-dot ${i === 0 ? "active" : ""}`,
2393
- role: "tab",
2394
- "aria-label": `Page ${i + 1} of ${t}`,
2395
- "aria-selected": i === 0 ? "true" : "false",
2396
- onClick: () => this._scrollToPage(i)
2397
- });
2398
- this._pagination.appendChild(r);
2399
- }
2400
- 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;
2401
- }
2402
- /**
2403
- * Subscribe to state changes
2404
- */
2405
- _subscribeToState() {
2406
- const e = this.state.on("change:selectedCategory", ({ value: t }) => {
2407
- this._chips.forEach((i, r) => {
2408
- i.setActive(r === t);
2409
- });
2410
- });
2411
- this._unsubscribers.push(e);
2412
- }
2413
- /**
2414
- * Select a category
2415
- * @param {string} categoryId
2416
- */
2417
- _selectCategory(e) {
2418
- var r;
2419
- this.state.set("selectedCategory", e);
2420
- const t = this.state.get("mode");
2421
- e === "crop" ? t !== "crop" && this.editor.setMode("crop") : t !== "filters" && this.editor.setMode("filters");
2422
- const i = this._chips.get(e);
2423
- i && this._carousel && i.scrollIntoView({ behavior: "smooth", inline: "center", block: "nearest" }), (r = this._onSelect) == null || r.call(this, e);
2424
- }
2425
- /**
2426
- * Scroll carousel left
2427
- */
2428
- _scrollLeft() {
2429
- this._scrollIndex > 0 && (this._scrollIndex--, this._scrollCarousel());
2430
- }
2431
- /**
2432
- * Scroll carousel right
2433
- */
2434
- _scrollRight() {
2435
- this._scrollIndex < ie.length - 3 && (this._scrollIndex++, this._scrollCarousel());
2436
- }
2437
- /**
2438
- * Scroll to a specific page
2439
- * @param {number} pageIndex
2440
- */
2441
- _scrollToPage(e) {
2442
- this._scrollIndex = e * 3, this._scrollCarousel();
2443
- }
2444
- /**
2445
- * Animate carousel scroll
2446
- */
2447
- _scrollCarousel() {
2448
- if (this._carousel) {
2449
- const e = this._carousel.querySelector(".chip"), t = (e == null ? void 0 : e.clientWidth) || 100;
2450
- this._carousel.scrollTo({
2451
- left: this._scrollIndex * (t + 8),
2452
- // 8px gap
2453
- behavior: "smooth"
2454
- });
2455
- }
2456
- this._updateNavButtons(), this._updatePagination();
2457
- }
2458
- /**
2459
- * Update navigation button states
2460
- */
2461
- _updateNavButtons() {
2462
- this._leftBtn && (this._leftBtn.disabled = this._scrollIndex === 0), this._rightBtn && (this._rightBtn.disabled = this._scrollIndex >= ie.length - 3);
2463
- }
2464
- /**
2465
- * Update pagination dot states
2466
- */
2467
- _updatePagination() {
2468
- if (this._pagination) {
2469
- const e = this._pagination.querySelectorAll(".pagination-dot"), t = Math.floor(this._scrollIndex / 3);
2470
- e.forEach((i, r) => {
2471
- const s = r === t;
2472
- i.classList.toggle("active", s), i.setAttribute("aria-selected", s ? "true" : "false");
2473
- });
2474
- }
2475
- }
2476
- /**
2477
- * Set the selected category
2478
- * @param {string} categoryId
2479
- */
2480
- setSelected(e) {
2481
- this._selectCategory(e);
2482
- }
2483
- /**
2484
- * Clean up
2485
- */
2486
- destroy() {
2487
- var e;
2488
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._chips.clear(), (e = this.element) == null || e.remove(), this.element = null;
2489
- }
2490
- }
2491
- class it {
2492
- constructor(e, t) {
2493
- 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;
2494
- }
2495
- /**
2496
- * Create and render the filter carousel
2497
- * @param {Object} options - { onToggle, onSelect }
2498
- * @returns {HTMLElement}
2499
- */
2500
- render({ onToggle: e, onSelect: t }) {
2501
- return this._onToggle = e, this._onSelect = t, this.element = d("div", { className: "filter-carousel-container" }), this._leftBtn = O({
2502
- icon: _e,
2503
- title: "Previous filters",
2504
- className: "carousel-nav carousel-nav-left",
2505
- onClick: () => this._scrollLeft()
2506
- }), this._carousel = d("div", { className: "filter-carousel" }), this._rightBtn = O({
2507
- icon: me,
2508
- title: "Next filters",
2509
- className: "carousel-nav carousel-nav-right",
2510
- onClick: () => this._scrollRight()
2511
- }), this.element.appendChild(this._leftBtn), this.element.appendChild(this._carousel), this.element.appendChild(this._rightBtn), this._subscribeToState(), this._renderFilters(), this.element;
2512
- }
2513
- /**
2514
- * Subscribe to state changes
2515
- */
2516
- _subscribeToState() {
2517
- const e = this.state.on("change:selectedCategory", () => {
2518
- this._renderFilters();
2519
- });
2520
- this._unsubscribers.push(e);
2521
- const t = this.state.on("change:activeFilters", () => {
2522
- this._updateActiveStates();
2523
- });
2524
- this._unsubscribers.push(t);
2525
- const i = this.state.on("change:selectedFilter", () => {
2526
- this._updateSelectedState();
2527
- });
2528
- this._unsubscribers.push(i);
2529
- }
2530
- /**
2531
- * Render filters for current category
2532
- */
2533
- _renderFilters() {
2534
- this._carousel.innerHTML = "", this._filterCards.clear(), this._scrollIndex = 0;
2535
- const e = this.state.get("selectedCategory");
2536
- if (e === "crop") return;
2537
- const t = this.filterManager.getFiltersByCategory(e), i = this.state.get("activeFilters"), r = this.state.get("selectedFilter");
2538
- t.forEach((s) => {
2539
- const o = this._createFilterCard(s, {
2540
- isActive: i.has(s.id),
2541
- isSelected: r === s.id
2542
- });
2543
- this._filterCards.set(s.id, o), this._carousel.appendChild(o);
2544
- }), this._updateNavButtons();
2545
- }
2546
- /**
2547
- * Create a filter card
2548
- * @param {Object} filter
2549
- * @param {Object} options
2550
- * @returns {HTMLElement}
2551
- */
2552
- _createFilterCard(e, { isActive: t, isSelected: i }) {
2553
- const r = d("div", {
2554
- className: `filter-card ${t ? "active" : ""} ${i ? "selected" : ""}`,
2555
- "data-filter": e.id,
2556
- "data-testid": `filter-${e.id}`,
2557
- onClick: () => this._handleCardClick(e.id)
2558
- }), s = d("div", { className: "filter-preview" }), o = d("span", { className: "filter-preview-text" }, e.name.charAt(0));
2559
- s.appendChild(o);
2560
- const n = d("span", { className: "filter-name", title: e.name }, e.name), a = d("button", {
2561
- className: `filter-toggle ${t ? "active" : ""}`,
2562
- onClick: (l) => {
2563
- var u;
2564
- l.stopPropagation();
2565
- const c = !this.state.get("activeFilters").has(e.id);
2566
- this._handleToggle(e.id, c), c && (this.state.set("selectedFilter", e.id), (u = this._onSelect) == null || u.call(this, e.id));
2567
- }
2568
- });
2569
- return a.innerHTML = t ? he : "", r.appendChild(s), r.appendChild(n), r.appendChild(a), r._toggle = a, r._isActive = t, r;
2570
- }
2571
- /**
2572
- * Handle filter card click (select)
2573
- * @param {string} filterId
2574
- */
2575
- _handleCardClick(e) {
2576
- var i;
2577
- this.state.get("activeFilters").has(e) || this._handleToggle(e, !0), this.state.set("selectedFilter", e), (i = this._onSelect) == null || i.call(this, e);
2578
- }
2579
- /**
2580
- * Handle filter toggle
2581
- * @param {string} filterId
2582
- * @param {boolean} enabled
2583
- */
2584
- _handleToggle(e, t) {
2585
- var i;
2586
- (i = this._onToggle) == null || i.call(this, e, t);
2587
- }
2588
- /**
2589
- * Update active states based on state
2590
- */
2591
- _updateActiveStates() {
2592
- const e = this.state.get("activeFilters");
2593
- this._filterCards.forEach((t, i) => {
2594
- const r = e.has(i);
2595
- t.classList.toggle("active", r), t._toggle.classList.toggle("active", r), t._toggle.innerHTML = r ? he : "", t._isActive = r;
2596
- });
2597
- }
2598
- /**
2599
- * Update selected state based on state
2600
- */
2601
- _updateSelectedState() {
2602
- const e = this.state.get("selectedFilter");
2603
- this._filterCards.forEach((t, i) => {
2604
- t.classList.toggle("selected", i === e);
2605
- });
2606
- }
2607
- /**
2608
- * Scroll carousel left
2609
- */
2610
- _scrollLeft() {
2611
- this._scrollIndex > 0 && (this._scrollIndex--, this._scrollCarousel());
2612
- }
2613
- /**
2614
- * Scroll carousel right
2615
- */
2616
- _scrollRight() {
2617
- const e = Math.max(0, this._filterCards.size - 3);
2618
- this._scrollIndex < e && (this._scrollIndex++, this._scrollCarousel());
2619
- }
2620
- /**
2621
- * Animate carousel scroll
2622
- */
2623
- _scrollCarousel() {
2624
- if (this._carousel) {
2625
- const e = this._carousel.querySelector(".filter-card"), t = (e == null ? void 0 : e.clientWidth) || 120;
2626
- this._carousel.scrollTo({
2627
- left: this._scrollIndex * (t + 8),
2628
- behavior: "smooth"
2629
- });
2630
- }
2631
- this._updateNavButtons();
2632
- }
2633
- /**
2634
- * Update navigation button states
2635
- */
2636
- _updateNavButtons() {
2637
- const e = Math.max(0, this._filterCards.size - 3);
2638
- this._leftBtn && (this._leftBtn.disabled = this._scrollIndex === 0), this._rightBtn && (this._rightBtn.disabled = this._scrollIndex >= e);
2639
- }
2640
- /**
2641
- * Clean up
2642
- */
2643
- destroy() {
2644
- var e;
2645
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._filterCards.clear(), (e = this.element) == null || e.remove(), this.element = null;
2646
- }
2647
- }
2648
- class rt {
2649
- constructor(e, t) {
2650
- 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 = [];
2651
- }
2652
- /**
2653
- * Create and render the filter adjustments panel
2654
- * @param {Object} options - { onChange, onReset, onAction }
2655
- * @returns {HTMLElement}
2656
- */
2657
- render({ onChange: e, onReset: t, onAction: i }) {
2658
- return this._onChange = e, this._onReset = t, this._onAction = i, this.element = d("div", { className: "filter-adjustments" }), this._subscribeToState(), this._renderControls(), this.element;
2659
- }
2660
- /**
2661
- * Subscribe to state changes
2662
- */
2663
- _subscribeToState() {
2664
- const e = this.state.on("change:selectedFilter", () => {
2665
- this._renderControls();
2666
- });
2667
- this._unsubscribers.push(e);
2668
- const t = this.state.on("change:filterValues", () => {
2669
- this._updateValues();
2670
- });
2671
- this._unsubscribers.push(t);
2672
- }
2673
- /**
2674
- * Render controls for the selected filter.
2675
- *
2676
- * Renders both the static controls from def.controls and any dynamic
2677
- * controls the filter instance exposes via getDynamicControls() (e.g.
2678
- * per-stop color/offset/alpha rows for Color Gradient).
2679
- */
2680
- _renderControls() {
2681
- this.element.innerHTML = "", this._controls.clear();
2682
- const e = this.state.get("selectedFilter");
2683
- if (!e) {
2684
- this.element.appendChild(
2685
- d("div", { className: "no-filter-selected" }, "Select a filter to adjust")
2686
- );
2687
- return;
2688
- }
2689
- const t = this.filterManager.getFilterDef(e);
2690
- if (!t) {
2691
- this.element.appendChild(
2692
- d("div", { className: "no-filter-selected" }, "Filter not found")
2693
- );
2694
- return;
2695
- }
2696
- const i = d("div", { className: "adjustments-header" });
2697
- i.appendChild(d("span", { className: "adjustments-title" }, t.name)), i.appendChild(j({
2698
- label: "Reset",
2699
- className: "btn-text",
2700
- onClick: () => this._handleReset(e)
2701
- })), this.element.appendChild(i);
2702
- const r = d("div", { className: "adjustments-grid" }), s = this.state.getFilterValues(e);
2703
- (t.controls || []).forEach((n) => {
2704
- const a = s[n.id] ?? n.default, l = this._createControl(e, n, a);
2705
- l && (this._controls.set(n.id, l), r.appendChild(l));
2706
- });
2707
- const o = this.filterManager.getInstance(e);
2708
- if (o && typeof o.getDynamicControls == "function") {
2709
- let n = [];
2710
- try {
2711
- n = o.getDynamicControls() || [];
2712
- } catch {
2713
- }
2714
- n.forEach((a) => {
2715
- const l = a.property || a.id, h = s[l] ?? a.default, c = this._createControl(e, a, h);
2716
- c && (this._controls.set(a.id, c), r.appendChild(c));
2717
- });
2718
- }
2719
- this.element.appendChild(r);
2720
- }
2721
- /**
2722
- * Create a control element based on type.
2723
- *
2724
- * The update key passed to _handleChange is `ctl.property || ctl.id`.
2725
- * Dynamic controls (e.g. from getDynamicControls) use their property
2726
- * path (e.g. 'colorStops[0].color') so FilterManager.updateValue can
2727
- * route the write correctly. Static controls without a property fall
2728
- * back to the control id, matching the legacy contract.
2729
- *
2730
- * @param {string} filterId
2731
- * @param {Object} ctl - Control definition
2732
- * @param {*} value - Current value
2733
- * @returns {HTMLElement|null}
2734
- */
2735
- _createControl(e, t, i) {
2736
- if (t.hidden) return null;
2737
- const r = t.label || t.id, s = this._normalizeControlType(t.type), o = t.property || t.id;
2738
- switch (s) {
2739
- case "slider":
2740
- return ve({
2741
- id: `${e}-${t.id}`,
2742
- label: r,
2743
- min: t.min ?? 0,
2744
- max: t.max ?? 1,
2745
- step: t.step ?? 0.01,
2746
- value: typeof i == "number" ? i : t.default ?? 0,
2747
- onChange: (l) => this._handleChange(e, o, l)
2748
- });
2749
- case "toggle":
2750
- return we({
2751
- id: `${e}-${t.id}`,
2752
- label: r,
2753
- checked: !!i,
2754
- onChange: (l) => this._handleChange(e, o, l)
2755
- });
2756
- case "color":
2757
- return xe({
2758
- id: `${e}-${t.id}`,
2759
- label: r,
2760
- value: typeof i == "string" && i.startsWith("#") ? i : t.default || "#000000",
2761
- onChange: (l) => this._handleChange(e, o, l)
2762
- });
2763
- case "select":
2764
- const n = this._normalizeOptions(t.options);
2765
- return ke({
2766
- id: `${e}-${t.id}`,
2767
- label: r,
2768
- options: n,
2769
- value: i ?? t.default,
2770
- onChange: (l) => this._handleChange(e, o, l)
2771
- });
2772
- case "text":
2773
- return Ce({
2774
- id: `${e}-${t.id}`,
2775
- label: r,
2776
- value: typeof i == "string" ? i : t.default ?? "",
2777
- placeholder: t.placeholder || "",
2778
- onCommit: (l) => this._handleChange(e, o, l)
2779
- });
2780
- case "button":
2781
- const a = d("div", { className: "button-control" });
2782
- return a.appendChild(j({
2783
- label: r,
2784
- className: "btn-secondary",
2785
- onClick: () => this._handleAction(e, t.action || t.id)
2786
- })), a;
2787
- default:
2788
- return null;
2789
- }
2790
- }
2791
- /**
2792
- * Normalize control type (handle aliases)
2793
- * @param {string} type
2794
- * @returns {string}
2795
- */
2796
- _normalizeControlType(e) {
2797
- return {
2798
- slider: "slider",
2799
- range: "slider",
2800
- toggle: "toggle",
2801
- checkbox: "toggle",
2802
- color: "color",
2803
- select: "select",
2804
- dropdown: "select",
2805
- button: "button",
2806
- text: "text"
2807
- }[e] || e;
2808
- }
2809
- /**
2810
- * Normalize options array format
2811
- * @param {Array} options
2812
- * @returns {Array}
2813
- */
2814
- _normalizeOptions(e) {
2815
- 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) });
2816
- }
2817
- /**
2818
- * Handle control value change
2819
- * @param {string} filterId
2820
- * @param {string} controlId
2821
- * @param {*} value
2822
- */
2823
- _handleChange(e, t, i) {
2824
- var r;
2825
- (r = this._onChange) == null || r.call(this, e, t, i);
2826
- }
2827
- /**
2828
- * Handle reset button
2829
- * @param {string} filterId
2830
- */
2831
- _handleReset(e) {
2832
- var t;
2833
- (t = this._onReset) == null || t.call(this, e), this._renderControls();
2834
- }
2835
- /**
2836
- * Handle button action. Dynamic controls (e.g. color stop rows) depend on
2837
- * filter state that can change as a result of the action (add/remove stop),
2838
- * so re-render after the action completes.
2839
- *
2840
- * @param {string} filterId
2841
- * @param {string} action
2842
- */
2843
- _handleAction(e, t) {
2844
- var i;
2845
- (i = this._onAction) == null || i.call(this, e, t), this._renderControls();
2846
- }
2847
- /**
2848
- * Update control values from state
2849
- */
2850
- _updateValues() {
2851
- const e = this.state.get("selectedFilter");
2852
- if (!e) return;
2853
- const t = this.state.getFilterValues(e), i = this.filterManager.getFilterDef(e);
2854
- i && i.controls.forEach((r) => {
2855
- const s = this._controls.get(r.id);
2856
- if (s && typeof s.setValue == "function") {
2857
- const o = t[r.id] ?? r.default;
2858
- s.setValue(o);
2859
- }
2860
- });
2861
- }
2862
- /**
2863
- * Show panel
2864
- */
2865
- show() {
2866
- this.element && (this.element.style.display = "");
2867
- }
2868
- /**
2869
- * Hide panel
2870
- */
2871
- hide() {
2872
- this.element && (this.element.style.display = "none");
2873
- }
2874
- /**
2875
- * Clean up
2876
- */
2877
- destroy() {
2878
- var e;
2879
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._controls.clear(), (e = this.element) == null || e.remove(), this.element = null;
2880
- }
2881
- }
2882
- class st {
2883
- constructor(e, t) {
2884
- 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;
2885
- }
2886
- /**
2887
- * Build the drawer DOM and append to container (should be the controls section)
2888
- * @param {HTMLElement} container - Parent element (controls-section)
2889
- * @param {Object} callbacks - { onChange, onReset, onRemove, onAction }
2890
- */
2891
- build(e, { onChange: t, onReset: i, onRemove: r, onAction: s }) {
2892
- this._onChange = t, this._onReset = i, this._onRemove = r, this._onAction = s, this._container = e, this._drawer = d("div", { className: "mobile-filter-drawer" });
2893
- const o = d("div", { className: "drawer-header" });
2894
- this._titleEl = d("span", { className: "drawer-title" }, "Filter");
2895
- const n = d("div", { className: "drawer-header-actions" }), a = j({
2896
- label: "Reset",
2897
- className: "btn-text",
2898
- onClick: () => {
2899
- var c;
2900
- this._currentFilterId && ((c = this._onReset) == null || c.call(this, this._currentFilterId), this._renderControls());
2901
- }
2902
- }), l = j({
2903
- label: "Remove",
2904
- className: "btn-text btn-danger",
2905
- onClick: () => {
2906
- var c;
2907
- this._currentFilterId && ((c = this._onRemove) == null || c.call(this, this._currentFilterId), this.close());
2908
- }
2909
- }), h = O({
2910
- icon: de,
2911
- title: "Close",
2912
- className: "btn-icon-sm",
2913
- ariaLabel: "Close drawer",
2914
- onClick: () => this.close()
2915
- });
2916
- 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);
2917
- }
2918
- /**
2919
- * Open the drawer for a specific filter
2920
- * @param {string} filterId
2921
- */
2922
- open(e) {
2923
- this._isOpen && this._currentFilterId === e || (this._currentFilterId = e, this._renderControls(), this._isOpen || (this._isOpen = !0, this._sizeToContainer(), this._animateOpen()));
2924
- }
2925
- /**
2926
- * Close the drawer
2927
- */
2928
- close() {
2929
- this._isOpen && (this._isOpen = !1, this._animateClose(), this._currentFilterId = null);
2930
- }
2931
- /**
2932
- * Check if drawer is open
2933
- * @returns {boolean}
2934
- */
2935
- get isOpen() {
2936
- return this._isOpen;
2937
- }
2938
- /**
2939
- * Size the drawer to match the container (controls section) height
2940
- */
2941
- _sizeToContainer() {
2942
- if (!this._container || !this._drawer) return;
2943
- const e = this._container.offsetHeight;
2944
- e > 0 && (this._drawer.style.height = e + "px");
2945
- }
2946
- /**
2947
- * Animate drawer open (CSS transition)
2948
- */
2949
- _animateOpen() {
2950
- 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)";
2951
- }
2952
- /**
2953
- * Animate drawer close (CSS transition)
2954
- */
2955
- _animateClose() {
2956
- this._drawer.style.transition = "transform 0.2s cubic-bezier(0.32, 0, 0.67, 0)", this._drawer.style.transform = "translateY(100%)";
2957
- }
2958
- /**
2959
- * Render controls for the current filter.
2960
- * Renders both static (def.controls) and dynamic (instance.getDynamicControls)
2961
- * controls so filters like Color Gradient show per-stop rows.
2962
- */
2963
- _renderControls() {
2964
- if (this._body.innerHTML = "", this._controls.clear(), !this._currentFilterId) return;
2965
- const e = this.filterManager.getFilterDef(this._currentFilterId);
2966
- if (!e) return;
2967
- this._titleEl.textContent = e.name;
2968
- const t = d("div", { className: "adjustments-grid" }), i = this.state.getFilterValues(this._currentFilterId);
2969
- (e.controls || []).forEach((s) => {
2970
- if (s.hidden) return;
2971
- const o = this._createControl(this._currentFilterId, s, i[s.id] ?? s.default);
2972
- o && (this._controls.set(s.id, o), t.appendChild(o));
2973
- });
2974
- const r = this.filterManager.getInstance(this._currentFilterId);
2975
- if (r && typeof r.getDynamicControls == "function") {
2976
- let s = [];
2977
- try {
2978
- s = r.getDynamicControls() || [];
2979
- } catch {
2980
- }
2981
- s.forEach((o) => {
2982
- const n = o.property || o.id, a = i[n] ?? o.default, l = this._createControl(this._currentFilterId, o, a);
2983
- l && (this._controls.set(o.id, l), t.appendChild(l));
2984
- });
2985
- }
2986
- this._body.appendChild(t);
2987
- }
2988
- /**
2989
- * Create a control element based on type. See FilterAdjustments._createControl
2990
- * for the update-key contract (property path for dynamic controls, id for static).
2991
- * @param {string} filterId
2992
- * @param {Object} ctl
2993
- * @param {*} value
2994
- * @returns {HTMLElement|null}
2995
- */
2996
- _createControl(e, t, i) {
2997
- if (t.hidden) return null;
2998
- const r = t.label || t.id, s = this._normalizeControlType(t.type), o = t.property || t.id;
2999
- switch (s) {
3000
- case "slider":
3001
- return ve({
3002
- id: `drawer-${e}-${t.id}`,
3003
- label: r,
3004
- min: t.min ?? 0,
3005
- max: t.max ?? 1,
3006
- step: t.step ?? 0.01,
3007
- value: typeof i == "number" ? i : t.default ?? 0,
3008
- onChange: (n) => {
3009
- var a;
3010
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3011
- }
3012
- });
3013
- case "toggle":
3014
- return we({
3015
- id: `drawer-${e}-${t.id}`,
3016
- label: r,
3017
- checked: !!i,
3018
- onChange: (n) => {
3019
- var a;
3020
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3021
- }
3022
- });
3023
- case "color":
3024
- return xe({
3025
- id: `drawer-${e}-${t.id}`,
3026
- label: r,
3027
- value: typeof i == "string" && i.startsWith("#") ? i : t.default || "#000000",
3028
- onChange: (n) => {
3029
- var a;
3030
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3031
- }
3032
- });
3033
- case "select": {
3034
- const n = this._normalizeOptions(t.options);
3035
- return ke({
3036
- id: `drawer-${e}-${t.id}`,
3037
- label: r,
3038
- options: n,
3039
- value: i ?? t.default,
3040
- onChange: (a) => {
3041
- var l;
3042
- return (l = this._onChange) == null ? void 0 : l.call(this, e, o, a);
3043
- }
3044
- });
3045
- }
3046
- case "text":
3047
- return Ce({
3048
- id: `drawer-${e}-${t.id}`,
3049
- label: r,
3050
- value: typeof i == "string" ? i : t.default ?? "",
3051
- placeholder: t.placeholder || "",
3052
- onCommit: (n) => {
3053
- var a;
3054
- return (a = this._onChange) == null ? void 0 : a.call(this, e, o, n);
3055
- }
3056
- });
3057
- case "button": {
3058
- const n = d("div", { className: "button-control" });
3059
- return n.appendChild(j({
3060
- label: r,
3061
- className: "btn-secondary",
3062
- onClick: () => {
3063
- var a;
3064
- (a = this._onAction) == null || a.call(this, e, t.action || t.id), this._renderControls();
3065
- }
3066
- })), n;
3067
- }
3068
- default:
3069
- return null;
3070
- }
3071
- }
3072
- /**
3073
- * Normalize control type
3074
- * @param {string} type
3075
- * @returns {string}
3076
- */
3077
- _normalizeControlType(e) {
3078
- return {
3079
- slider: "slider",
3080
- range: "slider",
3081
- toggle: "toggle",
3082
- checkbox: "toggle",
3083
- color: "color",
3084
- select: "select",
3085
- dropdown: "select",
3086
- button: "button",
3087
- text: "text"
3088
- }[e] || e;
3089
- }
3090
- /**
3091
- * Normalize options array
3092
- * @param {Array} options
3093
- * @returns {Array}
3094
- */
3095
- _normalizeOptions(e) {
3096
- 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) });
3097
- }
3098
- /**
3099
- * Clean up
3100
- */
3101
- destroy() {
3102
- var e;
3103
- this._controls.clear(), (e = this._drawer) == null || e.remove(), this._drawer = null, this._isOpen = !1, this._currentFilterId = null, this._container = null;
3104
- }
3105
- }
3106
- class nt {
3107
- constructor(e, t) {
3108
- 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;
3109
- }
3110
- /**
3111
- * @param {Object} callbacks - { onToggle(filterId, enabled), onSelect(filterId) }
3112
- * @returns {HTMLElement}
3113
- */
3114
- render({ onToggle: e, onSelect: t }) {
3115
- return this._onToggle = e, this._onSelect = t, this.element = d("div", {
3116
- className: "mobile-active-filters",
3117
- "data-testid": "mobile-active-filters"
3118
- }), this._scrollContainer = d("div", { className: "mobile-active-filters-scroll" }), this.element.appendChild(this._scrollContainer), this._subscribeToState(), this._renderChips(), this.element;
3119
- }
3120
- _subscribeToState() {
3121
- const e = this.state.on("change:activeFilters", () => {
3122
- this._renderChips();
3123
- });
3124
- this._unsubscribers.push(e);
3125
- const t = this.state.on("change:selectedFilter", () => {
3126
- this._updateSelectedState();
3127
- });
3128
- this._unsubscribers.push(t);
3129
- }
3130
- _renderChips() {
3131
- this._scrollContainer.innerHTML = "", this._chips.clear();
3132
- const e = this.state.get("activeFilters");
3133
- if (!e || e.size === 0) {
3134
- const t = d("div", {
3135
- className: "active-filter-placeholder"
3136
- }, "No active filters");
3137
- this._scrollContainer.appendChild(t);
3138
- return;
3139
- }
3140
- e.forEach((t) => {
3141
- const i = this.filterManager.getFilterDef(t);
3142
- if (!i) return;
3143
- const r = this._createChip(t, i);
3144
- this._chips.set(t, r), this._scrollContainer.appendChild(r);
3145
- }), this._updateSelectedState();
3146
- }
3147
- _createChip(e, t) {
3148
- const i = d("div", {
3149
- className: "active-filter-chip",
3150
- "data-filter-id": e,
3151
- "data-testid": `active-chip-${e}`
3152
- }), r = d("span", {
3153
- className: "active-filter-chip-label",
3154
- onClick: (o) => {
3155
- var n;
3156
- o.stopPropagation(), (n = this._onSelect) == null || n.call(this, e);
3157
- }
3158
- }, t.name), s = d("button", {
3159
- className: "active-filter-chip-check",
3160
- "aria-label": `Remove ${t.name} filter`,
3161
- onClick: (o) => {
3162
- var n;
3163
- o.stopPropagation(), (n = this._onToggle) == null || n.call(this, e, !1);
3164
- }
3165
- });
3166
- return s.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(r), i.appendChild(s), i;
3167
- }
3168
- _updateSelectedState() {
3169
- const e = this.state.get("selectedFilter");
3170
- this._chips.forEach((t, i) => {
3171
- t.classList.toggle("selected", i === e);
3172
- });
3173
- }
3174
- destroy() {
3175
- var e;
3176
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._chips.clear(), (e = this.element) == null || e.remove(), this.element = null;
3177
- }
3178
- }
3179
- const ot = [
3180
- { id: "free", name: "Free", icon: Ze },
3181
- { id: "square", name: "Square", icon: We },
3182
- { id: "circle", name: "Circle", icon: $e }
3183
- ], at = [
3184
- { id: "free", name: "Free" },
3185
- { id: "1:1", name: "1:1" },
3186
- { id: "4:3", name: "4:3" },
3187
- { id: "16:9", name: "16:9" },
3188
- { id: "3:2", name: "3:2" },
3189
- { id: "2:3", name: "2:3" }
3190
- ], lt = [
3191
- { id: "0", label: "0°", angle: 0 },
3192
- { id: "90", label: "90°", angle: 90 },
3193
- { id: "180", label: "180°", angle: 180 },
3194
- { id: "270", label: "270°", angle: 270 }
3195
- ];
3196
- function W(f) {
3197
- if (!Number.isFinite(Number(f))) return 0;
3198
- const e = Number(f) % 360;
3199
- return e < 0 ? e + 360 : e;
3200
- }
3201
- class ht {
3202
- constructor(e, t, i = null) {
3203
- 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 = [];
3204
- }
3205
- /**
3206
- * Apply crop and return the editor UI to filter mode.
3207
- * If crop application cannot complete, force-disable crop mode to avoid
3208
- * leaving the editor stuck in crop controls.
3209
- */
3210
- _applyCropAndReturnToFilters() {
3211
- this.cropManager.apply() === null && typeof this.cropManager.disable == "function" && this.cropManager.disable(), this.state.set("selectedCategory", "adjust");
3212
- }
3213
- /**
3214
- * Create and render the crop controls
3215
- * @returns {HTMLElement}
3216
- */
3217
- render() {
3218
- this.element = d("div", {
3219
- className: "crop-controls",
3220
- "data-testid": "crop-controls"
3221
- }), this.element.appendChild(this._renderRotationSection());
3222
- const e = d("div", { className: "crop-section" });
3223
- e.appendChild(d("label", { className: "section-label" }, "Shape"));
3224
- const t = d("div", { className: "chip-row" }), i = this.state.get("crop.shape");
3225
- ot.forEach((l) => {
3226
- const h = re({
3227
- label: l.name,
3228
- icon: l.icon,
3229
- active: i === l.id,
3230
- onClick: () => this._selectShape(l.id)
3231
- });
3232
- h.dataset.shape = l.id, h.dataset.testid = `crop-shape-${l.id}`, this._shapeChips.set(l.id, h), t.appendChild(h);
3233
- }), 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"));
3234
- const r = d("div", { className: "chip-row aspect-row" }), s = this.state.get("crop.aspect");
3235
- at.forEach((l) => {
3236
- const h = re({
3237
- label: l.name,
3238
- active: s === l.id,
3239
- onClick: () => this._selectAspect(l.id)
3240
- });
3241
- h.dataset.ratio = l.id, h.dataset.testid = `crop-ratio-${l.id}`, this._aspectChips.set(l.id, h), r.appendChild(h);
3242
- }), this._aspectSection.appendChild(r), this.element.appendChild(this._aspectSection), this._updateAspectVisibility();
3243
- const o = d("div", { className: "crop-actions" }), n = j({
3244
- label: "Cancel",
3245
- className: "btn-secondary crop-cancel-btn",
3246
- icon: de,
3247
- onClick: () => this.cropManager.cancel()
3248
- });
3249
- n.dataset.testid = "cancel-crop";
3250
- const a = j({
3251
- label: "Apply Crop",
3252
- className: "btn-primary crop-apply-btn",
3253
- icon: he,
3254
- onClick: () => this._applyCropAndReturnToFilters()
3255
- });
3256
- return a.dataset.testid = "apply-crop", o.appendChild(n), o.appendChild(a), this.element.appendChild(o), this._subscribeToState(), this.element;
3257
- }
3258
- _renderRotationSection() {
3259
- const e = d("div", { className: "crop-section rotation-section" });
3260
- e.appendChild(d("label", { className: "section-label" }, "Rotate"));
3261
- const t = d("div", { className: "rotation-action-row" }), i = j({
3262
- label: "Left 90°",
3263
- className: "btn-secondary rotation-action-btn",
3264
- icon: ze,
3265
- onClick: () => this._rotateBy(-90)
3266
- });
3267
- i.dataset.testid = "rotate-left-90";
3268
- const r = j({
3269
- label: "Right 90°",
3270
- className: "btn-secondary rotation-action-btn",
3271
- icon: Pe,
3272
- onClick: () => this._rotateBy(90)
3273
- });
3274
- r.dataset.testid = "rotate-right-90";
3275
- const s = j({
3276
- label: "Rotate 180°",
3277
- className: "btn-secondary rotation-action-btn",
3278
- onClick: () => this._rotateBy(180)
3279
- });
3280
- s.dataset.testid = "rotate-180";
3281
- const o = j({
3282
- label: "Rotate 270°",
3283
- className: "btn-secondary rotation-action-btn",
3284
- onClick: () => this._rotateBy(270)
3285
- });
3286
- o.dataset.testid = "rotate-270", t.appendChild(i), t.appendChild(r), t.appendChild(s), t.appendChild(o), e.appendChild(t);
3287
- const n = d("div", { className: "chip-row rotation-preset-row" }), a = W(this.state.get("transform.rotation") ?? 0);
3288
- lt.forEach((p) => {
3289
- const g = re({
3290
- label: p.label,
3291
- active: Math.round(a) === p.angle,
3292
- onClick: () => this._setRotation(p.angle)
3293
- });
3294
- g.dataset.angle = p.id, g.dataset.testid = `rotation-preset-${p.id}`, this._rotationPresetChips.set(p.id, g), n.appendChild(g);
3295
- }), e.appendChild(n);
3296
- const l = d("div", { className: "rotation-arbitrary" }), h = d(
3297
- "div",
3298
- { className: "rotation-angle-header" },
3299
- d("label", { className: "rotation-angle-label", for: "image-editor-rotation-angle" }, "Angle"),
3300
- d("span", { className: "rotation-angle-value" }, this._formatAngle(a))
3301
- );
3302
- this._rotationValue = h.querySelector(".rotation-angle-value"), this._rotationRange = d("input", {
3303
- id: "image-editor-rotation-angle",
3304
- type: "range",
3305
- className: "slider-input rotation-range",
3306
- min: "0",
3307
- max: "360",
3308
- step: "1",
3309
- value: String(Math.round(a)),
3310
- "aria-label": "Rotation angle",
3311
- onInput: (p) => this._previewAngleValue(p.target.value),
3312
- onChange: (p) => this._setRotation(Number(p.target.value))
3313
- }), this._rotationNumber = d("input", {
3314
- type: "number",
3315
- className: "text-input rotation-number-input",
3316
- min: "-360",
3317
- max: "360",
3318
- step: "1",
3319
- value: this._formatPlainAngle(a),
3320
- "aria-label": "Rotation angle in degrees",
3321
- onKeyDown: (p) => {
3322
- p.key === "Enter" && (p.preventDefault(), this._setRotation(Number(p.target.value)), p.target.blur());
3323
- },
3324
- onBlur: (p) => this._setRotation(Number(p.target.value))
3325
- });
3326
- const c = d(
3327
- "div",
3328
- { className: "rotation-angle-row" },
3329
- this._rotationRange,
3330
- this._rotationNumber
3331
- );
3332
- l.appendChild(h), l.appendChild(c);
3333
- const u = j({
3334
- label: "Reset rotation",
3335
- className: "btn-text rotation-reset-btn",
3336
- onClick: () => this._setRotation(0)
3337
- });
3338
- return u.dataset.testid = "reset-rotation", l.appendChild(u), e.appendChild(l), e;
3339
- }
3340
- /**
3341
- * Subscribe to state changes
3342
- */
3343
- _subscribeToState() {
3344
- const e = this.state.on("change:crop.shape", ({ value: r }) => {
3345
- this._shapeChips.forEach((s, o) => {
3346
- s.setActive(o === r);
3347
- }), this._updateAspectVisibility();
3348
- });
3349
- this._unsubscribers.push(e);
3350
- const t = this.state.on("change:crop.aspect", ({ value: r }) => {
3351
- this._aspectChips.forEach((s, o) => {
3352
- s.setActive(o === r);
3353
- });
3354
- });
3355
- this._unsubscribers.push(t);
3356
- const i = this.state.on("change:transform.rotation", ({ value: r }) => {
3357
- this._syncRotationControls(r);
3358
- });
3359
- this._unsubscribers.push(i);
3360
- }
3361
- _rotateBy(e) {
3362
- var i, r;
3363
- ((r = (i = this.editor) == null ? void 0 : i.rotateBy) == null ? void 0 : r.call(i, e)) !== !1 && this._syncRotationControls(this.state.get("transform.rotation") ?? 0);
3364
- }
3365
- _setRotation(e) {
3366
- var r, s;
3367
- const t = W(e);
3368
- if (((s = (r = this.editor) == null ? void 0 : r.setRotationAngle) == null ? void 0 : s.call(r, t)) === !1) {
3369
- this._syncRotationControls(this.state.get("transform.rotation") ?? 0);
3370
- return;
3371
- }
3372
- this._syncRotationControls(t);
3373
- }
3374
- _previewAngleValue(e) {
3375
- const t = W(Number(e));
3376
- this._rotationValue && (this._rotationValue.textContent = this._formatAngle(t)), this._rotationNumber && (this._rotationNumber.value = this._formatPlainAngle(t)), this._refreshPresetChips(t);
3377
- }
3378
- _syncRotationControls(e) {
3379
- const t = W(e);
3380
- 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);
3381
- }
3382
- _refreshPresetChips(e) {
3383
- const t = Math.round(W(e)) % 360;
3384
- this._rotationPresetChips.forEach((i, r) => {
3385
- i.setActive(Number(r) === t);
3386
- });
3387
- }
3388
- _formatAngle(e) {
3389
- return `${this._formatPlainAngle(e)}°`;
3390
- }
3391
- _formatPlainAngle(e) {
3392
- const t = W(e);
3393
- return Number.isInteger(t) ? String(t) : t.toFixed(1);
3394
- }
3395
- /**
3396
- * Select a crop shape
3397
- * @param {string} shapeId
3398
- */
3399
- _selectShape(e) {
3400
- this.cropManager.setShape(e);
3401
- }
3402
- /**
3403
- * Select an aspect ratio
3404
- * @param {string} aspectId
3405
- */
3406
- _selectAspect(e) {
3407
- this.cropManager.setAspect(e);
3408
- }
3409
- /**
3410
- * Update aspect ratio section visibility
3411
- */
3412
- _updateAspectVisibility() {
3413
- const e = this.state.get("crop.shape"), t = this.state.get("lockAspectRatio");
3414
- this._aspectSection && (this._aspectSection.style.display = e === "free" && !t ? "" : "none");
3415
- }
3416
- /**
3417
- * Show controls
3418
- */
3419
- show() {
3420
- this.element && (this.element.style.display = "");
3421
- }
3422
- /**
3423
- * Hide controls
3424
- */
3425
- hide() {
3426
- this.element && (this.element.style.display = "none");
3427
- }
3428
- /**
3429
- * Clean up
3430
- */
3431
- destroy() {
3432
- var e;
3433
- 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;
3434
- }
3435
- }
3436
- class ct {
3437
- constructor(e, t) {
3438
- 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;
3439
- }
3440
- /**
3441
- * Create and render the active filters panel
3442
- * @param {Object} options - { onRemove, onReset, onClearAll, onUpdateValue, onSelect }
3443
- * @returns {HTMLElement}
3444
- */
3445
- render({ onRemove: e, onReset: t, onClearAll: i, onUpdateValue: r, onSelect: s }) {
3446
- this._onRemove = e, this._onReset = t, this._onClearAll = i, this._onUpdateValue = r, this._onSelect = s, this.element = d("div", {
3447
- className: "active-filters-panel",
3448
- "data-testid": "active-filters-panel"
3449
- });
3450
- const o = d("div", { className: "panel-header" });
3451
- o.appendChild(d("h3", { className: "panel-title" }, "Active Filters"));
3452
- const n = j({
3453
- label: "Clear All",
3454
- className: "btn-text btn-danger",
3455
- onClick: () => this._handleClearAll()
3456
- });
3457
- 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;
3458
- }
3459
- /**
3460
- * Subscribe to state changes
3461
- */
3462
- _subscribeToState() {
3463
- const e = this.state.on("change:activeFilters", () => {
3464
- this._renderFilterList();
3465
- });
3466
- this._unsubscribers.push(e);
3467
- const t = this.state.on("change:filterValues", () => {
3468
- this._updateValuesDisplay();
3469
- });
3470
- this._unsubscribers.push(t);
3471
- const i = this.state.on("change:selectedFilter", () => {
3472
- this._updateSelectedState();
3473
- });
3474
- this._unsubscribers.push(i);
3475
- }
3476
- /**
3477
- * Update selected state for all items
3478
- */
3479
- _updateSelectedState() {
3480
- const e = this.state.get("selectedFilter");
3481
- this._filterItems.forEach((t, i) => {
3482
- t.classList.toggle("selected", i === e);
3483
- });
3484
- }
3485
- /**
3486
- * Render the filter list
3487
- */
3488
- _renderFilterList() {
3489
- this._listContainer.innerHTML = "", this._filterItems.clear();
3490
- const e = this.state.get("activeFilters");
3491
- if (e.size === 0) {
3492
- this._listContainer.appendChild(
3493
- d("div", { className: "no-filters-message" }, "No filters active")
3494
- );
3495
- return;
3496
- }
3497
- e.forEach((t) => {
3498
- const i = this.filterManager.getFilterDef(t);
3499
- if (!i) return;
3500
- const r = this._createFilterItem(t, i);
3501
- this._filterItems.set(t, r), this._listContainer.appendChild(r);
3502
- });
3503
- }
3504
- /**
3505
- * Create a filter item
3506
- * @param {string} filterId
3507
- * @param {Object} def - Filter definition
3508
- * @returns {HTMLElement}
3509
- */
3510
- _createFilterItem(e, t) {
3511
- const r = this.state.get("selectedFilter") === e, s = d("div", {
3512
- className: `active-filter-item ${r ? "selected" : ""}`,
3513
- "data-active-filter": e,
3514
- "data-testid": `active-filter-${e}`,
3515
- onClick: (p) => {
3516
- p.target.closest(".filter-item-actions") || this._handleSelect(e);
3517
- }
3518
- }), o = d("div", { className: "filter-item-header" }), n = d("span", { className: "filter-item-name" }, t.name), a = d("div", { className: "filter-item-actions" }), l = O({
3519
- icon: ye,
3520
- title: "Reset filter",
3521
- className: "btn-icon-sm",
3522
- onClick: () => this._handleReset(e)
3523
- }), h = O({
3524
- icon: He,
3525
- title: "Remove filter",
3526
- className: "btn-icon-sm btn-danger",
3527
- onClick: () => this._handleRemove(e)
3528
- });
3529
- a.appendChild(l), a.appendChild(h), o.appendChild(n), o.appendChild(a), s.appendChild(o);
3530
- const c = this.state.getFilterValues(e), u = d("div", { className: "filter-item-summary" });
3531
- return u.textContent = this._getValuesSummary(t, c), s.appendChild(u), s._summaryEl = u, s;
3532
- }
3533
- /**
3534
- * Normalize control type (handle aliases)
3535
- * @param {string} type
3536
- * @returns {string}
3537
- */
3538
- _normalizeControlType(e) {
3539
- return {
3540
- slider: "slider",
3541
- range: "slider",
3542
- toggle: "toggle",
3543
- checkbox: "toggle",
3544
- color: "color",
3545
- select: "select",
3546
- dropdown: "select",
3547
- button: "button"
3548
- }[e] || e;
3549
- }
3550
- /**
3551
- * Get a summary of filter values
3552
- * @param {Object} def - Filter definition
3553
- * @param {Object} values - Current values
3554
- * @returns {string}
3555
- */
3556
- _getValuesSummary(e, t) {
3557
- if (!e.controls || !Array.isArray(e.controls))
3558
- return "Default values";
3559
- const i = [];
3560
- return e.controls.forEach((r) => {
3561
- const s = this._normalizeControlType(r.type);
3562
- if (s === "button") return;
3563
- const o = r.label || r.id, n = t[r.id] ?? r.default;
3564
- n !== r.default && (s === "slider" ? i.push(`${o}: ${this._formatValue(n)}`) : s === "toggle" ? n && i.push(o) : s === "color" ? i.push(`${o}: ${n}`) : s === "select" && i.push(`${o}: ${n}`));
3565
- }), i.length > 0 ? i.join(", ") : "Default values";
3566
- }
3567
- /**
3568
- * Format a numeric value for display
3569
- * @param {number} value
3570
- * @returns {string}
3571
- */
3572
- _formatValue(e) {
3573
- return typeof e != "number" || Number.isInteger(e) ? String(e) : e.toFixed(2);
3574
- }
3575
- /**
3576
- * Update values display for all items
3577
- */
3578
- _updateValuesDisplay() {
3579
- this._filterItems.forEach((e, t) => {
3580
- const i = this.filterManager.getFilterDef(t);
3581
- if (!i || !e._summaryEl) return;
3582
- const r = this.state.getFilterValues(t);
3583
- e._summaryEl.textContent = this._getValuesSummary(i, r);
3584
- });
3585
- }
3586
- /**
3587
- * Handle filter removal
3588
- * @param {string} filterId
3589
- */
3590
- _handleRemove(e) {
3591
- var t;
3592
- (t = this._onRemove) == null || t.call(this, e);
3593
- }
3594
- /**
3595
- * Handle filter reset
3596
- * @param {string} filterId
3597
- */
3598
- _handleReset(e) {
3599
- var t;
3600
- (t = this._onReset) == null || t.call(this, e);
3601
- }
3602
- /**
3603
- * Handle clear all
3604
- */
3605
- _handleClearAll() {
3606
- var e;
3607
- (e = this._onClearAll) == null || e.call(this);
3608
- }
3609
- /**
3610
- * Handle filter selection
3611
- * @param {string} filterId
3612
- */
3613
- _handleSelect(e) {
3614
- var t;
3615
- this.state.set("selectedFilter", e), (t = this._onSelect) == null || t.call(this, e);
3616
- }
3617
- /**
3618
- * Show panel
3619
- */
3620
- show() {
3621
- this.element && (this.element.style.display = "");
3622
- }
3623
- /**
3624
- * Hide panel
3625
- */
3626
- hide() {
3627
- this.element && (this.element.style.display = "none");
3628
- }
3629
- /**
3630
- * Clean up
3631
- */
3632
- destroy() {
3633
- var e;
3634
- this._unsubscribers.forEach((t) => t()), this._unsubscribers = [], this._filterItems.clear(), (e = this.element) == null || e.remove(), this.element = null;
3635
- }
3636
- }
3637
- const $ = {
3638
- name: "free",
3639
- initialMode: "filters",
3640
- cropShape: "free",
3641
- aspectRatio: "free",
3642
- autoZoomOnCropOverflow: !1,
3643
- lockCropShape: !1,
3644
- lockAspectRatio: !1,
3645
- showFilters: !0,
3646
- showCropControls: !0,
3647
- maxExportWidth: void 0,
3648
- maxExportHeight: void 0
3649
- }, le = {
3650
- free: {
3651
- ...$
3652
- },
3653
- avatar: {
3654
- ...$,
3655
- name: "avatar",
3656
- initialMode: "crop",
3657
- cropShape: "circle",
3658
- aspectRatio: "1:1",
3659
- autoZoomOnCropOverflow: !0,
3660
- lockCropShape: !0,
3661
- lockAspectRatio: !0
3662
- },
3663
- banner: {
3664
- ...$,
3665
- name: "banner",
3666
- initialMode: "crop",
3667
- cropShape: "square",
3668
- aspectRatio: "16:9",
3669
- autoZoomOnCropOverflow: !1,
3670
- lockCropShape: !1,
3671
- lockAspectRatio: !0
3672
- },
3673
- cover: {
3674
- ...$,
3675
- name: "cover",
3676
- initialMode: "crop",
3677
- cropShape: "free",
3678
- aspectRatio: "24:5",
3679
- autoZoomOnCropOverflow: !0,
3680
- lockCropShape: !0,
3681
- lockAspectRatio: !0
3682
- },
3683
- product: {
3684
- ...$,
3685
- name: "product",
3686
- initialMode: "filters",
3687
- cropShape: "square",
3688
- aspectRatio: "1:1",
3689
- autoZoomOnCropOverflow: !1,
3690
- lockCropShape: !1,
3691
- lockAspectRatio: !1
3692
- }
3693
- };
3694
- function dt(f) {
3695
- if (!f)
3696
- return { ...le.free };
3697
- if (typeof f == "object" && f !== null)
3698
- return { ...$, ...f };
3699
- const e = le[f];
3700
- return e ? { ...e } : { ...le.free };
3701
- }
3702
- function Z(f) {
3703
- if (!Number.isFinite(Number(f))) return 0;
3704
- const e = Number(f) % 360;
3705
- return e < 0 ? e + 360 : e;
3706
- }
3707
- function ut(f, e) {
3708
- const t = Z(f);
3709
- return (Z(e) - t + 540) % 360 - 180;
3710
- }
3711
- class kt extends se {
3712
- /**
3713
- * Create a new VanillaImageEditor
3714
- * @param {HTMLElement} container - Container element to mount the editor
3715
- * @param {Object} options - Configuration options
3716
- */
3717
- constructor(e, t = {}) {
3718
- var s, o, n, a;
3719
- if (super(), !e)
3720
- throw new Error("VanillaImageEditor: container element is required");
3721
- this._container = e, this._destroyed = !1, this._loadVersion = 0, this._objectUrls = /* @__PURE__ */ new Set(), this._activeObjectUrl = null;
3722
- const i = t.preset ? dt(t.preset) : null, r = i ? {
3723
- initialMode: i.initialMode === "crop" ? "crop" : "adjust",
3724
- cropShape: i.cropShape,
3725
- initialAspectRatio: i.aspectRatio
3726
- } : {};
3727
- this._options = {
3728
- theme: "auto",
3729
- initialImage: null,
3730
- initialMode: "adjust",
3731
- cropShape: "free",
3732
- initialAspectRatio: "free",
3733
- backgroundRemoval: {
3734
- enabled: !0,
3735
- endpoint: "/api/v1/media/background-removal/preview",
3736
- optionsEndpoint: "/api/v1/media/background-removal/options",
3737
- fallbackEndpoint: null
3738
- },
3739
- ...r,
3740
- ...t
3741
- }, 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 ce(this._state, this._renderer), this._removeBgManager = null, this._backgroundRemovalAvailable = !1, ((s = this._options.backgroundRemoval) == null ? void 0 : s.enabled) !== !1 && (this._removeBgManager = new Xe({
3742
- endpoint: (o = this._options.backgroundRemoval) == null ? void 0 : o.endpoint,
3743
- optionsEndpoint: (n = this._options.backgroundRemoval) == null ? void 0 : n.optionsEndpoint,
3744
- fallbackEndpoint: (a = this._options.backgroundRemoval) == null ? void 0 : a.fallbackEndpoint
3745
- })), 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) => {
3746
- this._destroyed || this.emit("error", l instanceof Error ? l : new Error(String(l)));
3747
- });
3748
- }
3749
- /**
3750
- * Initialize the editor
3751
- */
3752
- async _init() {
3753
- this._state.detectTheme(), this._buildDOM(), !(!await this._renderer.mount(
3754
- this._canvasContainer,
3755
- this._state.get("isDarkMode") ? 657930 : 16777215
3756
- ) || 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"));
3757
- }
3758
- /**
3759
- * Build DOM structure
3760
- */
3761
- _buildDOM() {
3762
- this._container.innerHTML = "", this._editorEl = d("div", {
3763
- className: `vanilla-image-editor ${this._state.get("isDarkMode") ? "dark" : "light"}`
3764
- });
3765
- const e = d("div", { className: "editor-toolbar-container" });
3766
- this._editorEl.appendChild(e), this._toolbarContainer = e;
3767
- const t = d("div", { className: "editor-content" }), i = d("div", { className: "canvas-section" });
3768
- 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", {
3769
- type: "file",
3770
- accept: "image/*",
3771
- className: "hidden-file-input",
3772
- "aria-label": "Choose image file to edit",
3773
- onChange: (r) => this._handleFileSelect(r)
3774
- }), this._editorEl.appendChild(this._fileInput), this._container.appendChild(this._editorEl);
3775
- }
3776
- /**
3777
- * Initialize UI components
3778
- */
3779
- _initUI() {
3780
- 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((r) => {
3781
- }));
3782
- const e = d("div", { className: "filter-layout" }), t = d("div", { className: "filter-left-column" });
3783
- this._filterCarousel = new it(this._state, this._filterManager), t.appendChild(this._filterCarousel.render({
3784
- onToggle: (r, s) => this._handleFilterToggle(r, s),
3785
- onSelect: (r) => {
3786
- this._handleFilterSelect(r), this._isMobile && this._mobileFilterDrawer && this._mobileFilterDrawer.open(r);
3787
- }
3788
- })), this._filterAdjustments = new rt(this._state, this._filterManager), t.appendChild(this._filterAdjustments.render({
3789
- onChange: (r, s, o) => this._handleFilterChange(r, s, o),
3790
- onReset: (r) => this._handleFilterReset(r),
3791
- onAction: (r, s) => this._handleFilterAction(r, s)
3792
- })), e.appendChild(t);
3793
- const i = d("div", { className: "filter-right-column" });
3794
- this._activeFiltersPanel = new ct(this._state, this._filterManager), i.appendChild(this._activeFiltersPanel.render({
3795
- onRemove: (r) => this._handleFilterToggle(r, !1),
3796
- onReset: (r) => this._handleFilterReset(r),
3797
- onClearAll: () => this.resetAll(),
3798
- onSelect: (r) => this._handleFilterSelect(r)
3799
- })), e.appendChild(i), this._filterControlsEl.appendChild(e), this._mobileActiveFilters = new nt(this._state, this._filterManager), this._filterControlsEl.appendChild(this._mobileActiveFilters.render({
3800
- onToggle: (r, s) => this._handleFilterToggle(r, s),
3801
- onSelect: (r) => {
3802
- this._handleFilterSelect(r), this._isMobile && this._mobileFilterDrawer && this._mobileFilterDrawer.open(r);
3803
- }
3804
- })), 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 st(this._state, this._filterManager), this._mobileFilterDrawer.build(this._controlsSection, {
3805
- onChange: (r, s, o) => this._handleFilterChange(r, s, o),
3806
- onReset: (r) => this._handleFilterReset(r),
3807
- onRemove: (r) => {
3808
- this._handleFilterToggle(r, !1), this._state.set("selectedFilter", null);
3809
- },
3810
- onAction: (r, s) => this._handleFilterAction(r, s)
3811
- });
3812
- }
3813
- /**
3814
- * Set up resize observer with debounce to prevent infinite loops.
3815
- * Without debounce, PIXI renderer.resize() changes the canvas buffer
3816
- * which triggers another ResizeObserver notification, causing a loop
3817
- * that floods the console with "ResizeObserver loop completed" errors
3818
- * and prevents the image from rendering (gray canvas).
3819
- */
3820
- _setupResizeObserver() {
3821
- let e = null;
3822
- this._resizeObserver = new ResizeObserver(() => {
3823
- clearTimeout(e), e = setTimeout(() => {
3824
- this._canvasContainer && (this._renderer.resizeTo(this._canvasContainer), this._state.get("mode") === "crop" && this._cropManager.drawOverlay());
3825
- }, 50);
3826
- }), this._resizeObserver.observe(this._canvasContainer);
3827
- }
3828
- /**
3829
- * Set up window resize listener for mobile detection
3830
- */
3831
- _setupWindowResize() {
3832
- this._handleWindowResize = () => {
3833
- var e;
3834
- this._isMobile = window.innerWidth <= 768, !this._isMobile && ((e = this._mobileFilterDrawer) != null && e.isOpen) && this._mobileFilterDrawer.close();
3835
- }, window.addEventListener("resize", this._handleWindowResize);
3836
- }
3837
- /**
3838
- * Subscribe to state changes
3839
- */
3840
- _subscribeToState() {
3841
- this._state.on("change:mode", ({ value: e }) => {
3842
- this._updateModeUI(e), e === "filters" && this._filterManager.applyFilters();
3843
- }), this._state.on("change:isDarkMode", ({ value: e }) => {
3844
- this._editorEl.classList.toggle("dark", e), this._editorEl.classList.toggle("light", !e), this._renderer.setBackgroundColor(e ? 657930 : 16777215);
3845
- }), this._state.on("change:hasImage", ({ value: e }) => {
3846
- this._editorEl.classList.toggle("has-image", e);
3847
- }), this._renderer.on("zoomChange", (e) => {
3848
- this._state.set("zoom", e);
3849
- });
3850
- }
3851
- /**
3852
- * Update UI based on mode
3853
- * @param {string} mode
3854
- */
3855
- _updateModeUI(e) {
3856
- var t;
3857
- 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");
3858
- }
3859
- /**
3860
- * Handle file selection
3861
- * @param {Event} event
3862
- */
3863
- async _handleFileSelect(e) {
3864
- var i;
3865
- const t = (i = e.target.files) == null ? void 0 : i[0];
3866
- t && await this.loadImage(t), this._fileInput.value = "";
3867
- }
3868
- /**
3869
- * Handle filter toggle
3870
- * @param {string} filterId
3871
- * @param {boolean} enabled
3872
- */
3873
- _handleFilterToggle(e, t) {
3874
- this._filterManager.toggle(e, t);
3875
- }
3876
- /**
3877
- * Handle filter select
3878
- * @param {string} filterId
3879
- */
3880
- _handleFilterSelect(e) {
3881
- this._state.set("selectedFilter", e);
3882
- }
3883
- /**
3884
- * Handle filter value change
3885
- * @param {string} filterId
3886
- * @param {string} controlId
3887
- * @param {*} value
3888
- */
3889
- _handleFilterChange(e, t, i) {
3890
- this._filterManager.updateValue(e, t, i) || this._filterManager.applyFilters(), this._renderer.render();
3891
- }
3892
- /**
3893
- * Handle filter reset
3894
- * @param {string} filterId
3895
- */
3896
- _handleFilterReset(e) {
3897
- this._filterManager.resetValues(e), this._filterManager.applyFilters();
3898
- }
3899
- /**
3900
- * Handle filter action (button click).
3901
- * Delegates to FilterManager.performFilterAction which syncs state after
3902
- * the action so the result survives filter recreation.
3903
- * @param {string} filterId
3904
- * @param {string} action
3905
- */
3906
- _handleFilterAction(e, t) {
3907
- this._filterManager.performFilterAction(e, t);
3908
- }
3909
- _revokeObjectUrl(e) {
3910
- !e || !this._objectUrls.has(e) || (URL.revokeObjectURL(e), this._objectUrls.delete(e), this._activeObjectUrl === e && (this._activeObjectUrl = null));
3911
- }
3912
- _replaceActiveObjectUrl(e) {
3913
- const t = this._activeObjectUrl;
3914
- this._activeObjectUrl = e, this._objectUrls.add(e), t && t !== e && this._revokeObjectUrl(t);
3915
- }
3916
- _clearActiveObjectUrl() {
3917
- this._activeObjectUrl && this._revokeObjectUrl(this._activeObjectUrl);
3918
- }
3919
- // ==================== Public API ====================
3920
- /**
3921
- * Set the filter registry (from mediables/filters)
3922
- * @param {Object} registry - { getFilter, getAllFilters, getFiltersByCategory }
3923
- */
3924
- setFilterRegistry(e) {
3925
- this._filterManager.setRegistry(e);
3926
- }
3927
- /**
3928
- * Load an image into the editor
3929
- * @param {string|Blob|File} imageSource - URL, data URL, Blob, or File object
3930
- * @param {Object} [options] - Load options
3931
- * @param {Object|null} [options.state] - ImageEditorSessionState to restore
3932
- * @param {string|number} [options.sessionKey] - Session boundary identifier
3933
- */
3934
- async loadImage(e, t = {}) {
3935
- var n, a;
3936
- if (this._destroyed) return;
3937
- (a = (n = this._removeBgManager) == null ? void 0 : n.cancelActiveRequests) == null || a.call(n, "image-load");
3938
- const i = ++this._loadVersion;
3939
- let r = e, s = null;
3940
- this._resetEditorState(), this._state.set("hasImage", !1), this._state.set("imageUrl", null), e instanceof Blob && (r = URL.createObjectURL(e), s = r, this._objectUrls.add(r));
3941
- const o = await this._renderer.loadTexture(r, {
3942
- isCurrent: () => !this._destroyed && this._loadVersion === i
3943
- });
3944
- if (this._destroyed || this._loadVersion !== i) {
3945
- this._revokeObjectUrl(s);
3946
- return;
3947
- }
3948
- if (!o) {
3949
- this._revokeObjectUrl(s), this.emit("error", new Error("Failed to load image"));
3950
- return;
3951
- }
3952
- s ? this._replaceActiveObjectUrl(s) : this._clearActiveObjectUrl(), this._state.set("hasImage", !0), this._state.set("imageUrl", r), t.state && this._hydrateState(t.state), this._options.initialMode === "crop" && this.setMode("crop"), this.emit("imageLoaded", { url: r });
3953
- }
3954
- /**
3955
- * Reset all editor state (filters + crop) to a clean baseline.
3956
- * Called at the start of loadImage() to prevent stale state bleed between sessions.
3957
- */
3958
- _resetEditorState() {
3959
- 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"));
3960
- }
3961
- /**
3962
- * Hydrate editor state from a canonical ImageEditorSessionState object.
3963
- * Restores filter stack + values and crop metadata (shape, aspect, rect).
3964
- * @param {Object} state - ImageEditorSessionState
3965
- */
3966
- _hydrateState(e) {
3967
- var i;
3968
- if (!e || e.version !== 1) return;
3969
- const t = Z(((i = e.transform) == null ? void 0 : i.rotation) ?? 0);
3970
- if (t !== 0 && this._renderer.rotateBy(t) && this._state.set("transform.rotation", t), e.crop) {
3971
- const r = e.crop.shape || "free", s = e.crop.aspectRatio || "free";
3972
- e.crop.rect && (this._cropManager.applyFromPixelRect(e.crop.rect, r), this._state.set("crop.appliedRect", { ...e.crop.rect }), this._state.set("crop.appliedShape", r), this._state.set("crop.appliedAspect", s)), this._state.set("crop.shape", r), this._state.set("crop.aspect", s);
3973
- }
3974
- if (Array.isArray(e.filters)) {
3975
- let r = null;
3976
- for (const s of e.filters)
3977
- if (s.enabled && (r === null && (r = s.id), this._state.toggleFilter(s.id, !0), this._filterManager.initializeValues(s.id), s.values))
3978
- for (const [o, n] of Object.entries(s.values))
3979
- this._state.setFilterValue(s.id, o, n);
3980
- if (r !== null) {
3981
- this._state.set("selectedFilter", r);
3982
- const s = this._filterManager.getFilterDef(r);
3983
- if (typeof (s == null ? void 0 : s.category) == "string") {
3984
- const o = Ye[s.category] ?? s.category;
3985
- this._state.set("selectedCategory", o);
3986
- }
3987
- }
3988
- this._filterManager.applyFilters();
3989
- }
3990
- }
3991
- /**
3992
- * Open the file picker dialog
3993
- */
3994
- openFilePicker() {
3995
- var e;
3996
- (e = this._fileInput) == null || e.click();
3997
- }
3998
- /**
3999
- * Export the current image.
4000
- *
4001
- * @param {string} format - 'png' or 'jpeg'
4002
- * @param {number} quality - Quality for jpeg (0-1)
4003
- * @param {Object} [options]
4004
- * @param {number} [options.maxEdge] - Cap the longest edge to this pixel count
4005
- * @param {number} [options.maxPixels] - Cap total pixel count (width*height)
4006
- * @returns {string|null} Data URL
4007
- */
4008
- exportImage(e = "png", t = 0.92, i = {}) {
4009
- return !this._state.get("hasImage") || !this._applyPendingCropBeforeExport("VanillaImageEditor.exportImage") ? null : this._renderer.exportImage(
4010
- e,
4011
- t,
4012
- i.maxEdge ?? 0,
4013
- i.dontUpscale !== !1,
4014
- i.maxPixels ?? 0
4015
- );
4016
- }
4017
- /**
4018
- * Export the current image as a Blob after finalizing pending crop edits.
4019
- *
4020
- * @param {string} format - 'png' or 'jpeg'
4021
- * @param {number} quality - Quality for jpeg (0-1)
4022
- * @param {Object} [options]
4023
- * @returns {Promise<{blob: Blob, width: number, height: number}|null>}
4024
- */
4025
- async exportBlob(e = "png", t = 0.92, i = {}) {
4026
- return !this._state.get("hasImage") || !this._applyPendingCropBeforeExport("VanillaImageEditor.exportBlob") ? null : this._renderer.exportBlob(e, t, i);
4027
- }
4028
- _applyPendingCropBeforeExport(e) {
4029
- return !(this._state.get("crop.rect") && !this._cropManager.apply());
4030
- }
4031
- /**
4032
- * Save the image (triggers download and emits 'save' event)
4033
- */
4034
- async save() {
4035
- if (!this._state.get("hasImage")) return;
4036
- this._state.set("isSaving", !0);
4037
- let e = null;
4038
- try {
4039
- const t = await this.exportBlob("png", 0.92);
4040
- if (!t) throw new Error("Failed to export image");
4041
- const i = document.createElement("a");
4042
- 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", {
4043
- blob: t.blob,
4044
- mimeType: t.blob.type || "image/png",
4045
- dimensions: { width: t.width, height: t.height },
4046
- state: this.getSerializableState()
4047
- });
4048
- } catch (t) {
4049
- this.emit("error", { error: t });
4050
- } finally {
4051
- e && URL.revokeObjectURL(e), this._state.set("isSaving", !1);
4052
- }
4053
- }
4054
- /**
4055
- * Get a canonical, serializable snapshot of the current editor state.
4056
- * Safe for persistence — returns a deep copy with no mutable references.
4057
- * @returns {Object} ImageEditorSessionState
4058
- */
4059
- getSerializableState() {
4060
- const e = this._state.get("activeFilters"), t = [];
4061
- if (e)
4062
- for (const o of e) {
4063
- const n = this._state.getFilterValues(o);
4064
- t.push({
4065
- id: o,
4066
- enabled: !0,
4067
- values: { ...n }
4068
- });
4069
- }
4070
- const i = this._state.get("crop.appliedRect"), r = this._state.get("crop.rect"), s = i || r;
4071
- return {
4072
- version: 1,
4073
- crop: {
4074
- rect: s ? { ...s } : null,
4075
- aspectRatio: this._state.get("crop.appliedAspect") || this._state.get("crop.aspect") || "free",
4076
- shape: this._state.get("crop.appliedShape") || this._state.get("crop.shape") || "free"
4077
- },
4078
- transform: {
4079
- rotation: Z(this._state.get("transform.rotation") ?? 0)
4080
- },
4081
- filters: t
4082
- };
4083
- }
4084
- /**
4085
- * Close the editor (emits 'cancel' event)
4086
- */
4087
- close() {
4088
- this.emit("cancel");
4089
- }
4090
- /**
4091
- * Set zoom level
4092
- * @param {number} zoom
4093
- */
4094
- setZoom(e) {
4095
- this._renderer.setZoom(e);
4096
- }
4097
- /**
4098
- * Fit image to screen
4099
- */
4100
- fitToScreen() {
4101
- this._renderer.fitToScreen();
4102
- }
4103
- /**
4104
- * Reset all filters
4105
- */
4106
- resetAll() {
4107
- this._filterManager.resetAll();
4108
- }
4109
- rotateBy(e) {
4110
- const t = this._state.get("transform.rotation") ?? 0;
4111
- return this.setRotationAngle(Number(t) + Number(e));
4112
- }
4113
- setRotationAngle(e) {
4114
- if (!this._state.get("hasImage")) return !1;
4115
- const t = Z(this._state.get("transform.rotation") ?? 0), i = Z(e), r = ut(t, i), s = this._state.get("mode") === "crop";
4116
- if (Math.abs(r) > 1e-3) {
4117
- if (s && this._cropManager.disable(), !this._renderer.rotateBy(r))
4118
- return s && this._cropManager.enable(), !1;
4119
- this._filterManager.applyFilters(), this._state.set("crop.rect", null), this._state.set("crop.dirty", !1), s && this._cropManager.enable();
4120
- }
4121
- return this._state.set("transform.rotation", i), this.emit("transformChanged", { rotation: i }), !0;
4122
- }
4123
- resetRotation() {
4124
- return this.setRotationAngle(0);
4125
- }
4126
- /**
4127
- * Toggle theme
4128
- */
4129
- toggleTheme() {
4130
- const e = this._state.get("isDarkMode");
4131
- this._state.set("isDarkMode", !e);
4132
- }
4133
- /**
4134
- * Set theme
4135
- * @param {'light'|'dark'|'auto'} theme
4136
- */
4137
- setTheme(e) {
4138
- this._state.set("theme", e), this._state.detectTheme();
4139
- }
4140
- /**
4141
- * Set editor mode
4142
- * @param {'filters'|'crop'} mode
4143
- */
4144
- setMode(e) {
4145
- e === "crop" ? this._cropManager.enable() : this._cropManager.disable(), this._state.set("mode", e);
4146
- }
4147
- /**
4148
- * Get current state (for debugging)
4149
- * @returns {Object}
4150
- */
4151
- getState() {
4152
- return this._state.getAll();
4153
- }
4154
- // ==================== Background Removal ====================
4155
- /**
4156
- * Remove the background from the current image
4157
- * @param {Object} options
4158
- * @param {string} [options.tier='balanced'] - Quality tier: 'fast', 'balanced', 'best'
4159
- * @param {string} [options.model] - Explicit model name (overrides tier)
4160
- * @param {boolean} [options.alpha_matting=false] - Enable alpha matting for edge refinement
4161
- * @returns {Promise<{model: string, processMs: string}>}
4162
- */
4163
- async removeBackground(e = {}) {
4164
- var t, i;
4165
- if (!this._removeBgManager)
4166
- throw new Error("Background removal is not enabled");
4167
- if (!this._state.get("hasImage"))
4168
- throw new Error("No image loaded");
4169
- if (!this.canRemoveBackground())
4170
- throw new Error("Background removal is not available for the current editor state");
4171
- this._state.set("isProcessing", !0), this._showLoadingOverlay("Removing background...");
4172
- try {
4173
- const r = this.exportImage("png");
4174
- if (!r)
4175
- throw new Error("Failed to export image for background removal");
4176
- const s = await this._removeBgManager.preparePreviewRequest(r, e), o = this._buildPreviewBackgroundRemovalContext(s), n = await this._removeBgManager.removeBackground(s.blob, s);
4177
- return await this._isCurrentPreviewBackgroundRemovalContext(o, n) ? (await this.loadImage(n.dataUrl), this.emit("background-removed", {
4178
- model: n.model,
4179
- processMs: n.processMs
4180
- }), {
4181
- model: n.model,
4182
- processMs: n.processMs
4183
- }) : (this.emit("background-removal-stale", {
4184
- operationId: o.operationId,
4185
- sourceHash: o.sourceHash
4186
- }), { stale: !0 });
4187
- } catch (r) {
4188
- throw this.emit("error", { error: r, context: "background-removal" }), r;
4189
- } finally {
4190
- (i = (t = this._state) == null ? void 0 : t.set) == null || i.call(t, "isProcessing", !1), this._hideLoadingOverlay();
4191
- }
4192
- }
4193
- /**
4194
- * Check if background removal is available
4195
- * @returns {Promise<boolean>}
4196
- */
4197
- async isBackgroundRemovalAvailable() {
4198
- return this._removeBgManager ? this._refreshBackgroundRemovalAvailability() : !1;
4199
- }
4200
- canRemoveBackground() {
4201
- return !!this._removeBgManager && this._state.get("hasImage") === !0 && this._backgroundRemovalAvailable === !0;
4202
- }
4203
- async _refreshBackgroundRemovalAvailability() {
4204
- if (!this._removeBgManager)
4205
- return this._backgroundRemovalAvailable = !1, this._state.set("backgroundRemovalAvailable", !1), !1;
4206
- const e = await this._removeBgManager.isAvailable();
4207
- return this._destroyed ? !1 : (this._backgroundRemovalAvailable = e, this._state.set("backgroundRemovalAvailable", e), e);
4208
- }
4209
- _buildPreviewBackgroundRemovalContext(e) {
4210
- return {
4211
- loadVersion: this._loadVersion,
4212
- imageUrl: this._state.get("imageUrl") ?? null,
4213
- operationId: e.operationId,
4214
- sessionKey: e.sessionKey,
4215
- sourceHash: e.sourceHash,
4216
- targetRef: e.targetRef ?? null
4217
- };
4218
- }
4219
- async _isCurrentPreviewBackgroundRemovalContext(e, t = null) {
4220
- 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))
4221
- return !1;
4222
- const i = this.exportImage("png");
4223
- return i ? await this._removeBgManager.computeImageDataHash(i) === e.sourceHash : !1;
4224
- }
4225
- // ==================== Loading Overlay ====================
4226
- /**
4227
- * Show the loading overlay
4228
- * @param {string} message - Message to display
4229
- */
4230
- _showLoadingOverlay(e) {
4231
- var i;
4232
- if (!this._loadingOverlay)
4233
- this._loadingOverlay = d("div", { className: "editor-loading-overlay" }, [
4234
- d("div", { className: "editor-loading-spinner" }),
4235
- d("div", { className: "editor-loading-text" }, e)
4236
- ]);
4237
- else {
4238
- const r = this._loadingOverlay.querySelector(".editor-loading-text");
4239
- r && (r.textContent = e);
4240
- }
4241
- const t = (i = this._canvasContainer) == null ? void 0 : i.parentElement;
4242
- t && !this._loadingOverlay.parentElement && t.appendChild(this._loadingOverlay);
4243
- }
4244
- /**
4245
- * Hide the loading overlay
4246
- */
4247
- _hideLoadingOverlay() {
4248
- var e;
4249
- (e = this._loadingOverlay) == null || e.remove();
4250
- }
4251
- /**
4252
- * Destroy the editor and clean up
4253
- */
4254
- destroy() {
4255
- var e, t, i, r, s, o, n, a, l, h, c, u;
4256
- this._destroyed = !0, (t = (e = this._removeBgManager) == null ? void 0 : e.cancelActiveRequests) == null || t.call(e, "editor-destroy");
4257
- for (const p of this._objectUrls)
4258
- try {
4259
- URL.revokeObjectURL(p);
4260
- } catch {
4261
- }
4262
- 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), (r = this._toolbar) == null || r.destroy(), (s = this._categoryCarousel) == null || s.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(), (u = this._editorEl) == null || u.remove(), this._container = null, this._state = null, this.emit("destroyed"), this.removeAllListeners();
4263
- }
4264
- }
4265
- export {
4266
- Ye as A,
4267
- Oe as B,
4268
- ce as C,
4269
- ue as E,
4270
- Qe as F,
4271
- Te as P,
4272
- Xe as R,
4273
- et as T,
4274
- kt as V,
4275
- me as a,
4276
- j as b,
4277
- he as c,
4278
- Ce as d,
4279
- d as e,
4280
- ke as f,
4281
- xe as g,
4282
- we as h,
4283
- ve as i,
4284
- de as j,
4285
- bt as k,
4286
- yt as l,
4287
- vt as m,
4288
- wt as n,
4289
- _e as o,
4290
- xt as p,
4291
- Ct as q,
4292
- Ue as r,
4293
- Le as s,
4294
- He as t,
4295
- O as u,
4296
- gt as v,
4297
- mt as w,
4298
- dt as x,
4299
- ht as y,
4300
- _t as z
4301
- };
4302
- //# sourceMappingURL=editor-BQ_nY4P6.js.map