@twick/studio 0.15.23 → 0.15.25

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.js CHANGED
@@ -4671,6 +4671,16 @@ function EffectStylePanelContainer({
4671
4671
  }
4672
4672
  );
4673
4673
  }
4674
+ const formatTime = (seconds) => {
4675
+ if (!Number.isFinite(seconds) || seconds < 0) return "0:00.00";
4676
+ const totalMs = Math.round(seconds * 1e3);
4677
+ const totalSeconds = Math.floor(totalMs / 1e3);
4678
+ const minutes = Math.floor(totalSeconds / 60);
4679
+ const secs = totalSeconds % 60;
4680
+ const ms = Math.floor(totalMs % 1e3 / 10);
4681
+ const pad = (n, l = 2) => String(n).padStart(l, "0");
4682
+ return `${minutes}:${pad(secs)}.${pad(ms)}`;
4683
+ };
4674
4684
  function CaptionsPanel({
4675
4685
  captions,
4676
4686
  addCaption,
@@ -4678,54 +4688,102 @@ function CaptionsPanel({
4678
4688
  deleteCaption,
4679
4689
  updateCaption
4680
4690
  }) {
4681
- return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container", children: [
4682
- /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "panel-title", children: "Captions" }),
4683
- captions.map((caption, i) => /* @__PURE__ */ jsxRuntime.jsxs(
4684
- "div",
4685
- {
4686
- className: "panel-section gap-2",
4687
- children: [
4688
- /* @__PURE__ */ jsxRuntime.jsx("div", { children: /* @__PURE__ */ jsxRuntime.jsx(
4689
- "input",
4690
- {
4691
- type: "text",
4692
- placeholder: "Enter caption text",
4693
- value: caption.t,
4694
- onChange: (e) => updateCaption(i, { ...caption, t: e.target.value }),
4695
- className: "input-dark"
4696
- }
4697
- ) }),
4698
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex-container justify-between", children: [
4699
- /* @__PURE__ */ jsxRuntime.jsx(
4700
- "button",
4701
- {
4702
- onClick: () => splitCaption(i),
4703
- className: "btn-ghost",
4704
- title: "Split caption",
4705
- children: /* @__PURE__ */ jsxRuntime.jsx(Scissors, { className: "icon-sm" })
4706
- }
4707
- ),
4708
- /* @__PURE__ */ jsxRuntime.jsx(
4709
- "button",
4710
- {
4711
- onClick: () => deleteCaption(i),
4712
- className: "btn-ghost",
4713
- title: "Delete caption",
4714
- children: /* @__PURE__ */ jsxRuntime.jsx(Trash2, { className: "icon-sm", color: "var(--color-red-500)" })
4715
- }
4716
- )
4717
- ] })
4718
- ]
4719
- },
4720
- i
4721
- )),
4722
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-section", children: /* @__PURE__ */ jsxRuntime.jsx("button", { onClick: addCaption, className: "btn-primary w-full", title: "Add caption", children: "Add" }) })
4691
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container captions-panel", children: [
4692
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "captions-panel-header", children: [
4693
+ /* @__PURE__ */ jsxRuntime.jsx("h3", { className: "panel-title", children: "Captions" }),
4694
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "captions-panel-header-meta", children: [
4695
+ captions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsx("span", { className: "captions-panel-count", children: "No captions yet" }) : null,
4696
+ /* @__PURE__ */ jsxRuntime.jsx(
4697
+ "button",
4698
+ {
4699
+ onClick: addCaption,
4700
+ className: "btn-primary captions-panel-add-button",
4701
+ title: "Add caption",
4702
+ children: "Add caption"
4703
+ }
4704
+ )
4705
+ ] })
4706
+ ] }),
4707
+ captions.length === 0 ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-section captions-panel-empty", children: [
4708
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "captions-panel-empty-title", children: "Start your first caption" }),
4709
+ /* @__PURE__ */ jsxRuntime.jsx("p", { className: "captions-panel-empty-subtitle", children: "Use the button above to add the first caption block for the active track." }),
4710
+ /* @__PURE__ */ jsxRuntime.jsx(
4711
+ "button",
4712
+ {
4713
+ onClick: addCaption,
4714
+ className: "btn-primary captions-panel-empty-button",
4715
+ title: "Add first caption",
4716
+ children: "Add caption"
4717
+ }
4718
+ )
4719
+ ] }) : /* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-section captions-panel-list", children: captions.map((caption, i) => {
4720
+ return /* @__PURE__ */ jsxRuntime.jsxs(
4721
+ "div",
4722
+ {
4723
+ className: "captions-panel-item",
4724
+ children: [
4725
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "captions-panel-item-header", children: [
4726
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "captions-panel-time captions-panel-time-start", children: formatTime(caption.s) }),
4727
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "captions-panel-time captions-panel-time-end", children: formatTime(caption.e) })
4728
+ ] }),
4729
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "captions-panel-item-body", children: [
4730
+ /* @__PURE__ */ jsxRuntime.jsx(
4731
+ "textarea",
4732
+ {
4733
+ placeholder: "Enter caption text",
4734
+ value: caption.t,
4735
+ onChange: (e) => updateCaption(i, { ...caption, t: e.target.value }),
4736
+ className: "input-dark captions-panel-textarea"
4737
+ }
4738
+ ),
4739
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "captions-panel-actions", children: [
4740
+ /* @__PURE__ */ jsxRuntime.jsx(
4741
+ "button",
4742
+ {
4743
+ onClick: () => splitCaption(i),
4744
+ className: "btn-ghost captions-panel-action-button",
4745
+ title: "Split caption at midpoint",
4746
+ children: /* @__PURE__ */ jsxRuntime.jsx(Scissors, { className: "icon-sm" })
4747
+ }
4748
+ ),
4749
+ /* @__PURE__ */ jsxRuntime.jsx(
4750
+ "button",
4751
+ {
4752
+ onClick: () => deleteCaption(i),
4753
+ className: "btn-ghost captions-panel-action-button",
4754
+ title: "Delete caption",
4755
+ children: /* @__PURE__ */ jsxRuntime.jsx(
4756
+ Trash2,
4757
+ {
4758
+ className: "icon-sm",
4759
+ color: "var(--color-red-500)"
4760
+ }
4761
+ )
4762
+ }
4763
+ )
4764
+ ] })
4765
+ ] })
4766
+ ]
4767
+ },
4768
+ i
4769
+ );
4770
+ }) })
4723
4771
  ] });
4724
4772
  }
4773
+ const HIGHLIGHT_BG_FONT_SIZE = 46;
4774
+ const WORD_BY_WORD_FONT_SIZE = 46;
4775
+ const WORD_BY_WORD_WITH_BG_FONT_SIZE = 46;
4776
+ const OUTLINE_ONLY_FONT_SIZE = 42;
4777
+ const SOFT_BOX_FONT_SIZE = 40;
4778
+ const HIGHLIGHT_BG_GEOMETRY = timeline.computeCaptionGeometry(HIGHLIGHT_BG_FONT_SIZE, timeline.CAPTION_STYLE.WORD_BG_HIGHLIGHT);
4779
+ const WORD_BY_WORD_GEOMETRY = timeline.computeCaptionGeometry(WORD_BY_WORD_FONT_SIZE, timeline.CAPTION_STYLE.WORD_BY_WORD);
4780
+ const WORD_BY_WORD_WITH_BG_GEOMETRY = timeline.computeCaptionGeometry(WORD_BY_WORD_WITH_BG_FONT_SIZE, timeline.CAPTION_STYLE.WORD_BY_WORD_WITH_BG);
4781
+ const OUTLINE_ONLY_GEOMETRY = timeline.computeCaptionGeometry(OUTLINE_ONLY_FONT_SIZE, timeline.CAPTION_STYLE.OUTLINE_ONLY);
4782
+ const SOFT_BOX_GEOMETRY = timeline.computeCaptionGeometry(SOFT_BOX_FONT_SIZE, timeline.CAPTION_STYLE.SOFT_BOX);
4725
4783
  const CAPTION_PROPS = {
4726
4784
  [timeline.CAPTION_STYLE.WORD_BG_HIGHLIGHT]: {
4727
4785
  font: {
4728
- size: 46,
4786
+ size: HIGHLIGHT_BG_FONT_SIZE,
4729
4787
  weight: 700,
4730
4788
  family: "Bangers"
4731
4789
  },
@@ -4734,15 +4792,16 @@ const CAPTION_PROPS = {
4734
4792
  highlight: "#ff4081",
4735
4793
  bgColor: "#444444"
4736
4794
  },
4737
- lineWidth: 0.35,
4795
+ lineWidth: HIGHLIGHT_BG_GEOMETRY.lineWidth,
4796
+ rectProps: HIGHLIGHT_BG_GEOMETRY.rectProps,
4738
4797
  stroke: "#000000",
4739
4798
  fontWeight: 700,
4740
- shadowOffset: [-3, 3],
4799
+ shadowOffset: [-1, 1],
4741
4800
  shadowColor: "#000000"
4742
4801
  },
4743
4802
  [timeline.CAPTION_STYLE.WORD_BY_WORD]: {
4744
4803
  font: {
4745
- size: 46,
4804
+ size: WORD_BY_WORD_FONT_SIZE,
4746
4805
  weight: 700,
4747
4806
  family: "Bangers"
4748
4807
  },
@@ -4751,15 +4810,16 @@ const CAPTION_PROPS = {
4751
4810
  highlight: "#ff4081",
4752
4811
  bgColor: "#444444"
4753
4812
  },
4754
- lineWidth: 0.35,
4813
+ lineWidth: WORD_BY_WORD_GEOMETRY.lineWidth,
4814
+ rectProps: WORD_BY_WORD_GEOMETRY.rectProps,
4755
4815
  stroke: "#000000",
4756
- shadowOffset: [-2, 2],
4816
+ shadowOffset: [-1, 1],
4757
4817
  shadowColor: "#000000",
4758
4818
  shadowBlur: 5
4759
4819
  },
4760
4820
  [timeline.CAPTION_STYLE.WORD_BY_WORD_WITH_BG]: {
4761
4821
  font: {
4762
- size: 46,
4822
+ size: WORD_BY_WORD_WITH_BG_FONT_SIZE,
4763
4823
  weight: 700,
4764
4824
  family: "Bangers"
4765
4825
  },
@@ -4768,14 +4828,15 @@ const CAPTION_PROPS = {
4768
4828
  highlight: "#ff4081",
4769
4829
  bgColor: "#444444"
4770
4830
  },
4771
- lineWidth: 0.35,
4772
- shadowOffset: [-2, 2],
4831
+ lineWidth: WORD_BY_WORD_WITH_BG_GEOMETRY.lineWidth,
4832
+ rectProps: WORD_BY_WORD_WITH_BG_GEOMETRY.rectProps,
4833
+ shadowOffset: [-1, 1],
4773
4834
  shadowColor: "#000000",
4774
4835
  shadowBlur: 5
4775
4836
  },
4776
4837
  [timeline.CAPTION_STYLE.OUTLINE_ONLY]: {
4777
4838
  font: {
4778
- size: 42,
4839
+ size: OUTLINE_ONLY_FONT_SIZE,
4779
4840
  weight: 600,
4780
4841
  family: "Arial"
4781
4842
  },
@@ -4784,7 +4845,8 @@ const CAPTION_PROPS = {
4784
4845
  highlight: "#ff4081",
4785
4846
  bgColor: "#000000"
4786
4847
  },
4787
- lineWidth: 0.5,
4848
+ lineWidth: OUTLINE_ONLY_GEOMETRY.lineWidth,
4849
+ rectProps: OUTLINE_ONLY_GEOMETRY.rectProps,
4788
4850
  stroke: "#000000",
4789
4851
  fontWeight: 600,
4790
4852
  shadowOffset: [0, 0],
@@ -4793,7 +4855,7 @@ const CAPTION_PROPS = {
4793
4855
  },
4794
4856
  [timeline.CAPTION_STYLE.SOFT_BOX]: {
4795
4857
  font: {
4796
- size: 40,
4858
+ size: SOFT_BOX_FONT_SIZE,
4797
4859
  weight: 600,
4798
4860
  family: "Montserrat"
4799
4861
  },
@@ -4802,7 +4864,8 @@ const CAPTION_PROPS = {
4802
4864
  highlight: "#ff4081",
4803
4865
  bgColor: "#333333"
4804
4866
  },
4805
- lineWidth: 0.2,
4867
+ lineWidth: SOFT_BOX_GEOMETRY.lineWidth,
4868
+ rectProps: SOFT_BOX_GEOMETRY.rectProps,
4806
4869
  stroke: "#000000",
4807
4870
  fontWeight: 600,
4808
4871
  shadowOffset: [-1, 1],
@@ -4907,141 +4970,13 @@ function CaptionsPanelContainer() {
4907
4970
  const captionsPanelProps = useCaptionsPanel();
4908
4971
  return /* @__PURE__ */ jsxRuntime.jsx(CaptionsPanel, { ...captionsPanelProps });
4909
4972
  }
4910
- const FAL_IMAGE_ENDPOINTS = [
4911
- {
4912
- provider: "fal",
4913
- endpointId: "fal-ai/flux-pro/kontext",
4914
- label: "FLUX.1 Kontext [pro]",
4915
- description: "Professional image generation with context-aware editing",
4916
- popularity: 5,
4917
- category: "image",
4918
- inputAsset: ["image"],
4919
- availableDimensions: [
4920
- { width: 1024, height: 1024, label: "1024x1024 (1:1)" },
4921
- { width: 1024, height: 576, label: "1024x576 (16:9)" },
4922
- { width: 576, height: 1024, label: "576x1024 (9:16)" }
4923
- ]
4924
- },
4925
- {
4926
- provider: "fal",
4927
- endpointId: "fal-ai/flux/dev",
4928
- label: "FLUX.1 [dev]",
4929
- description: "High-quality image generation",
4930
- popularity: 5,
4931
- category: "image",
4932
- minSteps: 1,
4933
- maxSteps: 50,
4934
- defaultSteps: 28,
4935
- minGuidanceScale: 1,
4936
- maxGuidanceScale: 20,
4937
- defaultGuidanceScale: 3.5,
4938
- hasSeed: true
4939
- },
4940
- {
4941
- provider: "fal",
4942
- endpointId: "fal-ai/flux/schnell",
4943
- label: "FLUX.1 [schnell]",
4944
- description: "Ultra-fast image generation",
4945
- popularity: 4,
4946
- category: "image",
4947
- defaultSteps: 4,
4948
- availableDimensions: [
4949
- { width: 1024, height: 1024, label: "1024x1024 (1:1)" },
4950
- { width: 1024, height: 576, label: "1024x576 (16:9)" },
4951
- { width: 576, height: 1024, label: "576x1024 (9:16)" }
4952
- ]
4953
- },
4954
- {
4955
- provider: "fal",
4956
- endpointId: "fal-ai/gemini-25-flash-image",
4957
- label: "Gemini 2.5 Flash Image",
4958
- description: "Rapid text-to-image generation",
4959
- popularity: 5,
4960
- category: "image",
4961
- availableDimensions: [
4962
- { width: 1024, height: 1024, label: "1024x1024 (1:1)" },
4963
- { width: 1024, height: 768, label: "1024x768 (4:3)" },
4964
- { width: 768, height: 1024, label: "768x1024 (3:4)" },
4965
- { width: 1024, height: 576, label: "1024x576 (16:9)" },
4966
- { width: 576, height: 1024, label: "576x1024 (9:16)" }
4967
- ]
4968
- },
4969
- {
4970
- provider: "fal",
4971
- endpointId: "fal-ai/ideogram/v3",
4972
- label: "Ideogram V3",
4973
- description: "Advanced text-to-image with superior text rendering",
4974
- popularity: 5,
4975
- category: "image",
4976
- hasSeed: true,
4977
- hasNegativePrompt: true
4978
- }
4979
- ];
4980
- const FAL_VIDEO_ENDPOINTS = [
4981
- {
4982
- provider: "fal",
4983
- endpointId: "fal-ai/veo3",
4984
- label: "Veo 3",
4985
- description: "Google Veo 3 text-to-video",
4986
- popularity: 5,
4987
- category: "video",
4988
- availableDurations: [4, 6, 8],
4989
- defaultDuration: 8,
4990
- availableDimensions: [
4991
- { width: 576, height: 1024, label: "576x1024 (9:16)" },
4992
- { width: 1024, height: 576, label: "1024x576 (16:9)" },
4993
- { width: 1024, height: 1024, label: "1024x1024 (1:1)" }
4994
- ]
4995
- },
4996
- {
4997
- provider: "fal",
4998
- endpointId: "fal-ai/veo3/fast",
4999
- label: "Veo 3 Fast",
5000
- description: "Accelerated Veo 3 text-to-video",
5001
- popularity: 5,
5002
- category: "video",
5003
- availableDurations: [4, 6, 8],
5004
- defaultDuration: 8,
5005
- availableDimensions: [
5006
- { width: 576, height: 1024, label: "576x1024 (9:16)" },
5007
- { width: 1024, height: 576, label: "1024x576 (16:9)" },
5008
- { width: 1024, height: 1024, label: "1024x1024 (1:1)" }
5009
- ]
5010
- },
5011
- {
5012
- provider: "fal",
5013
- endpointId: "fal-ai/veo3/image-to-video",
5014
- label: "Veo 3 Image-to-Video",
5015
- description: "Animate images with Veo 3",
5016
- popularity: 5,
5017
- category: "video",
5018
- inputAsset: ["image"],
5019
- availableDurations: [8],
5020
- defaultDuration: 8
5021
- },
5022
- {
5023
- provider: "fal",
5024
- endpointId: "fal-ai/kling-video/v2.5-turbo/pro/text-to-video",
5025
- label: "Kling 2.5 Turbo Pro",
5026
- description: "Text-to-video with fluid motion",
5027
- popularity: 5,
5028
- category: "video",
5029
- availableDurations: [5, 10],
5030
- defaultDuration: 5,
5031
- availableDimensions: [
5032
- { width: 1024, height: 576, label: "1024x576 (16:9)" },
5033
- { width: 576, height: 1024, label: "576x1024 (9:16)" },
5034
- { width: 1024, height: 1024, label: "1024x1024 (1:1)" }
5035
- ]
5036
- }
5037
- ];
5038
4973
  const DEFAULT_IMAGE_DURATION = 5;
5039
4974
  function GenerateMediaPanelContainer({
5040
4975
  videoResolution,
5041
4976
  addElement,
5042
4977
  studioConfig
5043
4978
  }) {
5044
- var _a;
4979
+ var _a, _b, _c;
5045
4980
  const { getCurrentTime } = livePlayer.useLivePlayerContext();
5046
4981
  const [tab, setTab] = react.useState("image");
5047
4982
  const [prompt2, setPrompt] = react.useState("");
@@ -5052,8 +4987,12 @@ function GenerateMediaPanelContainer({
5052
4987
  const imageService = studioConfig == null ? void 0 : studioConfig.imageGenerationService;
5053
4988
  const videoService = studioConfig == null ? void 0 : studioConfig.videoGenerationService;
5054
4989
  const hasAnyService = !!imageService || !!videoService;
5055
- const endpoints = tab === "image" ? FAL_IMAGE_ENDPOINTS : FAL_VIDEO_ENDPOINTS;
5056
- const defaultEndpointId = ((_a = endpoints[0]) == null ? void 0 : _a.endpointId) ?? "";
4990
+ const imageModels = ((_a = imageService == null ? void 0 : imageService.getAvailableModels) == null ? void 0 : _a.call(imageService)) ?? [];
4991
+ const videoModels = ((_b = videoService == null ? void 0 : videoService.getAvailableModels) == null ? void 0 : _b.call(videoService)) ?? [];
4992
+ const endpoints = tab === "image" ? imageModels : videoModels;
4993
+ const defaultEndpointId = ((_c = endpoints[0]) == null ? void 0 : _c.endpointId) ?? "";
4994
+ const selectedEndpoint = endpoints.find((endpoint) => endpoint.endpointId === selectedEndpointId) ?? endpoints[0];
4995
+ const selectedProvider = selectedEndpoint == null ? void 0 : selectedEndpoint.provider;
5057
4996
  react.useEffect(() => {
5058
4997
  if (!selectedEndpointId && defaultEndpointId) {
5059
4998
  setSelectedEndpointId(defaultEndpointId);
@@ -5115,9 +5054,16 @@ function GenerateMediaPanelContainer({
5115
5054
  setStatus("Starting...");
5116
5055
  try {
5117
5056
  const endpointId = selectedEndpointId || defaultEndpointId;
5057
+ const provider = selectedProvider;
5058
+ if (!endpointId || !provider) {
5059
+ setError("No model is configured for this tab");
5060
+ setIsGenerating(false);
5061
+ setStatus(null);
5062
+ return;
5063
+ }
5118
5064
  if (tab === "image" && imageService) {
5119
5065
  const requestId = await imageService.generateImage({
5120
- provider: "fal",
5066
+ provider,
5121
5067
  endpointId,
5122
5068
  prompt: prompt2.trim()
5123
5069
  });
@@ -5127,7 +5073,7 @@ function GenerateMediaPanelContainer({
5127
5073
  }
5128
5074
  } else if (tab === "video" && videoService) {
5129
5075
  const requestId = await videoService.generateVideo({
5130
- provider: "fal",
5076
+ provider,
5131
5077
  endpointId,
5132
5078
  prompt: prompt2.trim()
5133
5079
  });
@@ -5149,7 +5095,8 @@ function GenerateMediaPanelContainer({
5149
5095
  defaultEndpointId,
5150
5096
  imageService,
5151
5097
  videoService,
5152
- pollStatus
5098
+ pollStatus,
5099
+ selectedProvider
5153
5100
  ]);
5154
5101
  if (!hasAnyService) {
5155
5102
  return /* @__PURE__ */ jsxRuntime.jsx("div", { className: "panel-container", children: /* @__PURE__ */ jsxRuntime.jsx("p", { className: "empty-state-text", children: "Image and video generation require configuration. Add imageGenerationService and videoGenerationService to StudioConfig." }) });
@@ -6437,6 +6384,116 @@ const CAPTION_COLOR = {
6437
6384
  bgColor: "#8C52FF",
6438
6385
  outlineColor: "#000000"
6439
6386
  };
6387
+ const CAPTION_STYLE_COLOR_META = {
6388
+ // Word background highlight - white text on colored pill
6389
+ highlight_bg: {
6390
+ // Text color, and background pill color used in animation.
6391
+ usedColors: ["text", "bgColor"],
6392
+ labels: {
6393
+ text: "Text Color",
6394
+ bgColor: "Highlight Background"
6395
+ }
6396
+ },
6397
+ // Simple word-by-word – text only
6398
+ word_by_word: {
6399
+ // Visualizer uses text as fill + outlineColor for stroke, and highlight for active word.
6400
+ usedColors: ["text", "highlight", "outlineColor"],
6401
+ labels: {
6402
+ text: "Text Color",
6403
+ highlight: "Highlight Color",
6404
+ outlineColor: "Outline Color"
6405
+ }
6406
+ },
6407
+ // Word-by-word with a phrase bar background
6408
+ word_by_word_with_bg: {
6409
+ // Text color (fill), highlight for active word, outlineColor (stroke), bgColor used by phrase rect.
6410
+ usedColors: ["text", "highlight", "bgColor", "outlineColor"],
6411
+ labels: {
6412
+ text: "Text Color",
6413
+ bgColor: "Bar Background",
6414
+ highlight: "Highlight Color",
6415
+ outlineColor: "Outline Color"
6416
+ }
6417
+ },
6418
+ // Classic outlined text
6419
+ outline_only: {
6420
+ // Outline-only style: fill + outline color; highlight not used in animation.
6421
+ usedColors: ["text", "outlineColor"],
6422
+ labels: {
6423
+ text: "Fill Color",
6424
+ outlineColor: "Outline Color"
6425
+ }
6426
+ },
6427
+ // Soft rounded box behind text
6428
+ soft_box: {
6429
+ usedColors: ["text", "bgColor", "highlight", "outlineColor"],
6430
+ labels: {
6431
+ text: "Text Color",
6432
+ highlight: "Highlight Color",
6433
+ bgColor: "Box Background",
6434
+ outlineColor: "Outline Color"
6435
+ }
6436
+ },
6437
+ // Broadcast style lower-third bar
6438
+ lower_third: {
6439
+ // Title text, bar background, highlight color and outline color.
6440
+ usedColors: ["text", "bgColor", "outlineColor"],
6441
+ labels: {
6442
+ text: "Title Text Color",
6443
+ bgColor: "Bar Background",
6444
+ highlight: "Highlight Color",
6445
+ outlineColor: "Outline Color"
6446
+ }
6447
+ },
6448
+ // Typewriter – text only
6449
+ typewriter: {
6450
+ // Text color and outline color (stroke) used by visualizer; highlight not animated.
6451
+ usedColors: ["text", "outlineColor"],
6452
+ labels: {
6453
+ text: "Text Color",
6454
+ outlineColor: "Outline Color"
6455
+ }
6456
+ },
6457
+ // Karaoke – base text plus active word highlight
6458
+ karaoke: {
6459
+ // Base text color, active word highlight color, outline color.
6460
+ usedColors: ["text", "highlight", "outlineColor"],
6461
+ labels: {
6462
+ text: "Text Color",
6463
+ highlight: "Highlight Color",
6464
+ outlineColor: "Outline Color"
6465
+ }
6466
+ },
6467
+ // Karaoke-word – single active word, previous words dimmed
6468
+ "karaoke-word": {
6469
+ // Same color needs as karaoke.
6470
+ usedColors: ["text", "highlight", "outlineColor"],
6471
+ labels: {
6472
+ text: "Text Color",
6473
+ highlight: "Highlight Color",
6474
+ outlineColor: "Outline Color"
6475
+ }
6476
+ },
6477
+ // Pop / scale – text only
6478
+ pop_scale: {
6479
+ // Text color, highlight color for active word, and outline color; no background.
6480
+ usedColors: ["text", "highlight", "outlineColor"],
6481
+ labels: {
6482
+ text: "Text Color",
6483
+ highlight: "Highlight Color",
6484
+ outlineColor: "Outline Color"
6485
+ }
6486
+ }
6487
+ };
6488
+ const DEFAULT_COLOR_META = {
6489
+ usedColors: ["text", "bgColor", "outlineColor"],
6490
+ labels: {
6491
+ text: "Text Color",
6492
+ bgColor: "Background Color",
6493
+ outlineColor: "Outline Color"
6494
+ }
6495
+ };
6496
+ const CAPTION_FONTS = Object.values(VideoEditor.AVAILABLE_TEXT_FONTS);
6440
6497
  function CaptionPropPanel({
6441
6498
  selectedElement,
6442
6499
  updateElement
@@ -6454,24 +6511,42 @@ function CaptionPropPanel({
6454
6511
  bgColor: CAPTION_COLOR.bgColor,
6455
6512
  outlineColor: CAPTION_COLOR.outlineColor
6456
6513
  });
6514
+ const [useHighlight, setUseHighlight] = react.useState(true);
6515
+ const [useOutline, setUseOutline] = react.useState(true);
6457
6516
  const track = selectedElement instanceof timeline.CaptionElement ? editor.getTrackById(selectedElement.getTrackId()) : null;
6458
6517
  const trackProps = (track == null ? void 0 : track.getProps()) ?? {};
6459
6518
  const applyToAll = (trackProps == null ? void 0 : trackProps.applyToAll) ?? false;
6460
6519
  const handleUpdateCaption = (updates) => {
6461
6520
  const captionElement = selectedElement;
6462
6521
  if (!captionElement) return;
6522
+ const nextFontSize = updates.fontSize ?? fontSize;
6523
+ const geometry = timeline.computeCaptionGeometry(nextFontSize, updates.style ?? (capStyle == null ? void 0 : capStyle.value) ?? "");
6524
+ const highlightEnabled = updates.useHighlightOverride ?? useHighlight;
6525
+ const outlineEnabled = updates.useOutlineOverride ?? useOutline;
6526
+ const rawNextColors = updates.colors ?? colors;
6527
+ let effectiveColors = { ...rawNextColors };
6528
+ if (!highlightEnabled) {
6529
+ const { highlight, ...rest } = effectiveColors;
6530
+ effectiveColors = rest;
6531
+ }
6532
+ if (!outlineEnabled) {
6533
+ const { outlineColor, ...rest } = effectiveColors;
6534
+ effectiveColors = rest;
6535
+ }
6463
6536
  if (applyToAll && track) {
6464
6537
  const nextFont = {
6465
- size: updates.fontSize ?? fontSize,
6538
+ size: nextFontSize,
6466
6539
  family: updates.fontFamily ?? fontFamily
6467
6540
  };
6468
- const nextColors = updates.colors ?? colors;
6541
+ const nextColors = effectiveColors;
6469
6542
  const nextCapStyle = updates.style ?? (capStyle == null ? void 0 : capStyle.value);
6470
6543
  track.setProps({
6471
6544
  ...trackProps,
6472
6545
  capStyle: nextCapStyle,
6473
6546
  font: { ...(trackProps == null ? void 0 : trackProps.font) ?? {}, ...nextFont },
6474
- colors: nextColors
6547
+ colors: nextColors,
6548
+ lineWidth: geometry.lineWidth,
6549
+ rectProps: geometry.rectProps
6475
6550
  });
6476
6551
  editor.refresh();
6477
6552
  } else {
@@ -6480,10 +6555,11 @@ function CaptionPropPanel({
6480
6555
  ...elementProps,
6481
6556
  capStyle: updates.style ?? (capStyle == null ? void 0 : capStyle.value),
6482
6557
  font: {
6483
- size: updates.fontSize ?? fontSize,
6558
+ size: nextFontSize,
6484
6559
  family: updates.fontFamily ?? fontFamily
6485
6560
  },
6486
- colors: updates.colors ?? colors
6561
+ colors: effectiveColors,
6562
+ lineWidth: geometry.lineWidth
6487
6563
  });
6488
6564
  updateElement == null ? void 0 : updateElement(captionElement);
6489
6565
  }
@@ -6509,11 +6585,62 @@ function CaptionPropPanel({
6509
6585
  bgColor: (c == null ? void 0 : c.bgColor) ?? CAPTION_COLOR.bgColor,
6510
6586
  outlineColor: (c == null ? void 0 : c.outlineColor) ?? CAPTION_COLOR.outlineColor
6511
6587
  });
6588
+ setUseHighlight((c == null ? void 0 : c.highlight) != null);
6589
+ setUseOutline((c == null ? void 0 : c.outlineColor) != null);
6512
6590
  }
6513
6591
  }, [selectedElement, applyToAll, changeLog]);
6514
6592
  if (!(selectedElement instanceof timeline.CaptionElement)) {
6515
6593
  return null;
6516
6594
  }
6595
+ const currentStyleKey = capStyle == null ? void 0 : capStyle.value;
6596
+ const currentColorMeta = currentStyleKey && CAPTION_STYLE_COLOR_META[currentStyleKey] || DEFAULT_COLOR_META;
6597
+ const defaultColorLabels = {
6598
+ text: "Text Color",
6599
+ bgColor: "Background Color",
6600
+ highlight: "Highlight Color",
6601
+ outlineColor: "Outline Color"
6602
+ };
6603
+ const renderColorControl = (key) => {
6604
+ if (key === "highlight" && !useHighlight) {
6605
+ return null;
6606
+ }
6607
+ if (key === "outlineColor" && !useOutline) {
6608
+ return null;
6609
+ }
6610
+ const label = currentColorMeta.labels[key] ?? defaultColorLabels[key];
6611
+ const value = colors[key];
6612
+ const handleChange = (next) => {
6613
+ const nextColors = { ...colors, [key]: next };
6614
+ setColors(nextColors);
6615
+ handleUpdateCaption({ colors: nextColors });
6616
+ };
6617
+ if (value == null) {
6618
+ return null;
6619
+ }
6620
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-control", children: [
6621
+ /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-small", children: label }),
6622
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-inputs", children: [
6623
+ /* @__PURE__ */ jsxRuntime.jsx(
6624
+ "input",
6625
+ {
6626
+ type: "color",
6627
+ value,
6628
+ onChange: (e) => handleChange(e.target.value),
6629
+ className: "color-picker"
6630
+ }
6631
+ ),
6632
+ /* @__PURE__ */ jsxRuntime.jsx(
6633
+ "input",
6634
+ {
6635
+ type: "text",
6636
+ value,
6637
+ onChange: (e) => handleChange(e.target.value),
6638
+ className: "color-text"
6639
+ }
6640
+ )
6641
+ ] })
6642
+ ] }, key);
6643
+ };
6517
6644
  return /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-container", children: [
6518
6645
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-section", children: [
6519
6646
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-dark", children: "Caption Style" }),
@@ -6560,7 +6687,7 @@ function CaptionPropPanel({
6560
6687
  ] }),
6561
6688
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-section", children: [
6562
6689
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-dark", children: "Font" }),
6563
- /* @__PURE__ */ jsxRuntime.jsxs(
6690
+ /* @__PURE__ */ jsxRuntime.jsx(
6564
6691
  "select",
6565
6692
  {
6566
6693
  value: fontFamily,
@@ -6570,111 +6697,59 @@ function CaptionPropPanel({
6570
6697
  handleUpdateCaption({ fontFamily: value });
6571
6698
  },
6572
6699
  className: "select-dark w-full",
6573
- children: [
6574
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "Bangers", children: "Bangers" }),
6575
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "Arial", children: "Arial" }),
6576
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "Helvetica", children: "Helvetica" }),
6577
- /* @__PURE__ */ jsxRuntime.jsx("option", { value: "Times New Roman", children: "Times New Roman" })
6578
- ]
6700
+ children: CAPTION_FONTS.map((font) => /* @__PURE__ */ jsxRuntime.jsx("option", { value: font, children: font }, font))
6579
6701
  }
6580
6702
  )
6581
6703
  ] }),
6582
6704
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "panel-section", children: [
6583
6705
  /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-dark", children: "Colors" }),
6584
6706
  /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-section", children: [
6585
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-control", children: [
6586
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-small", children: "Text Color" }),
6587
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-inputs", children: [
6588
- /* @__PURE__ */ jsxRuntime.jsx(
6589
- "input",
6590
- {
6591
- type: "color",
6592
- value: colors.text,
6593
- onChange: (e) => {
6594
- const newColors = { ...colors, text: e.target.value };
6595
- setColors(newColors);
6596
- handleUpdateCaption({ colors: newColors });
6597
- },
6598
- className: "color-picker"
6599
- }
6600
- ),
6601
- /* @__PURE__ */ jsxRuntime.jsx(
6602
- "input",
6603
- {
6604
- type: "text",
6605
- value: colors.text,
6606
- onChange: (e) => {
6607
- const newColors = { ...colors, text: e.target.value };
6608
- setColors(newColors);
6609
- handleUpdateCaption({ colors: newColors });
6610
- },
6611
- className: "color-text"
6612
- }
6613
- )
6614
- ] })
6615
- ] }),
6616
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-control", children: [
6617
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-small", children: "Background Color" }),
6618
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-inputs", children: [
6619
- /* @__PURE__ */ jsxRuntime.jsx(
6620
- "input",
6621
- {
6622
- type: "color",
6623
- value: colors.bgColor,
6624
- onChange: (e) => {
6625
- const newColors = { ...colors, bgColor: e.target.value };
6626
- setColors(newColors);
6627
- handleUpdateCaption({ colors: newColors });
6628
- },
6629
- className: "color-picker"
6630
- }
6631
- ),
6632
- /* @__PURE__ */ jsxRuntime.jsx(
6633
- "input",
6634
- {
6635
- type: "text",
6636
- value: colors.bgColor,
6637
- onChange: (e) => {
6638
- const newColors = { ...colors, bgColor: e.target.value };
6639
- setColors(newColors);
6640
- handleUpdateCaption({ colors: newColors });
6641
- },
6642
- className: "color-text"
6643
- }
6644
- )
6645
- ] })
6646
- ] }),
6647
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-control", children: [
6648
- /* @__PURE__ */ jsxRuntime.jsx("label", { className: "label-small", children: "Outline Color" }),
6649
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "color-inputs", children: [
6650
- /* @__PURE__ */ jsxRuntime.jsx(
6651
- "input",
6652
- {
6653
- type: "color",
6654
- value: colors.outlineColor,
6655
- onChange: (e) => {
6656
- const newColors = { ...colors, outlineColor: e.target.value };
6657
- setColors(newColors);
6658
- handleUpdateCaption({ colors: newColors });
6659
- },
6660
- className: "color-picker"
6661
- }
6662
- ),
6663
- /* @__PURE__ */ jsxRuntime.jsx(
6664
- "input",
6665
- {
6666
- type: "text",
6667
- value: colors.outlineColor,
6668
- onChange: (e) => {
6669
- const newColors = { ...colors, outlineColor: e.target.value };
6670
- setColors(newColors);
6671
- handleUpdateCaption({ colors: newColors });
6672
- },
6673
- className: "color-text"
6674
- }
6675
- )
6676
- ] })
6677
- ] })
6707
+ currentColorMeta.usedColors.includes("highlight") && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "checkbox-control", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "checkbox-label", children: [
6708
+ /* @__PURE__ */ jsxRuntime.jsx(
6709
+ "input",
6710
+ {
6711
+ type: "checkbox",
6712
+ checked: useHighlight,
6713
+ onChange: (e) => {
6714
+ const enabled = e.target.checked;
6715
+ setUseHighlight(enabled);
6716
+ const nextColors = enabled ? { ...colors, highlight: colors.highlight || CAPTION_COLOR.highlight } : { ...colors };
6717
+ setColors(nextColors);
6718
+ handleUpdateCaption({
6719
+ colors: nextColors,
6720
+ useHighlightOverride: enabled
6721
+ });
6722
+ },
6723
+ className: "checkbox-purple"
6724
+ }
6725
+ ),
6726
+ "Use Highlight Color"
6727
+ ] }) }),
6728
+ currentColorMeta.usedColors.includes("outlineColor") && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "checkbox-control", children: /* @__PURE__ */ jsxRuntime.jsxs("label", { className: "checkbox-label", children: [
6729
+ /* @__PURE__ */ jsxRuntime.jsx(
6730
+ "input",
6731
+ {
6732
+ type: "checkbox",
6733
+ checked: useOutline,
6734
+ onChange: (e) => {
6735
+ const enabled = e.target.checked;
6736
+ setUseOutline(enabled);
6737
+ const nextColors = enabled ? {
6738
+ ...colors,
6739
+ outlineColor: colors.outlineColor || CAPTION_COLOR.outlineColor
6740
+ } : { ...colors };
6741
+ setColors(nextColors);
6742
+ handleUpdateCaption({
6743
+ colors: nextColors,
6744
+ useOutlineOverride: enabled
6745
+ });
6746
+ },
6747
+ className: "checkbox-purple"
6748
+ }
6749
+ ),
6750
+ "Use Outline Color"
6751
+ ] }) }),
6752
+ currentColorMeta.usedColors.map((key) => renderColorControl(key))
6678
6753
  ] })
6679
6754
  ] })
6680
6755
  ] });