├── .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 |
80 |
81 |
82 |
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 |
35 |
36 | {{value.filename}}
37 |
38 |
39 |
42 |
43 |
44 |
45 |
83 |
--------------------------------------------------------------------------------
/dashboard/elements/ad-list.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
16 |
17 |
18 |
23 |
24 |
25 |
26 |
77 |
--------------------------------------------------------------------------------
/dashboard/elements/disabled-cover.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
68 |
--------------------------------------------------------------------------------
/dashboard/elements/edit-runner.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
18 |
19 |
20 | Runner [[_calcRunnerDisplayIndex(index)]]
21 |
22 |
23 |
24 |
25 |
26 |
35 |
--------------------------------------------------------------------------------
/dashboard/elements/label-value.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
34 |
35 |
36 | [[label]]
37 | [[value]]
38 |
39 |
40 |
41 |
51 |
--------------------------------------------------------------------------------
/dashboard/elements/time-ago.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
66 |
--------------------------------------------------------------------------------
/dashboard/elements/tweet-item.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
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 |
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 |
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 |
57 |
58 | [[song]]
59 |
60 |
61 |
62 |
63 | [[album]]
64 |
65 |
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 |
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 |
119 |
120 |
121 |
122 | Edit Stopwatch
123 |
124 |
125 |
126 |
127 |
129 |
130 |
134 |
135 |
136 |
137 | Reset 's Stopwatch
138 |
139 | Are you sure you wish to reset 's stopwatch to 00:00:00?
140 |
141 |
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 |
--------------------------------------------------------------------------------