@codingfactory/mediables-vue 2.5.0 → 2.6.2

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 (36) hide show
  1. package/dist/{PixiFrameExporter-BrsHCMHb.cjs → PixiFrameExporter-CUqM394V.cjs} +2 -2
  2. package/dist/{PixiFrameExporter-BrsHCMHb.cjs.map → PixiFrameExporter-CUqM394V.cjs.map} +1 -1
  3. package/dist/{PixiFrameExporter-DFq2mc-y.js → PixiFrameExporter-DJsxS-v6.js} +2 -2
  4. package/dist/{PixiFrameExporter-DFq2mc-y.js.map → PixiFrameExporter-DJsxS-v6.js.map} +1 -1
  5. package/dist/components/ImageEditor/ImageEditor.vue.d.ts +1 -0
  6. package/dist/components/ImageEditorModal.vue.d.ts +2 -0
  7. package/dist/components/MediaManagementView.vue.d.ts +2 -0
  8. package/dist/composables/useFloatingPills.d.ts +4 -0
  9. package/dist/composables/useMediaDeletion.d.ts +66 -0
  10. package/dist/composables/useMediaTrash.d.ts +64 -0
  11. package/dist/editor-BTwIhrcA.cjs +2 -0
  12. package/dist/editor-BTwIhrcA.cjs.map +1 -0
  13. package/dist/{editor-2Q72CZjK.js → editor-CYj5y5bp.js} +1417 -1167
  14. package/dist/editor-CYj5y5bp.js.map +1 -0
  15. package/dist/filters/registry.d.ts +4 -0
  16. package/dist/index-BUrSZVu3.cjs +342 -0
  17. package/dist/index-BUrSZVu3.cjs.map +1 -0
  18. package/dist/{index-CaQc9GBh.js → index-CtHJav8G.js} +12112 -10236
  19. package/dist/index-CtHJav8G.js.map +1 -0
  20. package/dist/index.d.ts +6 -0
  21. package/dist/mediables-vanilla.cjs +1 -1
  22. package/dist/mediables-vanilla.mjs +1 -1
  23. package/dist/mediables-vue.cjs +1 -1
  24. package/dist/mediables-vue.mjs +69 -66
  25. package/dist/render-page/assets/{index-ZZVWF3LA.js → index-y90zwXpc.js} +713 -531
  26. package/dist/render-page/index.html +1 -1
  27. package/dist/style.css +1 -1
  28. package/dist/types/media.d.ts +26 -0
  29. package/dist/types/mediaLibraryPicker.d.ts +1 -0
  30. package/package.json +2 -2
  31. package/dist/editor-2Q72CZjK.js.map +0 -1
  32. package/dist/editor-DjvxEsss.cjs +0 -42
  33. package/dist/editor-DjvxEsss.cjs.map +0 -1
  34. package/dist/index-BTGCVCn6.cjs +0 -342
  35. package/dist/index-BTGCVCn6.cjs.map +0 -1
  36. package/dist/index-CaQc9GBh.js.map +0 -1
@@ -63773,7 +63773,7 @@ __publicField$1(_TwistFilter, "DEFAULT_OPTIONS", {
63773
63773
  angle: 4,
63774
63774
  offset: { x: 0, y: 0 }
63775
63775
  });
63776
- let TwistFilter = _TwistFilter;
63776
+ let TwistFilter$1 = _TwistFilter;
63777
63777
  var fragment = "precision highp float;\nin vec2 vTextureCoord;\nout vec4 finalColor;\n\nuniform sampler2D uTexture;\nuniform float uStrength;\nuniform vec2 uCenter;\nuniform vec2 uRadii;\n\nuniform vec4 uInputSize;\n\nconst float MAX_KERNEL_SIZE = ${MAX_KERNEL_SIZE};\n\n// author: http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/\nhighp float rand(vec2 co, float seed) {\n const highp float a = 12.9898, b = 78.233, c = 43758.5453;\n highp float dt = dot(co + seed, vec2(a, b)), sn = mod(dt, 3.14159);\n return fract(sin(sn) * c + seed);\n}\n\nvoid main() {\n float minGradient = uRadii[0] * 0.3;\n float innerRadius = (uRadii[0] + minGradient * 0.5) / uInputSize.x;\n\n float gradient = uRadii[1] * 0.3;\n float radius = (uRadii[1] - gradient * 0.5) / uInputSize.x;\n\n float countLimit = MAX_KERNEL_SIZE;\n\n vec2 dir = vec2(uCenter.xy / uInputSize.xy - vTextureCoord);\n float dist = length(vec2(dir.x, dir.y * uInputSize.y / uInputSize.x));\n\n float strength = uStrength;\n\n float delta = 0.0;\n float gap;\n if (dist < innerRadius) {\n delta = innerRadius - dist;\n gap = minGradient;\n } else if (radius >= 0.0 && dist > radius) { // radius < 0 means it's infinity\n delta = dist - radius;\n gap = gradient;\n }\n\n if (delta > 0.0) {\n float normalCount = gap / uInputSize.x;\n delta = (normalCount - delta) / normalCount;\n countLimit *= delta;\n strength *= delta;\n if (countLimit < 1.0)\n {\n gl_FragColor = texture(uTexture, vTextureCoord);\n return;\n }\n }\n\n // randomize the lookup values to hide the fixed number of samples\n float offset = rand(vTextureCoord, 0.0);\n\n float total = 0.0;\n vec4 color = vec4(0.0);\n\n dir *= strength;\n\n for (float t = 0.0; t < MAX_KERNEL_SIZE; t++) {\n float percent = (t + offset) / MAX_KERNEL_SIZE;\n float weight = 4.0 * (percent - percent * percent);\n vec2 p = vTextureCoord + dir * percent;\n vec4 sample = texture(uTexture, p);\n\n // switch to pre-multiplied alpha to correctly blur transparent images\n // sample.rgb *= sample.a;\n\n color += sample * weight;\n total += weight;\n\n if (t > countLimit){\n break;\n }\n }\n\n color /= total;\n // switch back from pre-multiplied alpha\n // color.rgb /= color.a + 0.00001;\n\n gl_FragColor = color;\n}\n";
63778
63778
  var source = "struct ZoomBlurUniforms {\n uStrength:f32,\n uCenter:vec2<f32>,\n uRadii:vec2<f32>,\n};\n\nstruct GlobalFilterUniforms {\n uInputSize:vec4<f32>,\n uInputPixel:vec4<f32>,\n uInputClamp:vec4<f32>,\n uOutputFrame:vec4<f32>,\n uGlobalFrame:vec4<f32>,\n uOutputTexture:vec4<f32>,\n};\n\n@group(0) @binding(0) var<uniform> gfu: GlobalFilterUniforms;\n\n@group(0) @binding(1) var uTexture: texture_2d<f32>; \n@group(0) @binding(2) var uSampler: sampler;\n@group(1) @binding(0) var<uniform> zoomBlurUniforms : ZoomBlurUniforms;\n\n@fragment\nfn mainFragment(\n @builtin(position) position: vec4<f32>,\n @location(0) uv : vec2<f32>\n) -> @location(0) vec4<f32> {\n let uStrength = zoomBlurUniforms.uStrength;\n let uCenter = zoomBlurUniforms.uCenter;\n let uRadii = zoomBlurUniforms.uRadii;\n\n let minGradient: f32 = uRadii[0] * 0.3;\n let innerRadius: f32 = (uRadii[0] + minGradient * 0.5) / gfu.uInputSize.x;\n\n let gradient: f32 = uRadii[1] * 0.3;\n let radius: f32 = (uRadii[1] - gradient * 0.5) / gfu.uInputSize.x;\n\n let MAX_KERNEL_SIZE: f32 = ${MAX_KERNEL_SIZE};\n\n var countLimit: f32 = MAX_KERNEL_SIZE;\n\n var dir: vec2<f32> = vec2<f32>(uCenter / gfu.uInputSize.xy - uv);\n let dist: f32 = length(vec2<f32>(dir.x, dir.y * gfu.uInputSize.y / gfu.uInputSize.x));\n\n var strength: f32 = uStrength;\n\n var delta: f32 = 0.0;\n var gap: f32;\n\n if (dist < innerRadius) {\n delta = innerRadius - dist;\n gap = minGradient;\n } else if (radius >= 0.0 && dist > radius) { // radius < 0 means it's infinity\n delta = dist - radius;\n gap = gradient;\n }\n\n var returnColorOnly: bool = false;\n\n if (delta > 0.0) {\n let normalCount: f32 = gap / gfu.uInputSize.x;\n delta = (normalCount - delta) / normalCount;\n countLimit *= delta;\n strength *= delta;\n \n if (countLimit < 1.0)\n {\n returnColorOnly = true;;\n }\n }\n\n // randomize the lookup values to hide the fixed number of samples\n let offset: f32 = rand(uv, 0.0);\n\n var total: f32 = 0.0;\n var color: vec4<f32> = vec4<f32>(0.);\n\n dir *= strength;\n\n for (var t = 0.0; t < MAX_KERNEL_SIZE; t += 1.0) {\n let percent: f32 = (t + offset) / MAX_KERNEL_SIZE;\n let weight: f32 = 4.0 * (percent - percent * percent);\n let p: vec2<f32> = uv + dir * percent;\n let sample: vec4<f32> = textureSample(uTexture, uSampler, p);\n \n if (t < countLimit)\n {\n color += sample * weight;\n total += weight;\n }\n }\n\n color /= total;\n\n return select(color, textureSample(uTexture, uSampler, uv), returnColorOnly);\n}\n\nfn modulo(x: f32, y: f32) -> f32\n{\n return x - y * floor(x/y);\n}\n\n// author: http://byteblacksmith.com/improvements-to-the-canonical-one-liner-glsl-rand-for-opengl-es-2-0/\nfn rand(co: vec2<f32>, seed: f32) -> f32\n{\n let a: f32 = 12.9898;\n let b: f32 = 78.233;\n let c: f32 = 43758.5453;\n let dt: f32 = dot(co + seed, vec2<f32>(a, b));\n let sn: f32 = modulo(dt, 3.14159);\n return fract(sin(sn) * c + seed);\n}";
63779
63779
  var __defProp = Object.defineProperty;
@@ -63929,7 +63929,7 @@ const PixiFilters = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineP
63929
63929
  SimplexNoiseFilter: SimplexNoiseFilter$1,
63930
63930
  TiltShiftAxisFilter,
