ugcinc-render 1.3.13 → 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
@@ -756,6 +756,8 @@ interface ImageEditorCompositionProps {
756
756
  imageUrls?: Record<string, string | null>;
757
757
  /** Text values keyed by textInputId (when using elements, for autoWidth calculation) */
758
758
  textValues?: Record<string, string>;
759
+ /** Dynamic crop configuration */
760
+ dynamicCrop?: DynamicCropConfig;
759
761
  }
760
762
  /**
761
763
  * ImageEditorComposition renders a complete image editor configuration.
@@ -790,7 +792,7 @@ interface ImageEditorCompositionProps {
790
792
  * />
791
793
  * ```
792
794
  */
793
- declare function ImageEditorComposition({ config, sources, scale, elements, width, height, backgroundType, backgroundColor, 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;
794
796
 
795
797
  interface VideoEditorCompositionProps {
796
798
  /** The editor configuration to render */
@@ -1144,6 +1146,25 @@ declare function getReferenceElementX(elements: ImageEditorElement[], elementId:
1144
1146
  */
1145
1147
  declare function getReferenceElementY(elements: ImageEditorElement[], elementId: string): ImageEditorElement | null;
1146
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
+
1147
1168
  /**
1148
1169
  * Hook exports for ugcinc-render
1149
1170
  *
@@ -1203,4 +1224,4 @@ declare function useResolvedPositions(elements: ImageEditorElement[], textValues
1203
1224
 
1204
1225
  declare const RenderRoot: React.FC;
1205
1226
 
1206
- 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
@@ -756,6 +756,8 @@ interface ImageEditorCompositionProps {
756
756
  imageUrls?: Record<string, string | null>;
757
757
  /** Text values keyed by textInputId (when using elements, for autoWidth calculation) */
758
758
  textValues?: Record<string, string>;
759
+ /** Dynamic crop configuration */
760
+ dynamicCrop?: DynamicCropConfig;
759
761
  }
760
762
  /**
761
763
  * ImageEditorComposition renders a complete image editor configuration.
@@ -790,7 +792,7 @@ interface ImageEditorCompositionProps {
790
792
  * />
791
793
  * ```
792
794
  */
793
- declare function ImageEditorComposition({ config, sources, scale, elements, width, height, backgroundType, backgroundColor, 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;
794
796
 
795
797
  interface VideoEditorCompositionProps {
796
798
  /** The editor configuration to render */
@@ -1144,6 +1146,25 @@ declare function getReferenceElementX(elements: ImageEditorElement[], elementId:
1144
1146
  */
1145
1147
  declare function getReferenceElementY(elements: ImageEditorElement[], elementId: string): ImageEditorElement | null;
1146
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
+
1147
1168
  /**
1148
1169
  * Hook exports for ugcinc-render
1149
1170
  *
@@ -1203,4 +1224,4 @@ declare function useResolvedPositions(elements: ImageEditorElement[], textValues
1203
1224
 
1204
1225
  declare const RenderRoot: React.FC;
1205
1226
 
1206
- 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) {
@@ -879,8 +988,11 @@ function ImageEditorComposition({
879
988
  backgroundFit = "cover",
880
989
  backgroundUrl,
881
990
  imageUrls = {},
882
- textValues = {}
991
+ textValues = {},
992
+ dynamicCrop
883
993
  }) {
994
+ const canvasWidth = width ?? config?.width ?? 1080;
995
+ const canvasHeight = height ?? config?.height ?? 1920;
884
996
  const resolvedElements = (0, import_react3.useMemo)(() => {
885
997
  if (!elements) return null;
886
998
  const result = resolveElementPositions(elements, textValues);
@@ -889,6 +1001,10 @@ function ImageEditorComposition({
889
1001
  }
890
1002
  return result.elements;
891
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]);
892
1008
  const segmentsFromElements = (0, import_react3.useMemo)(() => {
893
1009
  if (!resolvedElements) return null;
894
1010
  const segments = [];
@@ -905,10 +1021,10 @@ function ImageEditorComposition({
905
1021
  }
906
1022
  return segments.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
907
1023
  }, [resolvedElements, imageUrls, textValues]);
908
- const canvasWidth = width ?? config?.width ?? 1080;
909
- const canvasHeight = height ?? config?.height ?? 1920;
910
1024
  const bgFit = backgroundFit ?? "cover";
911
1025
  const bgUrl = backgroundUrl ?? sources.background;
1026
+ const cropOffsetX = cropBounds?.x ?? 0;
1027
+ const cropOffsetY = cropBounds?.y ?? 0;
912
1028
  const contentSegments = segmentsFromElements ?? (() => {
913
1029
  if (!config) return [];
914
1030
  const sorted = getSortedSegments(config);
@@ -929,61 +1045,73 @@ function ImageEditorComposition({
929
1045
  return void 0;
930
1046
  };
931
1047
  const containerBgColor = backgroundType === "color" && backgroundColor ? backgroundColor : "#000000";
932
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(import_remotion2.AbsoluteFill, { style: { backgroundColor: containerBgColor }, children: [
933
- backgroundType === "image" && bgUrl && segmentsFromElements && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
934
- import_remotion2.Img,
935
- {
936
- src: bgUrl,
937
- style: {
938
- position: "absolute",
939
- left: 0,
940
- top: 0,
941
- width: canvasWidth * scale,
942
- height: canvasHeight * scale,
943
- objectFit: bgFit
944
- }
945
- }
946
- ),
947
- legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
948
- BackgroundImage,
949
- {
950
- segment: legacyBackgroundSegment,
951
- src: getSource(legacyBackgroundSegment),
952
- width: canvasWidth,
953
- height: canvasHeight,
954
- scale
955
- }
956
- ),
957
- contentSegments.map((segment) => {
958
- if (segment.type === "text") {
959
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
960
- TextElement,
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,
961
1061
  {
962
- segment,
963
- scale
964
- },
965
- segment.id
966
- );
967
- }
968
- if (segment.type === "image") {
969
- const src = segment.source || getSource(segment);
970
- if (!src) {
971
- console.warn(`No source found for image segment: ${segment.id}`);
972
- return null;
973
- }
974
- return /* @__PURE__ */ (0, import_jsx_runtime3.jsx)(
975
- 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,
976
1075
  {
977
- segment,
978
- src,
1076
+ segment: legacyBackgroundSegment,
1077
+ src: getSource(legacyBackgroundSegment),
1078
+ width: canvasWidth,
1079
+ height: canvasHeight,
979
1080
  scale
980
- },
981
- segment.id
982
- );
983
- }
984
- return null;
985
- })
986
- ] });
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
+ ) });
987
1115
  }
