@meframe/core 0.0.29 → 0.0.30-beta

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/dist/Meframe.d.ts +2 -13
  2. package/dist/Meframe.d.ts.map +1 -1
  3. package/dist/Meframe.js +6 -100
  4. package/dist/Meframe.js.map +1 -1
  5. package/dist/cache/CacheManager.d.ts +35 -19
  6. package/dist/cache/CacheManager.d.ts.map +1 -1
  7. package/dist/cache/CacheManager.js +223 -134
  8. package/dist/cache/CacheManager.js.map +1 -1
  9. package/dist/cache/l1/VideoL1Cache.d.ts +15 -2
  10. package/dist/cache/l1/VideoL1Cache.d.ts.map +1 -1
  11. package/dist/cache/l1/VideoL1Cache.js +58 -38
  12. package/dist/cache/l1/VideoL1Cache.js.map +1 -1
  13. package/dist/cache/l2/L2Cache.d.ts.map +1 -1
  14. package/dist/cache/l2/L2Cache.js +5 -5
  15. package/dist/cache/l2/L2Cache.js.map +1 -1
  16. package/dist/cache/l2/L2OPFSStore.d.ts +37 -0
  17. package/dist/cache/l2/L2OPFSStore.d.ts.map +1 -0
  18. package/dist/cache/l2/L2OPFSStore.js +89 -0
  19. package/dist/cache/l2/L2OPFSStore.js.map +1 -0
  20. package/dist/cache/resource/AudioSampleCache.d.ts +52 -0
  21. package/dist/cache/resource/AudioSampleCache.d.ts.map +1 -0
  22. package/dist/cache/resource/AudioSampleCache.js +69 -0
  23. package/dist/cache/resource/AudioSampleCache.js.map +1 -0
  24. package/dist/cache/resource/ImageBitmapCache.d.ts +65 -0
  25. package/dist/cache/resource/ImageBitmapCache.d.ts.map +1 -0
  26. package/dist/cache/resource/ImageBitmapCache.js +101 -0
  27. package/dist/cache/resource/ImageBitmapCache.js.map +1 -0
  28. package/dist/cache/resource/MP4IndexCache.d.ts +48 -0
  29. package/dist/cache/resource/MP4IndexCache.d.ts.map +1 -0
  30. package/dist/cache/resource/MP4IndexCache.js +104 -0
  31. package/dist/cache/resource/MP4IndexCache.js.map +1 -0
  32. package/dist/cache/resource/ResourceCache.d.ts +46 -0
  33. package/dist/cache/resource/ResourceCache.d.ts.map +1 -0
  34. package/dist/cache/resource/ResourceCache.js +92 -0
  35. package/dist/cache/resource/ResourceCache.js.map +1 -0
  36. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts +75 -0
  37. package/dist/cache/storage/indexeddb/ChunkRecordStore.d.ts.map +1 -0
  38. package/dist/cache/{l2/IndexedDBStore.js → storage/indexeddb/ChunkRecordStore.js} +3 -3
  39. package/dist/cache/storage/indexeddb/ChunkRecordStore.js.map +1 -0
  40. package/dist/cache/storage/opfs/OPFSManager.d.ts +54 -0
  41. package/dist/cache/storage/opfs/OPFSManager.d.ts.map +1 -0
  42. package/dist/cache/storage/opfs/OPFSManager.js +133 -0
  43. package/dist/cache/storage/opfs/OPFSManager.js.map +1 -0
  44. package/dist/cache/storage/opfs/types.d.ts +16 -0
  45. package/dist/cache/storage/opfs/types.d.ts.map +1 -0
  46. package/dist/config/defaults.d.ts.map +1 -1
  47. package/dist/config/defaults.js +21 -2
  48. package/dist/config/defaults.js.map +1 -1
  49. package/dist/config/types.d.ts +28 -0
  50. package/dist/config/types.d.ts.map +1 -1
  51. package/dist/controllers/ExportController.d.ts +16 -0
  52. package/dist/controllers/ExportController.d.ts.map +1 -0
  53. package/dist/controllers/ExportController.js +44 -0
  54. package/dist/controllers/ExportController.js.map +1 -0
  55. package/dist/controllers/PlaybackController.d.ts +28 -4
  56. package/dist/controllers/PlaybackController.d.ts.map +1 -1
  57. package/dist/controllers/PlaybackController.js +116 -51
  58. package/dist/controllers/PlaybackController.js.map +1 -1
  59. package/dist/controllers/index.d.ts +2 -3
  60. package/dist/controllers/index.d.ts.map +1 -1
  61. package/dist/controllers/types.d.ts +0 -28
  62. package/dist/controllers/types.d.ts.map +1 -1
  63. package/dist/event/events.d.ts +8 -0
  64. package/dist/event/events.d.ts.map +1 -1
  65. package/dist/event/events.js +1 -0
  66. package/dist/event/events.js.map +1 -1
  67. package/dist/model/CompositionModel.d.ts.map +1 -1
  68. package/dist/model/CompositionModel.js +11 -6
  69. package/dist/model/CompositionModel.js.map +1 -1
  70. package/dist/model/RcFrame.d.ts +2 -0
  71. package/dist/model/RcFrame.d.ts.map +1 -1
  72. package/dist/model/RcFrame.js +3 -0
  73. package/dist/model/RcFrame.js.map +1 -1
  74. package/dist/orchestrator/ExportScheduler.d.ts +35 -0
  75. package/dist/orchestrator/ExportScheduler.d.ts.map +1 -0
  76. package/dist/orchestrator/ExportScheduler.js +241 -0
  77. package/dist/orchestrator/ExportScheduler.js.map +1 -0
  78. package/dist/orchestrator/GlobalAudioSession.d.ts +21 -7
  79. package/dist/orchestrator/GlobalAudioSession.d.ts.map +1 -1
  80. package/dist/orchestrator/GlobalAudioSession.js +132 -140
  81. package/dist/orchestrator/GlobalAudioSession.js.map +1 -1
  82. package/dist/orchestrator/OnDemandVideoSession.d.ts +73 -0
  83. package/dist/orchestrator/OnDemandVideoSession.d.ts.map +1 -0
  84. package/dist/orchestrator/OnDemandVideoSession.js +281 -0
  85. package/dist/orchestrator/OnDemandVideoSession.js.map +1 -0
  86. package/dist/orchestrator/Orchestrator.d.ts +22 -17
  87. package/dist/orchestrator/Orchestrator.d.ts.map +1 -1
  88. package/dist/orchestrator/Orchestrator.js +231 -297
  89. package/dist/orchestrator/Orchestrator.js.map +1 -1
  90. package/dist/orchestrator/VideoClipSession.d.ts.map +1 -1
  91. package/dist/orchestrator/VideoClipSession.js +3 -15
  92. package/dist/orchestrator/VideoClipSession.js.map +1 -1
  93. package/dist/orchestrator/index.d.ts +0 -1
  94. package/dist/orchestrator/index.d.ts.map +1 -1
  95. package/dist/orchestrator/types.d.ts +4 -4
  96. package/dist/orchestrator/types.d.ts.map +1 -1
  97. package/dist/stages/compose/FilterProcessor.d.ts +1 -1
  98. package/dist/stages/compose/FilterProcessor.d.ts.map +1 -1
  99. package/dist/stages/compose/FilterProcessor.js +226 -0
  100. package/dist/stages/compose/FilterProcessor.js.map +1 -0
  101. package/dist/stages/compose/LayerRenderer.d.ts +1 -1
  102. package/dist/stages/compose/LayerRenderer.d.ts.map +1 -1
  103. package/dist/stages/compose/LayerRenderer.js +270 -0
  104. package/dist/stages/compose/LayerRenderer.js.map +1 -0
  105. package/dist/stages/compose/TransitionProcessor.d.ts +1 -1
  106. package/dist/stages/compose/TransitionProcessor.d.ts.map +1 -1
  107. package/dist/stages/compose/TransitionProcessor.js +189 -0
  108. package/dist/stages/compose/TransitionProcessor.js.map +1 -0
  109. package/dist/stages/compose/VideoComposer.d.ts +4 -2
  110. package/dist/stages/compose/VideoComposer.d.ts.map +1 -1
  111. package/dist/stages/compose/VideoComposer.js +229 -0
  112. package/dist/stages/compose/VideoComposer.js.map +1 -0
  113. package/dist/stages/compose/text-renderers/animation-utils.js +76 -0
  114. package/dist/stages/compose/text-renderers/animation-utils.js.map +1 -0
  115. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts +2 -2
  116. package/dist/stages/compose/text-renderers/basic-text-renderer.d.ts.map +1 -1
  117. package/dist/stages/compose/text-renderers/basic-text-renderer.js +93 -0
  118. package/dist/stages/compose/text-renderers/basic-text-renderer.js.map +1 -0
  119. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts +1 -1
  120. package/dist/stages/compose/text-renderers/character-ktv-renderer.d.ts.map +1 -1
  121. package/dist/stages/compose/text-renderers/character-ktv-renderer.js +132 -0
  122. package/dist/stages/compose/text-renderers/character-ktv-renderer.js.map +1 -0
  123. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts +1 -1
  124. package/dist/stages/compose/text-renderers/word-by-word-renderer.d.ts.map +1 -1
  125. package/dist/stages/compose/text-renderers/word-by-word-renderer.js +128 -0
  126. package/dist/stages/compose/text-renderers/word-by-word-renderer.js.map +1 -0
  127. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts +1 -1
  128. package/dist/stages/compose/text-renderers/word-fancy-renderer.d.ts.map +1 -1
  129. package/dist/stages/compose/text-renderers/word-fancy-renderer.js +135 -0
  130. package/dist/stages/compose/text-renderers/word-fancy-renderer.js.map +1 -0
  131. package/dist/stages/compose/text-utils/locale-detector.js +16 -0
  132. package/dist/stages/compose/text-utils/locale-detector.js.map +1 -0
  133. package/dist/stages/compose/text-utils/text-metrics.js +21 -0
  134. package/dist/stages/compose/text-utils/text-metrics.js.map +1 -0
  135. package/dist/stages/compose/text-utils/text-wrapper.js +225 -0
  136. package/dist/stages/compose/text-utils/text-wrapper.js.map +1 -0
  137. package/dist/stages/compose/types.d.ts +2 -1
  138. package/dist/stages/compose/types.d.ts.map +1 -1
  139. package/dist/stages/decode/BaseDecoder.js +0 -3
  140. package/dist/stages/decode/BaseDecoder.js.map +1 -1
  141. package/dist/stages/demux/MP4Demuxer.d.ts +5 -0
  142. package/dist/stages/demux/MP4Demuxer.d.ts.map +1 -1
  143. package/dist/stages/demux/MP4Demuxer.js +281 -0
  144. package/dist/stages/demux/MP4Demuxer.js.map +1 -0
  145. package/dist/stages/demux/MP4IndexParser.d.ts +71 -0
  146. package/dist/stages/demux/MP4IndexParser.d.ts.map +1 -0
  147. package/dist/stages/demux/MP4IndexParser.js +416 -0
  148. package/dist/stages/demux/MP4IndexParser.js.map +1 -0
  149. package/dist/stages/demux/types.d.ts +48 -0
  150. package/dist/stages/demux/types.d.ts.map +1 -1
  151. package/dist/stages/load/ResourceLoader.d.ts +44 -2
  152. package/dist/stages/load/ResourceLoader.d.ts.map +1 -1
  153. package/dist/stages/load/ResourceLoader.js +281 -37
  154. package/dist/stages/load/ResourceLoader.js.map +1 -1
  155. package/dist/stages/load/TaskManager.d.ts +6 -2
  156. package/dist/stages/load/TaskManager.d.ts.map +1 -1
  157. package/dist/stages/load/TaskManager.js +27 -4
  158. package/dist/stages/load/TaskManager.js.map +1 -1
  159. package/dist/stages/load/types.d.ts +7 -0
  160. package/dist/stages/load/types.d.ts.map +1 -1
  161. package/dist/stages/mux/MP4Muxer.d.ts +2 -2
  162. package/dist/stages/mux/MP4Muxer.d.ts.map +1 -1
  163. package/dist/stages/mux/MP4Muxer.js +24 -13
  164. package/dist/stages/mux/MP4Muxer.js.map +1 -1
  165. package/dist/stages/mux/MuxManager.d.ts +10 -21
  166. package/dist/stages/mux/MuxManager.d.ts.map +1 -1
  167. package/dist/stages/mux/MuxManager.js +21 -162
  168. package/dist/stages/mux/MuxManager.js.map +1 -1
  169. package/dist/stages/mux/index.d.ts +0 -1
  170. package/dist/stages/mux/index.d.ts.map +1 -1
  171. package/dist/utils/binary-search.d.ts +12 -4
  172. package/dist/utils/binary-search.d.ts.map +1 -1
  173. package/dist/utils/binary-search.js +52 -6
  174. package/dist/utils/binary-search.js.map +1 -1
  175. package/dist/workers/{BaseDecoder.BWYu1W0B.js → BaseDecoder.CTW-vr29.js} +1 -4
  176. package/dist/workers/BaseDecoder.CTW-vr29.js.map +1 -0
  177. package/dist/workers/{MP4Demuxer.lMOUMWFh.js → MP4Demuxer.BEa6PLJm.js} +9 -2
  178. package/dist/workers/{MP4Demuxer.lMOUMWFh.js.map → MP4Demuxer.BEa6PLJm.js.map} +1 -1
  179. package/dist/workers/stages/compose/{video-compose.worker.CIeEIJO7.js → video-compose.worker.DHQ8B105.js} +59 -31
  180. package/dist/workers/stages/compose/video-compose.worker.DHQ8B105.js.map +1 -0
  181. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js → audio-decode.worker.CP8bXXa4.js} +2 -2
  182. package/dist/workers/stages/decode/{audio-decode.worker.DnS17GD9.js.map → audio-decode.worker.CP8bXXa4.js.map} +1 -1
  183. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js → video-decode.worker.BIspTxgV.js} +2 -2
  184. package/dist/workers/stages/decode/{video-decode.worker.BEYsjOXp.js.map → video-decode.worker.BIspTxgV.js.map} +1 -1
  185. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js → audio-demux.worker._VRQdLdv.js} +2 -2
  186. package/dist/workers/stages/demux/{audio-demux.worker.DcurGC8i.js.map → audio-demux.worker._VRQdLdv.js.map} +1 -1
  187. package/dist/workers/stages/demux/{video-demux.worker.B1_wntU4.js → video-demux.worker.CSkxGtmx.js} +3 -19
  188. package/dist/workers/stages/demux/video-demux.worker.CSkxGtmx.js.map +1 -0
  189. package/dist/workers/worker-manifest.json +5 -5
  190. package/package.json +1 -1
  191. package/dist/cache/l2/IndexedDBStore.js.map +0 -1
  192. package/dist/cache/l2/OPFSStore.js +0 -131
  193. package/dist/cache/l2/OPFSStore.js.map +0 -1
  194. package/dist/controllers/PreRenderService.d.ts +0 -59
  195. package/dist/controllers/PreRenderService.d.ts.map +0 -1
  196. package/dist/controllers/PreRenderService.js +0 -185
  197. package/dist/controllers/PreRenderService.js.map +0 -1
  198. package/dist/controllers/PreRenderTaskQueue.d.ts +0 -21
  199. package/dist/controllers/PreRenderTaskQueue.d.ts.map +0 -1
  200. package/dist/orchestrator/ClipSessionManager.d.ts +0 -70
  201. package/dist/orchestrator/ClipSessionManager.d.ts.map +0 -1
  202. package/dist/orchestrator/ClipSessionManager.js +0 -158
  203. package/dist/orchestrator/ClipSessionManager.js.map +0 -1
  204. package/dist/stages/decode/AudioChunkDecoder.js +0 -169
  205. package/dist/stages/decode/AudioChunkDecoder.js.map +0 -1
  206. package/dist/stages/mux/OPFSWriter.d.ts +0 -46
  207. package/dist/stages/mux/OPFSWriter.d.ts.map +0 -1
  208. package/dist/utils/BackpressureAdapter.d.ts +0 -26
  209. package/dist/utils/BackpressureAdapter.d.ts.map +0 -1
  210. package/dist/workers/BaseDecoder.BWYu1W0B.js.map +0 -1
  211. package/dist/workers/stages/compose/video-compose.worker.CIeEIJO7.js.map +0 -1
  212. package/dist/workers/stages/demux/video-demux.worker.B1_wntU4.js.map +0 -1