63931
63931
  TiltShiftFilter: TiltShiftFilter$1,
63932
- TwistFilter,
63932
+ TwistFilter: TwistFilter$1,
63933
63933
  ZoomBlurFilter: ZoomBlurFilter$1,
63934
63934
  angleFromCssOrientation,
63935
63935
  angleFromDirectionalValue,
@@ -64472,60 +64472,30 @@ registerFilter({
64472
64472
  }
64473
64473
  ]
64474
64474
  });
64475
+ const clamp01 = (n2) => Math.max(0, Math.min(1, n2));
64475
64476
  registerFilter({
64476
64477
  id: "alpha",
64477
64478
  name: "Alpha",
64478
64479
  category: "adjust",
64479
- description: "Adjust the transparency of the image",
64480
- // Create an instance of the ColorMatrixFilter with alpha adjustment
64480
+ description: "Adjust the opacity of the image",
64481
64481
  createFilter: (params) => {
64482
- try {
64483
- console.log("Creating Alpha filter with params:", params);
64484
- const alpha = params.alpha !== void 0 ? params.alpha : 1;
64485
- const filter = new ColorMatrixFilter$3();
64486
- filter.alpha = alpha;
64487
- filter._customParams = {
64488
- alpha
64489
- };
64490
- filter.updateUIParam = function(key, value) {
64491
- try {
64492
- console.log(`Alpha filter updateUIParam called: ${key} = ${value}`);
64493
- const customParams = this._customParams || {};
64494
- this._customParams = customParams;
64495
- if (key === "alpha") {
64496
- const numValue = Number(value);
64497
- this.alpha = numValue;
64498
- customParams.alpha = numValue;
64499
- console.log(`Updated alpha to ${numValue}`);
64500
- } else {
64501
- if (key in this) {
64502
- this[key] = value;
64503
- console.log(`Updated ${key} to ${value}`);
64504
- } else {
64505
- console.warn(`Unknown parameter for Alpha filter: ${key}`);
64506
- }
64507
- }
64508
- } catch (error) {
64509
- console.error(`Error updating Alpha filter parameter ${key}:`, error);
64510
- }
64511
- };
64512
- console.log("Alpha filter created successfully");
64513
- return filter;
64514
- } catch (error) {
64515
- console.error("Failed to create Alpha filter:", error);
64516
- return null;
64517
- }
64482
+ const alpha = clamp01(params.alpha !== void 0 ? Number(params.alpha) : 1);
64483
+ const filter = new AlphaFilter(alpha);
64484
+ filter.updateUIParam = function(key, value) {
64485
+ if (key === "alpha") {
64486
+ this.alpha = clamp01(Number(value));
64487
+ }
64488
+ };
64489
+ return filter;
64518
64490
  },
64519
- // Default parameter values
64520
64491
  defaultParams: {
64521
64492
  alpha: 1
64522
64493
  },
64523
- // UI controls for the filter
64524
64494
  controls: [
64525
64495
  {
64526
64496
  id: "alpha",
64527
64497
  type: "slider",
64528
- label: "Transparency",
64498
+ label: "Opacity",
64529
64499
  property: "alpha",
64530
64500
  min: 0,
64531
64501
  max: 1,
@@ -64611,57 +64581,80 @@ registerFilter({
64611
64581
  }
64612
64582
  ]
64613
64583
  });
64584
+ function applyColorMatrixParams(filter, params) {
64585
+ if (typeof filter.reset === "function") {
64586
+ filter.reset();
64587
+ }
64588
+ if (params.brightness !== 1) {
64589
+ filter.brightness(params.brightness, false);
64590
+ }
64591
+ if (params.contrast !== 1) {
64592
+ filter.contrast(params.contrast, false);
64593
+ }
64594
+ if (params.saturation !== 1) {
64595
+ filter.saturate(params.saturation, false);
64596
+ }
64597
+ if (params.hue !== 0) {
64598
+ filter.hue(params.hue, false);
64599
+ }
64600
+ if (params.sepia) {
64601
+ filter.sepia(false);
64602
+ }
64603
+ if (params.negative) {
64604
+ filter.negative(false);
64605
+ }
64606
+ }
64607
+ function normalizeParams(raw) {
64608
+ return {
64609
+ brightness: Number(raw.brightness ?? 1),
64610
+ contrast: Number(raw.contrast ?? 1),
64611
+ saturation: Number(raw.saturation ?? 1),
64612
+ hue: Number(raw.hue ?? 0),
64613
+ sepia: Boolean(raw.sepia),
64614
+ negative: Boolean(raw.negative)
64615
+ };
64616
+ }
64614
64617
  registerFilter({
64615
64618
  id: "color-matrix",
64616
64619
  name: "Color Matrix",
64617
64620
  category: "advanced",
64618
- description: "Advanced color adjustments including sepia, hue rotation, and more",
64619
- // Create an instance of the ColorMatrixFilter with the provided parameters
64621
+ description: "Advanced color adjustments including sepia, hue rotation, and negative",
64620
64622
  createFilter: (params) => {
64621
64623
  const MatrixFilterClass = ColorMatrixFilter$3;
64622
- if (!MatrixFilterClass) {
64623
- console.error("ColorMatrixFilter not available in PIXI");
64624
- return null;
64625
- }
64624
+ if (!MatrixFilterClass) return null;
64626
64625
  const filter = new MatrixFilterClass();
64627
- try {
64628
- if (typeof filter.reset === "function") {
64629
- filter.reset();
64630
- }
64631
- if (params.brightness !== 1 && typeof filter.brightness === "function") {
64632
- filter.brightness(params.brightness, false);
64633
- }
64634
- if (params.contrast !== 1 && typeof filter.contrast === "function") {
64635
- filter.contrast(params.contrast, false);
64636
- }
64637
- if (params.saturation !== 1 && typeof filter.saturate === "function") {
64638
- filter.saturate(params.saturation, false);
64639
- }
64640
- if (params.hue !== 0 && typeof filter.hue === "function") {
64641
- filter.hue(params.hue, false);
64642
- }
64643
- if (params.sepia > 0 && typeof filter.sepia === "function") {
64644
- filter.sepia(params.sepia);
64645
- }
64646
- if (params.negative && typeof filter.negative === "function") {
64647
- filter.negative(false);
64626
+ const normalized = normalizeParams(params);
64627
+ filter._customParams = { ...normalized };
64628
+ applyColorMatrixParams(filter, normalized);
64629
+ filter.updateUIParam = function(key, value) {
64630
+ const stored = this._customParams ?? normalizeParams({});
64631
+ switch (key) {
64632
+ case "brightness":
64633
+ case "contrast":
64634
+ case "saturation":
64635
+ case "hue":
64636
+ stored[key] = Number(value);
64637
+ break;
64638
+ case "sepia":
64639
+ case "negative":
64640
+ stored[key] = Boolean(value);
64641
+ break;
64642
+ default:
64643
+ return;
64648
64644
  }
64649
- return filter;
64650
- } catch (error) {
64651
- console.error("Error configuring ColorMatrixFilter:", error);
64652
- return filter;
64653
- }
64645
+ this._customParams = stored;
64646
+ applyColorMatrixParams(this, stored);
64647
+ };
64648
+ return filter;
64654
64649
  },
64655
- // Default parameter values
64656
64650
  defaultParams: {
64657
64651
  brightness: 1,
64658
64652
  contrast: 1,
64659
64653
  saturation: 1,
64660
64654
  hue: 0,
64661
- sepia: 0,
64655
+ sepia: false,
64662
64656
  negative: false
64663
64657
  },
64664
- // UI controls for the filter
64665
64658
  controls: [
64666
64659
  {
64667
64660
  id: "brightness",
@@ -64703,15 +64696,14 @@ registerFilter({
64703
64696
  step: 1,
64704
64697
  default: 0
64705
64698
  },
64699
+ // Sepia is a toggle in phase 1 — PIXI v8's ColorMatrixFilter.sepia()
64700
+ // doesn't accept a smooth intensity, so a slider here would be misleading.
64706
64701
  {
64707
64702
  id: "sepia",
64708
- type: "slider",
64703
+ type: "toggle",
64709
64704
  label: "Sepia",
64710
64705
  property: "sepia",
64711
- min: 0,
64712
- max: 1,
64713
- step: 0.01,
64714
- default: 0
64706
+ default: false
64715
64707
  },
64716
64708
  {
64717
64709
  id: "negative",
@@ -64829,10 +64821,11 @@ registerFilter({
64829
64821
  */
64830
64822
  createFilter: (params) => {
64831
64823
  try {
64832
- console.log("Creating Drop Shadow filter with params:", params);
64833
64824
  const color = params.color ? params.color.replace("#", "0x") : "0x000000";
64834
64825
  const distance = params.distance !== void 0 ? params.distance : 5;
64835
64826
  const angle = params.angle !== void 0 ? params.angle : 90;
64827
+ const blur = params.blur !== void 0 ? params.blur : 2;
64828
+ const quality = params.quality !== void 0 ? params.quality : 3;
64836
64829
  const offset = {
64837
64830
  x: distance * Math.cos(angle * Math.PI / 180),
64838
64831
  y: distance * Math.sin(angle * Math.PI / 180)
@@ -64841,14 +64834,21 @@ registerFilter({
64841
64834
  offset,
64842
64835
  color: parseInt(color, 16),
64843
64836
  alpha: params.alpha !== void 0 ? params.alpha : 0.5,
64844
- blur: params.blur !== void 0 ? params.blur : 2,
64845
- quality: params.quality !== void 0 ? params.quality : 3,
64837
+ blur,
64838
+ quality,
64846
64839
  shadowOnly: params.shadowOnly !== void 0 ? params.shadowOnly : false,
64847
64840
  pixelSize: {
64848
64841
  x: params.pixelSizeX !== void 0 ? params.pixelSizeX : 1,
64849
64842
  y: params.pixelSizeY !== void 0 ? params.pixelSizeY : 1
64850
64843
  }
64851
64844
  });
64845
+ const computePadding = (b2, q, offX, offY) => {
64846
+ const kernel = b2 * q * 2;
64847
+ const mag = Math.sqrt(offX * offX + offY * offY);
64848
+ return Math.ceil(kernel + mag + 2);
64849
+ };
64850
+ filter._exportPadding = computePadding(blur, quality, offset.x, offset.y);
64851
+ filter.padding = Math.max(filter.padding ?? 0, filter._exportPadding);
64852
64852
  filter._customParams = {
64853
64853
  ...params,
64854
64854
  // Also store the calculated offset for use in updateUIParam
@@ -64856,6 +64856,50 @@ registerFilter({
64856
64856
  _distance: distance,
64857
64857
  _angle: angle
64858
64858
  };
64859
+ const createScaledForExport = (rawParams, scale) => {
64860
+ const dist = Number(rawParams._distance ?? rawParams.distance ?? 5) * scale;
64861
+ const ang = Number(rawParams._angle ?? rawParams.angle ?? 90);
64862
+ const blr = Number(rawParams.blur ?? 2) * scale;
64863
+ const qual = Number(rawParams.quality ?? 3);
64864
+ const off = {
64865
+ x: dist * Math.cos(ang * Math.PI / 180),
64866
+ y: dist * Math.sin(ang * Math.PI / 180)
64867
+ };
64868
+ const colorStr = rawParams.color ? String(rawParams.color).replace("#", "0x") : "0x000000";
64869
+ const next = new DropShadowFilter({
64870
+ offset: off,
64871
+ color: parseInt(colorStr, 16),
64872
+ alpha: rawParams.alpha !== void 0 ? Number(rawParams.alpha) : 0.5,
64873
+ blur: blr,
64874
+ quality: qual,
64875
+ shadowOnly: rawParams.shadowOnly !== void 0 ? Boolean(rawParams.shadowOnly) : false,
64876
+ pixelSize: {
64877
+ x: rawParams.pixelSizeX !== void 0 ? Number(rawParams.pixelSizeX) : 1,
64878
+ y: rawParams.pixelSizeY !== void 0 ? Number(rawParams.pixelSizeY) : 1
64879
+ }
64880
+ });
64881
+ const padding = computePadding(blr, qual, off.x, off.y);
64882
+ next._exportPadding = padding;
64883
+ next.padding = Math.max(next.padding ?? 0, padding);
64884
+ return next;
64885
+ };
64886
+ filter.createExportFilter = function(options = {}) {
64887
+ const scale = Number.isFinite(options.previewToNativeScale) ? Math.max(1, Number(options.previewToNativeScale)) : 1;
64888
+ const stored = this._customParams || params;
64889
+ return createScaledForExport(stored, scale);
64890
+ };
64891
+ filter.getExportPadding = function() {
64892
+ return Number(this._exportPadding || this.padding || 0);
64893
+ };
64894
+ const refreshPadding = (f2) => {
64895
+ var _a;
64896
+ const off = ((_a = f2._customParams) == null ? void 0 : _a._offset) ?? { x: 0, y: 0 };
64897
+ const b2 = Number(f2.blur ?? 0);
64898
+ const q = Number(f2.quality ?? 1);
64899
+ const p2 = computePadding(b2, q, off.x, off.y);
64900
+ f2._exportPadding = p2;
64901
+ f2.padding = Math.max(f2.padding ?? 0, p2);
64902
+ };
64859
64903
  filter.updateUIParam = function(key, value) {
64860
64904
  try {
64861
64905
  const customParams = this._customParams || {};
@@ -64864,43 +64908,39 @@ registerFilter({
64864
64908
  switch (key) {
64865
64909
  case "color":
64866
64910
  if (typeof value === "string") {
64867
- const colorNum = parseInt(value.replace("#", "0x"), 16);
64868
- this.color = colorNum;
64869
- console.log(`Updated color to ${value} (${colorNum})`);
64911
+ this.color = parseInt(value.replace("#", "0x"), 16);
64870
64912
  }
64871
64913
  break;
64872
64914
  case "alpha":
64873
64915
  this.alpha = Number(value);
64874
- console.log(`Updated alpha to ${value}`);
64875
64916
  break;
64876
64917
  case "blur":
64877
64918
  this.blur = Number(value);
64878
- console.log(`Updated blur to ${value}`);
64919
+ refreshPadding(this);
64879
64920
  break;
64880
64921
  case "quality":
64881
64922
  this.quality = Number(value);
64882
- console.log(`Updated quality to ${value}`);
64923
+ refreshPadding(this);
64883
64924
  break;
64884
64925
  case "shadowOnly":
64885
64926
  this.shadowOnly = Boolean(value);
64886
- console.log(`Updated shadowOnly to ${value}`);
64887
64927
  break;
64888
64928
  case "pixelSizeX":
64889
64929
  this.pixelSizeX = Number(value);
64890
64930
  customParams.pixelSizeX = Number(value);
64891
- console.log(`Updated pixelSizeX to ${value}`);
64892
64931
  break;
64893
64932
  case "pixelSizeY":
64894
64933
  this.pixelSizeY = Number(value);
64895
64934
  customParams.pixelSizeY = Number(value);
64896
- console.log(`Updated pixelSizeY to ${value}`);
64897
64935
  break;
64898
64936
  case "distance":
64899
- case "angle":
64937
+ case "angle": {
64900
64938
  if (key === "distance") {
64901
64939
  customParams._distance = Number(value);
64940
+ customParams.distance = Number(value);
64902
64941
  } else {
64903
64942
  customParams._angle = Number(value);
64943
+ customParams.angle = Number(value);
64904
64944
  }
64905
64945
  const newOffset = {
64906
64946
  x: customParams._distance * Math.cos(customParams._angle * Math.PI / 180),
@@ -64908,27 +64948,23 @@ registerFilter({
64908
64948
  };
64909
64949
  customParams._offset = newOffset;
64910
64950
  this.offset = newOffset;
64911
- console.log(`Updated offset to (${newOffset.x}, ${newOffset.y}) from ${key}=${value}`);
64951
+ refreshPadding(this);
64912
64952
  break;
64953
+ }
64913
64954
  default:
64914
64955
  if (key in this) {
64915
64956
  this[key] = value;
64916
- console.log(`Updated ${key} to ${value}`);
64917
- } else {
64918
- console.warn(`Attempted to update unknown property ${key}`);
64919
64957
  }
64920
64958
  break;
64921
64959
  }
64922
64960
  return true;
64923
- } catch (error) {
64924
- console.error(`Failed to update parameter ${key} using updateUIParam:`, error);
64961
+ } catch {
64925
64962
  if (this._customParams) {
64926
64963
  this._customParams[key] = value;
64927
64964
  }
64928
64965
  return false;
64929
64966
  }
64930
64967
  };
64931
- console.log("Drop Shadow filter created successfully");
64932
64968
  return filter;
64933
64969
  } catch (error) {
64934
64970
  console.error("Failed to create Drop Shadow filter:", error);
@@ -65955,261 +65991,200 @@ registerFilter({
65955
65991
  ]
65956
65992
  });
65957
65993
  const { ColorGradientFilter } = PixiFilters;
65994
+ function hexToInt(hex) {
65995
+ return parseInt(hex.replace("#", "0x"), 16);
65996
+ }
65997
+ function intToHex(num) {
65998
+ return "#" + num.toString(16).padStart(6, "0");
65999
+ }
66000
+ function normalizeStopForFilter(stop) {
66001
+ return {
66002
+ offset: Number(stop.offset),
66003
+ color: typeof stop.color === "string" ? hexToInt(stop.color) : Number(stop.color),
66004
+ alpha: Number(stop.alpha)
66005
+ };
66006
+ }
66007
+ function hexStopFromFilter(stop) {
66008
+ return {
66009
+ offset: stop.offset,
66010
+ color: typeof stop.color === "number" ? intToHex(stop.color) : stop.color,
66011
+ alpha: stop.alpha
66012
+ };
66013
+ }
65958
66014
  registerFilter({
65959
66015
  id: "color-gradient",
65960
66016
  name: "Color Gradient",
65961
66017
  category: "color",
65962
66018
  description: "Applies a linear, radial or conic color gradient over the image with multiple color stops",
65963
- /**
65964
- * Create an instance of the ColorGradientFilter with the provided parameters
65965
- * This implementation supports unlimited color stops like the PixiJS example app
65966
- */
65967
66019
  createFilter: (params) => {
65968
- try {
65969
- console.log("Creating ColorGradientFilter with params:", params);
65970
- let stops = [];
65971
- stops = (params.colorStops || [
65972
- { offset: 0, color: "#ff0000", alpha: 1 },
65973
- { offset: 1, color: "#0000ff", alpha: 1 }
65974
- ]).map((stop) => ({
65975
- offset: stop.offset,
65976
- color: typeof stop.color === "string" ? parseInt(stop.color.replace("#", "0x")) : stop.color,
65977
- alpha: stop.alpha
65978
- }));
65979
- if (stops.length < 2) {
65980
- console.warn("ColorGradientFilter requires at least 2 stops, adding default stops");
65981
- stops = [
65982
- { offset: 0, color: 16711680, alpha: 1 },
65983
- { offset: 1, color: 255, alpha: 1 }
65984
- ];
65985
- }
65986
- stops.sort((a2, b2) => a2.offset - b2.offset);
65987
- const filter = new ColorGradientFilter({
65988
- type: params.gradientType,
65989
- // 0: linear, 1: radial, 2: conic
65990
- stops,
65991
- angle: params.angle,
65992
- alpha: params.alpha,
65993
- maxColors: params.maxColors || 0,
65994
- replace: params.replace
65995
- });
65996
- filter._customParams = { ...params };
65997
- filter.getColorStopsForUI = function() {
65998
- console.log("FILTER getColorStopsForUI - Called", {
65999
- hasStops: !!this.stops,
66000
- stopsLength: this.stops ? this.stops.length : 0,
66001
- firstStop: this.stops && this.stops.length > 0 ? this.stops[0] : null
66002
- });
66003
- if (!this.stops || !Array.isArray(this.stops)) {
66004
- console.error("FILTER getColorStopsForUI - No stops array found on filter instance!");
66005
- return [];
66006
- }
66007
- const result = this.stops.map((stop) => ({
66008
- offset: stop.offset,
66009
- color: typeof stop.color === "number" ? "#" + stop.color.toString(16).padStart(6, "0") : stop.color,
66010
- alpha: stop.alpha
66011
- }));
66012
- console.log("FILTER getColorStopsForUI - Returning formatted stops:", result);
66013
- return result;
66020
+ const incomingStops = params.colorStops || [
66021
+ { offset: 0, color: "#ff0000", alpha: 1 },
66022
+ { offset: 1, color: "#0000ff", alpha: 1 }
66023
+ ];
66024
+ let stops = incomingStops.map(normalizeStopForFilter);
66025
+ if (stops.length < 2) {
66026
+ stops = [
66027
+ { offset: 0, color: 16711680, alpha: 1 },
66028
+ { offset: 1, color: 255, alpha: 1 }
66029
+ ];
66030
+ }
66031
+ stops.sort((a2, b2) => a2.offset - b2.offset);
66032
+ const filter = new ColorGradientFilter({
66033
+ type: params.gradientType,
66034
+ stops,
66035
+ angle: params.angle,
66036
+ alpha: params.alpha,
66037
+ maxColors: params.maxColors || 0,
66038
+ replace: params.replace
66039
+ });
66040
+ filter._customParams = {
66041
+ cssGradient: params.cssGradient || ""
66042
+ };
66043
+ filter.getSerializableParams = function() {
66044
+ var _a;
66045
+ const stopsHex = Array.isArray(this.stops) ? this.stops.map(hexStopFromFilter) : [];
66046
+ return {
66047
+ gradientType: this.type,
66048
+ colorStops: stopsHex,
66049
+ angle: this.angle,
66050
+ alpha: this.alpha,
66051
+ maxColors: this.maxColors,
66052
+ replace: this.replace,
66053
+ cssGradient: ((_a = this._customParams) == null ? void 0 : _a.cssGradient) ?? ""
66014
66054
  };
66015
- filter.getDynamicControls = function() {
66016
- console.log("FILTER getDynamicControls - Called on filter instance", this);
66017
- const colorStops = this.getColorStopsForUI();
66018
- console.log("FILTER getDynamicControls - Retrieved colorStops:", colorStops);
66019
- const controls = [];
66020
- colorStops.forEach((stop, index) => {
66021
- console.log(`FILTER getDynamicControls - Creating controls for stop ${index}:`, stop);
66022
- controls.push({
66023
- id: `colorStop-${index}-color`,
66024
- type: "color",
66025
- label: `Stop ${index + 1} Color`,
66026
- property: `colorStops[${index}].color`,
66027
- default: stop.color
66028
- });
66029
- controls.push({
66030
- id: `colorStop-${index}-offset`,
66031
- type: "slider",
66032
- label: `Stop ${index + 1} Position`,
66033
- property: `colorStops[${index}].offset`,
66034
- min: 0,
66035
- max: 1,
66036
- step: 0.01,
66037
- default: stop.offset
66038
- });
66039
- controls.push({
66040
- id: `colorStop-${index}-alpha`,
66041
- type: "slider",
66042
- label: `Stop ${index + 1} Alpha`,
66043
- property: `colorStops[${index}].alpha`,
66044
- min: 0,
66045
- max: 1,
66046
- step: 0.01,
66047
- default: stop.alpha
66048
- });
66055
+ };
66056
+ filter.getColorStopsForUI = function() {
66057
+ if (!Array.isArray(this.stops)) return [];
66058
+ return this.stops.map(hexStopFromFilter);
66059
+ };
66060
+ filter.getDynamicControls = function() {
66061
+ const colorStops = this.getColorStopsForUI();
66062
+ const controls = [];
66063
+ colorStops.forEach((stop, index) => {
66064
+ controls.push({
66065
+ id: `colorStop-${index}-color`,
66066
+ type: "color",
66067
+ label: `Stop ${index + 1} Color`,
66068
+ property: `colorStops[${index}].color`,
66069
+ default: stop.color
66049
66070
  });
66050
- console.log("FILTER getDynamicControls - Returning controls array:", controls.length, "items");
66051
- return controls;
66052
- };
66053
- filter.handleButtonAction = function(action) {
66054
- console.log(`Handling button action: ${action}`);
66055
- if (action === "addColorStop" || action === "removeColorStop") {
66056
- this.updateUIParam(action, true);
66057
- }
66058
- };
66059
- filter.updateUIParam = function(key, value) {
66060
- const customParams = this._customParams || {};
66061
- this._customParams = customParams;
66062
- customParams[key] = value;
66063
- switch (key) {
66064
- case "gradientType":
66065
- this.type = value;
66066
- break;
66067
- case "angle":
66068
- case "alpha":
66069
- case "maxColors":
66070
- case "replace":
66071
- if (key in this) {
66072
- this[key] = value;
66073
- }
66074
- break;
66075
- case "addColorStop":
66076
- console.log("Add color stop command received");
66077
- const currentStopsForAdd = [...this.stops];
66078
- const getRandomColor = () => {
66079
- const r2 = Math.floor(Math.random() * 255);
66080
- const g2 = Math.floor(Math.random() * 255);
66081
- const b2 = Math.floor(Math.random() * 255);
66082
- return r2 << 16 | g2 << 8 | b2;
66083
- };
66084
- const randomColor = getRandomColor();
66085
- const newColorStop = {
66086
- offset: 1,
66087
- // Will be adjusted later
66088
- alpha: 1,
66089
- color: randomColor
66090
- };
66091
- if (currentStopsForAdd.length > 0) {
66092
- const scaleFactor = 0.8;
66093
- for (let i2 = 0; i2 < currentStopsForAdd.length; i2++) {
66094
- currentStopsForAdd[i2].offset *= scaleFactor;
66095
- }
66096
- newColorStop.offset = 1;
66097
- }
66098
- currentStopsForAdd.push(newColorStop);
66099
- this.stops = currentStopsForAdd;
66100
- customParams.colorStops = this.stops.map((stop) => ({
66101
- offset: stop.offset,
66102
- color: typeof stop.color === "number" ? "#" + stop.color.toString(16).padStart(6, "0") : stop.color,
66103
- alpha: stop.alpha
66104
- }));
66105
- console.log("Added new color stop, total stops:", this.stops.length, "Updated colorStops:", customParams.colorStops);
66106
- break;
66107
- case "removeColorStop":
66108
- console.log("Remove color stop command received");
66109
- const currentStopsForRemove = [...this.stops];
66110
- if (currentStopsForRemove.length > 2) {
66111
- currentStopsForRemove.pop();
66112
- this.stops = currentStopsForRemove;
66113
- customParams.colorStops = this.stops.map((stop) => ({
66114
- offset: stop.offset,
66115
- color: typeof stop.color === "number" ? "#" + stop.color.toString(16).padStart(6, "0") : stop.color,
66116
- alpha: stop.alpha
66117
- }));
66118
- console.log("Removed last color stop, remaining stops:", this.stops.length, "Updated colorStops:", customParams.colorStops);
66119
- } else {
66120
- console.warn("Cannot remove stop - minimum of 2 stops required");
66121
- }
66122
- break;
66123
- case "cssGradient":
66124
- if (value && typeof value === "string" && value.trim() !== "") {
66125
- try {
66126
- const tempFilter = new ColorGradientFilter({ css: value });
66127
- this.type = tempFilter.type;
66128
- this.angle = tempFilter.angle;
66129
- this.stops = [...tempFilter.stops];
66130
- customParams.colorStops = this.stops.map((stop) => ({
66131
- offset: stop.offset,
66132
- color: typeof stop.color === "number" ? "#" + stop.color.toString(16).padStart(6, "0") : stop.color,
66133
- alpha: stop.alpha
66134
- }));
66135
- console.log("Applied CSS gradient, new stops:", this.stops.length, "Updated colorStops:", customParams.colorStops);
66136
- } catch (error) {
66137
- console.error("Failed to parse CSS gradient:", error);
66138
- }
66139
- }
66140
- break;
66141
- case "colorStops":
66142
- if (Array.isArray(value)) {
66143
- const processedStops = value.map((stop) => ({
66144
- offset: stop.offset,
66145
- color: typeof stop.color === "string" ? parseInt(stop.color.replace("#", "0x")) : stop.color,
66146
- alpha: stop.alpha
66147
- }));
66148
- processedStops.sort((a2, b2) => a2.offset - b2.offset);
66149
- this.stops = processedStops;
66071
+ controls.push({
66072
+ id: `colorStop-${index}-offset`,
66073
+ type: "slider",
66074
+ label: `Stop ${index + 1} Position`,
66075
+ property: `colorStops[${index}].offset`,
66076
+ min: 0,
66077
+ max: 1,
66078
+ step: 0.01,
66079
+ default: stop.offset
66080
+ });
66081
+ controls.push({
66082
+ id: `colorStop-${index}-alpha`,
66083
+ type: "slider",
66084
+ label: `Stop ${index + 1} Alpha`,
66085
+ property: `colorStops[${index}].alpha`,
66086
+ min: 0,
66087
+ max: 1,
66088
+ step: 0.01,
66089
+ default: stop.alpha
66090
+ });
66091
+ });
66092
+ return controls;
66093
+ };
66094
+ filter.handleButtonAction = function(action) {
66095
+ if (action === "addColorStop" || action === "removeColorStop") {
66096
+ this.updateUIParam(action, true);
66097
+ }
66098
+ };
66099
+ filter.updateUIParam = function(key, value) {
66100
+ const customParams = this._customParams ?? {};
66101
+ this._customParams = customParams;
66102
+ switch (key) {
66103
+ case "gradientType":
66104
+ this.type = Number(value);
66105
+ return;
66106
+ case "angle":
66107
+ case "alpha":
66108
+ case "maxColors":
66109
+ this[key] = Number(value);
66110
+ return;
66111
+ case "replace":
66112
+ this.replace = Boolean(value);
66113
+ return;
66114
+ case "addColorStop": {
66115
+ const current = Array.isArray(this.stops) ? [...this.stops] : [];
66116
+ const randomColor = Math.floor(Math.random() * 255) << 16 | Math.floor(Math.random() * 255) << 8 | Math.floor(Math.random() * 255);
66117
+ if (current.length > 0) {
66118
+ const scale = 0.8;
66119
+ for (const stop of current) {
66120
+ stop.offset *= scale;
66150
66121
  }
66151
- break;
66152
- default:
66153
- if (/colorStops\[\d+\]\..*/.test(key)) {
66154
- const match = key.match(/colorStops\[(\d+)\]\.(.*)/);
66155
- if (match) {
66156
- const [_, indexStr, prop] = match;
66157
- const index = parseInt(indexStr);
66158
- const updatedStops = [...this.stops];
66159
- if (index >= 0 && index < updatedStops.length) {
66160
- if (prop === "color" && typeof value === "string") {
66161
- updatedStops[index].color = parseInt(value.replace("#", "0x"));
66162
- console.log(`Converting color ${value} to ${updatedStops[index].color}`);
66163
- } else if (prop === "offset" || prop === "alpha") {
66164
- updatedStops[index][prop] = value;
66165
- }
66166
- this.stops = updatedStops;
66167
- this.stops.sort((a2, b2) => a2.offset - b2.offset);
66168
- customParams.colorStops = this.getColorStopsForUI();
66169
- console.log(`Updated color stop ${index}.${prop} to ${value}`);
66170
- console.log("New stops array:", this.stops);
66171
- console.log("New colorStops for UI:", customParams.colorStops);
66172
- return true;
66173
- } else {
66174
- console.warn(`Color stop index out of range: ${index}, stops length: ${updatedStops.length}`);
66175
- }
66176
- }
66177
- } else if (key in this) {
66178
- this[key] = value;
66122
+ }
66123
+ current.push({ offset: 1, color: randomColor, alpha: 1 });
66124
+ current.sort((a2, b2) => a2.offset - b2.offset);
66125
+ this.stops = current;
66126
+ return;
66127
+ }
66128
+ case "removeColorStop": {
66129
+ const current = Array.isArray(this.stops) ? [...this.stops] : [];
66130
+ if (current.length > 2) {
66131
+ current.pop();
66132
+ this.stops = current;
66133
+ }
66134
+ return;
66135
+ }
66136
+ case "cssGradient": {
66137
+ const str = typeof value === "string" ? value.trim() : "";
66138
+ customParams.cssGradient = str;
66139
+ if (!str) return;
66140
+ try {
66141
+ const parsed = new ColorGradientFilter({ css: str });
66142
+ this.type = parsed.type;
66143
+ this.angle = parsed.angle;
66144
+ this.stops = [...parsed.stops];
66145
+ } catch {
66146
+ }
66147
+ return;
66148
+ }
66149
+ case "colorStops": {
66150
+ if (!Array.isArray(value)) return;
66151
+ const normalized = value.map(normalizeStopForFilter);
66152
+ normalized.sort((a2, b2) => a2.offset - b2.offset);
66153
+ this.stops = normalized;
66154
+ return;
66155
+ }
66156
+ default: {
66157
+ const dynamicMatch = /^colorStops\[(\d+)\]\.(offset|color|alpha)$/.exec(key);
66158
+ if (dynamicMatch) {
66159
+ const [, indexStr, prop] = dynamicMatch;
66160
+ const index = Number(indexStr);
66161
+ const stops2 = Array.isArray(this.stops) ? [...this.stops] : [];
66162
+ if (index < 0 || index >= stops2.length) return;
66163
+ if (prop === "color") {
66164
+ stops2[index].color = typeof value === "string" ? hexToInt(value) : Number(value);
66165
+ } else if (prop === "offset" || prop === "alpha") {
66166
+ stops2[index][prop] = Number(value);
66179
66167
  }
66180
- break;
66168
+ this.stops = stops2;
66169
+ }
66181
66170
  }
66182
- };
66183
- console.log("Enhanced ColorGradientFilter created successfully");
66184
- return filter;
66185
- } catch (error) {
66186
- console.error("Failed to create ColorGradientFilter:", error);
66187
- return null;
66188
- }
66171
+ }
66172
+ };
66173
+ return filter;
66189
66174
  },
66190
- // Default parameter values - matching the example app
66191
66175
  defaultParams: {
66192
66176
  gradientType: 0,
66193
- // 0: linear, 1: radial, 2: conic
66194
66177
  colorStops: [
66195
66178
  { offset: 0, color: "#ff0000", alpha: 1 },
66196
66179
  { offset: 1, color: "#0000ff", alpha: 1 }
66197
66180
  ],
66198
- // Controls for adding/removing stops
66199
- addColorStop: false,
66200
- // Button trigger for adding a color stop
66201
- removeColorStop: false,
66202
- // Button trigger for removing a color stop
66203
66181
  cssGradient: "",
66204
- // CSS gradient string input
66205
66182
  angle: 90,
66206
66183
  alpha: 0.75,
66207
66184
  maxColors: 0,
66208
66185
  replace: false
66209
66186
  },
66210
- // UI controls for the filter
66211
66187
  controls: [
66212
- // Basic gradient controls
66213
66188
  {
66214
66189
  id: "gradientType",
66215
66190
  type: "select",
@@ -66259,7 +66234,6 @@ registerFilter({
66259
66234
  property: "replace",
66260
66235
  default: false
66261
66236
  },
66262
- // Advanced color stop management - simpler approach
66263
66237
  {
66264
66238
  id: "addColorStop",
66265
66239
  type: "button",
@@ -66278,7 +66252,7 @@ registerFilter({
66278
66252
  label: "CSS Gradient",
66279
66253
  property: "cssGradient",
66280
66254
  default: "",
66281
- tooltip: 'Enter a CSS gradient like "linear-gradient(to right, red, blue)"'
66255
+ placeholder: "e.g. linear-gradient(to right, red, blue)"
66282
66256
  }
66283
66257
  ]
66284
66258
  });
@@ -67549,29 +67523,37 @@ registerFilter({
67549
67523
  ]
67550
67524
  });
67551
67525
  const { BulgePinchFilter } = PixiFilters;
67526
+ const computeBulgePadding = (radius) => {
67527
+ const r2 = Math.max(0, Number(radius) || 0);
67528
+ return Math.ceil(r2 + 2);
67529
+ };
67552
67530
  registerFilter({
67553
67531
  id: "bulge-pinch",
67554
- // ID must match what the application expects
67555
67532
  name: "Bulge/Pinch",
67556
67533
  category: "distortion",
67557
67534
  description: "Creates a bulge or pinch effect in a circular area",
67558
- // Create an instance of the BulgePinchFilter with the provided parameters
67559
67535
  createFilter: (params) => {
67560
67536
  try {
67561
- console.log("Creating BulgePinchFilter with params:", params);
67562
67537
  const centerX = params.centerX ?? 0.5;
67563
67538
  const centerY = params.centerY ?? 0.5;
67564
67539
  const radius = params.radius ?? 100;
67565
67540
  const strength = params.strength ?? 1;
67566
67541
  const filter = new BulgePinchFilter({
67567
- center: {
67568
- x: centerX,
67569
- y: centerY
67570
- },
67542
+ center: { x: centerX, y: centerY },
67571
67543
  radius,
67572
67544
  strength
67573
67545
  });
67546
+ const padding = computeBulgePadding(radius);
67547
+ filter.padding = Math.max(filter.padding ?? 0, padding);
67548
+ filter._exportPadding = padding;
67574
67549
  filter._customParams = { ...params };
67550
+ const refreshPadding = (f2) => {
67551
+ var _a;
67552
+ const r2 = Number(((_a = f2._customParams) == null ? void 0 : _a.radius) ?? f2.radius ?? 0);
67553
+ const p2 = computeBulgePadding(r2);
67554
+ f2._exportPadding = p2;
67555
+ f2.padding = Math.max(f2.padding ?? 0, p2);
67556
+ };
67575
67557
  filter.updateUIParam = function(key, value) {
67576
67558
  const customParams = this._customParams || {};
67577
67559
  this._customParams = customParams;
@@ -67580,44 +67562,58 @@ registerFilter({
67580
67562
  case "centerX":
67581
67563
  if (!this.center) this.center = { x: 0.5, y: 0.5 };
67582
67564
  this.center.x = value;
67583
- console.log(`Updated center.x to ${value}`);
67584
67565
  break;
67585
67566
  case "centerY":
67586
67567
  if (!this.center) this.center = { x: 0.5, y: 0.5 };
67587
67568
  this.center.y = value;
67588
- console.log(`Updated center.y to ${value}`);
67589
67569
  break;
67590
67570
  case "radius":
67571
+ this.radius = Number(value);
67572
+ refreshPadding(this);
67573
+ break;
67591
67574
  case "strength":
67592
- this[key] = value;
67593
- console.log(`Updated ${key} to ${value}`);
67575
+ this.strength = Number(value);
67594
67576
  break;
67595
67577
  default:
67596
67578
  if (key in this) {
67597
67579
  this[key] = value;
67598
- console.log(`Updated ${key} to ${value}`);
67599
- } else {
67600
- console.warn(`Attempted to update unknown property ${key}`);
67601
67580
  }
67602
67581
  break;
67603
67582
  }
67604
67583
  return true;
67605
67584
  };
67606
- console.log("BulgePinchFilter created successfully with updateUIParam method");
67585
+ filter.createExportFilter = function(options = {}) {
67586
+ const scale = Number.isFinite(options.previewToNativeScale) ? Math.max(1, Number(options.previewToNativeScale)) : 1;
67587
+ const stored = this._customParams || params;
67588
+ const scaledRadius = Number(stored.radius ?? 100) * scale;
67589
+ const next = new BulgePinchFilter({
67590
+ center: {
67591
+ x: Number(stored.centerX ?? 0.5),
67592
+ y: Number(stored.centerY ?? 0.5)
67593
+ },
67594
+ radius: scaledRadius,
67595
+ strength: Number(stored.strength ?? 1)
67596
+ });
67597
+ const padding2 = computeBulgePadding(scaledRadius);
67598
+ next._exportPadding = padding2;
67599
+ next.padding = Math.max(next.padding ?? 0, padding2);
67600
+ return next;
67601
+ };
67602
+ filter.getExportPadding = function() {
67603
+ return Number(this._exportPadding || this.padding || 0);
67604
+ };
67607
67605
  return filter;
67608
67606
  } catch (error) {
67609
67607
  console.error("Failed to create BulgePinchFilter:", error);
67610
67608
  return null;
67611
67609
  }
67612
67610
  },
67613
- // Default parameter values
67614
67611
  defaultParams: {
67615
67612
  centerX: 0.5,
67616
67613
  centerY: 0.5,
67617
67614
  radius: 100,
67618
67615
  strength: 1
67619
67616
  },
67620
- // UI controls for the filter
67621
67617
  controls: [
67622
67618
  {
67623
67619
  id: "centerX",
@@ -70454,131 +70450,31 @@ registerFilter({
70454
70450
  }
70455
70451
  ]
70456
70452
  });
70457
- class NormalizedTwistFilter extends Filter {
70458
- constructor(params) {
70459
- const glProgram2 = GlProgram.from({
70460
- vertex: `
70461
- attribute vec2 aPosition;
70462
- varying vec2 vTextureCoord;
70463
- uniform mat3 projectionMatrix;
70464
-
70465
- void main() {
70466
- vTextureCoord = aPosition;
70467
- gl_Position = vec4((projectionMatrix * vec3(aPosition, 1.0)).xy, 0.0, 1.0);
70468
- }
70469
- `,
70470
- fragment: `
70471
- precision mediump float;
70472
-
70473
- varying vec2 vTextureCoord;
70474
- uniform sampler2D uSampler;
70475
-
70476
- uniform vec2 uCenter;
70477
- uniform float uRadius;
70478
- uniform float uAngle;
70479
-
70480
- void main() {
70481
- vec2 coord = vTextureCoord;
70482
- vec2 dir = coord - uCenter;
70483
- float dist = length(dir);
70484
-
70485
- if (dist < uRadius) {
70486
- float percent = (uRadius - dist) / uRadius;
70487
- float theta = percent * percent * uAngle;
70488
- float s = sin(theta);
70489
- float c = cos(theta);
70490
-
70491
- dir = vec2(
70492
- dir.x * c - dir.y * s,
70493
- dir.x * s + dir.y * c
70494
- );
70495
-
70496
- coord = dir + uCenter;
70497
- }
70498
-
70499
- gl_FragColor = texture2D(uSampler, coord);
70500
- }
70501
- `
70502
- });
70503
- super({
70504
- glProgram: glProgram2,
70505
- resources: {
70506
- twistUniforms: {
70507
- uCenter: { type: "vec2<f32>", value: [0.5, 0.5] },
70508
- uRadius: { type: "f32", value: 0.25 },
70509
- uAngle: { type: "f32", value: 4 }
70510
- }
70511
- }
70512
- });
70513
- this._centerX = 0.5;
70514
- this._centerY = 0.5;
70515
- this._radius = 0.25;
70516
- this._angle = 4;
70517
- if (params) {
70518
- if (params.centerX !== void 0) this._centerX = params.centerX;
70519
- if (params.centerY !== void 0) this._centerY = params.centerY;
70520
- if (params.radius !== void 0) this._radius = params.radius;
70521
- if (params.angle !== void 0) this._angle = params.angle;
70522
- }
70523
- this._updateUniforms();
70524
- }
70525
- _updateUniforms() {
70526
- if (this.uniforms) {
70527
- this.uniforms.uCenter = [this._centerX, this._centerY];
70528
- this.uniforms.uRadius = this._radius;
70529
- this.uniforms.uAngle = this._angle;
70530
- }
70531
- }
70532
- get centerX() {
70533
- return this._centerX;
70534
- }
70535
- set centerX(value) {
70536
- this._centerX = value;
70537
- this._updateUniforms();
70538
- }
70539
- get centerY() {
70540
- return this._centerY;
70541
- }
70542
- set centerY(value) {
70543
- this._centerY = value;
70544
- this._updateUniforms();
70545
- }
70546
- get radius() {
70547
- return this._radius;
70548
- }
70549
- set radius(value) {
70550
- this._radius = value;
70551
- this._updateUniforms();
70552
- }
70553
- get angle() {
70554
- return this._angle;
70555
- }
70556
- set angle(value) {
70557
- this._angle = value;
70558
- this._updateUniforms();
70559
- }
70560
- /**
70561
- * Handle UI parameter updates
70562
- */
70563
- updateUIParam(key, value) {
70564
- switch (key) {
70565
- case "centerX":
70566
- this.centerX = value;
70567
- break;
70568
- case "centerY":
70569
- this.centerY = value;
70570
- break;
70571
- case "radius":
70572
- this.radius = value;
70573
- break;
70574
- case "angle":
70575
- this.angle = value;
70576
- break;
70577
- default:
70578
- console.warn(`Unknown parameter '${key}' for TwistFilter`);
70579
- }
70580
- }
70581
- }
70453
+ const { TwistFilter } = PixiFilters;
70454
+ const computeTwistPadding = (radiusPx) => {
70455
+ return Math.ceil(Math.max(0, Number(radiusPx) || 0) + 2);
70456
+ };
70457
+ const buildTwistFilter = (raw, previewToNativeScale = 1) => {
70458
+ const width = Math.max(1, Number(raw._sourceWidth ?? 0));
70459
+ const height = Math.max(1, Number(raw._sourceHeight ?? 0));
70460
+ const minEdge = Math.min(width, height);
70461
+ const centerX = Number(raw.centerX ?? 0.5);
70462
+ const centerY = Number(raw.centerY ?? 0.5);
70463
+ const normalizedRadius = Number(raw.radius ?? 0.25);
70464
+ const angle = Number(raw.angle ?? 4);
70465
+ const offsetX = centerX * width * previewToNativeScale;
70466
+ const offsetY = centerY * height * previewToNativeScale;
70467
+ const radiusPx = normalizedRadius * minEdge * previewToNativeScale;
70468
+ const filter = new TwistFilter({
70469
+ offset: { x: offsetX, y: offsetY },
70470
+ radius: radiusPx,
70471
+ angle
70472
+ });
70473
+ const padding = computeTwistPadding(radiusPx);
70474
+ filter.padding = Math.max(filter.padding ?? 0, padding);
70475
+ filter._exportPadding = padding;
70476
+ return filter;
70477
+ };
70582
70478
  registerFilter({
70583
70479
  id: "twist",
70584
70480
  name: "Twist",
@@ -70586,33 +70482,59 @@ registerFilter({
70586
70482
  description: "Creates a twisting distortion effect around a central point",
70587
70483
  createFilter: (params) => {
70588
70484
  try {
70589
- console.log("Creating TwistFilter with params:", params);
70590
- const filter = new NormalizedTwistFilter({
70485
+ const raw = {
70591
70486
  centerX: params.centerX ?? 0.5,
70592
70487
  centerY: params.centerY ?? 0.5,
70593
70488
  radius: params.radius ?? 0.25,
70594
- angle: params.angle ?? 4
70595
- });
70596
- filter._customParams = { ...params };
70597
- console.log("TwistFilter created successfully");
70489
+ angle: params.angle ?? 4,
70490
+ _sourceWidth: params._sourceWidth,
70491
+ _sourceHeight: params._sourceHeight
70492
+ };
70493
+ const filter = buildTwistFilter(raw, 1);
70494
+ filter._customParams = { ...raw };
70495
+ filter.updateUIParam = function(key, value) {
70496
+ const customParams = this._customParams || {};
70497
+ this._customParams = customParams;
70498
+ customParams[key] = value;
70499
+ const width = Math.max(1, Number(customParams._sourceWidth ?? 0));
70500
+ const height = Math.max(1, Number(customParams._sourceHeight ?? 0));
70501
+ const minEdge = Math.min(width, height);
70502
+ const centerX = Number(customParams.centerX ?? 0.5);
70503
+ const centerY = Number(customParams.centerY ?? 0.5);
70504
+ const normalizedRadius = Number(customParams.radius ?? 0.25);
70505
+ const angle = Number(customParams.angle ?? 4);
70506
+ if (this.offset) {
70507
+ this.offset.x = centerX * width;
70508
+ this.offset.y = centerY * height;
70509
+ }
70510
+ this.radius = normalizedRadius * minEdge;
70511
+ this.angle = angle;
70512
+ const padding = computeTwistPadding(this.radius);
70513
+ this._exportPadding = padding;
70514
+ this.padding = Math.max(this.padding ?? 0, padding);
70515
+ return true;
70516
+ };
70517
+ filter.createExportFilter = function(options = {}) {
70518
+ const scale = Number.isFinite(options.previewToNativeScale) ? Math.max(1, Number(options.previewToNativeScale)) : 1;
70519
+ const stored = this._customParams || raw;
70520
+ return buildTwistFilter(stored, scale);
70521
+ };
70522
+ filter.getExportPadding = function() {
70523
+ return Number(this._exportPadding || this.padding || 0);
70524
+ };
70598
70525
  return filter;
70599
70526
  } catch (error) {
70600
70527
  console.error("Failed to create TwistFilter:", error);
70601
70528
  return null;
70602
70529
  }
70603
70530
  },
70604
- // Default parameter values - all normalized (0-1) for intuitive slider use
70531
+ // Default parameter values normalized (0-1) for intuitive slider use.
70605
70532
  defaultParams: {
70606
70533
  centerX: 0.5,
70607
- // Center of image
70608
70534
  centerY: 0.5,
70609
- // Center of image
70610
70535
  radius: 0.25,
70611
- // 25% of image size
70612
70536
  angle: 4
70613
- // Twist amount in radians
70614
70537
  },
70615
- // UI controls for the filter
70616
70538
  controls: [
70617
70539
  {
70618
70540
  id: "angle",
@@ -70634,7 +70556,7 @@ registerFilter({
70634
70556
  max: 0.75,
70635
70557
  step: 0.01,
70636
70558
  default: 0.25,
70637
- tooltip: "Size of the twist effect (0-1, relative to image)"
70559
+ tooltip: "Size of the twist effect (relative to image)"
70638
70560
  },
70639
70561
  {
70640
70562
  id: "centerX",
@@ -71008,6 +70930,124 @@ function createChunkWriteQueue() {
71008
70930
  }
71009
70931
  };
71010
70932
  }
70933
+ const DEFAULT_SEEK_TIMEOUT_MS = 4e3;
70934
+ const DEFAULT_SEEK_RETRY_ATTEMPTS = 3;
70935
+ const MAX_SEEK_RETRY_ATTEMPTS = 6;
70936
+ const MIN_SEEK_TIMEOUT_MS = 250;
70937
+ const MAX_SEEK_NUDGE_SECONDS = 0.25;
70938
+ const MIN_SEEK_EPSILON_SECONDS = 1e-3;
70939
+ function normalizeSeekTimeoutMs(value) {
70940
+ if (!Number.isFinite(value) || value === void 0 || value < MIN_SEEK_TIMEOUT_MS) {
70941
+ return DEFAULT_SEEK_TIMEOUT_MS;
70942
+ }
70943
+ return Math.floor(value);
70944
+ }
70945
+ function normalizeSeekRetryAttempts(value) {
70946
+ if (!Number.isFinite(value) || value === void 0 || value < 1) {
70947
+ return DEFAULT_SEEK_RETRY_ATTEMPTS;
70948
+ }
70949
+ return Math.min(MAX_SEEK_RETRY_ATTEMPTS, Math.floor(value));
70950
+ }
70951
+ function clampSeekTime(targetTime, videoDuration) {
70952
+ const normalizedTarget = Number.isFinite(targetTime) ? Math.max(0, targetTime) : 0;
70953
+ if (videoDuration === null || !Number.isFinite(videoDuration) || videoDuration <= 0) {
70954
+ return normalizedTarget;
70955
+ }
70956
+ const maxSeekTime = Math.max(0, videoDuration - MIN_SEEK_EPSILON_SECONDS);
70957
+ return Math.min(normalizedTarget, maxSeekTime);
70958
+ }
70959
+ function resolveSeekAttemptPlan(targetTime, { attempt, fps, videoDuration }) {
70960
+ const normalizedAttempt = Math.max(1, Math.floor(attempt));
70961
+ const normalizedFps = Number.isFinite(fps) && fps > 0 ? fps : 30;
70962
+ const clampedTarget = clampSeekTime(targetTime, videoDuration);
70963
+ if (normalizedAttempt === 1) {
70964
+ return [clampedTarget];
70965
+ }
70966
+ const retryIndex = normalizedAttempt - 1;
70967
+ const preferredDirection = retryIndex % 2 === 1 ? 1 : -1;
70968
+ const nudgeMagnitude = Math.min(
70969
+ Math.max(1 / normalizedFps, 0.05) * Math.ceil(retryIndex / 2),
70970
+ MAX_SEEK_NUDGE_SECONDS
70971
+ );
70972
+ for (const direction of [preferredDirection, -preferredDirection]) {
70973
+ const nudgedTarget = clampSeekTime(
70974
+ clampedTarget + direction * nudgeMagnitude,
70975
+ videoDuration
70976
+ );
70977
+ if (Math.abs(nudgedTarget - clampedTarget) < MIN_SEEK_EPSILON_SECONDS) {
70978
+ continue;
70979
+ }
70980
+ return [nudgedTarget, clampedTarget];
70981
+ }
70982
+ return [clampedTarget];
70983
+ }
70984
+ const PROBE_TIMEOUT_MS = 5e3;
70985
+ const PROBE_SEEK_TARGET = 1e9;
70986
+ async function resolveVideoDuration(video, recipe) {
70987
+ var _a;
70988
+ const initial = video.duration;
70989
+ if (Number.isFinite(initial) && initial > 0) {
70990
+ return initial;
70991
+ }
70992
+ console.warn(
70993
+ `[Render] video.duration=${initial}; attempting seek-past-end workaround`
70994
+ );
70995
+ const probed = await probeDurationViaSeek(video);
70996
+ if (Number.isFinite(probed) && probed > 0) {
70997
+ resetTime(video);
70998
+ return probed;
70999
+ }
71000
+ const fallback = (_a = recipe.source) == null ? void 0 : _a.duration;
71001
+ if (typeof fallback === "number" && Number.isFinite(fallback) && fallback > 0) {
71002
+ console.warn(
71003
+ `[Render] seek probe did not yield a valid duration; falling back to recipe.source.duration=${fallback}`
71004
+ );
71005
+ resetTime(video);
71006
+ return fallback;
71007
+ }
71008
+ throw new Error(`Invalid video duration: ${video.duration}`);
71009
+ }
71010
+ function probeDurationViaSeek(video) {
71011
+ return new Promise((resolve) => {
71012
+ let done = false;
71013
+ const cleanup = () => {
71014
+ video.removeEventListener("seeked", onSeeked);
71015
+ video.removeEventListener("durationchange", onDurationChange);
71016
+ video.removeEventListener("error", onError);
71017
+ window.clearTimeout(timeoutId);
71018
+ };
71019
+ const finish = (value) => {
71020
+ if (done) return;
71021
+ done = true;
71022
+ cleanup();
71023
+ resolve(value);
71024
+ };
71025
+ const onSeeked = () => finish(video.duration);
71026
+ const onDurationChange = () => {
71027
+ if (Number.isFinite(video.duration) && video.duration > 0) {
71028
+ finish(video.duration);
71029
+ }
71030
+ };
71031
+ const onError = () => finish(Number.NaN);
71032
+ const timeoutId = window.setTimeout(() => finish(Number.NaN), PROBE_TIMEOUT_MS);
71033
+ video.addEventListener("seeked", onSeeked);
71034
+ video.addEventListener("durationchange", onDurationChange);
71035
+ video.addEventListener("error", onError);
71036
+ try {
71037
+ video.currentTime = PROBE_SEEK_TARGET;
71038
+ } catch {
71039
+ finish(Number.NaN);
71040
+ }
71041
+ });
71042
+ }
71043
+ function resetTime(video) {
71044
+ try {
71045
+ video.currentTime = 0;
71046
+ } catch {
71047
+ }
71048
+ }
71049
+ const HAVE_CURRENT_DATA = 2;
71050
+ const PENDING_SEEK_DRAIN_TIMEOUT_MS = 1e3;
71011
71051
  function resolveFitMode(recipe) {
71012
71052
  var _a;
71013
71053
  const mode = (_a = recipe.metadata) == null ? void 0 : _a.fit_mode;
@@ -71062,47 +71102,181 @@ function toBase64(buffer) {
71062
71102
  }
71063
71103
  return btoa(binary);
71064
71104
  }
71065
- async function ensureFrameDecoded(video) {
71105
+ function waitForMilliseconds(milliseconds) {
71066
71106
  return new Promise((resolve) => {
71067
- let resolved = false;
71068
- const rvfcId = video.requestVideoFrameCallback(() => {
71069
- if (!resolved) {
71070
- resolved = true;
71071
- resolve();
71107
+ window.setTimeout(resolve, milliseconds);
71108
+ });
71109
+ }
71110
+ function touchRenderHeartbeat(stage) {
71111
+ window.__RENDER_HEARTBEAT__ = Date.now();
71112
+ if (stage !== void 0) {
71113
+ window.__RENDER_STAGE__ = stage;
71114
+ }
71115
+ }
71116
+ async function waitForPendingSeekToSettle(video, timeoutMs) {
71117
+ if (!video.seeking) {
71118
+ return true;
71119
+ }
71120
+ return new Promise((resolve) => {
71121
+ let settled = false;
71122
+ const finish = (result) => {
71123
+ if (settled) {
71124
+ return;
71072
71125
  }
71073
- });
71126
+ settled = true;
71127
+ window.clearTimeout(timeoutId);
71128
+ video.removeEventListener("seeked", onSeeked);
71129
+ video.removeEventListener("error", onError);
71130
+ resolve(result);
71131
+ };
71132
+ const onSeeked = () => finish(true);
71133
+ const onError = () => finish(false);
71134
+ const timeoutId = window.setTimeout(() => finish(false), timeoutMs);
71135
+ video.addEventListener("seeked", onSeeked);
71136
+ video.addEventListener("error", onError);
71137
+ });
71138
+ }
71139
+ async function drawCurrentVideoFrameToCanvas(video, sourceCtx, sourceWidth, sourceHeight) {
71140
+ if (video.readyState < HAVE_CURRENT_DATA) {
71141
+ throw new Error(
71142
+ `Video frame data unavailable before source copy (readyState=${video.readyState}, seeking=${video.seeking}, currentTime=${video.currentTime.toFixed(4)}, duration=${video.duration}, networkState=${video.networkState})`
71143
+ );
71144
+ }
71145
+ if (typeof createImageBitmap === "function") {
71146
+ const bitmap = await createImageBitmap(video);
71147
+ try {
71148
+ sourceCtx.clearRect(0, 0, sourceWidth, sourceHeight);
71149
+ sourceCtx.drawImage(bitmap, 0, 0, sourceWidth, sourceHeight);
71150
+ } finally {
71151
+ bitmap.close();
71152
+ }
71153
+ return;
71154
+ }
71155
+ sourceCtx.clearRect(0, 0, sourceWidth, sourceHeight);
71156
+ sourceCtx.drawImage(video, 0, 0, sourceWidth, sourceHeight);
71157
+ }
71158
+ async function ensureFrameDecoded(video, timeoutMs) {
71159
+ return new Promise((resolve, reject) => {
71160
+ let settled = false;
71161
+ let rvfcId = null;
71162
+ const finish = (error) => {
71163
+ if (settled) {
71164
+ return;
71165
+ }
71166
+ settled = true;
71167
+ window.clearTimeout(timeoutId);
71168
+ if (rvfcId !== null && typeof video.cancelVideoFrameCallback === "function") {
71169
+ video.cancelVideoFrameCallback(rvfcId);
71170
+ }
71171
+ if (error !== void 0) {
71172
+ reject(error);
71173
+ return;
71174
+ }
71175
+ resolve();
71176
+ };
71177
+ const timeoutId = window.setTimeout(() => {
71178
+ finish(new Error(`Frame decode timed out after ${timeoutMs}ms`));
71179
+ }, timeoutMs);
71180
+ if (typeof video.requestVideoFrameCallback === "function") {
71181
+ rvfcId = video.requestVideoFrameCallback(() => {
71182
+ finish();
71183
+ });
71184
+ }
71074
71185
  requestAnimationFrame(() => {
71075
71186
  requestAnimationFrame(() => {
71076
- if (!resolved) {
71077
- resolved = true;
71078
- video.cancelVideoFrameCallback(rvfcId);
71079
- resolve();
71080
- }
71187
+ finish();
71081
71188
  });
71082
71189
  });
71083
71190
  });
71084
71191
  }
71085
- function seekVideo(video, timeSec) {
71192
+ function seekVideoOnce(video, timeSec, timeoutMs) {
71086
71193
  return new Promise((resolve, reject) => {
71087
- if (Math.abs(video.currentTime - timeSec) < 1e-3) {
71088
- ensureFrameDecoded(video).then(resolve, resolve);
71089
- return;
71090
- }
71091
- const onSeeked = () => {
71194
+ let settled = false;
71195
+ const finish = (error) => {
71196
+ if (settled) {
71197
+ return;
71198
+ }
71199
+ settled = true;
71200
+ window.clearTimeout(timeoutId);
71092
71201
  video.removeEventListener("seeked", onSeeked);
71093
71202
  video.removeEventListener("error", onError);
71094
- ensureFrameDecoded(video).then(resolve, resolve);
71203
+ if (error !== void 0) {
71204
+ reject(error);
71205
+ return;
71206
+ }
71207
+ resolve();
71208
+ };
71209
+ const onSeeked = () => {
71210
+ ensureFrameDecoded(video, timeoutMs).then(() => finish()).catch((error) => {
71211
+ const message = error instanceof Error ? error.message : String(error);
71212
+ finish(new Error(`Frame decode failed at ${timeSec}s: ${message}`));
71213
+ });
71095
71214
  };
71096
71215
  const onError = () => {
71097
- video.removeEventListener("seeked", onSeeked);
71098
- video.removeEventListener("error", onError);
71099
- reject(new Error(`Video seek failed at ${timeSec}s`));
71216
+ finish(new Error(`Video seek failed at ${timeSec}s`));
71100
71217
  };
71218
+ const timeoutId = window.setTimeout(() => {
71219
+ finish(new Error(
71220
+ `Video seek timed out at ${timeSec}s after ${timeoutMs}ms (readyState=${video.readyState}, seeking=${video.seeking}, currentTime=${video.currentTime.toFixed(4)}, duration=${video.duration}, networkState=${video.networkState}, paused=${video.paused}, ended=${video.ended})`
71221
+ ));
71222
+ }, timeoutMs);
71223
+ if (!video.seeking && Math.abs(video.currentTime - timeSec) < 1e-3) {
71224
+ ensureFrameDecoded(video, timeoutMs).then(() => finish()).catch((error) => {
71225
+ const message = error instanceof Error ? error.message : String(error);
71226
+ finish(new Error(`Frame decode failed at ${timeSec}s: ${message}`));
71227
+ });
71228
+ return;
71229
+ }
71101
71230
  video.addEventListener("seeked", onSeeked);
71102
71231
  video.addEventListener("error", onError);
71103
- video.currentTime = timeSec;
71232
+ try {
71233
+ video.currentTime = timeSec;
71234
+ } catch (error) {
71235
+ const message = error instanceof Error ? error.message : String(error);
71236
+ finish(new Error(`Unable to start video seek at ${timeSec}s: ${message}`));
71237
+ }
71104
71238
  });
71105
71239
  }
71240
+ async function seekVideo(video, timeSec, timeoutMs, retryAttempts, fps) {
71241
+ const normalizedTimeoutMs = normalizeSeekTimeoutMs(timeoutMs);
71242
+ const normalizedRetryAttempts = normalizeSeekRetryAttempts(retryAttempts);
71243
+ const normalizedDuration = Number.isFinite(video.duration) && video.duration > 0 ? video.duration : null;
71244
+ let lastError = null;
71245
+ for (let attempt = 1; attempt <= normalizedRetryAttempts; attempt += 1) {
71246
+ const targets = resolveSeekAttemptPlan(timeSec, {
71247
+ attempt,
71248
+ fps,
71249
+ videoDuration: normalizedDuration
71250
+ });
71251
+ try {
71252
+ for (const target of targets) {
71253
+ touchRenderHeartbeat("seeking");
71254
+ await seekVideoOnce(video, target, normalizedTimeoutMs);
71255
+ }
71256
+ return;
71257
+ } catch (error) {
71258
+ lastError = error instanceof Error ? error : new Error(String(error));
71259
+ console.warn("[RenderVideo] seek attempt failed", {
71260
+ attempt,
71261
+ targets,
71262
+ error: lastError.message
71263
+ });
71264
+ if (attempt < normalizedRetryAttempts) {
71265
+ const pendingSeekSettled = await waitForPendingSeekToSettle(
71266
+ video,
71267
+ Math.min(PENDING_SEEK_DRAIN_TIMEOUT_MS, normalizedTimeoutMs)
71268
+ );
71269
+ if (!pendingSeekSettled) {
71270
+ break;
71271
+ }
71272
+ await waitForMilliseconds(Math.min(250 * attempt, 1e3));
71273
+ }
71274
+ }
71275
+ }
71276
+ throw new Error(
71277
+ `Video seek failed at ${timeSec}s after ${normalizedRetryAttempts} attempts: ${(lastError == null ? void 0 : lastError.message) ?? "unknown error"}`
71278
+ );
71279
+ }
71106
71280
  function waitForVideoReady(video) {
71107
71281
  return new Promise((resolve, reject) => {
71108
71282
  if (video.readyState >= 1) {
@@ -71178,6 +71352,8 @@ async function main() {
71178
71352
  window.__RENDER_PROGRESS__ = 0;
71179
71353
  window.__RENDER_ERROR__ = null;
71180
71354
  window.__RENDER_AUDIO_STATUS__ = null;
71355
+ window.__RENDER_HEARTBEAT__ = Date.now();
71356
+ window.__RENDER_STAGE__ = "initializing";
71181
71357
  try {
71182
71358
  const config = window.__RENDER_CONFIG__;
71183
71359
  if (!config) throw new Error("No __RENDER_CONFIG__ found on window");
@@ -71192,13 +71368,12 @@ async function main() {
71192
71368
  } = config;
71193
71369
  const trimStart = resolveTrimStart(recipe);
71194
71370
  const video = document.getElementById("source-video");
71371
+ touchRenderHeartbeat("loading-source");
71195
71372
  video.src = sourceUrl;
71196
71373
  video.load();
71197
71374
  await waitForVideoReady(video);
71198
- const videoDuration = video.duration;
71199
- if (!Number.isFinite(videoDuration) || videoDuration <= 0) {
71200
- throw new Error(`Invalid video duration: ${videoDuration}`);
71201
- }
71375
+ touchRenderHeartbeat("probing-duration");
71376
+ const videoDuration = await resolveVideoDuration(video, recipe);
71202
71377
  const trimEnd = resolveTrimEnd(recipe, videoDuration);
71203
71378
  const exportDuration = Math.max(0, trimEnd - trimStart);
71204
71379
  const totalFrames = Math.max(1, Math.ceil(exportDuration * fps));
@@ -71208,6 +71383,7 @@ async function main() {
71208
71383
  window.__RENDER_AUDIO_STATUS__ = "skipped:AudioEncoder-unavailable";
71209
71384
  }
71210
71385
  const audioPromise = hasAudioEncoder ? decodeAudioFromSource(sourceUrl) : Promise.resolve(null);
71386
+ touchRenderHeartbeat("initializing-pixi");
71211
71387
  const app = new Application();
71212
71388
  await app.init({
71213
71389
  width,
@@ -71335,26 +71511,24 @@ async function main() {
71335
71511
  }
71336
71512
  });
71337
71513
  encoder.configure(encoderConfig);
71338
- const targetCanvas = document.createElement("canvas");
71339
- targetCanvas.width = width;
71340
- targetCanvas.height = height;
71341
- const targetCtx = targetCanvas.getContext("2d");
71342
- if (!targetCtx) throw new Error("Failed to create 2D context for export");
71343
71514
  for (let frame = 0; frame < totalFrames; frame++) {
71344
71515
  const timeSec = trimStart + frame / fps;
71345
- await seekVideo(video, timeSec);
71346
- sourceCtx.drawImage(video, 0, 0, sourceWidth, sourceHeight);
71516
+ await seekVideo(
71517
+ video,
71518
+ timeSec,
71519
+ config.seekTimeoutMs ?? 4e3,
71520
+ config.seekRetryAttempts ?? 3,
71521
+ fps
71522
+ );
71523
+ await drawCurrentVideoFrameToCanvas(video, sourceCtx, sourceWidth, sourceHeight);
71347
71524
  videoTexture.source.update();
71348
71525
  app.render();
71349
- const gl = app.renderer.gl;
71350
- if (gl) {
71351
- gl.finish();
71352
- }
71353
- targetCtx.clearRect(0, 0, width, height);
71354
- targetCtx.drawImage(app.canvas, 0, 0, width, height);
71355
71526
  const timestamp = Math.floor(frame / fps * 1e6);
71356
71527
  const frameDuration = Math.floor(1e6 / fps);
71357
- const videoFrame = new VideoFrame(targetCanvas, { timestamp, duration: frameDuration });
71528
+ const videoFrame = new VideoFrame(
71529
+ app.canvas,
71530
+ { timestamp, duration: frameDuration }
71531
+ );
71358
71532
  const keyFrame = frame % (fps * 2) === 0;
71359
71533
  encoder.encode(videoFrame, { keyFrame });
71360
71534
  videoFrame.close();
@@ -71364,11 +71538,14 @@ async function main() {
71364
71538
  });
71365
71539
  }
71366
71540
  window.__RENDER_PROGRESS__ = Math.round((frame + 1) / totalFrames * 90);
71541
+ touchRenderHeartbeat("encoding-video");
71367
71542
  }
71543
+ touchRenderHeartbeat("flushing-video");
71368
71544
  await encoder.flush();
71369
71545
  encoder.close();
71370
71546
  if (hasAudio) {
71371
71547
  window.__RENDER_PROGRESS__ = 91;
71548
+ touchRenderHeartbeat("encoding-audio");
71372
71549
  const trimmed = trimAudioBuffer(audioBuffer, trimStart, trimEnd);
71373
71550
  const audioEncoderConfig = {
71374
71551
  codec: "mp4a.40.2",
@@ -71414,7 +71591,9 @@ async function main() {
71414
71591
  audioEncoder.addEventListener("dequeue", () => resolve(), { once: true });
71415
71592
  });
71416
71593
  }
71594
+ touchRenderHeartbeat("encoding-audio");
71417
71595
  }
71596
+ touchRenderHeartbeat("flushing-audio");
71418
71597
  await audioEncoder.flush();
71419
71598
  audioEncoder.close();
71420
71599
  console.log("[RenderAudio] Audio encoding complete");
@@ -71425,15 +71604,18 @@ async function main() {
71425
71604
  }
71426
71605
  window.__RENDER_PROGRESS__ = 99;
71427
71606
  }
71607
+ touchRenderHeartbeat("finalizing-muxer");
71428
71608
  muxer.finalize();
71429
71609
  await chunkWriteQueue.flush();
71430
71610
  window.__RENDER_PROGRESS__ = 100;
71431
71611
  window.__RENDER_STATUS__ = "done";
71612
+ touchRenderHeartbeat("done");
71432
71613
  app.destroy(true);
71433
71614
  } catch (err) {
71434
71615
  const message = err instanceof Error ? err.message : String(err);
71435
71616
  window.__RENDER_ERROR__ = message;
71436
71617
  window.__RENDER_STATUS__ = "error";
71618
+ touchRenderHeartbeat("error");
71437
71619
  throw err;
71438
71620
  }
71439
71621
  }