ugcinc-render 1.5.10 → 1.5.12

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
@@ -1290,6 +1290,34 @@ declare function generateSegmentId(): string;
1290
1290
  */
1291
1291
  declare function generateOverlayId(): string;
1292
1292
 
1293
+ /**
1294
+ * Emoji utilities for rendering Apple-style emojis consistently
1295
+ * across all platforms using CDN-hosted emoji images.
1296
+ */
1297
+ /**
1298
+ * Convert an emoji character to its unified unicode codepoint format
1299
+ * Example: "😀" -> "1f600", "👨‍👩‍👧" -> "1f468-200d-1f469-200d-1f467"
1300
+ */
1301
+ declare function emojiToUnified(emoji: string): string;
1302
+ /**
1303
+ * Get the Apple emoji CDN URL for a given emoji character
1304
+ */
1305
+ declare function getAppleEmojiUrl(emoji: string): string;
1306
+ /**
1307
+ * Check if a string contains any emoji characters
1308
+ */
1309
+ declare function hasEmoji(text: string): boolean;
1310
+ /**
1311
+ * Split text into segments of text and emojis
1312
+ * Returns an array of segments with type 'text' or 'emoji'
1313
+ */
1314
+ interface TextSegmentPart {
1315
+ type: 'text' | 'emoji';
1316
+ content: string;
1317
+ imageUrl?: string;
1318
+ }
1319
+ declare function splitTextAndEmojis(text: string): TextSegmentPart[];
1320
+
1293
1321
  /**
1294
1322
  * Hook exports for ugcinc-render
1295
1323
  *
@@ -1349,4 +1377,4 @@ declare function useResolvedPositions(elements: ImageEditorElement[], textValues
1349
1377
 
1350
1378
  declare const RenderRoot: React.FC;
1351
1379
 
1352
- 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 SegmentTimelinePosition, 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, calculateEstimatedDuration, calculateFitDimensions, calculateLineWidth, calculateTimelineContentEnd, canSetAsReference, defaultOffset, formatTime, generateOverlayId, generateSegmentId, getBaseSegments, getBorderRadii, getDependentElements, getFontFamily, getOverlays, getReferenceElementX, getReferenceElementY, getSegmentTimelinePosition, hexToRgba, isDynamicCropEnabled, isSegmentVisibleAtTime, parseHexColor, parseTime, preloadFonts, resolveElementPositions, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
1380
+ 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 SegmentTimelinePosition, type SegmentType, type StaticSegment, TEXT_DEFAULTS, type TextAlignment, type TextDirection, TextElement, type TextElementProps, type TextOverflow, type TextSegment, type TextSegmentPart, 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, calculateEstimatedDuration, calculateFitDimensions, calculateLineWidth, calculateTimelineContentEnd, canSetAsReference, defaultOffset, emojiToUnified, formatTime, generateOverlayId, generateSegmentId, getAppleEmojiUrl, getBaseSegments, getBorderRadii, getDependentElements, getFontFamily, getOverlays, getReferenceElementX, getReferenceElementY, getSegmentTimelinePosition, hasEmoji, hexToRgba, isDynamicCropEnabled, isSegmentVisibleAtTime, parseHexColor, parseTime, preloadFonts, resolveElementPositions, splitTextAndEmojis, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
package/dist/index.d.ts CHANGED
@@ -1290,6 +1290,34 @@ declare function generateSegmentId(): string;
1290
1290
  */
1291
1291
  declare function generateOverlayId(): string;
1292
1292
 
