I’m doing a Pomodoro Timer using HTML+CSS+JavaScript.
I want to have a progress animation around the circle that starts at the top of the circle and rotates clockwise until completing the full circle.
I’ve already tried a lot of different ways to do the code but no matter what I do, I can’t make it to work right.
Here is my code:
<code>const bells = new Audio('./sounds/bell.wav');
const startBtn = document.querySelector('.btn-start');
const pauseBtn = document.querySelector('.btn-pause');
const resetBtn = document.querySelector('.btn-reset');
const session = document.querySelector('.minutes');
const sessionInput = document.querySelector('#session-length');
const breakInput = document.querySelector('#break-length');
let myInterval;
let state = true;
let isPaused = false
let totalSeconds;
let initialSeconds;
const updateTimerDisplay = () => {
const minuteDiv = document.querySelector('.minutes');
const secondDiv = document.querySelector('.seconds');
let minutesLeft = Math.floor(totalSeconds / 60);
let secondsLeft = totalSeconds % 60;
secondDiv.textContent = secondsLeft < 10 ? '0' + secondsLeft : secondsLeft;
minuteDiv.textContent = `${minutesLeft}`;
// Update the circle animation
const leftSide = document.querySelector('.left-side');
const rightSide = document.querySelector('.right-side');
const duration = initialSeconds; // Total duration in seconds
const elapsed = initialSeconds - totalSeconds;
const percentage = (elapsed / duration) * 100;
let rotationRight, rotationLeft;
if (percentage <= 50) {
// First half: rotate right side down from top to bottom
rotationRight = (percentage / 50) * 180 - 90;
rotationLeft = -90; // Keep left side at the top
} else {
// Second half: rotate left side up from bottom to top
rotationRight = 90; // Keep right side at the bottom
rotationLeft = ((percentage - 50) / 50) * 180 + 90;
}
rightSide.style.transform = `rotate(${rotationRight}deg)`;
leftSide.style.transform = `rotate(${rotationLeft}deg)`;
};
const appTimer = () => {
const sessionAmount = Number.parseInt(sessionInput.value)
if (isNaN(sessionAmount) || sessionAmount <= 0) {
alert('Please enter a valid session duration.');
return;
}
session.textContent = sessionAmount; // Update the session display
if(state) {
state = false;
totalSeconds = sessionAmount * 60;
initialSeconds = totalSeconds;
myInterval = setInterval(() => {
if (!isPaused) {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds <= 0) {
bells.play();
clearInterval(myInterval);
startBreakTimer();
}
}
}, 1000);
} else {
alert('Session has already started.');
}
};
const pauseTimer = () => {
if (!state) {
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? 'resume' : 'pause';
}
}
const startBreakTimer = () => {
const breakAmount = Number.parseInt(breakInput.value);
if (isNaN(breakAmount) || breakAmount <= 0) {
alert('Please enter a valid break duration.');
return;
}
totalSeconds = breakAmount * 60;
initialSeconds = totalSeconds;
myInterval = setInterval(() => {
if (!isPaused) {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds <= 0) {
bells.play();
clearInterval(myInterval);
state = true;
}
}
}, 1000);
};
const resetTimer = () => {
clearInterval(myInterval);
state = true;
isPaused = false;
pauseBtn.textContent = 'pause';
const minuteDiv = document.querySelector('.minutes');
const secondDiv = document.querySelector('.seconds');
minuteDiv.textContent = sessionInput.value;;
secondDiv.textContent = '00';
// Reset circle animation
const leftSide = document.querySelector('.left-side');
const rightSide = document.querySelector('.right-side');
leftSide.style.transform = 'rotate(-90deg)';
rightSide.style.transform = 'rotate(-90deg)';
}
startBtn.addEventListener('click', appTimer);
pauseBtn.addEventListener('click', pauseTimer);
resetBtn.addEventListener('click', resetTimer);</code>
<code>const bells = new Audio('./sounds/bell.wav');
const startBtn = document.querySelector('.btn-start');
const pauseBtn = document.querySelector('.btn-pause');
const resetBtn = document.querySelector('.btn-reset');
const session = document.querySelector('.minutes');
const sessionInput = document.querySelector('#session-length');
const breakInput = document.querySelector('#break-length');
let myInterval;
let state = true;
let isPaused = false
let totalSeconds;
let initialSeconds;
const updateTimerDisplay = () => {
const minuteDiv = document.querySelector('.minutes');
const secondDiv = document.querySelector('.seconds');
let minutesLeft = Math.floor(totalSeconds / 60);
let secondsLeft = totalSeconds % 60;
secondDiv.textContent = secondsLeft < 10 ? '0' + secondsLeft : secondsLeft;
minuteDiv.textContent = `${minutesLeft}`;
// Update the circle animation
const leftSide = document.querySelector('.left-side');
const rightSide = document.querySelector('.right-side');
const duration = initialSeconds; // Total duration in seconds
const elapsed = initialSeconds - totalSeconds;
const percentage = (elapsed / duration) * 100;
let rotationRight, rotationLeft;
if (percentage <= 50) {
// First half: rotate right side down from top to bottom
rotationRight = (percentage / 50) * 180 - 90;
rotationLeft = -90; // Keep left side at the top
} else {
// Second half: rotate left side up from bottom to top
rotationRight = 90; // Keep right side at the bottom
rotationLeft = ((percentage - 50) / 50) * 180 + 90;
}
rightSide.style.transform = `rotate(${rotationRight}deg)`;
leftSide.style.transform = `rotate(${rotationLeft}deg)`;
};
const appTimer = () => {
const sessionAmount = Number.parseInt(sessionInput.value)
if (isNaN(sessionAmount) || sessionAmount <= 0) {
alert('Please enter a valid session duration.');
return;
}
session.textContent = sessionAmount; // Update the session display
if(state) {
state = false;
totalSeconds = sessionAmount * 60;
initialSeconds = totalSeconds;
myInterval = setInterval(() => {
if (!isPaused) {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds <= 0) {
bells.play();
clearInterval(myInterval);
startBreakTimer();
}
}
}, 1000);
} else {
alert('Session has already started.');
}
};
const pauseTimer = () => {
if (!state) {
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? 'resume' : 'pause';
}
}
const startBreakTimer = () => {
const breakAmount = Number.parseInt(breakInput.value);
if (isNaN(breakAmount) || breakAmount <= 0) {
alert('Please enter a valid break duration.');
return;
}
totalSeconds = breakAmount * 60;
initialSeconds = totalSeconds;
myInterval = setInterval(() => {
if (!isPaused) {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds <= 0) {
bells.play();
clearInterval(myInterval);
state = true;
}
}
}, 1000);
};
const resetTimer = () => {
clearInterval(myInterval);
state = true;
isPaused = false;
pauseBtn.textContent = 'pause';
const minuteDiv = document.querySelector('.minutes');
const secondDiv = document.querySelector('.seconds');
minuteDiv.textContent = sessionInput.value;;
secondDiv.textContent = '00';
// Reset circle animation
const leftSide = document.querySelector('.left-side');
const rightSide = document.querySelector('.right-side');
leftSide.style.transform = 'rotate(-90deg)';
rightSide.style.transform = 'rotate(-90deg)';
}
startBtn.addEventListener('click', appTimer);
pauseBtn.addEventListener('click', pauseTimer);
resetBtn.addEventListener('click', resetTimer);</code>
const bells = new Audio('./sounds/bell.wav');
const startBtn = document.querySelector('.btn-start');
const pauseBtn = document.querySelector('.btn-pause');
const resetBtn = document.querySelector('.btn-reset');
const session = document.querySelector('.minutes');
const sessionInput = document.querySelector('#session-length');
const breakInput = document.querySelector('#break-length');
let myInterval;
let state = true;
let isPaused = false
let totalSeconds;
let initialSeconds;
const updateTimerDisplay = () => {
const minuteDiv = document.querySelector('.minutes');
const secondDiv = document.querySelector('.seconds');
let minutesLeft = Math.floor(totalSeconds / 60);
let secondsLeft = totalSeconds % 60;
secondDiv.textContent = secondsLeft < 10 ? '0' + secondsLeft : secondsLeft;
minuteDiv.textContent = `${minutesLeft}`;
// Update the circle animation
const leftSide = document.querySelector('.left-side');
const rightSide = document.querySelector('.right-side');
const duration = initialSeconds; // Total duration in seconds
const elapsed = initialSeconds - totalSeconds;
const percentage = (elapsed / duration) * 100;
let rotationRight, rotationLeft;
if (percentage <= 50) {
// First half: rotate right side down from top to bottom
rotationRight = (percentage / 50) * 180 - 90;
rotationLeft = -90; // Keep left side at the top
} else {
// Second half: rotate left side up from bottom to top
rotationRight = 90; // Keep right side at the bottom
rotationLeft = ((percentage - 50) / 50) * 180 + 90;
}
rightSide.style.transform = `rotate(${rotationRight}deg)`;
leftSide.style.transform = `rotate(${rotationLeft}deg)`;
};
const appTimer = () => {
const sessionAmount = Number.parseInt(sessionInput.value)
if (isNaN(sessionAmount) || sessionAmount <= 0) {
alert('Please enter a valid session duration.');
return;
}
session.textContent = sessionAmount; // Update the session display
if(state) {
state = false;
totalSeconds = sessionAmount * 60;
initialSeconds = totalSeconds;
myInterval = setInterval(() => {
if (!isPaused) {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds <= 0) {
bells.play();
clearInterval(myInterval);
startBreakTimer();
}
}
}, 1000);
} else {
alert('Session has already started.');
}
};
const pauseTimer = () => {
if (!state) {
isPaused = !isPaused;
pauseBtn.textContent = isPaused ? 'resume' : 'pause';
}
}
const startBreakTimer = () => {
const breakAmount = Number.parseInt(breakInput.value);
if (isNaN(breakAmount) || breakAmount <= 0) {
alert('Please enter a valid break duration.');
return;
}
totalSeconds = breakAmount * 60;
initialSeconds = totalSeconds;
myInterval = setInterval(() => {
if (!isPaused) {
totalSeconds--;
updateTimerDisplay();
if (totalSeconds <= 0) {
bells.play();
clearInterval(myInterval);
state = true;
}
}
}, 1000);
};
const resetTimer = () => {
clearInterval(myInterval);
state = true;
isPaused = false;
pauseBtn.textContent = 'pause';
const minuteDiv = document.querySelector('.minutes');
const secondDiv = document.querySelector('.seconds');
minuteDiv.textContent = sessionInput.value;;
secondDiv.textContent = '00';
// Reset circle animation
const leftSide = document.querySelector('.left-side');
const rightSide = document.querySelector('.right-side');
leftSide.style.transform = 'rotate(-90deg)';
rightSide.style.transform = 'rotate(-90deg)';
}
startBtn.addEventListener('click', appTimer);
pauseBtn.addEventListener('click', pauseTimer);
resetBtn.addEventListener('click', resetTimer);
<code>html {
font-family: 'Fira Sans', sans-serif;
font-size: 20px;
letter-spacing: 0.8px;
min-height: 100vh;
color: #d8e9ef;
background-image: linear-gradient(-20deg, #025159 0%, #733b36 100%);
background-size: cover;
}
h1 {
margin: 0 auto 10px auto;
color: #d8e9ef;
}
p {
margin: 0;
}
.app-message {
height: 20px;
margin: 10px auto 20px auto;
}
.app-container {
width: 250px;
height: 420px;
margin: 40px auto;
text-align: center;
border-radius: 5px;
padding: 20px;
}
/*@keyframes rotate-right-from-top {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(90deg);
}
}
@keyframes rotate-left-from-top {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(90deg);
}
}*/
.app-circle {
position: relative;
margin: 0 auto;
width: 200px;
height: 200px;
}
.circle-shape {
pointer-events: none;
}
.semi-circle {
position: absolute;
width: 100px;
height: 200px;
box-sizing: border-box;
border: solid 6px;
transform: rotate(-90deg);
transform-origin: 50% 100%; /* Set the transform origin to the bottom center */
}
.left-side {
top: 0;
left: 0;
transform-origin: right center;
/*transform: rotate(0deg);*/
border-top-left-radius: 100px;
border-bottom-left-radius: 100px;
border-right: none;
z-index: 1;
/*animation-name: rotate-left-from-top;*/
}
.right-side {
top: 0;
left: 100px;
transform-origin: left center;
/*transform: rotate(0deg);*/
border-top-right-radius: 100px;
border-bottom-right-radius: 100px;
border-left: none;
/*animation-name: rotate-right-from-top;*/
}
.circle {
border-color: #bf5239;
}
.circle-mask {
border-color: #e85a71;
}
.app-counter-box {
font-family: 'Droid Sans Mono', monospace;
font-size: 250%;
position: relative;
top: 50px;
color: #d8e9ef;
}
button {
position: relative;
top: 50px;
font-size: 80%;
text-transform: uppercase;
letter-spacing: 1px;
border: none;
background: none;
outline: none;
color: #d8e9ef;
margin: 5px;
}
button:hover {
color: #90c0d1;
}
.btn-pause, .btn-reset {
display: inline-block;
}
.settings {
position: relative;
top: 100px;
display: flex;
flex-direction: column;
align-items: center;
}
.settings label {
margin: 5px 0;
color: #d8e9ef;
}</code>
<code>html {
font-family: 'Fira Sans', sans-serif;
font-size: 20px;
letter-spacing: 0.8px;
min-height: 100vh;
color: #d8e9ef;
background-image: linear-gradient(-20deg, #025159 0%, #733b36 100%);
background-size: cover;
}
h1 {
margin: 0 auto 10px auto;
color: #d8e9ef;
}
p {
margin: 0;
}
.app-message {
height: 20px;
margin: 10px auto 20px auto;
}
.app-container {
width: 250px;
height: 420px;
margin: 40px auto;
text-align: center;
border-radius: 5px;
padding: 20px;
}
/*@keyframes rotate-right-from-top {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(90deg);
}
}
@keyframes rotate-left-from-top {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(90deg);
}
}*/
.app-circle {
position: relative;
margin: 0 auto;
width: 200px;
height: 200px;
}
.circle-shape {
pointer-events: none;
}
.semi-circle {
position: absolute;
width: 100px;
height: 200px;
box-sizing: border-box;
border: solid 6px;
transform: rotate(-90deg);
transform-origin: 50% 100%; /* Set the transform origin to the bottom center */
}
.left-side {
top: 0;
left: 0;
transform-origin: right center;
/*transform: rotate(0deg);*/
border-top-left-radius: 100px;
border-bottom-left-radius: 100px;
border-right: none;
z-index: 1;
/*animation-name: rotate-left-from-top;*/
}
.right-side {
top: 0;
left: 100px;
transform-origin: left center;
/*transform: rotate(0deg);*/
border-top-right-radius: 100px;
border-bottom-right-radius: 100px;
border-left: none;
/*animation-name: rotate-right-from-top;*/
}
.circle {
border-color: #bf5239;
}
.circle-mask {
border-color: #e85a71;
}
.app-counter-box {
font-family: 'Droid Sans Mono', monospace;
font-size: 250%;
position: relative;
top: 50px;
color: #d8e9ef;
}
button {
position: relative;
top: 50px;
font-size: 80%;
text-transform: uppercase;
letter-spacing: 1px;
border: none;
background: none;
outline: none;
color: #d8e9ef;
margin: 5px;
}
button:hover {
color: #90c0d1;
}
.btn-pause, .btn-reset {
display: inline-block;
}
.settings {
position: relative;
top: 100px;
display: flex;
flex-direction: column;
align-items: center;
}
.settings label {
margin: 5px 0;
color: #d8e9ef;
}</code>
html {
font-family: 'Fira Sans', sans-serif;
font-size: 20px;
letter-spacing: 0.8px;
min-height: 100vh;
color: #d8e9ef;
background-image: linear-gradient(-20deg, #025159 0%, #733b36 100%);
background-size: cover;
}
h1 {
margin: 0 auto 10px auto;
color: #d8e9ef;
}
p {
margin: 0;
}
.app-message {
height: 20px;
margin: 10px auto 20px auto;
}
.app-container {
width: 250px;
height: 420px;
margin: 40px auto;
text-align: center;
border-radius: 5px;
padding: 20px;
}
/*@keyframes rotate-right-from-top {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(90deg);
}
}
@keyframes rotate-left-from-top {
0% {
transform: rotate(-90deg);
}
100% {
transform: rotate(90deg);
}
}*/
.app-circle {
position: relative;
margin: 0 auto;
width: 200px;
height: 200px;
}
.circle-shape {
pointer-events: none;
}
.semi-circle {
position: absolute;
width: 100px;
height: 200px;
box-sizing: border-box;
border: solid 6px;
transform: rotate(-90deg);
transform-origin: 50% 100%; /* Set the transform origin to the bottom center */
}
.left-side {
top: 0;
left: 0;
transform-origin: right center;
/*transform: rotate(0deg);*/
border-top-left-radius: 100px;
border-bottom-left-radius: 100px;
border-right: none;
z-index: 1;
/*animation-name: rotate-left-from-top;*/
}
.right-side {
top: 0;
left: 100px;
transform-origin: left center;
/*transform: rotate(0deg);*/
border-top-right-radius: 100px;
border-bottom-right-radius: 100px;
border-left: none;
/*animation-name: rotate-right-from-top;*/
}
.circle {
border-color: #bf5239;
}
.circle-mask {
border-color: #e85a71;
}
.app-counter-box {
font-family: 'Droid Sans Mono', monospace;
font-size: 250%;
position: relative;
top: 50px;
color: #d8e9ef;
}
button {
position: relative;
top: 50px;
font-size: 80%;
text-transform: uppercase;
letter-spacing: 1px;
border: none;
background: none;
outline: none;
color: #d8e9ef;
margin: 5px;
}
button:hover {
color: #90c0d1;
}
.btn-pause, .btn-reset {
display: inline-block;
}
.settings {
position: relative;
top: 100px;
display: flex;
flex-direction: column;
align-items: center;
}
.settings label {
margin: 5px 0;
color: #d8e9ef;
}
<code><!DOCTYPE html>
<html lang="en">
<head>
<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=EG+Garamond:wght@400;500;600;700&family=Fira+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style.css" />
<title>Pomodoro App</title>
</head>
<body>
<div class="app-container">
<h1>pomodoro</h1>
<div class="app-message">press start to begin</div>
<div class="app-circle">
<div class="circle-shape">
<div class="semi-circle right-side circle-mask"></div>
<div class="semi-circle right-side circle"></div>
<div class="semi-circle left-side circle-mask"></div>
<div class="semi-circle left-side circle"></div>
</div>
<div class="app-counter-box">
<p><span class="minutes">25</span>:<span class="seconds">00</span></p>
</div>
<button class="btn-start">start</button>
<button class="btn-pause">pause</button>
<button class="btn-reset">reset</button>
<div class="settings">
<label>Session (minutes): <input type="number" id="session-length" value="25"></label>
<label>Break (minutes): <input type="number" id="break-length" value="5"></label>
</div>
</div>
</div>
</body>
<script src="./app.js"></script>
</html></code>
<code><!DOCTYPE html>
<html lang="en">
<head>
<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=EG+Garamond:wght@400;500;600;700&family=Fira+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style.css" />
<title>Pomodoro App</title>
</head>
<body>
<div class="app-container">
<h1>pomodoro</h1>
<div class="app-message">press start to begin</div>
<div class="app-circle">
<div class="circle-shape">
<div class="semi-circle right-side circle-mask"></div>
<div class="semi-circle right-side circle"></div>
<div class="semi-circle left-side circle-mask"></div>
<div class="semi-circle left-side circle"></div>
</div>
<div class="app-counter-box">
<p><span class="minutes">25</span>:<span class="seconds">00</span></p>
</div>
<button class="btn-start">start</button>
<button class="btn-pause">pause</button>
<button class="btn-reset">reset</button>
<div class="settings">
<label>Session (minutes): <input type="number" id="session-length" value="25"></label>
<label>Break (minutes): <input type="number" id="break-length" value="5"></label>
</div>
</div>
</div>
</body>
<script src="./app.js"></script>
</html></code>
<!DOCTYPE html>
<html lang="en">
<head>
<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=EG+Garamond:wght@400;500;600;700&family=Fira+Sans:wght@400;500;600;700&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="./style.css" />
<title>Pomodoro App</title>
</head>
<body>
<div class="app-container">
<h1>pomodoro</h1>
<div class="app-message">press start to begin</div>
<div class="app-circle">
<div class="circle-shape">
<div class="semi-circle right-side circle-mask"></div>
<div class="semi-circle right-side circle"></div>
<div class="semi-circle left-side circle-mask"></div>
<div class="semi-circle left-side circle"></div>
</div>
<div class="app-counter-box">
<p><span class="minutes">25</span>:<span class="seconds">00</span></p>
</div>
<button class="btn-start">start</button>
<button class="btn-pause">pause</button>
<button class="btn-reset">reset</button>
<div class="settings">
<label>Session (minutes): <input type="number" id="session-length" value="25"></label>
<label>Break (minutes): <input type="number" id="break-length" value="5"></label>
</div>
</div>
</div>
</body>
<script src="./app.js"></script>
</html>
Right now nothing is happening until the timer reaches half of the total time, then, it fills the top half of the circle and the animation starts from the middle of the left side of the circle.
Can someone help me with this please?
Thank you!