diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..76c257b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +i_* diff --git a/index.html b/index.html new file mode 100644 index 0000000..884b3e4 --- /dev/null +++ b/index.html @@ -0,0 +1,133 @@ + + + + + + + + + + + Lilac Timer + + +
+
+ + + +
+
+ +
+

+ Leaderboard: +

+ +
+
+
+

+ 1 +

+
+
+

+ Usernamethatisverylong +

+

+ Usernamethatisverylong +

+
+
+

+ 0000 +

+
+
+

+ 2 +

+
+
+

+ Usernamethatisverylong +

+

+ Usernamethatisverylong +

+
+
+

+ 25 +

+
+
+

+ 3 +

+
+
+

+ Usernamethatisverylong +

+

+ Usernamethatisverylong +

+
+
+

+ 1 +

+
+
+
+
+ + + +
+

+ 00:00 +

+

+ !subathon for details +

+
+ +
+

+ Next Goal: +

+
+
+
+

+ Discord Watch Party + Puzzles +

+ +

+ Discord Watch Party + Puzzles +

+ +
+
+
+
+
+

+ 1234/5678 +

+
+

+ 1234/5678 +

+
+
+ +
+
+
+
+ + \ No newline at end of file diff --git a/sample-payload.json b/sample-payload.json new file mode 100644 index 0000000..221a205 --- /dev/null +++ b/sample-payload.json @@ -0,0 +1,144 @@ +{ + "type": "DO", + "method": "setTimer", + "args": { + "name": "Subathon", + "end_at": "2025-10-30T19:34:05.867637+00:00", + "is_running": false, + "score_table": { + "bit_score": 0.1, + "t1_score": 5.0, + "t2_score": 10.0, + "t3_score": 20.0, + "score_time": 10 + }, + "total_contribution": 120.0, + "total_subpoints": 24, + "total_bits": 0, + "goals_met": 4, + "goals_total": 16, + "last_goal": { + "required": 100.0, + "name": "reveal new sub and bit badges" + }, + "next_goal": { + "required": 150.0, + "name": "discord watch party + puzzles" + }, + "goals": [ + { + "required": 25.0, + "name": "merch reveal! (drops on nov 6 anniversary)" + }, + { + "required": 50.0, + "name": "chat picks my pfp for a week" + }, + { + "required": 75.0, + "name": "make subathon magma board" + }, + { + "required": 100.0, + "name": "reveal new sub and bit badges" + }, + { + "required": 150.0, + "name": "discord watch party + puzzles" + }, + { + "required": 200.0, + "name": "new emotes reveal (static)" + }, + { + "required": 300.0, + "name": "karaoke time" + }, + { + "required": 400.0, + "name": "voicemod on for an hour" + }, + { + "required": 500.0, + "name": "new emotes reveal (animated)" + }, + { + "required": 750.0, + "name": "chat picks a skeb to get" + }, + { + "required": 1000.0, + "name": "chat picks new emote i will draw" + }, + { + "required": 1250.0, + "name": "play peak with mods" + }, + { + "required": 1500.0, + "name": "read voicelines/copypastas/memes that chat picks" + }, + { + "required": 1750.0, + "name": "add bald merch to merch drop" + }, + { + "required": 2000.0, + "name": "play horror game on stream (chat picks)" + }, + { + "required": 3000.0, + "name": "upload a song cover to youtube" + } + ], + "last_contribution": { + "user_name": null, + "user_id": null, + "amount": 25.0, + "seconds_added": 250, + "timestamp": "2025-10-30T22:43:53.786952+10:00" + }, + "leaderboard": [ + { + "user_name": "Unknown", + "user_id": 4, + "amount": 25.0 + }, + { + "user_name": "Maza1862", + "user_id": 5, + "amount": 25.0 + }, + { + "user_name": "Unknown", + "user_id": 6, + "amount": 25.0 + }, + { + "user_name": "lalzan", + "user_id": 9, + "amount": 25.0 + }, + { + "user_name": "DesmondGTX1", + "user_id": 10, + "amount": 5.0 + }, + { + "user_name": "SokuReload2", + "user_id": 11, + "amount": 5.0 + }, + { + "user_name": "Unknown", + "user_id": 12, + "amount": 5.0 + }, + { + "user_name": "Sum1wiserthanu", + "user_id": 13, + "amount": 5.0 + } + ] + } +} diff --git a/timer.css b/timer.css new file mode 100644 index 0000000..0ab19d5 --- /dev/null +++ b/timer.css @@ -0,0 +1,334 @@ +:root { + --time-color: #453960; + --details-color: #403060; + --background-color: transparent; +} + +* { + box-sizing: border-box; +} + +html, body { + height: 100%; + margin: 0; + padding: 0; + font-family: 'Inter', sans-serif; + background-color: var(--background-color); + background-color: transparent; + overflow: hidden; +} + +main { + font-family: "Silkscreen", sans-serif; + font-weight: 400; + font-style: normal; + color: #EA4045; + height: 100%; +} + +h1 { + margin: 0; + font-size: 3.6em; + color: #404145; + font-weight: normal; +} + +h2 { + margin: 0; + font-size: 1.4em; + color: #404145; + font-weight: normal; +} + +h3 { + margin: 0; + font-size: 1em; + color: #404145; + font-weight: normal; +} + +p { + margin: 0; + font-size: 1.3em; +} + +.contentContainer { + height: fit-content; + width: fit-content; + position: relative; + text-align: center; + } + +.mainHeart { + max-height: 100vh; + max-width: 72vw; + } + + #SecondHeart { + position: absolute; + top: 0px; + left: 0px; + } + +.infoWrap { + container-type: size; + height: 100%; + width: 100%; + position: absolute; + left: 0px; + top: 0px; + display: grid; + grid-template-columns: 56% 34% 1fr; + grid-template-rows: 11% 1fr 29%; + place-items: center; + text-wrap: nowrap; +} + +.infoGroup { + height: 100%; + width: 100%; + grid-column-start: 2; + grid-column-end: 2; + grid-row-start: 2; + grid-row-end: 2; +} + +.infoBox { + height: auto; + width: 100%; + font-size: 4cqh; +} + +#InfoBox1 { + transform: translate(-2%, 0%); + margin-top: -50%; +} + +#InfoBox2 { + /* -- !!DO NOT REMOVE!! -- + Removing this transform kills the goal label and I do not know why + */ + transform: translate(0%, 0%); +} + +#InfoBox3 { + width: 142%; + position: relative; + transform: translate(65%, 10%) scale(1, 1); + border-radius: 3cqh; + border: .4cqh solid; + border-color: #00A0F3; + background-color: white; + padding: .4ch 0 .4ch 0; +} + +#Timer { + /* + margin-top: 10%; + margin-bottom: 2%; + */ + transform: translate(0%, 0%) scale(.95, 1.2); +} + +#TimerInfo { + transform: translate(0%, -60%); + display: none; +} + +#GoalLabel { + color: #404145; +} + +#GoalName { + width: fit-content; + position: relative; + color: #32B993; + text-wrap: nowrap; + display: inline-block; + padding-right: 4%; + transform: translate(.5ch, 0%); + /* --- animation-duration = JS VAR --- */ +} + +#GoalNameDupe { + width: fit-content; + position: relative; + color: #32B993; + text-wrap: nowrap; + padding-right: 4%; + transform: translate(.5ch, 0%); + /* --- animation-duration = JS VAR --- */ +} + +#GoalProgressWrap { + width: fit-content; + padding-left: .8cqh; + padding-right: .8cqh; + border-radius: 3cqh; + border: .4cqh solid; + border-color: #404145; + margin: .4ch auto 0 auto; + overflow: hidden; +} + +#GoalProgressBar { + width: 110%; + background-color: #FC98B3; + transform: translate(-5%, 0%) scale(1, 1.5); +} + +#GoalProgress { + transform: translate(0%, -8%) scale(1); + text-align: center; + margin-top: -19%; + color: #404145; +} + +#TopUsers { + margin-bottom: 2%; + font-weight: normal; + color: #404145; +} + +.giftUserWrap { + transform: translate(9%, -8%) scale(1, 1); +} + +.giftUserRow { + width: 100%; + display: grid; + grid-template-columns: 2ch 14ch 5ch; + grid-template-rows: 1fr; + text-wrap: nowrap; + overflow: hidden; + text-align: center; +} + +.giftUserNum { + border-right: 1.5px solid #00A0F3; + border-image: linear-gradient(to top, + transparent 0%, + transparent 5%, + #00A0F3 35%, + #00A0F3 65%, + transparent 95%) 1; +} + +.giftUserScore { + border-left: 1.5px solid #00A0F3; + border-image: linear-gradient(to top, + transparent 0%, + transparent 5%, + #00A0F3 40%, + #00A0F3 60%, + transparent 95%) 1; + text-align: right; + padding-right: .25ch; +} + +.giftUserName { + width: fit-content; + margin-left: 0%; + position: relative; + color: #32B993; + text-align: left; + display: inline-block; + padding-right: 4%; + transform: translate(.5ch, 0%); +} + +.giftUserNameDupe { + width: fit-content; + margin-left: 0%; + position: relative; + color: #32B993; + text-align: left; + padding-right: 4%; + transform: translate(.5ch, 0%); +} + +#GiftUserName1 { + /* --- animation-duration = JS VAR --- */ +} + +#GiftUserName2 { + /* --- animation-duration = JS VAR --- */ +} + +#GiftUserName3 { + /* --- animation-duration = JS VAR --- */ +} + +#GiftUserNameDupe1 { + /* --- animation-duration = JS VAR --- */ +} + +#GiftUserNameDupe2 { + /* --- animation-duration = JS VAR --- */ +} + +#GiftUserNameDupe3 { + /* --- animation-duration = JS VAR --- */ +} + +.scrollWrap { + width: 100%; +} + +.rightFadeWrap { + /* + mask-image: linear-gradient(to right, black 75%, transparent 90%); + */ +} + +#RightFadeWrapInfoBox2 { + width: 90%; + border-left: 1.5px solid #00A0F3; + border-right: 1.5px solid #00A0F3; + border-image: linear-gradient(to top, + transparent 0%, + transparent 5%, + #00A0F3 40%, + #00A0F3 60%, + transparent 95%) 1; + transform: translate(1ch, 0%) scale(1); +} + +.fadeLeft { + mask-image: linear-gradient(to right, + transparent 0%, + black 1ch, + black calc(100% - 2ch), + transparent 100%); +} + +#ScrollWrapGoal { + /* + width: 90%; + */ +} + +.scrollWrapName { +} + +.scrollWrapW { + width: fit-content; + display: flex; +} + +#ScrollWrapGoalW { +} + +.hide { + display: none; +} + +.scrollAnim { + animation: myScroll linear infinite; +} + +@keyframes myScroll { + 0% {left: 0%; top: 0%;} + 42% {left: 0%; top: 0%;} + 100% {left: -54%; top: 0%;} +} \ No newline at end of file diff --git a/timer.js b/timer.js new file mode 100644 index 0000000..ef76dfb --- /dev/null +++ b/timer.js @@ -0,0 +1,500 @@ +window.addEventListener("DOMContentLoaded", () => { + const websocket = new ReconnectingWebSocket("wss://lilac.thewisewolf.dev/tracker/timer/ws"); + communicate(websocket); +}); + + +class Timer { + constructor(renderer, timer_data) { + this.renderer = renderer; + this.destroying = false; + this.ended = false; + + this.update_from_data(timer_data); + } + + update_from_data(timer_data) { + this.end_at = new Date(timer_data.end_at); + this.running = timer_data.is_running; + this.name = timer_data.name; + this.score_table = timer_data.score_table; + + this.total_contribution = timer_data.total_contribution; + this.total_subpoints = timer_data.total_subpoints; + this.total_bits = timer_data.total_bits; + + this.goals_met = timer_data.goals_met; + this.goals_total = timer_data.goals_total; + this.last_goal = timer_data.last_goal; + this.next_goal = timer_data.next_goal; + this.goals = timer_data.goals; + + this.last_contribution = timer_data.last_contribution; + this.leaderboard = timer_data.leaderboard; + } + + check_ended() { + if (!this.ended && this.end_at < new Date()) { + this.ended = true; + this.renderer.finale(); + } + return this.ended; + } + + set_ended() { + if (!this.ended && this.end_at < new Date()) { + this.ended = true; + this.renderer.finale(); + } + } + + render_time() { + if (this.check_ended()) { + return; + } + this.renderer.render_time( + Math.floor((this.end_at - new Date()) / 1000) + ); + } + + render() { + if (this.ended) { + return; + } + // Render goal + if (this.next_goal != null) { + this.renderer.render_current_goal( + this.next_goal.name, + this.total_contribution, + this.next_goal.required, + ); + this.renderer.animate(); + } else if (this.last_goal != null) { + this.renderer.render_current_goal( + this.last_goal.name, + this.total_contribution, + this.last_goal.required, + ); + this.renderer.animate(); + } else { + this.renderer.clear_current_goal(); + } + + // Render leaderboard + this.renderer.render_users(this.leaderboard); + this.renderer.animate(); + + // Render timer + this.render_time(); + + this.set_ended(); + } + + /** + * Recursive call run per second to update the rendered timer + */ + tick() { + if (!this.destroying) { + this.render_time(); + + if (this.running) { + setTimeout(this.tick.bind(this), 1000); + } + } + } +} + +class TimerRenderer { + constructor() { + // HH:MM or MM:SS + this.timer = document.getElementById("Timer"); + + // Current Goal: + this.goal_label = document.getElementById("GoalLabel") + + // Name of the goal to display + this.goal_name = document.getElementById("GoalName") + this.goal_namedupe = document.getElementById("GoalNameDupe") + + // 1524/3000 Points + this.goal_progress = document.getElementById("GoalProgress") + this.goal_bar = document.getElementById("GoalProgressBar") + + // Top 3 Gifters + this.topusers_label = document.getElementById("TopUsers") + + // Leaderboard items + // 1. (or points?) (1 as in 1st place, static) + this.topusers_user1_num = document.getElementById("GiftUserNum1") + // Name of user 1 + this.topusers_user1_name = document.getElementById("GiftUserName1") + this.topusers_userdupe1_name = document.getElementById("GiftUserNameDupe1") + this.topusers_user1_score = document.getElementById("GiftUserScore1") + this.topusers_user2_num = document.getElementById("GiftUserNum2") + this.topusers_user2_name = document.getElementById("GiftUserName2") + this.topusers_userdupe2_name = document.getElementById("GiftUserNameDupe2") + this.topusers_user2_score = document.getElementById("GiftUserScore2") + this.topusers_user3_num = document.getElementById("GiftUserNum3") + this.topusers_user3_name = document.getElementById("GiftUserName3") + this.topusers_userdupe3_name = document.getElementById("GiftUserNameDupe3") + this.topusers_user3_score = document.getElementById("GiftUserScore3") + + // -- Animation -- + // Left-fade wrappers + this.scrollwraps = document.getElementsByClassName("scrollWrap") + // Parent wrappers + this.scrollboxes = [document.getElementById("GiftUserRow1"), + document.getElementById("GiftUserRow2"), + document.getElementById("GiftUserRow3"), + document.getElementById("ScrollWrapGoal")] + // Leaderboard names + this.userboxes = [this.topusers_user1_name, this.topusers_user2_name, this.topusers_user3_name] + this.userdupeboxes = [this.topusers_userdupe1_name, this.topusers_userdupe2_name, this.topusers_userdupe3_name] + + // Thank you! + this.thankyou = "Thank you! The subathon has now ended, thank you so much for contributions and viewership to celebrate!" + } + + // -- Toggles animation if text overflows container and adjusts speed for uniform movement -- + animate() { + + for (let i = 0; i < this.scrollboxes.length; i++) { + // Check whether the itteration is a username or goal + if (this.scrollboxes[i].id == 'ScrollWrapGoal') { + + // Check if element text overflows + if (this.goal_name.getBoundingClientRect().width > (this.scrollboxes[i].getBoundingClientRect().width)) { + // Toggle animation + this.goal_name.classList.add("scrollAnim"); + this.goal_namedupe.classList.add("scrollAnim"); + this.goal_namedupe.style.display = 'inline-block'; + this.scrollwraps[i].classList.add("fadeLeft"); + // Set animation speed + this.goal_name.style.animationDuration = `${this.goal_name.getBoundingClientRect().width / 40}s`; + this.goal_namedupe.style.animationDuration = `${this.goal_name.getBoundingClientRect().width / 40}s`; + } else { + // Toggle animation + this.goal_name.classList.remove("scrollAnim"); + this.goal_namedupe.classList.remove("scrollAnim"); + this.goal_namedupe.style.display = 'none'; + this.scrollwraps[i].classList.remove("fadeLeft"); + } + } else { + + // Check if element text overflows + if (this.userboxes[i].getBoundingClientRect().width > (this.scrollboxes[i].getBoundingClientRect().width * .65)) { + // Toggle animation + this.userboxes[i].classList.add("scrollAnim"); + this.userdupeboxes[i].classList.add("scrollAnim"); + this.userdupeboxes[i].style.display = 'inline-block'; + this.scrollwraps[i].classList.add("fadeLeft"); + // Set animation speed + this.userboxes[i].style.animationDuration = `${this.userboxes[i].getBoundingClientRect().width / 40}s`; + this.userdupeboxes[i].style.animationDuration = `${this.userboxes[i].getBoundingClientRect().width / 40}s`; + } else { + // Toggle animation + this.userboxes[i].classList.remove("scrollAnim"); + this.userdupeboxes[i].classList.remove("scrollAnim"); + this.userdupeboxes[i].style.display = 'none'; + this.scrollwraps[i].classList.remove("fadeLeft"); + } + } + } + } + + /** + * Bring the display into a standard initial state. + */ + reset() { + this.timer.textContent = "88:88"; + this.goal_label.textContent = "Next Goal:"; + this.goal_name.textContent = "Goal name for the subathon"; + this.goal_namedupe.textContent = "Goal name for the subathon"; + this.goal_progress.textContent = "1234/1234"; + + this.topusers_label.textContent = "Leaderboard:"; + this.topusers_user1_num.textContent = "1"; + this.topusers_user1_name.textContent = "Username1024"; + this.topusers_userdupe1_name.textContent = "Username1024"; + this.topusers_user1_score.textContent = "0000"; + this.topusers_user2_num.textContent = "2"; + this.topusers_user2_name.textContent = "User"; + this.topusers_userdupe2_name.textContent = "User"; + this.topusers_user2_score.textContent = "0000"; + this.topusers_user3_num.textContent = "3"; + this.topusers_user3_name.textContent = "Username_Very_Super_Long12"; + this.topusers_userdupe3_name.textContent = "Username_Very_Super_Long12"; + this.topusers_user3_score.textContent = "0000"; + } + + // End subathon, change timer color, and show thank you message + finale() { + this.timer.textContent = "00:00"; + this.timer.style.color = '#82C8B6'; + this.goal_label.textContent = "Completed!"; + this.goal_name.textContent = this.thankyou; + this.goal_namedupe.textContent = this.thankyou; + this.animate(); + } + + /** + * Render the timer from a time remaining. + * + * @param {integer} seconds_remaining - The number of seconds remaining on the timer. + */ + render_time(seconds_remaining) { + var timestr; + // Render a time remaining given in seconds + if (seconds_remaining <= 0) { + timestr = "00:00"; + } else { + var seconds = seconds_remaining % 60; + seconds_remaining = seconds_remaining - seconds; + var minutes = Math.floor(seconds_remaining / 60); + var hours = Math.floor(minutes / 60); + minutes = minutes - 60 * hours; + + // Change rendering mode based on hours or minutes left + if (hours > 0) { + timestr = String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0'); + } else { + timestr = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0'); + } + } + this.timer.textContent = timestr; + } + + /** + * Render the current goal. + * + * @param {string} name - The name of the current goal. + * @param {number} current - The number of points we currently have. + * @param {number} required - The number of points to achieve the goal. + */ + render_current_goal(name, current, required) { + // TODO: Some kind of end condition, ability to clear a goal etc + // E.g. if subathon doesn't have a goal at all + // Or after we accomplish all goals + + this.goal_name.textContent = name; + this.goal_namedupe.textContent = name; + //this.goal_name.style.animationDuration = `${this.goal_name.getBoundingClientRect().width / 50}s`; + + // Bitwise OR done to convert floats to integers if needed + this.goal_progress.textContent = String(current | 0) + '/' + String(required | 0) + + // Update the progress bar + if (current / required > .02) { + this.goal_bar.style.maskImage = `linear-gradient(to right, #FC98B3 0%, #FC98B3 ${((current / required) - .02) * 100}%, transparent ${(current / required) * 100}%)`; + } else { + this.goal_bar.style.maskImage = `linear-gradient(to right, #FC98B3 0%, transparent .01%)`; + } + } + + /** + * Clear the current goal field. + */ + clear_current_goal() { + this.goal_name.textContent = ""; + //this.goal_namedupe.textContent = ""; + this.goal_progress.textContent = ""; + this.goal_label.textContent = ""; + } + + /** + * Render the user leaderboard. + * + * @param {Object[]} users - The user leaderboard in descending (points, name) order. + * @param {integer} users[].amount - The number of points this users has contrib. + * @param {string} users[].user_name - The name of this user. + */ + render_users(users) { + + if (users.length >= 1) { + this.topusers_user1_num.textContent = "1"; + this.topusers_user1_name.textContent = users[0].user_name; + this.topusers_userdupe1_name.textContent = users[0].user_name; + this.topusers_user1_score.textContent = Math.round(users[0].amount); + //this.topusers_user1_name.style.animationDuration = `${this.topusers_user1_name.getBoundingClientRect().width / 50}s`; + } else { + this.topusers_user1_num.textContent = "1"; + this.topusers_user1_name.textContent = ""; + this.topusers_userdupe1_name.textContent = ""; + this.topusers_user1_score.textContent = "0000"; + } + + if (users.length >= 2) { + this.topusers_user2_num.textContent = "2"; + this.topusers_user2_name.textContent = users[1].user_name; + this.topusers_userdupe2_name.textContent = users[1].user_name; + this.topusers_user2_score.textContent = Math.round(users[1].amount); + //this.topusers_user2_name.style.animationDuration = `${this.topusers_user2_name.getBoundingClientRect().width / 50}s`; + } else { + this.topusers_user2_num.textContent = "2"; + this.topusers_user2_name.textContent = ""; + this.topusers_userdupe2_name.textContent = ""; + this.topusers_user2_score.textContent = "0000"; + } + + if (users.length >= 3) { + this.topusers_user3_num.textContent = "3"; + this.topusers_user3_name.textContent = users[2].user_name; + this.topusers_userdupe3_name.textContent = users[2].user_name; + this.topusers_user3_score.textContent = Math.round(users[2].amount); + //this.topusers_user3_name.style.animationDuration = `${this.topusers_user3_name.getBoundingClientRect().width / 50}s`; + } else { + this.topusers_user3_num.textContent = "3"; + this.topusers_user3_name.textContent = ""; + this.topusers_userdupe3_name.textContent = ""; + this.topusers_user3_score.textContent = "0000"; + } + + } + +} + +class ReconnectingWebSocket { + constructor(url) { + this.url = url; + this.ws = null; + this.attempt = 0; + this.connect(); + + this.closing = false; + this.onopen = null; + this.onmessage = null; + this.onclose = null; + this.onerror = null; + } + + connect() { + this.ws = new WebSocket(this.url); + + this.ws.onopen = () => { + console.log('WebSocket connected'); + this.attempt = 0; + if (this.onopen != null) { + this.onopen(); + } + }; + + this.ws.onmessage = (msg) => { + console.log('Received:', msg.data); + if (this.onmessage != null) { + this.onmessage(msg); + } + }; + + this.ws.onclose = () => { + if (this.closing) { + console.log('Closing Websocket.') + } { + console.warn('WebSocket closed. Reconnecting...'); + this.reconnect(); + } + }; + + this.ws.onerror = (err) => { + console.error('WebSocket error:', err); + if (this.onerror != null) { + this.onerror(); + } + this.close(); + }; + } + + reconnect() { + const delay = this.getBackoffDelay(this.attempt); + console.log(`Reconnecting in ${delay}ms`); + + setTimeout(() => { + this.attempt++; + this.connect(); + }, delay); + } + + close() { + this.closing = true; + this.ws.close(); + if (this.onclose != null) { + this.onclose(); + } + } + + getBackoffDelay(attempt) { + const base = 500; // 0.5 second + const max = 30000; // 30 seconds + const jitter = Math.random() * 1000; + return Math.min(base * 2 ** attempt + jitter, max); + } + + send(data) { + if (this.ws.readyState === WebSocket.OPEN) { + this.ws.send(data); + } else { + console.warn('WebSocket not connected'); + } + } +} + + + + +function communicate(websocket) { + console.log("Communicating"); + const params = new URLSearchParams(window.location.search); + var renderer = new TimerRenderer(); + let timer; + + websocket.onopen = () => { + websocket.send( + JSON.stringify( + { + type: "init", + channel: "SubTimer", + community: params.get('community') + } + ) + ); + renderer.reset(); + renderer.animate(); + }; + + websocket.onmessage = ({ data }) => { + const event = JSON.parse(data); + switch (event.type) { + case "DO": + let args = event.args; + + // Call the specified method + switch (event.method) { + case "setTimer": + if (timer == null) { + timer = new Timer(renderer, args); + } else { + timer.update_from_data(args); + } + timer.render(); + timer.tick(); + break; + case "noTimer": + if (timer != null) { + timer.destroying = true; + timer = null; + } + renderer.reset(); + renderer.animate(); + case "endTimer": + if (timer != null) { + timer.destroying = true; + timer = null; + } + default: + throw new Error(`Unsupported method requested: ${event.method}.`) + } + break; + default: + throw new Error(`Unsupported event type: ${event.type}.`); + } + }; +}