diff --git a/server/camlistored/ui/animation_loop.js b/server/camlistored/ui/animation_loop.js new file mode 100644 index 000000000..03368e7c0 --- /dev/null +++ b/server/camlistored/ui/animation_loop.js @@ -0,0 +1,128 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('camlistore.AnimationLoop'); + +goog.require('goog.events.EventTarget'); + +/** + * Provides an easier-to-use interface around + * window.requestAnimationFrame(), and abstracts away browser differences. + * @param {Window} win + */ +camlistore.AnimationLoop = function(win) { + /** + * @type {Window} + * @private + */ + this.win_ = win; + + /** + * @type {Function} + * @private + */ + this.requestAnimationFrame_ = win.requestAnimationFrame || + win.mozRequestAnimationFrame || win.webkitRequestAnimationFrame || + win.msRequestAnimationFrame; + + /** + * @type {Function} + * @private + */ + this.handleFrame_ = this.handleFrame_.bind(this); + + /** + * @type {number} + * @private + */ + this.lastTimestamp_ = 0; + + if (this.requestAnimationFrame_) { + this.requestAnimationFrame_ = this.requestAnimationFrame_.bind(win); + } else { + this.requestAnimationFrame_ = this.simulateAnimationFrame_.bind(this); + } +}; + +goog.inherits(camlistore.AnimationLoop, goog.events.EventTarget); + +/** + * @type {string} + */ +camlistore.AnimationLoop.FRAME_EVENT_TYPE = 'frame'; + +/** + * @returns {boolean} + */ +camlistore.AnimationLoop.prototype.isRunning = function() { + return Boolean(this.lastTimestamp_); +}; + +camlistore.AnimationLoop.prototype.start = function() { + if (this.isRunning()) { + return; + } + + this.lastTimestamp_ = -1; + this.schedule_(); +}; + +camlistore.AnimationLoop.prototype.stop = function() { + this.lastTimestamp_ = 0; +}; + +/** + * @private + */ +camlistore.AnimationLoop.prototype.schedule_ = function() { + this.requestAnimationFrame_(this.handleFrame_); +}; + +/** + * @param {number=} opt_timestamp A timestamp in milliseconds that is used to + * measure progress through the animation. + * @private + */ +camlistore.AnimationLoop.prototype.handleFrame_ = function(opt_timestamp) { + if (this.lastTimestamp_ == 0) { + return; + } + + var timestamp = opt_timestamp || new Date().getTime(); + if (this.lastTimestamp_ == -1) { + this.lastTimestamp_ = timestamp; + } else { + this.dispatchEvent({ + type: this.constructor.FRAME_EVENT_TYPE, + delay: timestamp - this.lastTimestamp_ + }); + this.lastTimestamp_ = timestamp; + } + + this.schedule_(); +}; + +/** + * Simulates requestAnimationFrame as best as possible for browsers that don't + * have it. + * @param {Function} fn + * @private + */ +camlistore.AnimationLoop.prototype.simulateAnimationFrame_ = function(fn) { + this.win_.setTimeout(function() { + fn(new Date().getTime()); + }, 0); +}; diff --git a/server/camlistored/ui/safe-no-wheel.svg b/server/camlistored/ui/safe-no-wheel.svg new file mode 100755 index 000000000..2eea36832 --- /dev/null +++ b/server/camlistored/ui/safe-no-wheel.svg @@ -0,0 +1,23 @@ + + + + + + + + + \ No newline at end of file diff --git a/server/camlistored/ui/safe-wheel.svg b/server/camlistored/ui/safe-wheel.svg new file mode 100755 index 000000000..883aeb2b9 --- /dev/null +++ b/server/camlistored/ui/safe-wheel.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/server/camlistored/ui/safe1.svg b/server/camlistored/ui/safe1.svg index 494b8087e..22bdd96e9 100755 --- a/server/camlistored/ui/safe1.svg +++ b/server/camlistored/ui/safe1.svg @@ -1,4 +1,4 @@ - + @@ -7,7 +7,7 @@ - + \ No newline at end of file diff --git a/server/camlistored/ui/spinner.css b/server/camlistored/ui/spinner.css new file mode 100644 index 000000000..93ab0d5a4 --- /dev/null +++ b/server/camlistored/ui/spinner.css @@ -0,0 +1,29 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +.cam-spinner { + position: relative; + background-size: 100%; +} + +.cam-spinner>div { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + background-size: 100%; +} diff --git a/server/camlistored/ui/spinner.js b/server/camlistored/ui/spinner.js new file mode 100644 index 000000000..00105c23a --- /dev/null +++ b/server/camlistored/ui/spinner.js @@ -0,0 +1,141 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +goog.provide('camlistore.Spinner'); + +goog.require('camlistore.AnimationLoop'); +goog.require('camlistore.style'); +goog.require('goog.dom'); +goog.require('goog.events.EventHandler'); +goog.require('goog.style'); +goog.require('goog.math.Coordinate'); +goog.require('goog.math.Size'); +goog.require('goog.ui.Control'); + + +/** + * An indeterminite progress meter using the safe icon. + * @param {goog.dom.DomHelper} domHelper + */ +camlistore.Spinner = function(domHelper) { + goog.base(this, null, this.dom_); + + /** + * @type {goog.dom.DomHelper} + * @private + */ + this.dom_ = domHelper; + + /** + * @type {goog.events.EventHandler} + * @private + */ + this.eh_ = new goog.events.EventHandler(this); + + /** + * @type {camlistore.AnimationLoop} + * @private + */ + this.animationLoop_ = new camlistore.AnimationLoop(this.dom_.getWindow()); + + /** + * @type {number} + * @private + */ + this.currentRotation_ = 0; +}; + +goog.inherits(camlistore.Spinner, goog.ui.Control); + +/** + * @type {string} + */ +camlistore.Spinner.prototype.backgroundImage = "safe-no-wheel.svg"; + +/** + * @type {string} + */ +camlistore.Spinner.prototype.foregroundImage = "safe-wheel.svg"; + +/** + * @type {number} + */ +camlistore.Spinner.prototype.degreesPerSecond = 500; + +/** + * The origin the safe wheel rotates around, expressed as a fraction of the + * image's width and height. + * + * @type {goog.math.Coordinate} + * @private + */ +camlistore.Spinner.prototype.wheelRotationOrigin_ = + new goog.math.Coordinate(0.37, 0.505); + +/** + * @override + */ +camlistore.Spinner.prototype.createDom = function() { + this.background_ = this.dom_.createDom('div', 'cam-spinner', + this.dom_.createDom('div')); + this.foreground_ = this.background_.firstChild; + + camlistore.style.setURLStyle(this.background_, 'background-image', + this.backgroundImage); + camlistore.style.setURLStyle(this.foreground_, 'background-image', + this.foregroundImage); + + // TODO(aa): This will need to be configurable. Not sure how makes sense yet. + var size = new goog.math.Size(75, 75); + goog.style.setSize(this.background_, size); + + // We should be able to set the origin as a percentage directly, but the + // browsers end up rounding differently, and we get less off-center spinning + // on the whole if we set this using pixels. + var origin = new goog.math.Coordinate(size.width, size.height); + camlistore.style.setTransformOrigin( + this.foreground_, + origin.scale(this.wheelRotationOrigin_.x, + this.wheelRotationOrigin_.y)); + + this.eh_.listen(this.animationLoop_, + camlistore.AnimationLoop.FRAME_EVENT_TYPE, + this.updateRotation_); + + this.decorateInternal(this.background_); +}; + +camlistore.Spinner.prototype.isRunning = function() { + return this.animationLoop_.isRunning(); +}; + +camlistore.Spinner.prototype.start = function() { + this.animationLoop_.start(); +}; + +camlistore.Spinner.prototype.stop = function() { + this.animationLoop_.stop(); +}; + +/** + * @private + */ +camlistore.Spinner.prototype.updateRotation_ = function(e) { + rotation = e.delay / 1000 * this.degreesPerSecond; + this.currentRotation_ += rotation; + this.currentRotation_ %= 360; + camlistore.style.setRotation(this.foreground_, this.currentRotation_); +}; diff --git a/server/camlistored/ui/spinner_test.html b/server/camlistored/ui/spinner_test.html new file mode 100644 index 000000000..ffe192662 --- /dev/null +++ b/server/camlistored/ui/spinner_test.html @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + diff --git a/server/camlistored/ui/style.js b/server/camlistored/ui/style.js new file mode 100644 index 000000000..f7e88b775 --- /dev/null +++ b/server/camlistored/ui/style.js @@ -0,0 +1,70 @@ +/* +Copyright 2013 The Camlistore Authors + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/** + * @fileoverview Some extra style utilties above what's included in goog.style. + */ +goog.provide('camlistore.style'); + +goog.require('goog.math.Coordinate'); +goog.require('goog.string'); +goog.require('goog.style'); + +/** + * Returns |url| wrapped in url() so that it can be used as a CSS property + * value. + * @param {string} url + * @returns {string} + */ +camlistore.style.getURLValue = function(url) { + return goog.string.subs('url(%s)', url); +}; + +/** + * Sets a style property to a URL value. + * @param {Element} elm + * @param {string} dashedCSSProperty The CSS property to set, formatted with + * dashes, in the CSS style, not camelCase. + * @param {string} url + */ +camlistore.style.setURLStyle = function(elm, dashedCSSProperty, url) { + goog.style.setStyle(elm, dashedCSSProperty, + camlistore.style.getURLValue(url)); +}; + +/** + * @param {Element} elm + * @param {goog.math.Coordinate} origin + * @param {string=} opt_unit The CSS units the origin is in. If unspecified, + * defaults to pixels. + */ +camlistore.style.setTransformOrigin = function(elm, origin, opt_unit) { + var unit = opt_unit || 'px'; + goog.style.setStyle(elm, 'transform-origin', + goog.string.subs('%s%s %s%s', origin.x, unit, origin.y, + unit)); +}; + +/** + * Note that this currently clears any previous CSS transform. Currently we only + * needs to support rotate(). + * @param {Element} elm + * @param {number} degrees + */ +camlistore.style.setRotation = function(elm, degrees) { + goog.style.setStyle(elm, 'transform', + goog.string.subs('rotate(%sdeg)', degrees)); +};