ugcinc-render 1.3.12 → 1.3.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.mts CHANGED
@@ -320,6 +320,10 @@ interface ImageEditorNodeConfig {
320
320
  dimensionPreset: DimensionPresetKey;
321
321
  /** Elements to render */
322
322
  elements: ImageEditorElement[];
323
+ /** Background type: 'image' for image input, 'color' for solid color */
324
+ backgroundType?: 'image' | 'color';
325
+ /** Background color (hex) when backgroundType is 'color' */
326
+ backgroundColor?: string;
323
327
  /** How the background image fits the canvas */
324
328
  backgroundFit?: FitMode;
325
329
  /** Cached background image URL for consistent preview */
@@ -740,6 +744,10 @@ interface ImageEditorCompositionProps {
740
744
  width?: number;
741
745
  /** Canvas height (required when using elements) */
742
746
  height?: number;
747
+ /** Background type: 'image' for image input, 'color' for solid color */
748
+ backgroundType?: 'image' | 'color';
749
+ /** Background color (hex) when backgroundType is 'color' */
750
+ backgroundColor?: string;
743
751
  /** Background fit mode (when using elements) */
744
752
  backgroundFit?: FitMode;
745
753
  /** Background image URL (when using elements) */
@@ -748,6 +756,8 @@ interface ImageEditorCompositionProps {
748
756
  imageUrls?: Record<string, string | null>;
749
757
  /** Text values keyed by textInputId (when using elements, for autoWidth calculation) */
750
758
  textValues?: Record<string, string>;
759
+ /** Dynamic crop configuration */
760
+ dynamicCrop?: DynamicCropConfig;
751
761
  }
752
762
  /**
753
763
  * ImageEditorComposition renders a complete image editor configuration.
@@ -782,7 +792,7 @@ interface ImageEditorCompositionProps {
782
792
  * />
783
793
  * ```
784
794
  */
785
- declare function ImageEditorComposition({ config, sources, scale, elements, width, height, backgroundFit, backgroundUrl, imageUrls, textValues, }: ImageEditorCompositionProps): react_jsx_runtime.JSX.Element;
795
+ declare function ImageEditorComposition({ config, sources, scale, elements, width, height, backgroundType, backgroundColor, backgroundFit, backgroundUrl, imageUrls, textValues, dynamicCrop, }: ImageEditorCompositionProps): react_jsx_runtime.JSX.Element;
786
796
 
787
797
  interface VideoEditorCompositionProps {
788
798
  /** The editor configuration to render */
@@ -1136,6 +1146,25 @@ declare function getReferenceElementX(elements: ImageEditorElement[], elementId:
1136
1146
  */
1137
1147
  declare function getReferenceElementY(elements: ImageEditorElement[], elementId: string): ImageEditorElement | null;
1138
1148
 
1149
+ /**
1150
+ * Utility functions for calculating dynamic crop bounds
1151
+ */
1152
+
1153
+ /**
1154
+ * Calculate dynamic crop bounds based on element positions
1155
+ *
1156
+ * @param elements - Array of resolved elements with absolute positions
1157
+ * @param dynamicCrop - Crop configuration
1158
+ * @param canvasWidth - Original canvas width
1159
+ * @param canvasHeight - Original canvas height
1160
+ * @returns CropBounds with x, y, width, height
1161
+ */
1162
+ declare function calculateCropBounds(elements: ImageEditorElement[], dynamicCrop: DynamicCropConfig | undefined, canvasWidth: number, canvasHeight: number): CropBounds;
1163
+ /**
1164
+ * Check if dynamic crop is enabled (either vertical or horizontal)
1165
+ */
1166
+ declare function isDynamicCropEnabled(dynamicCrop: DynamicCropConfig | undefined): boolean;
1167
+
1139
1168
  /**
1140
1169
  * Hook exports for ugcinc-render
1141
1170
  *
@@ -1195,4 +1224,4 @@ declare function useResolvedPositions(elements: ImageEditorElement[], textValues
1195
1224
 
1196
1225
  declare const RenderRoot: React.FC;
1197
1226
 
1198
- export { type AudioSegment, type BaseEditorConfig, type BaseSegment, type BorderRadiusConfig, type Channel, type CropAxisConfig, type CropBoundary, type CropBounds, DIMENSION_PRESETS, type DimensionPreset, type DimensionPresetKey, type DynamicCropConfig, type EditorConfig, type EditorSegment, FONT_FAMILIES, FONT_URLS, type FitDimensions, type FitMode, type FontType, type FontWeight, type HorizontalAnchor, type HorizontalSelfAnchor, type Hyphenation, IMAGE_DEFAULTS, ImageEditorComposition, type ImageEditorCompositionProps, type ImageEditorConfig, type ImageEditorElement, type ImageEditorNodeConfig, ImageElement, type ImageElementProps, type ImageSegment, type PictureSegment, type PositionResolutionError, type PositionResolutionResult, type RelativePositionConfigX, type RelativePositionConfigY, RenderRoot, type Segment, type SegmentType, type StaticSegment, TEXT_DEFAULTS, type TextAlignment, type TextDirection, TextElement, type TextElementProps, type TextOverflow, type TextSegment, type TextWrap, type TimeMode, type TimeValue, VIDEO_DEFAULTS, VISUAL_DEFAULTS, type VerticalAlignment, type VerticalAnchor, type VerticalSelfAnchor, type VideoEditorAudioSegment, type VideoEditorBaseSegment, type VideoEditorChannel, VideoEditorComposition, type VideoEditorCompositionProps, type VideoEditorConfig, type VideoEditorImageSegment, type VideoEditorNodeConfig, type VideoEditorSegment, type VideoEditorTextSegment, type VideoEditorVideoSegment, type VideoEditorVisualSegment, VideoElement, type VideoElementProps, type VideoSegment, type VisualSegment, type VisualSegmentUnion, type WordBreak, applyImageDefaults, applyTextDefaults, applyVideoDefaults, areFontsLoaded, buildFontString, calculateAutoWidthDimensions, calculateFitDimensions, calculateLineWidth, canSetAsReference, getBorderRadii, getDependentElements, getFontFamily, getReferenceElementX, getReferenceElementY, hexToRgba, parseHexColor, preloadFonts, resolveElementPositions, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
1227
+ export { type AudioSegment, type BaseEditorConfig, type BaseSegment, type BorderRadiusConfig, type Channel, type CropAxisConfig, type CropBoundary, type CropBounds, DIMENSION_PRESETS, type DimensionPreset, type DimensionPresetKey, type DynamicCropConfig, type EditorConfig, type EditorSegment, FONT_FAMILIES, FONT_URLS, type FitDimensions, type FitMode, type FontType, type FontWeight, type HorizontalAnchor, type HorizontalSelfAnchor, type Hyphenation, IMAGE_DEFAULTS, ImageEditorComposition, type ImageEditorCompositionProps, type ImageEditorConfig, type ImageEditorElement, type ImageEditorNodeConfig, ImageElement, type ImageElementProps, type ImageSegment, type PictureSegment, type PositionResolutionError, type PositionResolutionResult, type RelativePositionConfigX, type RelativePositionConfigY, RenderRoot, type Segment, type SegmentType, type StaticSegment, TEXT_DEFAULTS, type TextAlignment, type TextDirection, TextElement, type TextElementProps, type TextOverflow, type TextSegment, type TextWrap, type TimeMode, type TimeValue, VIDEO_DEFAULTS, VISUAL_DEFAULTS, type VerticalAlignment, type VerticalAnchor, type VerticalSelfAnchor, type VideoEditorAudioSegment, type VideoEditorBaseSegment, type VideoEditorChannel, VideoEditorComposition, type VideoEditorCompositionProps, type VideoEditorConfig, type VideoEditorImageSegment, type VideoEditorNodeConfig, type VideoEditorSegment, type VideoEditorTextSegment, type VideoEditorVideoSegment, type VideoEditorVisualSegment, VideoElement, type VideoElementProps, type VideoSegment, type VisualSegment, type VisualSegmentUnion, type WordBreak, applyImageDefaults, applyTextDefaults, applyVideoDefaults, areFontsLoaded, buildFontString, calculateAutoWidthDimensions, calculateCropBounds, calculateFitDimensions, calculateLineWidth, canSetAsReference, getBorderRadii, getDependentElements, getFontFamily, getReferenceElementX, getReferenceElementY, hexToRgba, isDynamicCropEnabled, parseHexColor, preloadFonts, resolveElementPositions, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
package/dist/index.d.ts CHANGED
@@ -320,6 +320,10 @@ interface ImageEditorNodeConfig {
320
320
  dimensionPreset: DimensionPresetKey;
321
321
  /** Elements to render */
322
322
  elements: ImageEditorElement[];
323
+ /** Background type: 'image' for image input, 'color' for solid color */
324
+ backgroundType?: 'image' | 'color';
325
+ /** Background color (hex) when backgroundType is 'color' */
326
+ backgroundColor?: string;
323
327
  /** How the background image fits the canvas */
324
328
  backgroundFit?: FitMode;
325
329
  /** Cached background image URL for consistent preview */
@@ -740,6 +744,10 @@ interface ImageEditorCompositionProps {
740
744
  width?: number;
741
745
  /** Canvas height (required when using elements) */
742
746
  height?: number;
747
+ /** Background type: 'image' for image input, 'color' for solid color */
748
+ backgroundType?: 'image' | 'color';
749
+ /** Background color (hex) when backgroundType is 'color' */
750
+ backgroundColor?: string;
743
751
  /** Background fit mode (when using elements) */
744
752
  backgroundFit?: FitMode;
745
753
  /** Background image URL (when using elements) */
@@ -748,6 +756,8 @@ interface ImageEditorCompositionProps {
748
756
  imageUrls?: Record<string, string | null>;
749
757
  /** Text values keyed by textInputId (when using elements, for autoWidth calculation) */
750
758
  textValues?: Record<string, string>;
759
+ /** Dynamic crop configuration */
760
+ dynamicCrop?: DynamicCropConfig;
751
761
  }
752
762
  /**
753
763
  * ImageEditorComposition renders a complete image editor configuration.
@@ -782,7 +792,7 @@ interface ImageEditorCompositionProps {
782
792
  * />
783
793
  * ```
784
794
  */
785
- declare function ImageEditorComposition({ config, sources, scale, elements, width, height, backgroundFit, backgroundUrl, imageUrls, textValues, }: ImageEditorCompositionProps): react_jsx_runtime.JSX.Element;
795
+ declare function ImageEditorComposition({ config, sources, scale, elements, width, height, backgroundType, backgroundColor, backgroundFit, backgroundUrl, imageUrls, textValues, dynamicCrop, }: ImageEditorCompositionProps): react_jsx_runtime.JSX.Element;
786
796
 
787
797
  interface VideoEditorCompositionProps {
788
798
  /** The editor configuration to render */
@@ -1136,6 +1146,25 @@ declare function getReferenceElementX(elements: ImageEditorElement[], elementId:
1136
1146
  */
1137
1147
  declare function getReferenceElementY(elements: ImageEditorElement[], elementId: string): ImageEditorElement | null;
1138
1148
 
1149
+ /**
1150
+ * Utility functions for calculating dynamic crop bounds
1151
+ */
1152
+
1153
+ /**
1154
+ * Calculate dynamic crop bounds based on element positions
1155
+ *
1156
+ * @param elements - Array of resolved elements with absolute positions
1157
+ * @param dynamicCrop - Crop configuration
1158
+ * @param canvasWidth - Original canvas width
1159
+ * @param canvasHeight - Original canvas height
1160
+ * @returns CropBounds with x, y, width, height
1161
+ */
1162
+ declare function calculateCropBounds(elements: ImageEditorElement[], dynamicCrop: DynamicCropConfig | undefined, canvasWidth: number, canvasHeight: number): CropBounds;
1163
+ /**
1164
+ * Check if dynamic crop is enabled (either vertical or horizontal)
1165
+ */
1166
+ declare function isDynamicCropEnabled(dynamicCrop: DynamicCropConfig | undefined): boolean;
1167
+
1139
1168
  /**
1140
1169
  * Hook exports for ugcinc-render
1141
1170
  *
@@ -1195,4 +1224,4 @@ declare function useResolvedPositions(elements: ImageEditorElement[], textValues
1195
1224
 
1196
1225
  declare const RenderRoot: React.FC;
1197
1226
 
1198
- export { type AudioSegment, type BaseEditorConfig, type BaseSegment, type BorderRadiusConfig, type Channel, type CropAxisConfig, type CropBoundary, type CropBounds, DIMENSION_PRESETS, type DimensionPreset, type DimensionPresetKey, type DynamicCropConfig, type EditorConfig, type EditorSegment, FONT_FAMILIES, FONT_URLS, type FitDimensions, type FitMode, type FontType, type FontWeight, type HorizontalAnchor, type HorizontalSelfAnchor, type Hyphenation, IMAGE_DEFAULTS, ImageEditorComposition, type ImageEditorCompositionProps, type ImageEditorConfig, type ImageEditorElement, type ImageEditorNodeConfig, ImageElement, type ImageElementProps, type ImageSegment, type PictureSegment, type PositionResolutionError, type PositionResolutionResult, type RelativePositionConfigX, type RelativePositionConfigY, RenderRoot, type Segment, type SegmentType, type StaticSegment, TEXT_DEFAULTS, type TextAlignment, type TextDirection, TextElement, type TextElementProps, type TextOverflow, type TextSegment, type TextWrap, type TimeMode, type TimeValue, VIDEO_DEFAULTS, VISUAL_DEFAULTS, type VerticalAlignment, type VerticalAnchor, type VerticalSelfAnchor, type VideoEditorAudioSegment, type VideoEditorBaseSegment, type VideoEditorChannel, VideoEditorComposition, type VideoEditorCompositionProps, type VideoEditorConfig, type VideoEditorImageSegment, type VideoEditorNodeConfig, type VideoEditorSegment, type VideoEditorTextSegment, type VideoEditorVideoSegment, type VideoEditorVisualSegment, VideoElement, type VideoElementProps, type VideoSegment, type VisualSegment, type VisualSegmentUnion, type WordBreak, applyImageDefaults, applyTextDefaults, applyVideoDefaults, areFontsLoaded, buildFontString, calculateAutoWidthDimensions, calculateFitDimensions, calculateLineWidth, canSetAsReference, getBorderRadii, getDependentElements, getFontFamily, getReferenceElementX, getReferenceElementY, hexToRgba, parseHexColor, preloadFonts, resolveElementPositions, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
1227
+ export { type AudioSegment, type BaseEditorConfig, type BaseSegment, type BorderRadiusConfig, type Channel, type CropAxisConfig, type CropBoundary, type CropBounds, DIMENSION_PRESETS, type DimensionPreset, type DimensionPresetKey, type DynamicCropConfig, type EditorConfig, type EditorSegment, FONT_FAMILIES, FONT_URLS, type FitDimensions, type FitMode, type FontType, type FontWeight, type HorizontalAnchor, type HorizontalSelfAnchor, type Hyphenation, IMAGE_DEFAULTS, ImageEditorComposition, type ImageEditorCompositionProps, type ImageEditorConfig, type ImageEditorElement, type ImageEditorNodeConfig, ImageElement, type ImageElementProps, type ImageSegment, type PictureSegment, type PositionResolutionError, type PositionResolutionResult, type RelativePositionConfigX, type RelativePositionConfigY, RenderRoot, type Segment, type SegmentType, type StaticSegment, TEXT_DEFAULTS, type TextAlignment, type TextDirection, TextElement, type TextElementProps, type TextOverflow, type TextSegment, type TextWrap, type TimeMode, type TimeValue, VIDEO_DEFAULTS, VISUAL_DEFAULTS, type VerticalAlignment, type VerticalAnchor, type VerticalSelfAnchor, type VideoEditorAudioSegment, type VideoEditorBaseSegment, type VideoEditorChannel, VideoEditorComposition, type VideoEditorCompositionProps, type VideoEditorConfig, type VideoEditorImageSegment, type VideoEditorNodeConfig, type VideoEditorSegment, type VideoEditorTextSegment, type VideoEditorVideoSegment, type VideoEditorVisualSegment, VideoElement, type VideoElementProps, type VideoSegment, type VisualSegment, type VisualSegmentUnion, type WordBreak, applyImageDefaults, applyTextDefaults, applyVideoDefaults, areFontsLoaded, buildFontString, calculateAutoWidthDimensions, calculateCropBounds, calculateFitDimensions, calculateLineWidth, canSetAsReference, getBorderRadii, getDependentElements, getFontFamily, getReferenceElementX, getReferenceElementY, hexToRgba, isDynamicCropEnabled, parseHexColor, preloadFonts, resolveElementPositions, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
package/dist/index.js CHANGED
@@ -39,6 +39,7 @@ __export(index_exports, {
39
39
  areFontsLoaded: () => areFontsLoaded,
40
40
  buildFontString: () => buildFontString,
41
41
  calculateAutoWidthDimensions: () => calculateAutoWidthDimensions,
42
+ calculateCropBounds: () => calculateCropBounds,
42
43
  calculateFitDimensions: () => calculateFitDimensions,
43
44
  calculateLineWidth: () => calculateLineWidth,
44
45
  canSetAsReference: () => canSetAsReference,
@@ -48,6 +49,7 @@ __export(index_exports, {
48
49
  getReferenceElementX: () => getReferenceElementX,
49
50
  getReferenceElementY: () => getReferenceElementY,
50
51
  hexToRgba: () => hexToRgba,
52
+ isDynamicCropEnabled: () => isDynamicCropEnabled,
51
53
  parseHexColor: () => parseHexColor,
52
54
  preloadFonts: () => preloadFonts,
53
55
  resolveElementPositions: () => resolveElementPositions,
@@ -799,6 +801,113 @@ function getReferenceElementY(elements, elementId) {
799
801
  return elements.find((e) => e.id === element.relativePositionY.elementId) ?? null;
800
802
  }
801
803
 
804
+ // src/utils/cropBounds.ts
805
+ function calculateCropBounds(elements, dynamicCrop, canvasWidth, canvasHeight) {
806
+ if (!dynamicCrop) {
807
+ return { x: 0, y: 0, width: canvasWidth, height: canvasHeight };
808
+ }
809
+ const elementMap = /* @__PURE__ */ new Map();
810
+ for (const elem of elements) {
811
+ elementMap.set(elem.id, elem);
812
+ }
813
+ const resolveBoundary = (boundary) => {
814
+ if (!boundary) return void 0;
815
+ if (boundary.elementId) return boundary.elementId;
816
+ return void 0;
817
+ };
818
+ let cropY = 0;
819
+ let cropHeight = canvasHeight;
820
+ if (dynamicCrop.vertical?.enabled) {
821
+ const vCrop = dynamicCrop.vertical;
822
+ const paddingStart = vCrop.paddingStart ?? 0;
823
+ const paddingEnd = vCrop.paddingEnd ?? 0;
824
+ if (vCrop.mode === "all-elements") {
825
+ let minY = canvasHeight;
826
+ let maxY = 0;
827
+ for (const elem of elements) {
828
+ minY = Math.min(minY, elem.y);
829
+ maxY = Math.max(maxY, elem.y + elem.height);
830
+ }
831
+ if (elements.length > 0) {
832
+ cropY = Math.max(0, minY - paddingStart);
833
+ const bottomY = Math.min(canvasHeight, maxY + paddingEnd);
834
+ cropHeight = bottomY - cropY;
835
+ }
836
+ } else if (vCrop.mode === "between-elements") {
837
+ const startElementId = resolveBoundary(vCrop.startBoundary);
838
+ const endElementId = resolveBoundary(vCrop.endBoundary);
839
+ let topY = 0;
840
+ let bottomY = canvasHeight;
841
+ if (startElementId) {
842
+ const startElem = elementMap.get(startElementId);
843
+ if (startElem) {
844
+ topY = startElem.y;
845
+ }
846
+ }
847
+ if (endElementId) {
848
+ const endElem = elementMap.get(endElementId);
849
+ if (endElem) {
850
+ bottomY = endElem.y + endElem.height;
851
+ }
852
+ }
853
+ cropY = Math.max(0, topY - paddingStart);
854
+ const adjustedBottom = Math.min(canvasHeight, bottomY + paddingEnd);
855
+ cropHeight = adjustedBottom - cropY;
856
+ }
857
+ if (vCrop.minSize && cropHeight < vCrop.minSize) {
858
+ cropHeight = vCrop.minSize;
859
+ }
860
+ }
861
+ let cropX = 0;
862
+ let cropWidth = canvasWidth;
863
+ if (dynamicCrop.horizontal?.enabled) {
864
+ const hCrop = dynamicCrop.horizontal;
865
+ const paddingStart = hCrop.paddingStart ?? 0;
866
+ const paddingEnd = hCrop.paddingEnd ?? 0;
867
+ if (hCrop.mode === "all-elements") {
868
+ let minX = canvasWidth;
869
+ let maxX = 0;
870
+ for (const elem of elements) {
871
+ minX = Math.min(minX, elem.x);
872
+ maxX = Math.max(maxX, elem.x + elem.width);
873
+ }
874
+ if (elements.length > 0) {
875
+ cropX = Math.max(0, minX - paddingStart);
876
+ const rightX = Math.min(canvasWidth, maxX + paddingEnd);
877
+ cropWidth = rightX - cropX;
878
+ }
879
+ } else if (hCrop.mode === "between-elements") {
880
+ const startElementId = resolveBoundary(hCrop.startBoundary);
881
+ const endElementId = resolveBoundary(hCrop.endBoundary);
882
+ let leftX = 0;
883
+ let rightX = canvasWidth;
884
+ if (startElementId) {
885
+ const startElem = elementMap.get(startElementId);
886
+ if (startElem) {
887
+ leftX = startElem.x;
888
+ }
889
+ }
890
+ if (endElementId) {
891
+ const endElem = elementMap.get(endElementId);
892
+ if (endElem) {
893
+ rightX = endElem.x + endElem.width;
894
+ }
895
+ }
896
+ cropX = Math.max(0, leftX - paddingStart);
897
+ const adjustedRight = Math.min(canvasWidth, rightX + paddingEnd);
898
+ cropWidth = adjustedRight - cropX;
899
+ }
900
+ if (hCrop.minSize && cropWidth < hCrop.minSize) {
901
+ cropWidth = hCrop.minSize;
902
+ }
903
+ }
904
+ return { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
905
+ }
906
+ function isDynamicCropEnabled(dynamicCrop) {
907
+ if (!dynamicCrop) return false;
908
+ return !!(dynamicCrop.vertical?.enabled || dynamicCrop.horizontal?.enabled);
909
+ }
910
+
802
911
  // src/compositions/ImageEditorComposition.tsx
803
912
  var import_jsx_runtime3 = require("react/jsx-runtime");
804
913
  function getSortedSegments(config) {
@@ -874,11 +983,16 @@ function ImageEditorComposition({
874
983
  elements,
875
984
  width,
876
985
  height,
986
+ backgroundType = "image",
987
+ backgroundColor,
877
988
  backgroundFit = "cover",
878
989
  backgroundUrl,
879
990
  imageUrls = {},
880
- textValues = {}
991
+ textValues = {},
992
+ dynamicCrop
881
993
  }) {
994
+ const canvasWidth = width ?? config?.width ?? 1080;
995
+ const canvasHeight = height ?? config?.height ?? 1920;
882
996
  const resolvedElements = (0, import_react3.useMemo)(() => {
883
997
  if (!elements) return null;
884
998
  const result = resolveElementPositions(elements, textValues);
@@ -887,6 +1001,10 @@ function ImageEditorComposition({
887
1001
  }
888
1002
  return result.elements;
889
1003
  }, [elements, textValues]);
1004
+ const cropBounds = (0, import_react3.useMemo)(() => {
1005
+ if (!isDynamicCropEnabled(dynamicCrop) || !resolvedElements) return null;
1006
+ return calculateCropBounds(resolvedElements, dynamicCrop, canvasWidth, canvasHeight);
1007
+ }, [resolvedElements, dynamicCrop, canvasWidth, canvasHeight]);
890
1008
  const segmentsFromElements = (0, import_react3.useMemo)(() => {
891
1009
  if (!resolvedElements) return null;
892
1010
  const segments = [];
@@ -903,10 +1021,10 @@ function ImageEditorComposition({
903
1021
  }
904
1022
  return segments.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
905
1023
  }, [resolvedElements, imageUrls, textValues]);
906
- const canvasWidth = width ?? config?.width ?? 1080;
907
- const canvasHeight = height ?? config?.height ?? 1920;
908
1024
  const bgFit = backgroundFit ?? "cover";
909
1025
  const bgUrl = backgroundUrl ?? sources.background;
1026
+ const cropOffsetX = cropBounds?.x ?? 0;
1027
+ const cropOffsetY = cropBounds?.y ?? 0;
910
1028
  const contentSegments = segmentsFromElements ?? (() => {
911
1029
  if (!config) return [];
912
1030
  const sorted = getSortedSegments(config);
@@ -926,61 +1044,74 @@ function ImageEditorComposition({
926
1044
  }
927
1045
  return void 0;
928
1046
  };
929
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_remotion2.AbsoluteFill, { style: { backgroundColor: "#000000" }, children: [
930
- bgUrl && segmentsFromElements && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
931
- import_remotion2.Img,
932
- {
933
- src: bgUrl,
934
- style: {
935
- position: "absolute",
936
- left: 0,
937
- top: 0,
938
- width: canvasWidth * scale,
939
- height: canvasHeight * scale,
940
- objectFit: bgFit
941
- }
942
- }
943
- ),
944
- legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
945
- BackgroundImage,
946
- {
947
- segment: legacyBackgroundSegment,
948
- src: getSource(legacyBackgroundSegment),
949
- width: canvasWidth,
950
- height: canvasHeight,
951
- scale
952
- }
953
- ),
954
- contentSegments.map((segment) => {
955
- if (segment.type === "text") {
956
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
957
- TextElement,
1047
+ const containerBgColor = backgroundType === "color" && backgroundColor ? backgroundColor : "#000000";
1048
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(import_remotion2.AbsoluteFill, { style: { backgroundColor: containerBgColor }, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1049
+ "div",
1050
+ {
1051
+ style: {
1052
+ position: "absolute",
1053
+ left: -cropOffsetX * scale,
1054
+ top: -cropOffsetY * scale,
1055
+ width: canvasWidth * scale,
1056
+ height: canvasHeight * scale
1057
+ },
1058
+ children: [
1059
+ backgroundType === "image" && bgUrl && segmentsFromElements && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1060
+ import_remotion2.Img,
958
1061
  {
959
- segment,
960
- scale
961
- },
962
- segment.id
963
- );
964
- }
965
- if (segment.type === "image") {
966
- const src = segment.source || getSource(segment);
967
- if (!src) {
968
- console.warn(`No source found for image segment: ${segment.id}`);
969
- return null;
970
- }
971
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
972
- ImageElement,
1062
+ src: bgUrl,
1063
+ style: {
1064
+ position: "absolute",
1065
+ left: 0,
1066
+ top: 0,
1067
+ width: canvasWidth * scale,
1068
+ height: canvasHeight * scale,
1069
+ objectFit: bgFit
1070
+ }
1071
+ }
1072
+ ),
1073
+ legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1074
+ BackgroundImage,
973
1075
  {
974
- segment,
975
- src,
1076
+ segment: legacyBackgroundSegment,
1077
+ src: getSource(legacyBackgroundSegment),
1078
+ width: canvasWidth,
1079
+ height: canvasHeight,
976
1080
  scale
977
- },
978
- segment.id
979
- );
980
- }
981
- return null;
982
- })
983
- ] });
1081
+ }
1082
+ ),
1083
+ contentSegments.map((segment) => {
1084
+ if (segment.type === "text") {
1085
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1086
+ TextElement,
1087
+ {
1088
+ segment,
1089
+ scale
1090
+ },
1091
+ segment.id
1092
+ );
1093
+ }
1094
+ if (segment.type === "image") {
1095
+ const src = segment.source || getSource(segment);
1096
+ if (!src) {
1097
+ console.warn(`No source found for image segment: ${segment.id}`);
1098
+ return null;
1099
+ }
1100
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
1101
+ ImageElement,
1102
+ {
1103
+ segment,
1104
+ src,
1105
+ scale
1106
+ },
1107
+ segment.id
1108
+ );
1109
+ }
1110
+ return null;
1111
+ })
1112
+ ]
1113
+ }
1114
+ ) });
984
1115
  }
985
1116
  function BackgroundImage({
986
1117
  segment,
@@ -1486,6 +1617,7 @@ var RenderRoot = () => {
1486
1617
  areFontsLoaded,
1487
1618
  buildFontString,
1488
1619
  calculateAutoWidthDimensions,
1620
+ calculateCropBounds,
1489
1621
  calculateFitDimensions,
1490
1622
  calculateLineWidth,
1491
1623
  canSetAsReference,
@@ -1495,6 +1627,7 @@ var RenderRoot = () => {
1495
1627
  getReferenceElementX,
1496
1628
  getReferenceElementY,
1497
1629
  hexToRgba,
1630
+ isDynamicCropEnabled,
1498
1631
  parseHexColor,
1499
1632
  preloadFonts,
1500
1633
  resolveElementPositions,
package/dist/index.mjs CHANGED
@@ -738,6 +738,113 @@ function getReferenceElementY(elements, elementId) {
738
738
  return elements.find((e) => e.id === element.relativePositionY.elementId) ?? null;
739
739
  }
740
740
 
741
+ // src/utils/cropBounds.ts
742
+ function calculateCropBounds(elements, dynamicCrop, canvasWidth, canvasHeight) {
743
+ if (!dynamicCrop) {
744
+ return { x: 0, y: 0, width: canvasWidth, height: canvasHeight };
745
+ }
746
+ const elementMap = /* @__PURE__ */ new Map();
747
+ for (const elem of elements) {
748
+ elementMap.set(elem.id, elem);
749
+ }
750
+ const resolveBoundary = (boundary) => {
751
+ if (!boundary) return void 0;
752
+ if (boundary.elementId) return boundary.elementId;
753
+ return void 0;
754
+ };
755
+ let cropY = 0;
756
+ let cropHeight = canvasHeight;
757
+ if (dynamicCrop.vertical?.enabled) {
758
+ const vCrop = dynamicCrop.vertical;
759
+ const paddingStart = vCrop.paddingStart ?? 0;
760
+ const paddingEnd = vCrop.paddingEnd ?? 0;
761
+ if (vCrop.mode === "all-elements") {
762
+ let minY = canvasHeight;
763
+ let maxY = 0;
764
+ for (const elem of elements) {
765
+ minY = Math.min(minY, elem.y);
766
+ maxY = Math.max(maxY, elem.y + elem.height);
767
+ }
768
+ if (elements.length > 0) {
769
+ cropY = Math.max(0, minY - paddingStart);
770
+ const bottomY = Math.min(canvasHeight, maxY + paddingEnd);
771
+ cropHeight = bottomY - cropY;
772
+ }
773
+ } else if (vCrop.mode === "between-elements") {
774
+ const startElementId = resolveBoundary(vCrop.startBoundary);
775
+ const endElementId = resolveBoundary(vCrop.endBoundary);
776
+ let topY = 0;
777
+ let bottomY = canvasHeight;
778
+ if (startElementId) {
779
+ const startElem = elementMap.get(startElementId);
780
+ if (startElem) {
781
+ topY = startElem.y;
782
+ }
783
+ }
784
+ if (endElementId) {
785
+ const endElem = elementMap.get(endElementId);
786
+ if (endElem) {
787
+ bottomY = endElem.y + endElem.height;
788
+ }
789
+ }
790
+ cropY = Math.max(0, topY - paddingStart);
791
+ const adjustedBottom = Math.min(canvasHeight, bottomY + paddingEnd);
792
+ cropHeight = adjustedBottom - cropY;
793
+ }
794
+ if (vCrop.minSize && cropHeight < vCrop.minSize) {
795
+ cropHeight = vCrop.minSize;
796
+ }
797
+ }
798
+ let cropX = 0;
799
+ let cropWidth = canvasWidth;
800
+ if (dynamicCrop.horizontal?.enabled) {
801
+ const hCrop = dynamicCrop.horizontal;
802
+ const paddingStart = hCrop.paddingStart ?? 0;
803
+ const paddingEnd = hCrop.paddingEnd ?? 0;
804
+ if (hCrop.mode === "all-elements") {
805
+ let minX = canvasWidth;
806
+ let maxX = 0;
807
+ for (const elem of elements) {
808
+ minX = Math.min(minX, elem.x);
809
+ maxX = Math.max(maxX, elem.x + elem.width);
810
+ }
811
+ if (elements.length > 0) {
812
+ cropX = Math.max(0, minX - paddingStart);
813
+ const rightX = Math.min(canvasWidth, maxX + paddingEnd);
814
+ cropWidth = rightX - cropX;
815
+ }
816
+ } else if (hCrop.mode === "between-elements") {
817
+ const startElementId = resolveBoundary(hCrop.startBoundary);
818
+ const endElementId = resolveBoundary(hCrop.endBoundary);
819
+ let leftX = 0;
820
+ let rightX = canvasWidth;
821
+ if (startElementId) {
822
+ const startElem = elementMap.get(startElementId);
823
+ if (startElem) {
824
+ leftX = startElem.x;
825
+ }
826
+ }
827
+ if (endElementId) {
828
+ const endElem = elementMap.get(endElementId);
829
+ if (endElem) {
830
+ rightX = endElem.x + endElem.width;
831
+ }
832
+ }
833
+ cropX = Math.max(0, leftX - paddingStart);
834
+ const adjustedRight = Math.min(canvasWidth, rightX + paddingEnd);
835
+ cropWidth = adjustedRight - cropX;
836
+ }
837
+ if (hCrop.minSize && cropWidth < hCrop.minSize) {
838
+ cropWidth = hCrop.minSize;
839
+ }
840
+ }
841
+ return { x: cropX, y: cropY, width: cropWidth, height: cropHeight };
842
+ }
843
+ function isDynamicCropEnabled(dynamicCrop) {
844
+ if (!dynamicCrop) return false;
845
+ return !!(dynamicCrop.vertical?.enabled || dynamicCrop.horizontal?.enabled);
846
+ }
847
+
741
848
  // src/compositions/ImageEditorComposition.tsx
742
849
  import { jsx as jsx3, jsxs } from "react/jsx-runtime";
743
850
  function getSortedSegments(config) {
@@ -813,11 +920,16 @@ function ImageEditorComposition({
813
920
  elements,
814
921
  width,
815
922
  height,
923
+ backgroundType = "image",
924
+ backgroundColor,
816
925
  backgroundFit = "cover",
817
926
  backgroundUrl,
818
927
  imageUrls = {},
819
- textValues = {}
928
+ textValues = {},
929
+ dynamicCrop
820
930
  }) {
931
+ const canvasWidth = width ?? config?.width ?? 1080;
932
+ const canvasHeight = height ?? config?.height ?? 1920;
821
933
  const resolvedElements = useMemo3(() => {
822
934
  if (!elements) return null;
823
935
  const result = resolveElementPositions(elements, textValues);
@@ -826,6 +938,10 @@ function ImageEditorComposition({
826
938
  }
827
939
  return result.elements;
828
940
  }, [elements, textValues]);
941
+ const cropBounds = useMemo3(() => {
942
+ if (!isDynamicCropEnabled(dynamicCrop) || !resolvedElements) return null;
943
+ return calculateCropBounds(resolvedElements, dynamicCrop, canvasWidth, canvasHeight);
944
+ }, [resolvedElements, dynamicCrop, canvasWidth, canvasHeight]);
829
945
  const segmentsFromElements = useMemo3(() => {
830
946
  if (!resolvedElements) return null;
831
947
  const segments = [];
@@ -842,10 +958,10 @@ function ImageEditorComposition({
842
958
  }
843
959
  return segments.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
844
960
  }, [resolvedElements, imageUrls, textValues]);
845
- const canvasWidth = width ?? config?.width ?? 1080;
846
- const canvasHeight = height ?? config?.height ?? 1920;
847
961
  const bgFit = backgroundFit ?? "cover";
848
962
  const bgUrl = backgroundUrl ?? sources.background;
963
+ const cropOffsetX = cropBounds?.x ?? 0;
964
+ const cropOffsetY = cropBounds?.y ?? 0;
849
965
  const contentSegments = segmentsFromElements ?? (() => {
850
966
  if (!config) return [];
851
967
  const sorted = getSortedSegments(config);
@@ -865,61 +981,74 @@ function ImageEditorComposition({
865
981
  }
866
982
  return void 0;
867
983
  };
868
- return /* @__PURE__ */ jsxs(AbsoluteFill, { style: { backgroundColor: "#000000" }, children: [
869
- bgUrl && segmentsFromElements && /* @__PURE__ */ jsx3(
870
- Img2,
871
- {
872
- src: bgUrl,
873
- style: {
874
- position: "absolute",
875
- left: 0,
876
- top: 0,
877
- width: canvasWidth * scale,
878
- height: canvasHeight * scale,
879
- objectFit: bgFit
880
- }
881
- }
882
- ),
883
- legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ jsx3(
884
- BackgroundImage,
885
- {
886
- segment: legacyBackgroundSegment,
887
- src: getSource(legacyBackgroundSegment),
888
- width: canvasWidth,
889
- height: canvasHeight,
890
- scale
891
- }
892
- ),
893
- contentSegments.map((segment) => {
894
- if (segment.type === "text") {
895
- return /* @__PURE__ */ jsx3(
896
- TextElement,
984
+ const containerBgColor = backgroundType === "color" && backgroundColor ? backgroundColor : "#000000";
985
+ return /* @__PURE__ */ jsx3(AbsoluteFill, { style: { backgroundColor: containerBgColor }, children: /* @__PURE__ */ jsxs(
986
+ "div",
987
+ {
988
+ style: {
989
+ position: "absolute",
990
+ left: -cropOffsetX * scale,
991
+ top: -cropOffsetY * scale,
992
+ width: canvasWidth * scale,
993
+ height: canvasHeight * scale
994
+ },
995
+ children: [
996
+ backgroundType === "image" && bgUrl && segmentsFromElements && /* @__PURE__ */ jsx3(
997
+ Img2,
897
998
  {
898
- segment,
899
- scale
900
- },
901
- segment.id
902
- );
903
- }
904
- if (segment.type === "image") {
905
- const src = segment.source || getSource(segment);
906
- if (!src) {
907
- console.warn(`No source found for image segment: ${segment.id}`);
908
- return null;
909
- }
910
- return /* @__PURE__ */ jsx3(
911
- ImageElement,
999
+ src: bgUrl,
1000
+ style: {
1001
+ position: "absolute",
1002
+ left: 0,
1003
+ top: 0,
1004
+ width: canvasWidth * scale,
1005
+ height: canvasHeight * scale,
1006
+ objectFit: bgFit
1007
+ }
1008
+ }
1009
+ ),
1010
+ legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ jsx3(
1011
+ BackgroundImage,
912
1012
  {
913
- segment,
914
- src,
1013
+ segment: legacyBackgroundSegment,
1014
+ src: getSource(legacyBackgroundSegment),
1015
+ width: canvasWidth,
1016
+ height: canvasHeight,
915
1017
  scale
916
- },
917
- segment.id
918
- );
919
- }
920
- return null;
921
- })
922
- ] });
1018
+ }
1019
+ ),
1020
+ contentSegments.map((segment) => {
1021
+ if (segment.type === "text") {
1022
+ return /* @__PURE__ */ jsx3(
1023
+ TextElement,
1024
+ {
1025
+ segment,
1026
+ scale
1027
+ },
1028
+ segment.id
1029
+ );
1030
+ }
1031
+ if (segment.type === "image") {
1032
+ const src = segment.source || getSource(segment);
1033
+ if (!src) {
1034
+ console.warn(`No source found for image segment: ${segment.id}`);
1035
+ return null;
1036
+ }
1037
+ return /* @__PURE__ */ jsx3(
1038
+ ImageElement,
1039
+ {
1040
+ segment,
1041
+ src,
1042
+ scale
1043
+ },
1044
+ segment.id
1045
+ );
1046
+ }
1047
+ return null;
1048
+ })
1049
+ ]
1050
+ }
1051
+ ) });
923
1052
  }
924
1053
  function BackgroundImage({
925
1054
  segment,
@@ -1424,6 +1553,7 @@ export {
1424
1553
  areFontsLoaded,
1425
1554
  buildFontString,
1426
1555
  calculateAutoWidthDimensions,
1556
+ calculateCropBounds,
1427
1557
  calculateFitDimensions,
1428
1558
  calculateLineWidth,
1429
1559
  canSetAsReference,
@@ -1433,6 +1563,7 @@ export {
1433
1563
  getReferenceElementX,
1434
1564
  getReferenceElementY,
1435
1565
  hexToRgba,
1566
+ isDynamicCropEnabled,
1436
1567
  parseHexColor,
1437
1568
  preloadFonts,
1438
1569
  resolveElementPositions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugcinc-render",
3
- "version": "1.3.12",
3
+ "version": "1.3.14",
4
4
  "description": "Unified rendering package for UGC Inc - shared types, components, and compositions for pixel-perfect client/server rendering",
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",