mirror of
https://github.com/steveseguin/captionninja.git
synced 2026-10-09 17:49:30 +00:00
Add vMix overlay style options
This commit is contained in:
@@ -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)
|
||||
|
||||
|
||||
+14
-8
@@ -71,10 +71,14 @@
|
||||
<body>
|
||||
<input type="textarea" id="userinput" />
|
||||
<div id="interm" class="output">
|
||||
<font style='color:black;font-size:60%;line-height: 1.4em;' ><div >This is a free-to-use captioning tool.</div></font><font style='color:black;font-size:30%;line-height: 1.4em;' >
|
||||
<div ><br /><br />
|
||||
The output of this app is mirrored here: <a id="shareLink" href="overlay.html" target='_blank'>*ERROR GENERATING LINK*</a>. Add it to OBS as a browser source overlay if wishing to use it for a live stream.
|
||||
</div></font>
|
||||
<font style='color:black;font-size:60%;line-height: 1.4em;' ><div >This is a free-to-use captioning tool.</div></font><font style='color:black;font-size:30%;line-height: 1.4em;' >
|
||||
<div ><br /><br />
|
||||
The output of this app is mirrored here: <a id="shareLink" href="overlay.html" target='_blank'>*ERROR GENERATING LINK*</a>. Add it to OBS as a browser source overlay if wishing to use it for a live stream.
|
||||
<br /><br />
|
||||
For vMix, use this no-highlight overlay link: <a id="vmixLink" href="vmix.html" target='_blank'>*ERROR GENERATING LINK*</a>.
|
||||
<br /><br />
|
||||
Overlay style options include <code>&nobg</code> or <code>&bg=transparent</code> to remove the black highlight, <code>&bg=COLOR</code> to set it, <code>&color=COLOR</code>, <code>&fontsize=SIZE</code>, <code>&scale=N</code>, <code>&align=center</code>, <code>&valign=top</code>, <code>&shadow=none</code>, and <code>&outline=2&outlinecolor=%23000</code>.
|
||||
</div></font>
|
||||
<br /><br />
|
||||
</div>
|
||||
<div id="output" class="output"></div>
|
||||
@@ -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 */
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
})();
|
||||
+6
-5
@@ -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); }
|
||||
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
} catch (e) { console.warn(e); }
|
||||
|
||||
</script>
|
||||
<script src="overlay-style-params.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1016,8 +1016,9 @@
|
||||
|
||||
socket.onclose = function (){
|
||||
setTimeout(function(){window.location.reload(true);},100);
|
||||
};
|
||||
|
||||
</script>
|
||||
</body>
|
||||
};
|
||||
|
||||
</script>
|
||||
<script src="overlay-style-params.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -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)
|
||||
|
||||
+17
-11
@@ -24,17 +24,23 @@
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://caption.ninja/overlay</loc>
|
||||
<lastmod>2025-01-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://caption.ninja/capture-pro</loc>
|
||||
<lastmod>2025-01-12</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
<url>
|
||||
<loc>https://caption.ninja/overlay</loc>
|
||||
<lastmod>2025-01-12</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.7</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://caption.ninja/vmix</loc>
|
||||
<lastmod>2026-04-22</lastmod>
|
||||
<changefreq>monthly</changefreq>
|
||||
<priority>0.6</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://caption.ninja/capture-pro</loc>
|
||||
<lastmod>2025-01-12</lastmod>
|
||||
<changefreq>weekly</changefreq>
|
||||
<priority>0.8</priority>
|
||||
</url>
|
||||
<url>
|
||||
<loc>https://caption.ninja/manual</loc>
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta content="text/html;charset=utf-8" http-equiv="Content-Type" />
|
||||
<meta content="utf-8" http-equiv="encoding" />
|
||||
<meta name="copyright" content="© 2020 Steve Seguin" />
|
||||
<link rel="icon" type="image/svg+xml" href="favicon.svg" />
|
||||
<link rel="shortcut icon" href="favicon.svg" />
|
||||
<title>Caption Overlay Window for vMix</title>
|
||||
<meta name="title" content="CAPTION.Ninja: vMix Overlay" />
|
||||
<meta name="description" content="vMix-friendly Caption Ninja overlay shortcut with transparent caption backing by default." />
|
||||
<meta name="author" content="Steve Seguin" />
|
||||
<style>
|
||||
html, body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: transparent;
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<script>
|
||||
(function () {
|
||||
var params = new URLSearchParams(window.location.search);
|
||||
var backgroundParams = [
|
||||
"nobg",
|
||||
"no-bg",
|
||||
"nobox",
|
||||
"nohighlight",
|
||||
"no-highlight",
|
||||
"transparentbg",
|
||||
"transparent-bg",
|
||||
"bg",
|
||||
"background",
|
||||
"captionbg",
|
||||
"captionbackground",
|
||||
"highlight",
|
||||
"highlightcolor",
|
||||
"boxcolor"
|
||||
];
|
||||
var hasBackgroundChoice = backgroundParams.some(function (param) {
|
||||
return params.has(param);
|
||||
});
|
||||
if (!hasBackgroundChoice) {
|
||||
params.set("nobg", "1");
|
||||
}
|
||||
|
||||
var target = "overlay.html";
|
||||
var query = params.toString();
|
||||
window.location.replace(target + (query ? "?" + query : ""));
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user