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); + } +};