Created
April 26, 2018 06:20
-
-
Save noblesilence/2af27eff64a5f0b276afa3f01abd80df to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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