diff --git a/app.yaml b/app.yaml
new file mode 100644
index 0000000..541b43b
--- /dev/null
+++ b/app.yaml
@@ -0,0 +1,15 @@
+runtime: python27
+version: 1
+api_version: 1
+application: babystats-root
+threadsafe: true
+
+handlers:
+- url: /
+ static_files: static/babystats.html
+ upload: static/babystats.html
+ secure: always
+
+- url: /static
+ static_dir: static
+ secure: always
diff --git a/static/babystats.css b/static/babystats.css
new file mode 100644
index 0000000..aeddec3
--- /dev/null
+++ b/static/babystats.css
@@ -0,0 +1,12 @@
+body {
+ margin: 0;
+ font-family: "proxima-nova";
+}
+
+#container {
+ position: fixed;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ right: 0;
+}
diff --git a/static/babystats.html b/static/babystats.html
new file mode 100644
index 0000000..f8ad338
--- /dev/null
+++ b/static/babystats.html
@@ -0,0 +1,15 @@
+
+
+ BabyStats
+
+
+
+
+
+
+
+
+
+
diff --git a/static/babystats.js b/static/babystats.js
new file mode 100644
index 0000000..040ea1d
--- /dev/null
+++ b/static/babystats.js
@@ -0,0 +1,258 @@
+var BabyStats = function(container) {
+ this.container_ = container;
+
+ this.tileScaleHeight_ = 1;
+ this.tileScaleWidth_ = 1;
+
+ this.tiles_ = [
+ ['asleep', 'Asleep'],
+ ['awake', 'Awake'],
+ ['diaper_feces', 'Diaper change\n(feces)'],
+ ['diaper_urine', 'Diaper change\n(urine only)'],
+ ['feeding_breast', 'Feeding\n(breast)'],
+ ['feeding_bottle_milk', 'Feeding\n(bottled breast milk)'],
+ ['feeding_formula', 'Feeding\n(formula)'],
+ ];
+
+ this.intervals_ = {};
+
+ this.buildCells_();
+ this.buildStylesheet_();
+ var grid = this.calculateGrid_();
+ this.gridWidthCells_ = grid.gridWidthCells;
+ this.gridHeightCells_ = grid.gridHeightCells;
+ this.buildGrid_();
+};
+
+/**
+ * Add a CSS class to a node if it doesn't already have it.
+ * @param {!Node} node Node object to add class to
+ * @param {!string} className Name of class to add
+ * @private
+ */
+BabyStats.prototype.addCSSClass_ = function(node, className) {
+ var classes = node.className.split(' ').filter(function(className) { return className; });
+ if (classes.indexOf(className) != -1) {
+ // Already has class.
+ return;
+ }
+ classes.push(className);
+ node.className = classes.join(' ');
+}
+
+/**
+ * Construct our stylesheet and insert it into the DOM.
+ * @private
+ */
+BabyStats.prototype.buildStylesheet_ = function() {
+ // http://www.colourlovers.com/palette/848743/(%E2%97%95_%E2%80%9D_%E2%97%95)
+ var style = document.createElement('style');
+ document.head.appendChild(style);
+
+ style.sheet.insertRule('babyStatsRow {}', 0);
+ this.rowRule_ = style.sheet.cssRules[0];
+ this.rowRule_.style.display = 'block';
+ this.rowRule_.style.textAlign = 'center';
+
+ style.sheet.insertRule('babyStatsCell {}', 0);
+ this.cellRule_ = style.sheet.cssRules[0];
+ this.cellRule_.style.display = 'inline-block';
+ this.cellRule_.style.position = 'relative';
+ this.cellRule_.style.height = '100%';
+ this.cellRule_.style.webkitUserSelect = 'none';
+ this.cellRule_.style.mozUserSelect = 'none';
+ this.cellRule_.style.userSelect = 'none';
+ this.cellRule_.style.cursor = 'default';
+
+ style.sheet.insertRule('babyStatsCellContents {}', 0);
+ var contents = style.sheet.cssRules[0];
+ contents.style.display = 'flex';
+ contents.style.position = 'absolute';
+ contents.style.alignItems = 'center';
+ contents.style.justifyContent = 'center';
+ contents.style.margin = '5px';
+ contents.style.padding = '5px';
+ contents.style.height = 'calc(100% - 20px)';
+ contents.style.width = 'calc(100% - 20px)';
+ contents.style.fontSize = '6vmin';
+ contents.style.fontWeight = 'bold';
+ contents.style.whiteSpace = 'pre-line';
+ contents.style.backgroundColor = 'rgb(73,10,61)';
+ contents.style.color = 'rgb(233,127,2)';
+ contents.style.borderRadius = '15px';
+
+ style.sheet.insertRule('babyStatsCellOverlay {}', 0);
+ var contents = style.sheet.cssRules[0];
+ contents.style.display = 'flex';
+ contents.style.position = 'absolute';
+ contents.style.alignItems = 'center';
+ contents.style.justifyContent = 'center';
+ contents.style.margin = '5px';
+ contents.style.height = 'calc(100% - 10px)';
+ contents.style.width = 'calc(100% - 10px)';
+ contents.style.fontSize = '20vmin';
+ contents.style.fontWeight = 'bold';
+ contents.style.backgroundColor = 'rgb(255,255,255)';
+ contents.style.color = 'rgb(189,21,80)';
+ contents.style.borderRadius = '15px';
+ contents.style.opacity = 0.0;
+ contents.style.transition = '0.4s';
+
+ style.sheet.insertRule('.babyStatsContainer {}', 0);
+ var containerRule = style.sheet.cssRules[0];
+ containerRule.style.backgroundColor = 'white';
+
+ this.addCSSClass_(this.container_, 'babyStatsContainer');
+};
+
+/**
+ * Construct cameraGridCell options for insertion into the DOM.
+ * @private
+ */
+BabyStats.prototype.buildCells_ = function() {
+ this.cells_ = [];
+ this.tiles_.forEach(function(tiles) {
+ var cell = document.createElement('babyStatsCell');
+ this.cells_.push(cell);
+
+ var contents = document.createElement('babyStatsCellContents');
+ contents.textContent = tiles[1];
+ cell.appendChild(contents);
+
+ var overlay = document.createElement('babyStatsCellOverlay');
+ cell.appendChild(overlay);
+
+ cell.addEventListener('click', this.onClick_.bind(this, tiles[0], overlay));
+ }, this);
+};
+
+BabyStats.prototype.onClick_ = function(eventName, overlay) {
+ if (this.intervals_[eventName]) {
+ window.clearInterval(this.intervals_[eventName]);
+ delete this.intervals_[eventName];
+ overlay.style.opacity = 0.0;
+ return;
+ }
+ var timer = 5;
+ overlay.textContent = timer;
+ overlay.style.opacity = 0.5;
+ this.intervals_[eventName] = window.setInterval(function() {
+ timer--;
+ switch (timer) {
+ case 0:
+ // XXX: send
+ overlay.textContent = '✓';
+ break;
+
+ case -1:
+ break;
+
+ case -2:
+ window.clearInterval(this.intervals_[eventName]);
+ delete this.intervals_[eventName];
+ overlay.style.opacity = 0.0;
+ break;
+
+ default:
+ overlay.textContent = timer;
+ break;
+ }
+ }.bind(this), 1000);
+};
+
+/**
+ * Calculate optimal grid sizing.
+ * This pile of magic math calculates the optimal grid width and height to
+ * maximize the size of all video feeds while preserving their aspect ratios.
+ * @return {{
+ * gridWidthCells: number,
+ * gridHeightCells: number,
+ * cellWidthPx: number,
+ * cellHeightPx: number
+ * }}
+ * @private
+ */
+BabyStats.prototype.calculateGrid_ = function() {
+ var containerWidth = this.container_.offsetWidth;
+ var containerHeight = this.container_.offsetHeight;
+ var numTiles = this.tiles_.length;
+
+ var scaleFactor = ((containerHeight / this.tileScaleHeight_)
+ / (containerWidth / this.tileScaleWidth_));
+
+ var gridHeight = Math.sqrt(scaleFactor * numTiles);
+ var gridWidth = Math.sqrt(numTiles / scaleFactor);
+
+ var gridOptions = [
+ [ Math.ceil(gridWidth), Math.floor(gridHeight) ],
+ [ Math.floor(gridWidth), Math.ceil(gridHeight) ],
+ [ Math.ceil(gridWidth), Math.ceil(gridHeight) ],
+ ];
+
+ // Check all possible options.
+ // We are optimizing for several dimensions (decreasing priority):
+ // 1) Be able to fit all the tiles.
+ // 2) Maximum scale for an image in each cell.
+ // 3) Minimize number of cells.
+ var minCells = Number.MAX_VALUE;
+ var maxScale = 0.0;
+ var chosenHeight, chosenWidth;
+ gridOptions.forEach(function(gridOption) {
+ var numCells = gridOption[0] * gridOption[1];
+ if (numCells < numTiles) {
+ // Can't fit all the tiles in (we've rounded down too far).
+ return;
+ }
+ var widthScale = (containerWidth / gridOption[0]) / this.tileScaleWidth_;
+ var heightScale = (containerHeight / gridOption[1]) / this.tileScaleHeight_;
+ var scale;
+ if (widthScale < heightScale) {
+ scale = widthScale;
+ } else {
+ scale = heightScale;
+ }
+ if (scale < maxScale) {
+ // This would make cells smaller than another viable solution.
+ return;
+ }
+ if (scale == maxScale && numCells > minCells) {
+ // Same cell size as another viable solution, but ours has more cells.
+ return;
+ }
+ chosenWidth = gridOption[0];
+ chosenHeight = gridOption[1];
+ minCells = numCells;
+ maxScale = scale;
+ }, this);
+
+ return /** @struct */ {
+ gridWidthCells: chosenWidth,
+ gridHeightCells: chosenHeight,
+ cellWidthPx: this.tileScaleWidth_ * maxScale,
+ cellHeightPx: this.tileScaleHeight_ * maxScale,
+ };
+};
+
+/**
+ * Construct the grid objects in the DOM.
+ * @private
+ */
+BabyStats.prototype.buildGrid_ = function() {
+ this.container_.innerHTML = '';
+
+ this.rowRule_.style.height = 100 / this.gridHeightCells_ + '%';
+ this.cellRule_.style.width = 100 / this.gridWidthCells_ + '%';
+
+ var i = 0;
+ for (var y = 0; y < this.gridHeightCells_; y++) {
+ var row = document.createElement('babyStatsRow');
+ for (var x = 0; x < this.gridWidthCells_; x++) {
+ if (i < this.cells_.length) {
+ var cell = this.cells_[i];
+ row.appendChild(cell);
+ i++;
+ }
+ }
+ this.container_.appendChild(row);
+ }
+};