From bc70594885cdc97fce5cfc5111962caec8ccf9bd Mon Sep 17 00:00:00 2001 From: steveseguin Date: Wed, 22 Apr 2026 10:44:55 -0400 Subject: [PATCH] Add vMix overlay style options --- README.md | 64 ++++- manual.html | 22 +- overlay-style-params.js | 498 +++++++++++++++++++++++++++++++++++++ overlay.html | 11 +- overlay_enhanced.html | 9 +- overlay_enhanced_readme.md | 11 + sitemap.xml | 28 ++- vmix.html | 57 +++++ 8 files changed, 665 insertions(+), 35 deletions(-) create mode 100644 overlay-style-params.js create mode 100644 vmix.html diff --git a/README.md b/README.md index 57b459d..7ff5d16 100644 --- a/README.md +++ b/README.md @@ -332,13 +332,63 @@ Examples: .../overlay?room=abc123&showtime=1500 # Faster inactivity clear .../overlay?room=abc123&maxlines=3 # Keep last 3 lines while active .../overlay?room=abc123&maxage=10000 # Drop lines older than 10s -.../overlay?room=abc123&maxlines=2&maxage=8000 # Combine both -.../overlay?room=abc123&intermclear=1 # Only show the interim/current line -``` - -### Interim Text Handling - -Browser speech recognition sometimes produces very long "interim" (non-finalized) results without breaking them into sentences. This is common when captioning talk shows, pre-recorded audio, or streams with continuous speech. The following options help manage long interim text blocks: +.../overlay?room=abc123&maxlines=2&maxage=8000 # Combine both +.../overlay?room=abc123&intermclear=1 # Only show the interim/current line +``` + +### Overlay Style Parameters + +The default overlay styling is unchanged. Add these optional URL parameters to customize the visual style in OBS, vMix, or another browser-source tool: + +| Parameter | Description | Example | +|-----------|-------------|---------| +| `&nobg` / `&bg=transparent` | Remove the black caption highlight behind text. | `&nobg` | +| `&bg=COLOR` | Set the caption highlight/background color. | `&bg=rgba(0,0,0,0.6)` | +| `&color=COLOR` / `&fontcolor=COLOR` | Set caption text color. | `&color=%23fff200` | +| `&fontsize=SIZE` / `&fs=SIZE` | Set text size. Numbers up to 10 use `em`; larger numbers use `px`. | `&fontsize=4em` | +| `&scale=N` | Scale the default font size. | `&scale=1.25` | +| `&font=FAMILY` | Set the font family. URL-encode spaces or quotes as needed. | `&font=Arial,sans-serif` | +| `&fontweight=VALUE` | Set font weight. | `&fontweight=700` | +| `&lineheight=VALUE` | Set line height. | `&lineheight=1.2` | +| `&letterspacing=SIZE` | Set letter spacing. | `&letterspacing=0.04em` | +| `&pad=SIZE` / `&padding=SIZE` | Set caption text padding. | `&pad=0` | +| `&padx=SIZE` / `&pady=SIZE` | Set horizontal or vertical caption padding. | `&padx=12&pady=4` | +| `&radius=SIZE` / `&rounded=SIZE` | Round caption highlight corners. | `&radius=6` | +| `&borderwidth=SIZE&bordercolor=COLOR` | Add a border around caption text spans. | `&borderwidth=2&bordercolor=%23fff` | +| `&shadow=none` / `&noshadow` | Remove the text shadow. | `&noshadow` | +| `&shadow=VALUE` | Replace the text shadow. URL-encode spaces. | `&shadow=2px%202px%204px%20%23000` | +| `&shadowcolor=COLOR` | Keep the default shadow size but change its color. | `&shadowcolor=%23000` | +| `&outline=SIZE&outlinecolor=COLOR` | Use a simple text outline via text-shadow. | `&outline=2&outlinecolor=%23000` | +| `&align=left|center|right` | Align caption lines horizontally. | `&align=center` | +| `&valign=top|middle|bottom` | Position the overlay vertically. | `&valign=top` | +| `&topoffset=SIZE` / `&bottomoffset=SIZE` | Offset the overlay from the top or bottom. | `&bottomoffset=40` | +| `&leftoffset=SIZE` / `&rightoffset=SIZE` | Offset the overlay horizontally. | `&leftoffset=30` | +| `&nowrap` | Keep each caption on one visual line with ellipsis overflow. | `&nowrap` | +| `&uppercase` / `&lowercase` | Transform caption text case. | `&uppercase` | +| `&opacity=N` | Set overall caption opacity. `0.0` to `1.0`, or `0` to `100`. | `&opacity=0.85` | +| `&bgopacity=N` | Apply opacity to hex/rgb caption backgrounds. | `&bg=%23000&bgopacity=0.5` | +| `&labelcolor=COLOR` / `&labelbg=COLOR` | Style speaker labels when labels are present. | `&labelcolor=%23ffd500` | +| `&translationcolor=COLOR` / `&originalcolor=COLOR` | Style dual-overlay translation/original lines. | `&originalcolor=%23ccc` | +| `&css=URL` | Load an external stylesheet for advanced customization. | `&css=https://example.com/captions.css` | +| `&base64css=...` | Inject base64-encoded CSS for advanced customization. | `&base64css=I291dHB1dHt9` | + +Examples: +``` +.../overlay.html?room=abc123&nobg&shadow=2px%202px%204px%20%23000 +.../overlay.html?room=abc123&bg=rgba(0,0,0,0.45)&radius=6&padx=12&pady=4 +.../overlay.html?room=abc123&color=%23fff200&fontsize=48px&align=center +``` + +For vMix users who want the black text highlight removed by default, use: +``` +.../vmix.html?room=abc123 +``` + +`vmix.html` forwards to `overlay.html` with `&nobg=1` unless a background/highlight parameter is already provided. Use `&bg=black` or another `&bg=COLOR` value to restore a caption highlight on that page. + +### Interim Text Handling + +Browser speech recognition sometimes produces very long "interim" (non-finalized) results without breaking them into sentences. This is common when captioning talk shows, pre-recorded audio, or streams with continuous speech. The following options help manage long interim text blocks: #### Capture Page Options (index.html & translate.html) diff --git a/manual.html b/manual.html index 72cf37a..efca044 100644 --- a/manual.html +++ b/manual.html @@ -71,10 +71,14 @@
-
This is a free-to-use captioning tool.
-


