diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a1b588c..4ed8c568 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [v4.2.1] - unreleased ### Fixed - Transpiled KSAs no longer dropped +- Resolve race condition to ensure Javascript dependencies load before "chart" is called +- Add responsive design elements to Scoreboard to reduce object collision + ## [v4.2.0] - 2020-03-26 ### Changed diff --git a/theme/basic.css b/theme/basic.css index 14a5a1e6..9a802a33 100644 --- a/theme/basic.css +++ b/theme/basic.css @@ -78,7 +78,34 @@ input:invalid { opacity: 0.8; position: absolute; right: 0.2em; + background-color: #292929; + background-blend-mode: darken; + padding: 0em 0.2em; + border-top-left-radius: 0.5em; + border-bottom-left-radius: 0.5em; + margin:0em; + height: 1.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition-property: max-width; + transition-duration: 2s; + transition-delay: 0s; +} + +#rankings span.teamname:hover { + max-width: 100%; } + +#rankings span.teampoints { + font-size:100%; + height:1.2em; + margin:0em; + padding:0em; + width:99%; +} + + #rankings div * {white-space: nowrap;} .cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;} .cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;} @@ -90,6 +117,74 @@ input:invalid { .cat7, .cat15, .cat23 {background-color: #ff7f00; color: black;} +/* Responsive design */ +/* Defaults */ + #rankings span.teampoints { + max-width:89%; + } + #rankings span.teamname { + max-width:10%; + } + + +/* Monitors with large enough screens to do side by side */ +@media only screen and (min-width: 170em) { + #chart, #rankings { + width: 49%; + display:inline-block; + vertical-align:middle; + } + +} + +/* Monitor +@media only screen and (max-width: 130em) { + #chart, #rankings { + width: 49%; + display:inline-block; + vertical-align: middle; + } + + #rankings span.teampoints { + max-width:89%; + } + #rankings span.teamname { + max-width:10%; + } +} + +/* Laptop size screen */ +@media only screen and (max-width: 100em) { + #rankings span.teampoints { + max-width:84%; + } + #rankings span.teamname { + max-width:15%; + } +} + +/* Roughly Tablet size */ +@media only screen and (max-width: 70em) { + #rankings span.teampoints { + max-width:79%; + } + #rankings span.teamname { + max-width:20%; + } +} + +/* Small screens phone size */ +@media only screen and (max-width: 40em) { + #rankings span.teampoints { + max-width:65%; + } + #rankings span.teamname { + max-width:34%; + } +} + + + #devel { background-color: #eee; color: black; diff --git a/theme/gibson.css b/theme/gibson.css new file mode 100644 index 00000000..bf8f1a7c --- /dev/null +++ b/theme/gibson.css @@ -0,0 +1,116 @@ +/* Moth ScreenSaver */ +#gibson { + position: fixed; + top: 0; + left: 0; + height: 100vh; + width: 100vw; + z-index: 1000; +} + +/* Transparency is inverted from the clip. Mostly because it was easier this way, you can still make out the content behind it, and the black moths stick out better */ +#gibson.window { + background-color: rgba(160, 160, 250, .2); + + background-image: + repeating-linear-gradient(to right, rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) 0.7em, rgba(0, 0, 0, 0) 1.3em, rgba(0, 0, 0, .8) 2em), + repeating-linear-gradient(rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) .3em, rgba(0, 0, 0, 0) 3em, rgba(0, 0, 0, .8) 3.3em); +} + +#gibson.svg { + background-image: url("data:image/svg+xml;utf8,5928"); + +} + +#gibson.combined { + background-color: rgba(0, 0, 0, .5); + + background-image: + repeating-linear-gradient(to right, rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) 0.7em, rgba(0, 0, 0, 0) 1.3em, rgba(0, 0, 0, .8) 2em), + repeating-linear-gradient(rgba(0, 0, 0, .8), rgba(0, 0, 0, 0) .3em, rgba(0, 0, 0, 0) 3em, rgba(0, 0, 0, .8) 3.3em), + url("data:image/svg+xml;utf8,MOTH"); +} + +#gibson img.moth { + top: 0; + right: 100vw; + position: absolute; + width: 4em; + height: auto; + z-index:1000; +} + +#gibson img.shadow, #gibson div.shadow { + position: absolute; + width: 4em; + height: 4em; + transform: rotate(-45deg); +} + +#gibson div.shadow { + background-color: #000; +/* background-image: radial-gradient(rgba(0, 0, 0, 0), rgba(255, 255, 255, 0.9) 99%); */ + -webkit-mask-image: url("luna-moth.png"); + mask-image: url("luna-moth.png"); + -webkit-mask-size: 4em; + mask-size: 4em; +} + +#gibson div.shadow.LR { + transform: rotate(135deg) !important; + background-color: #d00; +} + +@media only screen and (min-width: 170em) { + #gibson img.moth, #gibson img.shadow { + animation-duration: 12s; + } +} + +@media only screen and (max-width: 130em) { + #gibson img.moth, #gibson img.shadow { + animation-duration: 10s; + } +} + +@media only screen and (max-width: 100em) { + #gibson img.moth, #gibson img.shadow { + animation-duration: 8s; + } +} + +@media only screen and (max-width: 70em) { + #gibson img.moth, #gibson img.shadow { + animation-duration: 6s; + } +} + +@media only screen and (max-width: 40em) { + #gibson img.moth, #gibson img.shadow { + animation-duration: 4s; + } +} + +@keyframes slideRL { + from { + left: 100vw; + transform: rotate(-45deg); + } + + to { + left: -5em; + transform: rotate(-45deg); + } +} + +@keyframes slideLR { + from { + left: -5em; + transform: rotate(135deg); + } + + to { + left: 100vw; + transform: rotate(135deg); + } +} diff --git a/theme/scoreboard-assets/entry-point.png b/theme/scoreboard-assets/entry-point.png new file mode 100644 index 00000000..54693bbf Binary files /dev/null and b/theme/scoreboard-assets/entry-point.png differ diff --git a/theme/scoreboard-assets/forensics.png b/theme/scoreboard-assets/forensics.png new file mode 100644 index 00000000..923e4518 Binary files /dev/null and b/theme/scoreboard-assets/forensics.png differ diff --git a/theme/scoreboard-assets/incident-coordination.png b/theme/scoreboard-assets/incident-coordination.png new file mode 100644 index 00000000..cf13863a Binary files /dev/null and b/theme/scoreboard-assets/incident-coordination.png differ diff --git a/theme/scoreboard-assets/malware.png b/theme/scoreboard-assets/malware.png new file mode 100644 index 00000000..16b5e1ed Binary files /dev/null and b/theme/scoreboard-assets/malware.png differ diff --git a/theme/scoreboard-assets/netarch.png b/theme/scoreboard-assets/netarch.png new file mode 100644 index 00000000..4bdb887d Binary files /dev/null and b/theme/scoreboard-assets/netarch.png differ diff --git a/theme/scoreboard-assets/ot.png b/theme/scoreboard-assets/ot.png new file mode 100644 index 00000000..1cea1b95 Binary files /dev/null and b/theme/scoreboard-assets/ot.png differ diff --git a/theme/scoreboard-base.css b/theme/scoreboard-base.css new file mode 100644 index 00000000..c3d7bb9b --- /dev/null +++ b/theme/scoreboard-base.css @@ -0,0 +1,418 @@ +/* http://paletton.com/#uid=63T0u0k7O9o3ouT6LjHih7ltq4c */ +body { + font-family: sans-serif; + max-width: 40em; + background: #282a33; + color: #f6efdc; +} +body.wide { + max-width: 100%; +} +a:any-link { + color: #8b969a; +} +h1 { + background: #5e576b; + color: #9e98a8; +} +.Fail, .Error, #messages { + background: #3a3119; + color: #ffcc98; +} +.Fail:before { + content: "Fail: "; +} +.Error:before { + content: "Error: "; +} +p { + margin: 1em 0em; +} +form, pre { + margin: 1em; +} +input, select { + padding: 0.6em; + margin: 0.2em; + max-width: 30em; +} +nav { + border: solid black 2px; +} +nav ul, .category ul { + padding: 1em; +} +nav li, .category li { + display: inline; + margin: 1em; +} +iframe#body { + border: inherit; + width: 100%; +} +img { + max-width: 100%; +} +input:invalid { + border-color: red; +} +#messages { + min-height: 3em; + border: solid black 2px; +} +#rankings { + width: 100%; + position: relative; +} + +#rankings span { + font-size: 75%; + display: inline-block; + overflow: hidden; + height: 1.7em; +} +#rankings span.teamname { + font-size: inherit; + color: white; + text-shadow: 0 0 3px black; + opacity: 0.8; + position: absolute; + right: 0.2em; + background-color: #292929; + background-blend-mode: darken; + padding: 0em 0.2em; + border-top-left-radius: 0.5em; + border-bottom-left-radius: 0.5em; + border-bottom: thin solid transparent; + margin:0em; + height: 1.5em; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + transition-property: max-width; + transition-duration: 2s; + transition-delay: 0s; + text-align:right; +} + +#rankings span.teamname:hover { + max-width: 100%; +} + +#rankings span.teampoints { + font-size:100%; + height:1.2em; + margin:0em; + padding:0em; + width:99%; +} + +#rankings .team { + transition-property: all; + transition-duration: 1s; + transition-delay: 0s; +} + + +#rankings div * {white-space: nowrap;} +.cat0, .cat8, .cat16 {background-color: #a6cee3; color: black;} +.cat1, .cat9, .cat17 {background-color: #1f78b4; color: white;} +.cat2, .cat10, .cat18 {background-color: #b2df8a; color: black;} +.cat3, .cat11, .cat19 {background-color: #33a02c; color: white;} +.cat4, .cat12, .cat20 {background-color: #fb9a99; color: black;} +.cat5, .cat13, .cat21 {background-color: #e31a1c; color: white;} +.cat6, .cat14, .cat22 {background-color: #fdbf6f; color: black;} +.cat7, .cat15, .cat23 {background-color: #ff7f00; color: black;} + + +#rankings .teampoints.inv { text-align: center;} + +.hidden { visibility: hidden; } + +/* Responsive design */ +/* Defaults */ + #rankings span.teampoints { + max-width:89%; + } + #rankings span.teamname { + max-width:10%; + min-width:10%; + } + + +/* Monitors with large enough screens to do side by side */ +@media only screen and (min-width: 170em) { + #chart, #rankings { + width: 49%; + display:inline-block; + vertical-align:middle; + } + +} + +/* Monitor +@media only screen and (max-width: 130em) { + #chart, #rankings { + width: 49%; + display:inline-block; + vertical-align: middle; + } + + #rankings span.teampoints { + max-width:89%; + } + #rankings span.teamname { + max-width:10%; + min-width:10%; + } +} + +/* Laptop size screen */ +@media only screen and (max-width: 100em) { + #rankings span.teampoints { + max-width:84%; + } + #rankings span.ueamname { + max-width:15%; + min-width:15%; + } +} + +/* Roughly Tablet size */ +@media only screen and (max-width: 70em) { + #rankings span.teampoints { + max-width:79%; + } + #rankings span.teamname { + max-width:20%; + min-width:20%; + } +} + +/* Small screens phone size */ +@media only screen and (max-width: 40em) { + #rankings span.teampoints { + max-width:65%; + } + #rankings span.teamname { + max-width:34%; + min-width:34%; + } +} + + + +#devel { + background-color: #eee; + color: black; + overflow: scroll; +} +#devel .string { + color: #9c27b0; +} +#devel .body { + background-color: #ffc107; +} +.kvpair { + border: solid black 2px; +} + +.spinner { + display: inline-block; + width: 64px; + height: 64px; + display: block; + width: 46px; + height: 46px; + margin: 1px; + border-radius: 50%; + border: 5px solid #fff; + border-color: #fff transparent #fff transparent; + animation: rotate 1.2s linear infinite; +} +@keyframes rotate { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} + +li[draggable]::before { + content: "↕"; + padding: 0.5em; + cursor: move; +} +li[draggable] { + list-style: none; +} + +[draggable].moving { + opacity: 0.4; +} + +[draggable].over { + border: 1px white dashed; +} + +#cacheButton.disabled { + display: none; +} + +#settings { + position: fixed; + top: 0.5em; + right: 1em; + opacity: 1; + background-blend-mode: darken; + margin:0em; + white-space: nowrap; + overflow: hidden; + text-align: right; + text-overflow: ellipsis; + transition-property: all; + transition-duration: 1s; + transition-delay: 0s; + cursor: pointer; + z-index:10000; + padding-bottom: 1em; + list-style: none; + border-top-left-radius: 0.5em; + border-bottom-left-radius: 0.5em; + border-top-right-radius: 0.5em; + border-bottom-right-radius: 0.5em; + padding:0em; + border: thin solid transparent; +} + +#settings ul { + padding:0em; + margin: 0.75em 0em; +} + +#settings ul > li { + margin-left: 1em; +} + +#settings .btn { + background-color:#33f; + padding: 0.5em 0.5em;; + vertical-align: middle; + border-top-left-radius: 0.5em; + border-top-right-radius: 0.5em; + border-bottom-left-radius: 0.5em; + border-bottom-right-radius: 0.5em; + display:inline-block; + color: white; +} + +#settings > li { + display: none; + width: 95%; + text-align: left; + padding-top: 0.2em; + padding-bottom: 0.2em; +} + +#settings:hover { + border: thin solid #66f; + background-color: #292929; + width: 15em; + padding-left: 0.5em; + padding-bottom: 1em; +} + +#settings:hover li { + display: block; +} + +#settings label { + cursor: pointer; +} + +#rankings .category { + transition-property: margin, width, offset; + transition-duration: 1s; + transition-delay: 0s; +} + +#rankHeading { + visibility:hidden +} + +.track #rankHeading { + visibility:visible; +} + +.leader .icon { + background-repeat: no-repeat; + background-color: transparent; + background-origin: padding-box; + background-size: contain; + padding-left: 1.5em; + height: 1em; + float: left; + display:none; +} + +.fun .leader .icon { + display:inline-block !important; +} + +.icon.track-entry-point { + background-image: url("scoreboard-assets/entry-point.png"); +} + +.icon.track-forensics { + background-image: url("scoreboard-assets/forensics.png"); +} +.icon.track-incident-coordination { + background-image: url("scoreboard-assets/incident-coordination.png"); +} + +.icon.track-malware { + background-image: url("scoreboard-assets/malware.png"); +} + +.icon.track-netarch { + background-image: url("scoreboard-assets/netarch.png"); +} + +.icon.track-ot { + background-image: url("scoreboard-assets/ot.png"); +} + +/* highlight row on hover so it is easier for users to see */ +#rankings > div { + border-bottom: thin solid transparent; +} + +#rankings > div:hover { + font-weight: bold; +/* border-bottom: thin solid #33f !important;*/ + background-color: #779 !important; +} + +#screensaver { + width:110vw; + height:110vh; + position:fixed; + top:-10em; + left:-10em; + background-color: rgba(0, 0, 0, 1); + background-image:url('luna-moth.svg'); + background-blend-mode:multiply; + background-size: 10em 10em; + background-repeat: space; + transition-property: all; + transition-duration: 5s; + transition-delay: 0s; +} + +#screensaverImg { + width:10em; + position: fixed; + top:0em; + left:0em; +} diff --git a/theme/scoreboard.html b/theme/scoreboard.html index ed0c339e..90e77686 100644 --- a/theme/scoreboard.html +++ b/theme/scoreboard.html @@ -2,23 +2,47 @@ Scoreboard - + + - - -

