summaryrefslogtreecommitdiffstats
path: root/assets
diff options
context:
space:
mode:
Diffstat (limited to 'assets')
-rw-r--r--assets/first-blood.pngbin0 -> 141954 bytes
-rw-r--r--assets/presenter.css221
-rw-r--r--assets/presenter.js70
3 files changed, 291 insertions, 0 deletions
diff --git a/assets/first-blood.png b/assets/first-blood.png
new file mode 100644
index 0000000..d36262d
--- /dev/null
+++ b/assets/first-blood.png
Binary files differ
diff --git a/assets/presenter.css b/assets/presenter.css
new file mode 100644
index 0000000..ee5bdec
--- /dev/null
+++ b/assets/presenter.css
@@ -0,0 +1,221 @@
+@import url('https://fonts.googleapis.com/css2?family=Source+Code+Pro:ital,wght@0,200..900;1,200..900&display=swap');
+
+html, body {
+ margin: 0;
+ height: 100%;
+ background: #000;
+ overflow: hidden;
+}
+
+#scoreboard-frame {
+ position: fixed;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ border: none;
+}
+
+#solve-overlay {
+ position: fixed;
+ inset: 0;
+ background: #050505;
+ opacity: 0;
+ transition: opacity 0.3s ease;
+ z-index: 10;
+ pointer-events: none;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+#solve-overlay.visible {
+ opacity: 1;
+}
+
+#solve-text {
+ text-align: center;
+ font-family: "Source Code Pro";
+ letter-spacing: 0.05em;
+ color: white;
+}
+
+#solve-first {
+ display: none;
+}
+
+#solve-first.visible {
+ display: flex;
+ height: 35vh;
+ margin: -5vh 0 -2vh 0;
+ align-items: center;
+ justify-content: center;
+ flex-direction: column;
+}
+
+#solve-first img {
+ z-index: 99999;
+ position: relative;
+ height: 100%
+}
+
+#solve-team {
+ white-space: nowrap;
+ font-size: 15vh;
+ font-weight: 900;
+ letter-spacing: 4px;
+}
+
+#solve-value {
+ margin-top: 2vh;
+ font-weight: 700;
+ font-size: 10vh;
+}
+
+#solve-challenge {
+ margin-top: 2vh;
+ font-weight: 400;
+ font-size: 10vh;
+ letter-spacing: -0.1em;
+}
+
+
+
+.glitch {
+ position: relative;
+}
+
+.glitch::before,
+.glitch::after {
+ content: attr(data-text);
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: #050505;
+}
+
+.glitch::before {
+ left: 3px;
+ text-shadow: -2px 0 #ff00c1;
+ clip-path: inset(44% 0 61% 0);
+ animation: glitch-anim 2s infinite linear alternate-reverse;
+}
+
+.glitch::after {
+ left: -3px;
+ text-shadow: -2px 0 #00fff9;
+ clip-path: inset(54% 0 10% 0);
+ animation: glitch-anim 2.5s infinite linear alternate-reverse;
+}
+
+@keyframes glitch-anim {
+ 0% { clip-path: inset(80% 0 10% 0); }
+ 20% { clip-path: inset(10% 0 70% 0); }
+ 40% { clip-path: inset(50% 0 20% 0); }
+ 60% { clip-path: inset(10% 0 60% 0); }
+ 80% { clip-path: inset(30% 0 40% 0); }
+ 100% { clip-path: inset(60% 0 5% 0); }
+}
+
+
+
+.scanlines {
+ overflow: hidden;
+ mix-blend-mode: difference;
+}
+
+.scanlines::before {
+ content: "";
+ position: absolute;
+ width: 100%;
+ height: 150%;
+ top: -25%;
+ left: 0;
+ background: repeating-linear-gradient(to bottom, transparent 0%, rgba(255, 255, 255, 0.02) 0.25%, transparent 0.5%);
+ animation: fudge 5s linear infinite;
+}
+
+@keyframes fudge {
+ from {
+ transform: translate(0px, 0);
+ }
+ to {
+ transform: translate(0px, 5%);
+ }
+}
+
+
+
+.sparkler-light {
+ width: 9rem;
+ height: 100%;
+ position: relative;
+ top: -10vh;
+}
+
+.spark {
+ position: absolute;
+ width: 0.1rem;
+ height: 0.1rem;
+ bottom: 4.3rem;
+ left: 4.35rem;
+ transform: rotate(var(--spark-rotate));
+}
+
+.spark::after {
+ content: '';
+ display: block;
+ position: absolute;
+ bottom: 0;
+ width: 0.1rem;
+ height: 2rem;
+ border-radius: 0.1rem;
+ opacity: 0;
+ background-color: white;
+ transform-origin: bottom center;
+ animation: sparkler-sparkle var(--spark-duration) infinite linear;
+ animation-delay: var(--spark-delay);
+}
+
+@keyframes sparkler-light-pulsating {
+ 0% {
+ transform: scale(1);
+ }
+ 25% {
+ transform: scale(1.1);
+ }
+ 75% {
+ transform: scale(0.9);
+ }
+ 100% {
+ transform: scale(1);
+ }
+}
+
+@keyframes sparkler-sparkle {
+ 0% {
+ transform: translateY(-0.5vh) scaleY(0.25);
+ opacity: 0;
+ }
+ 10% {
+ transform: translateY(-3vh) scaleY(0.5);
+ opacity: 0.35;
+ }
+ 30% {
+ transform: translateY(-6vh) scaleY(0.5);
+ opacity: 0.7;
+ }
+ 50% {
+ transform: translateY(-10vh) scaleY(1.5);
+ opacity: 0.7;
+ }
+ 51% {
+ opacity: 0;
+ transform: translateY(-4vh) scaleY(1);
+ }
+ 100% {
+ opacity: 0;
+ transform: translateY(0) scaleY(0.25);
+ }
+}
diff --git a/assets/presenter.js b/assets/presenter.js
new file mode 100644
index 0000000..bfe41b8
--- /dev/null
+++ b/assets/presenter.js
@@ -0,0 +1,70 @@
+const overlayEl = document.getElementById("solve-overlay");
+const firstBloodEl = document.getElementById("solve-first");
+const teamEl = document.getElementById("solve-team");
+const challengeEl = document.getElementById("solve-challenge");
+const valueEl = document.getElementById("solve-value");
+const scoreboardFrameEl = document.getElementById("scoreboard-frame");
+
+const solveQueue = [];
+let isShowingOverlay = false;
+
+function pollNextSolve() {
+ if (isShowingOverlay) {
+ return;
+ }
+
+ if (solveQueue.length > 0) {
+ const nextSolve = solveQueue.shift();
+ showSolve(nextSolve.team, nextSolve.challenge, nextSolve.value, nextSolve.first);
+ }
+}
+
+function showSolve(team, challenge, value, first) {
+ teamEl.textContent = team;
+ teamEl.setAttribute("data-text", team);
+ challengeEl.textContent = challenge;
+ challengeEl.setAttribute("data-text", challenge);
+ valueEl.textContent = `+${value}`;
+ valueEl.setAttribute("data-text", `+${value}`);
+
+ if (first) {
+ firstBloodEl.classList.add("visible");
+ }
+
+ overlayEl.classList.add("visible");
+ isShowingOverlay = true;
+
+ setTimeout(() => {
+ overlayEl.classList.remove("visible");
+ firstBloodEl.classList.remove("visible");
+
+ setTimeout(() => {
+ isShowingOverlay = false;
+ pollNextSolve();
+ }, 1000);
+ }, first ? 8500 : 4500);
+}
+
+let reloadTimeout = null;
+
+function reloadScoreboard(delay = 1000) {
+ if (reloadTimeout) {
+ clearTimeout(reloadTimeout);
+ }
+
+ reloadTimeout = setTimeout(() => {
+ scoreboardFrameEl.src = "/scoreboard?ts=" + Date.now();
+ reloadTimeout = null;
+ }, delay);
+}
+
+const es = new EventSource("/presenter/events");
+
+es.onmessage = (e) => {
+ const data = JSON.parse(e.data);
+ if (data.type === "solve") {
+ solveQueue.push(data);
+ reloadScoreboard();
+ pollNextSolve();
+ }
+};