- The output of this app is mirrored here: *ERROR GENERATING LINK*. Add it to OBS as a browser source overlay if wishing to use it for a live stream. -
+
This is a free-to-use captioning tool.
+


+ The output of this app is mirrored here: *ERROR GENERATING LINK*. Add it to OBS as a browser source overlay if wishing to use it for a live stream. +

+ For vMix, use this no-highlight overlay link: *ERROR GENERATING LINK*. +

+ Overlay style options include &nobg or &bg=transparent to remove the black highlight, &bg=COLOR to set it, &color=COLOR, &fontsize=SIZE, &scale=N, &align=center, &valign=top, &shadow=none, and &outline=2&outlinecolor=%23000. +


@@ -170,10 +174,12 @@ var counter=0; var url = document.URL.substr(0,document.URL.lastIndexOf('/')); - document.getElementById("shareLink").href= url+"/overlay?room="+roomID; - document.getElementById("shareLink").innerHTML = url+"/overlay?room="+roomID; - - navigator.clipboard.writeText(url+"/overlay?room="+roomID).then(() => { + document.getElementById("shareLink").href= url+"/overlay?room="+roomID; + document.getElementById("shareLink").innerHTML = url+"/overlay?room="+roomID; + document.getElementById("vmixLink").href= url+"/vmix.html?room="+roomID; + document.getElementById("vmixLink").innerHTML = url+"/vmix.html?room="+roomID; + + navigator.clipboard.writeText(url+"/overlay?room="+roomID).then(() => { /* clipboard successfully set */ }, () => { /* clipboard write failed */ diff --git a/overlay-style-params.js b/overlay-style-params.js new file mode 100644 index 0000000..4a297d6 --- /dev/null +++ b/overlay-style-params.js @@ -0,0 +1,498 @@ +(function () { + "use strict"; + + var params; + try { + params = window.urlParams || new URLSearchParams(window.location.search); + } catch (e) { + return; + } + + function hasValue(value) { + return value !== null && value !== undefined && String(value).trim() !== ""; + } + + function isOff(value) { + if (!hasValue(value)) { + return false; + } + return /^(0|false|off|no|none|disable|disabled)$/i.test(String(value).trim()); + } + + function getParam(names) { + for (var i = 0; i < names.length; i++) { + if (params.has(names[i])) { + return params.get(names[i]); + } + } + return null; + } + + function hasParam(names) { + for (var i = 0; i < names.length; i++) { + if (params.has(names[i])) { + return true; + } + } + return false; + } + + function hasEnabledParam(names) { + for (var i = 0; i < names.length; i++) { + if (params.has(names[i]) && !isOff(params.get(names[i]))) { + return true; + } + } + return false; + } + + function safeCssValue(value, property) { + if (!hasValue(value)) { + return null; + } + value = String(value).trim(); + if (/[{};<>]/.test(value)) { + return null; + } + if (window.CSS && window.CSS.supports && !window.CSS.supports(property, value)) { + return null; + } + return value; + } + + function normalizeColor(value) { + if (!hasValue(value)) { + return null; + } + value = String(value).trim(); + if (isOff(value)) { + return "transparent"; + } + if (/^[0-9a-f]{3,4}$/i.test(value) || /^[0-9a-f]{6}([0-9a-f]{2})?$/i.test(value)) { + value = "#" + value; + } + if (/[{};<>]/.test(value)) { + return null; + } + if (window.CSS && window.CSS.supports && window.CSS.supports("color", value)) { + return value; + } + if (/^#[0-9a-f]{3,8}$/i.test(value) || /^rgba?\([^)]+\)$/i.test(value) || /^hsla?\([^)]+\)$/i.test(value) || /^(transparent|currentcolor)$/i.test(value)) { + return value; + } + return null; + } + + function cssLength(value, defaultUnit) { + if (!hasValue(value)) { + return null; + } + value = String(value).trim(); + if (/[{};<>]/.test(value)) { + return null; + } + if (/^-?[\d.]+$/.test(value)) { + value += defaultUnit || "px"; + } + if (window.CSS && window.CSS.supports && !window.CSS.supports("width", value) && !window.CSS.supports("font-size", value)) { + return null; + } + return value; + } + + function cssFontSize(value) { + if (!hasValue(value)) { + return null; + } + value = String(value).trim(); + if (/^-?[\d.]+$/.test(value)) { + var numeric = parseFloat(value); + value = (Math.abs(numeric) > 10) ? numeric + "px" : numeric + "em"; + } + return safeCssValue(value, "font-size"); + } + + function cssBoxValue(value, defaultUnit) { + if (!hasValue(value)) { + return null; + } + value = String(value).trim().replace(/,/g, " "); + if (/[{};<>]/.test(value)) { + return null; + } + var parts = value.split(/\s+/).filter(Boolean); + if (parts.length < 1 || parts.length > 4) { + return null; + } + for (var i = 0; i < parts.length; i++) { + parts[i] = cssLength(parts[i], defaultUnit || "px"); + if (!parts[i]) { + return null; + } + } + return parts.join(" "); + } + + function cssFontFamily(value) { + if (!hasValue(value)) { + return null; + } + value = String(value).trim(); + if (/[{};<>]/.test(value)) { + return null; + } + if (window.CSS && window.CSS.supports && !window.CSS.supports("font-family", value)) { + return null; + } + return value; + } + + function parseOpacity(value) { + if (!hasValue(value)) { + return null; + } + var opacity = parseFloat(value); + if (isNaN(opacity)) { + return null; + } + if (opacity > 1) { + opacity = opacity / 100; + } + return Math.max(0, Math.min(1, opacity)); + } + + function withOpacity(color, opacity) { + if (!hasValue(color) || opacity === null || color === "transparent") { + return null; + } + var match; + color = String(color).trim(); + if (/^#[0-9a-f]{3,4}$/i.test(color)) { + var shortHex = color.slice(1); + return "rgba(" + + parseInt(shortHex[0] + shortHex[0], 16) + ", " + + parseInt(shortHex[1] + shortHex[1], 16) + ", " + + parseInt(shortHex[2] + shortHex[2], 16) + ", " + opacity + ")"; + } + if (/^#[0-9a-f]{6}([0-9a-f]{2})?$/i.test(color)) { + var hex = color.slice(1); + return "rgba(" + + parseInt(hex.slice(0, 2), 16) + ", " + + parseInt(hex.slice(2, 4), 16) + ", " + + parseInt(hex.slice(4, 6), 16) + ", " + opacity + ")"; + } + match = color.match(/^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i); + if (match) { + return "rgba(" + match[1] + ", " + match[2] + ", " + match[3] + ", " + opacity + ")"; + } + return null; + } + + function negativeLength(value) { + if (value === "0" || value === "0px" || value === "0em") { + return "0"; + } + return String(value).charAt(0) === "-" ? String(value).slice(1) : "-" + value; + } + + function buildOutline(width, color) { + var neg = negativeLength(width); + return [ + neg + " " + neg + " 0 " + color, + "0 " + neg + " 0 " + color, + width + " " + neg + " 0 " + color, + neg + " 0 0 " + color, + width + " 0 0 " + color, + neg + " " + width + " 0 " + color, + "0 " + width + " 0 " + color, + width + " " + width + " 0 " + color + ].join(", "); + } + + var rules = {}; + function addRule(selector, property, value) { + if (!hasValue(value)) { + return; + } + if (!rules[selector]) { + rules[selector] = []; + } + rules[selector].push(property + ": " + value + " !important"); + } + + function addCss(cssText) { + if (!hasValue(cssText)) { + return; + } + var style = document.createElement("style"); + style.type = "text/css"; + style.textContent = cssText; + document.head.appendChild(style); + } + + var outputSelector = "#output, #interm"; + var spanSelector = "#output span, #interm span"; + var labelSelector = "#output .label, #interm .label"; + + var noBg = hasEnabledParam(["nobg", "no-bg", "nobox", "nohighlight", "no-highlight", "transparentbg", "transparent-bg"]); + var bgParam = getParam(["bg", "background", "captionbg", "captionbackground", "highlight", "highlightcolor", "boxcolor"]); + var bg = noBg ? "transparent" : normalizeColor(bgParam); + if (hasValue(bgParam) && isOff(bgParam)) { + bg = "transparent"; + } + var bgOpacity = parseOpacity(getParam(["bgopacity", "backgroundopacity", "boxopacity", "highlightopacity"])); + var bgWithOpacity = withOpacity(bg, bgOpacity); + if (bgWithOpacity) { + bg = bgWithOpacity; + } + if (bg) { + addRule(spanSelector, "background-color", bg); + addRule(spanSelector, "background", bg); + } + + var pageBg = normalizeColor(getParam(["pagebg", "bodybg", "screenbg", "canvasbg"])); + if (hasEnabledParam(["transparent", "transparency"])) { + pageBg = "transparent"; + } + if (pageBg) { + addRule("html, body", "background-color", pageBg); + } + + var textColor = normalizeColor(getParam(["color", "fg", "fontcolor", "textcolor"])); + if (textColor) { + addRule(outputSelector, "color", textColor); + } + + var fontFamily = cssFontFamily(getParam(["font", "fontfamily", "font-family"])); + if (fontFamily) { + addRule(outputSelector, "font-family", fontFamily); + } + + var fontSize = cssFontSize(getParam(["fontsize", "font-size", "fs", "size"])); + var scale = parseFloat(getParam(["scale", "zoom"])); + if (!fontSize && !isNaN(scale) && scale > 0) { + fontSize = (3.2 * scale).toFixed(3) + "em"; + } + if (fontSize) { + addRule(outputSelector, "font-size", fontSize); + } + + var fontWeight = safeCssValue(getParam(["fontweight", "font-weight", "weight"]), "font-weight"); + if (fontWeight) { + addRule(outputSelector, "font-weight", fontWeight); + } + + var lineHeight = safeCssValue(getParam(["lineheight", "line-height", "lh"]), "line-height"); + if (lineHeight) { + addRule(outputSelector, "line-height", lineHeight); + } + + var letterSpacing = cssLength(getParam(["letterspacing", "letter-spacing", "spacing", "ls"]), "em"); + if (letterSpacing) { + addRule(outputSelector, "letter-spacing", letterSpacing); + } + + var textTransform = getParam(["texttransform", "text-transform", "transform"]); + if (hasEnabledParam(["uppercase", "upper"])) { + textTransform = "uppercase"; + } else if (hasEnabledParam(["lowercase", "lower"])) { + textTransform = "lowercase"; + } else if (hasEnabledParam(["capitalize"])) { + textTransform = "capitalize"; + } + textTransform = safeCssValue(textTransform, "text-transform"); + if (textTransform) { + addRule(outputSelector, "text-transform", textTransform); + } + + var padding = cssBoxValue(getParam(["padding", "pad", "textpadding", "textpad"]), "px"); + if (padding) { + addRule(spanSelector, "padding", padding); + } + var padX = cssLength(getParam(["padx", "paddingx"]), "px"); + var padY = cssLength(getParam(["pady", "paddingy"]), "px"); + if (padX) { + addRule(spanSelector, "padding-left", padX); + addRule(spanSelector, "padding-right", padX); + } + if (padY) { + addRule(spanSelector, "padding-top", padY); + addRule(spanSelector, "padding-bottom", padY); + } + + var radius = cssLength(getParam(["radius", "rounded", "borderradius", "border-radius"]), "px"); + if (radius) { + addRule(spanSelector, "border-radius", radius); + } + + var borderColor = normalizeColor(getParam(["bordercolor", "border-color"])); + var borderWidth = cssLength(getParam(["borderwidth", "border-width", "border"]), "px"); + if (borderColor || borderWidth) { + addRule(spanSelector, "border-style", safeCssValue(getParam(["borderstyle", "border-style"]) || "solid", "border-style")); + addRule(spanSelector, "border-width", borderWidth || "1px"); + addRule(spanSelector, "border-color", borderColor || "currentColor"); + } + + var captionOpacity = parseOpacity(getParam(["opacity", "captionopacity", "textopacity"])); + if (captionOpacity !== null) { + addRule(outputSelector, "opacity", captionOpacity); + } + + var maxWidth = cssLength(getParam(["maxwidth", "max-width"]), "px"); + if (maxWidth) { + addRule(spanSelector, "max-width", maxWidth); + } + var width = cssLength(getParam(["width"]), "px"); + if (width) { + addRule(outputSelector, "width", width); + } + + var align = getParam(["align", "textalign", "text-align"]); + if (hasEnabledParam(["center", "centre"])) { + align = "center"; + } else if (hasEnabledParam(["right", "alignright"])) { + align = "right"; + } else if (hasEnabledParam(["left", "alignleft"])) { + align = "left"; + } + if (/^(left|right|center|justify)$/i.test(String(align || ""))) { + addRule("body", "left", "0"); + addRule("body", "right", "0"); + addRule("body", "width", "auto"); + addRule(outputSelector, "text-align", align.toLowerCase()); + } + + var valign = getParam(["valign", "vertical", "position"]); + if (hasEnabledParam(["top", "aligntop"])) { + valign = "top"; + } else if (hasEnabledParam(["middle", "alignmiddle"])) { + valign = "middle"; + } else if (hasEnabledParam(["alignbottom"])) { + valign = "bottom"; + } + if (/^(top|middle|center|bottom)$/i.test(String(valign || ""))) { + valign = valign.toLowerCase(); + if (valign === "top") { + addRule("body", "top", "0"); + addRule("body", "bottom", "auto"); + addRule("body", "flex-direction", "column"); + } else if (valign === "middle" || valign === "center") { + addRule("body", "top", "50%"); + addRule("body", "bottom", "auto"); + addRule("body", "transform", "translateY(-50%)"); + addRule("body", "flex-direction", "column"); + } else if (valign === "bottom") { + addRule("body", "top", "auto"); + addRule("body", "bottom", "0"); + } + } + + var topPos = cssLength(getParam(["toppos", "topoffset"]), "px"); + var bottomPos = cssLength(getParam(["bottompos", "bottomoffset"]), "px"); + var leftPos = cssLength(getParam(["leftpos", "leftoffset"]), "px"); + var rightPos = cssLength(getParam(["rightpos", "rightoffset"]), "px"); + if (topPos) { + addRule("body", "top", topPos); + addRule("body", "bottom", "auto"); + } + if (bottomPos) { + addRule("body", "bottom", bottomPos); + addRule("body", "top", "auto"); + } + if (leftPos) { + addRule("body", "left", leftPos); + } + if (rightPos) { + addRule("body", "right", rightPos); + } + + if (hasEnabledParam(["nowrap", "singleline"])) { + addRule(outputSelector, "white-space", "nowrap"); + addRule(outputSelector, "overflow", "hidden"); + addRule(spanSelector, "display", "inline-block"); + addRule(spanSelector, "overflow", "hidden"); + addRule(spanSelector, "text-overflow", "ellipsis"); + } else if (hasEnabledParam(["wrap"])) { + addRule(outputSelector, "white-space", "normal"); + } + + if (hasEnabledParam(["block", "inlineblock", "inline-block"])) { + addRule(spanSelector, "display", "inline-block"); + } + + var shadowParam = getParam(["shadow", "textshadow", "text-shadow"]); + var noShadow = hasEnabledParam(["noshadow", "no-shadow"]) || (hasParam(["shadow", "textshadow", "text-shadow"]) && isOff(shadowParam)); + if (noShadow) { + addRule(outputSelector, "text-shadow", "none"); + } else { + var shadowValue = safeCssValue(shadowParam, "text-shadow"); + if (shadowValue && !/^(1|true|on|yes)$/i.test(shadowValue)) { + addRule(outputSelector, "text-shadow", shadowValue); + } else { + var shadowColor = normalizeColor(getParam(["shadowcolor", "shadow-color"])); + var shadowX = cssLength(getParam(["shadowx", "shadow-x"]), "px"); + var shadowY = cssLength(getParam(["shadowy", "shadow-y"]), "px"); + var shadowBlur = cssLength(getParam(["shadowblur", "shadow-blur"]), "px"); + if (shadowColor || shadowX || shadowY || shadowBlur) { + addRule(outputSelector, "text-shadow", (shadowX || "0.05em") + " " + (shadowY || "0.05em") + " " + (shadowBlur || "0px") + " " + (shadowColor || "rgba(0,0,0,1)")); + } + } + } + + var outlineParam = getParam(["outline", "outlinewidth", "outline-width", "stroke", "strokewidth", "stroke-width"]); + if (hasEnabledParam(["nooutline", "no-outline"])) { + addRule(outputSelector, "text-shadow", "none"); + } else if (hasValue(outlineParam) && !isOff(outlineParam)) { + var outlineWidth = cssLength(outlineParam === "" ? "2" : outlineParam, "px"); + var outlineColor = normalizeColor(getParam(["outlinecolor", "outline-color", "strokecolor", "stroke-color"])) || "#000"; + if (outlineWidth && outlineColor) { + addRule(outputSelector, "text-shadow", buildOutline(outlineWidth, outlineColor)); + } + } + + var labelColor = normalizeColor(getParam(["labelcolor", "label-color"])); + var labelBg = normalizeColor(getParam(["labelbg", "labelbackground", "label-bg"])); + if (labelColor) { + addRule(labelSelector, "color", labelColor); + } + if (labelBg) { + addRule(labelSelector, "background-color", labelBg); + addRule(labelSelector, "background", labelBg); + } + + var translationColor = normalizeColor(getParam(["translationcolor", "translation-color"])); + var originalColor = normalizeColor(getParam(["originalcolor", "original-color"])); + if (translationColor) { + addRule("#output .dual-translation, #interm .dual-translation", "color", translationColor); + } + if (originalColor) { + addRule("#output .dual-original, #interm .dual-original", "color", originalColor); + } + + var css = ""; + for (var selector in rules) { + if (Object.prototype.hasOwnProperty.call(rules, selector) && rules[selector].length) { + css += selector + " { " + rules[selector].join("; ") + "; }\n"; + } + } + if (css) { + addCss(css); + } + + var cssUrl = getParam(["css", "customcssurl", "styleurl"]); + if (hasValue(cssUrl) && !/[<>"']/.test(cssUrl)) { + var link = document.createElement("link"); + link.rel = "stylesheet"; + link.href = cssUrl; + document.head.appendChild(link); + } + + var cssBase64 = getParam(["base64css", "b64css", "cssbase64", "cssb64"]); + if (hasValue(cssBase64)) { + try { + addCss(atob(cssBase64)); + } catch (e) { + console.warn("Unable to decode overlay CSS parameter", e); + } + } +})(); diff --git a/overlay.html b/overlay.html index 3b76891..a4230b5 100644 --- a/overlay.html +++ b/overlay.html @@ -1258,8 +1258,9 @@ var bridgeUrl = window.location.origin + window.location.pathname.replace(/\/[^^/]*$/, '') + '/tts.rocks/caption-bridge.html?' + qs.toString(); window.open(bridgeUrl, '_blank', 'noopener'); } - } catch (e) { console.warn(e); } - - - - + } catch (e) { console.warn(e); } + + + + + diff --git a/overlay_enhanced.html b/overlay_enhanced.html index ecee190..ec8dcf3 100644 --- a/overlay_enhanced.html +++ b/overlay_enhanced.html @@ -1016,8 +1016,9 @@ socket.onclose = function (){ setTimeout(function(){window.location.reload(true);},100); - }; - - - + }; + + + + diff --git a/overlay_enhanced_readme.md b/overlay_enhanced_readme.md index 9c296e3..524e53c 100644 --- a/overlay_enhanced_readme.md +++ b/overlay_enhanced_readme.md @@ -30,6 +30,17 @@ This enhanced overlay supports local Mozilla Bergamot translation plus remote pr - `intermclear=1` clears finished captions while interim text streams. - `dual=1` (or `view=dual`) shows translated text above original text. +### 6. Overlay Style Parameters +- Existing overlay defaults are unchanged unless a style parameter is added. +- `nobg=1` or `bg=transparent` removes the black caption highlight. +- `bg=COLOR`, `color=COLOR`, `fontcolor=COLOR`, `fontsize=SIZE`, `scale=N`, `font=FAMILY`, `fontweight=VALUE`, `lineheight=VALUE`, and `letterspacing=SIZE` customize text appearance. +- `pad=SIZE`, `padx=SIZE`, `pady=SIZE`, `radius=SIZE`, `borderwidth=SIZE`, and `bordercolor=COLOR` customize the caption highlight box. +- `shadow=none`, `noshadow=1`, `shadow=VALUE`, `shadowcolor=COLOR`, `outline=SIZE`, and `outlinecolor=COLOR` customize text shadow/outline. +- `align=left|center|right`, `valign=top|middle|bottom`, `topoffset=SIZE`, `bottomoffset=SIZE`, `leftoffset=SIZE`, and `rightoffset=SIZE` customize placement. +- `nowrap=1`, `uppercase=1`, `lowercase=1`, `opacity=N`, `bgopacity=N`, `labelcolor=COLOR`, `labelbg=COLOR`, `translationcolor=COLOR`, and `originalcolor=COLOR` provide additional display control. +- `css=URL` and `base64css=...` allow advanced custom styles. +- `vmix.html?room=abc123` is a vMix-friendly shortcut that forwards to `overlay.html` with `nobg=1` unless a background/highlight parameter is already provided. + ## Examples ### Google Cloud (Backward Compatible) diff --git a/sitemap.xml b/sitemap.xml index 78758b0..2a7abc4 100644 --- a/sitemap.xml +++ b/sitemap.xml @@ -24,17 +24,23 @@ weekly 0.8 - - https://caption.ninja/overlay - 2025-01-12 - monthly - 0.7 - - - https://caption.ninja/capture-pro - 2025-01-12 - weekly - 0.8 + + https://caption.ninja/overlay + 2025-01-12 + monthly + 0.7 + + + https://caption.ninja/vmix + 2026-04-22 + monthly + 0.6 + + + https://caption.ninja/capture-pro + 2025-01-12 + weekly + 0.8 https://caption.ninja/manual diff --git a/vmix.html b/vmix.html new file mode 100644 index 0000000..e66568a --- /dev/null +++ b/vmix.html @@ -0,0 +1,57 @@ + + + + + + + + + Caption Overlay Window for vMix + + + + + + + + +