diff --git a/client/src/App.jsx b/client/src/App.jsx
index fd461f721..725681327 100644
--- a/client/src/App.jsx
+++ b/client/src/App.jsx
@@ -17,7 +17,9 @@ const StageManager = lazy(() =>
import('./features/viewers/backstage/StageManager')
);
const Public = lazy(() => import('./features/viewers/foh/Public'));
-const Lower = lazy(() => import('./features/viewers/production/Lower'));
+const Lower = lazy(() =>
+ import('./features/viewers/production/lower/LowerWrapper')
+);
const Pip = lazy(() => import('./features/viewers/production/Pip'));
const queryClient = new QueryClient();
diff --git a/client/src/features/viewers/production/lower/LowerClean.jsx b/client/src/features/viewers/production/lower/LowerClean.jsx
new file mode 100644
index 000000000..780a3e1ef
--- /dev/null
+++ b/client/src/features/viewers/production/lower/LowerClean.jsx
@@ -0,0 +1,131 @@
+import { AnimatePresence, motion } from 'framer-motion';
+import { useEffect, useState } from 'react';
+import style from './LowerClean.module.css';
+
+export default function LowerClean(props) {
+ const { lower, title, options } = props;
+ const defaults = {
+ size: 1,
+ transitionIn: 3,
+ textColour: '#fffffa',
+ posX: 50,
+ posY: 700,
+ };
+
+ const [showLower, setShowLower] = useState(true);
+ // Unmount if fadeOut
+ useEffect(() => {
+ if (!options.fadeOut) return;
+
+ // Calculate time
+ const fadeOutTime =
+ (parseInt(options.fadeOut) +
+ (options.transitionIn || defaults.transitionIn)) *
+ 1000;
+ if (isNaN(fadeOutTime)) return;
+
+ const timeout = setTimeout(() => {
+ setShowLower(false);
+ }, fadeOutTime);
+
+ return () => clearTimeout(timeout);
+ }, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
+
+ // calculate transition times
+ useEffect(() => {});
+ // Format messages
+ const showLowerMessage = lower.text !== '' && lower.visible;
+
+ // motion
+ // transition segments
+ const t = options.transitionIn || defaults.transitionIn;
+ const quarter = t / 4;
+ const third = t / 3;
+ const half = t / 2;
+
+ const lowerThirdVariants = {
+ hidden: {
+ opacity: 0,
+ },
+ visible: {
+ opacity: 1,
+ transition: {
+ duration: third,
+ },
+ },
+ exit: {
+ opacity: 0,
+ transition: {
+ duration: third,
+ },
+ },
+ };
+
+ const titleVariants = {
+ hidden: {
+ opacity: 0,
+ },
+ visible: {
+ opacity: 1,
+ transition: {
+ delay: quarter,
+ duration: half,
+ },
+ },
+ };
+
+ const sizeMultiplier = (options.size || 1) * 4;
+
+ return (
+
+
+ {showLower && (
+
+
+ {title.titleNow}
+
+
+ {title.presenterNow}
+
+
+ )}
+
+
+
+ {showLowerMessage && (
+
+ {lower.text}
+
+ )}
+
+
+ );
+}
diff --git a/client/src/features/viewers/production/lower/LowerClean.module.css b/client/src/features/viewers/production/lower/LowerClean.module.css
new file mode 100644
index 000000000..409a473c3
--- /dev/null
+++ b/client/src/features/viewers/production/lower/LowerClean.module.css
@@ -0,0 +1,27 @@
+.lowerThird {
+ margin: 0;
+ box-sizing: border-box; /* reset */
+ overflow: hidden;
+ width: 100%; /* restrict the page width to viewport */
+ height: 100%;
+
+ color: #fffffa;
+ font-size: 4vh;
+}
+
+.lowerContainer {
+ position: absolute;
+ padding: 1vh 2vh;
+ border-radius: 1vh;
+}
+
+.messageContainer {
+ position: absolute;
+ bottom: 2vh;
+ width: 100%;
+ text-align: center;
+}
+
+.message {
+ font-size: 3.5vh;
+}
diff --git a/client/src/features/viewers/production/Lower.jsx b/client/src/features/viewers/production/lower/LowerLines.jsx
similarity index 69%
rename from client/src/features/viewers/production/Lower.jsx
rename to client/src/features/viewers/production/lower/LowerLines.jsx
index 7fe81f8ff..7c2c28d66 100644
--- a/client/src/features/viewers/production/Lower.jsx
+++ b/client/src/features/viewers/production/lower/LowerLines.jsx
@@ -1,36 +1,26 @@
import { AnimatePresence, motion } from 'framer-motion';
import { useEffect, useState } from 'react';
-import style from './Lower.module.css';
+import style from './LowerLines.module.css';
-export default function Lower(props) {
- const { lower, title, time, general } = props;
+export default function LowerLines(props) {
+ const { lower, title, options } = props;
+ const defaults = {
+ size: 1,
+ transitionIn: 3,
+ textColour: '#fffffa',
+ bgColour: '#00000033',
+ };
const [showLower, setShowLower] = useState(true);
- // Set window title
+ // Unmount if fadeOut
useEffect(() => {
- document.title = 'ontime - Lower Thirds';
- }, []);
-
- // TODO: sanitize data
- // getting config from URL: preset, size, transition, bg, text, key
- // eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
- let params = new URLSearchParams(props.location.search);
-
- const preset = params.get('preset') ? params.get('preset') : 1;
-
- const size = params.get('size') ? params.get('size') : 1;
- const transitionIn = params.get('transition') ? params.get('transition') : 3;
- const textColour = params.get('text') ? `#${params.get('text')}` : '#fffffa';
- const bgColour = params.get('bg') ? `#${params.get('bg')}` : '#00000033';
- const key = params.get('key') ? `#${params.get('key')}` : 'null';
- const fadeOut = params.get('fadeout') ? `${params.get('fadeout')}` : 'null';
-
- useEffect(() => {
- if (!fadeOut) return;
+ if (!options.fadeOut) return;
// Calculate time
- let transitionTime = parseInt(transitionIn) || 0;
- let fadeOutTime = (parseInt(fadeOut) + transitionTime) * 1000;
+ const fadeOutTime =
+ (parseInt(options.fadeOut) +
+ (options.transitionIn || defaults.transitionIn)) *
+ 1000;
if (isNaN(fadeOutTime)) return;
const timeout = setTimeout(() => {
@@ -38,18 +28,19 @@ export default function Lower(props) {
}, fadeOutTime);
return () => clearTimeout(timeout);
- }, []);
+ }, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
// Format messages
const showLowerMessage = lower.text !== '' && lower.visible;
- // transition segments
- const eight = transitionIn / 8;
- const quarter = transitionIn / 4;
- const third = transitionIn / 3;
- const half = transitionIn / 2;
-
// motion
+ // transition segments
+ const t = options.transitionIn || defaults.transitionIn;
+ const eight = t / 8;
+ const quarter = t / 4;
+ const third = t / 3;
+ const half = t / 2;
+
const lowerThirdVariants = {
hidden: {
opacity: 0,
@@ -120,20 +111,22 @@ export default function Lower(props) {
},
};
+ const sizeMultiplier = (options.size || 1) * 4;
+
return (
{showLower && (
{
+ document.title = 'ontime - Lower Thirds';
+ }, []);
+
+ // TODO: sanitize data
+ // getting config from URL: preset, size, transition, bg, text, key
+ // eg. http://localhost:3000/lower?bg=ff2&text=f00&size=0.6&transition=5
+ // Check for user options
+ useEffect(() => {
+ // get parameters
+ const params = new URLSearchParams(props.location.search);
+ // create aux
+ let options = {};
+
+ // preset: selector
+ // Should be a number 1-n
+ let p = parseInt(params.get('preset'));
+ if (!isNaN(p)) setPreset(p);
+
+ // size: multiplier
+ // Should be a number 0.0-n
+ let s = params.get('size');
+ if (s) options.size = s;
+
+ // transitionIn: seconds
+ // Should be a number 0-n
+ let t = parseInt(params.get('transition'));
+ if (!isNaN(t)) options.transitionIn = t;
+
+ // textColour: string
+ // Should be a hex string '#ffffff'
+ let c = params.get('text');
+ if (c) options.textColour = `#${c}`;
+
+ // bgColour: string
+ // Should be a hex string '#ffffff'
+ let b = params.get('bg');
+ if (b) options.bgColour = `#${b}`;
+
+ // key: string
+ // Should be a hex string '#00FF00' with key colour
+ let k = params.get('key');
+ if (k) options.keyColour = `#${k}`;
+
+ // fadeOut: seconds
+ // Should be a number 0-n
+ let f = parseInt(params.get('fadeout'));
+ if (!isNaN(f)) options.fadeOut = f;
+
+ // x: pixels
+ // Should be a number 0-n
+ let x = parseInt(params.get('x'));
+ if (!isNaN(x)) options.posX = x;
+
+ // y: pixels
+ // Should be a number 0-n
+ let y = parseInt(params.get('y'));
+ if (!isNaN(y)) options.posY = y;
+
+ setLowerOptions({
+ ...options,
+ set: true,
+ });
+ }, [props.location.search]);
+
+ // Defer rendering until we have data ready
+ if (!lowerOptions.set) return null;
+
+ switch (preset) {
+ case 0:
+ return ;
+ case 1:
+ return ;
+ default:
+ return ;
+ }
+}