@@ -0,0 +1,93 @@
1
+ import { formEvenLinesWithWords, wrapText } from "../text-utils/text-wrapper.js";
2
+ import { getLetterCaseText } from "../text-utils/text-metrics.js";
3
+ import { interpolate, springEasing } from "./animation-utils.js";
4
+ import { needsSpaceBetweenWords } from "../text-utils/locale-detector.js";
5
+ function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
6
+ if (!globalPosition) {
7
+ return canvasHeight / 2 - totalHeight / 2;
8
+ }
9
+ if (globalPosition.top) {
10
+ const topPercent = parseFloat(globalPosition.top) / 100;
11
+ return canvasHeight * topPercent;
12
+ }
13
+ if (globalPosition.bottom) {
14
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
15
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
16
+ }
17
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
18
+ return canvasHeight / 2 - totalHeight / 2;
19
+ }
20
+ return canvasHeight / 2 - totalHeight / 2;
21
+ }
22
+ function renderBasicText(ctx, layer, canvasWidth, canvasHeight, _relativeFrame) {
23
+ const fontConfig = layer.fontConfig?.textStyle;
24
+ if (!fontConfig) return;
25
+ const fontSize = fontConfig.fontSize;
26
+ const fontFamily = fontConfig.fontFamily;
27
+ const fontWeight = fontConfig.fontWeight;
28
+ const fill = fontConfig.fill;
29
+ const stroke = fontConfig.stroke;
30
+ const strokeWidth = fontConfig.strokeWidth || 0;
31
+ const lineHeight = fontConfig.lineHeight || 1.2;
32
+ const maxWidth = canvasWidth * 0.64;
33
+ const text = getLetterCaseText(layer.text, layer.letterCase);
34
+ let lines;
35
+ if (layer.wordTimings && layer.wordTimings.length > 0) {
36
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
37
+ const words = text.split(needsSpace ? /\s+/ : "");
38
+ lines = formEvenLinesWithWords(
39
+ ctx,
40
+ words,
41
+ maxWidth,
42
+ fontSize,
43
+ needsSpace,
44
+ fontFamily,
45
+ fontWeight
46
+ );
47
+ } else {
48
+ lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);
49
+ }
50
+ ctx.save();
51
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
52
+ ctx.textAlign = "center";
53
+ ctx.textBaseline = "middle";
54
+ ctx.lineJoin = "round";
55
+ ctx.lineCap = "round";
56
+ const totalHeight = lines.length * fontSize * lineHeight;
57
+ const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
58
+ for (let i = 0; i < lines.length; i++) {
59
+ const line = lines[i];
60
+ const y = startY + i * fontSize * lineHeight + fontSize / 2;
61
+ if (stroke && strokeWidth > 0) {
62
+ ctx.strokeStyle = stroke;
63
+ ctx.lineWidth = strokeWidth;
64
+ ctx.strokeText(line, canvasWidth / 2, y);
65
+ }
66
+ ctx.fillStyle = fill;
67
+ ctx.fillText(line, canvasWidth / 2, y);
68
+ }
69
+ ctx.restore();
70
+ }
71
+ function renderTextWithEntrance(ctx, layer, canvasWidth, canvasHeight, relativeFrame) {
72
+ const fontConfig = layer.fontConfig?.textStyle;
73
+ if (!fontConfig) return;
74
+ const entrance = springEasing(relativeFrame, {
75
+ damping: 200,
76
+ mass: 1,
77
+ stiffness: 300
78
+ });
79
+ const opacity = interpolate(entrance, [0, 1], [0, 1]);
80
+ const scale = interpolate(entrance, [0, 1], [0.9, 1]);
81
+ ctx.save();
82
+ ctx.globalAlpha = opacity;
83
+ ctx.translate(canvasWidth / 2, canvasHeight / 2);
84
+ ctx.scale(scale, scale);
85
+ ctx.translate(-canvasWidth / 2, -canvasHeight / 2);
86
+ renderBasicText(ctx, layer, canvasWidth, canvasHeight);
87
+ ctx.restore();
88
+ }
89
+ export {
90
+ renderBasicText,
91
+ renderTextWithEntrance
92
+ };
93
+ //# sourceMappingURL=basic-text-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"basic-text-renderer.js","sources":["../../../../src/stages/compose/text-renderers/basic-text-renderer.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { wrapText, formEvenLinesWithWords } from '../text-utils/text-wrapper';\nimport { getLetterCaseText } from '../text-utils/text-metrics';\nimport { springEasing, interpolate } from './animation-utils';\nimport { needsSpaceBetweenWords } from '../text-utils/locale-detector';\n\nfunction calculateYPosition(\n canvasHeight: number,\n totalHeight: number,\n globalPosition?: {\n position?: 'absolute';\n top?: string;\n bottom?: string;\n left?: string;\n right?: string;\n display?: string;\n alignItems?: string;\n justifyContent?: string;\n }\n): number {\n if (!globalPosition) {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n // Handle percentage-based positioning\n if (globalPosition.top) {\n const topPercent = parseFloat(globalPosition.top) / 100;\n return canvasHeight * topPercent;\n }\n\n if (globalPosition.bottom) {\n const bottomPercent = parseFloat(globalPosition.bottom) / 100;\n return canvasHeight * (1 - bottomPercent) - totalHeight;\n }\n\n // Handle center alignment\n if (globalPosition.justifyContent === 'center' || globalPosition.alignItems === 'center') {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n // Default to center\n return canvasHeight / 2 - totalHeight / 2;\n}\n\nexport function renderBasicText(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n _relativeFrame: number\n): void {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return;\n\n const fontSize = fontConfig.fontSize;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const fill = fontConfig.fill;\n const stroke = fontConfig.stroke;\n const strokeWidth = fontConfig.strokeWidth || 0;\n const lineHeight = fontConfig.lineHeight || 1.2;\n\n const maxWidth = canvasWidth * 0.64;\n const text = getLetterCaseText(layer.text, layer.letterCase);\n\n let lines: string[];\n if (layer.wordTimings && layer.wordTimings.length > 0) {\n const needsSpace = needsSpaceBetweenWords(layer.localeCode || 'en-US', text);\n const words = text.split(needsSpace ? /\\s+/ : '');\n lines = formEvenLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n } else {\n lines = wrapText(ctx, text, maxWidth, fontSize, fontFamily, fontWeight);\n }\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n const totalHeight = lines.length * fontSize * lineHeight;\n const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);\n\n for (let i = 0; i < lines.length; i++) {\n const line = lines[i]!;\n const y = startY + i * fontSize * lineHeight + fontSize / 2;\n\n if (stroke && strokeWidth > 0) {\n ctx.strokeStyle = stroke;\n ctx.lineWidth = strokeWidth;\n ctx.strokeText(line, canvasWidth / 2, y);\n }\n ctx.fillStyle = fill;\n ctx.fillText(line, canvasWidth / 2, y);\n }\n\n ctx.restore();\n}\n\nexport function renderTextWithEntrance(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n relativeFrame: number\n): void {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return;\n\n const entrance = springEasing(relativeFrame, {\n damping: 200,\n mass: 1,\n stiffness: 300,\n });\n\n const opacity = interpolate(entrance, [0, 1], [0, 1]);\n const scale = interpolate(entrance, [0, 1], [0.9, 1]);\n\n ctx.save();\n ctx.globalAlpha = opacity;\n ctx.translate(canvasWidth / 2, canvasHeight / 2);\n ctx.scale(scale, scale);\n ctx.translate(-canvasWidth / 2, -canvasHeight / 2);\n\n renderBasicText(ctx, layer, canvasWidth, canvasHeight, relativeFrame);\n\n ctx.restore();\n}\n"],"names":[],"mappings":";;;;AAMA,SAAS,mBACP,cACA,aACA,gBAUQ;AACR,MAAI,CAAC,gBAAgB;AACnB,WAAO,eAAe,IAAI,cAAc;AAAA,EAC1C;AAGA,MAAI,eAAe,KAAK;AACtB,UAAM,aAAa,WAAW,eAAe,GAAG,IAAI;AACpD,WAAO,eAAe;AAAA,EACxB;AAEA,MAAI,eAAe,QAAQ;AACzB,UAAM,gBAAgB,WAAW,eAAe,MAAM,IAAI;AAC1D,WAAO,gBAAgB,IAAI,iBAAiB;AAAA,EAC9C;AAGA,MAAI,eAAe,mBAAmB,YAAY,eAAe,eAAe,UAAU;AACxF,WAAO,eAAe,IAAI,cAAc;AAAA,EAC1C;AAGA,SAAO,eAAe,IAAI,cAAc;AAC1C;AAEO,SAAS,gBACd,KACA,OACA,aACA,cACA,gBACM;AACN,QAAM,aAAa,MAAM,YAAY;AACrC,MAAI,CAAC,WAAY;AAEjB,QAAM,WAAW,WAAW;AAC5B,QAAM,aAAa,WAAW;AAC9B,QAAM,aAAa,WAAW;AAC9B,QAAM,OAAO,WAAW;AACxB,QAAM,SAAS,WAAW;AAC1B,QAAM,cAAc,WAAW,eAAe;AAC9C,QAAM,aAAa,WAAW,cAAc;AAE5C,QAAM,WAAW,cAAc;AAC/B,QAAM,OAAO,kBAAkB,MAAM,MAAM,MAAM,UAAU;AAE3D,MAAI;AACJ,MAAI,MAAM,eAAe,MAAM,YAAY,SAAS,GAAG;AACrD,UAAM,aAAa,uBAAuB,MAAM,cAAc,SAAS,IAAI;AAC3E,UAAM,QAAQ,KAAK,MAAM,aAAa,QAAQ,EAAE;AAChD,YAAQ;AAAA,MACN;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAAA,EAEJ,OAAO;AACL,YAAQ,SAAS,KAAK,MAAM,UAAU,UAAU,YAAY,UAAU;AAAA,EACxE;AAEA,MAAI,KAAA;AACJ,MAAI,OAAO,GAAG,UAAU,IAAI,QAAQ,MAAM,UAAU;AACpD,MAAI,YAAY;AAChB,MAAI,eAAe;AACnB,MAAI,WAAW;AACf,MAAI,UAAU;AAEd,QAAM,cAAc,MAAM,SAAS,WAAW;AAC9C,QAAM,SAAS,mBAAmB,cAAc,aAAa,MAAM,YAAY,cAAc;AAE7F,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,UAAM,OAAO,MAAM,CAAC;AACpB,UAAM,IAAI,SAAS,IAAI,WAAW,aAAa,WAAW;AAE1D,QAAI,UAAU,cAAc,GAAG;AAC7B,UAAI,cAAc;AAClB,UAAI,YAAY;AAChB,UAAI,WAAW,MAAM,cAAc,GAAG,CAAC;AAAA,IACzC;AACA,QAAI,YAAY;AAChB,QAAI,SAAS,MAAM,cAAc,GAAG,CAAC;AAAA,EACvC;AAEA,MAAI,QAAA;AACN;AAEO,SAAS,uBACd,KACA,OACA,aACA,cACA,eACM;AACN,QAAM,aAAa,MAAM,YAAY;AACrC,MAAI,CAAC,WAAY;AAEjB,QAAM,WAAW,aAAa,eAAe;AAAA,IAC3C,SAAS;AAAA,IACT,MAAM;AAAA,IACN,WAAW;AAAA,EAAA,CACZ;AAED,QAAM,UAAU,YAAY,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AACpD,QAAM,QAAQ,YAAY,UAAU,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;AAEpD,MAAI,KAAA;AACJ,MAAI,cAAc;AAClB,MAAI,UAAU,cAAc,GAAG,eAAe,CAAC;AAC/C,MAAI,MAAM,OAAO,KAAK;AACtB,MAAI,UAAU,CAAC,cAAc,GAAG,CAAC,eAAe,CAAC;AAEjD,kBAAgB,KAAK,OAAO,aAAa,YAA2B;AAEpE,MAAI,QAAA;AACN;"}
@@ -1,4 +1,4 @@
1
1
  import { TextLayer } from '../types';
2
2
 
3
- export declare function renderCharacterKTV(ctx: OffscreenCanvasRenderingContext2D, layer: TextLayer, canvasWidth: number, canvasHeight: number, relativeFrame: number, fps?: number): void;
3
+ export declare function renderCharacterKTV(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, layer: TextLayer, canvasWidth: number, canvasHeight: number, relativeFrame: number, fps?: number): void;
4
4
  //# sourceMappingURL=character-ktv-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"character-ktv-renderer.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-renderers/character-ktv-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAkD1C,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,iCAAiC,EACtC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,MAAW,GACf,IAAI,CA+HN"}
1
+ {"version":3,"file":"character-ktv-renderer.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-renderers/character-ktv-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAkD1C,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,MAAW,GACf,IAAI,CA+HN"}
@@ -0,0 +1,132 @@
1
+ import { wrapText } from "../text-utils/text-wrapper.js";
2
+ import { getLetterCaseText, measureTextWidth } from "../text-utils/text-metrics.js";
3
+ import { needsSpaceBetweenWords } from "../text-utils/locale-detector.js";
4
+ function usToFrame(us, fps) {
5
+ return Math.floor(us / (1e6 / fps));
6
+ }
7
+ function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
8
+ if (!globalPosition) {
9
+ return canvasHeight / 2 - totalHeight / 2;
10
+ }
11
+ if (globalPosition.top) {
12
+ const topPercent = parseFloat(globalPosition.top) / 100;
13
+ return canvasHeight * topPercent;
14
+ }
15
+ if (globalPosition.bottom) {
16
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
17
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
18
+ }
19
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
20
+ return canvasHeight / 2 - totalHeight / 2;
21
+ }
22
+ return canvasHeight / 2 - totalHeight / 2;
23
+ }
24
+ function renderCharacterKTV(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
25
+ const fontConfig = layer.fontConfig?.textStyle;
26
+ if (!fontConfig) return;
27
+ const fontSize = fontConfig.fontSize;
28
+ const fontFamily = fontConfig.fontFamily;
29
+ const fontWeight = fontConfig.fontWeight;
30
+ const baseFill = fontConfig.fill;
31
+ const stroke = fontConfig.stroke;
32
+ const strokeWidth = fontConfig.strokeWidth || 0;
33
+ const lineHeight = fontConfig.lineHeight || 1.2;
34
+ const highlightFill = layer.animation?.highlightTextStyle?.fill || "rgb(255, 215, 0)";
35
+ const glowColor = layer.animation?.glowColor || "#ffffff";
36
+ const glowIntensity = layer.animation?.glowIntensity || 3;
37
+ const transitionFrames = layer.animation?.transitionFrames || 10;
38
+ const maxWidth = canvasWidth * 0.9;
39
+ const text = getLetterCaseText(layer.text, layer.letterCase);
40
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
41
+ const characterTimings = [];
42
+ if (layer.wordTimings && layer.wordTimings.length > 0) {
43
+ let charIndex = 0;
44
+ for (let wordIndex = 0; wordIndex < layer.wordTimings.length; wordIndex++) {
45
+ const word = layer.wordTimings[wordIndex];
46
+ const wordChars = word.text.split("");
47
+ const wordStartFrame = usToFrame(word.startUs, fps);
48
+ const wordEndFrame = usToFrame(word.endUs, fps);
49
+ const framesPerChar = wordChars.length > 0 ? (wordEndFrame - wordStartFrame) / wordChars.length : 0;
50
+ for (let i = 0; i < wordChars.length; i++) {
51
+ const charStartFrame = Math.floor(wordStartFrame + i * framesPerChar);
52
+ characterTimings.push({
53
+ char: wordChars[i],
54
+ index: charIndex,
55
+ startFrame: charStartFrame
56
+ });
57
+ charIndex++;
58
+ }
59
+ if (needsSpace && wordIndex < layer.wordTimings.length - 1) {
60
+ const nextWord = layer.wordTimings[wordIndex + 1];
61
+ const nextWordFirstChar = nextWord?.text?.[0] || "";
62
+ const isPunctuation = /[.,!?;:)]/.test(nextWordFirstChar);
63
+ if (!isPunctuation) {
64
+ const spaceStartFrame = characterTimings[characterTimings.length - 1]?.startFrame || 0;
65
+ characterTimings.push({
66
+ char: " ",
67
+ index: charIndex,
68
+ startFrame: spaceStartFrame
69
+ });
70
+ charIndex++;
71
+ }
72
+ }
73
+ }
74
+ } else {
75
+ const totalFrames = 100;
76
+ const chars = text.split("");
77
+ const framesPerChar = chars.length > 0 ? totalFrames / chars.length : 0;
78
+ for (let i = 0; i < chars.length; i++) {
79
+ characterTimings.push({
80
+ char: chars[i],
81
+ index: i,
82
+ startFrame: Math.floor(i * framesPerChar)
83
+ });
84
+ }
85
+ }
86
+ const fullText = characterTimings.map((ct) => ct.char).join("");
87
+ const fullTextLines = wrapText(ctx, fullText, maxWidth, fontSize, fontFamily, fontWeight);
88
+ ctx.save();
89
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
90
+ ctx.textAlign = "left";
91
+ ctx.textBaseline = "middle";
92
+ ctx.lineJoin = "round";
93
+ ctx.lineCap = "round";
94
+ const totalHeight = fullTextLines.length * fontSize * lineHeight;
95
+ const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
96
+ let charIndexInText = 0;
97
+ for (let lineIndex = 0; lineIndex < fullTextLines.length; lineIndex++) {
98
+ const line = fullTextLines[lineIndex];
99
+ const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
100
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
101
+ let currentX = canvasWidth / 2 - lineWidth / 2;
102
+ for (let i = 0; i < line.length; i++) {
103
+ const char = line[i];
104
+ const timing = characterTimings[charIndexInText];
105
+ if (timing) {
106
+ const hasScanned = relativeFrame >= timing.startFrame;
107
+ const isCurrentlySinging = hasScanned && relativeFrame < timing.startFrame + transitionFrames;
108
+ ctx.fillStyle = hasScanned ? highlightFill : baseFill;
109
+ if (isCurrentlySinging) {
110
+ ctx.shadowColor = glowColor;
111
+ ctx.shadowBlur = glowIntensity * 10;
112
+ } else {
113
+ ctx.shadowBlur = 0;
114
+ }
115
+ if (stroke && strokeWidth > 0) {
116
+ ctx.strokeStyle = stroke;
117
+ ctx.lineWidth = strokeWidth;
118
+ ctx.strokeText(char, currentX, y);
119
+ }
120
+ ctx.fillText(char, currentX, y);
121
+ ctx.shadowBlur = 0;
122
+ }
123
+ currentX += measureTextWidth(ctx, char, fontSize, fontFamily, fontWeight);
124
+ charIndexInText++;
125
+ }
126
+ }
127
+ ctx.restore();
128
+ }
129
+ export {
130
+ renderCharacterKTV
131
+ };
132
+ //# sourceMappingURL=character-ktv-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"character-ktv-renderer.js","sources":["../../../../src/stages/compose/text-renderers/character-ktv-renderer.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { wrapText } from '../text-utils/text-wrapper';\nimport { getLetterCaseText, measureTextWidth } from '../text-utils/text-metrics';\nimport { needsSpaceBetweenWords } from '../text-utils/locale-detector';\n\ninterface CharacterTiming {\n char: string;\n index: number;\n startFrame: number;\n}\n\nfunction usToFrame(us: number, fps: number): number {\n return Math.floor(us / (1000000 / fps));\n}\n\nfunction calculateYPosition(\n canvasHeight: number,\n totalHeight: number,\n globalPosition?: {\n position?: 'absolute';\n top?: string;\n bottom?: string;\n left?: string;\n right?: string;\n display?: string;\n alignItems?: string;\n justifyContent?: string;\n }\n): number {\n if (!globalPosition) {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n if (globalPosition.top) {\n const topPercent = parseFloat(globalPosition.top) / 100;\n return canvasHeight * topPercent;\n }\n\n if (globalPosition.bottom) {\n const bottomPercent = parseFloat(globalPosition.bottom) / 100;\n return canvasHeight * (1 - bottomPercent) - totalHeight;\n }\n\n if (globalPosition.justifyContent === 'center' || globalPosition.alignItems === 'center') {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n return canvasHeight / 2 - totalHeight / 2;\n}\n\nexport function renderCharacterKTV(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n relativeFrame: number,\n fps: number = 30\n): void {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return;\n\n const fontSize = fontConfig.fontSize;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const baseFill = fontConfig.fill;\n const stroke = fontConfig.stroke;\n const strokeWidth = fontConfig.strokeWidth || 0;\n const lineHeight = fontConfig.lineHeight || 1.2;\n\n const highlightFill = layer.animation?.highlightTextStyle?.fill || 'rgb(255, 215, 0)';\n const glowColor = layer.animation?.glowColor || '#ffffff';\n const glowIntensity = layer.animation?.glowIntensity || 3;\n const transitionFrames = layer.animation?.transitionFrames || 10;\n\n const maxWidth = canvasWidth * 0.9;\n const text = getLetterCaseText(layer.text, layer.letterCase);\n const needsSpace = needsSpaceBetweenWords(layer.localeCode || 'en-US', text);\n const characterTimings: CharacterTiming[] = [];\n\n if (layer.wordTimings && layer.wordTimings.length > 0) {\n let charIndex = 0;\n\n for (let wordIndex = 0; wordIndex < layer.wordTimings.length; wordIndex++) {\n const word = layer.wordTimings[wordIndex]!;\n const wordChars = word.text.split('');\n const wordStartFrame = usToFrame(word.startUs, fps);\n const wordEndFrame = usToFrame(word.endUs, fps);\n const framesPerChar =\n wordChars.length > 0 ? (wordEndFrame - wordStartFrame) / wordChars.length : 0;\n\n for (let i = 0; i < wordChars.length; i++) {\n const charStartFrame = Math.floor(wordStartFrame + i * framesPerChar);\n characterTimings.push({\n char: wordChars[i]!,\n index: charIndex,\n startFrame: charStartFrame,\n });\n charIndex++;\n }\n\n if (needsSpace && wordIndex < layer.wordTimings.length - 1) {\n const nextWord = layer.wordTimings[wordIndex + 1];\n const nextWordFirstChar = nextWord?.text?.[0] || '';\n const isPunctuation = /[.,!?;:)]/.test(nextWordFirstChar);\n\n if (!isPunctuation) {\n const spaceStartFrame = characterTimings[characterTimings.length - 1]?.startFrame || 0;\n characterTimings.push({\n char: ' ',\n index: charIndex,\n startFrame: spaceStartFrame,\n });\n charIndex++;\n }\n }\n }\n } else {\n const totalFrames = 100;\n const chars = text.split('');\n const framesPerChar = chars.length > 0 ? totalFrames / chars.length : 0;\n\n for (let i = 0; i < chars.length; i++) {\n characterTimings.push({\n char: chars[i]!,\n index: i,\n startFrame: Math.floor(i * framesPerChar),\n });\n }\n }\n\n const fullText = characterTimings.map((ct) => ct.char).join('');\n const fullTextLines = wrapText(ctx, fullText, maxWidth, fontSize, fontFamily, fontWeight);\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n ctx.textAlign = 'left';\n ctx.textBaseline = 'middle';\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n const totalHeight = fullTextLines.length * fontSize * lineHeight;\n const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);\n\n let charIndexInText = 0;\n\n for (let lineIndex = 0; lineIndex < fullTextLines.length; lineIndex++) {\n const line = fullTextLines[lineIndex]!;\n const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n let currentX = canvasWidth / 2 - lineWidth / 2;\n\n for (let i = 0; i < line.length; i++) {\n const char = line[i]!;\n const timing = characterTimings[charIndexInText];\n\n if (timing) {\n const hasScanned = relativeFrame >= timing.startFrame;\n const isCurrentlySinging =\n hasScanned && relativeFrame < timing.startFrame + transitionFrames;\n\n ctx.fillStyle = hasScanned ? highlightFill : baseFill;\n\n if (isCurrentlySinging) {\n ctx.shadowColor = glowColor;\n ctx.shadowBlur = glowIntensity * 10;\n } else {\n ctx.shadowBlur = 0;\n }\n\n if (stroke && strokeWidth > 0) {\n ctx.strokeStyle = stroke;\n ctx.lineWidth = strokeWidth;\n ctx.strokeText(char, currentX, y);\n }\n ctx.fillText(char, currentX, y);\n ctx.shadowBlur = 0;\n }\n\n currentX += measureTextWidth(ctx, char, fontSize, fontFamily, fontWeight);\n charIndexInText++;\n }\n }\n\n ctx.restore();\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { TextLayer } from '../types';
2
2
 