988
1116
  function BackgroundImage({
989
1117
  segment,
@@ -1489,6 +1617,7 @@ var RenderRoot = () => {
1489
1617
  areFontsLoaded,
1490
1618
  buildFontString,
1491
1619
  calculateAutoWidthDimensions,
1620
+ calculateCropBounds,
1492
1621
  calculateFitDimensions,
1493
1622
  calculateLineWidth,
1494
1623
  canSetAsReference,
@@ -1498,6 +1627,7 @@ var RenderRoot = () => {
1498
1627
  getReferenceElementX,
1499
1628
  getReferenceElementY,
1500
1629
  hexToRgba,
1630
+ isDynamicCropEnabled,
1501
1631
  parseHexColor,
1502
1632
  preloadFonts,
1503
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) {
@@ -818,8 +925,11 @@ function ImageEditorComposition({
818
925
  backgroundFit = "cover",
819
926
  backgroundUrl,
820
927
  imageUrls = {},
821
- textValues = {}
928
+ textValues = {},
929
+ dynamicCrop
822
930
  }) {
931
+ const canvasWidth = width ?? config?.width ?? 1080;
932
+ const canvasHeight = height ?? config?.height ?? 1920;
823
933
  const resolvedElements = useMemo3(() => {
824
934
  if (!elements) return null;
825
935
  const result = resolveElementPositions(elements, textValues);
@@ -828,6 +938,10 @@ function ImageEditorComposition({
828
938
  }
829
939
  return result.elements;
830
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]);
831
945
  const segmentsFromElements = useMemo3(() => {
832
946
  if (!resolvedElements) return null;
833
947
  const segments = [];
@@ -844,10 +958,10 @@ function ImageEditorComposition({
844
958
  }
845
959
  return segments.sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
846
960
  }, [resolvedElements, imageUrls, textValues]);
847
- const canvasWidth = width ?? config?.width ?? 1080;
848
- const canvasHeight = height ?? config?.height ?? 1920;
849
961
  const bgFit = backgroundFit ?? "cover";
850
962
  const bgUrl = backgroundUrl ?? sources.background;
963
+ const cropOffsetX = cropBounds?.x ?? 0;
964
+ const cropOffsetY = cropBounds?.y ?? 0;
851
965
  const contentSegments = segmentsFromElements ?? (() => {
852
966
  if (!config) return [];
853
967
  const sorted = getSortedSegments(config);
@@ -868,61 +982,73 @@ function ImageEditorComposition({
868
982
  return void 0;
869
983
  };
870
984
  const containerBgColor = backgroundType === "color" && backgroundColor ? backgroundColor : "#000000";
871
- return /* @__PURE__ */ jsxs(AbsoluteFill, { style: { backgroundColor: containerBgColor }, children: [
872
- backgroundType === "image" && bgUrl && segmentsFromElements && /* @__PURE__ */ jsx3(
873
- Img2,
874
- {
875
- src: bgUrl,
876
- style: {
877
- position: "absolute",
878
- left: 0,
879
- top: 0,
880
- width: canvasWidth * scale,
881
- height: canvasHeight * scale,
882
- objectFit: bgFit
883
- }
884
- }
885
- ),
886
- legacyBackgroundSegment && !segmentsFromElements && /* @__PURE__ */ jsx3(
887
- BackgroundImage,
888
- {
889
- segment: legacyBackgroundSegment,
890
- src: getSource(legacyBackgroundSegment),
891
- width: canvasWidth,
892
- height: canvasHeight,
893
- scale
894
- }
895
- ),
896
- contentSegments.map((segment) => {
897
- if (segment.type === "text") {
898
- return /* @__PURE__ */ jsx3(
899
- TextElement,
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,
900
998
  {
901
- segment,
902
- scale
903
- },
904
- segment.id
905
- );
906
- }
907
- if (segment.type === "image") {
908
- const src = segment.source || getSource(segment);
909
- if (!src) {
910
- console.warn(`No source found for image segment: ${segment.id}`);
911
- return null;
912
- }
913
- return /* @__PURE__ */ jsx3(
914
- 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,
915
1012
  {
916
- segment,
917
- src,
1013
+ segment: legacyBackgroundSegment,
1014
+ src: getSource(legacyBackgroundSegment),
1015
+ width: canvasWidth,
1016
+ height: canvasHeight,
918
1017
  scale
919
- },
920
- segment.id
921
- );
922
- }
923
- return null;
924
- })
925
- ] });
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
+ ) });
926
1052
  }
927
1053
  function BackgroundImage({
928
1054
  segment,
@@ -1427,6 +1553,7 @@ export {
1427
1553
  areFontsLoaded,
1428
1554
  buildFontString,
1429
1555
  calculateAutoWidthDimensions,
1556
+ calculateCropBounds,
1430
1557
  calculateFitDimensions,
1431
1558
  calculateLineWidth,
1432
1559
  canSetAsReference,
@@ -1436,6 +1563,7 @@ export {
1436
1563
  getReferenceElementX,
1437
1564
  getReferenceElementY,
1438
1565
  hexToRgba,
1566
+ isDynamicCropEnabled,
1439
1567
  parseHexColor,
1440
1568
  preloadFonts,
1441
1569
  resolveElementPositions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugcinc-render",
3
- "version": "1.3.13",
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",