mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-22 15:39:11 +00:00
feat: lower presets
added clean lower with no styling and minimal animation
This commit is contained in:
+3
-1
@@ -17,7 +17,9 @@ const StageManager = lazy(() =>
|
|||||||
import('./features/viewers/backstage/StageManager')
|
import('./features/viewers/backstage/StageManager')
|
||||||
);
|
);
|
||||||
const Public = lazy(() => import('./features/viewers/foh/Public'));
|
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 Pip = lazy(() => import('./features/viewers/production/Pip'));
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
<div
|
||||||
|
className={style.lowerThird}
|
||||||
|
style={{
|
||||||
|
backgroundColor: options.keyColour || defaults.keyColour,
|
||||||
|
color: options.textColour || defaults.textColour,
|
||||||
|
fontSize: `${sizeMultiplier}vh`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<AnimatePresence>
|
||||||
|
{showLower && (
|
||||||
|
<motion.div
|
||||||
|
className={style.lowerContainer}
|
||||||
|
style={{
|
||||||
|
backgroundColor: options.bgColour || defaults.bgColour,
|
||||||
|
top: options.posY || defaults.posY,
|
||||||
|
left: options.posX || defaults.posX,
|
||||||
|
}}
|
||||||
|
variants={lowerThirdVariants}
|
||||||
|
initial='hidden'
|
||||||
|
animate='visible'
|
||||||
|
exit='exit'
|
||||||
|
>
|
||||||
|
<motion.div className={style.title} variants={titleVariants}>
|
||||||
|
{title.titleNow}
|
||||||
|
</motion.div>
|
||||||
|
<motion.div className={style.subtitle} variants={titleVariants}>
|
||||||
|
{title.presenterNow}
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
|
||||||
|
<AnimatePresence>
|
||||||
|
{showLowerMessage && (
|
||||||
|
<motion.div
|
||||||
|
className={style.messageContainer}
|
||||||
|
style={{
|
||||||
|
backgroundColor: options.bgColour || defaults.bgColour,
|
||||||
|
}}
|
||||||
|
key='modal'
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ scaleY: 0, opacity: 0 }}
|
||||||
|
transition={{ duration: 0.5 }}
|
||||||
|
>
|
||||||
|
<div className={style.message}>{lower.text}</div>
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
+29
-36
@@ -1,36 +1,26 @@
|
|||||||
import { AnimatePresence, motion } from 'framer-motion';
|
import { AnimatePresence, motion } from 'framer-motion';
|
||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import style from './Lower.module.css';
|
import style from './LowerLines.module.css';
|
||||||
|
|
||||||
export default function Lower(props) {
|
export default function LowerLines(props) {
|
||||||
const { lower, title, time, general } = props;
|
const { lower, title, options } = props;
|
||||||
|
const defaults = {
|
||||||
|
size: 1,
|
||||||
|
transitionIn: 3,
|
||||||
|
textColour: '#fffffa',
|
||||||
|
bgColour: '#00000033',
|
||||||
|
};
|
||||||
const [showLower, setShowLower] = useState(true);
|
const [showLower, setShowLower] = useState(true);
|
||||||
|
|
||||||
// Set window title
|
// Unmount if fadeOut
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.title = 'ontime - Lower Thirds';
|
if (!options.fadeOut) return;
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
// Calculate time
|
// Calculate time
|
||||||
let transitionTime = parseInt(transitionIn) || 0;
|
const fadeOutTime =
|
||||||
let fadeOutTime = (parseInt(fadeOut) + transitionTime) * 1000;
|
(parseInt(options.fadeOut) +
|
||||||
|
(options.transitionIn || defaults.transitionIn)) *
|
||||||
|
1000;
|
||||||
if (isNaN(fadeOutTime)) return;
|
if (isNaN(fadeOutTime)) return;
|
||||||
|
|
||||||
const timeout = setTimeout(() => {
|
const timeout = setTimeout(() => {
|
||||||
@@ -38,18 +28,19 @@ export default function Lower(props) {
|
|||||||
}, fadeOutTime);
|
}, fadeOutTime);
|
||||||
|
|
||||||
return () => clearTimeout(timeout);
|
return () => clearTimeout(timeout);
|
||||||
}, []);
|
}, [options.fadeOut, options.transitionIn, defaults.transitionIn]);
|
||||||
|
|
||||||
// Format messages
|
// Format messages
|
||||||
const showLowerMessage = lower.text !== '' && lower.visible;
|
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
|
// 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 = {
|
const lowerThirdVariants = {
|
||||||
hidden: {
|
hidden: {
|
||||||
opacity: 0,
|
opacity: 0,
|
||||||
@@ -120,20 +111,22 @@ export default function Lower(props) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const sizeMultiplier = (options.size || 1) * 4;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={style.lowerThird}
|
className={style.lowerThird}
|
||||||
style={{
|
style={{
|
||||||
backgroundColor: key,
|
backgroundColor: options.keyColour || defaults.keyColour,
|
||||||
color: textColour,
|
color: options.textColour || defaults.textColour,
|
||||||
fontSize: `${size * 4}vh`,
|
fontSize: `${sizeMultiplier}vh`,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{showLower && (
|
{showLower && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className={style.lowerContainer}
|
className={style.lowerContainer}
|
||||||
style={{ backgroundColor: bgColour }}
|
style={{ backgroundColor: options.bgColour || defaults.bgColour }}
|
||||||
variants={lowerThirdVariants}
|
variants={lowerThirdVariants}
|
||||||
initial='hidden'
|
initial='hidden'
|
||||||
animate='visible'
|
animate='visible'
|
||||||
+1
-2
@@ -21,8 +21,7 @@
|
|||||||
border-radius: 0 1vh 1vh 0;
|
border-radius: 0 1vh 1vh 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.messageContainer,
|
.messageContainer {
|
||||||
.messageContainerHidden {
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 2vh;
|
bottom: 2vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useState } from 'react';
|
||||||
|
import LowerClean from './LowerClean';
|
||||||
|
import LowerLines from './LowerLines';
|
||||||
|
|
||||||
|
export default function Lower(props) {
|
||||||
|
const [preset, setPreset] = useState(1);
|
||||||
|
const [lowerOptions, setLowerOptions] = useState({});
|
||||||
|
|
||||||
|
// Set window title
|
||||||
|
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
|
||||||
|
// 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 <LowerClean {...props} options={lowerOptions} />;
|
||||||
|
case 1:
|
||||||
|
return <LowerLines {...props} options={lowerOptions} />;
|
||||||
|
default:
|
||||||
|
return <LowerLines {...props} options={lowerOptions} />;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user