1293
+ /**
1294
+ * Emoji utilities for rendering Apple-style emojis consistently
1295
+ * across all platforms using CDN-hosted emoji images.
1296
+ */
1297
+ /**
1298
+ * Convert an emoji character to its unified unicode codepoint format
1299
+ * Example: "😀" -> "1f600", "👨‍👩‍👧" -> "1f468-200d-1f469-200d-1f467"
1300
+ */
1301
+ declare function emojiToUnified(emoji: string): string;
1302
+ /**
1303
+ * Get the Apple emoji CDN URL for a given emoji character
1304
+ */
1305
+ declare function getAppleEmojiUrl(emoji: string): string;
1306
+ /**
1307
+ * Check if a string contains any emoji characters
1308
+ */
1309
+ declare function hasEmoji(text: string): boolean;
1310
+ /**
1311
+ * Split text into segments of text and emojis
1312
+ * Returns an array of segments with type 'text' or 'emoji'
1313
+ */
1314
+ interface TextSegmentPart {
1315
+ type: 'text' | 'emoji';
1316
+ content: string;
1317
+ imageUrl?: string;
1318
+ }
1319
+ declare function splitTextAndEmojis(text: string): TextSegmentPart[];
1320
+
1293
1321
  /**
1294
1322
  * Hook exports for ugcinc-render
1295
1323
  *
@@ -1349,4 +1377,4 @@ declare function useResolvedPositions(elements: ImageEditorElement[], textValues
1349
1377
 
1350
1378
  declare const RenderRoot: React.FC;
1351
1379
 
1352
- 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 SegmentTimelinePosition, 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, calculateEstimatedDuration, calculateFitDimensions, calculateLineWidth, calculateTimelineContentEnd, canSetAsReference, defaultOffset, formatTime, generateOverlayId, generateSegmentId, getBaseSegments, getBorderRadii, getDependentElements, getFontFamily, getOverlays, getReferenceElementX, getReferenceElementY, getSegmentTimelinePosition, hexToRgba, isDynamicCropEnabled, isSegmentVisibleAtTime, parseHexColor, parseTime, preloadFonts, resolveElementPositions, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
1380
+ 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 SegmentTimelinePosition, type SegmentType, type StaticSegment, TEXT_DEFAULTS, type TextAlignment, type TextDirection, TextElement, type TextElementProps, type TextOverflow, type TextSegment, type TextSegmentPart, 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, calculateEstimatedDuration, calculateFitDimensions, calculateLineWidth, calculateTimelineContentEnd, canSetAsReference, defaultOffset, emojiToUnified, formatTime, generateOverlayId, generateSegmentId, getAppleEmojiUrl, getBaseSegments, getBorderRadii, getDependentElements, getFontFamily, getOverlays, getReferenceElementX, getReferenceElementY, getSegmentTimelinePosition, hasEmoji, hexToRgba, isDynamicCropEnabled, isSegmentVisibleAtTime, parseHexColor, parseTime, preloadFonts, resolveElementPositions, splitTextAndEmojis, useFontsLoaded, useImageLoader, useImagePreloader, useResolvedPositions, wrapText };
package/dist/index.js CHANGED
@@ -56,9 +56,11 @@ __export(index_exports, {
56
56
  calculateTimelineContentEnd: () => calculateTimelineContentEnd,
57
57
  canSetAsReference: () => canSetAsReference,
58
58
  defaultOffset: () => defaultOffset,
59
+ emojiToUnified: () => emojiToUnified,
59
60
  formatTime: () => formatTime,
60
61
  generateOverlayId: () => generateOverlayId,
61
62
  generateSegmentId: () => generateSegmentId,
63
+ getAppleEmojiUrl: () => getAppleEmojiUrl,
62
64
  getBaseSegments: () => getBaseSegments,
63
65
  getBorderRadii: () => getBorderRadii,
64
66
  getDependentElements: () => getDependentElements,
@@ -67,6 +69,7 @@ __export(index_exports, {
67
69
  getReferenceElementX: () => getReferenceElementX,
68
70
  getReferenceElementY: () => getReferenceElementY,
69
71
  getSegmentTimelinePosition: () => getSegmentTimelinePosition,
72
+ hasEmoji: () => hasEmoji,
70
73
  hexToRgba: () => hexToRgba,
71
74
  isDynamicCropEnabled: () => isDynamicCropEnabled,
72
75
  isSegmentVisibleAtTime: () => isSegmentVisibleAtTime,
@@ -74,6 +77,7 @@ __export(index_exports, {
74
77
  parseTime: () => parseTime,
75
78
  preloadFonts: () => preloadFonts,
76
79
  resolveElementPositions: () => resolveElementPositions,
80
+ splitTextAndEmojis: () => splitTextAndEmojis,
77
81
  useFontsLoaded: () => useFontsLoaded,
78
82
  useImageLoader: () => useImageLoader,
79
83
  useImagePreloader: () => useImagePreloader,
@@ -302,6 +306,54 @@ function hexToRgba(hex, opacity = 100) {
302
306
  return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
303
307
  }
304
308
 
309
+ // src/utils/emoji.ts
310
+ var APPLE_EMOJI_CDN_BASE = "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/";
311
+ function emojiToUnified(emoji) {
312
+ const codePoints = [];
313
+ for (const codePoint of emoji) {
314
+ const hex = codePoint.codePointAt(0)?.toString(16);
315
+ if (hex) {
316
+ codePoints.push(hex);
317
+ }
318
+ }
319
+ return codePoints.join("-");
320
+ }
321
+ function getAppleEmojiUrl(emoji) {
322
+ const unified = emojiToUnified(emoji);
323
+ return `${APPLE_EMOJI_CDN_BASE}${unified}.png`;
324
+ }
325
+ var EMOJI_REGEX = /(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\p{Emoji_Modifier}|\uFE0F|\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\p{Emoji_Modifier})?)*|\p{Regional_Indicator}{2}/gu;
326
+ function hasEmoji(text) {
327
+ return EMOJI_REGEX.test(text);
328
+ }
329
+ function splitTextAndEmojis(text) {
330
+ const segments = [];
331
+ let lastIndex = 0;
332
+ EMOJI_REGEX.lastIndex = 0;
333
+ let match;
334
+ while ((match = EMOJI_REGEX.exec(text)) !== null) {
335
+ if (match.index > lastIndex) {
336
+ segments.push({
337
+ type: "text",
338
+ content: text.slice(lastIndex, match.index)
339
+ });
340
+ }
341
+ segments.push({
342
+ type: "emoji",
343
+ content: match[0],
344
+ imageUrl: getAppleEmojiUrl(match[0])
345
+ });
346
+ lastIndex = match.index + match[0].length;
347
+ }
348
+ if (lastIndex < text.length) {
349
+ segments.push({
350
+ type: "text",
351
+ content: text.slice(lastIndex)
352
+ });
353
+ }
354
+ return segments;
355
+ }
356
+
305
357
  // src/components/TextElement.tsx
306
358
  var import_jsx_runtime = require("react/jsx-runtime");
307
359
  function calculateAutoWidthAndLines({
@@ -436,14 +488,22 @@ function TextElement({ segment, scale = 1 }) {
436
488
  const backgroundOpacity = segment.backgroundOpacity ?? TEXT_DEFAULTS.backgroundOpacity;
437
489
  const backgroundBorderRadius = segment.backgroundBorderRadius;
438
490
  const fontFamily = getFontFamily(fontType);
439
- const { autoWidthResult, isMultiLine } = (0, import_react.useMemo)(() => {
491
+ const { calculatedWidth, calculatedLines, paddingTop, paddingRight, paddingBottom, paddingLeft } = (0, import_react.useMemo)(() => {
492
+ let finalPaddingTop = basePaddingTop;
493
+ let finalPaddingRight = basePaddingRight;
494
+ let finalPaddingBottom = basePaddingBottom;
495
+ let finalPaddingLeft = basePaddingLeft;
440
496
  if (!autoWidth && !hasExtraPadding) {
441
497
  return {
442
- autoWidthResult: null,
443
- isMultiLine: false
498
+ calculatedWidth: width,
499
+ calculatedLines: [segment.text],
500
+ paddingTop: finalPaddingTop,
501
+ paddingRight: finalPaddingRight,
502
+ paddingBottom: finalPaddingBottom,
503
+ paddingLeft: finalPaddingLeft
444
504
  };
445
505
  }
446
- const result = calculateAutoWidthAndLines({
506
+ const baseResult = calculateAutoWidthAndLines({
447
507
  text: segment.text,
448
508
  maxWidth: width,
449
509
  paddingLeft: basePaddingLeft,
@@ -454,16 +514,47 @@ function TextElement({ segment, scale = 1 }) {
454
514
  letterSpacing,
455
515
  lineHeight
456
516
  });
517
+ const isMultiLine = baseResult.lines.length >= 2;
518
+ if (isMultiLine) {
519
+ finalPaddingTop = basePaddingTop + extraPaddingTop;
520
+ finalPaddingRight = basePaddingRight + extraPaddingRight;
521
+ finalPaddingBottom = basePaddingBottom + extraPaddingBottom;
522
+ finalPaddingLeft = basePaddingLeft + extraPaddingLeft;
523
+ }
524
+ let finalResult = baseResult;
525
+ if (isMultiLine && (extraPaddingLeft > 0 || extraPaddingRight > 0)) {
526
+ finalResult = calculateAutoWidthAndLines({
527
+ text: segment.text,
528
+ maxWidth: width,
529
+ paddingLeft: finalPaddingLeft,
530
+ paddingRight: finalPaddingRight,
531
+ fontSize,
532
+ fontWeight,
533
+ fontFamily,
534
+ letterSpacing,
535
+ lineHeight
536
+ });
537
+ }
457
538
  return {
458
- autoWidthResult: autoWidth ? result : null,
459
- isMultiLine: result.lines.length >= 2
539
+ calculatedWidth: autoWidth ? finalResult.width : width,
540
+ calculatedLines: autoWidth ? finalResult.lines : [segment.text],
541
+ paddingTop: finalPaddingTop,
542
+ paddingRight: finalPaddingRight,
543
+ paddingBottom: finalPaddingBottom,
544
+ paddingLeft: finalPaddingLeft
460
545
  };
461
546
  }, [
462
547
  autoWidth,
463
548
  segment.text,
464
549
  width,
465
- basePaddingLeft,
550
+ basePaddingTop,
466
551
  basePaddingRight,
552
+ basePaddingBottom,
553
+ basePaddingLeft,
554
+ extraPaddingTop,
555
+ extraPaddingRight,
556
+ extraPaddingBottom,
557
+ extraPaddingLeft,
467
558
  hasExtraPadding,
468
559
  fontSize,
469
560
  fontWeight,
@@ -471,51 +562,6 @@ function TextElement({ segment, scale = 1 }) {
471
562
  letterSpacing,
472
563
  lineHeight
473
564
  ]);
474
- const calculatedLines = autoWidthResult?.lines ?? [segment.text];
475
- const paddingTop = basePaddingTop + (isMultiLine ? extraPaddingTop : 0);
476
- const paddingRight = basePaddingRight + (isMultiLine ? extraPaddingRight : 0);
477
- const paddingBottom = basePaddingBottom + (isMultiLine ? extraPaddingBottom : 0);
478
- const paddingLeft = basePaddingLeft + (isMultiLine ? extraPaddingLeft : 0);
479
- const calculatedWidth = (0, import_react.useMemo)(() => {
480
- if (!autoWidth) return width;
481
- if (!isMultiLine || extraPaddingLeft === 0 && extraPaddingRight === 0) {
482
- return autoWidthResult?.width ?? width;
483
- }
484
- if (typeof document === "undefined") return width;
485
- const measureSpan = document.createElement("span");
486
- measureSpan.style.cssText = `
487
- position: absolute;
488
- visibility: hidden;
489
- pointer-events: none;
490
- font-family: ${fontFamily};
491
- font-size: ${fontSize}px;
492
- font-weight: ${fontWeight};
493
- letter-spacing: ${letterSpacing}px;
494
- white-space: nowrap;
495
- `;
496
- document.body.appendChild(measureSpan);
497
- let widestLineWidth = 0;
498
- for (const line of calculatedLines) {
499
- measureSpan.textContent = line;
500
- widestLineWidth = Math.max(widestLineWidth, measureSpan.getBoundingClientRect().width);
501
- }
502
- document.body.removeChild(measureSpan);
503
- return Math.min(widestLineWidth + paddingLeft + paddingRight, width);
504
- }, [
505
- autoWidth,
506
- autoWidthResult,
507
- width,
508
- isMultiLine,
509
- extraPaddingLeft,
510
- extraPaddingRight,
511
- paddingLeft,
512
- paddingRight,
513
- calculatedLines,
514
- fontFamily,
515
- fontSize,
516
- fontWeight,
517
- letterSpacing
518
- ]);
519
565
  const borderRadiusStyle = (0, import_react.useMemo)(() => {
520
566
  if (!backgroundBorderRadius) return void 0;
521
567
  const radii = getBorderRadii(backgroundBorderRadius);
@@ -610,9 +656,35 @@ function TextElement({ segment, scale = 1 }) {
610
656
  autoWidth,
611
657
  verticalAlign
612
658
  ]);
659
+ const renderTextWithEmojis = (text) => {
660
+ if (!hasEmoji(text)) {
661
+ return text;
662
+ }
663
+ const segments = splitTextAndEmojis(text);
664
+ return segments.map((segment2, i) => {
665
+ if (segment2.type === "emoji" && segment2.imageUrl) {
666
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
667
+ "img",
668
+ {
669
+ src: segment2.imageUrl,
670
+ alt: segment2.content,
671
+ style: {
672
+ height: "1em",
673
+ width: "1em",
674
+ verticalAlign: "-0.1em",
675
+ display: "inline"
676
+ },
677
+ draggable: false
678
+ },
679
+ i
680
+ );
681
+ }
682
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_react.default.Fragment, { children: segment2.content }, i);
683
+ });
684
+ };
613
685
  if (autoWidth) {
614
686
  const textContent = calculatedLines.map((line, index) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_react.default.Fragment, { children: [
615
- line,
687
+ renderTextWithEmojis(line),
616
688
  index < calculatedLines.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("br", {})
617
689
  ] }, index));
618
690
  if (backgroundColor) {
@@ -631,7 +703,7 @@ function TextElement({ segment, scale = 1 }) {
631
703
  }
632
704
  return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: positioningContainerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: { width: calculatedWidth, maxWidth: width }, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: textStyle, children: textContent }) }) });
633
705
  }
634
- return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: positioningContainerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: backgroundBoxStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: textStyle, children: segment.text }) }) });
706
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: positioningContainerStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: backgroundBoxStyle, children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: textStyle, children: renderTextWithEmojis(segment.text) }) }) });
635
707
  }
636
708
 
637
709
  // src/components/ImageElement.tsx
@@ -1951,9 +2023,11 @@ var RenderRoot = () => {
1951
2023
  calculateTimelineContentEnd,
1952
2024
  canSetAsReference,
1953
2025
  defaultOffset,
2026
+ emojiToUnified,
1954
2027
  formatTime,
1955
2028
  generateOverlayId,
1956
2029
  generateSegmentId,
2030
+ getAppleEmojiUrl,
1957
2031
  getBaseSegments,
1958
2032
  getBorderRadii,
1959
2033
  getDependentElements,
@@ -1962,6 +2036,7 @@ var RenderRoot = () => {
1962
2036
  getReferenceElementX,
1963
2037
  getReferenceElementY,
1964
2038
  getSegmentTimelinePosition,
2039
+ hasEmoji,
1965
2040
  hexToRgba,
1966
2041
  isDynamicCropEnabled,
1967
2042
  isSegmentVisibleAtTime,
@@ -1969,6 +2044,7 @@ var RenderRoot = () => {
1969
2044
  parseTime,
1970
2045
  preloadFonts,
1971
2046
  resolveElementPositions,
2047
+ splitTextAndEmojis,
1972
2048
  useFontsLoaded,
1973
2049
  useImageLoader,
1974
2050
  useImagePreloader,
package/dist/index.mjs CHANGED
@@ -218,6 +218,54 @@ function hexToRgba(hex, opacity = 100) {
218
218
  return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
219
219
  }
220
220
 
221
+ // src/utils/emoji.ts
222
+ var APPLE_EMOJI_CDN_BASE = "https://cdn.jsdelivr.net/npm/emoji-datasource-apple/img/apple/64/";
223
+ function emojiToUnified(emoji) {
224
+ const codePoints = [];
225
+ for (const codePoint of emoji) {
226
+ const hex = codePoint.codePointAt(0)?.toString(16);
227
+ if (hex) {
228
+ codePoints.push(hex);
229
+ }
230
+ }
231
+ return codePoints.join("-");
232
+ }
233
+ function getAppleEmojiUrl(emoji) {
234
+ const unified = emojiToUnified(emoji);
235
+ return `${APPLE_EMOJI_CDN_BASE}${unified}.png`;
236
+ }
237
+ var EMOJI_REGEX = /(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\p{Emoji_Modifier}|\uFE0F|\u200D(?:\p{Emoji_Presentation}|\p{Emoji}\uFE0F)(?:\p{Emoji_Modifier})?)*|\p{Regional_Indicator}{2}/gu;
238
+ function hasEmoji(text) {
239
+ return EMOJI_REGEX.test(text);
240
+ }
241
+ function splitTextAndEmojis(text) {
242
+ const segments = [];
243
+ let lastIndex = 0;
244
+ EMOJI_REGEX.lastIndex = 0;
245
+ let match;
246
+ while ((match = EMOJI_REGEX.exec(text)) !== null) {
247
+ if (match.index > lastIndex) {
248
+ segments.push({
249
+ type: "text",
250
+ content: text.slice(lastIndex, match.index)
251
+ });
252
+ }
253
+ segments.push({
254
+ type: "emoji",
255
+ content: match[0],
256
+ imageUrl: getAppleEmojiUrl(match[0])
257
+ });
258
+ lastIndex = match.index + match[0].length;
259
+ }
260
+ if (lastIndex < text.length) {
261
+ segments.push({
262
+ type: "text",
263
+ content: text.slice(lastIndex)
264
+ });
265
+ }
266
+ return segments;
267
+ }
268
+
221
269
  // src/components/TextElement.tsx
222
270
  import { jsx, jsxs } from "react/jsx-runtime";
223
271
  function calculateAutoWidthAndLines({
@@ -352,14 +400,22 @@ function TextElement({ segment, scale = 1 }) {
352
400
  const backgroundOpacity = segment.backgroundOpacity ?? TEXT_DEFAULTS.backgroundOpacity;
353
401
  const backgroundBorderRadius = segment.backgroundBorderRadius;
354
402
  const fontFamily = getFontFamily(fontType);
355
- const { autoWidthResult, isMultiLine } = useMemo(() => {
403
+ const { calculatedWidth, calculatedLines, paddingTop, paddingRight, paddingBottom, paddingLeft } = useMemo(() => {
404
+ let finalPaddingTop = basePaddingTop;
405
+ let finalPaddingRight = basePaddingRight;
406
+ let finalPaddingBottom = basePaddingBottom;
407
+ let finalPaddingLeft = basePaddingLeft;
356
408
  if (!autoWidth && !hasExtraPadding) {
357
409
  return {
358
- autoWidthResult: null,
359
- isMultiLine: false
410
+ calculatedWidth: width,
411
+ calculatedLines: [segment.text],
412
+ paddingTop: finalPaddingTop,
413
+ paddingRight: finalPaddingRight,
414
+ paddingBottom: finalPaddingBottom,
415
+ paddingLeft: finalPaddingLeft
360
416
  };
361
417
  }
362
- const result = calculateAutoWidthAndLines({
418
+ const baseResult = calculateAutoWidthAndLines({
363
419
  text: segment.text,
364
420
  maxWidth: width,
365
421
  paddingLeft: basePaddingLeft,
@@ -370,16 +426,47 @@ function TextElement({ segment, scale = 1 }) {
370
426
  letterSpacing,
371
427
  lineHeight
372
428
  });
429
+ const isMultiLine = baseResult.lines.length >= 2;
430
+ if (isMultiLine) {
431
+ finalPaddingTop = basePaddingTop + extraPaddingTop;
432
+ finalPaddingRight = basePaddingRight + extraPaddingRight;
433
+ finalPaddingBottom = basePaddingBottom + extraPaddingBottom;
434
+ finalPaddingLeft = basePaddingLeft + extraPaddingLeft;
435
+ }
436
+ let finalResult = baseResult;
437
+ if (isMultiLine && (extraPaddingLeft > 0 || extraPaddingRight > 0)) {
438
+ finalResult = calculateAutoWidthAndLines({
439
+ text: segment.text,
440
+ maxWidth: width,
441
+ paddingLeft: finalPaddingLeft,
442
+ paddingRight: finalPaddingRight,
443
+ fontSize,
444
+ fontWeight,
445
+ fontFamily,
446
+ letterSpacing,
447
+ lineHeight
448
+ });
449
+ }
373
450
  return {
374
- autoWidthResult: autoWidth ? result : null,
375
- isMultiLine: result.lines.length >= 2
451
+ calculatedWidth: autoWidth ? finalResult.width : width,
452
+ calculatedLines: autoWidth ? finalResult.lines : [segment.text],
453
+ paddingTop: finalPaddingTop,
454
+ paddingRight: finalPaddingRight,
455
+ paddingBottom: finalPaddingBottom,
456
+ paddingLeft: finalPaddingLeft
376
457
  };
377
458
  }, [
378
459
  autoWidth,
379
460
  segment.text,
380
461
  width,
381
- basePaddingLeft,
462
+ basePaddingTop,
382
463
  basePaddingRight,
464
+ basePaddingBottom,
465
+ basePaddingLeft,
466
+ extraPaddingTop,
467
+ extraPaddingRight,
468
+ extraPaddingBottom,
469
+ extraPaddingLeft,
383
470
  hasExtraPadding,
384
471
  fontSize,
385
472
  fontWeight,
@@ -387,51 +474,6 @@ function TextElement({ segment, scale = 1 }) {
387
474
  letterSpacing,
388
475
  lineHeight
389
476
  ]);
390
- const calculatedLines = autoWidthResult?.lines ?? [segment.text];
391
- const paddingTop = basePaddingTop + (isMultiLine ? extraPaddingTop : 0);
392
- const paddingRight = basePaddingRight + (isMultiLine ? extraPaddingRight : 0);
393
- const paddingBottom = basePaddingBottom + (isMultiLine ? extraPaddingBottom : 0);
394
- const paddingLeft = basePaddingLeft + (isMultiLine ? extraPaddingLeft : 0);
395
- const calculatedWidth = useMemo(() => {
396
- if (!autoWidth) return width;
397
- if (!isMultiLine || extraPaddingLeft === 0 && extraPaddingRight === 0) {
398
- return autoWidthResult?.width ?? width;
399
- }
400
- if (typeof document === "undefined") return width;
401
- const measureSpan = document.createElement("span");
402
- measureSpan.style.cssText = `
403
- position: absolute;
404
- visibility: hidden;
405
- pointer-events: none;
406
- font-family: ${fontFamily};
407
- font-size: ${fontSize}px;
408
- font-weight: ${fontWeight};
409
- letter-spacing: ${letterSpacing}px;
410
- white-space: nowrap;
411
- `;
412
- document.body.appendChild(measureSpan);
413
- let widestLineWidth = 0;
414
- for (const line of calculatedLines) {
415
- measureSpan.textContent = line;
416
- widestLineWidth = Math.max(widestLineWidth, measureSpan.getBoundingClientRect().width);
417
- }
418
- document.body.removeChild(measureSpan);
419
- return Math.min(widestLineWidth + paddingLeft + paddingRight, width);
420
- }, [
421
- autoWidth,
422
- autoWidthResult,
423
- width,
424
- isMultiLine,
425
- extraPaddingLeft,
426
- extraPaddingRight,
427
- paddingLeft,
428
- paddingRight,
429
- calculatedLines,
430
- fontFamily,
431
- fontSize,
432
- fontWeight,
433
- letterSpacing
434
- ]);
435
477
  const borderRadiusStyle = useMemo(() => {
436
478
  if (!backgroundBorderRadius) return void 0;
437
479
  const radii = getBorderRadii(backgroundBorderRadius);
@@ -526,9 +568,35 @@ function TextElement({ segment, scale = 1 }) {
526
568
  autoWidth,
527
569
  verticalAlign
528
570
  ]);
571
+ const renderTextWithEmojis = (text) => {
572
+ if (!hasEmoji(text)) {
573
+ return text;
574
+ }
575
+ const segments = splitTextAndEmojis(text);
576
+ return segments.map((segment2, i) => {
577
+ if (segment2.type === "emoji" && segment2.imageUrl) {
578
+ return /* @__PURE__ */ jsx(
579
+ "img",
580
+ {
581
+ src: segment2.imageUrl,
582
+ alt: segment2.content,
583
+ style: {
584
+ height: "1em",
585
+ width: "1em",
586
+ verticalAlign: "-0.1em",
587
+ display: "inline"
588
+ },
589
+ draggable: false
590
+ },
591
+ i
592
+ );
593
+ }
594
+ return /* @__PURE__ */ jsx(React.Fragment, { children: segment2.content }, i);
595
+ });
596
+ };
529
597
  if (autoWidth) {
530
598
  const textContent = calculatedLines.map((line, index) => /* @__PURE__ */ jsxs(React.Fragment, { children: [
531
- line,
599
+ renderTextWithEmojis(line),
532
600
  index < calculatedLines.length - 1 && /* @__PURE__ */ jsx("br", {})
533
601
  ] }, index));
534
602
  if (backgroundColor) {
@@ -547,7 +615,7 @@ function TextElement({ segment, scale = 1 }) {
547
615
  }
548
616
  return /* @__PURE__ */ jsx("div", { style: positioningContainerStyle, children: /* @__PURE__ */ jsx("div", { style: { width: calculatedWidth, maxWidth: width }, children: /* @__PURE__ */ jsx("div", { style: textStyle, children: textContent }) }) });
549
617
  }
550
- return /* @__PURE__ */ jsx("div", { style: positioningContainerStyle, children: /* @__PURE__ */ jsx("div", { style: backgroundBoxStyle, children: /* @__PURE__ */ jsx("div", { style: textStyle, children: segment.text }) }) });
618
+ return /* @__PURE__ */ jsx("div", { style: positioningContainerStyle, children: /* @__PURE__ */ jsx("div", { style: backgroundBoxStyle, children: /* @__PURE__ */ jsx("div", { style: textStyle, children: renderTextWithEmojis(segment.text) }) }) });
551
619
  }