+ +
+ + + + + diff --git a/theme/scoreboard.js b/theme/scoreboard.js index 104efcb8..2a459ea0 100644 --- a/theme/scoreboard.js +++ b/theme/scoreboard.js @@ -1,8 +1,34 @@ // jshint asi:true +var MOTH_RANKING_STANDARD=0; +var MOTH_RANKING_CATEGORY=1; +var MOTH_RANKING_TRACK=2; + +// Comparison functions +var MOTH_COMP_TEAMOVERALL=function (a, b) { + return a.overallScore - b.overallScore; +} + +var MOTH_COMP_POINTSLOGTIME=function(a, b) { + return a[0] - b[0]; +} + +var MOTH_COMP_SCORE=function(a, b) { + return a - b; +} + +var teamNames={}; +var pointsLog={}; + function scoreboardInit() { + // Visual flare + var cortex={ + ranking: MOTH_RANKING_STANDARD, + sorting: MOTH_COMP_TEAMOVERALL, + screenSaverBg: "svg", + }; - chartColors = [ + var chartColors = [ "rgb(255, 99, 132)", "rgb(255, 159, 64)", "rgb(255, 205, 86)", @@ -11,7 +37,19 @@ function scoreboardInit() { "rgb(153, 102, 255)", "rgb(201, 203, 207)" ] - + + // Placeholder for Track mappings + trackMap = { + "Operational-Technology" : "ot", + "Safe_Malware" : "malware", + "linux_memory_intro" : "forensics", + "js" : "netarch", + "sequence" : "netarch", + "networking" : "entry-point", + "codebreaking" : "incident-coordination", + "nocode" : "entry-point", + } + function update(state) { window.state = state @@ -19,10 +57,9 @@ function scoreboardInit() { rotate.appendChild(rotate.firstElementChild) } - let element = document.getElementById("rankings") - let teamNames = state.TeamNames - let pointsLog = state.PointsLog - + teamNames = state.TeamNames + pointsLog = state.PointsLog + // Every machine that's displaying the scoreboard helpfully stores the last 20 values of // points.json for us, in case of catastrophe. Thanks, y'all! // @@ -34,64 +71,68 @@ function scoreboardInit() { } stateHistory.push(state) localStorage.setItem("stateHistory", JSON.stringify(stateHistory)) - + + draw(); + } + + function draw() { let teams = {} + let categories = {} // map[string][team]int let highestCategoryScore = {} // map[string]int - + + let uiRanking=cortex.ranking.valueOf() + let element = document.getElementById("rankings") + + // Initialize data structures for (let teamId in teamNames) { - teams[teamId] = { + teams[teamId] = { categoryScore: {}, // map[string]int + trackScore: {}, // map[string]int overallScore: 0, // int historyLine: [], // []{x: int, y: int} + history: [], // []{t: timestamp, c: category, s: int} name: teamNames[teamId], id: teamId } } - + // Dole out points + pointsLog.sort(MOTH_COMP_POINTSLOGTIME) + for (let entry of pointsLog) { let timestamp = entry[0] let teamId = entry[1] let category = entry[2] - let points = entry[3] + let points = parseInt(entry[3]) let team = teams[teamId] - + let score = team.categoryScore[category] || 0 + let trackScore = team.trackScore[trackMap[category]] || 0 score += points + trackScore += points + team.categoryScore[category] = score - - let highest = highestCategoryScore[category] || 0 - if (score > highest) { - highestCategoryScore[category] = score + + team.history.push({t: new Date(timestamp * 1000), cat: category, score: points}) + + if (!categories[category]) { + categories[category]={} } - } - for (let teamId in teamNames) { - teams[teamId].categoryScore = {} + categories[category][teamId]=score } - - for (let entry of pointsLog) { - let timestamp = entry[0] - let teamId = entry[1] - let category = entry[2] - let points = entry[3] - - let team = teams[teamId] - - let score = team.categoryScore[category] || 0 - score += points - team.categoryScore[category] = score - - let overall = 0 - for (let cat in team.categoryScore) { - overall += team.categoryScore[cat] / highestCategoryScore[cat] - } - team.historyLine.push({t: new Date(timestamp * 1000), y: overall}) + // Search Team score aggregates for highest scores and key markers + for (let cat in categories) { + let scores=Object.values(categories[cat]) + scores.sort(MOTH_COMP_SCORE) + scores.reverse(); + let highest=scores[0] + highestCategoryScore[cat]=highest.valueOf() } - + // Compute overall scores based on current highest for (let teamId in teams) { let team = teams[teamId] @@ -99,19 +140,24 @@ function scoreboardInit() { for (let cat in team.categoryScore) { team.overallScore += team.categoryScore[cat] / highestCategoryScore[cat] } + + // HistoryLine + let overall = 0 + for (let history in team.history) { + let entry=team.history[history] + overall+=entry.score/highestCategoryScore[entry.cat] + team.historyLine.push({t: entry.t, y: overall.toFixed(2)}) + } + } - - // Sort by team score - function teamCompare(a, b) { - return a.overallScore - b.overallScore - } + // Figure out how to order each team on the scoreboard let winners = [] for (let teamId in teams) { winners.push(teams[teamId]) } - winners.sort(teamCompare) + winners.sort(cortex.sorting) winners.reverse() // Let's make some better names for things we've computed @@ -121,30 +167,88 @@ function scoreboardInit() { // Clear out the element we're about to populate Array.from(element.childNodes).map(e => e.remove()) - let maxWidth = 100 / winningScore + let maxWidth = (100 / winningScore) + let avgWidth = (100 / numCategories) + + // Pre-load heading + let headingRow=document.createElement("div") + headingRow.id="rankHeading"; + let headingRowPoints=document.createElement("span") + headingRowPoints.classList.add("teampoints") + headingRowPoints.classList.add("inv") + + let headingNcat=0 + for (let category in highestCategoryScore) { + let bar=document.createElement("span") + bar.title=category + bar.style.width=avgWidth +"%" + bar.classList.add("cat" +headingNcat) + bar.textContent=category + bar.dataset.category=category; + bar.onclick=sortByCategory; + + headingRowPoints.appendChild(bar); + headingNcat+=1 + } + + headingRow.appendChild(headingRowPoints); + element.appendChild(headingRow) + for (let team of winners) { let row = document.createElement("div") + row.classList.add("team"); + row.dataset.overallScore=team.overallScore.toFixed(2); + let ncat = 0 + + let teamPoints=document.createElement("span") + teamPoints.classList.add("teampoints") + + let leader=[]; + for (let category in highestCategoryScore) { let catHigh = highestCategoryScore[category] let catTeam = team.categoryScore[category] || 0 let catPct = catTeam / catHigh - let width = maxWidth * catPct + let width = (maxWidth * catPct) + let catWidth = (avgWidth * catPct) let bar = document.createElement("span") bar.classList.add("category") bar.classList.add("cat" + ncat) - bar.style.width = width + "%" - bar.textContent = category + ": " + catTeam - bar.title = bar.textContent + bar.dataset.standardWidth=width; + bar.dataset.categoryWidth=catWidth; + bar.dataset.category=category; + bar.dataset.points=catTeam; + bar.dataset.categoryMargin=(avgWidth - catWidth) + bar.title = bar.dataset.category + ": " + bar.dataset.points + + if ((catTeam == catHigh) && (trackMap[category])){ + leader.push(trackMap[category]) + } + + displayMothRanking(uiRanking, bar); - row.appendChild(bar) + teamPoints.appendChild(bar) ncat += 1 } + + row.appendChild(teamPoints) let te = document.createElement("span") te.classList.add("teamname") te.textContent = team.name + + for (let track in leader) { + te.classList.add("leader"); + + let img=document.createElement("img"); + img.classList.add("icon"); + img.classList.add("track-"+leader[track]); + + te.prepend(img); + } + row.appendChild(te) element.appendChild(row) @@ -230,22 +334,329 @@ function scoreboardInit() { }) } + let imgRL=null; + let imgLR=null; + let canvas=null; + + function setScreenSaver() { + let uiScreenSaver=document.querySelector("#mothScreenSaver"); + + if (uiScreenSaver.checked) { + if (canvas == null) { + canvas=document.createElement("picture"); + canvas.id="gibson" + } + + document.querySelector("body").appendChild(canvas); + + if (imgRL == null) { + imgRL=document.createElement("img"); + imgRL.classList.add("moth"); + imgRL.src="luna-moth.svg"; + imgRL.style.animation="slideRL 4s linear infinite"; + imgRL.dataset.direction="RL"; + imgRL.addEventListener("animationiteration", lunaRLIterListener, false); + } + + imgRL.style.top="4em"; + + if (imgLR == null) { + imgLR=document.createElement("img"); + imgLR.classList.add("moth"); + imgLR.src="luna-moth.svg"; + imgLR.style.animation="slideLR 4s linear infinite"; + imgLR.dataset.direction="LR"; + imgLR.addEventListener("animationiteration", lunaLRIterListener, false); + } + + imgLR.style.top=0; + + canvas.appendChild(imgLR); + canvas.appendChild(imgRL); + + setScreenSaverBg(); + + setTimeout(lunaShadow, Math.random()*100+400); + + } else { + canvas=document.querySelector("#gibson"); + if (canvas) { + canvas.remove(); + } + + canvas=null; + imgRL=null; + imgLR=null; + } + } + + function setScreenSaverBg() { + let options=["svg", "window", "combined"]; + document.querySelectorAll("input[name=mothScreenSaverBg]").forEach(function(item) { + if (item.checked === true) { + cortex.screenSaverBg=item.value; + } + }); + + + if (canvas !== null) { + for (let i=0; i canvas.clientHeight) { + // Repeat in infinite loop +// imgRL.style.top="4em"; + + // Uncomment to run through screen once + imgRL.style.animationPlayState="paused"; + } else { + imgRL.style.top=(coord.y + (2*coord.height))+"px"; + } + } + + function lunaLRIterListener(event) { + let coord=imgLR.getBoundingClientRect(); + + if ((coord.y + coord.height) > canvas.clientHeight) { + // Repeat in infinite loop +// imgLR.style.top="0px"; + + // Uncomment to run through screen once + imgLR.style.animationPlayState="paused"; + } else { + imgLR.style.top=(coord.y + (2*coord.height))+"px"; + } + } + + function lunaShadow() { + // let coordCanvas=canvas.getBoundingClientRect(); + let targets=[imgRL, imgLR]; + + targets.forEach(function(item) { + if (item == null) { + return false; + } + + let coord=item.getBoundingClientRect(); + let div=document.createElement("div"); + div.className="shadow "+item.dataset.direction; + div.style.top=item.style.top; + div.style.left=coord.left +"px"; + + canvas.appendChild(div); + }); + + // Only run through screen once + if ((imgRL != null) && (imgRL.style.animationPlayState != "paused")) { + setTimeout(lunaShadow, Math.random()*400+200); + } + + } + + function init() { let base = window.location.href.replace("scoreboard.html", "") let location = document.querySelector("#location") + let params = new URLSearchParams(document.location.search.substring(1)); + if (location) { location.textContent = base } - + + // Grab initial settings and set event handlers + document.querySelectorAll("input[name=rankingPerspective]").forEach(function(item) { + if (item.value == params.get(item.name)) { + item.checked=true; + } + item.onchange=setRankingTrack; + });; + + // Leader Icons + let leaderIcons=document.querySelector("#mothLeaderIcons"); + if (leaderIcons) { + leaderIcons.checked=(params.get(leaderIcons.name) == leaderIcons.value); + leaderIcons.onchange=setShowLeaderIcons; + } + + // Screensaver + let screenSaver=document.querySelector("#mothScreenSaver"); + if (screenSaver) { + screenSaver.checked=(params.get(screenSaver.name) == screenSaver.value); + screenSaver.onchange=setScreenSaver; + } + + document.querySelectorAll("input[name=mothScreenSaverBg]").forEach(function(item) { + if (item.value == params.get(item.name)) { + item.checked=true; + } + + item.onchange=setScreenSaverBg;; + }); + + setRankingTrack(); + setShowLeaderIcons(); + setScreenSaver(); + setInterval(refresh, 60000) refresh() } - + + + init() } + if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", scoreboardInit) } else { scoreboardInit() } + +