4 <meta http-equiv="content-type" value="text/html; charset=utf-8" />
5 <meta charset="utf-8" />
6 <!-- <link href="https://fonts.googleapis.com/css?family=Lato" rel="stylesheet"> -->
7 <link href="fonts/lato.css" rel="stylesheet">
10 -webkit-box-sizing: border-box;
11 Box-Sizing: border-box;
12 -webkit-backface-visibility: hidden;
13 -webkit-transition: translate3d(0,0,0);
20 background: transparent;
22 -webkit-font-smoothing: antialiased !important;
25 font-family: 'Lato', sans-serif;
35 border-collapse: collapse;
37 border: 3px solid black;
41 border: 3px solid #008;
43 .team1color, .team2color {
67 /* Clock, to the right of score */
73 border-collapse: collapse;
75 border: 3px solid black;
78 border: 3px solid #008;
85 -webkit-transform:translateX(-200%);
87 .clockbug-animate-in {
88 -webkit-animation: from-left 1s ease;
90 .clockbug-animate-out {
91 -webkit-animation: to-left 1s ease;
92 -webkit-transform:translateX(-200%);
95 /* Comment, below score */
101 border-collapse: collapse;
103 border: 3px solid black;
106 border: 3px solid #008;
114 -webkit-transform:translateY(-40px);
116 .commentbug-animate-in {
117 -webkit-animation: from-top 0.5s ease;
119 .commentbug-animate-out {
120 -webkit-animation: to-top 0.5s ease;
121 -webkit-transform:translateY(-40px);
125 .lowerthird-headline {
130 border-collapse: collapse;
132 border: 2px solid #ccc;
136 /*background: linear-gradient(to right, #ccc, #fff 15px); */
137 background-image: url(lowerthird-bg.png);
139 /* padding-top: 20px; */
141 .lowerthird-headline-hidden {
146 .lowerthird-headline-animate-in {
147 -webkit-animation: scale-out 1.0s;
149 .lowerthird-headline-animate-out {
150 -webkit-animation: scale-in 1.0s;
152 .lowerthird-headline-content {
156 .lowerthird-headline-content-hidden {
157 clip: rect(0px,0px,200px,0px);
159 .lowerthird-headline-content-animate-in {
160 -webkit-animation: wipe-out 2.0s ease;
162 .lowerthird-headline-content-animate-out {
163 -webkit-animation: wipe-in 2.0s ease;
164 clip: rect(0px,0px,200px,0px);
166 .lowerthird-subheading {
173 border-collapse: collapse;
175 border: 1px solid black;
177 align-items: center; */
178 /*background: linear-gradient(to right, #44c, #33a 15px); */
179 background-image: url(lowerthird-bg2.png);
181 .lowerthird-subheading-hidden {
182 clip: rect(0px,0px,200px,0px);
184 .lowerthird-subheading-animate-in {
185 -webkit-animation: scale-out-small 1.6s ease;
187 .lowerthird-subheading-animate-out {
188 -webkit-animation: scale-in-small 1.6s ease;
192 .lowerthird-subheading-content {
196 .lowerthird-subheading-content-hidden {
200 .lowerthird-subheading-content-animate-in {
201 -webkit-animation: wipe-out 2.2s ease;
203 .lowerthird-subheading-content-animate-out {
204 -webkit-animation: wipe-in 2.2s ease;
205 clip: rect(0px,0px,200px,0px);
208 /* these are hidden when actually run in casparcg */
222 /* background: rgba(0, 0, 170, 0.8); */
223 background: transparent;
228 border-spacing: 0px 7px;
230 border-collapse: separate;
231 text-transform: uppercase;
235 text-transform: none;
239 text-transform: none;
242 -webkit-animation: fade-in 1.0s ease;
243 -webkit-animation-delay: 0.0s;
244 -webkit-animation-fill-mode: both;
246 #carousel tr.subfooter {
247 -webkit-animation: fade-in 1.0s ease;
248 -webkit-animation-delay: 0.25s;
249 -webkit-animation-fill-mode: both;
252 /* background: rgba(0, 0, 170, 0.8); */
253 background: linear-gradient(to bottom, rgba(0, 0, 170, 0.8), rgba(0, 0, 130, 0.8));
254 /* -webkit-animation: fade-in calc(counter(rowNumber) * 1.0)s ease; */
255 -webkit-animation: fade-in calc(counter-value(rowNumber) * 1.0s) ease;
257 #carousel tr.footer {
258 -webkit-animation: fade-in 2.0s ease;
259 -webkit-animation-delay: 0.5s;
260 -webkit-animation-fill-mode: both;
262 #carousel td, #carousel th {
266 #carousel tr.footer td {
268 text-transform: none;
277 .nplayed, .gd, .pts {
288 @-webkit-keyframes from-left {
290 -webkit-transform:translateX(-200%);
293 -webkit-transform:translateX(0);
296 @-webkit-keyframes to-left {
298 -webkit-transform:translateX(0);
301 -webkit-transform:translateX(-200%);
304 @-webkit-keyframes from-top {
306 -webkit-transform:translateY(-40px);
309 -webkit-transform:translateY(0);
312 @-webkit-keyframes to-top {
314 -webkit-transform:translateY(0);
317 -webkit-transform:translateY(-40px);
321 @-webkit-keyframes scale-out {
329 @-webkit-keyframes scale-in {
332 border: 2px solid #ccc;
336 border: 2px solid #ccc;
339 @-webkit-keyframes scale-out-small {
349 border: 1px solid black;
355 @-webkit-keyframes scale-in-small {
358 border: 1px solid black;
362 border: 1px solid black;
372 @-webkit-keyframes wipe-out {
374 clip: rect(0px,0px,200px,0px);
377 clip: rect(0px,0px,200px,0px);
380 clip: rect(0px,1050px,200px,0px);
383 @-webkit-keyframes wipe-in {
385 clip: rect(0px,1050px,200px,0px);
387 20% { /* Not symmetrical! */
388 clip: rect(0px,0px,200px,0px);
391 clip: rect(0px,0px,200px,0px);
394 @-webkit-keyframes fade-in {
405 <div id="area"></div>
406 <table class="scorebug">
408 <td class="team1color" id="team1color"></td>
409 <td class="team1" id="team1">PCL</td>
410 <td class="score" id="score">17 – 11</td>
411 <td class="team2" id="team2">NMBUI</td>
412 <td class="team2color" id="team2color"></td>
415 <table class="clockbug clockbug-hidden" id="clockbug">
417 <td class="clock" id="clock">25:00</td>
420 <table class="commentbug commentbug-hidden" id="commentbug">
422 <td class="comment" id="comment">Pagacap: First to 9 points</td>
425 <div class="lowerthird-headline lowerthird-headline-hidden" id="lowerthird-headline"><div class="lowerthird-headline-content lowerthird-headline-content-hidden" id="lowerthird-headline-content">
426 John Doe<br>Ola Nordmann
428 <div class="lowerthird-subheading lowerthird-subheading-hidden" id="lowerthird-subheading"><div class="lowerthird-subheading-content lowerthird-subheading-content-hidden" id="lowerthird-subheading-content">
429 Commentators, Trøndisk 2016
431 <table id="carousel">
434 <th colspan="5">Current standings, TrønDisk 2017<br />Group A</th>
437 <tr class="subfooter">
438 <th class="rank"></th>
439 <th class="team"></th>
440 <th class="nplayed">P</th>
441 <th class="gd">GD</th>
442 <th class="pts">Pts</th>
446 <th class="rank">1</th>
447 <td class="team">Trondheim Frisbeeklubb</td>
448 <td class="nplayed">3</td>
449 <td class="gd">+7</td>
450 <td class="pts">6</td>
453 <th class="rank">2</th>
454 <td class="team">Pancake Circle Ltd.</td>
455 <td class="nplayed">2</td>
456 <td class="gd">+2</td>
457 <td class="pts">4</td>
460 <th class="rank">3</th>
461 <td class="team">Norges Handelshøyskole 1</td>
462 <td class="nplayed">2</td>
463 <td class="gd">-2</td>
464 <td class="pts">2</td>
467 <th class="rank">3</th>
468 <td class="team">Pick-up Team</td>
469 <td class="nplayed">2</td>
470 <td class="gd">-2</td>
471 <td class="pts">2</td>
474 <th class="rank">5</th>
475 <td class="team">Whatever Team</td>
476 <td class="nplayed">3</td>
477 <td class="gd">-11</td>
478 <td class="pts">0</td>
481 <td colspan="5">www.trondheimfrisbeeklubb.no | #trøndisk</td>
485 <div id="manualcontrols">
487 <a href="javascript:startclock()">start clock</a>
488 <a href="javascript:stopclock()">stop clock</a>
489 <a href="javascript:setclock(25*60)">reset clock</a>
490 <a href="javascript:showclock()">show clock</a>
491 <a href="javascript:hideclock()">hide clock</a>
494 <a href="javascript:goalA()">goal team A</a>
495 <a href="javascript:goalB()">goal team B</a>
496 <a href="javascript:ungoalA()">ungoal team A</a>
497 <a href="javascript:ungoalB()">ungoal team B</a>
498 <a href="javascript:resetscore()">reset score</a>
501 <a href="javascript:showcomment()">show comment</a>
502 <a href="javascript:hidecomment()">hide comment</a>
505 <a href="javascript:showlowerthird()">show lower third</a>
506 <a href="javascript:hidelowerthird()">hide lower third</a>
511 var clock_running = false;
512 var clock_visible = false;
513 var comment_visible = false;
514 var lowerthird_visible = false;
515 var clock_left = 25 * 60;
523 document.getElementById('team1').innerHTML = state['team1'];
524 document.getElementById('team2').innerHTML = state['team2'];
529 document.getElementById('team1color').style.backgroundColor = state['team1color'];
530 document.getElementById('team2color').style.backgroundColor = state['team2color'];
535 scoreA = state['score1'];
536 scoreB = state['score2'];
540 function startclock()
542 if (!clock_running) {
543 clock_origin = Date.now();
544 clock_running = true;
551 if (!clock_running) return;
552 clock_left = time_left();
553 clock_origin = Date.now();
554 clock_running = false;
557 function setclock(amount)
560 clock_origin = Date.now();
564 function setclockfromstate()
566 var amount = parseInt(state['clock_min']) * 60 + parseInt(state['clock_sec']);
572 if (clock_visible) return;
573 var clockbug = document.getElementById('clockbug');
574 clockbug.className = 'clockbug clockbug-animate-in';
575 clock_visible = true;
580 if (!clock_visible) return;
581 var clockbug = document.getElementById('clockbug');
582 clockbug.className = 'clockbug clockbug-animate-out';
583 clock_visible = false;
586 function setcomment()
588 document.getElementById('comment').innerHTML = state['comment'];
591 function showcomment()
593 if (comment_visible) return;
594 var commentbug = document.getElementById('commentbug');
595 commentbug.className = 'commentbug commentbug-animate-in';
596 comment_visible = true;
599 function hidecomment()
601 if (!comment_visible) return;
602 var commentbug = document.getElementById('commentbug');
603 commentbug.className = 'commentbug commentbug-animate-out';
604 comment_visible = false;
607 function showlowerthird()
609 if (lowerthird_visible) return;
611 // With no flexbox, this is how it has to be...
612 var f = document.getElementById('lowerthird-headline');
613 var g = document.getElementById('lowerthird-headline-content');
614 f.style.paddingTop = Math.round((f.clientHeight - g.clientHeight) / 2) + 'px';
616 f = document.getElementById('lowerthird-subheading');
617 g = document.getElementById('lowerthird-subheading-content');
618 f.style.paddingTop = Math.round((f.clientHeight - g.clientHeight) / 2) + 'px';
620 document.getElementById('lowerthird-headline').className = 'lowerthird-headline lowerthird-headline-animate-in';
621 document.getElementById('lowerthird-headline-content').className = 'lowerthird-headline-content lowerthird-headline-content-animate-in';
622 document.getElementById('lowerthird-subheading').className = 'lowerthird-subheading lowerthird-subheading-animate-in';
623 document.getElementById('lowerthird-subheading-content').className = 'lowerthird-subheading-content lowerthird-subheading-content-animate-in';
624 lowerthird_visible = true;
627 function setandshowlowerthird()
629 document.getElementById('lowerthird-headline-content').innerHTML = state['text1'];
630 document.getElementById('lowerthird-subheading-content').innerHTML = state['text2'];
634 function hidelowerthird()
636 if (!lowerthird_visible) return;
637 document.getElementById('lowerthird-headline').className = 'lowerthird-headline lowerthird-headline-hidden lowerthird-headline-animate-out';
638 document.getElementById('lowerthird-headline-content').className = 'lowerthird-headline-content lowerthird-headline-content-animate-out';
639 document.getElementById('lowerthird-subheading').className = 'lowerthird-subheading lowerthird-subheading-animate-out';
640 document.getElementById('lowerthird-subheading-content').className = 'lowerthird-subheading-content lowerthird-subheading-content-animate-out';
641 lowerthird_visible = false;
646 var elapsed = (Date.now() - clock_origin) * 1e-3;
647 if (elapsed > clock_left) return 0;
648 return Math.ceil(clock_left - elapsed);
651 function update_clock()
653 var left = time_left();
654 var min = Math.floor(left / 60);
657 if (sec < 10) sec = "0" + sec;
658 document.getElementById('clock').innerHTML = min + ":" + sec;
675 if (scoreA > 0) --scoreA;
681 if (scoreB > 0) --scoreB;
685 function resetscore()
691 function update_score()
693 document.getElementById('score').innerHTML = scoreA + " – " + scoreB;
696 /* called by caspar only */
699 document.getElementById('manualcontrols').style.display = 'none';
700 document.getElementById('area').style.display = 'none';
702 // Old CEF workaround
703 document.getElementById('lowerthird-subheading').style.top = '638px';
708 console.log('[[[' + v + ']]]');
709 var j = JSON.parse(v);
710 for(var key in j) state[key] = j[key];
713 setInterval(function() {
723 addtd = function(tr, className, content) {
724 var td = document.createElement("td");
725 td.appendChild(document.createTextNode(content));
726 td.className = className;
729 addth = function(tr, className, content) {
730 var th = document.createElement("th");
731 th.appendChild(document.createTextNode(content));
732 th.className = className;
736 subrank_partitions = function(games, parts, start_rank, tiebreakers) {
738 for (var i = 0; i < parts.length; ++i) {
739 var part = rank(games, parts[i], start_rank, tiebreakers);
740 for (var j = 0; j < part.length; ++j) {
741 result.push(part[j]);
743 start_rank += part.length;
748 partition = function(teams, compare)
753 var curr_part = [teams[0]];
754 for (var i = 1; i < teams.length; ++i) {
755 if (compare(teams[i], curr_part[0]) != 0) {
756 parts.push(curr_part);
759 curr_part.push(teams[i]);
761 if (curr_part.length != 0) {
762 parts.push(curr_part);
767 explain_tiebreaker = function(parts, rule_name)
770 for (var i = 0; i < parts.length; ++i) {
771 result.push(parts[i].map(function(x) { return x.shortname; }).join("/"));
773 return result.join(" > ") + " (" + rule_name + ")";
776 make_teams_to_idx = function(teams)
778 var teams_to_idx = [];
779 for (var i = 0; i < teams.length; i++) {
780 teams_to_idx[teams[i].name] = i;
785 partition_by_beat = function(games, teams)
787 // Head-to-head score by way of components. First construct the beat matrix.
788 var n = teams.length;
789 var beat = new Array(n);
790 var teams_to_idx = make_teams_to_idx(teams);
791 for (var i = 0; i < n; i++) {
792 beat[i] = new Array(n);
793 for (var j = 0; j < n; j++) {
797 for (i = 0; i < games.length; ++i) {
798 var idx1 = teams_to_idx[games[i].name1];
799 var idx2 = teams_to_idx[games[i].name2];
800 if (idx1 !== undefined && idx2 !== undefined) {
801 if (games[i].score1 > games[i].score2) {
802 beat[idx1][idx2] = 1;
804 if (games[i].score1 < games[i].score2) {
805 beat[idx2][idx1] = 1;
809 // Floyd-Warshall for transitive closure.
810 for (var k = 0; k < n; ++k) {
811 for (var i = 0; i < n; ++i) {
812 for (var j = 0; j < n; ++j) {
813 if (beat[i][k] && beat[k][j]) {
820 // See if we can find any team that is comparable to all others.
821 for (var pivot_idx = 0; pivot_idx < n; pivot_idx++) {
822 var incomparable = false;
823 for (var i = 0; i < n; ++i) {
824 if (i != pivot_idx && beat[pivot_idx][i] == 0 && beat[i][pivot_idx] == 0) {
830 // Split the teams into three partitions:
831 var better_than_pivot = [], equal = [], worse_than_pivot = [];
832 for (var i = 0; i < n; ++i) {
833 var we_beat = (beat[pivot_idx][i] == 1);
834 var they_beat = (beat[i][pivot_idx] == 1);
835 if ((i == pivot_idx) || (we_beat && they_beat)) {
836 equal.push(teams[i]);
837 } else if (we_beat && !they_beat) {
838 worse_than_pivot.push(teams[i]);
839 } else if (they_beat && !we_beat) {
840 better_than_pivot.push(teams[i]);
842 console.log("this shouldn't happen");
846 if (better_than_pivot.length > 0) {
847 result = partition_by_beat(games, better_than_pivot);
849 result.push(equal); // Obviously can't be partitioned further.
850 if (worse_than_pivot.length > 0) {
851 result = result.concat(partition_by_beat(games, worse_than_pivot));
857 // No usable pivot was found, so the graph is inherently
858 // disconnected, and we cannot partition it.
862 // Takes in an array, gives every element a rank starting with 1, and returns.
863 rank = function(games, teams, start_rank, tiebreakers) {
864 if (teams.length <= 1) {
865 // Only one team, so trivial.
866 teams[0].rank = start_rank;
870 // Rule #0: Partition the teams by score.
871 var score_parts = partition(teams, function(a, b) { return b.pts - a.pts });
872 if (score_parts.length > 1) {
873 return subrank_partitions(games, score_parts, start_rank, tiebreakers);
876 // Rule #1: Head-to-head wins.
877 var beat_parts = partition_by_beat(games, teams);
878 if (beat_parts.length > 1) {
879 tiebreakers.push(explain_tiebreaker(beat_parts, 'head-to-head'));
880 return subrank_partitions(games, beat_parts, start_rank, tiebreakers);
883 // Rule #2: Number of games played (fewer is better).
884 // Actually the rule says “fewest losses”, but fewer games is equivalent
885 // as long as teams have the same amount of points and ties don't exist.
886 var nplayed_parts = partition(teams, function(a, b) { return a.nplayed - b.nplayed });
887 if (nplayed_parts.length > 1) {
888 tiebreakers.push(explain_tiebreaker(nplayed_parts, 'fewer losses'));
889 return subrank_partitions(games, nplayed_parts, start_rank, tiebreakers);
892 // Rule #3: Head-to-head goal difference.
893 var teams_to_idx = make_teams_to_idx(teams);
894 for (var i = 0; i < teams.length; i++) {
896 teams[i].h2h_goals = 0;
899 for (i = 0; i < games.length; ++i) {
900 var idx1 = teams_to_idx[games[i].name1];
901 var idx2 = teams_to_idx[games[i].name2];
902 if (idx1 !== undefined && idx2 !== undefined) {
903 teams[idx1].h2h_gd += games[i].score1;
904 teams[idx1].h2h_gd -= games[i].score2;
905 teams[idx2].h2h_gd += games[i].score2;
906 teams[idx2].h2h_gd -= games[i].score1;
908 teams[idx1].h2h_goals += games[i].score1;
909 teams[idx2].h2h_goals += games[i].score2;
911 if (idx1 !== undefined) {
912 teams[idx1].goals += games[i].score1;
914 if (idx2 !== undefined) {
915 teams[idx2].goals += games[i].score2;
918 var h2h_gd_parts = partition(teams, function(a, b) { return b.h2h_gd - a.h2h_gd });
919 if (h2h_gd_parts.length > 1) {
920 tiebreakers.push(explain_tiebreaker(h2h_gd_parts, 'head-to-head goal difference'));
921 return subrank_partitions(games, h2h_gd_parts, start_rank, tiebreakers);
924 // Rule #4: Global goal difference. (Well, not strictly, but good enough.)
925 var gd_parts = partition(teams, function(a, b) { return b.gd - a.gd });
926 if (gd_parts.length > 1) {
927 tiebreakers.push(explain_tiebreaker(gd_parts, 'overall goal difference'));
928 return subrank_partitions(games, gd_parts, start_rank, tiebreakers);
931 // Rule #5: Head-to-head scored goals.
932 var h2h_goals_parts = partition(teams, function(a, b) { return b.h2h_goals - a.h2h_goals });
933 if (h2h_goals_parts.length > 1) {
934 tiebreakers.push(explain_tiebreaker(h2h_goals_parts, 'head-to-head scored goals'));
935 return subrank_partitions(games, h2h_goals_parts, start_rank, tiebreakers);
938 // Rule #6: Overall scored goals. (Same caveat as #4.)
939 var goals_parts = partition(teams, function(a, b) { return b.goals - a.goals });
940 if (goals_parts.length > 1) {
941 tiebreakers.push(explain_tiebreaker(goals_parts, 'scored goals'));
942 return subrank_partitions(games, goals_parts, start_rank, tiebreakers);
945 // OK, it's a tie. Give them all the same rank.
947 for (var i = 0; i < teams.length; ++i) {
948 result.push(teams[i]);
949 result[i].rank = start_rank;
954 var req = new XMLHttpRequest();
955 req.onload = function(e) {
956 var response = JSON.parse(req.responseText);
957 console.log(response.values);
958 var carousel = document.getElementById('carousel');
960 for (var i = 2; response.values[i].length >= 1; ++i) {
962 "name": response.values[i][0],
963 "shortname": response.values[i][1],
964 "nplayed": parseInt(response.values[i][2]),
965 "gd": parseInt(response.values[i][3].replace(/−/, '-')),
966 "pts": parseInt(response.values[i][4])
970 for (var i = 12; response.values[i] !== undefined && response.values[i].length >= 1; ++i) {
971 if (response.values[i][2] && response.values[i][3]) {
973 "name1": response.values[i][0],
974 "name2": response.values[i][1],
975 "score1": parseInt(response.values[i][2]),
976 "score2": parseInt(response.values[i][3])
983 teams = rank(games, teams, 1, tiebreakers);
984 console.log(tiebreakers. join(", "));
987 for (i = 0; i < teams.length; ++i) {
988 var tr = document.createElement("tr");
990 addth(tr, "rank", teams[i].rank);
991 addtd(tr, "team", teams[i].name);
992 addtd(tr, "nplayed", teams[i].nplayed);
993 addtd(tr, "gd", teams[i].gd.toString().replace(/-/, '−'));
994 addtd(tr, "pts", teams[i].pts);
996 tr.style = "-webkit-animation: fade-in 1.0s ease; -webkit-animation-delay: " + 0.25 * (row_num++) + "s; -webkit-animation-fill-mode: both;";
997 carousel.appendChild(tr);
1000 if (tiebreakers.length > 0) {
1001 var tie_tr = document.createElement("tr");
1002 tie_tr.className = "footer";
1003 tie_tr.style = "-webkit-animation: fade-in 2.0s ease; -webkit-animation-delay: " + 0.25 * (row_num++) + "s; -webkit-animation-fill-mode: both;";
1004 var td = document.createElement("td");
1005 td.appendChild(document.createTextNode("Tiebreaks applied: " + tiebreakers.join(', ')));
1006 td.setAttribute("colspan", "5");
1007 tie_tr.appendChild(td);
1008 carousel.appendChild(tie_tr);
1011 var footer_tr = document.createElement("tr");
1012 footer_tr.className = "footer";
1013 footer_tr.style = "-webkit-animation: fade-in 2.0s ease; -webkit-animation-delay: " + 0.25 * (row_num++) + "s; -webkit-animation-fill-mode: both;";
1014 var td = document.createElement("td");
1015 td.appendChild(document.createTextNode("www.trondheimfrisbeeklubb.no | #trøndisk"));
1016 td.setAttribute("colspan", "5");
1017 footer_tr.appendChild(td);
1018 carousel.appendChild(footer_tr);
1019 console.log(footer_tr);
1021 carousel.style.display = 'table';
1023 req.open('GET', 'https://sheets.googleapis.com/v4/spreadsheets/1CwRHQtpokVMGTPJu2FYYG-6rnG7OfISIcEHwBfXh-Y4/values/A1:E22?key=AIzaSyAuP9yQn8g0bSay6r_RpGtpFeIbwprH1TU');