552
620
 
553
621
  // src/components/ImageElement.tsx
@@ -1866,9 +1934,11 @@ export {
1866
1934
  calculateTimelineContentEnd,
1867
1935
  canSetAsReference,
1868
1936
  defaultOffset,
1937
+ emojiToUnified,
1869
1938
  formatTime,
1870
1939
  generateOverlayId,
1871
1940
  generateSegmentId,
1941
+ getAppleEmojiUrl,
1872
1942
  getBaseSegments,
1873
1943
  getBorderRadii,
1874
1944
  getDependentElements,
@@ -1877,6 +1947,7 @@ export {
1877
1947
  getReferenceElementX,
1878
1948
  getReferenceElementY,
1879
1949
  getSegmentTimelinePosition,
1950
+ hasEmoji,
1880
1951
  hexToRgba,
1881
1952
  isDynamicCropEnabled,
1882
1953
  isSegmentVisibleAtTime,
@@ -1884,6 +1955,7 @@ export {
1884
1955
  parseTime,
1885
1956
  preloadFonts,
1886
1957
  resolveElementPositions,
1958
+ splitTextAndEmojis,
1887
1959
  useFontsLoaded,
1888
1960
  useImageLoader,
1889
1961
  useImagePreloader,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ugcinc-render",
3
- "version": "1.5.10",
3
+ "version": "1.5.12",
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",