3
- export declare function renderWordByWord(ctx: OffscreenCanvasRenderingContext2D, layer: TextLayer, canvasWidth: number, canvasHeight: number, relativeFrame: number, fps?: number): void;
3
+ export declare function renderWordByWord(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, layer: TextLayer, canvasWidth: number, canvasHeight: number, relativeFrame: number, fps?: number): void;
4
4
  //# sourceMappingURL=word-by-word-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"word-by-word-renderer.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-renderers/word-by-word-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAsD1C,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,iCAAiC,EACtC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,MAAW,GACf,IAAI,CAyHN"}
1
+ {"version":3,"file":"word-by-word-renderer.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-renderers/word-by-word-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAsD1C,wBAAgB,gBAAgB,CAC9B,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,MAAW,GACf,IAAI,CAyHN"}
@@ -0,0 +1,128 @@
1
+ import { formEvenLinesWithWords } from "../text-utils/text-wrapper.js";
2
+ import { getLetterCaseText, measureTextWidth } from "../text-utils/text-metrics.js";
3
+ import { interpolate, interpolateColor } from "./animation-utils.js";
4
+ import { needsSpaceBetweenWords } from "../text-utils/locale-detector.js";
5
+ function usToFrame(us, fps) {
6
+ return Math.floor(us / (1e6 / fps));
7
+ }
8
+ function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
9
+ if (!globalPosition) {
10
+ return canvasHeight / 2 - totalHeight / 2;
11
+ }
12
+ if (globalPosition.top) {
13
+ const topPercent = parseFloat(globalPosition.top) / 100;
14
+ return canvasHeight * topPercent;
15
+ }
16
+ if (globalPosition.bottom) {
17
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
18
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
19
+ }
20
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
21
+ return canvasHeight / 2 - totalHeight / 2;
22
+ }
23
+ return canvasHeight / 2 - totalHeight / 2;
24
+ }
25
+ function renderWordByWord(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
26
+ const fontConfig = layer.fontConfig?.textStyle;
27
+ if (!fontConfig) return;
28
+ const fontSize = fontConfig.fontSize;
29
+ const fontFamily = fontConfig.fontFamily;
30
+ const fontWeight = fontConfig.fontWeight;
31
+ const fill = fontConfig.fill;
32
+ const stroke = fontConfig.stroke;
33
+ const strokeWidth = fontConfig.strokeWidth || 0;
34
+ const lineHeight = fontConfig.lineHeight || 1.2;
35
+ const highlightFill = layer.animation?.highlightTextStyle?.fill || "rgb(255, 215, 0)";
36
+ const highlightStroke = layer.animation?.highlightTextStyle?.stroke || stroke;
37
+ const maxWidth = canvasWidth * 0.64;
38
+ const text = getLetterCaseText(layer.text, layer.letterCase);
39
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
40
+ const words = text.split(needsSpace ? /\s+/ : "");
41
+ const lines = formEvenLinesWithWords(
42
+ ctx,
43
+ words,
44
+ maxWidth,
45
+ fontSize,
46
+ needsSpace,
47
+ fontFamily,
48
+ fontWeight
49
+ );
50
+ const wordPositions = [];
51
+ let wordIndex = 0;
52
+ const totalHeight = lines.length * fontSize * lineHeight;
53
+ const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
54
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
55
+ const line = lines[lineIndex];
56
+ const lineWords = line.split(needsSpace ? /\s+/ : "");
57
+ const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
58
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
59
+ let currentX = canvasWidth / 2 - lineWidth / 2;
60
+ for (const word of lineWords) {
61
+ const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
62
+ const wordTimingUs = layer.wordTimings?.[wordIndex];
63
+ const wordTiming = wordTimingUs ? {
64
+ startFrame: usToFrame(wordTimingUs.startUs, fps),
65
+ endFrame: usToFrame(wordTimingUs.endUs, fps)
66
+ } : void 0;
67
+ wordPositions.push({
68
+ text: word,
69
+ x: currentX + wordWidth / 2,
70
+ y,
71
+ lineIndex,
72
+ wordIndex,
73
+ timing: wordTiming
74
+ });
75
+ currentX += wordWidth + (needsSpace ? measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight) : 0);
76
+ wordIndex++;
77
+ }
78
+ }
79
+ ctx.save();
80
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
81
+ ctx.textAlign = "center";
82
+ ctx.textBaseline = "middle";
83
+ ctx.lineJoin = "round";
84
+ ctx.lineCap = "round";
85
+ for (const wordPos of wordPositions) {
86
+ let currentFill = fill;
87
+ let currentStroke = stroke;
88
+ if (wordPos.timing) {
89
+ const { startFrame, endFrame } = wordPos.timing;
90
+ if (relativeFrame >= startFrame && relativeFrame <= endFrame) {
91
+ const transitionProgressIn = interpolate(
92
+ relativeFrame,
93
+ [startFrame, startFrame + 3],
94
+ [0, 1],
95
+ {
96
+ extrapolateLeft: "clamp",
97
+ extrapolateRight: "clamp"
98
+ }
99
+ );
100
+ currentFill = interpolateColor(fill, highlightFill, transitionProgressIn);
101
+ if (stroke && highlightStroke) {
102
+ currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressIn);
103
+ }
104
+ } else if (relativeFrame > endFrame) {
105
+ const transitionProgressOut = interpolate(relativeFrame, [endFrame, endFrame + 3], [1, 0], {
106
+ extrapolateLeft: "clamp",
107
+ extrapolateRight: "clamp"
108
+ });
109
+ currentFill = interpolateColor(fill, highlightFill, transitionProgressOut);
110
+ if (stroke && highlightStroke) {
111
+ currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressOut);
112
+ }
113
+ }
114
+ }
115
+ if (currentStroke && strokeWidth > 0) {
116
+ ctx.strokeStyle = currentStroke;
117
+ ctx.lineWidth = strokeWidth;
118
+ ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);
119
+ }
120
+ ctx.fillStyle = currentFill;
121
+ ctx.fillText(wordPos.text, wordPos.x, wordPos.y);
122
+ }
123
+ ctx.restore();
124
+ }
125
+ export {
126
+ renderWordByWord
127
+ };
128
+ //# sourceMappingURL=word-by-word-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"word-by-word-renderer.js","sources":["../../../../src/stages/compose/text-renderers/word-by-word-renderer.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { formEvenLinesWithWords } from '../text-utils/text-wrapper';\nimport { getLetterCaseText, measureTextWidth } from '../text-utils/text-metrics';\nimport { interpolate, interpolateColor } from './animation-utils';\nimport { needsSpaceBetweenWords } from '../text-utils/locale-detector';\n\ninterface WordPosition {\n text: string;\n x: number;\n y: number;\n lineIndex: number;\n wordIndex: number;\n timing?: { startFrame: number; endFrame: number };\n}\n\nfunction usToFrame(us: number, fps: number): number {\n return Math.floor(us / (1000000 / fps));\n}\n\nfunction calculateYPosition(\n canvasHeight: number,\n totalHeight: number,\n globalPosition?: {\n position?: 'absolute';\n top?: string;\n bottom?: string;\n left?: string;\n right?: string;\n display?: string;\n alignItems?: string;\n justifyContent?: string;\n }\n): number {\n if (!globalPosition) {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n if (globalPosition.top) {\n const topPercent = parseFloat(globalPosition.top) / 100;\n return canvasHeight * topPercent;\n }\n\n if (globalPosition.bottom) {\n const bottomPercent = parseFloat(globalPosition.bottom) / 100;\n return canvasHeight * (1 - bottomPercent) - totalHeight;\n }\n\n if (globalPosition.justifyContent === 'center' || globalPosition.alignItems === 'center') {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n return canvasHeight / 2 - totalHeight / 2;\n}\n\nexport function renderWordByWord(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n relativeFrame: number,\n fps: number = 30\n): void {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return;\n\n const fontSize = fontConfig.fontSize;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const fill = fontConfig.fill;\n const stroke = fontConfig.stroke;\n const strokeWidth = fontConfig.strokeWidth || 0;\n const lineHeight = fontConfig.lineHeight || 1.2;\n\n const highlightFill = layer.animation?.highlightTextStyle?.fill || 'rgb(255, 215, 0)';\n const highlightStroke = layer.animation?.highlightTextStyle?.stroke || stroke;\n\n const maxWidth = canvasWidth * 0.64;\n const text = getLetterCaseText(layer.text, layer.letterCase);\n const needsSpace = needsSpaceBetweenWords(layer.localeCode || 'en-US', text);\n const words = text.split(needsSpace ? /\\s+/ : '');\n const lines = formEvenLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n\n const wordPositions: WordPosition[] = [];\n let wordIndex = 0;\n\n const totalHeight = lines.length * fontSize * lineHeight;\n const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const lineWords = line.split(needsSpace ? /\\s+/ : '');\n const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;\n\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n let currentX = canvasWidth / 2 - lineWidth / 2;\n\n for (const word of lineWords) {\n const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n const wordTimingUs = layer.wordTimings?.[wordIndex];\n\n const wordTiming = wordTimingUs\n ? {\n startFrame: usToFrame(wordTimingUs.startUs, fps),\n endFrame: usToFrame(wordTimingUs.endUs, fps),\n }\n : undefined;\n\n wordPositions.push({\n text: word,\n x: currentX + wordWidth / 2,\n y,\n lineIndex,\n wordIndex,\n timing: wordTiming,\n });\n\n currentX +=\n wordWidth + (needsSpace ? measureTextWidth(ctx, ' ', fontSize, fontFamily, fontWeight) : 0);\n wordIndex++;\n }\n }\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n for (const wordPos of wordPositions) {\n let currentFill = fill;\n let currentStroke = stroke;\n\n if (wordPos.timing) {\n const { startFrame, endFrame } = wordPos.timing;\n\n if (relativeFrame >= startFrame && relativeFrame <= endFrame) {\n const transitionProgressIn = interpolate(\n relativeFrame,\n [startFrame, startFrame + 3],\n [0, 1],\n {\n extrapolateLeft: 'clamp',\n extrapolateRight: 'clamp',\n }\n );\n\n currentFill = interpolateColor(fill, highlightFill, transitionProgressIn);\n if (stroke && highlightStroke) {\n currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressIn);\n }\n } else if (relativeFrame > endFrame) {\n const transitionProgressOut = interpolate(relativeFrame, [endFrame, endFrame + 3], [1, 0], {\n extrapolateLeft: 'clamp',\n extrapolateRight: 'clamp',\n });\n\n currentFill = interpolateColor(fill, highlightFill, transitionProgressOut);\n if (stroke && highlightStroke) {\n currentStroke = interpolateColor(stroke, highlightStroke, transitionProgressOut);\n }\n }\n }\n\n if (currentStroke && strokeWidth > 0) {\n ctx.strokeStyle = currentStroke;\n ctx.lineWidth = strokeWidth;\n ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);\n }\n ctx.fillStyle = currentFill;\n ctx.fillText(wordPos.text, wordPos.x, wordPos.y);\n }\n\n ctx.restore();\n}\n"],"names":[],"mappings":"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"}
@@ -1,4 +1,4 @@
1
1
  import { TextLayer } from '../types';
