diff --git a/.eslintignore b/.eslintignore index b3410cb..985c9b2 100755 --- a/.eslintignore +++ b/.eslintignore @@ -2,7 +2,6 @@ .eslintrc.*.js webpack.config.ts dashboard/**/* -extension/**/* graphics/**/* node_modules/**/* schemas/**/* diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 389a1dd..b57725d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,7 +71,7 @@ jobs: continue-on-error: true working-directory: ./sac-overlays run: | - git add -f graphics + git add -f graphics dashboard git commit -m "Build result from master branch" -a - name: Push built files to "build" branch diff --git a/.gitignore b/.gitignore index 131e577..a7187cc 100755 --- a/.gitignore +++ b/.gitignore @@ -4,7 +4,6 @@ bower_components /graphics /dashboard -/extension /.idea diff --git a/extension/index.js b/extension/index.js new file mode 100644 index 0000000..0e37b7e --- /dev/null +++ b/extension/index.js @@ -0,0 +1,8 @@ +module.exports = function (nodecg) { + const breakCastersVisible = nodecg.Replicant('breakCastersVisible', { defaultValue: false }); + const activeBreakScene = nodecg.Replicant('activeBreakScene', 'ipl-overlay-controls'); + + activeBreakScene.on('change', () => { + breakCastersVisible.value = false; + }); +} diff --git a/package.json b/package.json index b5b893e..94a5a44 100755 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ ], "scripts": { "build": "cross-env NODE_ENV=production webpack", - "clean": "trash node_modules/.cache && trash graphics", + "clean": "trash node_modules/.cache && trash graphics && trash dashboard", "cleanbuild": "npm run clean && npm run build", "start": "node ../..", "watch": "webpack -w", @@ -43,6 +43,16 @@ "width": 1920, "height": 1080 } + ], + "dashboardPanels": [ + { + "name": "sac-dash", + "title": "SAC Options", + "width": 2, + "file": "sac-dash.html", + "workspace": "ipl", + "headerColor": "#F0094C" + } ] }, "license": "SEE LICENSE IN LICENSE", diff --git a/src/dashboard/.eslintrc.json b/src/dashboard/.eslintrc.json new file mode 100755 index 0000000..e2b695b --- /dev/null +++ b/src/dashboard/.eslintrc.json @@ -0,0 +1,3 @@ +{ + "extends": "../../.eslintrc.browser.json" +} diff --git a/src/dashboard/sac-dash/main.ts b/src/dashboard/sac-dash/main.ts new file mode 100644 index 0000000..49d3279 --- /dev/null +++ b/src/dashboard/sac-dash/main.ts @@ -0,0 +1,10 @@ +const breakCastersCheckbox = document.getElementById('break-casters-checkbox') as HTMLInputElement; +const breakCastersVisible = nodecg.Replicant('breakCastersVisible', { defaultValue: false }); + +breakCastersCheckbox.addEventListener('change', event => { + breakCastersVisible.value = (event.target as HTMLInputElement).checked; +}); + +breakCastersVisible.on('change', newValue => { + breakCastersCheckbox.checked = newValue; +}); diff --git a/src/dashboard/sac-dash/sac-dash.html b/src/dashboard/sac-dash/sac-dash.html new file mode 100644 index 0000000..9d352e3 --- /dev/null +++ b/src/dashboard/sac-dash/sac-dash.html @@ -0,0 +1,13 @@ + + + + + sac-dash@sac-overlays + + + + + diff --git a/src/dashboard/tsconfig.json b/src/dashboard/tsconfig.json new file mode 100755 index 0000000..18ee81b --- /dev/null +++ b/src/dashboard/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../../tsconfig-browser.json" +} diff --git a/src/graphics/break/break.html b/src/graphics/break/break.html index 07edf3b..2c6fa22 100755 --- a/src/graphics/break/break.html +++ b/src/graphics/break/break.html @@ -63,6 +63,11 @@
+
+
+
+
+
diff --git a/src/graphics/break/scripts/casters.ts b/src/graphics/break/scripts/casters.ts index 0c675fb..0fa9d86 100644 --- a/src/graphics/break/scripts/casters.ts +++ b/src/graphics/break/scripts/casters.ts @@ -2,11 +2,14 @@ import { casters } from '../../helpers/replicants'; import { elementById } from '../../helpers/elem'; import gsap from 'gsap'; import { getPrevious } from '../../helpers/object'; +import { isBlank } from '../../helpers/string'; +import { Caster } from 'schemas'; const castersWrapper = elementById('info-bar-casters'); const twittersWrapper = elementById('info-bar-twitters'); +const castersLayout = elementById('casters-layout'); -casters.on('change', newValue => { +casters.on('change', (newValue, oldValue) => { const values = Object.values(newValue); castersWrapper.innerHTML = values.reduce((existing, elem) => { @@ -21,6 +24,51 @@ casters.on('change', newValue => { }, ''); gsap.set([ castersWrapper, twittersWrapper ], { scale: values.length > 2 ? 0.7 : 1 }); + + const newCasterIds = Object.keys(newValue); + const oldCasterIds = oldValue == null ? null : Object.keys(oldValue); + const shouldRecreateCasterElements + = oldCasterIds == null + || newCasterIds.length !== oldCasterIds.length + || newCasterIds.some((elem, i) => oldCasterIds[i] !== elem); + + if (shouldRecreateCasterElements) { + const casterWidth = getCasterWidth(values.length); + castersLayout.innerHTML = values.reduce((result, elem, i) => { + result += ` +
+
+ ${getCasterVisual(elem)} +
+
+ + +
+
+ `; + + return result; + }, ''); + } else { + newCasterIds.forEach((casterId) => { + const casterElem = document.querySelector(`[data-caster-id="${casterId}"]`); + const caster = newValue[casterId]; + const oldCaster = oldValue[casterId]; + (casterElem.querySelector('.caster-name') as FittedText).text = caster.name; + (casterElem.querySelector('.caster-twitter') as FittedText).text = caster.twitter; + + if ( + caster.videoUrl !== oldCaster.videoUrl + || (isBlank(caster.videoUrl) && caster.imageUrl !== oldCaster.imageUrl) + ) { + casterElem.querySelector('.caster-visual-wrapper').innerHTML = getCasterVisual(caster); + } + }); + } }); const casterInfoLoopTl = gsap.timeline({ repeat: -1 }); @@ -34,3 +82,44 @@ for (let i = 0; i < casterInfoElems.length; i++) { .to(`#${previousElem}`, { duration: 0.35, opacity: 1 }) .to({}, { duration: 30 }); } + +function getCasterWidth(casterCount: number): number { + switch (casterCount) { + case 1: + return 1200; + case 2: + return 750; + default: + return 625; + } +} + +function getCasterVisual(caster: Caster): string { + if (!isBlank(caster.videoUrl)) { + return ` +
+ +
+ `; + } else if (!isBlank(caster.imageUrl)) { + return ` + + `; + } else { + return ` + + `; + } + +} diff --git a/src/graphics/break/scripts/sceneSwitcher.ts b/src/graphics/break/scripts/sceneSwitcher.ts index 3c103ea..f90a0bc 100644 --- a/src/graphics/break/scripts/sceneSwitcher.ts +++ b/src/graphics/break/scripts/sceneSwitcher.ts @@ -1,4 +1,5 @@ -import { activeBreakScene } from '../../helpers/replicants'; +import { ActiveBreakScene } from 'schemas'; +import { activeBreakScene, breakCastersVisible } from '../../helpers/replicants'; import gsap from 'gsap'; export const sceneSwitchTl = gsap.timeline({ @@ -8,21 +9,42 @@ export const sceneSwitchTl = gsap.timeline({ } }); -activeBreakScene.on('change', (newValue, oldValue) => { +NodeCG.waitForReplicants(breakCastersVisible, activeBreakScene).then(() => { + breakCastersVisible.on('change', (newValue, oldValue) => { + if (oldValue == null && !newValue) return; + onSceneSwitch(activeBreakScene.value, activeBreakScene.value, newValue); + }); + + activeBreakScene.on('change', (newValue, oldValue) => { + if (breakCastersVisible.value) return; + onSceneSwitch(newValue, oldValue, null); + }); +}); + +function onSceneSwitch( + newActiveBreakScene: ActiveBreakScene, + oldActiveBreakScene: ActiveBreakScene, + breakCastersVisible: boolean | null +) { sceneSwitchTl.addLabel('sceneHide'); - switch (oldValue) { - case 'main': - sceneSwitchTl.add(hideMainScene(), 'sceneHide'); - break; - case 'teams': - sceneSwitchTl.add(hideTeams(), 'sceneHide'); - break; - case 'stages': - sceneSwitchTl.add(hideStages(), 'sceneHide'); + if (breakCastersVisible === false) { + sceneSwitchTl.add(hideCasters(), 'sceneHide'); + } else { + switch (oldActiveBreakScene) { + case 'main': + sceneSwitchTl.add(hideMainScene(), 'sceneHide'); + break; + case 'teams': + sceneSwitchTl.add(hideTeams(), 'sceneHide'); + break; + case 'stages': + sceneSwitchTl.add(hideStages(), 'sceneHide'); + } + } - if (newValue === 'main') { + if (newActiveBreakScene === 'main' && breakCastersVisible !== true) { sceneSwitchTl.add(hideInfoBar(), 'sceneHide'); sceneSwitchTl.addLabel('sceneShow'); } else { @@ -30,17 +52,21 @@ activeBreakScene.on('change', (newValue, oldValue) => { sceneSwitchTl.add(showInfoBar(), 'sceneShow'); } - switch (newValue) { - case 'main': - sceneSwitchTl.add(showMainScene(), 'sceneShow'); - break; - case 'teams': - sceneSwitchTl.add(showTeams(), 'sceneShow'); - break; - case 'stages': - sceneSwitchTl.add(showStages(), 'sceneShow'); + if (breakCastersVisible) { + sceneSwitchTl.add(showCasters(), 'sceneShow'); + } else { + switch (newActiveBreakScene) { + case 'main': + sceneSwitchTl.add(showMainScene(), 'sceneShow'); + break; + case 'teams': + sceneSwitchTl.add(showTeams(), 'sceneShow'); + break; + case 'stages': + sceneSwitchTl.add(showStages(), 'sceneShow'); + } } -}); +} function hideMainScene(): gsap.core.Timeline { const tl = gsap.timeline({ @@ -226,3 +252,28 @@ function hideStages(): gsap.core.Timeline { return tl; } + +function showCasters(): gsap.core.Timeline { + const tl = gsap.timeline({ + onStart: () => { + gsap.set('.caster-wrapper', { opacity: 0 }); + gsap.set('.casters-wrapper', { display: 'flex' }); + } + }); + + tl.to('.caster-wrapper', { opacity: 1, duration: 0.5, stagger: 0.1 }); + + return tl; +} + +function hideCasters(): gsap.core.Timeline { + const tl = gsap.timeline({ + onComplete: () => { + gsap.set('.casters-wrapper', { display: 'none' }); + } + }); + + tl.to('.caster-wrapper', { opacity: 0, duration: 0.5, stagger: 0.1 }); + + return tl; +} diff --git a/src/graphics/break/styles/break.scss b/src/graphics/break/styles/break.scss index 246c890..268c080 100755 --- a/src/graphics/break/styles/break.scss +++ b/src/graphics/break/styles/break.scss @@ -6,6 +6,7 @@ @import './infoBar.scss'; @import './teams.scss'; @import './stages.scss'; +@import './casters.scss'; body { margin: 0; diff --git a/src/graphics/break/styles/casters.scss b/src/graphics/break/styles/casters.scss new file mode 100644 index 0000000..be72202 --- /dev/null +++ b/src/graphics/break/styles/casters.scss @@ -0,0 +1,81 @@ +.casters-wrapper { + display: none; +} + +.casters-layout { + width: 1600px; + height: 750px; + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: center; + margin-bottom: 150px; + + .caster-wrapper { + background-color: $sac-background-2; + filter: $global-drop-shadow; + width: var(--caster-width); + margin: 16px; + aspect-ratio: 16 / 9; + display: flex; + align-items: center; + justify-content: center; + position: relative; + } + + .caster-visual-wrapper { + display: contents; + } + + .caster-nametag { + display: flex; + flex-direction: column; + align-items: flex-end; + margin-top: 5px; + position: absolute; + bottom: -15px; + right: -10px; + z-index: 2; + filter: drop-shadow(0 0 5px rgba(0, 0, 0, 0.5)); + text-align: right; + } + + .caster-name { + font-weight: 700; + background-color: $sac-accent; + padding: 0 8px; + line-height: 45px; + font-size: 38px; + } + + .caster-twitter { + font-weight: 300; + background-color: $sac-accent; + padding: 0 8px 4px; + line-height: 35px; + font-size: 32px; + } + + .caster-image { + width: 200px; + height: 200px; + border-radius: 999px; + } + + .caster-image-placeholder { + width: 250px; + opacity: 0.75; + } + + .video-loader-wrapper { + position: relative; + width: 100%; + height: 100%; + + > iframe { + border-width: 0; + width: 100%; + height: 100%; + } + } +} diff --git a/src/graphics/helpers/replicants.ts b/src/graphics/helpers/replicants.ts index 62fff43..81dfb93 100755 --- a/src/graphics/helpers/replicants.ts +++ b/src/graphics/helpers/replicants.ts @@ -12,3 +12,4 @@ export const nextRoundTime = nodecg.Replicant('nextRoundStar export const scoreboardData = nodecg.Replicant('scoreboardData', DASHBOARD_BUNDLE_NAME); export const nextRound = nodecg.Replicant('nextRound', DASHBOARD_BUNDLE_NAME); export const assetPaths = nodecg.Replicant('assetPaths', DASHBOARD_BUNDLE_NAME); +export const breakCastersVisible = nodecg.Replicant('breakCastersVisible', { defaultValue: false }); diff --git a/src/graphics/helpers/string.ts b/src/graphics/helpers/string.ts index 508fd91..f558bd1 100755 --- a/src/graphics/helpers/string.ts +++ b/src/graphics/helpers/string.ts @@ -8,3 +8,7 @@ export function addDots(value: string, maxLength = 48): string { return value; } + +export function isBlank(value: unknown): boolean { + return value === null || value === undefined || (typeof value === 'string' && value.trim() === ''); +} diff --git a/webpack.config.ts b/webpack.config.ts index 192d3c0..d171fd8 100755 --- a/webpack.config.ts +++ b/webpack.config.ts @@ -10,9 +10,9 @@ import CopyPlugin from 'copy-webpack-plugin'; const isProd = process.env.NODE_ENV === 'production'; -function graphicsConfig(): webpack.Configuration { +function browserConfig(type: 'graphics' | 'dashboard'): webpack.Configuration { const entries: { [key: string]: string } = globby - .sync(['*/main.js', '*/main.ts'], { cwd: 'src/graphics' }) + .sync(['*/main.js', '*/main.ts'], { cwd: `src/${type}` }) .reduce((prev, curr) => { prev[path.basename(path.dirname(curr))] = `./${curr}`; return prev; @@ -31,15 +31,18 @@ function graphicsConfig(): webpack.Configuration { title: entryName, template: `./${entryName}/${entryName}.html` }) - ), - new CopyPlugin({ - patterns: [ - { from: 'assets/**/*' } - ] - }), + ) ] ); + if (type === 'graphics') { + plugins.push(new CopyPlugin({ + patterns: [ + { from: 'assets/**/*' } + ] + })); + } + if (!isProd) { plugins.push( new LiveReloadPlugin({ @@ -50,12 +53,12 @@ function graphicsConfig(): webpack.Configuration { } return { - context: path.resolve(__dirname, 'src/graphics'), + context: path.resolve(__dirname, `src/${type}`), mode: isProd ? 'production' : 'development', target: 'web', entry: entries, output: { - path: path.resolve(__dirname, 'graphics'), + path: path.resolve(__dirname, type), filename: 'js/[name].js' }, resolve: { @@ -115,5 +118,6 @@ function graphicsConfig(): webpack.Configuration { } export default [ - graphicsConfig() + browserConfig('graphics'), + browserConfig('dashboard') ];