Compare commits
22 Commits
master
...
v2-Prototy
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a09ea3d80 | |||
| ff7c48dab4 | |||
| 6c2b897522 | |||
| 192f3c0eb1 | |||
| 8e95f378d7 | |||
| f2fc02ff7e | |||
| 75cdfcf09a | |||
| 11ca4470fe | |||
| 2d9e320d42 | |||
| da6ae0b9c3 | |||
| 960c037362 | |||
| 1f49e82035 | |||
| 31bc26c72f | |||
| 45502c00e4 | |||
| c79f969059 | |||
| 9544579ebe | |||
| f8bce7a50c | |||
| b1fb0299d2 | |||
| 70c0b3984c | |||
| 47218deb87 | |||
| df6ce48681 | |||
| 90c8774011 |
122
index.html
122
index.html
@@ -2,16 +2,132 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Silkscreen:wght@400;700&family=VT323&display=swap" rel="stylesheet">
|
||||
<link href='https://fonts.googleapis.com/css?family=Inter' rel='stylesheet'>
|
||||
<link rel="stylesheet" href="timer.css" />
|
||||
<script src="timer.js"></script>
|
||||
<title>Croccy Timer</title>
|
||||
<title>Lilac Timer</title>
|
||||
</head>
|
||||
<body>
|
||||
<div class="timer-box">
|
||||
<div class="timer-time">
|
||||
<main>
|
||||
<div class='contentContainer'>
|
||||
|
||||
<img class='mainHeart' src='src/youvegotmail.webp'>
|
||||
|
||||
<div class='infoWrap'>
|
||||
<div class='infoGroup'>
|
||||
|
||||
<div class='infoBox' id='InfoBox3'>
|
||||
<p id='TopUsers'>
|
||||
Leaderboard:
|
||||
</p>
|
||||
|
||||
<div class='rightFadeWrap' id='RightFadeWrapInfoBox3'>
|
||||
<div class='giftUserWrap' id='GiftUserWrap'>
|
||||
<div class='giftUserRow' id='GiftUserRow1'>
|
||||
<p class='giftUserNum' id='GiftUserNum1'>
|
||||
1
|
||||
</p>
|
||||
<div class='scrollWrap' id='ScrollWrapName1'>
|
||||
<div class='scrollWrapW' id='ScrollWrapNameW1'>
|
||||
<p class='giftUserName'id='GiftUserName1'>
|
||||
Usernamethatisverylong
|
||||
</p>
|
||||
<p class='giftUserNameDupe'id='GiftUserNameDupe1'>
|
||||
Usernamethatisverylong
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class='giftUserScore' id='GiftUserScore1'>
|
||||
0000
|
||||
</p>
|
||||
</div>
|
||||
<div class='giftUserRow' id='GiftUserRow2'>
|
||||
<p class='giftUserNum' id='GiftUserNum2'>
|
||||
2
|
||||
</p>
|
||||
<div class='scrollWrap' id='ScrollWrapName2'>
|
||||
<div class='scrollWrapW' id='ScrollWrapNameW2'>
|
||||
<p class='giftUserName'id='GiftUserName2'>
|
||||
Usernamethatisverylong
|
||||
</p>
|
||||
<p class='giftUserNameDupe'id='GiftUserNameDupe2'>
|
||||
Usernamethatisverylong
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class='giftUserScore' id='GiftUserScore2'>
|
||||
25
|
||||
</p>
|
||||
</div>
|
||||
<div class='giftUserRow' id='GiftUserRow3'>
|
||||
<p class='giftUserNum' id='GiftUserNum3'>
|
||||
3
|
||||
</p>
|
||||
<div class='scrollWrap' id='ScrollWrapName3'>
|
||||
<div class='scrollWrapW' id='ScrollWrapNameW3'>
|
||||
<p class='giftUserName'id='GiftUserName3'>
|
||||
Usernamethatisverylong
|
||||
</p>
|
||||
<p class='giftUserNameDupe'id='GiftUserNameDupe3'>
|
||||
Usernamethatisverylong
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class='giftUserScore' id='GiftUserScore3'>
|
||||
1
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<img class='mainHeart' id='SecondHeart' src='src/youvegotmail.webp'>
|
||||
|
||||
<div class='infoBox' id='InfoBox1'>
|
||||
<h1 id='Timer'>
|
||||
00:00
|
||||
</h1>
|
||||
<p id='TimerInfo'>
|
||||
!subathon for details
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class='infoBox' id='InfoBox2'>
|
||||
<p id='GoalLabel'>
|
||||
Next Goal:
|
||||
</p>
|
||||
<div class='rightFadeWrap' id='RightFadeWrapInfoBox2'>
|
||||
<div class='scrollWrap' id='ScrollWrapGoal'>
|
||||
<div class='scrollWrapW' id='ScrollWrapGoalW'>
|
||||
<p id='GoalName'>
|
||||
Discord Watch Party + Puzzles
|
||||
</p>
|
||||
|
||||
<p id='GoalNameDupe'>
|
||||
Discord Watch Party + Puzzles
|
||||
</p>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id='GoalProgressWrap'>
|
||||
<div id='GoalProgressBar'>
|
||||
<p id='GoalProgressTransparent' style='color: transparent;'>
|
||||
1234/5678
|
||||
</p>
|
||||
</div>
|
||||
<p id='GoalProgress'>
|
||||
1234/5678
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
144
sample-payload.json
Normal file
144
sample-payload.json
Normal file
@@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
src/youve_got_mail_screenheart.png
Normal file
BIN
src/youve_got_mail_screenheart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 517 KiB |
BIN
src/youve_got_mail_whiteheart.png
Normal file
BIN
src/youve_got_mail_whiteheart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 481 KiB |
BIN
src/youvegotmail.webp
Normal file
BIN
src/youvegotmail.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 110 KiB |
332
timer.css
332
timer.css
@@ -4,33 +4,331 @@
|
||||
--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;
|
||||
}
|
||||
|
||||
|
||||
.timer-box {
|
||||
height: 175px;
|
||||
width: 397px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.timer-time {
|
||||
font-size: clamp(6rem, 22vw, 16rem);
|
||||
main {
|
||||
font-family: "Silkscreen", sans-serif;
|
||||
font-weight: 400;
|
||||
color: var(--time-color);
|
||||
line-height: 1;
|
||||
font-style: normal;
|
||||
color: #EA4045;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.timer-details {
|
||||
display: fill;
|
||||
font-weight: 600;
|
||||
color: var(--details-color);
|
||||
font-size: 16px;
|
||||
overflow-wrap: break-word;
|
||||
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%;}
|
||||
}
|
||||
496
timer.js
496
timer.js
@@ -1,36 +1,259 @@
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
const websocket = new WebSocket("wss://izashi.thewisewolf.dev/tracker/timer/ws");
|
||||
|
||||
websocket.addEventListener("open", () => {
|
||||
const websocket = new ReconnectingWebSocket("wss://lilac.thewisewolf.dev/tracker/timer/ws");
|
||||
communicate(websocket);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
||||
class Timer {
|
||||
constructor(renderer, end_time, running) {
|
||||
constructor(renderer, timer_data) {
|
||||
this.renderer = renderer;
|
||||
this.end_time = end_time;
|
||||
this.running = running;
|
||||
|
||||
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() {
|
||||
// Render timer status to the document
|
||||
var timestr;
|
||||
// How many seconds to the end of time
|
||||
var dur_seconds = Math.floor( (this.end_time - new Date()) / 1000);
|
||||
if (this.check_ended()) {
|
||||
return;
|
||||
}
|
||||
this.renderer.render_time(
|
||||
Math.floor((this.end_at - new Date()) / 1000)
|
||||
);
|
||||
}
|
||||
|
||||
if (dur_seconds < 0) {
|
||||
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 = dur_seconds % 60;
|
||||
dur_seconds = dur_seconds - seconds;
|
||||
var minutes = Math.floor(dur_seconds / 60);
|
||||
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');
|
||||
@@ -38,59 +261,206 @@ class Timer {
|
||||
timestr = String(minutes).padStart(2, '0') + ':' + String(seconds).padStart(2, '0');
|
||||
}
|
||||
}
|
||||
|
||||
this.renderer.update_time(timestr);
|
||||
this.timer.textContent = timestr;
|
||||
}
|
||||
|
||||
tick() {
|
||||
// Recursive call run per second to update and change stage
|
||||
/**
|
||||
* 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
|
||||
|
||||
if (this.destroying) {
|
||||
return
|
||||
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%)`;
|
||||
}
|
||||
this.render_time();
|
||||
if (this.running) {
|
||||
setTimeout(this.tick.bind(this), 1000);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TimerRenderer {
|
||||
constructor (){
|
||||
this.background = document.getElementsByClassName("timer-bg")[0];
|
||||
this.time_element = document.getElementsByClassName("timer-time")[0];
|
||||
|
||||
this.bg_width = parseInt(
|
||||
window.getComputedStyle(
|
||||
this.background, null
|
||||
).getPropertyValue('width'),
|
||||
10
|
||||
);
|
||||
this.bg_height = parseInt(
|
||||
window.getComputedStyle(
|
||||
this.background, null
|
||||
).getPropertyValue('height'),
|
||||
10
|
||||
)
|
||||
}
|
||||
|
||||
update_time (timestr) {
|
||||
this.time_element.textContent = timestr;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
function communicate(websocket) {
|
||||
console.log("Communicating");
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
websocket.send(JSON.stringify({type: "init", channel: "SubTimer", community: params.get('community')}));
|
||||
|
||||
var renderer = new TimerRenderer();
|
||||
let timer;
|
||||
|
||||
websocket.addEventListener("message", ({ data }) => {
|
||||
console.log("Rec Event " + data);
|
||||
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":
|
||||
@@ -99,25 +469,25 @@ function communicate(websocket) {
|
||||
// Call the specified method
|
||||
switch (event.method) {
|
||||
case "setTimer":
|
||||
if (timer != null) {
|
||||
timer.destroying = true;
|
||||
if (timer == null) {
|
||||
timer = new Timer(renderer, args);
|
||||
} else {
|
||||
timer.update_from_data(args);
|
||||
}
|
||||
timer = new Timer(
|
||||
renderer,
|
||||
new Date(args.end_at),
|
||||
args.running
|
||||
)
|
||||
timer.render();
|
||||
timer.tick();
|
||||
break;
|
||||
case "noTimer":
|
||||
if (timer != null) {
|
||||
timer.renderer.update_time("--:--")
|
||||
timer.destroying = true;
|
||||
timer = null;
|
||||
}
|
||||
renderer.reset();
|
||||
renderer.animate();
|
||||
case "endTimer":
|
||||
if (timer != null) {
|
||||
timer.renderer.update_time("00:00")
|
||||
timer.destroying = true;
|
||||
timer = null;
|
||||
}
|
||||
default:
|
||||
throw new Error(`Unsupported method requested: ${event.method}.`)
|
||||
@@ -126,5 +496,5 @@ function communicate(websocket) {
|
||||
default:
|
||||
throw new Error(`Unsupported event type: ${event.type}.`);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user