2
2
 
3
- export declare function renderWordByWordFancy(ctx: OffscreenCanvasRenderingContext2D, layer: TextLayer, canvasWidth: number, canvasHeight: number, relativeFrame: number, fps?: number): void;
3
+ export declare function renderWordByWordFancy(ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D, layer: TextLayer, canvasWidth: number, canvasHeight: number, relativeFrame: number, fps?: number): void;
4
4
  //# sourceMappingURL=word-fancy-renderer.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"word-fancy-renderer.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-renderers/word-fancy-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAuD1C,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,iCAAiC,EACtC,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,MAAW,GACf,IAAI,CAkIN"}
1
+ {"version":3,"file":"word-fancy-renderer.d.ts","sourceRoot":"","sources":["../../../../src/stages/compose/text-renderers/word-fancy-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAuD1C,wBAAgB,qBAAqB,CACnC,GAAG,EAAE,iCAAiC,GAAG,wBAAwB,EACjE,KAAK,EAAE,SAAS,EAChB,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,aAAa,EAAE,MAAM,EACrB,GAAG,GAAE,MAAW,GACf,IAAI,CAkIN"}
@@ -0,0 +1,135 @@
1
+ import { formEvenLinesWithWords } from "../text-utils/text-wrapper.js";
2
+ import { getLetterCaseText, measureTextWidth } from "../text-utils/text-metrics.js";
3
+ import { springEasing, interpolate } from "./animation-utils.js";
4
+ import { needsSpaceBetweenWords } from "../text-utils/locale-detector.js";
5
+ function usToFrame(us, fps) {
6
+ return Math.floor(us / (1e6 / fps));
7
+ }
8
+ function calculateYPosition(canvasHeight, totalHeight, globalPosition) {
9
+ if (!globalPosition) {
10
+ return canvasHeight / 2 - totalHeight / 2;
11
+ }
12
+ if (globalPosition.top) {
13
+ const topPercent = parseFloat(globalPosition.top) / 100;
14
+ return canvasHeight * topPercent;
15
+ }
16
+ if (globalPosition.bottom) {
17
+ const bottomPercent = parseFloat(globalPosition.bottom) / 100;
18
+ return canvasHeight * (1 - bottomPercent) - totalHeight;
19
+ }
20
+ if (globalPosition.justifyContent === "center" || globalPosition.alignItems === "center") {
21
+ return canvasHeight / 2 - totalHeight / 2;
22
+ }
23
+ return canvasHeight / 2 - totalHeight / 2;
24
+ }
25
+ function renderWordByWordFancy(ctx, layer, canvasWidth, canvasHeight, relativeFrame, fps = 30) {
26
+ const fontConfig = layer.fontConfig?.textStyle;
27
+ if (!fontConfig) return;
28
+ const fontSize = fontConfig.fontSize;
29
+ const fontFamily = fontConfig.fontFamily;
30
+ const fontWeight = fontConfig.fontWeight;
31
+ const fill = fontConfig.fill;
32
+ const stroke = fontConfig.stroke;
33
+ const strokeWidth = fontConfig.strokeWidth || 0;
34
+ const lineHeight = fontConfig.lineHeight || 1.2;
35
+ const highlightBackgroundColor = layer.animation?.highlightColor || "rgb(255, 215, 0)";
36
+ const maxWidth = canvasWidth * 0.64;
37
+ const text = getLetterCaseText(layer.text, layer.letterCase);
38
+ const needsSpace = needsSpaceBetweenWords(layer.localeCode || "en-US", text);
39
+ const words = text.split(needsSpace ? /\s+/ : "");
40
+ const lines = formEvenLinesWithWords(
41
+ ctx,
42
+ words,
43
+ maxWidth,
44
+ fontSize,
45
+ needsSpace,
46
+ fontFamily,
47
+ fontWeight
48
+ );
49
+ const wordPositions = [];
50
+ let wordIndex = 0;
51
+ const totalHeight = lines.length * fontSize * lineHeight;
52
+ const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);
53
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {
54
+ const line = lines[lineIndex];
55
+ const lineWords = line.split(needsSpace ? /\s+/ : "");
56
+ const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;
57
+ const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);
58
+ let currentX = canvasWidth / 2 - lineWidth / 2;
59
+ for (const word of lineWords) {
60
+ const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);
61
+ const wordTimingUs = layer.wordTimings?.[wordIndex];
62
+ const wordTiming = wordTimingUs ? {
63
+ startFrame: usToFrame(wordTimingUs.startUs, fps),
64
+ endFrame: usToFrame(wordTimingUs.endUs, fps)
65
+ } : void 0;
66
+ wordPositions.push({
67
+ text: word,
68
+ x: currentX + wordWidth / 2,
69
+ y,
70
+ width: wordWidth,
71
+ lineIndex,
72
+ wordIndex,
73
+ timing: wordTiming
74
+ });
75
+ currentX += wordWidth + (needsSpace ? measureTextWidth(ctx, " ", fontSize, fontFamily, fontWeight) : 0);
76
+ wordIndex++;
77
+ }
78
+ }
79
+ ctx.save();
80
+ ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;
81
+ ctx.textAlign = "center";
82
+ ctx.textBaseline = "middle";
83
+ ctx.lineJoin = "round";
84
+ ctx.lineCap = "round";
85
+ for (const wordPos of wordPositions) {
86
+ let backgroundOpacity = 0;
87
+ let backgroundScale = 0.8;
88
+ if (wordPos.timing) {
89
+ const { startFrame, endFrame } = wordPos.timing;
90
+ const preStartFrames = 3;
91
+ const isActive = relativeFrame >= startFrame && relativeFrame <= endFrame;
92
+ if (isActive) {
93
+ const scaleSpringIn = springEasing(relativeFrame - (startFrame - preStartFrames), {
94
+ damping: 6,
95
+ mass: 0.35,
96
+ stiffness: 200,
97
+ overshootClamping: false
98
+ });
99
+ const inProgress = interpolate(relativeFrame, [startFrame, startFrame + 1], [0, 1], {
100
+ extrapolateLeft: "clamp",
101
+ extrapolateRight: "clamp"
102
+ });
103
+ backgroundOpacity = 0.9 * inProgress;
104
+ backgroundScale = 0.8 + scaleSpringIn * 0.45;
105
+ } else if (relativeFrame > endFrame) {
106
+ backgroundOpacity = 0;
107
+ backgroundScale = 0.8;
108
+ }
109
+ if (backgroundOpacity > 0) {
110
+ const padding = 8;
111
+ const bgWidth = (wordPos.width + padding * 2) * backgroundScale;
112
+ const bgHeight = (fontSize + padding) * backgroundScale;
113
+ ctx.save();
114
+ ctx.globalAlpha = backgroundOpacity;
115
+ ctx.fillStyle = highlightBackgroundColor;
116
+ ctx.beginPath();
117
+ ctx.roundRect(wordPos.x - bgWidth / 2, wordPos.y - bgHeight / 2, bgWidth, bgHeight, 8);
118
+ ctx.fill();
119
+ ctx.restore();
120
+ }
121
+ }
122
+ if (stroke && strokeWidth > 0) {
123
+ ctx.strokeStyle = stroke;
124
+ ctx.lineWidth = strokeWidth;
125
+ ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);
126
+ }
127
+ ctx.fillStyle = fill;
128
+ ctx.fillText(wordPos.text, wordPos.x, wordPos.y);
129
+ }
130
+ ctx.restore();
131
+ }
132
+ export {
133
+ renderWordByWordFancy
134
+ };
135
+ //# sourceMappingURL=word-fancy-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"word-fancy-renderer.js","sources":["../../../../src/stages/compose/text-renderers/word-fancy-renderer.ts"],"sourcesContent":["import type { TextLayer } from '../types';\nimport { formEvenLinesWithWords } from '../text-utils/text-wrapper';\nimport { getLetterCaseText, measureTextWidth } from '../text-utils/text-metrics';\nimport { springEasing, interpolate } from './animation-utils';\nimport { needsSpaceBetweenWords } from '../text-utils/locale-detector';\n\ninterface WordPosition {\n text: string;\n x: number;\n y: number;\n width: number;\n lineIndex: number;\n wordIndex: number;\n timing?: { startFrame: number; endFrame: number };\n}\n\nfunction usToFrame(us: number, fps: number): number {\n return Math.floor(us / (1000000 / fps));\n}\n\nfunction calculateYPosition(\n canvasHeight: number,\n totalHeight: number,\n globalPosition?: {\n position?: 'absolute';\n top?: string;\n bottom?: string;\n left?: string;\n right?: string;\n display?: string;\n alignItems?: string;\n justifyContent?: string;\n }\n): number {\n if (!globalPosition) {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n if (globalPosition.top) {\n const topPercent = parseFloat(globalPosition.top) / 100;\n return canvasHeight * topPercent;\n }\n\n if (globalPosition.bottom) {\n const bottomPercent = parseFloat(globalPosition.bottom) / 100;\n return canvasHeight * (1 - bottomPercent) - totalHeight;\n }\n\n if (globalPosition.justifyContent === 'center' || globalPosition.alignItems === 'center') {\n return canvasHeight / 2 - totalHeight / 2;\n }\n\n return canvasHeight / 2 - totalHeight / 2;\n}\n\nexport function renderWordByWordFancy(\n ctx: OffscreenCanvasRenderingContext2D | CanvasRenderingContext2D,\n layer: TextLayer,\n canvasWidth: number,\n canvasHeight: number,\n relativeFrame: number,\n fps: number = 30\n): void {\n const fontConfig = layer.fontConfig?.textStyle;\n if (!fontConfig) return;\n\n const fontSize = fontConfig.fontSize;\n const fontFamily = fontConfig.fontFamily;\n const fontWeight = fontConfig.fontWeight;\n const fill = fontConfig.fill;\n const stroke = fontConfig.stroke;\n const strokeWidth = fontConfig.strokeWidth || 0;\n const lineHeight = fontConfig.lineHeight || 1.2;\n\n const highlightBackgroundColor = layer.animation?.highlightColor || 'rgb(255, 215, 0)';\n\n const maxWidth = canvasWidth * 0.64;\n const text = getLetterCaseText(layer.text, layer.letterCase);\n const needsSpace = needsSpaceBetweenWords(layer.localeCode || 'en-US', text);\n const words = text.split(needsSpace ? /\\s+/ : '');\n const lines = formEvenLinesWithWords(\n ctx,\n words,\n maxWidth,\n fontSize,\n needsSpace,\n fontFamily,\n fontWeight\n );\n\n const wordPositions: WordPosition[] = [];\n let wordIndex = 0;\n\n const totalHeight = lines.length * fontSize * lineHeight;\n const startY = calculateYPosition(canvasHeight, totalHeight, layer.fontConfig?.globalPosition);\n\n for (let lineIndex = 0; lineIndex < lines.length; lineIndex++) {\n const line = lines[lineIndex]!;\n const lineWords = line.split(needsSpace ? /\\s+/ : '');\n const y = startY + lineIndex * fontSize * lineHeight + fontSize / 2;\n\n const lineWidth = measureTextWidth(ctx, line, fontSize, fontFamily, fontWeight);\n let currentX = canvasWidth / 2 - lineWidth / 2;\n\n for (const word of lineWords) {\n const wordWidth = measureTextWidth(ctx, word, fontSize, fontFamily, fontWeight);\n const wordTimingUs = layer.wordTimings?.[wordIndex];\n\n const wordTiming = wordTimingUs\n ? {\n startFrame: usToFrame(wordTimingUs.startUs, fps),\n endFrame: usToFrame(wordTimingUs.endUs, fps),\n }\n : undefined;\n\n wordPositions.push({\n text: word,\n x: currentX + wordWidth / 2,\n y,\n width: wordWidth,\n lineIndex,\n wordIndex,\n timing: wordTiming,\n });\n\n currentX +=\n wordWidth + (needsSpace ? measureTextWidth(ctx, ' ', fontSize, fontFamily, fontWeight) : 0);\n wordIndex++;\n }\n }\n\n ctx.save();\n ctx.font = `${fontWeight} ${fontSize}px ${fontFamily}`;\n ctx.textAlign = 'center';\n ctx.textBaseline = 'middle';\n ctx.lineJoin = 'round';\n ctx.lineCap = 'round';\n\n for (const wordPos of wordPositions) {\n let backgroundOpacity = 0;\n let backgroundScale = 0.8;\n\n if (wordPos.timing) {\n const { startFrame, endFrame } = wordPos.timing;\n const preStartFrames = 3;\n const isActive = relativeFrame >= startFrame && relativeFrame <= endFrame;\n\n if (isActive) {\n const scaleSpringIn = springEasing(relativeFrame - (startFrame - preStartFrames), {\n damping: 6,\n mass: 0.35,\n stiffness: 200,\n overshootClamping: false,\n });\n\n const inProgress = interpolate(relativeFrame, [startFrame, startFrame + 1], [0, 1], {\n extrapolateLeft: 'clamp',\n extrapolateRight: 'clamp',\n });\n\n backgroundOpacity = 0.9 * inProgress;\n backgroundScale = 0.8 + scaleSpringIn * 0.45;\n } else if (relativeFrame > endFrame) {\n backgroundOpacity = 0;\n backgroundScale = 0.8;\n }\n\n if (backgroundOpacity > 0) {\n const padding = 8;\n const bgWidth = (wordPos.width + padding * 2) * backgroundScale;\n const bgHeight = (fontSize + padding) * backgroundScale;\n\n ctx.save();\n ctx.globalAlpha = backgroundOpacity;\n ctx.fillStyle = highlightBackgroundColor;\n ctx.beginPath();\n ctx.roundRect(wordPos.x - bgWidth / 2, wordPos.y - bgHeight / 2, bgWidth, bgHeight, 8);\n ctx.fill();\n ctx.restore();\n }\n }\n\n if (stroke && strokeWidth > 0) {\n ctx.strokeStyle = stroke;\n ctx.lineWidth = strokeWidth;\n ctx.strokeText(wordPos.text, wordPos.x, wordPos.y);\n }\n ctx.fillStyle = fill;\n ctx.fillText(wordPos.text, wordPos.x, wordPos.y);\n }\n\n ctx.restore();\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,16 @@
1
+ const CJK_REGEX = /[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\u3400-\u4dbf\uac00-\ud7af]/g;
2
+ function needsSpaceBetweenWords(locale, text) {
3
+ if (text) {
4
+ const cjkMatches = text.match(CJK_REGEX);
5
+ const cjkCount = cjkMatches ? cjkMatches.length : 0;
6
+ if (cjkCount > 0 && cjkCount / text.length >= 0.6) {
7
+ return false;
8
+ }
9
+ return true;
10
+ }
11
+ return !["zh-CN", "ja-JP", "ko-KR"].includes(locale);
12
+ }
13
+ export {
14
+ needsSpaceBetweenWords
15
+ };
16
+ //# sourceMappingURL=locale-detector.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"locale-detector.js","sources":["../../../../src/stages/compose/text-utils/locale-detector.ts"],"sourcesContent":["import type { LocaleCode } from '../font-system/types';\n\nconst CJK_REGEX = /[\\u4e00-\\u9fff\\u3040-\\u309f\\u30a0-\\u30ff\\u3400-\\u4dbf\\uac00-\\ud7af]/g;\n\nexport function needsSpaceBetweenWords(locale: LocaleCode | string, text?: string): boolean {\n if (text) {\n const cjkMatches = text.match(CJK_REGEX);\n const cjkCount = cjkMatches ? cjkMatches.length : 0;\n\n if (cjkCount > 0 && cjkCount / text.length >= 0.6) {\n return false;\n }\n return true;\n }\n\n return !['zh-CN', 'ja-JP', 'ko-KR'].includes(locale);\n}\n\nexport function detectLocaleFromText(text: string): LocaleCode {\n const cjkMatches = text.match(CJK_REGEX);\n const cjkCount = cjkMatches ? cjkMatches.length : 0;\n\n if (cjkCount > 0 && cjkCount / text.length >= 0.6) {\n const chineseCount = (text.match(/[\\u4e00-\\u9fff]/g) || []).length;\n const japaneseCount = (text.match(/[\\u3040-\\u309f\\u30a0-\\u30ff]/g) || []).length;\n const koreanCount = (text.match(/[\\uac00-\\ud7af]/g) || []).length;\n\n if (chineseCount >= japaneseCount && chineseCount >= koreanCount) {\n return 'zh-CN';\n }\n if (japaneseCount >= koreanCount) {\n return 'ja-JP';\n }\n return 'ko-KR';\n }\n\n const arabicCount = (text.match(/[\\u0600-\\u06FF]/g) || []).length;\n if (arabicCount > 0 && arabicCount / text.length >= 0.5) {\n return 'ar-SA';\n }\n\n return 'en-US';\n}\n"],"names":[],"mappings":"AAEA,MAAM,YAAY;AAEX,SAAS,uBAAuB,QAA6B,MAAwB;AAC1F,MAAI,MAAM;AACR,UAAM,aAAa,KAAK,MAAM,SAAS;AACvC,UAAM,WAAW,aAAa,WAAW,SAAS;AAElD,QAAI,WAAW,KAAK,WAAW,KAAK,UAAU,KAAK;AACjD,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAEA,SAAO,CAAC,CAAC,SAAS,SAAS,OAAO,EAAE,SAAS,MAAM;AACrD;"}