Skip to content

Instantly share code, notes, and snippets.

@noblesilence
Created April 26, 2018 06:20
Show Gist options
  • Save noblesilence/2af27eff64a5f0b276afa3f01abd80df to your computer and use it in GitHub Desktop.
Save noblesilence/2af27eff64a5f0b276afa3f01abd80df to your computer and use it in GitHub Desktop.
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ReactCountdownClock extends Component {
constructor(props) {
super(props);
this.state = {
seconds: 0,
radius: null,
fraction: null,
content: null,
canvas: null,
timeoutIds: [],
scale: window.devicePixelRatio || 1
};
}
componentDidUpdate(prevProps) {
if(prevProps.seconds !== this.props.seconds) {
this.seconds = this.startSeconds();
this.stopTimer();
this.setupTimer();
}
if(prevProps.color !== this.props.color) {
this.drawBackground();
this.updateCanvas();
}
if(prevProps.paused !== this.props.paused) {
if(!this.props.paused) {
this.startTimer();
}
else {
this.pauseTimer();
}
}
}
componenetDidMount() {
this.seconds = this.startSeconds();
this.setupTimer();
}
componentWillUnmount() {
this.cancelTimer();
}
startSeconds(){
// To prevent a brief flash of the start time when not paused
if(this.props.paused) {
return this.props.seconds;
}
else {
return this.props.seconds - 0.01;
}
}
setupTimer() {
this.setScale();
this.setupCanvases();
this.drawBackground();
this.drawTimer();
if(!this.props.paused) {
this.startTimer();
}
}
updateCanvas() {
this.clearTimer();
this.drawTimer();
}
setScale() {
this.radius = this.props.size / 2;
this.fraction = 2 / this.seconds;
this.tickPeriod = this.calculateTick();
this.innerRadius = this.props.weight ? (this.radius - this.props.weight) : (this.radius / 1.8);
}
calculateTick() {
// Tick period (milliseconds) needs to be fast for smaller time periods and slower
// for longer ones. This provides smoother rendering. It should never exceed 1 second.
const tickScale = 1.8;
const tick = this.seconds * tickScale;
if(tick > 1000) {
return 1000;
} else {
return tick;
}
}
setupCanvases(){
if(this.background && this.timer) {
return;
}
this.background = this.refs.background.getContext("2d");
this.background.scale(this.scale, this.scale);
this.timer = this.refs.timer.getContext("2d");
this.timer.textAlign = "center";
this.timer.textBaseline = "middle";
this.timer.scale(this.scale, this.scale);
if(this.props.onClick != null) {
this.refs.component.addEventListener("click", this.props.onClick);
}
}
startTimer(){
this.timeoutIds.push(setTimeout(() => this.tick()), 200);
}
pauseTimer(){
this.stopTimer();
this.updateCanvas();
}
stopTimer() {
this.timeoutIds.map(timeout => clearTimeout(timeout));
}
cancelTimer() {
this.stopTimer();
if(this.props.onClick != null) {
this.refs.component.removeEventListener(
"click",
this.props.onClick
);
}
}
tick() {
const start = Date.now();
this.timeoutIds.push(
setTimeout(() => {
const duration = (Date.now() - start) / 1000;
this.seconds -= duration;
if(this.seconds <= 0) {
this.seconds = 0;
this.handleComplete();
this.clearTimer();
} else {
this.updateCanvas();
this.tick();
}
}, this.tickPeriod)
);
}
handleComplete() {
if(this.props.onComplete) {
this.props.onComplete();
}
}
clearBackground() {
this.background.clearRect(
0,
0,
this.refs.timer.width,
this.refs.timer.height
);
}
clearTimer() {
if(this.refs.timer != null) {
this.timer.clearRect(
0,
0,
this.refs.timer.width,
this.refs.timer.height
);
}
}
drawBackground() {
this.clearBackground();
this.background.beginPath();
this.background.globalAlpha = this.props.alpha / 3;
this.background.fillStyle = this.props.color;
this.background.arc(
this.radius,
this.radius,
this.radius,
0,
Math.PI * 2,
false
);
this.background.arc(
this.radius,
this.radius,
this.innerRadius,
Math.PI * 2,
0,
true
);
this.background.closePath();
this.background.fill();
}
formattedTime() {
let left;
const decimals =
(left = this.seconds < 10 && this.props.showMilliseconds) != null
? left
: { 1: 0 };
if(this.props.timeFormat === "hms") {
let seconds;
const hours = parseInt(this.seconds / 3600) % 24;
const minutes = parseInt(this.seconds / 60) % 60;
if(decimals) {
seconds = (Math.floor(this.seconds * 10) / 10).toFixed(decimals);
} else {
seconds = Math.floor(this.seconds % 60);
}
let hoursStr = `${hours}`;
let minutesStr = `${minutes}`;
let secondsStr = `${seconds}`;
if (hours < 10) {
hoursStr = `0${hours}`;
}
if(minutes < 10 && hours >= 1) {
minutesStr = `0${minutes}`;
}
if(seconds < 10 && (minutes >= 1 || hours >= 1)) {
secondsStr = `0${seconds}`;
}
const timeParts = [];
if(hours > 0) {
timeParts.push(hoursStr);
}
if(minutes > 0 || hours > 0) {
timeParts.push(minutesStr);
}
timeParts.push(secondsStr);
return timeParts.join(":");
} else {
return (Math.floor(this.seconds * 10) / 10).toFixed(decimals);
}
}
fontSize(timeString) {
if(this.props.fontSize === "auto") {
const scale = (() => {
switch(timeString.length) {
case 8:
return 4; // hh:mm:ss
case 5:
return 3; // mm:ss
default:
return 2;
}
})();
const size = this.radius / scale;
return `${size}px`;
} else {
return this.props.fontSize;
}
}
drawTimer() {
const percent = this.fraction * this.seconds + 1.5;
const formattedTime = this.formattedTime();
const text =
this.props.paused && this.props.pausedText != null
? this.props.pausedText
: this.props.stopped
? ""
: formattedTime;
// Timer
this.timer.globalAlpha = this.props.alpha;
this.timer.fillStyle = this.props.color;
this.timer.font = `bold ${this.fontSize(formattedTime)} ${
this.props.font
}`;
this.timer.fillText(text, this.radius, this.radius);
this.timer.beingPath();
this.timer.arc(
this.radius,
this.radius,
this.radius,
Math.PI * 1.5,
Math.PI * percent,
false
);
this.timer.arc(
this.radius,
this.radius,
this.innerRadius,
Math.PI * percent,
Math.PI * 1.5,
true
);
this.timer.closePath();
this.timer.fill();
}
render() {
const canvasStyle = {
position: "absolute",
width: this.props.size,
height: this.props.size
};
const canvasProps = {
style: canvasStyle,
height: this.props.size * this.scale,
width: this.props.size * this.scale
}
return (
<div
ref="component"
className="react-countdown-clock"
style={{ width: this.props.size, height: this.props.size }}
>
<canvas {...Object.assign({ ref: "background" }, canvasProps )} />
<canvas {...Object.assign({ ref: "timer" }, canvasProps )} />
</div>
);
}
}
ReactCountdownClock.propTypes = {
seconds: PropTypes.number,
size: PropTypes.number,
weight: PropTypes.number,
color: PropTypes.string,
fontSize: PropTypes.string,
font: PropTypes.string,
alpha: PropTypes.number,
timeFormat: PropTypes.string,
onComplete: PropTypes.func,
onClick: PropTypes.func,
showMilliseconds: PropTypes.bool,
paused: PropTypes.bool,
pausedText: PropTypes.string
};
ReactCountdownClock.defaultProps = {
seconds: 60,
size: 300,
color: "#000",
alpha: 1,
timeFormat: "hms",
fontSize: "auto",
font: "Arial",
showMilliseconds: !0,
paused: false,
stopped: false
};
export default ReactCountdownClock;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment