├── .gitignore ├── LICENSE ├── README.md ├── bower.json ├── configschema.json ├── dashboard ├── advertisements.html ├── advertisements.js ├── checklist.html ├── dialogs │ ├── edit-current-run.html │ └── edit-total.html ├── elements │ ├── ad-item.html │ ├── ad-list.html │ ├── disabled-cover.html │ ├── edit-runner.html │ ├── label-value.html │ ├── time-ago.html │ └── tweet-item.html ├── interview.html ├── interview.js ├── nowplaying.html ├── omnibar.html ├── schedule.html ├── schedule.js ├── shared-panel-styles.css ├── total.html ├── twitter.html └── twitter.js ├── download_boxart.js ├── electron.js ├── extension ├── advertisements.js ├── bids.js ├── checklist.js ├── index.js ├── interview.js ├── nowplaying.js ├── osc.js ├── prizes.js ├── schedule.js ├── sponsors.js ├── state.js ├── stopwatches.js ├── total.js └── twitter │ ├── index.js │ └── shared.css ├── graphics ├── advertisements │ └── .empty_directory ├── app │ ├── advertisements.js │ ├── classes │ │ ├── compact_nameplate.js │ │ ├── nameplate.js │ │ └── stage.js │ ├── components │ │ ├── background.js │ │ ├── compact_nameplates.js │ │ ├── nameplates.js │ │ ├── now-playing │ │ │ ├── music_note.png │ │ │ ├── now-playing.html │ │ │ └── now-playing.js │ │ ├── omnibar.js │ │ ├── speedrun.js │ │ ├── sponsor-display │ │ │ ├── sponsor-background.png │ │ │ ├── sponsor-display.html │ │ │ └── sponsor-display.js │ │ └── twitter-display │ │ │ ├── twitter-display.html │ │ │ ├── twitter-display.js │ │ │ └── twitter.png │ ├── debug.js │ ├── globals.js │ ├── index.js │ ├── layout.js │ ├── layouts │ │ ├── 16x9_1.js │ │ ├── 16x9_2.js │ │ ├── 3ds.js │ │ ├── 3x2_1.js │ │ ├── 3x2_2.js │ │ ├── 4x3_1.js │ │ ├── 4x3_2.js │ │ ├── 4x3_3.js │ │ ├── 4x3_4.js │ │ ├── break.js │ │ ├── ds.js │ │ ├── ds_portrait.js │ │ └── interview.js │ ├── obs.js │ ├── preloader.js │ └── tabulate.js ├── custom_controls │ └── stopwatches │ │ ├── elements │ │ ├── finish.png │ │ ├── gdq-stopwatch.html │ │ ├── time-only-validator.html │ │ └── unfinish.png │ │ ├── index.html │ │ └── stopwatches.js ├── img │ ├── backgrounds │ │ ├── 16x9_1.png │ │ ├── 16x9_2.png │ │ ├── 3ds.png │ │ ├── 3x2_1.png │ │ ├── 3x2_2.png │ │ ├── 4x3_1.png │ │ ├── 4x3_2.png │ │ ├── 4x3_3.png │ │ ├── 4x3_4.png │ │ ├── break.png │ │ ├── ds.png │ │ ├── ds_portrait.png │ │ └── interview.png │ ├── boxart │ │ └── default.png │ ├── consoles │ │ ├── 3ds.png │ │ ├── arc.png │ │ ├── dc.png │ │ ├── ds.png │ │ ├── gb.png │ │ ├── gba.png │ │ ├── gbc.png │ │ ├── gcn.png │ │ ├── gen.png │ │ ├── n64.png │ │ ├── nes.png │ │ ├── pc.png │ │ ├── ps1.png │ │ ├── ps2.png │ │ ├── ps3.png │ │ ├── ps4.png │ │ ├── psp.png │ │ ├── sat.png │ │ ├── snes.png │ │ ├── unknown.png │ │ ├── wii.png │ │ ├── wiiu.png │ │ ├── wshp.png │ │ ├── x360.png │ │ ├── xbox.png │ │ └── xboxone.png │ ├── nameplate │ │ ├── audio-off.png │ │ ├── audio-on.png │ │ └── twitch-logo.png │ ├── omnibar │ │ ├── logo-gdq.png │ │ └── logo-pcf.png │ └── sponsors │ │ ├── sponsor1-horizontal.png │ │ ├── sponsor1-vertical.png │ │ └── sponsor2-horizontal.png ├── index.html ├── lib │ ├── obs-remote.js │ ├── preloadjs-NEXT.min.js │ ├── require.js │ └── video-preloader.js └── style │ └── base.css └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependency directory 2 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 3 | node_modules 4 | bower_components 5 | 6 | /graphics/advertisements/* 7 | /graphics/img/boxart/* 8 | !/graphics/img/boxart/default.png 9 | !.empty_directory 10 | 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2016 Games Done Quick 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # agdq16-layouts 2 | The on-stream graphics used during Awesome Games Done Quick 2016. 3 | 4 | This is a [NodeCG](http://github.com/nodecg/nodecg) 0.7 bundle. You will need to have NodeCG 0.7 installed to run it. 5 | 6 | ## Video Walkthrough 7 | [A ten-part video series explaining the structure and function of this NodeCG bundle.](https://www.youtube.com/playlist?list=PL1EO2PfU4nFnB4c40SzUpulvYvVmPxeTx) 8 | 9 | ## Installation 10 | - Install to `nodecg/bundles/agdq16-layouts`. 11 | - Install `bower` if you have not already (`npm install -g bower`) 12 | - **WINDOWS**: Follow [these instructions](https://github.com/nodejs/node-gyp/issues/629#issuecomment-153196245) to set up a build chain to compile `agdq16-layouts`' dependencies. 13 | - **LINUX**: Install `build-essential` and Python 2.7, which are needed to compile `agdq16-layouts`' dependencies. 14 | - `cd nodecg/bundles/agdq16-layouts` and run `npm install`, then `bower install` 15 | - Run `node ./download_boxart.js` to populate the boxart. 16 | - Create the configuration file (see the [configuration][id] section below for more details) 17 | - Run the nodecg server: `nodecg start` (or `node index.js` if you don't have nodecg-cli) from the `nodecg` root directory. 18 | - Run the electron window: 19 | - For Windows: 20 | - Create a shortcut in the `bundles/agdq16-layouts` folder with the location set to 21 | `C:\path\to\nodecg\bundles\agdq16-layouts\node_modules\electron-prebuilt\dist\electron.exe` called Electron. 22 | - Next, edit the properties of the link you created, add ` electron.js --remote-debugging-port=9222` to the end of 23 | the `Target` value, and change the `Start in` folder to be `C:\path\to\nodecg\bundles\agdq16-layouts\`. 24 | - For Linux/Mac: 25 | - `cd` to the `bundles/agdq16-bundles` directory, then run `./node_modules/electron-prebuild/dist/electron electron.js --remote-debugging-port=9222` 26 | 27 | Please note that you **must manually run `npm install` for this bundle**. NodeCG currently cannot reliably 28 | compile this bundle's npm dependencies. This is an issue we hope to address in the future. 29 | 30 | ## Usage 31 | This bundle is not intended to be used verbatim. Many of the assets have been replaced with placeholders, and 32 | most of the data sources are hardcoded. We are open-sourcing this bundle in hopes that people will use it as a 33 | learning tool and base to build from, rather than just taking and using it wholesale in their own productions. 34 | 35 | To reiterate, please don't just download and use this bundle as-is. Build something new from it. 36 | 37 | [id]: configuration 38 | ## Configuration 39 | To configure this bundle, create and edit `nodecg/cfg/agdq16-layouts.json`. 40 | Refer to [configschema.json][] for the structure of this file. 41 | [configschema.json]: configschema.json 42 | 43 | Example config: 44 | ```json 45 | { 46 | "enableRestApi": true, 47 | "x32": { 48 | "address": "192.168.1.10", 49 | "gameAudioChannels": [ 50 | { 51 | "sd": 17, 52 | "hd": 25 53 | }, 54 | { 55 | "sd": 19, 56 | "hd": 27 57 | }, 58 | { 59 | "sd": 21, 60 | "hd": null 61 | }, 62 | { 63 | "sd": 23, 64 | "hd": null 65 | } 66 | ] 67 | }, 68 | "twitter": { 69 | "userId": "1234", 70 | "consumerKey": "aaa", 71 | "consumerSecret": "bbb", 72 | "accessTokenKey": "ccc", 73 | "accessTokenSecret": "ddd" 74 | }, 75 | "lastfm": { 76 | "apiKey": "eee", 77 | "secret": "fff", 78 | "targetAccount": "youraccount" 79 | }, 80 | "debug": true 81 | } 82 | ``` 83 | 84 | ## Timer REST API 85 | There is a REST API to integrate with the footpedal that [@TestRunnerSRL](https://github.com/TestRunnerSRL) 86 | built for the runners to start and stop the timer themselves. 87 | This REST API is **completely unsecured** and **anyone will be able to manipulate the timers**. 88 | As such, it is **not safe to run on the public internet**. Only activate the REST API on a secure local network. 89 | 90 | To activate the Timer REST API, create `nodecg/cfg/agdq16-layouts.json` with the following content: 91 | ``` 92 | { 93 | "enableRestApi": true 94 | } 95 | ``` 96 | 97 | ### GET /agdq16-layouts/stopwatches 98 | Returns a JSON array containing all 4 stopwatches. 99 | 100 | ### PUT /agdq16-layouts/stopwatch/:index/start 101 | Starts (or resumes, if paused/finished) one of the four stopwatches. Index is zero-based. 102 | If index is 'all', starts all stopwatches. Responds with the current status of the affected stopwatch(es). 103 | 104 | ### PUT /agdq16-layouts/stopwatch/:index/pause 105 | Pauses one of the four stopwatches. Index is zero-based. 106 | If index is 'all', pauses all stopwatches. Paused stopwatches have a gray background in the layouts. 107 | Responds with the current status of the affected stopwatch(es). 108 | 109 | ### PUT /agdq16-layouts/stopwatch/:index/finish 110 | Finishes one of the four stopwatches. Index is zero-based. 111 | If index is 'all', finishes all stopwatches. Finished stopwatches have a green background in the layouts. 112 | Responds with the current status of the affected stopwatch(es). 113 | 114 | ### PUT /agdq16-layouts/stopwatch/:index/reset 115 | Resets one of the four stopwatches to 00:00:00 and stops it. Index is zero-based. 116 | If index is 'all', resets all stopwatches. Responds with the current status of the affected stopwatch(es). 117 | 118 | ### PUT /agdq16-layouts/stopwatch/:index/startfinish 119 | If the stopwatch *is not* running, this starts it. If the stopwatch *is* running, this sets it to "finished". 120 | Index is zero-based. If index is 'all', resets all stopwatches. 121 | Responds with the current status of the affected stopwatch(es). 122 | 123 | ## Fonts 124 | agdq16-layouts relies on the following [TypeKit](https://typekit.com/) fonts and weights: 125 | 126 | - Proxima Nova 127 | - Semibold 128 | - Bold 129 | - Extrabold 130 | - Black 131 | 132 | If you wish to access agdq16-layouts from anything other than `localhost`, 133 | you will need to make your own TypeKit with these fonts and whitelist the appropriate addresses. 134 | 135 | ## License 136 | agdq16-layouts is provided under the Apache v2 license, which is available to read in the [LICENSE][] file. 137 | [license]: LICENSE 138 | 139 | ### Credits 140 | Developed by [Support Class](http://supportclass.net/) 141 | - [Alex "Lange" Van Camp](https://twitter.com/VanCamp/), developer 142 | - [Chris Hanel](https://twitter.com/ChrisHanel), designer 143 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agdq16-layouts", 3 | "dependencies": { 4 | "EaselJS": "0.8.1", 5 | "gsap": "1.18.0", 6 | "iron-icons": "PolymerElements/iron-icons#1.0.5", 7 | "iron-image": "PolymerElements/iron-image#^1.1.0", 8 | "iron-validator-behavior": "PolymerElements/iron-validator-behavior#1.0.1", 9 | "javascipt-debounce": "javascript-debounce#~1.0.0", 10 | "moment": "~2.10.6", 11 | "nodecg-replicant": "NodeCGElements/nodecg-replicant#~0.5.3", 12 | "nodecg-replicant-input": "NodeCGElements/nodecg-replicant-input#~0.3.1", 13 | "nodecg-toast": "NodeCGElements/nodecg-toast#0.1.2", 14 | "nodecg-toggle": "NodeCGElements/nodecg-toggle#~0.1.1", 15 | "nodecg-typeahead-input": "NodeCGElements/nodecg-typeahead-input#~0.1.2", 16 | "numeral": "~1.5.3", 17 | "paper-button": "PolymerElements/paper-button#1.0.8", 18 | "paper-checkbox": "PolymerElements/paper-checkbox#^1.0.15", 19 | "paper-dialog": "PolymerElements/paper-dialog#^1.0.3", 20 | "paper-dropdown-menu": "PolymerElements/paper-dropdown-menu#^1.1.1", 21 | "paper-icon-button": "PolymerElements/paper-icon-button#^1.0.5", 22 | "paper-input": "PolymerElements/paper-input#1.1.1", 23 | "paper-item": "PolymerElements/paper-item#^1.1.2", 24 | "paper-listbox": "PolymerElements/paper-listbox#^1.1.0", 25 | "paper-progress": "PolymerElements/paper-progress#^1.0.7" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /configschema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-04/schema#", 3 | "type": "object", 4 | 5 | "properties": { 6 | "enableRestApi": { 7 | "type": "boolean", 8 | "description": "Whether or not to enable the Stopwatch REST API.", 9 | "default": false 10 | }, 11 | "x32": { 12 | "type": "object", 13 | "properties": { 14 | "address": { 15 | "type": "string", 16 | "description": "The IP address or hostname of a Behringer X32 digital mixer." 17 | }, 18 | "gameAudioChannels": { 19 | "type": "array", 20 | "items": { 21 | "type": "object", 22 | "properties": { 23 | "sd": { 24 | "type": ["integer", "null"] 25 | }, 26 | "hd": { 27 | "type": ["integer", "null"] 28 | } 29 | } 30 | }, 31 | "minItems": 4, 32 | "maxItems": 4 33 | } 34 | }, 35 | "required": ["address", "gameAudioChannels"] 36 | }, 37 | "twitter": { 38 | "type": "object", 39 | "properties": { 40 | "userId": { 41 | "type": "string", 42 | "description": "The numeric userid of the Twitter account that owns these API keys. http://mytwitterid.com/" 43 | }, 44 | "consumerKey": { 45 | "type": "string" 46 | }, 47 | "consumerSecret": { 48 | "type": "string" 49 | }, 50 | "accessTokenKey": { 51 | "type": "string" 52 | }, 53 | "accessTokenSecret": { 54 | "type": "string" 55 | } 56 | }, 57 | "required": ["userId", "consumerKey", "consumerSecret", "accessTokenKey", "accessTokenSecret"] 58 | }, 59 | "lastfm": { 60 | "type": "object", 61 | "properties": { 62 | "apiKey": { 63 | "type": "string" 64 | }, 65 | "secret": { 66 | "type": "string" 67 | }, 68 | "targetAccount": { 69 | "type": "string" 70 | } 71 | }, 72 | "description": "Configuration object for Last.fm API, used by nowplaying graphic.", 73 | "required": ["apiKey", "secret", "targetAccount"] 74 | }, 75 | "debug": { 76 | "type": "boolean", 77 | "default": false, 78 | "description": "Whether or not to enable the client-side debug logging." 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /dashboard/advertisements.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 70 | 71 | 72 |
73 | 74 |
75 |
76 |
Images
77 | 78 |
79 |
80 | 81 | 82 |
83 |
84 |
Videos
85 | 86 |
87 |
88 |
89 | 90 |
91 | 92 | Play Selected Ad 93 | 94 | 95 | Play Selected Ad 96 | 97 | Stop 98 | FTB 99 |
100 | 101 |
102 | Not currently playing an ad. 103 |
104 | 105 | 106 | 107 | The layout graphic appears to be closed. 108 |
109 | Open the layout graphic to enable these controls. 110 |
111 | 112 | 113 | The layout graphic is preloading. 114 |
115 | Please wait... 116 |
117 |
118 | 119 | 120 | 121 | 122 | -------------------------------------------------------------------------------- /dashboard/advertisements.js: -------------------------------------------------------------------------------- 1 | /* jshint -W106 */ 2 | (function() { 3 | 'use strict'; 4 | 5 | var disabledCover = document.getElementById('cover'); 6 | 7 | var layoutState = nodecg.Replicant('layoutState'); 8 | layoutState.on('change', function(oldVal, newVal) { 9 | disabledCover.reason = newVal.page; 10 | 11 | if (newVal.page === 'open') { 12 | disabledCover.reason = null; 13 | 14 | /* When the dashboard first loads, the layout might already be open and have all ads preloaded. 15 | * Therefore, on first load we have to ask the layout what the status of all the ads is. 16 | * This message will trigger the layout to send `adLoadProgress` or `adLoadFinished` events 17 | * for all ads. */ 18 | setTimeout(function() { 19 | nodecg.sendMessage('getLoadedAds'); 20 | }, 100); 21 | } 22 | 23 | else { 24 | disabledCover.reason = newVal.page; 25 | 26 | if (newVal.page === 'closed') { 27 | var adItems = Array.prototype.slice.call(document.querySelectorAll('ad-item')); 28 | adItems.forEach(function(adItem) { 29 | adItem.percentLoaded = 0; 30 | adItem.loaded = false; 31 | }); 32 | } 33 | } 34 | }); 35 | 36 | /* ----- */ 37 | 38 | var playImageButton = document.getElementById('play-image'); 39 | var playVideoButton = document.getElementById('play-video'); 40 | 41 | window.checkVideoPlayButton = function () { 42 | if (!window.playCooldown 43 | && window.adListSelectedAd 44 | && window.adListSelectedAd.type === 'video' 45 | && ftb.value === true) { 46 | playVideoButton.removeAttribute('disabled'); 47 | } else { 48 | playVideoButton.setAttribute('disabled', 'true'); 49 | } 50 | }; 51 | 52 | playImageButton.addEventListener('click', playButtonClick); 53 | playVideoButton.addEventListener('click', playButtonClick); 54 | 55 | function playButtonClick() { 56 | // window.adListSelectedAd is set by elements/ad-list.html 57 | nodecg.sendMessage('playAd', window.adListSelectedAd); 58 | 59 | playImageButton.querySelector('span').innerText = 'Starting playback...'; 60 | playVideoButton.querySelector('span').innerText = 'Starting playback...'; 61 | playImageButton.setAttribute('disabled', 'true'); 62 | playVideoButton.setAttribute('disabled', 'true'); 63 | 64 | window.playCooldown = setTimeout(function() { 65 | window.playCooldown = null; 66 | playImageButton.removeAttribute('disabled'); 67 | playImageButton.querySelector('span').innerText = 'Play Selected Ad'; 68 | playVideoButton.querySelector('span').innerText = 'Play Selected Ad'; 69 | window.checkVideoPlayButton(); 70 | }, 1000); 71 | } 72 | 73 | var stopButton = document.getElementById('stop'); 74 | stopButton.addEventListener('click', function() { 75 | nodecg.sendMessage('stopAd'); 76 | }); 77 | 78 | var ftbButton = document.getElementById('ftb'); 79 | ftbButton.addEventListener('click', function() { 80 | ftb.value = !ftb.value; 81 | }); 82 | 83 | var ftb = nodecg.Replicant('ftb'); 84 | ftb.on('change', function(oldVal, newVal) { 85 | window.checkVideoPlayButton(); 86 | if (newVal) { 87 | ftbButton.classList.add('nodecg-warning'); 88 | playVideoButton.querySelector('span').innerText = 'Play Selected Ad'; 89 | } else { 90 | ftbButton.classList.remove('nodecg-warning'); 91 | playVideoButton.querySelector('span').innerText = 'FTB To Play Video'; 92 | } 93 | }); 94 | 95 | /* ----- */ 96 | 97 | var imageList = document.getElementById('imageList'); 98 | var videoList = document.getElementById('videoList'); 99 | nodecg.Replicant('ads').on('change', function (oldVal, newVal) { 100 | imageList.ads = newVal.filter(function(ad) { 101 | return ad.type === 'image'; 102 | }); 103 | 104 | videoList.ads = newVal.filter(function(ad) { 105 | return ad.type === 'video'; 106 | }); 107 | }); 108 | 109 | nodecg.listenFor('adLoadProgress', function(data) { 110 | var el = document.querySelector('ad-item[filename="' + data.filename + '"]'); 111 | if (el) el.percentLoaded = data.percentLoaded; 112 | }); 113 | 114 | nodecg.listenFor('adLoaded', function(filename) { 115 | var el = document.querySelector('ad-item[filename="' + filename + '"]'); 116 | if (el) el.loaded = true; 117 | }); 118 | 119 | /* ----- */ 120 | 121 | var adState = nodecg.Replicant('adState'); 122 | var status = document.getElementById('status'); 123 | adState.on('change', function(oldVal, newVal) { 124 | switch (newVal) { 125 | case 'stopped': 126 | status.innerText = 'Not currently playing an ad.'; 127 | status.style.fontWeight = 'normal'; 128 | break; 129 | case 'playing': 130 | status.innerText = 'An ad is in progress.'; 131 | status.style.fontWeight = 'bold'; 132 | break; 133 | default: 134 | throw new Error('Unexpected adState: "' + newVal + '"'); 135 | } 136 | }); 137 | })(); 138 | -------------------------------------------------------------------------------- /dashboard/checklist.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 16 | 17 | 18 |
19 | 20 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /dashboard/dialogs/edit-current-run.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 15 | 16 | 17 |
18 | 19 |
20 |
General Run Info
21 | 22 | 23 | 24 | 25 | Unknown/None 26 | 3DS 27 | Arcade 28 | Dreamcast 29 | DS 30 | Game Boy 31 | Game Boy Advance 32 | Game Boy Color 33 | GameCube 34 | Genesis 35 | Nintendo 64 36 | NES 37 | PC 38 | PlayStation 1 39 | PlayStation 2 40 | PlayStation 3 41 | PlayStation 4 42 | PSP 43 | Saturn 44 | SNES 45 | Wii 46 | WiiU 47 | Wii Virtal Console 48 | 49 | 50 | 51 | 52 |
53 | 54 | 55 |
56 |
57 | 58 | 119 | 120 | 121 | -------------------------------------------------------------------------------- /dashboard/dialogs/edit-total.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /dashboard/elements/ad-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 33 | 34 | 43 | 44 | 45 | 83 | -------------------------------------------------------------------------------- /dashboard/elements/ad-list.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 16 | 17 | 24 | 25 | 26 | 77 | -------------------------------------------------------------------------------- /dashboard/elements/disabled-cover.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 19 | 20 | 23 | 24 | 25 | 68 | -------------------------------------------------------------------------------- /dashboard/elements/edit-runner.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 18 | 19 | 24 | 25 | 26 | 35 | -------------------------------------------------------------------------------- /dashboard/elements/label-value.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 34 | 35 | 39 | 40 | 41 | 51 | -------------------------------------------------------------------------------- /dashboard/elements/time-ago.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 8 | 9 | 10 | 66 | -------------------------------------------------------------------------------- /dashboard/elements/tweet-item.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 124 | 125 | 159 | 160 | 161 | 211 | -------------------------------------------------------------------------------- /dashboard/interview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 35 | 36 | 37 |
38 |
39 |
Preview
40 | 41 | 42 | 43 | 44 | Take 45 |
46 | 47 |
48 |
Program
49 | 50 | 51 | 52 | 53 | 54 |
55 | Show 56 | Hide 57 | Auto 58 |
59 |
60 |
61 | 62 | 63 | 64 | The layout graphic appears to be closed. 65 |
66 | Open the layout graphic to enable these controls. 67 |
68 | 69 | 70 | The layout graphic is preloading. 71 |
72 | Please wait... 73 |
74 | 75 | 76 | This graphic is only available on the Interview layout. 77 | 78 |
79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /dashboard/interview.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var disabledCover = document.getElementById('cover'); 5 | 6 | var layoutState = nodecg.Replicant('layoutState'); 7 | layoutState.on('change', function(oldVal, newVal) { 8 | if (newVal.page === 'open') { 9 | if (layoutState.value.currentLayout !== 'interview') { 10 | disabledCover.reason = 'badLayout'; 11 | } else { 12 | disabledCover.reason = null; 13 | } 14 | } 15 | 16 | else { 17 | disabledCover.reason = newVal.page; 18 | } 19 | }); 20 | 21 | /* ----- */ 22 | 23 | var interviewNames = nodecg.Replicant('interviewNames'); 24 | var take = document.getElementById('take'); 25 | take.addEventListener('click', function() { 26 | interviewNames.value = [ 27 | document.getElementById('preview1').value, 28 | document.getElementById('preview2').value, 29 | document.getElementById('preview3').value, 30 | document.getElementById('preview4').value 31 | ]; 32 | }); 33 | 34 | interviewNames.on('change', function(oldVal, newVal) { 35 | document.getElementById('program1').value = newVal[0]; 36 | document.getElementById('program2').value = newVal[1]; 37 | document.getElementById('program3').value = newVal[2]; 38 | document.getElementById('program4').value = newVal[3]; 39 | }); 40 | 41 | /* ------ */ 42 | 43 | var show = document.getElementById('show'); 44 | var hide = document.getElementById('hide'); 45 | var auto = document.getElementById('auto'); 46 | var showing = nodecg.Replicant('interviewLowerthirdShowing'); 47 | 48 | show.addEventListener('click', function() { 49 | showing.value = true; 50 | }); 51 | 52 | hide.addEventListener('click', function() { 53 | showing.value = false; 54 | }); 55 | 56 | auto.addEventListener('click', function() { 57 | nodecg.sendMessage('pulseInterviewLowerthird', 10); 58 | }); 59 | 60 | showing.on('change', function(oldVal, newVal) { 61 | if (newVal) { 62 | show.setAttribute('disabled', 'true'); 63 | hide.removeAttribute('disabled'); 64 | auto.setAttribute('disabled', 'true'); 65 | } else { 66 | show.removeAttribute('disabled'); 67 | hide.setAttribute('disabled', 'true'); 68 | auto.removeAttribute('disabled'); 69 | } 70 | }); 71 | 72 | nodecg.Replicant('interviewLowerthirdPulsing').on('change', function(oldVal, newVal) { 73 | var shouldDisableHideButton = !showing.value ? true : newVal; 74 | if (shouldDisableHideButton) { 75 | hide.setAttribute('disabled', 'true'); 76 | } else { 77 | hide.removeAttribute('disabled'); 78 | } 79 | }); 80 | })(); 81 | -------------------------------------------------------------------------------- /dashboard/nowplaying.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 17 | 18 | 19 | Waiting for data... 20 | Show for 15 seconds 21 | 22 | 44 | 45 | 46 | -------------------------------------------------------------------------------- /dashboard/omnibar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /dashboard/schedule.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 38 | 39 | 40 | 41 | 42 |
43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | Take 51 | 52 | 53 |
54 | 55 |
56 |
Sequential Select
57 |
58 | 60 | 61 | 62 | Prev 63 | 64 | 65 | 67 | 68 | Next 69 | 70 | 71 |
72 | 73 |
74 | Next Run:  75 | 76 |
77 |
78 | 79 | 80 | Force Update 81 |
82 | 83 | 84 |
85 |
Current Run Info
86 | 87 |
88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 |
96 | 97 | 98 | EDIT CURRENT RUN 99 | 100 |
101 |
102 | 103 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /dashboard/schedule.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var toast = document.getElementById('toast'); 5 | var update = document.getElementById('update'); 6 | 7 | update.addEventListener('click', function() { 8 | update.setAttribute('disabled', 'true'); 9 | nodecg.sendMessage('updateSchedule', function (err, updated) { 10 | update.removeAttribute('disabled'); 11 | 12 | if (err) { 13 | console.error(err.message); 14 | toast.text = 'Error updating schedule. Check console.'; 15 | toast.show(); 16 | return; 17 | } 18 | 19 | if (updated) { 20 | console.info('[agdq16-layouts] Schedule successfully updated'); 21 | toast.text = 'Successfully updated schedule.'; 22 | toast.show(); 23 | } else { 24 | console.info('[agdq16-layouts] Schedule unchanged, not updated'); 25 | toast.text = 'Schedule unchanged, not updated.'; 26 | toast.show(); 27 | } 28 | }); 29 | }); 30 | 31 | /* ----- */ 32 | 33 | var typeahead = document.getElementById('typeahead'); 34 | typeahead.addEventListener('keyup', function(e) { 35 | // Enter key 36 | if (e.which === 13 && typeahead.inputValue) { 37 | takeTypeahead(); 38 | } 39 | }); 40 | 41 | var schedule = nodecg.Replicant('schedule'); 42 | schedule.on('change', function(oldVal, newVal) { 43 | typeahead.localCandidates = newVal.map(function(speedrun) { 44 | return speedrun.name; 45 | }); 46 | }); 47 | 48 | // This is quite inefficient, but it works for now. 49 | var take = document.getElementById('take'); 50 | take.addEventListener('click', takeTypeahead); 51 | 52 | function takeTypeahead() { 53 | take.setAttribute('disabled', 'true'); 54 | 55 | var nameToFind = typeahead.inputValue; 56 | 57 | // Find the run based on the name. 58 | var matched = schedule.value.some(function(run) { 59 | if (run.name.toLowerCase() === nameToFind.toLowerCase()) { 60 | nodecg.sendMessage('setCurrentRunByOrder', run.order, function() { 61 | take.removeAttribute('disabled'); 62 | typeahead.inputValue = ''; 63 | typeahead._suggestions = []; 64 | }); 65 | return true; 66 | } 67 | }); 68 | 69 | if (!matched) { 70 | take.removeAttribute('disabled'); 71 | toast.text = 'Could not find speedrun with name "' + nameToFind + '".'; 72 | toast.show(); 73 | } 74 | } 75 | 76 | /* ----- */ 77 | 78 | var nextBtn = document.getElementById('next'); 79 | var previousBtn = document.getElementById('previous'); 80 | var nextRunSpan = document.getElementById('nextRun'); 81 | 82 | nextBtn.addEventListener('click', function() { 83 | nextBtn.setAttribute('disabled', 'true'); 84 | nodecg.sendMessage('nextRun'); 85 | }); 86 | 87 | previousBtn.addEventListener('click', function() { 88 | previousBtn.setAttribute('disabled', 'true'); 89 | nodecg.sendMessage('previousRun'); 90 | }); 91 | 92 | schedule.on('declared', function() { 93 | var currentRun = nodecg.Replicant('currentRun'); 94 | var runInfoName = document.querySelector('label-value[label="Name"]'); 95 | var runInfoConsole = document.querySelector('label-value[label="Console"]'); 96 | var runInfoRunners = document.querySelector('label-value[label="Runners"]'); 97 | var runInfoReleaseYear = document.querySelector('label-value[label="Release Year"]'); 98 | var runInfoEstimate = document.querySelector('label-value[label="Estimate"]'); 99 | var runInfoCategory = document.querySelector('label-value[label="Category"]'); 100 | var runInfoOrder = document.querySelector('label-value[label="Order"]'); 101 | currentRun.on('change', function(oldVal, newVal) { 102 | if (!newVal) return; 103 | 104 | runInfoName.value = newVal.name; 105 | runInfoConsole.value = newVal.console; 106 | runInfoRunners.value = newVal.concatenatedRunners; 107 | runInfoReleaseYear.value = newVal.releaseYear; 108 | runInfoEstimate.value = newVal.estimate; 109 | runInfoCategory.value = newVal.category; 110 | runInfoOrder.value = newVal.order; 111 | 112 | // Disable "next" button if at end of schedule 113 | if (newVal.nextRun) { 114 | nextRunSpan.innerText = newVal.nextRun.name; 115 | nextBtn.removeAttribute('disabled'); 116 | } else { 117 | nextRunSpan.innerText = 'None'; 118 | nextBtn.setAttribute('disabled', 'true'); 119 | } 120 | 121 | // Disable "prev" button if at start of schedule 122 | if (newVal.order <= 1) { 123 | previousBtn.setAttribute('disabled', 'true'); 124 | } else { 125 | previousBtn.removeAttribute('disabled'); 126 | } 127 | }); 128 | }); 129 | })(); 130 | -------------------------------------------------------------------------------- /dashboard/shared-panel-styles.css: -------------------------------------------------------------------------------- 1 | paper-button { 2 | display: block; 3 | margin-left: 0; 4 | margin-right: 0; 5 | margin-bottom: 8px; 6 | } 7 | 8 | h5 { 9 | margin-bottom: 0; 10 | } 11 | 12 | hr { 13 | box-sizing: content-box; 14 | width: 90%; 15 | height: 0; 16 | border: 0; 17 | border-top: 1px solid #9E9E9E; 18 | margin-top: 21px; 19 | margin-bottom: 21px; 20 | } 21 | 22 | .column { 23 | margin-left: 8px; 24 | margin-right: 8px; 25 | } 26 | 27 | .column:first-child { 28 | margin-left: 0; 29 | } 30 | 31 | .column:last-child { 32 | margin-left: 0; 33 | } 34 | -------------------------------------------------------------------------------- /dashboard/total.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 25 | 26 | 27 | 28 | 29 |

?

30 | 31 |
32 | Force Update 33 | Edit... 34 |
35 | 36 | Automatic Updates 37 | 38 | 77 | 78 | 79 | -------------------------------------------------------------------------------- /dashboard/twitter.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 50 | 51 | 52 |
53 |
54 |
There are no tweets in the queue.
55 |
56 | 57 | 58 | 59 | The layout graphic appears to be closed. 60 |
61 | Open the layout graphic to enable these controls. 62 |
63 | 64 | 65 | The layout graphic is preloading. 66 |
67 | Please wait... 68 |
69 | 70 | 71 | This layout () does not support Twitter. 72 |
73 | Switch to another layout to enable these controls. 74 |
75 |
76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /dashboard/twitter.js: -------------------------------------------------------------------------------- 1 | /* jshint -W106 */ 2 | (function () { 3 | 'use strict'; 4 | 5 | var tweetsContainer = document.getElementById('tweets'); 6 | var tweets = nodecg.Replicant('tweets'); 7 | var disabledCover = document.getElementById('cover'); 8 | var empty = document.getElementById('empty'); 9 | var layoutName = disabledCover.querySelector('.layoutName'); 10 | 11 | tweets.on('change', function (oldVal, newVal) { 12 | empty.style.display = newVal.length > 0 ? 'none' : 'flex'; 13 | 14 | // Remove existing tweets from div 15 | while (tweetsContainer.firstChild) { 16 | tweetsContainer.removeChild(tweetsContainer.firstChild); 17 | } 18 | 19 | var sortedTweets = newVal.slice(0); 20 | sortedTweets.sort(function (a, b) { 21 | return new Date(b.created_at) - new Date(a.created_at); 22 | }); 23 | 24 | sortedTweets.forEach(function(tweet) { 25 | var tweetItem = document.createElement('tweet-item'); 26 | tweetItem.value = tweet; 27 | tweetsContainer.appendChild(tweetItem); 28 | }); 29 | }); 30 | 31 | var layoutState = nodecg.Replicant('layoutState'); 32 | layoutState.on('change', function (oldVal, newVal) { 33 | if (newVal.page === 'open') { 34 | layoutName.innerHTML = newVal.currentLayout; 35 | switch (newVal.currentLayout) { 36 | case '4x3_4': 37 | layoutName.innerHTML = '3x2_4, 4x3_4'; 38 | /* falls through */ 39 | case 'ds': 40 | disabledCover.reason = 'badLayout'; 41 | break; 42 | default: 43 | disabledCover.reason = null; 44 | } 45 | } 46 | 47 | else { 48 | disabledCover.reason = newVal.page; 49 | } 50 | }); 51 | })(); 52 | -------------------------------------------------------------------------------- /download_boxart.js: -------------------------------------------------------------------------------- 1 | /* jshint -W106 */ 2 | 'use strict'; 3 | 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var rp = require('request-promise'); 7 | var request = require('request'); 8 | var failed = []; 9 | 10 | var schedule; 11 | rp({ 12 | uri: 'https://gamesdonequick.com/tracker/search', 13 | qs: { 14 | type: 'run', 15 | event: 17 16 | }, 17 | json: true 18 | }).then(function(s) { 19 | schedule = s; 20 | fetchBoxart(schedule[0]); 21 | }); 22 | 23 | function fetchBoxart(run) { 24 | var boxartUrl = 'http://static-cdn.jtvnw.net/ttv-boxart/'+run.fields.name+'-469x655.jpg'; 25 | console.log('Fetching %s...', boxartUrl); 26 | 27 | var filename = new Buffer(run.fields.display_name).toString('base64'); 28 | var filepath = path.join(__dirname, '/graphics/img/boxart/' + filename + '.jpg'); 29 | 30 | var stream = request(boxartUrl); 31 | stream.pipe(fs.createWriteStream(filepath)); 32 | 33 | stream.on('error', function(err) { 34 | failed.push(run); 35 | console.error('Failed to fetch', boxartUrl); 36 | fetchNext(run); 37 | }); 38 | 39 | stream.on('end', function() { 40 | console.log('Successfully fetched', boxartUrl); 41 | fetchNext(run); 42 | }); 43 | } 44 | 45 | function fetchNext(run) { 46 | if (run.fields.order < schedule.length - 2) { 47 | fetchBoxart(schedule[run.fields.order]); 48 | } else { 49 | if (failed.length > 0) { 50 | console.warn('%s downloads failed, writing to failed.json', failed.length); 51 | fs.writeFileSync('failed.json', JSON.stringify(failed)); 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /electron.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | const electron = require('electron'); 3 | const app = electron.app; // Module to control application life. 4 | const BrowserWindow = electron.BrowserWindow; // Module to create native browser window. 5 | 6 | // Report crashes to our server. 7 | electron.crashReporter.start(); 8 | 9 | // Keep a global reference of the window object, if you don't, the window will 10 | // be closed automatically when the JavaScript object is garbage collected. 11 | let mainWindow; 12 | 13 | // Quit when all windows are closed. 14 | app.on('window-all-closed', function() { 15 | // On OS X it is common for applications and their menu bar 16 | // to stay active until the user quits explicitly with Cmd + Q 17 | if (process.platform != 'darwin') { 18 | app.quit(); 19 | } 20 | }); 21 | 22 | // This method will be called when Electron has finished 23 | // initialization and is ready to create browser windows. 24 | app.on('ready', function() { 25 | // Create the browser window. 26 | mainWindow = new BrowserWindow({ 27 | width: 1280, 28 | height: 720, 29 | useContentSize: true, 30 | resizable: false, 31 | fullscreen: false, 32 | frame: false 33 | }); 34 | 35 | // and load the index.html of the app. 36 | mainWindow.loadURL('http://localhost:9090/graphics/agdq16-layouts/index.html'); 37 | 38 | // Emitted when the window is closed. 39 | mainWindow.on('minimize', function() { 40 | mainWindow.restore(); 41 | }); 42 | }); 43 | -------------------------------------------------------------------------------- /extension/advertisements.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chokidar = require('chokidar'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var format = require('util').format; 7 | var debounce = require('debounce'); 8 | var md5File = require('md5-file'); 9 | 10 | var ADVERTISEMENTS_PATH = path.resolve(__dirname, '../graphics/advertisements'); 11 | var BASE_URL = '/graphics/agdq16-layouts/advertisements/'; 12 | var IMAGE_EXTS = ['.png', '.jpg', '.gif']; 13 | var VIDEO_EXTS = ['.webm']; 14 | 15 | module.exports = function(nodecg) { 16 | nodecg.log.info('Monitoring "%s" for changes to advertisement assets...', ADVERTISEMENTS_PATH); 17 | 18 | var currentRun = nodecg.Replicant('currentRun'); 19 | nodecg.listenFor('logAdPlay', function(ad) { 20 | var logStr = format('%s, %s, %s, %s\n', 21 | new Date().toISOString(), ad.filename, currentRun.value.name, currentRun.value.concatenatedRunners); 22 | 23 | fs.appendFile('logs/ad_log.csv', logStr, function (err) { 24 | if (err) { 25 | nodecg.log.error('[advertisements] Error appending to log:', err.stack); 26 | } 27 | }); 28 | }); 29 | 30 | var ads = nodecg.Replicant('ads', {defaultValue: [], persistent: false}); 31 | nodecg.Replicant('ftb', {defaultValue: false}); 32 | 33 | var watcher = chokidar.watch([ 34 | ADVERTISEMENTS_PATH + '/*.png', 35 | ADVERTISEMENTS_PATH + '/*.jpg', 36 | ADVERTISEMENTS_PATH + '/*.gif', 37 | ADVERTISEMENTS_PATH + '/*.webm' 38 | ],{ 39 | ignored: /[\/\\]\./, 40 | persistent: true, 41 | ignoreInitial: true 42 | }); 43 | 44 | watcher.on('add', debounce(reloadAdvertisements, 500)); 45 | watcher.on('change', debounce(reloadAdvertisements, 500)); 46 | 47 | watcher.on('unlink', function(filepath) { 48 | var adFilename = path.basename(filepath); 49 | nodecg.log.info('Advertisement "%s" deleted, removing from list...', adFilename); 50 | 51 | ads.value.some(function(ad, index) { 52 | if (ad.filename === adFilename) { 53 | var adData = ads.value[index]; 54 | ads.value.splice(index, 1); 55 | nodecg.sendMessage('adRemoved', adData); 56 | return true; 57 | } 58 | }); 59 | }); 60 | 61 | watcher.on('error', function(e) { 62 | nodecg.error(e.stack); 63 | }); 64 | 65 | // Initialize 66 | reloadAdvertisements(); 67 | 68 | // On changed/added 69 | function reloadAdvertisements(filepath) { 70 | if (filepath) { 71 | nodecg.log.info('Advertisement "%s" changed, reloading all advertisements...', path.basename(filepath)); 72 | } 73 | 74 | // Scan the images dir 75 | var adsDir = fs.readdirSync(ADVERTISEMENTS_PATH); 76 | adsDir.forEach(function(adFilename) { 77 | var ext = path.extname(adFilename); 78 | var adPath = path.join(ADVERTISEMENTS_PATH, adFilename); 79 | 80 | var type; 81 | if (isImage(ext)) { 82 | type = 'image'; 83 | } else if (isVideo(ext)) { 84 | type = 'video'; 85 | } else { 86 | return; 87 | } 88 | 89 | md5File(adPath, function (err, sum) { 90 | if (err) { 91 | nodecg.log.error(err); 92 | return; 93 | } 94 | 95 | var adData = { 96 | url: BASE_URL + adFilename, 97 | filename: adFilename, 98 | type: type, 99 | checksum: sum 100 | }; 101 | 102 | // Look for an existing entry in the replicant with this filename, and update if found and md5 changed. 103 | var foundExistingAd = ads.value.some(function(ad, index) { 104 | if (ad.filename === adFilename) { 105 | if (ad.checksum !== sum) { 106 | ads.value[index] = adData; 107 | nodecg.sendMessage('adChanged', adData); 108 | } 109 | return true; 110 | } 111 | }); 112 | 113 | // If there was no existing ad with this filename, add a new one. 114 | if (!foundExistingAd) { 115 | ads.value.push(adData); 116 | nodecg.sendMessage('newAd', adData); 117 | } 118 | }); 119 | }); 120 | } 121 | }; 122 | 123 | function isImage(ext) { 124 | return IMAGE_EXTS.indexOf(ext) >= 0; 125 | } 126 | 127 | function isVideo(ext) { 128 | return VIDEO_EXTS.indexOf(ext) >= 0; 129 | } 130 | -------------------------------------------------------------------------------- /extension/checklist.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(nodecg) { 4 | // Create defaults array 5 | var checklistDefault = [ 6 | {name: 'Check for Interview', complete: false}, 7 | {name: 'Cue game music', complete: false}, 8 | {name: 'Check for Advertisement', complete: false}, 9 | {name: 'Commentator Mics', complete: false}, 10 | {name: 'Runner Game Audio', complete: false}, 11 | {name: 'TVs have Video', complete: false}, 12 | {name: 'Restart Recording', complete: false}, 13 | {name: 'Stream Audio', complete: false}, 14 | {name: 'Stream Video & Deinterlacing', complete: false}, 15 | {name: 'Stream Layout', complete: false}, 16 | {name: 'RACE ONLY: Confirm Runner Names Match Game Positions', complete: false}, 17 | {name: 'STEAM ONLY: Turn off Steam notifications', complete: false}, 18 | {name: 'Camera', complete: false}, 19 | {name: 'Reset Timer', complete: false}, 20 | {name: 'Check Notes', complete: false} 21 | ]; 22 | 23 | // Instantiate replicant with defaults object, which will load if no persisted data is present. 24 | var checklist = nodecg.Replicant('checklist', {defaultValue: checklistDefault}); 25 | 26 | // If any entries in the config aren't present in the replicant, 27 | // (which could happen when a persisted replicant value is loaded) add them. 28 | checklistDefault.forEach(function(task){ 29 | var exists = checklist.value.some(function(existingTask) { 30 | return existingTask.name === task.name; 31 | }); 32 | 33 | if (!exists) { 34 | checklist.value.push(task); 35 | } 36 | }); 37 | 38 | // Likewise, if there are any entries in the replicant that are no longer present in the config, remove them. 39 | checklist.value.forEach(function(existingTask, index) { 40 | var exists = checklistDefault.some(function(task) { 41 | return task.name === existingTask.name; 42 | }); 43 | 44 | if (!exists) { 45 | checklist.value.splice(index, 1); 46 | } 47 | }); 48 | 49 | var checklistComplete = nodecg.Replicant('checklistComplete', {defaultValue: false}); 50 | checklist.on('change', function(oldVal, newVal) { 51 | var numUnfinishedTasks = newVal.filter(function(task) { 52 | return !task.complete; 53 | }).length; 54 | 55 | checklistComplete.value = numUnfinishedTasks === 0; 56 | }); 57 | 58 | return { 59 | reset: function() { 60 | checklist.value.forEach(function(task) { 61 | task.complete = false; 62 | }); 63 | } 64 | }; 65 | }; 66 | -------------------------------------------------------------------------------- /extension/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(nodecg) { 4 | // Initialize this here because there's kinda nowhere better to do it. 5 | nodecg.Replicant('displayDuration', {defaultValue: 10}); 6 | 7 | try { 8 | require('./schedule')(nodecg); 9 | } catch (e) { 10 | nodecg.log.error('Failed to load "schedule" lib:', e.stack); 11 | process.exit(1); 12 | } 13 | 14 | try { 15 | require('./prizes')(nodecg); 16 | } catch (e) { 17 | nodecg.log.error('Failed to load "prizes" lib:', e.stack); 18 | process.exit(1); 19 | } 20 | 21 | try { 22 | require('./bids')(nodecg); 23 | } catch (e) { 24 | nodecg.log.error('Failed to load "bids" lib:', e.stack); 25 | process.exit(1); 26 | } 27 | 28 | try { 29 | require('./total')(nodecg); 30 | } catch (e) { 31 | nodecg.log.error('Failed to load "total" lib:', e.stack); 32 | process.exit(1); 33 | } 34 | 35 | try { 36 | require('./stopwatches')(nodecg); 37 | } catch (e) { 38 | nodecg.log.error('Failed to load "stopwatches" lib:', e.stack); 39 | process.exit(1); 40 | } 41 | 42 | try { 43 | require('./sponsors')(nodecg); 44 | } catch (e) { 45 | nodecg.log.error('Failed to load "sponsors" lib:', e.stack); 46 | process.exit(1); 47 | } 48 | 49 | try { 50 | require('./advertisements')(nodecg); 51 | } catch (e) { 52 | nodecg.log.error('Failed to load "advertisements" lib:', e.stack); 53 | process.exit(1); 54 | } 55 | 56 | try { 57 | require('./twitter')(nodecg); 58 | } catch (e) { 59 | nodecg.log.error('Failed to load "twitter" lib:', e.stack); 60 | process.exit(1); 61 | } 62 | 63 | try { 64 | require('./osc')(nodecg); 65 | } catch (e) { 66 | nodecg.log.error('Failed to load "osc" lib:', e.stack); 67 | process.exit(1); 68 | } 69 | 70 | try { 71 | require('./interview')(nodecg); 72 | } catch (e) { 73 | nodecg.log.error('Failed to load "interview" lib:', e.stack); 74 | process.exit(1); 75 | } 76 | 77 | try { 78 | require('./nowplaying')(nodecg); 79 | } catch (e) { 80 | nodecg.log.error('Failed to load "nowplaying" lib:', e.stack); 81 | process.exit(1); 82 | } 83 | 84 | try { 85 | require('./state')(nodecg); 86 | } catch (e) { 87 | nodecg.log.error('Failed to load "state" lib:', e.stack); 88 | process.exit(1); 89 | } 90 | }; 91 | -------------------------------------------------------------------------------- /extension/interview.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(nodecg) { 4 | var lowerthirdShowing = nodecg.Replicant('interviewLowerthirdShowing', {defaultValue: false, persistent: false}); 5 | var lowerthirdPulsing = nodecg.Replicant('interviewLowerthirdPulsing', {defaultValue: false, persistent: false}); 6 | nodecg.Replicant('interviewNames', {defaultValue: [], persistent: false}); 7 | 8 | nodecg.listenFor('pulseInterviewLowerthird', function pulse(duration) { 9 | // Don't stack pulses 10 | if (lowerthirdPulsing.value) return; 11 | 12 | lowerthirdShowing.value = true; 13 | lowerthirdPulsing.value = true; 14 | 15 | // End pulse after "duration" seconds 16 | setTimeout(function() { 17 | lowerthirdShowing.value = false; 18 | lowerthirdPulsing.value = false; 19 | }, duration * 1000); 20 | }); 21 | }; 22 | -------------------------------------------------------------------------------- /extension/nowplaying.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var LastFmNode = require('lastfm').LastFmNode; 4 | 5 | module.exports = function(nodecg) { 6 | if (!nodecg.bundleConfig) { 7 | nodecg.log.error('cfg/agdq16-layouts.json was not found. "Now playing" graphic will be disabled.'); 8 | return; 9 | } else if (typeof nodecg.bundleConfig.lastfm === 'undefined') { 10 | nodecg.log.error('"lastfm" is not defined in cfg/agdq16-layouts.json! ' + 11 | '"Now playing" graphic will be disabled.'); 12 | return; 13 | } 14 | 15 | /* jshint -W106 */ 16 | var lastfm = new LastFmNode({ 17 | api_key: nodecg.bundleConfig.lastfm.apiKey, 18 | secret: nodecg.bundleConfig.lastfm.secret 19 | }); 20 | var trackStream = lastfm.stream(nodecg.bundleConfig.lastfm.targetAccount); 21 | /* jshint +W106 */ 22 | 23 | var pulseTimeout; 24 | var pulsing = nodecg.Replicant('nowPlayingPulsing', {defaultValue: false, persistent: false}); 25 | var nowPlaying = nodecg.Replicant('nowPlaying', {defaultValue: {}, persistent: false}); 26 | 27 | nodecg.listenFor('pulseNowPlaying', pulse); 28 | function pulse() { 29 | // Don't stack pulses 30 | if (pulsing.value) return; 31 | pulsing.value = true; 32 | 33 | // Hard-coded 12 second duration 34 | pulseTimeout = setTimeout(function() { 35 | pulsing.value = false; 36 | }, 12 * 1000); 37 | } 38 | 39 | trackStream.on('nowPlaying', function(track) { 40 | var newNp = { 41 | artist: track.artist['#text'], 42 | song: track.name, 43 | album: track.album['#text'] || track.artist['#text'], 44 | cover: track.image.pop()['#text'], 45 | artistSong: track.artist['#text'] + ' - ' + track.name 46 | }; 47 | 48 | // As of 2015-11-22, Last.fm seems to sometimes send duplicate "nowPlaying" events. 49 | // This filters them out. 50 | if (typeof nowPlaying.value.artistSong === 'string') { 51 | if (newNp.artistSong.toLowerCase() === nowPlaying.value.artistSong.toLowerCase()) { 52 | return; 53 | } 54 | } 55 | 56 | nowPlaying.value = newNp; 57 | 58 | // If the graphic is already showing, end it prematurely and show the new song 59 | if (pulsing.value) { 60 | clearTimeout(pulseTimeout); 61 | pulsing.value = false; 62 | } 63 | 64 | // Show the graphic 65 | pulse(); 66 | }); 67 | 68 | trackStream.on('error', function() { 69 | // Just ignore it, this lib generates tons of errors. 70 | }); 71 | 72 | trackStream.start(); 73 | }; 74 | -------------------------------------------------------------------------------- /extension/osc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /* 4 | * NOTE: It is absolutely critical that the `args` param of any udpPort.send command not be null or undefined. 5 | * Doing so causes the osc lib to actually encode it as a null argument (,N). Instead, use an empty array ([]). 6 | */ 7 | 8 | var X32_UDP_PORT = 10023; 9 | var FADE_THRESHOLD = 0.12; 10 | var DEFAULT_CHANNEL_OBJ = { 11 | sd: {muted: true, fadedBelowThreshold: true}, 12 | hd: {muted: true, fadedBelowThreshold: true} 13 | }; 14 | 15 | var clone = require('clone'); 16 | var osc = require('osc'); 17 | 18 | module.exports = function(nodecg) { 19 | var gameAudioChannels = nodecg.Replicant('gameAudioChannels', { 20 | defaultValue: [ 21 | clone(DEFAULT_CHANNEL_OBJ), 22 | clone(DEFAULT_CHANNEL_OBJ), 23 | clone(DEFAULT_CHANNEL_OBJ), 24 | clone(DEFAULT_CHANNEL_OBJ) 25 | ], 26 | persistent: false 27 | }); 28 | 29 | if (!nodecg.bundleConfig) { 30 | nodecg.log.error('cfg/agdq16-layouts.json was not found. Behringer X32 OSC integration will be disabled.'); 31 | return; 32 | } else if (typeof nodecg.bundleConfig.twitter === 'undefined') { 33 | nodecg.log.error('"x32" is not defined in cfg/agdq16-layouts.json! ' + 34 | 'Behringer X32 OSC integration will be disabled.'); 35 | return; 36 | } 37 | 38 | var channelNumberToReplicantObject = {}; 39 | nodecg.bundleConfig.x32.gameAudioChannels.forEach(function(item, index) { 40 | if (typeof item.sd === 'number') { 41 | channelNumberToReplicantObject[item.sd] = gameAudioChannels.value[index].sd; 42 | } 43 | 44 | if (typeof item.hd === 'number') { 45 | channelNumberToReplicantObject[item.hd] = gameAudioChannels.value[index].hd; 46 | } 47 | }); 48 | 49 | var udpPort = new osc.UDPPort({ 50 | localAddress: '0.0.0.0', 51 | localPort: 52361, 52 | remoteAddress: nodecg.bundleConfig.x32.address, 53 | remotePort: X32_UDP_PORT, 54 | metadata: true 55 | }); 56 | 57 | udpPort.on('raw', function (buf) { 58 | var str = buf.toString('ascii'); 59 | var valueBytes, replicantObject; 60 | var channelNumber = 0; 61 | var i = 0; 62 | var valueArray = []; 63 | 64 | if (str.indexOf('/chMutes') === 0) { 65 | // For this particular message, we know that the values start at byte 21 and stop 3 bytes from the end. 66 | valueBytes = buf.slice(21, -3); 67 | 68 | for (i = 0; i < valueBytes.length; i+=4) { 69 | var muted = !Boolean(valueBytes.readFloatBE(i)); 70 | valueArray.push(muted); 71 | 72 | replicantObject = channelNumberToReplicantObject[String(channelNumber+1)]; 73 | if (replicantObject) { 74 | replicantObject.muted = muted; 75 | } 76 | 77 | channelNumber++; 78 | } 79 | } 80 | 81 | else if (str.indexOf('/chFaders') === 0) { 82 | // For this particular message, we know that the values start at byte 24 83 | valueBytes = buf.slice(24); 84 | 85 | for (i = 0; i < valueBytes.length; i+=4) { 86 | var fadedBelowThreshold = valueBytes.readFloatLE(i) < FADE_THRESHOLD; 87 | valueArray.push(fadedBelowThreshold); 88 | 89 | replicantObject = channelNumberToReplicantObject[String(channelNumber+1)]; 90 | if (replicantObject) { 91 | replicantObject.fadedBelowThreshold = fadedBelowThreshold; 92 | } 93 | 94 | channelNumber++; 95 | } 96 | } 97 | }); 98 | 99 | udpPort.on('error', function (error) { 100 | nodecg.log.warn('[osc] Error:', error.stack); 101 | }); 102 | 103 | udpPort.on('open', function () { 104 | nodecg.log.info('[osc] Connected to Behringer X32'); 105 | }); 106 | 107 | udpPort.on('close', function () { 108 | nodecg.log.warn('[osc] Disconnected from Behringer X32'); 109 | }); 110 | 111 | // Open the socket. 112 | udpPort.open(); 113 | 114 | renewSubscriptions(); 115 | setInterval(renewSubscriptions, 10000); 116 | 117 | function renewSubscriptions() { 118 | udpPort.send({ 119 | address: '/batchsubscribe', 120 | args: [ 121 | // This first argument seems to define local endpoint that the X32 will send this subscription data to. 122 | {type: 's', value: '/chMutes'}, 123 | {type: 's', value: '/mix/on'}, 124 | {type: 'i', value: 0}, 125 | {type: 'i', value: 63}, 126 | {type: 'i', value: 10} 127 | ] 128 | }); 129 | 130 | udpPort.send({ 131 | address: '/batchsubscribe', 132 | args: [ 133 | // This first argument seems to define local endpoint that the X32 will send this subscription data to. 134 | {type: 's', value: '/chFaders'}, 135 | {type: 's', value: '/mix/fader'}, 136 | {type: 'i', value: 0}, 137 | {type: 'i', value: 63}, 138 | {type: 'i', value: 10} 139 | ] 140 | }); 141 | } 142 | }; 143 | -------------------------------------------------------------------------------- /extension/prizes.js: -------------------------------------------------------------------------------- 1 | /* jshint -W106 */ 2 | 'use strict'; 3 | 4 | var PRIZES_URL = 'https://gamesdonequick.com/tracker/search/?type=prize&event=17'; 5 | var CURRENT_PRIZES_URL = 'https://gamesdonequick.com/tracker/search/?type=prize&feed=current&event=17'; 6 | //var PRIZES_URL = 'https://dl.dropboxusercontent.com/u/6089084/agdq_mock/allPrizes.json'; 7 | //var CURRENT_PRIZES_URL = 'https://dl.dropboxusercontent.com/u/6089084/agdq_mock/currentPrizes.json'; 8 | 9 | var POLL_INTERVAL = 60 * 1000; 10 | 11 | var format = require('util').format; 12 | var Q = require('q'); 13 | var request = require('request'); 14 | var equal = require('deep-equal'); 15 | var numeral = require('numeral'); 16 | 17 | module.exports = function (nodecg) { 18 | var currentPrizes = nodecg.Replicant('currentPrizes', {defaultValue: []}); 19 | var allPrizes = nodecg.Replicant('allPrizes', {defaultValue: []}); 20 | 21 | // Get initial data 22 | update(); 23 | 24 | // Get latest prize data every POLL_INTERVAL milliseconds 25 | nodecg.log.info('Polling prizes every %d seconds...', POLL_INTERVAL / 1000); 26 | var updateInterval = setInterval(update.bind(this), POLL_INTERVAL); 27 | 28 | // Dashboard can invoke manual updates 29 | nodecg.listenFor('updatePrizes', function(data, cb) { 30 | nodecg.log.info('Manual prize update button pressed, invoking update...'); 31 | clearInterval(updateInterval); 32 | updateInterval = setInterval(update.bind(this), POLL_INTERVAL); 33 | update() 34 | .spread(function (updatedCurrent, updatedAll) { 35 | var updatedEither = updatedCurrent || updatedAll; 36 | if (updatedEither) { 37 | nodecg.log.info('Prizes successfully updated'); 38 | } else { 39 | nodecg.log.info('Prizes unchanged, not updated'); 40 | } 41 | 42 | cb(null, updatedEither); 43 | }, function (error) { 44 | cb(error); 45 | }); 46 | }); 47 | 48 | function update() { 49 | var currentPromise = Q.defer(); 50 | request(CURRENT_PRIZES_URL, function(err, res, body) { 51 | handleResponse(err, res, body, currentPromise, { 52 | label: 'current prizes', 53 | replicant: currentPrizes 54 | }); 55 | }); 56 | 57 | var allPromise = Q.defer(); 58 | request(PRIZES_URL, function(err, res, body) { 59 | handleResponse(err, res, body, allPromise, { 60 | label: 'all prizes', 61 | replicant: allPrizes 62 | }); 63 | }); 64 | 65 | return Q.all([ 66 | currentPromise.promise, 67 | allPromise.promise 68 | ]); 69 | } 70 | 71 | function handleResponse(error, response, body, deferred, opts) { 72 | if (!error && response.statusCode === 200) { 73 | var prizes; 74 | 75 | try { 76 | prizes = JSON.parse(body); 77 | } catch(e) { 78 | nodecg.log.error('Could not parse %s, response not valid JSON:\n\t', opts.label, body); 79 | return; 80 | } 81 | 82 | // The response we get has a tremendous amount of cruft that we just don't need. We filter that out. 83 | var relevantData = prizes.map(formatPrize); 84 | 85 | if (equal(relevantData, opts.replicant.value)) { 86 | deferred.resolve(false); 87 | } else { 88 | opts.replicant.value = relevantData; 89 | deferred.resolve(true); 90 | } 91 | } else { 92 | var msg = format('Could not get %s, unknown error', opts.label); 93 | if (error) msg = format('Could not get %s:', opts.label, error.message); 94 | else if (response) msg = format('Could not get %s, response code %d', opts.label, response.statusCode); 95 | nodecg.log.error(msg); 96 | deferred.reject(msg); 97 | } 98 | } 99 | 100 | function formatPrize(prize) { 101 | return { 102 | name: prize.fields.name, 103 | provided: prize.fields.provider, 104 | description: prize.fields.shortdescription || prize.fields.name, 105 | image: prize.fields.altimage, 106 | minimumbid: numeral(prize.fields.minimumbid).format('$0,0[.]00'), 107 | grand: prize.fields.category__name === 'Grand', 108 | type: 'prize' 109 | }; 110 | } 111 | }; 112 | -------------------------------------------------------------------------------- /extension/sponsors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chokidar = require('chokidar'); 4 | var debounce = require('debounce'); 5 | var fs = require('fs'); 6 | var md5File = require('md5-file'); 7 | var path = require('path'); 8 | 9 | var SPONSOR_IMAGES_PATH = path.resolve(__dirname, '../graphics/img/sponsors'); 10 | var BASE_URL = '/graphics/agdq16-layouts/img/sponsors/'; 11 | var ALLOWED_EXTS = [ 12 | '.png' 13 | ]; 14 | 15 | module.exports = function(nodecg) { 16 | nodecg.log.info('Monitoring "%s" for changes to sponsor logos...', SPONSOR_IMAGES_PATH); 17 | 18 | var sponsors = nodecg.Replicant('sponsors', {defaultValue: [], persistent: false}); 19 | var watcher = chokidar.watch(SPONSOR_IMAGES_PATH + '/*.png', { 20 | ignored: /[\/\\]\./, 21 | ignoreInitial: true 22 | }); 23 | 24 | watcher.on('add', debounce(reloadSponsors, 500)); 25 | watcher.on('change', debounce(reloadSponsors, 500)); 26 | 27 | watcher.on('unlink', function(filepath) { 28 | var parsedPath = path.parse(filepath); 29 | var nameParts = parsedPath.name.split('-'); 30 | var sponsorName = nameParts[0]; 31 | var orientation = nameParts[1]; 32 | 33 | if (!sponsorName || !isValidOrientation(orientation) || nameParts.length !== 2) { 34 | return; 35 | } 36 | 37 | sponsors.value.some(function(sponsor, index) { 38 | if (sponsor.name === sponsorName) { 39 | sponsor[orientation] = null; 40 | if (!sponsor.vertical && !sponsor.horizontal) { 41 | sponsors.value.splice(index, 1); 42 | } 43 | nodecg.log.info('[sponsors] "%s" deleted, removing from rotation', parsedPath.base); 44 | return true; 45 | } 46 | }); 47 | }); 48 | 49 | watcher.on('error', function(e) { 50 | nodecg.error(e.stack); 51 | }); 52 | 53 | // Initialize 54 | reloadSponsors(); 55 | 56 | // On changed/added 57 | function reloadSponsors(changeOrAddition) { 58 | if (changeOrAddition) { 59 | nodecg.log.info('[sponsors] Change detected, reloading all sponsors...'); 60 | } 61 | 62 | // Scan the images dir 63 | var sponsorsDir = fs.readdirSync(SPONSOR_IMAGES_PATH); 64 | sponsorsDir.forEach(function(filename) { 65 | var ext = path.extname(filename); 66 | var filepath = path.join(SPONSOR_IMAGES_PATH, filename); 67 | 68 | if (!extAllowed(ext)) { 69 | return; 70 | } 71 | 72 | var parsedPath = path.parse(filepath); 73 | var nameParts = parsedPath.name.split('-'); 74 | var sponsorName = nameParts[0]; 75 | var orientation = nameParts[1]; 76 | 77 | if (!sponsorName || !isValidOrientation(orientation) || nameParts.length !== 2) { 78 | nodecg.log.error('[sponsors] Unexpected file name "%s". ' + 79 | 'Please rename to this format: {name}-{orientation}.png', filename); 80 | return; 81 | } 82 | 83 | md5File(filepath, function (err, sum) { 84 | if (err) { 85 | nodecg.log.error(err); 86 | return; 87 | } 88 | 89 | var fileData = { 90 | url: BASE_URL + filename, 91 | filename: filename, 92 | checksum: sum 93 | }; 94 | 95 | // Look for an existing entry in the replicant with this filename, and update if found and md5 changed. 96 | var foundExistingSponsor = sponsors.value.some(function(sponsor) { 97 | if (sponsor.name === sponsorName) { 98 | if (!sponsor[orientation] || sponsor[orientation].checksum !== sum) { 99 | sponsor[orientation] = fileData; 100 | } 101 | return true; 102 | } 103 | }); 104 | 105 | // If there was no existing sponsor with this filename, add a new one. 106 | if (!foundExistingSponsor) { 107 | var sponsor = {name: sponsorName}; 108 | sponsor[orientation] = fileData; 109 | sponsors.value.push(sponsor); 110 | } 111 | }); 112 | }); 113 | } 114 | }; 115 | 116 | function extAllowed(ext) { 117 | return ALLOWED_EXTS.indexOf(ext) >= 0; 118 | } 119 | 120 | function isValidOrientation(orientation) { 121 | if (typeof orientation !== 'string') return false; 122 | return orientation === 'horizontal' || orientation === 'vertical'; 123 | } 124 | -------------------------------------------------------------------------------- /extension/state.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var singleInstance = require('../../../lib/graphics/single_instance'); 4 | 5 | module.exports = function(nodecg) { 6 | var adState = nodecg.Replicant('adState', {defaultValue: 'stopped', persistent: false}); 7 | var layoutState = nodecg.Replicant('layoutState', { 8 | defaultValue: { 9 | currentLayout: null, 10 | page: 'closed' 11 | }, 12 | persistent: false 13 | }); 14 | 15 | singleInstance.on('graphicAvailable', function(url) { 16 | if (url === '/graphics/agdq16-layouts/index.html') { 17 | layoutState.value.page = 'closed'; 18 | layoutState.value.currentLayout = null; 19 | adState.value = 'stopped'; 20 | } 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /extension/total.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var DONATION_STATS_URL = 'https://gamesdonequick.com/tracker/17?json'; 4 | var POLL_INTERVAL = 60 * 1000; 5 | 6 | var util = require('util'); 7 | var Q = require('q'); 8 | var request = require('request'); 9 | var numeral = require('numeral'); 10 | 11 | var updateInterval; 12 | 13 | module.exports = function (nodecg) { 14 | var total = nodecg.Replicant('total', { 15 | defaultValue: { 16 | raw: 0, 17 | formatted: '$0' 18 | } 19 | }); 20 | 21 | var autoUpdateTotal = nodecg.Replicant('autoUpdateTotal', {defaultValue: true}); 22 | autoUpdateTotal.on('change', function(oldVal, newVal) { 23 | if (newVal) { 24 | nodecg.log.info('Automatic updating of donation total enabled'); 25 | updateTotal(true); 26 | } else { 27 | nodecg.log.warn('Automatic updating of donation total DISABLED'); 28 | clearInterval(updateInterval); 29 | } 30 | }); 31 | 32 | nodecg.listenFor('setTotal', function(raw) { 33 | total.value = { 34 | raw: parseFloat(raw), 35 | formatted: numeral(raw).format('$0,0') 36 | }; 37 | }); 38 | 39 | // Get initial data 40 | update(); 41 | 42 | if (autoUpdateTotal.value) { 43 | // Get latest prize data every POLL_INTERVAL milliseconds 44 | nodecg.log.info('Polling donation total every %d seconds...', POLL_INTERVAL / 1000); 45 | clearInterval(updateInterval); 46 | updateInterval = setInterval(update, POLL_INTERVAL); 47 | } else { 48 | nodecg.log.info('Automatic update of total is disabled, will not poll until enabled'); 49 | } 50 | 51 | // Dashboard can invoke manual updates 52 | nodecg.listenFor('updateTotal', updateTotal); 53 | 54 | function updateTotal(silent, cb) { 55 | if (!silent) nodecg.log.info('Manual donation total update button pressed, invoking update...'); 56 | clearInterval(updateInterval); 57 | updateInterval = setInterval(update, POLL_INTERVAL); 58 | update() 59 | .then(function (updated) { 60 | if (updated) { 61 | nodecg.log.info('Donation total successfully updated'); 62 | } else { 63 | nodecg.log.info('Donation total unchanged, not updated'); 64 | } 65 | 66 | cb(null, updated); 67 | }, function (error) { 68 | cb(error); 69 | }); 70 | } 71 | 72 | function update() { 73 | var deferred = Q.defer(); 74 | request(DONATION_STATS_URL, function (error, response, body) { 75 | if (!error && response.statusCode === 200) { 76 | var stats; 77 | 78 | try { 79 | stats = JSON.parse(body); 80 | } catch(e) { 81 | nodecg.log.error('Could not parse total, response not valid JSON:\n\t', body); 82 | return; 83 | } 84 | 85 | var freshTotal = parseFloat(stats.agg.amount || 0); 86 | 87 | if (freshTotal !== total.value.raw) { 88 | total.value = { 89 | raw: freshTotal, 90 | formatted: numeral(freshTotal).format('$0,0') 91 | }; 92 | deferred.resolve(true); 93 | } else { 94 | deferred.resolve(false); 95 | } 96 | } else { 97 | var msg = 'Could not get donation total, unknown error'; 98 | if (error) msg = util.format('Could not get donation total:', error.message); 99 | else if (response) msg = util.format('Could not get donation total, response code %d', 100 | response.statusCode); 101 | nodecg.log.error(msg); 102 | deferred.reject(msg); 103 | } 104 | }); 105 | return deferred.promise; 106 | } 107 | }; 108 | -------------------------------------------------------------------------------- /extension/twitter/shared.css: -------------------------------------------------------------------------------- 1 | .agdqHashtag { 2 | font-weight: 700; 3 | color: #6ecff6; 4 | } 5 | -------------------------------------------------------------------------------- /graphics/advertisements/.empty_directory: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/advertisements/.empty_directory -------------------------------------------------------------------------------- /graphics/app/classes/stage.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(function() { 3 | 'use strict'; 4 | 5 | var createjs = requirejs('easel'); 6 | var containerEl = document.getElementById('container'); 7 | 8 | function Stage(w, h, id) { 9 | // Create the canvas element that will become the render target. 10 | var stageEl = document.createElement('canvas'); 11 | if (id) stageEl.id = id; 12 | stageEl.width = w; 13 | stageEl.height = h; 14 | stageEl.style.position = 'absolute'; 15 | 16 | // Add the canvas to the DOM 17 | containerEl.appendChild(stageEl); 18 | 19 | // Create the stage on the target canvas, and create a ticker that will render at 60 fps. 20 | var stage = new createjs.Stage(stageEl); 21 | createjs.Ticker.addEventListener('tick', function(event) { 22 | if (Stage.globalPause || event.paused) return; 23 | stage.update(); 24 | }); 25 | 26 | return stage; 27 | } 28 | 29 | Stage.globalPause = false; 30 | 31 | return Stage; 32 | }); 33 | -------------------------------------------------------------------------------- /graphics/app/components/background.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'debug', 4 | 'preloader' 5 | ], function(debug, preloader) { 6 | 'use strict'; 7 | 8 | var containerEl = document.getElementById('container'); 9 | var lastBg; 10 | 11 | return function(bgName) { 12 | debug.log('[background] setBackground(%s)', bgName); 13 | 14 | // Remove the last background, if any. 15 | if (lastBg) { 16 | lastBg.remove(); 17 | } 18 | 19 | var newBg = preloader.getResult('bg-' + bgName); 20 | newBg.id = 'background'; 21 | containerEl.appendChild(newBg); 22 | lastBg = newBg; 23 | }; 24 | }); 25 | -------------------------------------------------------------------------------- /graphics/app/components/compact_nameplates.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'globals', 4 | 'classes/compact_nameplate' 5 | ], function (globals, CompactNameplate) { 6 | 'use strict'; 7 | 8 | var compactNameplates = [ 9 | new CompactNameplate(0, 'left'), 10 | new CompactNameplate(1, 'right'), 11 | new CompactNameplate(2, 'left'), 12 | new CompactNameplate(3, 'right') 13 | ]; 14 | 15 | // Start disabled 16 | compactNameplates.forEach(function(nameplate) { 17 | nameplate.disable(); 18 | }); 19 | 20 | return { 21 | disable: function() { 22 | compactNameplates.forEach(function(nameplate) { 23 | nameplate.disable(); 24 | }); 25 | }, 26 | 27 | enable: function() { 28 | compactNameplates.forEach(function(nameplate) { 29 | nameplate.enable(); 30 | }); 31 | }, 32 | 33 | /** 34 | * 35 | */ 36 | configure: function (arrayOfOpts) { 37 | arrayOfOpts = arrayOfOpts || []; 38 | var numNameplates = arrayOfOpts.length; 39 | 40 | // Enable/disable nameplates as appropriate. 41 | compactNameplates.forEach(function(nameplate, index) { 42 | if (index <= numNameplates - 1) { 43 | nameplate.enable(); 44 | nameplate.configure(arrayOfOpts[index]); 45 | } else { 46 | nameplate.disable(); 47 | } 48 | }); 49 | } 50 | }; 51 | }); 52 | -------------------------------------------------------------------------------- /graphics/app/components/nameplates.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'globals', 4 | 'classes/nameplate' 5 | ], function (globals, Nameplate) { 6 | 'use strict'; 7 | 8 | var nameplates = [ 9 | new Nameplate(0, 'left'), 10 | new Nameplate(1, 'right'), 11 | new Nameplate(2, 'left'), 12 | new Nameplate(3, 'right') 13 | ]; 14 | 15 | // Start disabled 16 | nameplates.forEach(function(nameplate) { 17 | nameplate.disable(); 18 | }); 19 | 20 | function extend(obj, src) { 21 | for (var key in src) { 22 | if (src.hasOwnProperty(key)) obj[key] = src[key]; 23 | } 24 | return obj; 25 | } 26 | 27 | return { 28 | disable: function() { 29 | nameplates.forEach(function(nameplate) { 30 | nameplate.disable(); 31 | }); 32 | }, 33 | 34 | enable: function() { 35 | nameplates.forEach(function(nameplate) { 36 | nameplate.enable(); 37 | }); 38 | }, 39 | 40 | /** 41 | * 42 | */ 43 | configure: function (globalOpts, perNameplateOpts) { 44 | var numNameplates = perNameplateOpts.length; 45 | 46 | // Enable/disable nameplates as appropriate. 47 | nameplates.forEach(function(nameplate, index) { 48 | if (index <= numNameplates - 1) { 49 | var opts = extend(perNameplateOpts[index], globalOpts); 50 | nameplate.enable(); 51 | nameplate.configure(opts); 52 | } else { 53 | nameplate.disable(); 54 | } 55 | }); 56 | } 57 | }; 58 | }); 59 | -------------------------------------------------------------------------------- /graphics/app/components/now-playing/music_note.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/app/components/now-playing/music_note.png -------------------------------------------------------------------------------- /graphics/app/components/now-playing/now-playing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 55 | 56 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /graphics/app/components/now-playing/now-playing.js: -------------------------------------------------------------------------------- 1 | /* global requirejs, Polymer, TimelineLite, TweenLite, Power2 */ 2 | requirejs(['debug'], function(debug) { 3 | 'use strict'; 4 | 5 | var SONG_EXTRA_WIDTH = 40; 6 | 7 | /* jshint -W064 */ 8 | Polymer({ 9 | /* jshint +W064 */ 10 | 11 | is: 'now-playing', 12 | 13 | properties: { 14 | song: String, 15 | album: String 16 | }, 17 | 18 | observers: [ 19 | '_resizeContainers(song, album)' 20 | ], 21 | 22 | ready: function() { 23 | var self = this; 24 | var tl = new TimelineLite({autoRemoveChildren: true}); 25 | var nowPlaying = nodecg.Replicant('nowPlaying'); 26 | 27 | var songContainer = this.$.songContainer; 28 | var songContainerWidth = 0; 29 | var songContainerX = '-100%'; 30 | var songContainerProxy = {}; 31 | Object.defineProperty(songContainerProxy, 'x', { 32 | set: function (newVal) { 33 | var percentage = parseFloat(newVal) / 100; 34 | songContainerX = newVal; 35 | TweenLite.set(songContainer, { 36 | x: Math.round(songContainerWidth * percentage) 37 | }); 38 | }, 39 | get: function() { 40 | return songContainerX; 41 | } 42 | }); 43 | 44 | var albumContainer = this.$.albumContainer; 45 | var albumContainerWidth = 0; 46 | var albumContainerX = '-100%'; 47 | var albumContainerProxy = {}; 48 | Object.defineProperty(albumContainerProxy, 'x', { 49 | set: function (newVal) { 50 | var percentage = parseFloat(newVal) / 100; 51 | albumContainerX = newVal; 52 | TweenLite.set(albumContainer, { 53 | x: Math.round(albumContainerWidth * percentage) 54 | }); 55 | }, 56 | get: function() { 57 | return albumContainerX; 58 | } 59 | }); 60 | 61 | nodecg.Replicant('nowPlayingPulsing').on('change', function(oldVal, newVal) { 62 | if (newVal) { 63 | tl.call(function() { 64 | self.style.visibility = 'visible'; 65 | 66 | self.song = nowPlaying.value.song; 67 | songContainerProxy.x = '-100%'; 68 | 69 | self.album = nowPlaying.value.album; 70 | albumContainerProxy.x = '-100%'; 71 | 72 | songContainerWidth = songContainer.getBoundingClientRect().width; 73 | albumContainerWidth = albumContainer.getBoundingClientRect().width; 74 | }, null, null, '+=0.1'); 75 | 76 | tl.to([songContainerProxy, albumContainerProxy], 1.2, { 77 | onStart: function() { 78 | debug.time('nowPlayingEnter'); 79 | }, 80 | x: '0%', 81 | ease: Power2.easeOut, 82 | onComplete: function() { 83 | debug.timeEnd('nowPlayingEnter'); 84 | } 85 | }); 86 | } 87 | 88 | else { 89 | tl.to([songContainerProxy, albumContainerProxy], 1.2, { 90 | onStart: function() { 91 | debug.time('nowPlayingExit'); 92 | }, 93 | x: '-100%', 94 | ease: Power2.easeIn, 95 | onComplete: function() { 96 | self.style.visibility = 'hidden'; 97 | debug.timeEnd('nowPlayingExit'); 98 | } 99 | }); 100 | } 101 | }); 102 | }, 103 | 104 | _resizeContainers: function() { 105 | this.$.songContainer.style.width = 'auto'; 106 | 107 | var songContainerWidth = this.$.songContainer.getBoundingClientRect().width; 108 | var albumContainerWidth = this.$.albumContainer.getBoundingClientRect().width; 109 | if (songContainerWidth < albumContainerWidth + SONG_EXTRA_WIDTH) { 110 | this.$.songContainer.style.width = albumContainerWidth + SONG_EXTRA_WIDTH + 'px'; 111 | } 112 | } 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /graphics/app/components/sponsor-display/sponsor-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/app/components/sponsor-display/sponsor-background.png -------------------------------------------------------------------------------- /graphics/app/components/sponsor-display/sponsor-display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /graphics/app/components/twitter-display/twitter-display.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 68 | 69 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /graphics/app/components/twitter-display/twitter.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/app/components/twitter-display/twitter.png -------------------------------------------------------------------------------- /graphics/app/debug.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(function() { 3 | 'use strict'; 4 | 5 | var ret = { 6 | log: function() { 7 | if (nodecg.bundleConfig.debug) { 8 | console.debug.apply(console, arguments); 9 | } 10 | }, 11 | time: function() { 12 | if (nodecg.bundleConfig.debug) { 13 | console.time.apply(console, arguments); 14 | } 15 | }, 16 | timeEnd: function() { 17 | if (nodecg.bundleConfig.debug) { 18 | console.timeEnd.apply(console, arguments); 19 | } 20 | } 21 | }; 22 | 23 | window.debug = ret; 24 | 25 | return ret; 26 | }); 27 | -------------------------------------------------------------------------------- /graphics/app/globals.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(function() { 3 | 'use strict'; 4 | 5 | var currentBidsRep = nodecg.Replicant('currentBids'); 6 | var scheduleRep = nodecg.Replicant('schedule'); 7 | var currentRunRep = nodecg.Replicant('currentRun'); 8 | var totalRep = nodecg.Replicant('total'); 9 | var displayDurationRep = nodecg.Replicant('displayDuration'); 10 | var stopwatchesRep = nodecg.Replicant('stopwatches'); 11 | var gameAudioChannelsRep = nodecg.Replicant('gameAudioChannels'); 12 | 13 | /* ----- */ 14 | 15 | var currentPrizesRep = nodecg.Replicant('currentPrizes'); 16 | var currentGrandPrizes = []; 17 | var currentNormalPrizes = []; 18 | currentPrizesRep.on('change', function (oldVal, newVal) { 19 | currentGrandPrizes = newVal.filter(function (prize) { 20 | return prize.grand; 21 | }); 22 | 23 | currentNormalPrizes = newVal.filter(function (prize) { 24 | return !prize.grand; 25 | }); 26 | }); 27 | 28 | /* ----- */ 29 | 30 | // This is really fragile, but whatever. 31 | var NUM_REPLICANTS = 8; 32 | var loadedReplicants = 0; 33 | 34 | currentBidsRep.on('declared', replicantDeclared); 35 | scheduleRep.on('declared', replicantDeclared); 36 | currentRunRep.on('declared', replicantDeclared); 37 | currentPrizesRep.on('declared', replicantDeclared); 38 | totalRep.on('declared', replicantDeclared); 39 | displayDurationRep.on('declared', replicantDeclared); 40 | stopwatchesRep.on('declared', replicantDeclared); 41 | gameAudioChannelsRep.on('declared', replicantDeclared); 42 | 43 | function replicantDeclared() { 44 | loadedReplicants++; 45 | if (loadedReplicants === NUM_REPLICANTS) { 46 | document.dispatchEvent(new CustomEvent('replicantsDeclared')); 47 | window.replicantsDeclared = true; 48 | } 49 | } 50 | 51 | /* ----- */ 52 | 53 | return Object.create(Object.prototype, { 54 | // Bids 55 | currentBids: { 56 | get: function() {return currentBidsRep.value;} 57 | }, 58 | 59 | // Prizes 60 | currentPrizesRep: { 61 | value: currentPrizesRep 62 | }, 63 | currentGrandPrizes: { 64 | get: function() {return currentGrandPrizes;} 65 | }, 66 | currentNormalPrizes: { 67 | get: function() {return currentNormalPrizes;} 68 | }, 69 | 70 | // Schedule 71 | schedule: { 72 | get: function() {return scheduleRep.value;} 73 | }, 74 | currentRun: { 75 | get: function() {return currentRunRep.value;} 76 | }, 77 | nextRun: { 78 | get: function() {return currentRunRep.value.nextRun;} 79 | }, 80 | currentRunRep: { 81 | value: currentRunRep 82 | }, 83 | 84 | // Other 85 | totalRep: { 86 | value: totalRep 87 | }, 88 | displayDuration: { 89 | get: function() {return displayDurationRep.value;} 90 | }, 91 | stopwatchesRep: { 92 | value: stopwatchesRep 93 | }, 94 | gameAudioChannelsRep: { 95 | value: gameAudioChannelsRep 96 | } 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /graphics/app/index.js: -------------------------------------------------------------------------------- 1 | /* global requirejs, TweenLite, Power1, Typekit */ 2 | (function() { 3 | 'use strict'; 4 | 5 | var layoutState = nodecg.Replicant('layoutState'); 6 | 7 | // Hack to prevent other instances of the layout from breaking the layoutState. 8 | // They have a brief window in which to change it before singleInstance kicks them off. 9 | layoutState.on('declared', function(rep) { 10 | if (rep.value.page === 'closed') { 11 | layoutState.value.page = 'preloading'; 12 | 13 | // Prevent NodeCG restarts from breaking the layoutState. 14 | layoutState.on('change', function(oldVal, newVal) { 15 | if (newVal.page === 'closed') { 16 | layoutState.value.page = 'open'; 17 | } 18 | }); 19 | } 20 | }); 21 | 22 | // Wait until Typekit fonts are loaded before setting up the graphic. 23 | try { 24 | Typekit.load({active: init}); 25 | } catch (e) { 26 | console.error(e); 27 | } 28 | 29 | var preloaderDone = false; 30 | var replicantsDone = false; 31 | 32 | function init() { 33 | requirejs(['debug', 'preloader', 'globals', 'easel'], function (debug, preloader, globals, createjs) { 34 | 35 | preloader.on('complete', handlePreloadComplete); 36 | 37 | function handlePreloadComplete() { 38 | preloader.removeAllEventListeners('complete'); 39 | preloaderDone = true; 40 | debug.log('preloading complete'); 41 | checkReplicantsAndPreloader(); 42 | } 43 | 44 | if (window.replicantsDeclared) { 45 | replicantsDone = true; 46 | debug.log('replicants declared'); 47 | checkReplicantsAndPreloader(); 48 | } else { 49 | document.addEventListener('replicantsDeclared', function() { 50 | replicantsDone = true; 51 | debug.log('replicants declared'); 52 | checkReplicantsAndPreloader(); 53 | }); 54 | } 55 | 56 | createjs.Ticker.timingMode = createjs.Ticker.RAF; 57 | }); 58 | } 59 | 60 | function checkReplicantsAndPreloader() { 61 | if (!preloaderDone || !replicantsDone) return; 62 | 63 | requirejs([ 64 | 'components/background', 65 | 'components/speedrun', 66 | 'components/omnibar', 67 | 'layout', 68 | 'obs', 69 | 'advertisements' 70 | ], function(bg, speedrun, omnibar, layout) { 71 | layout.changeTo('break'); 72 | window.layout = layout; 73 | layoutState.value.page = 'open'; 74 | 75 | // Fade up the body once everything is loaded 76 | TweenLite.to(document.body, 0.5, { 77 | delay: 0.2, 78 | opacity: 1, 79 | ease: Power1.easeInOut 80 | }); 81 | }); 82 | } 83 | 84 | if (window.process && window.process.versions && window.process.versions.electron) { 85 | console.log('electron environment detected, hooking f5 keyup'); 86 | document.addEventListener('keyup', function(e) { 87 | if (e.which === 116) { 88 | document.location.reload(); 89 | } 90 | }); 91 | } 92 | })(); 93 | -------------------------------------------------------------------------------- /graphics/app/layout.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'debug', 4 | 5 | 'layouts/3ds', 6 | 7 | 'layouts/3x2_1', 8 | 'layouts/3x2_2', 9 | 10 | 'layouts/4x3_1', 11 | 'layouts/4x3_2', 12 | 'layouts/4x3_3', 13 | 'layouts/4x3_4', 14 | 15 | 'layouts/16x9_1', 16 | 'layouts/16x9_2', 17 | 18 | 'layouts/break', 19 | 20 | 'layouts/ds', 21 | 'layouts/ds_portrait', 22 | 23 | 'layouts/interview' 24 | ], function(debug) { 25 | 'use strict'; 26 | 27 | var layoutState = nodecg.Replicant('layoutState'); 28 | 29 | var layouts = { 30 | '3ds': arguments[1], 31 | 32 | '3x2_1': arguments[2], 33 | '3x2_2': arguments[3], 34 | 35 | '4x3_1': arguments[4], 36 | '4x3_2': arguments[5], 37 | '4x3_3': arguments[6], 38 | '4x3_4': arguments[7], 39 | 40 | '16x9_1': arguments[8], 41 | '16x9_2': arguments[9], 42 | 43 | 'break': arguments[10], 44 | 45 | 'ds': arguments[11], 46 | 'ds_portrait': arguments[12], 47 | 48 | 'interview': arguments[13] 49 | }; 50 | 51 | var currentLayoutName, currentLayoutIndex; 52 | var numLayouts = Object.keys(layouts).length; 53 | 54 | function setLayout(name) { 55 | debug.log('[layout] setLayout(%s)', name); 56 | 57 | layoutState.value.currentLayout = name; 58 | 59 | if (currentLayoutName && layouts[currentLayoutName].detached){ 60 | layouts[currentLayoutName].detached(); 61 | } 62 | 63 | layouts[name].attached(); 64 | 65 | currentLayoutName = name; 66 | currentLayoutIndex = Object.keys(layouts).indexOf(name); 67 | } 68 | 69 | return Object.create(Object.prototype, { 70 | next: { 71 | value: function() { 72 | if (typeof currentLayoutIndex === 'undefined') { 73 | setLayout(Object.keys(layouts)[0]); 74 | return; 75 | } 76 | 77 | currentLayoutIndex += 1; 78 | if (currentLayoutIndex >= numLayouts) { 79 | currentLayoutIndex = 0; 80 | console.log('--- END OF LAYOUTS, STARTING FROM BEGINNING ---'); 81 | } 82 | 83 | setLayout(Object.keys(layouts)[currentLayoutIndex]); 84 | } 85 | }, 86 | changeTo: { 87 | value: setLayout 88 | }, 89 | currentLayoutName: { 90 | get: function() {return currentLayoutName;} 91 | }, 92 | currentLayoutIndex: { 93 | get: function() {return currentLayoutIndex;} 94 | } 95 | }); 96 | }); 97 | -------------------------------------------------------------------------------- /graphics/app/layouts/16x9_1.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '16x9_1'; 10 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 11 | var sponsorDisplay = document.querySelector('sponsor-display'); 12 | var twitterDisplay = document.querySelector('twitter-display'); 13 | 14 | return { 15 | attached: function() { 16 | setBackground(LAYOUT_NAME); 17 | 18 | speedrun.configure(0, 543, 469, 122, { 19 | nameY: 10, 20 | categoryY: 74, 21 | nameMaxHeight: 70 22 | }); 23 | 24 | nameplates.configure({},[{ 25 | x: 469, 26 | y: 572, 27 | width: 498, 28 | height: 65, 29 | nameFontSize: 35, 30 | estimateFontSize: 23, 31 | timeFontSize: 61 32 | }]); 33 | 34 | sponsorsAndTwitter.style.top = '302px'; 35 | sponsorsAndTwitter.style.left = '967px'; 36 | sponsorsAndTwitter.style.width = '313px'; 37 | sponsorsAndTwitter.style.height = '363px'; 38 | 39 | sponsorDisplay.orientation = 'vertical'; 40 | sponsorDisplay.style.padding = '20px 20px'; 41 | 42 | twitterDisplay.bodyStyle = { 43 | fontSize: 26, 44 | top: 57, 45 | horizontalMargin: 22 46 | }; 47 | 48 | twitterDisplay.namebarStyle = { 49 | top: 264, 50 | width: 297, 51 | fontSize: 21 52 | }; 53 | } 54 | }; 55 | }); 56 | -------------------------------------------------------------------------------- /graphics/app/layouts/16x9_2.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '16x9_2'; 10 | var COLUMN_WIDTH = 420; 11 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 12 | var sponsorDisplay = document.querySelector('sponsor-display'); 13 | var twitterDisplay = document.querySelector('twitter-display'); 14 | 15 | return { 16 | attached: function() { 17 | setBackground(LAYOUT_NAME); 18 | 19 | speedrun.configure(0, 447, COLUMN_WIDTH, 218, { 20 | nameY: 41, 21 | categoryY: 133, 22 | nameMaxHeight: 100 23 | }); 24 | 25 | nameplates.configure({ 26 | nameFontSize: 28, 27 | estimateFontSize: 18, 28 | timeFontSize: 48, 29 | width: COLUMN_WIDTH, 30 | height: 51, 31 | y: 394, 32 | bottomBorder: true, 33 | audioIcon: true 34 | },[ 35 | { 36 | x: 0, 37 | alignment: 'right' 38 | },{ 39 | x: 860, 40 | alignment: 'left' 41 | } 42 | ]); 43 | 44 | sponsorsAndTwitter.style.top = '447px'; 45 | sponsorsAndTwitter.style.left = '860px'; 46 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 47 | sponsorsAndTwitter.style.height = '218px'; 48 | 49 | sponsorDisplay.orientation = 'horizontal'; 50 | sponsorDisplay.style.padding = '20px 20px'; 51 | 52 | twitterDisplay.bodyStyle = { 53 | fontSize: 26, 54 | top: 18, 55 | horizontalMargin: 17 56 | }; 57 | twitterDisplay.namebarStyle = { 58 | top: 161, 59 | width: 358, 60 | fontSize: 25 61 | }; 62 | } 63 | }; 64 | }); 65 | -------------------------------------------------------------------------------- /graphics/app/layouts/3ds.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '3ds'; 10 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 11 | var sponsorDisplay = document.querySelector('sponsor-display'); 12 | var twitterDisplay = document.querySelector('twitter-display'); 13 | 14 | return { 15 | attached: function() { 16 | setBackground(LAYOUT_NAME); 17 | 18 | speedrun.configure(0, 567, 335, 98, { 19 | scale: 0.834, 20 | nameY: 10, 21 | nameMaxHeight: 50, 22 | categoryY: 64 23 | }); 24 | 25 | nameplates.configure({},[{ 26 | x: 335, 27 | y: 581, 28 | width: 592, 29 | height: 70, 30 | nameFontSize: 40, 31 | estimateFontSize: 28, 32 | timeFontSize: 68 33 | }]); 34 | 35 | sponsorsAndTwitter.style.top = '477px'; 36 | sponsorsAndTwitter.style.left = '928px'; 37 | sponsorsAndTwitter.style.width = '352px'; 38 | sponsorsAndTwitter.style.height = '188px'; 39 | 40 | sponsorDisplay.orientation = 'horizontal'; 41 | sponsorDisplay.style.padding = '20px 20px'; 42 | 43 | twitterDisplay.bodyStyle = { 44 | fontSize: 25, 45 | top: 19, 46 | horizontalMargin: 10 47 | }; 48 | 49 | twitterDisplay.namebarStyle = { 50 | top: 137, 51 | width: 316, 52 | fontSize: 22 53 | }; 54 | } 55 | }; 56 | }); 57 | -------------------------------------------------------------------------------- /graphics/app/layouts/3x2_1.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '3x2_1'; 10 | var COLUMN_X = 950; 11 | var COLUMN_WIDTH = 330; 12 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 13 | var sponsorDisplay = document.querySelector('sponsor-display'); 14 | var twitterDisplay = document.querySelector('twitter-display'); 15 | 16 | return { 17 | attached: function() { 18 | setBackground(LAYOUT_NAME); 19 | 20 | speedrun.configure(COLUMN_X, 0, COLUMN_WIDTH, 146, { 21 | scale: 0.834, 22 | nameY: 26, 23 | nameMaxHeight: 80, 24 | categoryY: 97 25 | }); 26 | 27 | nameplates.configure({},[{ 28 | x: COLUMN_X, 29 | y: 341, 30 | width: COLUMN_WIDTH, 31 | height: 45, 32 | nameFontSize: 23, 33 | estimateFontSize: 15, 34 | timeFontSize: 40, 35 | bottomBorder: true 36 | }]); 37 | 38 | sponsorsAndTwitter.style.top = '388px'; 39 | sponsorsAndTwitter.style.left = COLUMN_X + 'px'; 40 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 41 | sponsorsAndTwitter.style.height = '277px'; 42 | 43 | sponsorDisplay.orientation = 'vertical'; 44 | sponsorDisplay.style.padding = '20px 30px'; 45 | 46 | twitterDisplay.bodyStyle = { 47 | fontSize: 25, 48 | top: 30, 49 | horizontalMargin: 16 50 | }; 51 | 52 | twitterDisplay.namebarStyle = { 53 | top: 220, 54 | width: 304, 55 | fontSize: 21 56 | }; 57 | } 58 | }; 59 | }); 60 | -------------------------------------------------------------------------------- /graphics/app/layouts/3x2_2.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '3x2_2'; 10 | var COLUMN_WIDTH = 430; 11 | var RIGHT_COLUMN_X = 850; 12 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 13 | var sponsorDisplay = document.querySelector('sponsor-display'); 14 | var twitterDisplay = document.querySelector('twitter-display'); 15 | 16 | return { 17 | attached: function () { 18 | setBackground(LAYOUT_NAME); 19 | 20 | speedrun.configure(0, 481, COLUMN_WIDTH, 184, { 21 | nameY: 35, 22 | categoryY: 124, 23 | nameMaxHeight: 90 24 | }); 25 | 26 | nameplates.configure({ 27 | nameFontSize: 28, 28 | estimateFontSize: 18, 29 | timeFontSize: 48, 30 | width: COLUMN_WIDTH, 31 | height: 52, 32 | y: 427, 33 | bottomBorder: true, 34 | audioIcon: true 35 | },[ 36 | { 37 | x: 0, 38 | alignment: 'right' 39 | },{ 40 | x: RIGHT_COLUMN_X, 41 | alignment: 'left' 42 | } 43 | ]); 44 | 45 | sponsorsAndTwitter.style.top = '481px'; 46 | sponsorsAndTwitter.style.left = RIGHT_COLUMN_X + 'px'; 47 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 48 | sponsorsAndTwitter.style.height = '184px'; 49 | 50 | sponsorDisplay.orientation = 'horizontal'; 51 | sponsorDisplay.style.padding = '40px 20px'; 52 | 53 | twitterDisplay.bodyStyle = { 54 | fontSize: 23, 55 | top: 19, 56 | horizontalMargin: 16 57 | }; 58 | 59 | twitterDisplay.namebarStyle = { 60 | top: 133, 61 | width: 350, 62 | fontSize: 25 63 | }; 64 | } 65 | }; 66 | }); 67 | -------------------------------------------------------------------------------- /graphics/app/layouts/4x3_1.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '4x3_1'; 10 | var COLUMN_WIDTH = 398; 11 | var COLUMN_X = 882; 12 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 13 | var sponsorDisplay = document.querySelector('sponsor-display'); 14 | var twitterDisplay = document.querySelector('twitter-display'); 15 | 16 | return { 17 | attached: function() { 18 | setBackground(LAYOUT_NAME); 19 | 20 | speedrun.configure(COLUMN_X, 0, COLUMN_WIDTH, 146, { 21 | nameY: 28, 22 | categoryY: 94, 23 | nameMaxHeight: 80 24 | }); 25 | 26 | nameplates.configure({},[{ 27 | x: COLUMN_X, 28 | y: 383, 29 | width: COLUMN_WIDTH, 30 | height: 52, 31 | nameFontSize: 24, 32 | estimateFontSize: 18, 33 | timeFontSize: 48, 34 | bottomBorder: true 35 | }]); 36 | 37 | sponsorsAndTwitter.style.top = '437px'; 38 | sponsorsAndTwitter.style.left = COLUMN_X + 'px'; 39 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 40 | sponsorsAndTwitter.style.height = '228px'; 41 | 42 | sponsorDisplay.orientation = 'horizontal'; 43 | sponsorDisplay.style.padding = '40px 30px'; 44 | 45 | twitterDisplay.bodyStyle = { 46 | fontSize: 24, 47 | top: 39, 48 | horizontalMargin: 14 49 | }; 50 | twitterDisplay.namebarStyle = { 51 | top: 160, 52 | width: 373, 53 | fontSize: 28 54 | }; 55 | } 56 | }; 57 | }); 58 | -------------------------------------------------------------------------------- /graphics/app/layouts/4x3_2.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = '4x3_2'; 10 | var COLUMN_WIDTH = 430; 11 | var RIGHT_COLUMN_X = 850; 12 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 13 | var sponsorDisplay = document.querySelector('sponsor-display'); 14 | var twitterDisplay = document.querySelector('twitter-display'); 15 | 16 | return { 17 | attached: function() { 18 | setBackground(LAYOUT_NAME); 19 | 20 | speedrun.configure(0, 536, COLUMN_WIDTH, 130, { 21 | nameY: 15, 22 | categoryY: 81, 23 | nameMaxHeight: 80 24 | }); 25 | 26 | nameplates.configure({ 27 | nameFontSize: 28, 28 | estimateFontSize: 18, 29 | timeFontSize: 48, 30 | width: COLUMN_WIDTH, 31 | height: 52, 32 | y: 481, 33 | bottomBorder: true, 34 | audioIcon: true 35 | },[ 36 | { 37 | x: 0, 38 | alignment: 'right' 39 | },{ 40 | x: RIGHT_COLUMN_X, 41 | alignment: 'left' 42 | } 43 | ]); 44 | 45 | sponsorsAndTwitter.style.top = '535px'; 46 | sponsorsAndTwitter.style.left = RIGHT_COLUMN_X + 'px'; 47 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 48 | sponsorsAndTwitter.style.height = '130px'; 49 | 50 | sponsorDisplay.orientation = 'horizontal'; 51 | sponsorDisplay.style.padding = '20px 20px'; 52 | 53 | twitterDisplay.bodyStyle = { 54 | fontSize: 17, 55 | top: 11, 56 | horizontalMargin: 10 57 | }; 58 | twitterDisplay.namebarStyle = { 59 | top: 86, 60 | width: 373, 61 | fontSize: 26 62 | }; 63 | } 64 | }; 65 | }); 66 | -------------------------------------------------------------------------------- /graphics/app/layouts/4x3_3.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/compact_nameplates', 6 | 'components/nameplates' 7 | ], function(setBackground, speedrun, compactNameplates, nameplates) { 8 | 'use strict'; 9 | 10 | var LAYOUT_NAME = '4x3_3'; 11 | var COLUMN_WIDTH = 396; 12 | var COLUMN_X = 442; 13 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 14 | var sponsorDisplay = document.querySelector('sponsor-display'); 15 | var twitterDisplay = document.querySelector('twitter-display'); 16 | 17 | return { 18 | attached: function() { 19 | setBackground(LAYOUT_NAME); 20 | 21 | speedrun.configure(COLUMN_X, 154, COLUMN_WIDTH, 179, { 22 | nameY: 17, 23 | categoryY: 84, 24 | showEstimate: true, 25 | nameMaxHeight: 80 26 | }); 27 | 28 | nameplates.disable(); 29 | 30 | compactNameplates.configure([ 31 | { 32 | threeOrMore: true, 33 | bottomBorder: true 34 | },{ 35 | threeOrMore: true, 36 | y: 78, 37 | alignRight: true 38 | },{ 39 | threeOrMore: true, 40 | y: 334, 41 | bottomBorder: true 42 | } 43 | ]); 44 | 45 | sponsorsAndTwitter.style.top = '412px'; 46 | sponsorsAndTwitter.style.left = COLUMN_X + 'px'; 47 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 48 | sponsorsAndTwitter.style.height = '253px'; 49 | 50 | sponsorDisplay.orientation = 'vertical'; 51 | sponsorDisplay.style.padding = '30px 30px'; 52 | 53 | twitterDisplay.bodyStyle = { 54 | fontSize: 24, 55 | top: 50, 56 | horizontalMargin: 9 57 | }; 58 | twitterDisplay.namebarStyle = { 59 | top: 164, 60 | width: 354, 61 | fontSize: 26 62 | }; 63 | }, 64 | 65 | detached: function() { 66 | compactNameplates.disable(); 67 | } 68 | }; 69 | }); 70 | -------------------------------------------------------------------------------- /graphics/app/layouts/4x3_4.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/compact_nameplates', 6 | 'components/nameplates' 7 | ], function(setBackground, speedrun, compactNameplates, nameplates) { 8 | 'use strict'; 9 | 10 | var LAYOUT_NAME = '4x3_4'; 11 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 12 | 13 | return { 14 | attached: function() { 15 | setBackground(LAYOUT_NAME); 16 | 17 | speedrun.configure(442, 154, 396, 170, { 18 | nameY: 20, 19 | categoryY: 81, 20 | nameMaxHeight: 70, 21 | showEstimate: true 22 | }); 23 | 24 | nameplates.disable(); 25 | 26 | compactNameplates.configure([ 27 | { 28 | threeOrMore: true, 29 | bottomBorder: true 30 | },{ 31 | threeOrMore: true, 32 | y: 78, 33 | alignRight: true 34 | },{ 35 | threeOrMore: true, 36 | y: 511, 37 | bottomBorder: true 38 | },{ 39 | threeOrMore: true, 40 | y: 589, 41 | alignRight: true 42 | } 43 | ]); 44 | 45 | sponsorsAndTwitter.style.display = 'none'; 46 | }, 47 | 48 | detached: function() { 49 | compactNameplates.disable(); 50 | sponsorsAndTwitter.style.display = 'block'; 51 | } 52 | }; 53 | }); 54 | -------------------------------------------------------------------------------- /graphics/app/layouts/break.js: -------------------------------------------------------------------------------- 1 | /* global define, requirejs, TimelineMax, Power2 */ 2 | define([ 3 | 'classes/stage', 4 | 'components/background', 5 | 'components/speedrun', 6 | 'components/nameplates', 7 | 'globals' 8 | ], function(Stage, setBackground, speedrun, nameplates, globals) { 9 | 'use strict'; 10 | 11 | var LAYOUT_NAME = 'break'; 12 | var STAGE_WIDTH = 371; 13 | var STAGE_HEIGHT = 330; 14 | var DESCRIPTION_HEIGHT = 53; 15 | 16 | var nowPlaying = document.querySelector('now-playing'); 17 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 18 | var sponsorDisplay = document.querySelector('sponsor-display'); 19 | var twitterDisplay = document.querySelector('twitter-display'); 20 | 21 | var createjs = requirejs('easel'); 22 | var stage = new Stage(STAGE_WIDTH, STAGE_HEIGHT, 'break-prizes'); 23 | stage.canvas.style.top = '308px'; 24 | stage.canvas.style.right = '0px'; 25 | stage.canvas.style.backgroundColor = 'black'; 26 | 27 | // Start hidden 28 | stage.visible = false; 29 | stage.paused = true; 30 | stage.canvas.style.display = 'none'; 31 | 32 | /* ----- */ 33 | 34 | var labelContainer = new createjs.Container(); 35 | labelContainer.x = STAGE_WIDTH - 203; 36 | 37 | var labelBackground = new createjs.Shape(); 38 | labelBackground.graphics.beginFill('#00aeef').drawRect(0, 0, 203, 27); 39 | labelBackground.alpha = 0.78; 40 | 41 | var labelText = new createjs.Text('RAFFLE PRIZES', '800 24px proxima-nova', 'white'); 42 | labelText.x = 14; 43 | labelText.y = 0; 44 | 45 | labelContainer.addChild(labelBackground, labelText); 46 | labelContainer.cache(0, 0, 203, 27); 47 | 48 | /* ----- */ 49 | 50 | var descriptionContainer = new createjs.Container(); 51 | descriptionContainer.y = STAGE_HEIGHT - DESCRIPTION_HEIGHT; 52 | 53 | var descriptionBackground = new createjs.Shape(); 54 | descriptionBackground.graphics.beginFill('#00aeef').drawRect(0, 0, STAGE_WIDTH, DESCRIPTION_HEIGHT); 55 | descriptionBackground.alpha = 0.73; 56 | 57 | var descriptionText = new createjs.Text('', '800 22px proxima-nova', 'white'); 58 | descriptionText.x = 8; 59 | descriptionText.y = 3; 60 | descriptionText.lineWidth = STAGE_WIDTH - descriptionText.x * 2; 61 | 62 | descriptionContainer.addChild(descriptionBackground, descriptionText); 63 | 64 | /* ----- */ 65 | 66 | var currentImage = new createjs.Bitmap(); 67 | var nextImage = new createjs.Bitmap(); 68 | stage.addChild(currentImage, nextImage, labelContainer, descriptionContainer); 69 | 70 | /* ----- */ 71 | 72 | var TRANSITION_DURATION = 1.2; 73 | var DESCRIPTION_TRANSITION_DURATON = TRANSITION_DURATION / 2 - 0.1; 74 | 75 | var preloadedImages = {}; 76 | var tl = new TimelineMax({repeat: -1}); 77 | globals.currentPrizesRep.on('change', function (oldVal, newVal) { 78 | tl.clear(); 79 | newVal.forEach(function (prize) { 80 | showPrize(prize); 81 | }); 82 | }); 83 | 84 | function showPrize(prize) { 85 | var imgEl; 86 | if (preloadedImages[prize.name]) { 87 | imgEl = preloadedImages[prize.name]; 88 | } else { 89 | imgEl = document.createElement('img'); 90 | imgEl.src = prize.image; 91 | preloadedImages[prize.name] = imgEl; 92 | } 93 | 94 | tl.call(function() { 95 | nextImage.x = STAGE_WIDTH; 96 | nextImage.image = imgEl; 97 | if (!imgEl.complete) { 98 | tl.pause(); 99 | imgEl.addEventListener('load', function() { 100 | tl.play(); 101 | }); 102 | } 103 | }, null, null, '+=0.1'); 104 | 105 | tl.add('prizeEnter'); 106 | 107 | tl.to(currentImage, TRANSITION_DURATION, { 108 | x: -STAGE_WIDTH, 109 | ease: Power2.easeInOut 110 | }, 'prizeEnter'); 111 | 112 | tl.to(nextImage, TRANSITION_DURATION, { 113 | x: 0, 114 | ease: Power2.easeInOut, 115 | onComplete: function() { 116 | currentImage.image = imgEl; 117 | currentImage.x = 0; 118 | nextImage.x = STAGE_WIDTH; 119 | } 120 | }, 'prizeEnter'); 121 | 122 | tl.to(descriptionContainer, DESCRIPTION_TRANSITION_DURATON, { 123 | y: STAGE_HEIGHT, 124 | ease: Power2.easeIn, 125 | onComplete: function() { 126 | descriptionText.text = prize.description; 127 | descriptionContainer.cache(0, 0, STAGE_WIDTH, DESCRIPTION_HEIGHT); 128 | } 129 | }, 'prizeEnter'); 130 | 131 | tl.to(descriptionContainer, DESCRIPTION_TRANSITION_DURATON, { 132 | y: STAGE_HEIGHT - DESCRIPTION_HEIGHT, 133 | ease: Power2.easeOut 134 | }, '-=' + DESCRIPTION_TRANSITION_DURATON); 135 | 136 | tl.to({}, globals.displayDuration, {}); 137 | } 138 | 139 | return { 140 | attached: function() { 141 | setBackground(LAYOUT_NAME); 142 | speedrun.disable(); 143 | nameplates.disable(); 144 | stage.visible = true; 145 | stage.paused = false; 146 | stage.canvas.style.display = 'block'; 147 | 148 | nowPlaying.style.display = 'flex'; 149 | 150 | sponsorsAndTwitter.style.top = '479px'; 151 | sponsorsAndTwitter.style.left = '387px'; 152 | sponsorsAndTwitter.style.width = '516px'; 153 | sponsorsAndTwitter.style.height = '146px'; 154 | 155 | sponsorDisplay.style.display = 'none'; 156 | 157 | twitterDisplay.style.zIndex = '-1'; 158 | twitterDisplay.bodyStyle = { 159 | fontSize: 21, 160 | top: 15, 161 | horizontalMargin: 13 162 | }; 163 | twitterDisplay.namebarStyle = { 164 | top: 98, 165 | width: 305, 166 | fontSize: 20 167 | }; 168 | }, 169 | detached: function() { 170 | stage.visible = false; 171 | stage.paused = true; 172 | stage.canvas.style.display = 'none'; 173 | nowPlaying.style.display = 'none'; 174 | sponsorDisplay.style.display = 'block'; 175 | twitterDisplay.style.zIndex = ''; 176 | } 177 | }; 178 | }); 179 | -------------------------------------------------------------------------------- /graphics/app/layouts/ds.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = 'ds'; 10 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 11 | 12 | return { 13 | attached: function() { 14 | setBackground(LAYOUT_NAME); 15 | 16 | speedrun.configure(882, 291, 398, 127, { 17 | nameY: 18, 18 | categoryY: 80, 19 | nameMaxHeight: 80 20 | }); 21 | 22 | nameplates.configure({},[{ 23 | x: 882, 24 | y: 418, 25 | width: 398, 26 | height: 54, 27 | nameFontSize: 28, 28 | estimateFontSize: 18, 29 | timeFontSize: 48 30 | }]); 31 | 32 | sponsorsAndTwitter.style.display = 'none'; 33 | }, 34 | 35 | detached: function() { 36 | sponsorsAndTwitter.style.display = 'block'; 37 | } 38 | }; 39 | }); 40 | -------------------------------------------------------------------------------- /graphics/app/layouts/ds_portrait.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define([ 3 | 'components/background', 4 | 'components/speedrun', 5 | 'components/nameplates' 6 | ], function (setBackground, speedrun, nameplates) { 7 | 'use strict'; 8 | 9 | var LAYOUT_NAME = 'ds_portrait'; 10 | var COLUMN_WIDTH = 305; 11 | var COLUMN_X = 975; 12 | 13 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 14 | var sponsorDisplay = document.querySelector('sponsor-display'); 15 | var twitterDisplay = document.querySelector('twitter-display'); 16 | 17 | return { 18 | attached: function() { 19 | setBackground(LAYOUT_NAME); 20 | 21 | speedrun.configure(COLUMN_X, 0, COLUMN_WIDTH, 151, { 22 | nameY: 29, 23 | categoryY: 89, 24 | nameMaxHeight: 70 25 | }); 26 | 27 | nameplates.configure({},[{ 28 | x: COLUMN_X, 29 | y: 151, 30 | width: COLUMN_WIDTH, 31 | height: 54, 32 | nameFontSize: 24, 33 | estimateFontSize: 18, 34 | timeFontSize: 36 35 | }]); 36 | 37 | sponsorsAndTwitter.style.top = '384px'; 38 | sponsorsAndTwitter.style.left = COLUMN_X + 'px'; 39 | sponsorsAndTwitter.style.width = COLUMN_WIDTH + 'px'; 40 | sponsorsAndTwitter.style.height = '281px'; 41 | 42 | sponsorDisplay.orientation = 'vertical'; 43 | sponsorDisplay.style.padding = '20px 20px'; 44 | 45 | twitterDisplay.bodyStyle = { 46 | fontSize: 21, 47 | top: 18, 48 | horizontalMargin: 13 49 | }; 50 | twitterDisplay.namebarStyle = { 51 | top: 207, 52 | width: 284, 53 | fontSize: 20 54 | }; 55 | } 56 | }; 57 | }); 58 | -------------------------------------------------------------------------------- /graphics/app/layouts/interview.js: -------------------------------------------------------------------------------- 1 | /* global define, requirejs, TimelineLite, Elastic, Back */ 2 | define([ 3 | 'debug', 4 | 'classes/stage', 5 | 'components/background', 6 | 'components/speedrun', 7 | 'components/nameplates', 8 | 'globals' 9 | ], function(debug, Stage, setBackground, speedrun, nameplates, globals) { 10 | 'use strict'; 11 | 12 | var LAYOUT_NAME = 'interview'; 13 | var STAGE_WIDTH = 1280; 14 | var STAGE_HEIGHT = 100; 15 | var PADDING = 37; 16 | 17 | var sponsorsAndTwitter = document.getElementById('sponsorsAndTwitter'); 18 | var sponsorDisplay = document.querySelector('sponsor-display'); 19 | var twitterDisplay = document.querySelector('twitter-display'); 20 | 21 | var createjs = requirejs('easel'); 22 | var stage = new Stage(STAGE_WIDTH, STAGE_HEIGHT, 'interview-lowerthird'); 23 | stage.canvas.style.top = '502px'; 24 | stage.canvas.style.left = '0px'; 25 | 26 | // Start hidden 27 | stage.visible = false; 28 | stage.paused = true; 29 | stage.canvas.style.display = 'none'; 30 | 31 | /* ----- */ 32 | 33 | var background = new createjs.Shape(); 34 | background.y = STAGE_HEIGHT / 2; 35 | stage.addChild(background); 36 | 37 | var backgroundLayer4 = background.graphics.beginFill('#0075a1').drawRect(0, 0, STAGE_WIDTH, 0).command; 38 | backgroundLayer4.targetHeight = 74; 39 | 40 | var backgroundLayer3 = background.graphics.beginFill('#00aeef').drawRect(0, 0, STAGE_WIDTH, 0).command; 41 | backgroundLayer3.targetHeight = 66; 42 | 43 | var backgroundLayer2 = background.graphics.beginFill('#0075a1').drawRect(0, 0, STAGE_WIDTH, 0).command; 44 | backgroundLayer2.targetHeight = 62; 45 | 46 | var backgroundLayer1 = background.graphics.beginFill('#00aeef').drawRect(0, 0, STAGE_WIDTH, 0).command; 47 | backgroundLayer1.targetHeight = 58; 48 | 49 | var backgroundLayers = [backgroundLayer1, backgroundLayer2, backgroundLayer3, backgroundLayer4]; 50 | var reverseBackgroundLayers = backgroundLayers.slice(0).reverse(); 51 | 52 | /* ----- */ 53 | 54 | var textContainer = new createjs.Container(); 55 | textContainer.y = 36; 56 | textContainer.mask = background; 57 | stage.addChild(textContainer); 58 | 59 | var nameText1 = new createjs.Text('', '800 25px proxima-nova', 'white'); 60 | var nameText2 = new createjs.Text('', '800 25px proxima-nova', 'white'); 61 | var nameText3 = new createjs.Text('', '800 25px proxima-nova', 'white'); 62 | var nameText4 = new createjs.Text('', '800 25px proxima-nova', 'white'); 63 | 64 | nameText1.textAlign = 'center'; 65 | nameText2.textAlign = 'center'; 66 | nameText3.textAlign = 'center'; 67 | nameText4.textAlign = 'center'; 68 | 69 | textContainer.addChild(nameText1, nameText2, nameText3, nameText4); 70 | 71 | /* ----- */ 72 | 73 | var interviewNames = nodecg.Replicant('interviewNames'); 74 | var tl = new TimelineLite({autoRemoveChildren: true}); 75 | 76 | nodecg.Replicant('interviewLowerthirdShowing').on('change', function(oldVal, newVal) { 77 | if (newVal) { 78 | tl.call(function() { 79 | var names = interviewNames.value; 80 | var numNames = names.filter(function(s) {return !!s;}).length; 81 | var maxWidth = (STAGE_WIDTH / numNames) - (PADDING * 2); 82 | nameText1.maxWidth = maxWidth; 83 | nameText2.maxWidth = maxWidth; 84 | nameText3.maxWidth = maxWidth; 85 | nameText4.maxWidth = maxWidth; 86 | 87 | nameText1.text = names[0] ? names[0].toUpperCase() : ''; 88 | nameText2.text = names[1] ? names[1].toUpperCase() : ''; 89 | nameText3.text = names[2] ? names[2].toUpperCase() : ''; 90 | nameText4.text = names[3] ? names[3].toUpperCase() : ''; 91 | 92 | var widthUnit = STAGE_WIDTH / numNames; 93 | names.forEach(function(name, index){ 94 | textContainer.children[index].x = (widthUnit * index) + (widthUnit / 2); 95 | }); 96 | }, null, null, '+=0.1'); 97 | 98 | tl.add('entry'); 99 | 100 | reverseBackgroundLayers.forEach(function(rect, index) { 101 | tl.to(rect, 1, { 102 | h: rect.targetHeight, 103 | roundProps: index === 0 ? 'h' : '', // Round the outermost rect 104 | ease: Elastic.easeOut.config(0.5, 0.5), 105 | onStart: function() { 106 | if (index === 0) { 107 | debug.time('interviewEnter'); 108 | } 109 | }, 110 | onUpdate: function() { 111 | // Round the outermost rect to avoid half pixels which can't be cleanly chroma keyed 112 | rect.y = index === 0 ? -Math.round(rect.h / 2) : -(rect.h / 2); 113 | }, 114 | onComplete: function() { 115 | if (index === 3) { 116 | debug.timeEnd('interviewEnter'); 117 | } 118 | } 119 | }, 'entry+=' + (index * 0.08)); 120 | }); 121 | } 122 | 123 | else { 124 | tl.add('exit'); 125 | 126 | backgroundLayers.forEach(function(rect, index) { 127 | tl.to(rect, 1, { 128 | h: 0, 129 | roundProps: index === 3 ? 'h' : '', 130 | ease: Back.easeIn.config(1.3), 131 | onStart: function() { 132 | if (index === 0) { 133 | debug.time('interviewExit'); 134 | } 135 | }, 136 | onUpdate: function() { 137 | rect.y = index === 3 ? -Math.round(rect.h / 2) : -(rect.h / 2); 138 | }, 139 | onComplete: function() { 140 | if (index === 3) { 141 | debug.timeEnd('interviewExit'); 142 | } 143 | } 144 | }, 'exit+=' + (index * 0.08)); 145 | }); 146 | } 147 | }); 148 | 149 | return { 150 | attached: function() { 151 | setBackground(LAYOUT_NAME); 152 | speedrun.disable(); 153 | nameplates.disable(); 154 | stage.visible = true; 155 | stage.paused = false; 156 | stage.canvas.style.display = 'block'; 157 | 158 | sponsorsAndTwitter.style.top = '356px'; 159 | sponsorsAndTwitter.style.left = '764px'; 160 | sponsorsAndTwitter.style.width = '516px'; 161 | sponsorsAndTwitter.style.height = '146px'; 162 | 163 | sponsorDisplay.style.display = 'none'; 164 | 165 | twitterDisplay.bodyStyle = { 166 | fontSize: 21, 167 | top: 15, 168 | horizontalMargin: 13 169 | }; 170 | twitterDisplay.namebarStyle = { 171 | top: 98, 172 | width: 305, 173 | fontSize: 20 174 | }; 175 | }, 176 | 177 | detached: function() { 178 | stage.visible = false; 179 | stage.paused = true; 180 | stage.canvas.style.display = 'none'; 181 | sponsorDisplay.style.display = 'block'; 182 | } 183 | }; 184 | }); 185 | -------------------------------------------------------------------------------- /graphics/app/obs.js: -------------------------------------------------------------------------------- 1 | /* global define, OBSRemote */ 2 | define([ 3 | 'debug', 4 | 'layout', 5 | 'debounce' 6 | ], function (debug, layout, debounce) { 7 | 'use strict'; 8 | 9 | var obs = new OBSRemote(); 10 | var retryConnection = debounce(connect, 5000); 11 | var _lastLayoutName; 12 | var _handleSceneSwitch = debounce(function () { 13 | obs.getCurrentScene(function (scene) { 14 | scene.sources.some(function(source) { 15 | if (source.name.indexOf('Layout ') === 0) { 16 | var layoutName = source.name.split(' ')[1]; 17 | 18 | // Only execute a layout change if this new layout is different from the previous one. 19 | // This prevents the boxart from needlessly resetting when checking/unchecking sources in OBS. 20 | if (layoutName !== _lastLayoutName) { 21 | layout.changeTo(layoutName); 22 | _lastLayoutName = layoutName; 23 | } 24 | } 25 | }); 26 | }); 27 | }, 10); 28 | 29 | obs.onConnectionOpened = function () { 30 | console.log('[OBS] Connected.'); 31 | _handleSceneSwitch(); 32 | }; 33 | 34 | obs.onConnectionClosed = function () { 35 | console.log('[OBS] Connection closed.'); 36 | retryConnection(); 37 | }; 38 | 39 | obs.onConnectionFailed = function () { 40 | console.log('[OBS] Failed to connect.'); 41 | retryConnection(); 42 | }; 43 | 44 | obs.onAuthenticationFailed = function (remainingAttempts) { 45 | console.log('[OBS] Authentication failed, %s attempts remaining.', remainingAttempts); 46 | }; 47 | 48 | obs.onSceneSwitched = function(sceneName) { 49 | debug.log('[OBS] Switched to scene "%s".', sceneName); 50 | _handleSceneSwitch(); 51 | }; 52 | obs.onSourceChanged = _handleSceneSwitch; 53 | obs.onSourceAddedOrRemoved = _handleSceneSwitch; 54 | 55 | function connect() { 56 | if (nodecg.bundleConfig && nodecg.bundleConfig.obs) { 57 | obs.connect(nodecg.bundleConfig.obs.host, nodecg.bundleConfig.obs.password); 58 | } else { 59 | obs.connect(); 60 | } 61 | } 62 | 63 | connect(); 64 | 65 | return obs; 66 | }); 67 | 68 | -------------------------------------------------------------------------------- /graphics/app/preloader.js: -------------------------------------------------------------------------------- 1 | /* global define, createjs */ 2 | define(function () { 3 | 'use strict'; 4 | 5 | // Preload images 6 | var queue = new createjs.LoadQueue(); 7 | queue.setMaxConnections(10); 8 | queue.loadManifest([ 9 | // Backgrounds 10 | {id: 'bg-3ds', src: 'img/backgrounds/3ds.png'}, 11 | 12 | {id: 'bg-3x2_1', src: 'img/backgrounds/3x2_1.png'}, 13 | {id: 'bg-3x2_2', src: 'img/backgrounds/3x2_2.png'}, 14 | {id: 'bg-3x2_3', src: 'img/backgrounds/4x3_3.png'}, 15 | {id: 'bg-3x2_4', src: 'img/backgrounds/4x3_4.png'}, 16 | 17 | {id: 'bg-4x3_1', src: 'img/backgrounds/4x3_1.png'}, 18 | {id: 'bg-4x3_2', src: 'img/backgrounds/4x3_2.png'}, 19 | {id: 'bg-4x3_3', src: 'img/backgrounds/4x3_3.png'}, 20 | {id: 'bg-4x3_4', src: 'img/backgrounds/4x3_4.png'}, 21 | 22 | {id: 'bg-16x9_1', src: 'img/backgrounds/16x9_1.png'}, 23 | {id: 'bg-16x9_2', src: 'img/backgrounds/16x9_2.png'}, 24 | 25 | {id: 'bg-break', src: 'img/backgrounds/break.png'}, 26 | 27 | {id: 'bg-ds', src: 'img/backgrounds/ds.png'}, 28 | {id: 'bg-ds_portrait', src: 'img/backgrounds/ds_portrait.png'}, 29 | 30 | {id: 'bg-interview', src: 'img/backgrounds/interview.png'}, 31 | 32 | // Console icons 33 | {id: 'console-3ds', src: 'img/consoles/3ds.png'}, 34 | {id: 'console-arc', src: 'img/consoles/arc.png'}, 35 | {id: 'console-dc', src: 'img/consoles/dc.png'}, 36 | {id: 'console-ds', src: 'img/consoles/ds.png'}, 37 | {id: 'console-gb', src: 'img/consoles/gb.png'}, 38 | {id: 'console-gba', src: 'img/consoles/gba.png'}, 39 | {id: 'console-gbc', src: 'img/consoles/gbc.png'}, 40 | {id: 'console-gcn', src: 'img/consoles/gcn.png'}, 41 | {id: 'console-gen', src: 'img/consoles/gen.png'}, 42 | {id: 'console-n64', src: 'img/consoles/n64.png'}, 43 | {id: 'console-nes', src: 'img/consoles/nes.png'}, 44 | {id: 'console-pc', src: 'img/consoles/pc.png'}, 45 | {id: 'console-ps1', src: 'img/consoles/ps1.png'}, 46 | {id: 'console-ps2', src: 'img/consoles/ps2.png'}, 47 | {id: 'console-ps3', src: 'img/consoles/ps3.png'}, 48 | {id: 'console-ps4', src: 'img/consoles/ps4.png'}, 49 | {id: 'console-psp', src: 'img/consoles/psp.png'}, 50 | {id: 'console-sat', src: 'img/consoles/sat.png'}, 51 | {id: 'console-snes', src: 'img/consoles/snes.png'}, 52 | {id: 'console-wii', src: 'img/consoles/wii.png'}, 53 | {id: 'console-wiiu', src: 'img/consoles/wiiu.png'}, 54 | {id: 'console-wshp', src: 'img/consoles/wshp.png'}, 55 | {id: 'console-xbox', src: 'img/consoles/xbox.png'}, 56 | {id: 'console-x360', src: 'img/consoles/x360.png'}, 57 | {id: 'console-xboxone', src: 'img/consoles/xboxone.png'}, 58 | {id: 'console-unknown', src: 'img/consoles/unknown.png'}, 59 | 60 | // Omnibar 61 | {id: 'omnibar-logo-gdq', src: 'img/omnibar/logo-gdq.png'}, 62 | {id: 'omnibar-logo-pcf', src: 'img/omnibar/logo-pcf.png'}, 63 | 64 | // Nameplates 65 | {id: 'nameplate-audio-on', src: 'img/nameplate/audio-on.png'}, 66 | {id: 'nameplate-audio-off', src: 'img/nameplate/audio-off.png'}, 67 | {id: 'nameplate-twitch-logo', src: 'img/nameplate/twitch-logo.png'} 68 | ]); 69 | 70 | return queue; 71 | }); 72 | 73 | -------------------------------------------------------------------------------- /graphics/app/tabulate.js: -------------------------------------------------------------------------------- 1 | /* global define */ 2 | define(function() { 3 | 'use strict'; 4 | 5 | /** 6 | * Given a string, modifies all numbers in that string to use the unicode characters 7 | * for tabular numbers. This makes all the numbers monospace, but leaves all other characters as-is. 8 | * This is hardcoded for TypeKit's version of Proxima Nova. It will not work with any other font. 9 | * @param {String} str - The string to tabulate. 10 | * @returns {String} 11 | */ 12 | return function(str) { 13 | // Disabled for now 14 | return str; 15 | return str.split('').map(function(char) { 16 | switch (char) { 17 | case '0': 18 | return '\uf639'; 19 | case '1': 20 | return '\uf6dc'; 21 | case '2': 22 | return '\uf63a'; 23 | case '3': 24 | return '\uf63b'; 25 | case '4': 26 | return '\uf63c'; 27 | case '5': 28 | return '\uf63d'; 29 | case '6': 30 | return '\uf63e'; 31 | case '7': 32 | return '\uf63f'; 33 | case '8': 34 | return '\uf640'; 35 | case '9': 36 | return '\uf641'; 37 | default: 38 | return char; 39 | } 40 | }).join(''); 41 | }; 42 | }); 43 | -------------------------------------------------------------------------------- /graphics/custom_controls/stopwatches/elements/finish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/custom_controls/stopwatches/elements/finish.png -------------------------------------------------------------------------------- /graphics/custom_controls/stopwatches/elements/time-only-validator.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 18 | -------------------------------------------------------------------------------- /graphics/custom_controls/stopwatches/elements/unfinish.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/custom_controls/stopwatches/elements/unfinish.png -------------------------------------------------------------------------------- /graphics/custom_controls/stopwatches/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Stopwatches 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 88 | 89 | 90 |
91 |
92 | 93 |
94 | 95 | 96 | 97 | 98 | 99 | 100 |
101 | 102 |
103 | 104 | Start All 105 |
106 | Checklist Incomplete 107 |
108 | 109 | 110 | 111 | Pause All 112 | 113 | 114 | 115 | 116 | Reset All 117 | 118 |
119 |
120 | 121 | 122 |

Edit Stopwatch

123 | 124 | 125 | 126 | 127 | 129 | 130 |
131 | Cancel 132 | Save 133 |
134 |
135 | 136 | 137 |

Reset 's Stopwatch

138 | 139 |

Are you sure you wish to reset 's stopwatch to 00:00:00?

140 | 141 |
142 | No, Cancel 143 | Yes, Reset 144 |
145 |
146 | 147 | 148 | 149 | 150 | -------------------------------------------------------------------------------- /graphics/custom_controls/stopwatches/stopwatches.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | 'use strict'; 3 | 4 | var startAll = document.getElementById('startAll'); 5 | startAll.addEventListener('click', function() { 6 | nodecg.sendMessage('startTime', 'all'); 7 | }); 8 | 9 | var pauseAll = document.getElementById('pauseAll'); 10 | pauseAll.addEventListener('click', function() { 11 | nodecg.sendMessage('pauseTime', 'all'); 12 | }); 13 | 14 | var resetAll = document.getElementById('resetAll'); 15 | resetAll.addEventListener('click', function() { 16 | window.setDialogInfo('all', 'everyone'); 17 | resetDialog.open(); 18 | }); 19 | 20 | /* ----- */ 21 | 22 | var globalButtons = document.getElementById('globalButtons'); 23 | var startButtons = Array.prototype.slice.call(document.querySelectorAll('#play')); 24 | var checklistStatusContainer = document.getElementById('checklistStatusContainer'); 25 | var checklistStatus = document.getElementById('checklistStatus'); 26 | 27 | var checklistComplete = nodecg.Replicant('checklistComplete'); 28 | checklistComplete.on('change', function(oldVal, newVal) { 29 | if (newVal) { 30 | startButtons.forEach(function(button) { 31 | button.removeAttribute('disabled'); 32 | }); 33 | 34 | startAll.removeAttribute('disabled'); 35 | startAll.querySelector('#startAll-notReady').style.display = 'none'; 36 | startAll.querySelector('#startAll-ready').style.display = 'flex'; 37 | 38 | checklistStatus.innerText = 'Checklist Complete'; 39 | checklistStatus.style.fontWeight = 'normal'; 40 | checklistStatusContainer.style.backgroundColor = ''; 41 | globalButtons.style.backgroundColor = ''; 42 | } else { 43 | startButtons.forEach(function(button) { 44 | button.setAttribute('disabled', 'true'); 45 | }); 46 | 47 | startAll.setAttribute('disabled', 'true'); 48 | startAll.querySelector('#startAll-notReady').style.display = 'inline'; 49 | startAll.querySelector('#startAll-ready').style.display = 'none'; 50 | 51 | checklistStatus.innerText = 'Checklist Incomplete, complete before starting'; 52 | checklistStatus.style.fontWeight = 'bold'; 53 | checklistStatusContainer.style.backgroundColor = '#ff6d6b'; 54 | globalButtons.style.backgroundColor = '#ff6d6b'; 55 | } 56 | }); 57 | 58 | /* ----- */ 59 | 60 | var dialogIndex = 0; 61 | var runnerNameEls = Array.prototype.slice.call(document.getElementsByClassName('runnerName')); 62 | 63 | window.setDialogInfo = function (index, name, currentTime) { 64 | dialogIndex = index; 65 | 66 | runnerNameEls.forEach(function(el) { 67 | el.innerText = name; 68 | }); 69 | 70 | if (currentTime) { 71 | editInput.value = currentTime; 72 | } 73 | }; 74 | 75 | /* ----- */ 76 | 77 | var resetDialog = document.getElementById('resetDialog'); 78 | var confirmReset = document.getElementById('confirmReset'); 79 | 80 | confirmReset.addEventListener('click', function() { 81 | confirmReset.setAttribute('disabled', 'true'); 82 | nodecg.sendMessage('resetTime', dialogIndex, function() { 83 | resetDialog.close(); 84 | confirmReset.removeAttribute('disabled'); 85 | }); 86 | }); 87 | 88 | /* ----- */ 89 | 90 | var editDialog = document.getElementById('editDialog'); 91 | var confirmEdit = document.getElementById('confirmEdit'); 92 | var editInput = document.getElementById('editInput'); 93 | 94 | editInput.addEventListener('iron-input-validate', function(e) { 95 | // e.target.validity.valid seems to be busted. Use this workaround. 96 | var isValid = !e.target.hasAttribute('invalid'); 97 | if (isValid) { 98 | confirmEdit.removeAttribute('disabled'); 99 | } else { 100 | confirmEdit.setAttribute('disabled', 'true'); 101 | } 102 | }); 103 | 104 | confirmEdit.addEventListener('click', function() { 105 | if (editInput.validate()) { 106 | var ts = editInput.value.split(':'); 107 | var ms = Date.UTC(1970, 0, 1, ts[0], ts[1], ts[2]); 108 | 109 | confirmEdit.setAttribute('disabled', 'true'); 110 | nodecg.sendMessage('setTime', {index: dialogIndex, milliseconds: ms}, function() { 111 | editDialog.close(); 112 | confirmEdit.removeAttribute('disabled'); 113 | }); 114 | } 115 | }); 116 | })(); 117 | -------------------------------------------------------------------------------- /graphics/img/backgrounds/16x9_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/16x9_1.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/16x9_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/16x9_2.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/3ds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/3ds.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/3x2_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/3x2_1.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/3x2_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/3x2_2.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/4x3_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/4x3_1.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/4x3_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/4x3_2.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/4x3_3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/4x3_3.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/4x3_4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/4x3_4.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/break.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/break.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/ds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/ds.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/ds_portrait.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/ds_portrait.png -------------------------------------------------------------------------------- /graphics/img/backgrounds/interview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/backgrounds/interview.png -------------------------------------------------------------------------------- /graphics/img/boxart/default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/boxart/default.png -------------------------------------------------------------------------------- /graphics/img/consoles/3ds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/3ds.png -------------------------------------------------------------------------------- /graphics/img/consoles/arc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/arc.png -------------------------------------------------------------------------------- /graphics/img/consoles/dc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/dc.png -------------------------------------------------------------------------------- /graphics/img/consoles/ds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/ds.png -------------------------------------------------------------------------------- /graphics/img/consoles/gb.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/gb.png -------------------------------------------------------------------------------- /graphics/img/consoles/gba.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/gba.png -------------------------------------------------------------------------------- /graphics/img/consoles/gbc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/gbc.png -------------------------------------------------------------------------------- /graphics/img/consoles/gcn.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/gcn.png -------------------------------------------------------------------------------- /graphics/img/consoles/gen.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/gen.png -------------------------------------------------------------------------------- /graphics/img/consoles/n64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/n64.png -------------------------------------------------------------------------------- /graphics/img/consoles/nes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/nes.png -------------------------------------------------------------------------------- /graphics/img/consoles/pc.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/pc.png -------------------------------------------------------------------------------- /graphics/img/consoles/ps1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/ps1.png -------------------------------------------------------------------------------- /graphics/img/consoles/ps2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/ps2.png -------------------------------------------------------------------------------- /graphics/img/consoles/ps3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/ps3.png -------------------------------------------------------------------------------- /graphics/img/consoles/ps4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/ps4.png -------------------------------------------------------------------------------- /graphics/img/consoles/psp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/psp.png -------------------------------------------------------------------------------- /graphics/img/consoles/sat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/sat.png -------------------------------------------------------------------------------- /graphics/img/consoles/snes.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/snes.png -------------------------------------------------------------------------------- /graphics/img/consoles/unknown.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/unknown.png -------------------------------------------------------------------------------- /graphics/img/consoles/wii.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/wii.png -------------------------------------------------------------------------------- /graphics/img/consoles/wiiu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/wiiu.png -------------------------------------------------------------------------------- /graphics/img/consoles/wshp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/wshp.png -------------------------------------------------------------------------------- /graphics/img/consoles/x360.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/x360.png -------------------------------------------------------------------------------- /graphics/img/consoles/xbox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/xbox.png -------------------------------------------------------------------------------- /graphics/img/consoles/xboxone.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/consoles/xboxone.png -------------------------------------------------------------------------------- /graphics/img/nameplate/audio-off.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/nameplate/audio-off.png -------------------------------------------------------------------------------- /graphics/img/nameplate/audio-on.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/nameplate/audio-on.png -------------------------------------------------------------------------------- /graphics/img/nameplate/twitch-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/nameplate/twitch-logo.png -------------------------------------------------------------------------------- /graphics/img/omnibar/logo-gdq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/omnibar/logo-gdq.png -------------------------------------------------------------------------------- /graphics/img/omnibar/logo-pcf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/omnibar/logo-pcf.png -------------------------------------------------------------------------------- /graphics/img/sponsors/sponsor1-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/sponsors/sponsor1-horizontal.png -------------------------------------------------------------------------------- /graphics/img/sponsors/sponsor1-vertical.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/sponsors/sponsor1-vertical.png -------------------------------------------------------------------------------- /graphics/img/sponsors/sponsor2-horizontal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/GamesDoneQuick/agdq16-layouts/e656659f96b6c118479221ea257935c5e75d60d3/graphics/img/sponsors/sponsor2-horizontal.png -------------------------------------------------------------------------------- /graphics/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
preload the font
13 | 14 |
15 | 16 | 17 |
18 |
19 |
20 |
21 | 22 | 23 | 24 | 25 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /graphics/lib/video-preloader.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 'use strict'; 3 | 4 | if (typeof define === 'function' && define.amd) { 5 | // AMD. Register as an anonymous module. 6 | define(factory); 7 | } else { 8 | // Browser globals 9 | root.preloadVideos = factory(); 10 | } 11 | }(this, function () { 12 | 'use strict'; 13 | 14 | var preloadedUrls = []; 15 | function isPreloaded(url) { 16 | return preloadedUrls.indexOf(url) > -1; 17 | } 18 | 19 | // Finds the earliest unbuffered timestamp in a video. 20 | // Returns undefined if the entire video is buffered. 21 | function findEarliestGap(videoNode) { 22 | var numChunks = videoNode.buffered.length; 23 | 24 | // If we have one chunk that spans the entire video, then hey there's no gaps! 25 | if (numChunks === 1) { 26 | var start = videoNode.buffered.start(0); 27 | var end = videoNode.buffered.end(0); 28 | if (start === 0 && end === videoNode.duration) { 29 | return; 30 | } 31 | } 32 | 33 | // Loop over each chunk, find any gaps. 34 | for (var i = 0; i < numChunks; i++) { 35 | var nextIndex = i + 1; 36 | 37 | // If this is the last chunk and its end is the end of the video, we have no gaps. 38 | // Else we need to buffer the end. 39 | if (nextIndex >= numChunks) { 40 | if (videoNode.buffered.end(i) === videoNode.duration) { 41 | return; 42 | } else { 43 | return videoNode.buffered.end(i); 44 | } 45 | } 46 | 47 | // If the next segment's start time isn't the same as this chunk's end time, we have a gap. 48 | var currentEnd = videoNode.buffered.end(i); 49 | var nextStart = videoNode.buffered.start(nextIndex); 50 | if (currentEnd !== nextStart) { 51 | return currentEnd; 52 | } 53 | } 54 | } 55 | 56 | return function (urls, cb) { 57 | var videoUrls = []; 58 | if (typeof urls === 'string') { 59 | videoUrls.push(urls); 60 | } else if (Array.isArray(urls)) { 61 | videoUrls = urls; 62 | } else { 63 | throw new Error('Invalid first argument type: ' + typeof urls); 64 | } 65 | 66 | // Filter out any urls that are already preloaded 67 | var urlsToPreload = []; 68 | videoUrls.forEach(function(url) { 69 | if (!isPreloaded(url)) { 70 | urlsToPreload.push(url); 71 | } 72 | }); 73 | 74 | // If all urls are preloaded, immediately invoke the callback 75 | if (urlsToPreload.length <= 0) { 76 | cb(null, typeof urls === 'string' ? urls : videoUrls); 77 | return; 78 | } 79 | 80 | /* We only allow WebM because we know it will have its metadata at the start of the file, 81 | * which this preload method depends on. */ 82 | var numLoaded = 0; 83 | urlsToPreload.forEach(function(url) { 84 | // Create a hidden and muted video tag that will be used to preload the video. 85 | var videoLoader = document.createElement('video'); 86 | videoLoader.style.display = 'none'; 87 | videoLoader.muted = true; 88 | document.body.appendChild(videoLoader); 89 | 90 | // Create a "source" tag for this webm and append it to videoLoader. 91 | var source = document.createElement('source'); 92 | source.src = url; 93 | source.type = 'video/webm'; 94 | videoLoader.appendChild(source); 95 | videoLoader.fullyLoaded = false; 96 | videoLoader.addEventListener('progress', function () { 97 | if (videoLoader.fullyLoaded) return; 98 | if (videoLoader.duration) { 99 | var percent = (videoLoader.buffered.end(0) / videoLoader.duration) * 100; 100 | if (percent >= 100) { 101 | if (!isPreloaded(url)) preloadedUrls.push(url); 102 | videoLoader.fullyLoaded = true; 103 | videoLoader.remove(); 104 | 105 | numLoaded++; 106 | if (numLoaded === urlsToPreload.length) { 107 | cb(null, typeof urls === 'string' ? urls : videoUrls); 108 | } 109 | } 110 | } 111 | }, false); 112 | 113 | /* I have no idea why, but this event seems to be emitted every time the "progress" events stop coming in. 114 | * So, we can use this event to double-check that we have actually buffered the entire video. 115 | * If we find a gap in the buffer, we seek to it and move the playhead a bit. 116 | * This appears to force the browser to resume buffering, and the "progress" events start up again. */ 117 | videoLoader.addEventListener('canplay', function () { 118 | var gap = findEarliestGap(videoLoader); 119 | if (gap) { 120 | videoLoader.currentTime = gap; 121 | videoLoader.currentTime++; 122 | } else { 123 | videoLoader.fullyLoaded = true; 124 | numLoaded++; 125 | if (numLoaded === urlsToPreload.length) { 126 | videoLoader.remove(); 127 | cb(null, typeof urls === 'string' ? urls : videoUrls); 128 | } 129 | } 130 | }, false); 131 | }); 132 | }; 133 | })); 134 | -------------------------------------------------------------------------------- /graphics/style/base.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | opacity: 0; 4 | background-color: rgb(0, 255, 0); 5 | } 6 | 7 | #container { 8 | position: absolute; 9 | width: 1280px; 10 | height: 720px; 11 | overflow: hidden; 12 | } 13 | 14 | #container > #background { 15 | z-index: -1; 16 | } 17 | 18 | #ftbCover { 19 | z-index: 10; 20 | background-color: black; 21 | opacity: 0; 22 | } 23 | 24 | #imageAdContainer { 25 | z-index: 11; 26 | background-color: black; 27 | transform: translateX(-1280px); 28 | } 29 | 30 | #imageAdContainer img { 31 | position: absolute; 32 | left: 640px; 33 | top: 360px; 34 | max-width: 1280px; 35 | height: 720px; 36 | transform: translate(-50%, -50%); 37 | } 38 | 39 | #videoPlayer { 40 | z-index: 11; 41 | } 42 | 43 | #fontPreloader { 44 | position: absolute; 45 | font-family: 'proxima-nova'; 46 | font-weight: 800; 47 | color: rgb(0, 255, 0); 48 | top: 720px; 49 | } 50 | 51 | #sponsorsAndTwitter { 52 | position: absolute; 53 | overflow: hidden; 54 | } 55 | 56 | .fullscreen { 57 | position: absolute; 58 | width: 1280px; 59 | height: 720px; 60 | top: 0; 61 | left: 0; 62 | } 63 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "agdq16-layouts", 3 | "version": "0.1.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "git://github.com/GamesDoneQuick/agdq16-layouts.git" 7 | }, 8 | "homepage": "http://gamesdonequick.com/", 9 | "contributors": [ 10 | "Alex Van Camp ", 11 | "Chris Hanel " 12 | ], 13 | "scripts": { 14 | "electron": "electron electron.js --remote-debugging-port=9222" 15 | }, 16 | "description": "The on-stream graphics used during Awesome Games Done Quick 2016.", 17 | "license": "Apache-2.0", 18 | "nodecg": { 19 | "compatibleRange": "~0.7.0", 20 | "dashboardPanels": [ 21 | { 22 | "name": "schedule", 23 | "title": "Schedule", 24 | "width": 4, 25 | "headerColor": "#2d4e8a", 26 | "file": "schedule.html" 27 | }, 28 | { 29 | "name": "edit-current-run", 30 | "title": "Edit Current Run", 31 | "width": 4, 32 | "dialog": true, 33 | "dialogButtons": [ 34 | { 35 | "name": "save", 36 | "type": "confirm" 37 | }, 38 | { 39 | "name": "cancel", 40 | "type": "dismiss" 41 | } 42 | ], 43 | "file": "dialogs/edit-current-run.html" 44 | }, 45 | { 46 | "name": "advertisements", 47 | "title": "Advertisements", 48 | "width": 4, 49 | "headerColor": "#2d4e8a", 50 | "file": "advertisements.html" 51 | }, 52 | { 53 | "name": "checklist", 54 | "title": "Checklist", 55 | "width": 4, 56 | "headerColor": "#2d4e8a", 57 | "file": "checklist.html" 58 | }, 59 | { 60 | "name": "omnibar", 61 | "title": "Omnibar", 62 | "width": 2, 63 | "headerColor": "#2d4e8a", 64 | "file": "omnibar.html" 65 | }, 66 | { 67 | "name": "total", 68 | "title": "Total", 69 | "width": 2, 70 | "headerColor": "#2d4e8a", 71 | "file": "total.html" 72 | }, 73 | { 74 | "name": "edit-total", 75 | "title": "Edit Total", 76 | "width": 2, 77 | "dialog": true, 78 | "dialogButtons": [ 79 | { 80 | "name": "save", 81 | "type": "confirm" 82 | }, 83 | { 84 | "name": "cancel", 85 | "type": "dismiss" 86 | } 87 | ], 88 | "file": "dialogs/edit-total.html" 89 | }, 90 | { 91 | "name": "twitter", 92 | "title": "Twitter", 93 | "width": 3, 94 | "headerColor": "#55acee", 95 | "file": "twitter.html" 96 | }, 97 | { 98 | "name": "interview", 99 | "title": "Interview", 100 | "width": 4, 101 | "headerColor": "#2d4e8a", 102 | "file": "interview.html" 103 | }, 104 | { 105 | "name": "nowplaying", 106 | "title": "Now Playing", 107 | "width": 2, 108 | "headerColor": "#2d4e8a", 109 | "file": "nowplaying.html" 110 | } 111 | ], 112 | "graphics": [ 113 | { 114 | "file": "index.html", 115 | "width": 1280, 116 | "height": 720, 117 | "singleInstance": true 118 | }, 119 | { 120 | "file": "custom_controls/stopwatches/index.html", 121 | "width": 1280, 122 | "height": 720 123 | } 124 | ] 125 | }, 126 | "dependencies": { 127 | "chokidar": "^1.4.2", 128 | "clone": "^1.0.2", 129 | "debounce": "^1.0.0", 130 | "deep-equal": "^1.0.1", 131 | "emoji": "^0.3.2", 132 | "lastfm": "^0.9.2", 133 | "md5-file": "^2.0.4", 134 | "numeral": "^1.5.3", 135 | "osc": "^2.0.2", 136 | "request": "^2.67.0", 137 | "request-promise": "^1.0.2", 138 | "rieussec": "^0.3.0", 139 | "twitter-stream-api": "^0.4.1" 140 | }, 141 | "devDependencies": { 142 | "electron-prebuilt": "~0.35.0" 143 | } 144 | } 145 | --------------------------------------------------------------------------------