Skip to content

Instantly share code, notes, and snippets.

@noblesilence
Created April 26, 2018 06:20
Show Gist options
  • Save noblesilence/f9e6efae30089e8e2efcfc00359fa925 to your computer and use it in GitHub Desktop.
Save noblesilence/f9e6efae30089e8e2efcfc00359fa925 to your computer and use it in GitHub Desktop.
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS104: Avoid inline assignments
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const React = require("react");
const PropTypes = require("prop-types");
const CreateReactClass = require("create-react-class");
const ReactCountdownClock = CreateReactClass({
_seconds: 0,
_radius: null,
_fraction: null,
_content: null,
_canvas: null,
_timeoutIds: [],
_scale: window.devicePixelRatio || 1,
displayName: "ReactCountdownClock",
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();
}
if (this.props.paused) {
this._pauseTimer();
}
}
if (prevProps.stopped !== this.props.stopped) {
this._clearBackground();
this._clearTimer();
this._seconds = this._startSeconds();
this._stopTimer();
return this._setupTimer();
}
},
componentDidMount() {
this._seconds = this._startSeconds();
return this._setupTimer();
},
componentWillUnmount() {
return this._cancelTimer();
},
_startSeconds() {
// To prevent a brief flash of the start time when not paused
if (this.props.paused && this.props.stopped) {
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.props.stopped) {
return this._startTimer();
}
},
_updateCanvas() {
this._clearTimer();
return this._drawTimer();
},
_setScale() {
this._radius = this.props.size / 2;
this._fraction = 2 / this._seconds;
this._tickPeriod = this._calculateTick();
return (this._innerRadius = this.props.weight
? this._radius - this.props.weight
: this._radius / 1.8);
},
_calculateTick() {
// Tick period (milleseconds) 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) {
return this.refs.component.addEventListener("click", this.props.onClick);
}
},
_startTimer() {
// Give it a moment to collect it's thoughts for smoother render
return this._timeoutIds.push(setTimeout(() => this._tick()), 200);
},
_pauseTimer() {
this._stopTimer();
return this._updateCanvas();
},
_stopTimer() {
return Array.from(this._timeoutIds).map(timeout => clearTimeout(timeout));
},
_cancelTimer() {
this._stopTimer();
if (this.props.onClick != null) {
return this.refs.component.removeEventListener(
"click",
this.props.onClick
);
}
},
_tick() {
const start = Date.now();
return this._timeoutIds.push(
setTimeout(() => {
const duration = (Date.now() - start) / 1000;
this._seconds -= duration;
if (this._seconds <= 0) {
this._seconds = 0;
this._handleComplete();
return this._clearTimer();
} else {
this._updateCanvas();
return this._tick();
}
}, this._tickPeriod)
);
},
_handleComplete() {
if (this.props.onComplete) {
return this.props.onComplete();
}
},
_clearBackground() {
return this._background.clearRect(
0,
0,
this.refs.timer.width,
this.refs.timer.height
);
},
_clearTimer() {
if (this.refs.timer != null) {
return 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();
return 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; // ss or ss.s
}
})();
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.beginPath();
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();
return 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,
stopped: 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: true,
paused: false,
stopped: false
};
module.exports = ReactCountdownClock;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment