I was searching the internet for a solution to a problem I'm facing, and came across this great topic! So what I'm looking for is a progress bar to visually display the remaining time of a quiz. The quiz has a preset duration value (ie. 3 mins), which I can easily pass to JS from PHP when calling the JS. Additionally, in order to deal with sneaky persons who might try to refresh the page, attempting to gain some extra time, at the first time the page loads, I store the timestamp of that moment.
So if someone attempts to do a page refresh, the script will load the stored starting time, subtract it from [now], and thus calculate the remaining amount of seconds. Ie. if the quiz initially started at 16:50:15 (a unix timestamp is stored actually), and the duration of the quiz is 180s (3 mins), normally the timer will stop at 16:53:15. Now, if a sneaky player refreshes the page at 16:51:15, the system will know that only 120s of time are left, and will start from there.
But with the timer of the link above, if I set the duration to 120s, it'll start with a full progress bar, only this time it'll reduce faster... Ideally I'd like it to start at two thirds (2 mins remaining) of the bar, and keep the previous reducing ratio...
The problem is I can't think of a way to achieve that. Could someone help me figure that out?
What I'm thinking is to calculate an additional @keyframe at [xx]%, where [xx] is the remaining time / total duration
, but then I don't know if/how I can set the animation to start from that @keyframe, instead of @100%... Am I on the correct path?
Update: Obviously I rushed into opening this topic... There is a CSS rule to achieve what I need... All that was needed is to add a negative animation-delay
in seconds, like animation-delay: -60s
and CSS takes care of this delay in terms of picking the correct color from the gradient between the two keyframes (0% - 100%), and also starts from that point... Absolutely brilliant... Take a look at this fiddle!!!
Also, while I changed the gradient (of start and end) colors to the ones that suit my needs better, I'd also like to make the whole progress bar have that pattern of diagonal "moving" lines (like this) while reducing. I tried with a gif and adding some alpha
to the color of the progress bar, but it didn't work. Or I may have done it wrong. Could someone help me figure that out as well?
CodePudding user response:
For the first part of your issue:
I assume you are setting TotalSeconds
to 120
.
Instead, you have a conveniently placed start
variable here.
var TotalSeconds = 180;
var documentWidth = $(document).width();
var start = Date.now(); // <<< this
var intervalSetted = null;
You simply need to make it so that the progress bar actually started x
seconds before the current time where x
is the time since the actual quiz started (30 seconds from your example).
var start = Date.now() - (30 * 1000);
The progress bar should work fine now.
EDIT: The keyframes part
That's actually a simple background image (with repeat) on the progress bar (a gradient in your example) and keyframes are used to move it to the left.
Something like
.myAnimatedProgressBar {
animation: striped-bar 1s linear infinite;
}
@keyframes striped-bar {
background-position: 10px 0;
}
CodePudding user response:
I addressed both your questions.
- Saving the progressBar state
- Adding some animated oblique bars
The second one is super easy. You just had to get the BootStrap style by inspecting the CSS applied.
The first one was a bit trickyer... The animation is CSS. You needed to modify the .inner
and @keyframes
definition.
That looks like this:
let keyFrames = `
.inner {
height: 15px;
animation: progressbar-countdown;
animation-duration: ${newAnimationDelay}s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-play-state: paused;
animation-timing-function: linear;
}
@keyframes progressbar-countdown {
0% {
width: ${newWidth};
background: ${newColor};
}
100% {
width: 0%;
background: #f00;
}
}`
I made it using the trick explained in this other SO answer.
So... Having saved the state of the progressBar constently (each 100ms), you can update that CSS. The width
, the color
, the startTime
, and the remaining time
are needed.
Here is the full code, but that snippet does not allow localStorage
, so have a look on CodePen.
If you hit the "Run" button, it is like a page refresh.
console.clear();
/*
* Creates a progressbar.
* @param id the id of the div we want to transform in a progressbar
* @param duration the duration of the timer example: '10s'
* @param callback, optional function which is called when the progressbar reaches 0.
*/
function createProgressbar(id, duration, callback) {
// We select the div that we want to turn into a progressbar
var progressbar = document.getElementById(id);
progressbar.className = "progressbar";
// We create the div that changes width to show progress
var progressbarinner = document.createElement("div");
progressbarinner.className = "inner";
// Now we set the animation parameters
progressbarinner.style.animationDuration = duration;
// Eventually couple a callback
if (typeof callback === "function") {
progressbarinner.addEventListener("animationend", callback);
}
// Add a div inside .inner just for the oblique bars animation
var bars = document.createElement("div");
bars.className = "bars";
progressbarinner.appendChild(bars);
// Append the progressbar to the main progressbardiv
progressbar.appendChild(progressbarinner);
// When everything is set up we start the animation
progressbarinner.style.animationPlayState = "running";
}
// A variable to get the progressbar width on page load
let initialProgressBarWidth;
addEventListener("load", function () {
let stored;
let duration;
// If there is a localStorage, get it and calculate the remaining duration in seconds
if (localStorage.getItem("savedProgress")) {
stored = JSON.parse(localStorage.getItem("savedProgress"));
duration = Math.floor(parseFloat(stored.remainingTime / 1000)) "s";
console.log(stored);
} else {
duration = "40s";
}
// Create the progressBar
createProgressbar("progressbar1", duration);
let progressBarInner = document.querySelector(".inner");
initialProgressBarWidth = window.getComputedStyle(progressBarInner).width;
// If there is a localStorage, You need to override the CSS
if (stored) {
let newWidth =
Math.floor(
(parseFloat(stored.progressBarWidth) /
parseFloat(initialProgressBarWidth)) *
100
) "%";
let newColor = stored.progressBarBackgroundColor;
let newAnimationDelay = Math.floor(parseFloat(stored.remainingTime / 1000));
let style = document.createElement("style");
let keyFrames = `
.inner {
height: 15px;
animation: progressbar-countdown;
animation-duration: ${newAnimationDelay}s;
animation-iteration-count: 1;
animation-fill-mode: forwards;
animation-play-state: paused;
animation-timing-function: linear;
}
@keyframes progressbar-countdown {
0% {
width: ${newWidth};
background: ${newColor};
}
100% {
width: 0%;
background: #f00;
}
}`;
style.innerHTML = keyFrames;
console.log(style);
document.getElementsByTagName("head")[0].appendChild(style);
}
});
// A variable to get the startTime on page load
let startTime;
if (!localStorage.getItem("savedProgress")) {
startTime = new Date().getTime();
} else {
startTime = JSON.parse(localStorage.getItem("savedProgress")).startTime;
}
console.log(startTime);
// At each 100ms Save to localStorage
let localStorageInterval = setInterval(function () {
let nowTime = new Date().getTime();
let remainingTime = 40000 - (nowTime - startTime);
let compStyle = window.getComputedStyle(document.querySelector(".inner"));
let progressBarWidth = compStyle.width;
let progressBarBackgroundColor = compStyle.backgroundColor;
let savedProgress = {
startTime,
remainingTime,
progressBarWidth,
progressBarBackgroundColor
};
console.log(savedProgress);
localStorage.setItem("savedProgress", JSON.stringify(savedProgress));
// Stop the interval at the end
if (remainingTime <= 0) {
localStorage.setItem(
"savedProgress",
JSON.stringify({ ...savedProgress, progressBarWidth: "0%" })
);
clearInterval(localStorageInterval);
alert("Time over");
}
}, 100);
// Just for this demo
document.querySelector(".reset").addEventListener("click", function () {
localStorage.clear();
});
.progressbar {
width: 80%;
margin: 25px auto;
border: solid 1px #000;
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0.15) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15) 75%,
transparent 75%,
transparent
);
}
.progressbar .inner {
height: 15px;
animation: progressbar-countdown;
/* Placeholder, this will be updated using javascript */
animation-duration: 40s;
/* We stop in the end */
animation-iteration-count: 1;
/* Stay on pause when the animation is finished finished */
animation-fill-mode: forwards;
/* We start paused, we start the animation using javascript */
animation-play-state: paused;
/* We want a linear animation, ease-out is standard */
animation-timing-function: linear;
}
@keyframes progressbar-countdown {
0% {
width: 100%;
background: #0f0;
}
100% {
width: 0%;
background: #f00;
}
}
.bars {
background-image: linear-gradient(
45deg,
rgba(255, 255, 255, 0.15) 25%,
transparent 25%,
transparent 50%,
rgba(255, 255, 255, 0.15) 50%,
rgba(255, 255, 255, 0.15) 75%,
transparent 75%,
transparent
);
background-size: 1rem 1rem;
animation: progress-bar-stripes 1s linear infinite;
width: 100%;
height: 15px;
}
@keyframes progress-bar-stripes {
0% {
background-position: 0 0;
}
100% {
background-position: 2rem 0;
}
}
<div id='progressbar1'></div>
<button >Reset localStorage</button>
Side note: That is great... But is not really secure. As you can notice, an easy way to hack it is to clear localStorage
. Anyway, it less easier to average people than just a page refresh. ;)