Initial timer commit

This commit is contained in:
2026-03-13 23:27:09 +10:00
commit 3979a0f68e
7 changed files with 389 additions and 0 deletions

BIN
assets/mute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
assets/unmute.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

BIN
audio/break_alert.wav Normal file

Binary file not shown.

BIN
audio/focus_alert.wav Normal file

Binary file not shown.

26
index.html Normal file
View File

@@ -0,0 +1,26 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<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>
</head>
<body style="background-color: #1A1A1A;">
<div class="timer-box">
<div class="timer-bg">
<div class="timer-time">
<span class="timer-time">00:00</span>
</div>
<div class="timer-details">
<span class="timer-details-stage">
<span class="timer-details-stage-text">BREAK</span>
</span>
<span class="timer-details-block">
10/12
</span>
</div>
</div>
</body>
</html>

85
timer.css Normal file
View File

@@ -0,0 +1,85 @@
:root {
--break-color: #8CD5CA;
--focus-color: #EED59F;
--stage-color: var(--break-color);
--clip-path: none;
}
.timer-box{
height: 175px;
width: 397px;
padding: 5px;
}
.timer-bg {
height: 100%;
width: 100%;
height: 175px;
width: 397px;
position: absolute;
background-color: #1E202F;
border-radius: 40px;
outline-offset: -5px;
outline: dotted 10px #534F72;
}
.timer-bg:before {
content: "";
position: absolute;
height: 175px;
width: 397px;
border-radius: 40px;
outline-offset: -5px;
outline: dotted 10px var(--stage-color);
border-radius: 40px;
clip-path: var(--clip-path);
}
.timer-time {
font-size: 100px;
font-family: Inter;
font-weight: 400;
letter-spacing: 0%;
text-align: center;
color: var(--break-color);
}
.timer-details {
display: flex;
justify-content: space-between;
align-items: center;
text-align: center;
width: 65%;
margin: auto;
margin-top: -2%;
color: #FFFFFF;
}
.timer-details-stage img {
height: 30px;
margin-right: 5%;
}
.timer-details-stage {
display: flex;
justify-content: center;
align-items: center;
text-align: center;
}
.timer-details-stage-text {
font-size: 30px;
font-family: Inter;
font-weight: 400;
letter-spacing: 6px;
text-align: center;
}
.timer-details-block {
font-size: 30px;
font-family: Inter;
font-weight: 400;
letter-spacing: 0%;
text-align: center;
}

278
timer.js Normal file
View File

@@ -0,0 +1,278 @@
window.addEventListener("DOMContentLoaded", () => {
const websocket = new WebSocket("ws://adamwalsh.name:9500");
websocket.addEventListener("open", () => {
communicate(websocket);
});
});
class Timer {
constructor(renderer, start_at, focus_length, break_length, block_goal) {
this.renderer = renderer;
this.start_at = start_at;
this.focus_length = focus_length;
this.break_length = break_length;
this.block_goal = block_goal;
this.counter = this.count_now;
this.break_notified = true;
this.destroying = false;
}
get block_length() {
// Return total block duration in seconds
return this.focus_length + this.break_length;
}
get block_start() {
// Get the Date when this block started.
return new Date(this.start_at.getTime() + (this.counter * this.block_length) * 1000);
}
get count_now() {
// Current actual block number. 0 for the first block, -1 if not running
var diff = Math.floor((new Date() - this.start_at) / 1000);
if (diff < 0) {
return -1;
} else {
var stage = Math.floor(diff / this.block_length);
return stage;
}
}
get is_break() {
// Whether the current block is on break.
if (this.counter < 0) {
return true
} else {
return ((new Date() - this.block_start) / 1000) > this.focus_length;
}
}
render_time() {
// Render timer status to the document
let timestr;
let proportion;
if (this.counter < 0) {
timestr = "--:--";
proportion = 0;
} else {
// How many seconds have passed in this block
var dur_seconds = Math.floor((new Date() - this.block_start) / 1000);
if (dur_seconds < this.focus_length) {
// How many seconds are left in the focus session
dur_seconds = this.focus_length - dur_seconds;
// What proportion of the focus session remains
proportion = dur_seconds / this.focus_length;
} else {
dur_seconds = this.block_length - dur_seconds;
proportion = dur_seconds / (this.break_length);
}
var seconds = dur_seconds % 60;
dur_seconds = dur_seconds - seconds;
var minutes = Math.floor(dur_seconds / 60);
var hours = Math.floor(minutes / 60);
minutes = minutes - 60 * hours;
timestr = String(hours).padStart(2, '0') + ':' + String(minutes).padStart(2, '0');
}
this.renderer.update_time(timestr);
this.renderer.update_proportion(proportion);
this.renderer.update_block(String(this.counter + 1) + '/' + String(this.block_goal));
if (this.is_break) {
this.renderer.update_stage("BREAK");
this.renderer.set_break_color();
} else {
this.renderer.update_stage("FOCUS");
this.renderer.set_focus_color();
}
}
tick() {
// Recursive call run per second to update and change stage
if (this.destroying) {
return
}
if (this.is_break && !this.break_notified && this.counter >= 0) {
// Notify for break
this.renderer.break_alert.play();
console.log("Notifying Break.")
this.break_notified = true;
} else if (this.count_now > this.counter) {
this.counter++;
this.break_notified = false;
// Notify for focus
this.renderer.focus_alert.play();
console.log("Notifying Focus.")
}
this.render_time();
setTimeout(this.tick.bind(this), 1000);
}
}
class TimerRenderer {
constructor() {
this.background = document.getElementsByClassName("timer-bg")[0];
this.time_element = document.getElementsByClassName("timer-time")[0];
this.stage_element = document.getElementsByClassName("timer-details-stage-text")[0];
this.block_element = document.getElementsByClassName("timer-details-block")[0];
this.break_alert = new Audio('audio/break_alert.wav');
this.focus_alert = new Audio('audio/focus_alert.wav');
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;
}
update_proportion(proportion) {
var bounds = rectBounds(
proportion, this.bg_width, this.bg_height
);
this.background.style.setProperty(
'--clip-path',
"polygon(" + bounds.join(', ') + ")"
);
}
update_block(blockstr) {
this.block_element.textContent = blockstr;
}
update_stage(stagestr) {
this.stage_element.textContent = stagestr;
}
set_break_color() {
this.background.style.setProperty('--stage-color', 'var(--break-color)');
this.time_element.style.color = "var(--break-color)";
}
set_focus_color() {
this.background.style.setProperty('--stage-color', 'var(--focus-color)');
this.time_element.style.color = "var(--focus-color)";
}
}
function rectBounds(prop, rectHeight, rectWidth) {
var totalCircum = 2 * rectHeight + 2 * rectWidth;
var propCircum = prop * totalCircum;
let bounds;
if (propCircum < rectWidth / 2) {
var xprop = Math.floor((
(rectWidth / 2 + propCircum) / rectWidth
) * 1000) / 10;
bounds = [
"50% -5%",
`${xprop}% -5%`,
`${xprop}% 4%`,
"50% 4%"
];
} else if (propCircum < rectWidth / 2 + rectHeight) {
var yprop = (
(propCircum - rectWidth / 2) / rectHeight
) * 100;
bounds = [
"50% -5%", "110% -5%",
`110% ${yprop}%`, `96% ${yprop}%`,
"95% 4%", "50% 4%"
];
} else if (propCircum < 3 * rectWidth / 2 + rectHeight) {
var xprop = (
((3 * rectWidth / 2 + rectHeight) - propCircum) / rectWidth
) * 100;
bounds = [
"50% -5%", "110% -5%",
"110% 110%",
`${xprop}% 110%`, `${xprop}% 96%`,
"95% 96%",
"95% 4%", "50% 4%"
];
} else if (propCircum < 3 * rectWidth / 2 + 2 * rectHeight) {
var yprop = (
((3 * rectWidth / 2 + 2 * rectHeight) - propCircum) / rectHeight
) * 100;
bounds = [
"50% -5%", "110% -5%",
"110% 110%", "-5% 110%",
`-5% ${yprop}%`, `4% ${yprop}%`,
"5% 96%", "95% 96%",
"95% 4%", "50% 4%"
];
} else {
var xprop = (
(propCircum - (3 * rectWidth / 2 + 2 * rectHeight)) / rectWidth
) * 100;
bounds = [
"50% -5%", "110% -5%",
"110% 110%", "-5% 110%",
"-5% -5%",
`${xprop}% -5%`, `${xprop}% 4%`,
"4% 4%",
"5% 96%", "95% 96%",
"95% 4%", "50% 4%"
];
}
return bounds;
}
function communicate(websocket) {
console.log("Communicating");
websocket.send(JSON.stringify({ type: "init", channel: "Timer" }));
var renderer = new TimerRenderer();
let timer;
websocket.addEventListener("message", ({ data }) => {
console.log("Rec Event " + 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.destroying = true;
}
timer = new Timer(
renderer,
new Date(args.start_at),
args.focus_length,
args.break_length,
args.block_goal
)
timer.tick();
break;
default:
throw new Error(`Unsupported method requested: ${event.method}.`)
}
break;
default:
throw new Error(`Unsupported event type: ${event.type}.`);
}
});
}